Skip to content

Commit 2530119

Browse files
committed
Honor lecture filter across the whole pipeline + add DeepSeek V4
The Lecture filter on the Pipeline page only narrowed the note-generation step. Download / transcribe / align silently processed every video, which made "regenerate one note" download and transcribe the entire course. Now the filter is forwarded all the way through: • app.js expands "1-5"/"1,3,5" into --download-video N N N for the downloader (replacing --download-video-all when set). • pipeline_worker.py gets --lectures, sorts videos by stem so the 1-based position matches note_generation's caption-sort numbering, and forwards --lectures to semantic_alignment. • semantic_alignment.process_course filters captions to those positions before running BGE-M3. Also surfaces DeepSeek V4 in the Settings → Tunable Constants dropdown (both NOTE_MODEL and VERIFY_MODEL). The provider router already routed any "deepseek*" model to api.deepseek.com; an 8k output cap was added to _MODEL_MAX_COMPLETION for the V4 / V3 / R1 model IDs. 8 new regression tests in test/test_lecture_filter.py cover the parser, the get_videos sort order, and the alignment-stage filter.
1 parent b17164c commit 2530119

6 files changed

Lines changed: 253 additions & 11 deletions

File tree

electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "auto-note",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "AutoNote — lecture notes generator from Canvas recordings",
55
"homepage": "https://github.com/nodeeeeee/Auto-Note",
66
"author": {

electron/renderer/app.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,7 +1140,11 @@ const CONSTANTS_DEF = [
11401140
['Gemini 2.5 Pro','gemini-2.5-pro'],['Gemini 2.5 Flash','gemini-2.5-flash'],
11411141
['Gemini 2.5 Flash Lite','gemini-2.5-flash-lite'],['Gemini 2.0 Flash','gemini-2.0-flash'],
11421142
// ── DeepSeek ────────────────────────────────────────────────────────
1143-
['DeepSeek V3 (chat)','deepseek-chat'],['DeepSeek R1 (reasoning)','deepseek-reasoner'],
1143+
// `deepseek-chat` is an alias that DeepSeek points at the current
1144+
// latest non-reasoning model (V4 as of 2026-04). `deepseek-v4` is the
1145+
// pinned alias if you want to lock in V4 specifically.
1146+
['DeepSeek V4 (chat)','deepseek-chat'],['DeepSeek V4 pinned','deepseek-v4'],
1147+
['DeepSeek V3','deepseek-v3'],['DeepSeek R1 (reasoning)','deepseek-reasoner'],
11441148
// ── xAI Grok ────────────────────────────────────────────────────────
11451149
['Grok 3','grok-3'],['Grok 3 mini','grok-3-mini'],
11461150
// ── Mistral ─────────────────────────────────────────────────────────
@@ -1160,7 +1164,7 @@ const CONSTANTS_DEF = [
11601164
['Gemini 2.5 Flash','gemini-2.5-flash'],['Gemini 2.5 Flash Lite','gemini-2.5-flash-lite'],
11611165
['Gemini 2.0 Flash','gemini-2.0-flash'],
11621166
// ── DeepSeek ────────────────────────────────────────────────────────
1163-
['DeepSeek V3 (chat)','deepseek-chat'],
1167+
['DeepSeek V4 (chat)','deepseek-chat'],['DeepSeek V4 pinned','deepseek-v4'],
11641168
// ── xAI / Mistral ───────────────────────────────────────────────────
11651169
['Grok 3 mini','grok-3-mini'],['Mistral Small','mistral-small-latest'],
11661170
]],
@@ -1538,13 +1542,39 @@ async function attachPageHandlers() {
15381542
const paths = await window.api.getScriptsPaths();
15391543
const chain = [];
15401544

1545+
// Expand the lecture filter ('1-5' / '1,3,5') into individual numbers
1546+
// so the downloader can pass them via --download-video N N N. The
1547+
// numbering convention here follows the post-download alphabetical
1548+
// sort used by transcribe / align / generate; passing those same
1549+
// numbers to the downloader assumes Panopto's course_num matches
1550+
// (it does in the common case when lectures upload chronologically).
1551+
const lecNums = [];
1552+
if (lf) {
1553+
for (const part of lf.split(',')) {
1554+
const t = part.trim();
1555+
if (!t) continue;
1556+
const m = t.match(/^(\d+)\s*-\s*(\d+)$/);
1557+
if (m) {
1558+
const a = parseInt(m[1], 10), b = parseInt(m[2], 10);
1559+
if (a > 0 && b >= a) for (let n = a; n <= b; n++) lecNums.push(n);
1560+
} else if (/^\d+$/.test(t)) {
1561+
lecNums.push(parseInt(t, 10));
1562+
}
1563+
}
1564+
}
1565+
15411566
if (steps.includes('dl_mat')) {
15421567
const c = [python, paths.downloader, '--course', cid, '--download-material-all', '--path', outDir];
15431568
if (stealth) c.push('--secretly');
15441569
chain.push(['Download materials', c]);
15451570
}
15461571
if (steps.includes('dl_vid')) {
1547-
const c = [python, paths.downloader, '--course', cid, '--download-video-all', '--path', outDir];
1572+
const c = [python, paths.downloader, '--course', cid, '--path', outDir];
1573+
if (lecNums.length) {
1574+
c.push('--download-video', ...lecNums.map(String));
1575+
} else {
1576+
c.push('--download-video-all');
1577+
}
15481578
if (stealth) c.push('--secretly');
15491579
chain.push(['Download videos', c]);
15501580
}
@@ -1560,6 +1590,7 @@ async function attachPageHandlers() {
15601590
if (force) c.push('--force');
15611591
if (imgSrc === 'slides') c.push('--skip-frames');
15621592
else if (imgSrc === 'frames') c.push('--force-screen');
1593+
if (lf) c.push('--lectures', lf);
15631594
chain.push(['Transcribe + Align', c]);
15641595
}
15651596
if (steps.includes('generate')) {

note_generation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,11 @@ def _translate(text: str, lang: str) -> str:
552552
"gpt-5.2": 128000,
553553
"o3": 100000,
554554
"o4-mini": 100000,
555+
# DeepSeek V4 / V3 / R1 share an 8k default output cap on the public API.
556+
"deepseek-chat": 8192,
557+
"deepseek-v4": 8192,
558+
"deepseek-v3": 8192,
559+
"deepseek-reasoner": 8192,
555560
}
556561

557562

pipeline_worker.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,14 @@ def _run(cmd: list[str], label: str) -> bool:
7676

7777

7878
def get_videos(course_id: str, base_dir: Path) -> list[dict]:
79-
"""Get list of downloaded videos for a course from manifest."""
79+
"""Get list of downloaded videos for a course from manifest.
80+
81+
Sorted by stem so the 1-based position matches the lecture numbering
82+
used by ``note_generation._discover_video_lectures`` and
83+
``semantic_alignment.process_course`` — both of which sort captions
84+
alphabetically. This makes ``--lectures`` filters consistent across
85+
transcribe / align / generate stages.
86+
"""
8087
if not MANIFEST_FILE.exists():
8188
return []
8289
with open(MANIFEST_FILE, encoding="utf-8") as f:
@@ -100,9 +107,32 @@ def get_videos(course_id: str, base_dir: Path) -> list[dict]:
100107
"stream_tag": entry.get("stream_tag", ""),
101108
"course_dir": base_dir / course_id,
102109
})
110+
videos.sort(key=lambda v: v["stem"])
103111
return videos
104112

