Repository navigation
Releases: drpwchen/lecture-to-notes
Release list
v0.8.0
v0.8.0: the synced HTML viewer treats every note as data (issue #1 point 6): allowlist sanitizer, script-safe timeline JSON, hash-pinned CSP with no network, relative-only media URLs. route_inputs --json gains a shell-free argv plan (point 4). pyproject.toml and three-OS test CI (point 7). transcript.json contract doc and SECURITY.md. See CHANGELOG.
v0.7.1
Fixed
- A GPU that is too small no longer reads as "GPU busy". A user with a 2 GB
laptop card reported the pipeline "stuck":gpu_check.pycompared free VRAM
against--min-free-mb 6000, saidblocked — wait for other tasks, and the
skill instructions say not to retry. Nothing was going to free up — the card
cannot hold alarge-v3-class model at all. The pre-flight now checks total
VRAM first and, when the card can never pass, printsGPU_TOO_SMALLwith the
three real options (CPU path,--model small,--engine groq) and exits0,
the same verdict as "no NVIDIA card".--require-gpukeeps the hard block for
batch hosts that must not silently run on CPU. - Load-time CUDA OOM falls back to CPU explicitly. The fallback used to fire
only because the OOM text happened to contain the wordcuda; it is now a
recognised case with its own message ("the card is too small for this model")
and log reason, in bothtranscribe_video.pyand the collapse-retry child. - The collapse-retry child no longer re-attempts CUDA after the parent already
fell back.transcribe_video.pyexportsLECTURE_ASR_DEVICE=cpuonce it is
on CPU;retranscribe_segment.pyhonours it instead of OOM-ing a second time.
Added
transcribe_video.py --device {auto,cuda,cpu}.auto(default) is the
existing behaviour;cpuskips the CUDA attempt and prints the ETA up front
(pair with--model small/mediumon 2–4 GB machines);cudanever falls
back and fails loudly.- README (both languages): a small GPU (2–4 GB) counts as no GPU — which flags to
use and which optional stages to skip.
Changed (synced from upstream, no user-facing behaviour on other machines)
- GPU stages take a machine-wide FIFO lease when the author's
gpu_lease.pyis
present next to the skill; on any other machinegpu_lease_ctx()is a
no-op. Replaces the older best-effort pause-flag wait intranscribe_video.py,
vlm_signals.py,retranscribe_segment.py,ocr_surya.py. batch/build_L1.py: a deck-only (unrecorded) slide keeps up to 24 text lines
in L1 instead of the 6 a video frame gets — the deck text is the only content
there.
v0.7.0
Changed
- Timecode citations are now parsed, not pattern-matched.
export_web.pyused a
single "one label + one time" regex, so every other way a writer naturally cites a
moment fell through as dead plain text — with no error anywhere, because a
non-match is indistinguishable from prose. Worse, the bullet'sdata-time-source
silently degraded toinherited, meaning it reused the previous bullet's
timestamp for scroll-sync. An audit of 79 delivered courses found 51 affected,
8,086 citations, and 7,118 of 27,177 timed bullets (26%) mis-positioned.
The parser now accepts:- ranges —
(V1 04:22–04:31)→ one pill showing the range, jumping to its start - lists —
(V1 05:02, 06:13)→ one jumpable pill per time - cross-clip spans —
(clipA 29:50–clipB 01:25)→ each end keeps its own pill - three-digit minutes
(V1 100:43)and second-only range ends(V2 28:30-42) - a leading note —
(practice V1 00:09),(c00 14:17)— kept verbatim - labels declared by the note's own
## 影片代號table, so an audio-only source
cited as(A1 06:59)resolves; this table now outranks the positional V-map,
which was wrong for notes that continue numbering across segments - an unlabelled
(08:27–12:33)only when the segment has exactly one media
file. With more than one candidate it stays plain text: a pill pointing at the
wrong video is worse than no pill.
Seconds are validated as00–59so prose like(120:80)cannot match.
- ranges —
Fixed
- A citation naming a file the course does not have no longer becomes a link.
OneMEDIA_RELcheck now gates every route that produces a filename (V-map,
declared table, literal citation, single-clip fallback), so a parsing slip
downgrades to plain text instead of minting a button that silently does nothing. - Filenames containing a comma resolve. A real clip may be named
20221029_143246-ACL, PCL, menisc, TFCC.mp4; the grammar would have read those
commas as a list of times. A fast path matches the whole parenthesised run against
the manifest before the general grammar runs. - Placeholder names in two docstrings replaced with
<speaker>/<topic>.
v0.6.7
Fixed
Two silent failures found while repairing a reordered course. Both share a shape
this project keeps meeting: a check that reports success about something it
never actually examined.
export_web.pyshipped 124 wrong-but-plausible figures under a green
check. The fast path for embedded images only ever looked in<course>/figures/,
whilebuild_L1.pywrites to<course>/_L1/figures/— so on every course built
by the batch pipeline it could not hit, and each embed fell through to a
recursive walk that indexes the whole work dir by basename. After a manifest
reorder there are two generations ofcNN_frame_XXXX.jpgon disk (old numbering
left in_L1/_stale_figures/, new in_L1/figures/), so one basename means two
different pictures. The resolver picked one — silently when segment context
happened to scope it — andembed-check: all N image embeds mapped + copied ✓
reported success. Captions were right; 124 of 896 images were not.
Now: figure roots are an ordered list (figures/, then_L1/figures/);
candidates are collapsed by content hash, so identical bytes under several paths
are not an ambiguity; a genuinely ambiguous basename is an error that fails the
export, and one resolved by segment context prints a warning instead of
resolving silently. Stage the intended generation in<course>/figures/to pin it.audit_segmentation.pyjudged delivered courses by a superseded artifact.
It read onlyproposal.md— a planning note — and neversegments.json, which
is what actually ships. Every reordered course therefore returnedREGENERATE,
including one whose 20 segments each still mapped to a contiguous block of the
corrected order and needed nothing but adisplay_orderfix; acting on that
verdict would have rewritten 41 L2/L3 notes for no reason. It now checks the
delivered segmentation first — contiguity (is the grouping intact?), coverage
(does any clip belong to no segment?), and whether playback order follows real
time — and downgrades proposal staleness to INFO when that segmentation holds up.
Overlapping source pairs already sitting in one segment are reported as intended,
not as a defect to fix.
v0.6.6
Documentation
The same-talk trap section shipped in 0.6.5 said what to look for but not what
should trip the alarm first — and its author then fell into the trap the same
day. Two additions to multi-camera.md:
- Check the spread before the vote count. Offsets recovered from two devices
at one event agree within seconds; tens of seconds means different sittings of
the same recurring course. The failure mode is comparing that spread against
the offset being proposed — 45 seconds against a 4.5-year clock error reads as
tight, and a wrong conclusion gets accepted. Compare it against seconds, not
against the number you are trying to prove. - Settle "are these the same files?" with categorical evidence — durations
equal to the second, byte sizes equal — not similarity scores. The score
ranges for "one file, two ASR runs" and "one script, two years" overlap
completely (measured: 0.71–0.86 for both). Also noted:difflib.quick_ratio()
compares character multisets and is an upper bound, not a comparison.
v0.6.5
Documentation
The reasoning behind 0.6.1–0.6.4 now lives in the reference docs, not just in
the commits that fixed the code.
multi-camera.mdgains three sections on placing material whose clock lies:
serial-number bracketing (a clip with no capture time at all can still be
placed, because stills and video share one counter on the same card — and a
below-threshold cross-correlation becomes usable once an independent line of
evidence agrees with it); the same-talk trap (cross-correlation votes on
shared N-grams, so a lecturer repeating a course scores as high as a second
camera at one event — what separates them is speech that happens only once);
and phantom years (a folder named from material timestamps inherits the
camera's wrong clock, inventing a plausible year that every derived artifact
then carries).pipeline.mdstates the label rule plainly: the viewer parsesVnand bare
filenames only, so anA1invented for an audio source yields a note that
reads perfectly and whose every timecode is dead.decisions.mdgains three entries on the shape of silent failure — an empty
scan reads like a clean one, an unmatched pattern reads like prose, and an
empty comparison must never fall through to the success path.
v0.6.4
Fixed
- Timecodes pointing into an audio file are now clickable in the exported
viewer. The citation pattern listed only video extensions, so an audio-only
session — a recorder file with photographed slides, which this pipeline
supports as a first-class source — rendered every(recording.mp3 03:23)as
dead plain text. Nothing errored, because a non-match looks exactly like
prose. On a two-day conference with four audio-only afternoon talks, 312 of
536 timecodes were dead this way.
v0.6.3
Fixed
audit_note.pyexempts the overview segment from the transcript-coverage
gate. That segment's transcript is every other segment's concatenated, so a
one-page catalog scores about 0.01 against a 0.1 floor and can never pass —
a WARN on every course with an overview, which is how readers learn to skim
past WARNs. Per-segment L3s are still gated.
v0.6.2
Fixed
audit_segmentation.pycompared timeline basenames against manifestsrc
values that may carry a subdirectory prefix. For those courses the two lists
never intersected, the order check found nothing, and the run fell through to
an INFO line saying the clip order "was corrected by real time" — about a
course it had not checked. Both sides are now compared as basenames. On a real
course this turned a clean-looking INFO into a REGENERATE naming four
genuinely out-of-order clips.
v0.6.1
Fixed
course_timeline.py --reorder-manifestcrashed with aKeyErroron any
course whose manifest references clips inside a subdirectory, e.g.
Demo-Prac\00008.MTS: the sort keyed on the clip'ssrcagainst a dict
built from bare filenames. The quieter half of the same bug was worse — two
clips in different subdirectories that share a basename (what you get when a
second camera card restarts its numbering) would both have resolved to one
capture time, with no error. The sort now keys on the manifest index, which
is unique by construction.
Documentation
- README (both languages) now links back to the beginner series and to the post
explaining this pipeline. No runtime change.