Skip to content

Commit 275ff4e

Browse files
committed
Fix alignment on Windows; auto-skip low-quality transcripts
Alignment Windows fixes: - config.json read now uses encoding='utf-8' explicitly (avoids cp1252 crash) - show_progress_bar=False in encode() avoids tqdm pipe-mode issues on Windows - captions_dir.exists() guard before Path.glob() (Python 3.12 compat) - get_embedder() wraps model load in try/except with helpful error message - process_course() prints course/captions dir at startup for easier diagnosis - All key print() calls have flush=True Auto-detect wrong recordings: - After transcription, compute words-per-minute; if < 50 words total or < 10 wpm → write quality='low' into caption JSON - process_course() reads quality flag and skips low-quality captions with a clear [skip] message, preventing wasted embedding + alignment work - Both local (faster-whisper) and API backends flag low-quality output
1 parent 2fd2d52 commit 275ff4e

3 files changed

Lines changed: 65 additions & 15 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": "0.8.6",
3+
"version": "0.8.7",
44
"description": "AutoNote — lecture notes generator from Canvas recordings",
55
"main": "main.js",
66
"scripts": {

extract_caption.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,12 +352,23 @@ def transcribe_api(video_path: Path, caption_path: Path) -> bool:
352352
"segments": all_segments,
353353
}
354354

355+
n_seg = len(all_segments)
356+
n_word = sum(len(s["words"]) for s in all_segments)
357+
358+
# Quality check: flag likely wrong/empty recordings so alignment skips them
359+
dur = total_dur or 1.0
360+
wpm = (n_word / dur) * 60
361+
if n_word < 50 or wpm < 10:
362+
result["quality"] = "low"
363+
print(f" [warn] Very sparse transcript ({n_word} words, {wpm:.0f} wpm) — "
364+
f"flagged as low quality, alignment will be skipped.")
365+
else:
366+
result["quality"] = "ok"
367+
355368
caption_path.parent.mkdir(parents=True, exist_ok=True)
356369
with open(caption_path, "w", encoding="utf-8") as f:
357370
json.dump(result, f, ensure_ascii=False, indent=2)
358371

359-
n_seg = len(result["segments"])
360-
n_word = sum(len(s["words"]) for s in result["segments"])
361372
drop_note = f" ({total_dropped} hallucinated segments removed)" if total_dropped else ""
362373
print(f" Saved: {n_seg} segments / {n_word} words -> {caption_path}{drop_note}")
363374
return True
@@ -452,6 +463,20 @@ def transcribe_local(video_path: Path, caption_path: Path) -> bool:
452463

453464
n_seg = len(result["segments"])
454465
n_word = sum(len(s["words"]) for s in result["segments"])
466+
467+
# Quality check: flag likely wrong/empty recordings so alignment skips them
468+
dur = result["duration"] or 1.0
469+
wpm = (n_word / dur) * 60
470+
if n_word < 50 or wpm < 10:
471+
result["quality"] = "low"
472+
print(f" [warn] Very sparse transcript ({n_word} words, {wpm:.0f} wpm) — "
473+
f"flagged as low quality, alignment will be skipped.")
474+
else:
475+
result["quality"] = "ok"
476+
477+
with open(caption_path, "w", encoding="utf-8") as f:
478+
json.dump(result, f, ensure_ascii=False, indent=2)
479+
455480
print(f" Saved: {n_seg} segments / {n_word} words -> {caption_path}")
456481
return True
457482

semantic_alignment.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@
5656

5757
# Course output directory: defaults to DATA_DIR but can be overridden by
5858
# OUTPUT_DIR in config.json so files land in the user's chosen Output Dir.
59-
_sa_config: dict = (
60-
json.loads((DATA_DIR / "config.json").read_text())
61-
if (DATA_DIR / "config.json").exists() else {}
62-
)
59+
try:
60+
_cfg_file = DATA_DIR / "config.json"
61+
_sa_config: dict = json.loads(_cfg_file.read_text(encoding="utf-8")) if _cfg_file.exists() else {}
62+
except Exception:
63+
_sa_config = {}
6364
_out_dir = _sa_config.get("OUTPUT_DIR", "").strip()
6465
COURSE_DATA_DIR = Path(_out_dir) if _out_dir else DATA_DIR
6566

