Skip to content

Commit b203ac8

Browse files
committed
Merge branch 'ci/lint' into 'main'
ci: add ci and lint See merge request nwpie/vibe/ai-claude-loop!5
2 parents 21aacfd + 610d101 commit b203ac8

File tree

10 files changed

+159
-38
lines changed

10 files changed

+159
-38
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ B2_APP_KEY=
1717
B2_BUCKET_NAME=
1818

1919
# Whishper from HugginfFace inference API
20-
HF_TOKEN=
20+
HF_TOKEN=

.github/workflows/lint.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Lint
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: '3.12'
18+
19+
- uses: pre-commit/action@v3.0.1

.gitlab-ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
stages:
2+
- lint
3+
4+
lint:
5+
stage: lint
6+
image: python:3.12-slim
7+
before_script:
8+
- apt-get update -qq && apt-get install -y -qq git >/dev/null
9+
- pip install --quiet pre-commit
10+
script:
11+
- pre-commit run --all-files
12+
cache:
13+
key: pre-commit
14+
paths:
15+
- ~/.cache/pre-commit

.pre-commit-config.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# See https://pre-commit.com for more information
2+
repos:
3+
- repo: https://github.com/pre-commit/pre-commit-hooks
4+
rev: v5.0.0
5+
hooks:
6+
- id: trailing-whitespace
7+
- id: end-of-file-fixer
8+
- id: check-yaml
9+
- id: check-json
10+
- id: check-added-large-files
11+
args: ['--maxkb=10000']
12+
- id: check-merge-conflict
13+
- id: detect-private-key
14+
15+
- repo: https://github.com/astral-sh/ruff-pre-commit
16+
rev: v0.9.10
17+
hooks:
18+
- id: ruff
19+
args: ['--fix']
20+
- id: ruff-format
21+
22+
- repo: https://github.com/shellcheck-py/shellcheck-py
23+
rev: v0.10.0.1
24+
hooks:
25+
- id: shellcheck

docs/example.ingest/digest-yt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515

1616

1717

18-
Compiled from @AIDailyBrief by Claude Code :zap:
18+
Compiled from @AIDailyBrief by Claude Code :zap:

scripts/slack_notify.sh

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ slack_send() {
103103
local payload resp ok
104104
payload=$(printf '{"channel":"%s","text":"%s"}' \
105105
"$SLACK_CHANNEL_ID" \
106-
"$(echo "$msg" | sed 's/"/\\"/g')")
106+
"${msg//\"/\\\"}")
107107

