Skip to content

Commit 4dce332

Browse files
nodeeeeeeclaude
andcommitted
Force regenerate applies to all selected pipeline steps
- Add --force flag to extract_caption.py: re-transcribe all videos even if caption files already exist - Add --force flag to semantic_alignment.py: re-align all captions even if alignment files already exist - Pipeline page now passes --force to transcribe and align steps (not just generate) when "Force regenerate" is checked - Selecting only "Generate notes" + force → only regenerates notes - Selecting "Transcribe" + "Align" + force → re-runs both stages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bb81889 commit 4dce332

3 files changed

Lines changed: 42 additions & 19 deletions

File tree

electron/renderer/app.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,10 +1470,14 @@ async function attachPageHandlers() {
14701470
chain.push(['Download videos', c]);
14711471
}
14721472
if (steps.includes('transcribe')) {
1473-
chain.push(['Transcribe', [python, paths.transcribe]]);
1473+
const c = [python, paths.transcribe];
1474+
if (force) c.push('--force');
1475+
chain.push(['Transcribe', c]);
14741476
}
14751477
if (steps.includes('align')) {
1476-
chain.push(['Align', [python, paths.align, '--course', cid]]);
1478+
const c = [python, paths.align, '--course', cid];
1479+
if (force) c.push('--force');
1480+
chain.push(['Align', c]);
14771481
}
14781482
if (steps.includes('generate')) {
14791483
const c = [python, paths.generate, '--course', cid, '--course-name', name || courseNameFromId(cid), '--detail', detail, '--per-video'];

extract_caption.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
# "api" → always use OpenAI Whisper API
4646
WHISPER_BACKEND = os.environ.get("AUTONOTE_WHISPER_BACKEND", "auto")
4747
VRAM_THRESHOLD_MIB = 15 * 1024 # ~15 GB nominal — accepts 16 GB cards (which report ~15.4 GB)
48+
FORCE_REGEN = False # set via --force CLI flag
4849

4950
WHISPER_MODEL_SIZE = "large-v3"
5051
WHISPER_BEAM_SIZE = 5
@@ -232,7 +233,7 @@ def transcribe_api(video_path: Path, caption_path: Path) -> bool:
232233
"""
233234
import time as _time
234235

235-
if caption_path.exists():
236+
if not FORCE_REGEN and caption_path.exists():
236237
print(f" [skip] Caption already exists: {caption_path.name}")
237238
return True
238239

@@ -417,7 +418,7 @@ def transcribe_local(video_path: Path, caption_path: Path) -> bool:
417418
Transcribe a video file directly with faster-whisper large-v3 on GPU.
418419
faster-whisper uses ffmpeg internally to decode audio from the video.
419420
"""
420-
if caption_path.exists():
421+
if not FORCE_REGEN and caption_path.exists():
421422
print(f" [skip] Caption already exists: {caption_path.name}")
422423
return True
423424

@@ -580,7 +581,7 @@ def process_video(video_path: Path, manifest: dict, manifest_key: str | None) ->
580581
course_dir = video_path.parent.parent # [project]/[course_id]/
581582
caption_path = course_dir / "captions" / f"{video_path.stem}.json"
582583

583-
if caption_path.exists():
584+
if not FORCE_REGEN and caption_path.exists():
584585
print(f" [skip] Already captioned: {video_path.name}")
585586
if manifest_key and manifest_key in manifest:
586587
manifest[manifest_key]["caption"] = str(caption_path)
@@ -601,17 +602,24 @@ def process_video(video_path: Path, manifest: dict, manifest_key: str | None) ->
601602
# ── Entry point ───────────────────────────────────────────────────────────────
602603

603604
def get_pending(manifest: dict) -> list[tuple[str, str]]:
604-
"""Return (key, video_path) for downloaded videos not yet captioned."""
605+
"""Return (key, video_path) for downloaded videos not yet captioned.
606+
607+
When FORCE_REGEN is True, returns ALL downloaded videos regardless of
608+
whether captions already exist.
609+
"""
605610
pending = []
606611
for key, entry in manifest.items():
607612
if entry.get("status") != "done":
608613
continue
609614
vpath = entry.get("path")
610615
if not vpath or not Path(vpath).exists():
611616
continue
612-
caption = Path(vpath).parent.parent / "captions" / f"{Path(vpath).stem}.json"
613-
if not caption.exists():
617+
if FORCE_REGEN:
614618
pending.append((key, vpath))
619+
else:
620+
caption = Path(vpath).parent.parent / "captions" / f"{Path(vpath).stem}.json"
621+
if not caption.exists():
622+
pending.append((key, vpath))
615623
return pending
616624

617625

@@ -620,8 +628,14 @@ def main() -> None:
620628
parser = argparse.ArgumentParser(description="Extract captions from Canvas lecture videos")
621629
parser.add_argument("--video", metavar="PATH",
622630
help="Process a single video file (ignores manifest)")
631+
parser.add_argument("--force", action="store_true",
632+
help="Re-transcribe all videos even if captions already exist")
623633
args = parser.parse_args()
624634

635+
global FORCE_REGEN
636+
if args.force:
637+
FORCE_REGEN = True
638+
625639
manifest = load_manifest()
626640

627641
if args.video:

semantic_alignment.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,7 +1578,8 @@ def _load_mapping(mapping_path: Path, course_dir: Path) -> dict[str, list[Path]]
15781578

15791579

15801580
def process_course(course_id: int | str, use_jina: bool = False,
1581-
mapping_path: Path | None = None) -> None:
1581+
mapping_path: Path | None = None,
1582+
force: bool = False) -> None:
15821583
course_dir = COURSE_DATA_DIR / str(course_id)
15831584
captions_dir = course_dir / "captions"
15841585
print(f"Course dir : {course_dir}", flush=True)
@@ -1677,15 +1678,16 @@ def process_course(course_id: int | str, use_jina: bool = False,
16771678
continue
16781679

16791680
# Check if all output files for this group already exist
1680-
if len(slide_group) == 1:
1681-
out_file = out_dir / f"{cap.stem}.json" # legacy single-file naming
1682-
if out_file.exists():
1683-
print(f" [skip] Already aligned: {cap.stem}")
1684-
continue
1685-
else:
1686-
if all((out_dir / f"{sp.stem}.json").exists() for sp in slide_group):
1687-
print(f" [skip] Already aligned: {[sp.name for sp in slide_group]}")
1688-
continue
1681+
if not force:
1682+
if len(slide_group) == 1:
1683+
out_file = out_dir / f"{cap.stem}.json" # legacy single-file naming
1684+
if out_file.exists():
1685+
print(f" [skip] Already aligned: {cap.stem}")
1686+
continue
1687+
else:
1688+
if all((out_dir / f"{sp.stem}.json").exists() for sp in slide_group):
1689+
print(f" [skip] Already aligned: {[sp.name for sp in slide_group]}")
1690+
continue
16891691

16901692
# Use Jina multimodal for single PDF slides if available
16911693
if use_jina and len(slide_group) == 1 and slide_group[0].suffix.lower() == ".pdf":
@@ -1726,6 +1728,8 @@ def main() -> None:
17261728
choices=["bge-m3", "jina", "mpnet"],
17271729
help="Embedding model for --suggest-matches "
17281730
"(default: bge-m3, alternatives: jina, mpnet)")
1731+
parser.add_argument("--force", action="store_true",
1732+
help="Re-align all captions even if alignment files already exist")
17291733
args = parser.parse_args()
17301734

17311735
if args.suggest_matches:
@@ -1750,7 +1754,8 @@ def main() -> None:
17501754

17511755
if args.course:
17521756
mapping = Path(args.mapping) if args.mapping else None
1753-
process_course(args.course, use_jina=args.jina, mapping_path=mapping)
1757+
process_course(args.course, use_jina=args.jina, mapping_path=mapping,
1758+
force=args.force)
17541759
return
17551760

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

0 commit comments

Comments
 (0)