@@ -393,21 +394,27 @@ def get_embedder():
393394
from sentence_transformers import SentenceTransformer
394395
import torch
395396
device = "cuda" if torch.cuda.is_available() else "cpu"
396-
print(f" [embed] Loading {EMBED_MODEL} on {device} ...")
397-
model = SentenceTransformer(EMBED_MODEL, device=device)
397+
print(f" [embed] Loading {EMBED_MODEL} on {device} ...", flush=True)
398+
try:
399+
model = SentenceTransformer(EMBED_MODEL, device=device)
400+
except Exception as e:
401+
print(f" [embed] Failed to load model: {e}", flush=True)
402+
raise
403+
print(f" [embed] Model loaded.", flush=True)
398404
return model
399405

400406

401407
def embed_texts(model, texts: list[str], desc: str = " embedding") -> np.ndarray:
402408
"""Return L2-normalised float32 embeddings, shape (N, D)."""
403-
print(f"{desc} ({len(texts)} texts)", flush=True)
409+
print(f"{desc} ({len(texts)} texts)...", flush=True)
404410
vecs = model.encode(
405411
texts,
406412
batch_size=BATCH_SIZE,
407-
show_progress_bar=True,
413+
show_progress_bar=False, # avoid tqdm issues in Windows pipe-mode subprocesses
408414
normalize_embeddings=True, # cosine sim → inner product on unit sphere
409415
convert_to_numpy=True,
410416
)
417+
print(f"{desc} done.", flush=True)
411418
return vecs.astype(np.float32)
412419

413420

@@ -1106,18 +1113,26 @@ def _content_match_slide_group(
11061113

11071114

11081115
def process_course(course_id: int | str) -> None:
1109-
course_dir = COURSE_DATA_DIR / str(course_id)
1110-
captions = sorted((course_dir / "captions").glob("*.json"))
1116+
course_dir = COURSE_DATA_DIR / str(course_id)
1117+
captions_dir = course_dir / "captions"
1118+
print(f"Course dir : {course_dir}", flush=True)
1119+
print(f"Captions : {captions_dir}", flush=True)
1120+
1121+
captions = sorted(captions_dir.glob("*.json")) if captions_dir.exists() else []
11111122
all_slides = _candidate_slides(course_dir)
11121123
out_dir = course_dir / "alignment"
11131124

11141125
if not captions:
1115-
print(f"No captions found in {course_dir}/captions/")
1126+
print(f"[warn] No captions found in {captions_dir}")
1127+
print(" Run 'Transcribe' first before aligning.")
11161128
return
11171129
if not all_slides:
1118-
print(f"No slide files found under {course_dir}/materials/")
1130+
print(f"[warn] No slide files found under {course_dir / 'materials'}")
1131+
print(" Run 'Download materials' first.")
11191132
return
11201133

1134+
print(f"Found {len(captions)} caption(s), {len(all_slides)} slide file(s).", flush=True)
1135+
11211136
# Group slide files by lecture number
11221137
slides_by_num: dict[int, list[Path]] = defaultdict(list)
11231138
for sp in all_slides:
@@ -1128,6 +1143,16 @@ def process_course(course_id: int | str) -> None:
11281143
embedder = get_embedder()
11291144

11301145
for cap in captions:
1146+
# Skip captions flagged as low-quality (wrong/empty recordings)
1147+
try:
1148+
with open(cap, encoding="utf-8") as _f:
1149+
_meta = json.load(_f)
1150+
if _meta.get("quality") == "low":
1151+
print(f" [skip] Low-quality transcript (wrong/empty recording): {cap.name}")
1152+
continue
1153+
except Exception:
1154+
pass # can't read → proceed and let align() handle it
1155+
11311156
slide_group = _find_best_slide_group(cap, slides_by_num, all_slides)
11321157
if not slide_group:
11331158
slide_group = _content_match_slide_group(

0 commit comments

Comments
 (0)