108108
resp=$(curl -s -X POST \
109109
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
@@ -122,7 +122,7 @@ slack_send() {
122122

123123
webhook)
124124
local payload
125-
payload=$(printf '{"text":"%s"}' "$(echo "$msg" | sed 's/"/\\"/g')")
125+
payload=$(printf '{"text":"%s"}' "${msg//\"/\\\"}")
126126
curl -s -X POST \
127127
-H 'Content-type: application/json' \
128128
-d "$payload" \
@@ -144,19 +144,21 @@ slack_send() {
144144
# Returns: 0 on success (sets _SLACK_LAST_THREAD_TS), 1 on failure
145145
slack_ask() {
146146
local question="${1:?Usage: slack_ask \"question\"}"
147-
local req_id="REQ-$(date +%s)"
147+
local req_id
148+
req_id="REQ-$(date +%s)"
148149
local project_name
149150
project_name=$(basename "$(pwd)")
150151

151152
case "${_SLACK_MODE:-}" in
152153
bot)
153-
local msg payload resp ok ts
154+
local msg payload resp ok
155+
# shellcheck disable=SC2016 # %s placeholders are printf args, not shell expansions
154156
msg=$(printf ':question: *[%s] Approval Request*\n`[%s]`\n\n%s\n\n_Reply in this thread to respond._' \
155157
"$project_name" "$req_id" "$question")
156158

157159
payload=$(printf '{"channel":"%s","text":"%s","unfurl_links":false}' \
158160
"$SLACK_CHANNEL_ID" \
159-
"$(echo "$msg" | sed 's/"/\\"/g')")
161+
"${msg//\"/\\\"}")
160162

161163
resp=$(curl -s -X POST \
162164
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
@@ -180,7 +182,7 @@ slack_ask() {
180182
local msg payload
181183
msg=$(printf '[%s] Approval Request [%s]: %s (webhook mode — reply not monitored)' \
182184
"$project_name" "$req_id" "$question")
183-
payload=$(printf '{"text":"%s"}' "$(echo "$msg" | sed 's/"/\\"/g')")
185+
payload=$(printf '{"text":"%s"}' "${msg//\"/\\\"}")
184186
curl -s -X POST \
185187
-H 'Content-type: application/json' \
186188
-d "$payload" \
@@ -224,7 +226,7 @@ slack_wait_reply() {
224226
echo "[slack] Waiting for reply (timeout: ${timeout}s, polling every ${interval}s)..." >&2
225227

226228
while (( elapsed < timeout )); do
227-
local resp replies reply_text reply_user
229+
local resp reply_text
228230
resp=$(curl -s -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
229231
"https://slack.com/api/conversations.replies?channel=${SLACK_CHANNEL_ID}&ts=${_SLACK_LAST_THREAD_TS}" \
230232
2>/dev/null)

scripts/yt/build_html.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def build_html(md_path: Path, title: str = "", lang: str = "en") -> str:
103103
if not title:
104104
first_line = md_text.split("\n")[0]
105105
import re
106+
106107
title = re.sub(r"^#+\s*", "", first_line).strip() or md_path.stem
107108

108109
lang_attr = "zh-Hant" if "zh" in lang else "en"

scripts/yt/fetch_recent_videos.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ def fetch_channel_videos(channel_url: str, max_items: int = 10) -> list[dict]:
2626
"""
2727
cmd = [
2828
"yt-dlp",
29-
"--playlist-items", f"1:{max_items}",
30-
"--print", "%(id)s\t%(title)s\t%(upload_date)s\t%(modified_date)s\t%(thumbnail)s",
29+
"--playlist-items",
30+
f"1:{max_items}",
31+
"--print",
32+
"%(id)s\t%(title)s\t%(upload_date)s\t%(modified_date)s\t%(thumbnail)s",
3133
"--skip-download",
3234
f"{channel_url}/videos",
3335
]
@@ -51,8 +53,9 @@ def fetch_channel_videos(channel_url: str, max_items: int = 10) -> list[dict]:
5153
"title": parts[1],
5254
"upload_date": upload,
5355
"modified_date": modified,
54-
"thumbnail": parts[4] if len(parts) >= 5 and parts[4] != "NA"
55-
else f"https://i.ytimg.com/vi/{vid_id}/hqdefault.jpg",
56+
"thumbnail": parts[4]
57+
if len(parts) >= 5 and parts[4] != "NA"
58+
else f"https://i.ytimg.com/vi/{vid_id}/hqdefault.jpg",
5659
}
5760
videos.append(vid)
5861
return videos

scripts/yt/get_transcript.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# Try to load .env
2222
try:
2323
from dotenv import load_dotenv
24+
2425
load_dotenv()
2526
except ImportError:
2627
pass
@@ -34,23 +35,41 @@ def download_subtitles(video_url: str, out_dir: Path, lang: str = "en") -> Path
3435
# Try manual subs first
3536
subprocess.run(
3637
[
37-
"yt-dlp", "--write-subs", "--sub-lang", lang,
38-
"--sub-format", "srt", "--skip-download",
39-
"-o", str(out_dir / "video"), video_url,
38+
"yt-dlp",
39+
"--write-subs",
40+
"--sub-lang",
41+
lang,
42+
"--sub-format",
43+
"srt",
44+
"--skip-download",
45+
"-o",
46+
str(out_dir / "video"),
47+
video_url,
4048
],
41-
capture_output=True, text=True, timeout=60,
49+
capture_output=True,
50+
text=True,
51+
timeout=60,
4252
)
4353
if srt_path.exists() and srt_path.stat().st_size > 0:
4454
return srt_path
4555

4656
# Try auto-generated subs
4757
subprocess.run(
4858
[
49-
"yt-dlp", "--write-auto-subs", "--sub-lang", lang,
50-
"--sub-format", "srt", "--skip-download",
51-
"-o", str(out_dir / "video"), video_url,
59+
"yt-dlp",
60+
"--write-auto-subs",
61+
"--sub-lang",
62+
lang,
63+
"--sub-format",
64+
"srt",
65+
"--skip-download",
66+
"-o",
67+
str(out_dir / "video"),
68+
video_url,
5269
],
53-
capture_output=True, text=True, timeout=60,
70+
capture_output=True,
71+
text=True,
72+
timeout=60,
5473
)
5574
if srt_path.exists() and srt_path.stat().st_size > 0:
5675
return srt_path
@@ -85,11 +104,19 @@ def whisper_transcribe_hf(video_url: str, out_dir: Path) -> str | None:
85104
audio_path = out_dir / "audio.m4a"
86105
subprocess.run(
87106
[
88-
"yt-dlp", "-f", "worstaudio[ext=m4a]/worstaudio",
89-
"--extract-audio", "--audio-format", "m4a",
90-
"-o", str(audio_path), video_url,
107+
"yt-dlp",
108+
"-f",
109+
"worstaudio[ext=m4a]/worstaudio",
110+
"--extract-audio",
111+
"--audio-format",
112+
"m4a",
113+
"-o",
114+
str(audio_path),
115+
video_url,
91116
],
92-
capture_output=True, text=True, timeout=300,
117+
capture_output=True,
118+
text=True,
119+
timeout=300,
93120
)
94121
if not audio_path.exists():
95122
# Check for alternative extensions
@@ -103,15 +130,28 @@ def whisper_transcribe_hf(video_url: str, out_dir: Path) -> str | None:
103130
# Convert to wav for HF API
104131
wav_path = out_dir / "audio.wav"
105132
subprocess.run(
106-
["ffmpeg", "-i", str(audio_path), "-ar", "16000", "-ac", "1", "-y", str(wav_path)],
107-
capture_output=True, text=True, timeout=300,
133+
[
134+
"ffmpeg",
135+
"-i",
136+
str(audio_path),
137+
"-ar",
138+
"16000",
139+
"-ac",
140+
"1",
141+
"-y",
142+
str(wav_path),
143+
],
144+
capture_output=True,
145+
text=True,
146+
timeout=300,
108147
)
109148
if not wav_path.exists():
110149
print("FFmpeg conversion failed", file=sys.stderr)
111150
return None
112151

113152
try:
114153
from huggingface_hub import InferenceClient
154+
115155
client = InferenceClient(token=hf_token)
116156
result = client.automatic_speech_recognition(
117157
str(wav_path),

scripts/yt/upload_b2.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# Try to load .env
2121
try:
2222
from dotenv import load_dotenv
23+
2324
load_dotenv()
2425
except ImportError:
2526
pass
@@ -32,7 +33,9 @@ def _b2_cmd() -> list[str]:
3233
# b2 v1.3.8 installed in system python without a CLI entrypoint
3334
for py in ["/usr/bin/python3", "python3", "python"]:
3435
if shutil.which(py):
35-
r = subprocess.run([py, "-m", "b2", "version"], capture_output=True, text=True)
36+
r = subprocess.run(
37+
[py, "-m", "b2", "version"], capture_output=True, text=True
38+
)
3639
if r.returncode == 0:
3740
return [py, "-m", "b2"]
3841
return ["b2"] # let it fail with a clear error
@@ -48,7 +51,9 @@ def authorize_b2() -> bool:
4851

4952
result = subprocess.run(
5053
[*_b2_cmd(), "authorize-account", key_id, app_key],
51-
capture_output=True, text=True, timeout=30,
54+
capture_output=True,
55+
text=True,
56+
timeout=30,
5257
)
5358
if result.returncode != 0:
5459
print(f"b2 authorize-account failed: {result.stderr[:300]}", file=sys.stderr)
@@ -65,7 +70,9 @@ def upload_file(local_path: Path, b2_path: str, duration: int = 604800) -> str |
6570

6671
result = subprocess.run(
6772
[*_b2_cmd(), "upload-file", bucket, str(local_path), b2_path],
68-
capture_output=True, text=True, timeout=120,
73+
capture_output=True,
74+
text=True,
75+
timeout=120,
6976
)
7077
if result.returncode != 0:
7178
print(f"b2 upload-file failed: {result.stderr[:300]}", file=sys.stderr)
@@ -75,17 +82,26 @@ def upload_file(local_path: Path, b2_path: str, duration: int = 604800) -> str |
7582
return _get_presigned_url(bucket, b2_path, duration)
7683

7784

78-
def _get_presigned_url(
79-
bucket: str, b2_path: str, duration: int = 604800
80-
) -> str | None:
85+
def _get_presigned_url(bucket: str, b2_path: str, duration: int = 604800) -> str | None:
8186
"""Get a presigned download URL. Default duration: 7 days (604800s)."""
8287
result = subprocess.run(
83-
[*_b2_cmd(), "get-download-url-with-auth", "--duration", str(duration),
84-
bucket, b2_path],
85-
capture_output=True, text=True, timeout=30,
88+
[
89+
*_b2_cmd(),
90+
"get-download-url-with-auth",
91+
"--duration",
92+
str(duration),
93+
bucket,
94+
b2_path,
95+
],
96+
capture_output=True,
97+
text=True,
98+
timeout=30,
8699
)
87100
if result.returncode != 0:
88-
print(f"b2 get-download-url-with-auth failed: {result.stderr[:300]}", file=sys.stderr)
101+
print(
102+
f"b2 get-download-url-with-auth failed: {result.stderr[:300]}",
103+
file=sys.stderr,
104+
)
89105
# Fallback to direct URL (will 401 on private buckets)
90106
return f"https://f005.backblazeb2.com/file/{bucket}/{b2_path}"
91107

@@ -115,7 +131,7 @@ def main():
115131

116132
b2_path = f"{args.prefix}/{local_path.name}"
117133

118-
print(f"Authorizing B2...", file=sys.stderr)
134+
print("Authorizing B2...", file=sys.stderr)
119135
if not authorize_b2():
120136
sys.exit(1)
121137

0 commit comments

Comments
 (0)