105113

114+
def _parse_lecture_filter(spec: str) -> set[int]:
115+
"""Parse '1-5' or '1,3,5' or '1-3,7' into a set of 1-based lecture nums.
116+
Returns an empty set when spec is empty/blank — caller treats that as
117+
'no filter'."""
118+
sel: set[int] = set()
119+
if not spec:
120+
return sel
121+
for part in spec.split(","):
122+
part = part.strip()
123+
if not part:
124+
continue
125+
if "-" in part:
126+
try:
127+
a, b = part.split("-", 1)
128+
sel.update(range(int(a), int(b) + 1))
129+
except ValueError:
130+
continue
131+
elif part.isdigit():
132+
sel.add(int(part))
133+
return sel
134+
135+
106136
def transcribe_one(video: dict, force: bool) -> bool:
107137
"""Transcribe a single video."""
108138
caption = video["course_dir"] / "captions" / f"{video['stem']}.json"
@@ -183,7 +213,8 @@ def align_one(video: dict, force: bool) -> bool:
183213

184214
def pipeline_sequential(videos: list[dict], force: bool,
185215
skip_frames: bool = False,
186-
force_screen: bool = False) -> None:
216+
force_screen: bool = False,
217+
lectures: str = "") -> None:
187218
"""Simple sequential pipeline: transcribe → frames → align per video."""
188219
for i, video in enumerate(videos, 1):
189220
print(f"\n{'═' * 60}")
@@ -201,12 +232,15 @@ def pipeline_sequential(videos: list[dict], force: bool,
201232
"--course", course_dir.name]
202233
if force:
203234
cmd.append("--force")
235+
if lectures:
236+
cmd.extend(["--lectures", lectures])
204237
_run(cmd, "Align all transcripts")
205238

206239

