Skip to content

Releases: drpwchen/lecture-to-notes

v0.8.0

Choose a tag to compare

@drpwchen drpwchen released this 13 Oct 09:32

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

Choose a tag to compare

@drpwchen drpwchen released this 01 Sep 12:23

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.py compared free VRAM
    against --min-free-mb 6000, said blocked — wait for other tasks, and the
    skill instructions say not to retry. Nothing was going to free up — the card
    cannot hold a large-v3-class model at all. The pre-flight now checks total
    VRAM first and, when the card can never pass, prints GPU_TOO_SMALL with the
    three real options (CPU path, --model small, --engine groq) and exits 0,
    the same verdict as "no NVIDIA card". --require-gpu keeps 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 word cuda; it is now a
    recognised case with its own message ("the card is too small for this model")
    and log reason, in both transcribe_video.py and the collapse-retry child.
  • The collapse-retry child no longer re-attempts CUDA after the parent already
    fell back.
    transcribe_video.py exports LECTURE_ASR_DEVICE=cpu once it is
    on CPU; retranscribe_segment.py honours it instead of OOM-ing a second time.

Added

  • transcribe_video.py --device {auto,cuda,cpu}. auto (default) is the
    existing behaviour; cpu skips the CUDA attempt and prints the ETA up front
    (pair with --model small/medium on 2–4 GB machines); cuda never 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.py is
    present next to the skill; on any other machine gpu_lease_ctx() is a
    no-op. Replaces the older best-effort pause-flag wait in transcribe_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

Choose a tag to compare

@drpwchen drpwchen released this 07 Aug 10:35

Changed

  • Timecode citations are now parsed, not pattern-matched. export_web.py used 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's data-time-source
    silently degraded to inherited, 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 as 00–59 so prose like (120:80) cannot match.

Fixed

  • A citation naming a file the course does not have no longer becomes a link.
    One MEDIA_REL check 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

Choose a tag to compare

@drpwchen drpwchen released this 05 Aug 07:17

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.py shipped 124 wrong-but-plausible figures under a green
    check.
    The fast path for embedded images only ever looked in <course>/figures/,
    while build_L1.py writes 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 of cNN_frame_XXXX.jpg on 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 — and embed-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.py judged delivered courses by a superseded artifact.
    It read only proposal.md — a planning note — and never segments.json, which
    is what actually ships. Every reordered course therefore returned REGENERATE,
    including one whose 20 segments each still mapped to a contiguous block of the
    corrected order and needed nothing but a display_order fix; 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

Choose a tag to compare

@drpwchen drpwchen released this 05 Aug 04:36

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

Choose a tag to compare

@drpwchen drpwchen released this 05 Aug 00:57

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.md gains 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.md states the label rule plainly: the viewer parses Vn and bare
    filenames only, so an A1 invented for an audio source yields a note that
    reads perfectly and whose every timecode is dead.
  • decisions.md gains 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

Choose a tag to compare

@drpwchen drpwchen released this 04 Aug 19:40

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

Choose a tag to compare

@drpwchen drpwchen released this 04 Aug 19:06

Fixed

  • audit_note.py exempts 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

Choose a tag to compare

@drpwchen drpwchen released this 04 Aug 18:43

Fixed

  • audit_segmentation.py compared timeline basenames against manifest src
    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

Choose a tag to compare

@drpwchen drpwchen released this 04 Aug 18:39

Fixed

  • course_timeline.py --reorder-manifest crashed with a KeyError on any
    course whose manifest references clips inside a subdirectory, e.g.
    Demo-Prac\00008.MTS: the sort keyed on the clip's src against 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.