207240
def pipeline_threaded(videos: list[dict], force: bool,
208241
skip_frames: bool = False,
209-
force_screen: bool = False) -> None:
242+
force_screen: bool = False,
243+
lectures: str = "") -> None:
210244
"""Threaded pipeline: transcribe and frame-extract/align overlap.
211245
212246
Thread 1 (transcriber): transcribes videos one at a time, pushes
@@ -256,6 +290,8 @@ def frame_processor():
256290
"--course", course_dir.name]
257291
if force:
258292
cmd.append("--force")
293+
if lectures:
294+
cmd.extend(["--lectures", lectures])
259295
_run(cmd, "Align all transcripts")
260296

261297
if errors:
@@ -285,6 +321,11 @@ def main() -> None:
285321
"screenshots so camera-style recordings still "
286322
"produce images instead of falling through to "
287323
"missing slide PDFs.")
324+
parser.add_argument("--lectures", metavar="N-N or N,N,N", default="",
325+
help="Filter to specific lectures, e.g. '1-5' or "
326+
"'1,3,5'. Numbers are 1-based positions over "
327+
"videos sorted alphabetically by filename — the "
328+
"same numbering note_generation uses.")
288329
args = parser.parse_args()
289330

290331
base_dir = Path(args.path) if args.path else COURSE_DATA_DIR
@@ -295,14 +336,27 @@ def main() -> None:
295336
print("Run 'Download videos' first.")
296337
return
297338

339+
sel = _parse_lecture_filter(args.lectures)
340+
if sel:
341+
before = len(videos)
342+
videos = [v for i, v in enumerate(videos, start=1) if i in sel]
343+
if not videos:
344+
print(f"[error] Lecture filter '{args.lectures}' selected no "
345+
f"videos (out of {before} available). Aborting.")
346+
return
347+
print(f"Lecture filter '{args.lectures}': "
348+
f"{len(videos)}/{before} video(s) selected.")
349+
298350
print(f"Found {len(videos)} video(s) for course {args.course}.")
299351

300352
if args.sequential:
301353
pipeline_sequential(videos, args.force, skip_frames=args.skip_frames,
302-
force_screen=args.force_screen)
354+
force_screen=args.force_screen,
355+
lectures=args.lectures)
303356
else:
304357
pipeline_threaded(videos, args.force, skip_frames=args.skip_frames,
305-
force_screen=args.force_screen)
358+
force_screen=args.force_screen,
359+
lectures=args.lectures)
306360

307361
print(f"\n✓ Pipeline complete: {len(videos)} video(s) processed.")
308362

semantic_alignment.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,13 +1786,19 @@ def _load_mapping(mapping_path: Path, course_dir: Path) -> dict[str, list[Path]]
17861786

17871787
def process_course(course_id: int | str, use_jina: bool = False,
17881788
mapping_path: Path | None = None,
1789-
force: bool = False) -> None:
1789+
force: bool = False,
1790+
lectures: set[int] | None = None) -> None:
17901791
course_dir = COURSE_DATA_DIR / str(course_id)
17911792
captions_dir = course_dir / "captions"
17921793
print(f"Course dir : {course_dir}", flush=True)
17931794
print(f"Captions : {captions_dir}", flush=True)
17941795

17951796
captions = sorted(captions_dir.glob("*.json")) if captions_dir.exists() else []
1797+
if lectures:
1798+
before = len(captions)
1799+
captions = [c for i, c in enumerate(captions, start=1) if i in lectures]
1800+
print(f"Lecture filter: {len(captions)}/{before} caption(s) selected.",
1801+
flush=True)
17961802
all_slides = _candidate_slides(course_dir)
17971803
out_dir = course_dir / "alignment"
17981804

@@ -2025,8 +2031,28 @@ def main() -> None:
20252031
"(default: bge-m3, alternatives: jina, google, mpnet)")
20262032
parser.add_argument("--force", action="store_true",
20272033
help="Re-align all captions even if alignment files already exist")
2034+
parser.add_argument("--lectures", metavar="N-N or N,N,N", default="",
2035+
help="Filter to specific lectures (1-based positions "
2036+
"over alphabetically sorted captions), e.g. "
2037+
"'1-5' or '1,3,5'.")
20282038
args = parser.parse_args()
20292039

2040+
def _parse_lec(spec: str) -> set[int]:
2041+
out: set[int] = set()
2042+
for part in (spec or "").split(","):
2043+
part = part.strip()
2044+
if not part:
2045+
continue
2046+
if "-" in part:
2047+
try:
2048+
a, b = part.split("-", 1)
2049+
out.update(range(int(a), int(b) + 1))
2050+
except ValueError:
2051+
continue
2052+
elif part.isdigit():
2053+
out.add(int(part))
2054+
return out
2055+
20302056
if args.suggest_matches:
20312057
if not args.course:
20322058
parser.error("--suggest-matches requires --course ID")
@@ -2049,8 +2075,9 @@ def main() -> None:
20492075

20502076
if args.course:
20512077
mapping = Path(args.mapping) if args.mapping else None
2078+
lec_filter = _parse_lec(args.lectures) or None
20522079
process_course(args.course, use_jina=args.jina, mapping_path=mapping,
2053-
force=args.force)
2080+
force=args.force, lectures=lec_filter)
20542081
return
20552082

20562083
if not args.caption or not args.slides:

0 commit comments

Comments
 (0)