Core quality pass: sentence-cut clips, exact captions, two-pass loudness, lazy reframe (0.8.0) - #58
Merged
Merged
Conversation
…ors, two-pass loudness, cleaner transcripts Captions - Lay caption groups out into explicit lines from a per-style em budget (measured glyph widths for the bundled fonts, aspect-aware), capped at two lines. The ASS export carries the breaks as \N with wrapping off and the preview draws one non-wrapping block per line, so both renderers always agree on where a caption wraps. - Hold a finished group on screen for up to 1.5 s, cut short by the next group, instead of blanking the frame on every pause. - Position every caption event middle-centre on the style anchor (\an5\pos) to match the preview's centred block. The bottom-aligned style sat two-line captions about half a block higher in the export than in the editor. Audio - Measure the clip's loudness first and normalise with one linear gain (measured_* + linear=true) instead of single-pass dynamic loudnorm, whose gain riding pumps on speech. Silent or unmeasurable sources fall back to the single-pass filter. Verified: a -38 LUFS source exports at -14.0 LUFS in linear mode. Transcripts - Drop Whisper segments it flags as hallucinations (no_speech_prob > 0.6 with avg_logprob < -1, or compression_ratio > 2.4) and the words inside them, and prime the next chunk with trusted text only. - Make word timings monotonic with a minimum on-screen duration so zero- or negative-length words still light up in karaoke captions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XZCKJpD6vn9eFsymhmhHhg
Face tracking plus LR-ASD at 25 fps is the slowest per-clip stage by an order of magnitude, and it ran for every candidate before the user saw a single clip. An hour of source produced ~40 clips and most of an hour of waiting, largely on clips nobody would open. - The pipeline now runs reframe analysis for the top tier only: the eight highest-scoring clips, extended to twelve for scores of 80+ (shared/reframe.ts). Everything else is saved as reframeStatus 'pending' with the video type's default layout. - A pending clip is analysed when opened in the editor (store.ensureReframe, triggered from EditorScreen) and, as a guard, before export. The editor shows a status line while it waits and stays editable. - main/pipeline/reframe.ts owns the on-demand run: analyses the clip's current trim, de-duplicates concurrent callers, and grafts only the analysis-owned fields (focus track, content type, layout defaults) onto the saved clip so edits made meanwhile survive. project:updateClip keeps a landed analysis when the renderer saves a copy taken before it landed. - Video types with no face tracking (product demos) still get their layout for every clip up front, since that is a cheap default. - Older projects load with every clip marked done. Tests: tier selection and merge rules (tests/reframe.test.ts) plus an integration test that runs the real analysis on a synthetic video under a stubbed Electron app and checks persistence, de-duplication and merging (tests/lazyReframe.test.ts). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XZCKJpD6vn9eFsymhmhHhg
The transcript the model chooses clips from was Whisper's segment list, and Whisper segments break wherever the decoder window ends — routinely mid-sentence. A model that only sees segment boundaries can only propose segment boundaries, and the snapping afterwards was guessing which sentence it meant. Sentence model (shared/sentences.ts) - transcriptSentences() derives sentences from word punctuation (falling back to a segment's punctuation when its last word lost it), splits unpunctuated runs longer than 20 s at their longest pauses, and carries a duration-weighted vocal energy per sentence. sentenceStartTimes/EndTimes now derive from it. Highlight pass (pipeline/highlights.ts) - The model reads one sentence per line with start/end times and is told that a clip's start is a line's start and its end a line's end. The ending and opening reviews tag the same sentences. - snapClipStart opens the sentence the model's start falls inside (within 2.5 s) rather than jumping to whichever boundary is nearest, which could drop the sentence the model wanted. snapClipEnd completes the sentence an end falls inside, or closes on the sentence just finished when the end lands in the pause after it. Tighten (shared/tighten.ts) - Keep 0.7 s after the last word and 0.3 s before the first. Tightening trimmed the tail to 0.3 s, so the export's 0.4 s fade ducked the final syllable of every tightened clip — the default path. Measurement - shared/clipMetrics.ts: per-clip boundary report (mid-sentence open/close, lead-in, tail, opening and closing sentence) and a summary. - scripts/eval-clips.ts: runs the report over saved projects, offline; with --rerun it re-detects clips on the saved transcript with the current prompts and reports both, so a prompt change can be A/B'd for cents. Tests - Sentence derivation, splitting, energy; start/end snapping rules; the model-facing transcript format; tighten head/tail room; the metrics. - tests/highlightsFlow.test.ts drives detectHighlights end to end against a scripted model (global fetch stub): sentence-per-line prompt, mid-sentence model boundaries landing on sentence boundaries with pre/post-roll, the ending review extending a clip, the opening review advancing one, dedupe and clamping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XZCKJpD6vn9eFsymhmhHhg
Review fixes - Captions: WrapStyle 2 had also switched off wrapping for the hook title, which carries no explicit breaks, so long hooks ran off the frame. The script keeps WrapStyle 0 and caption events carry \q2 instead, so only the lines the layout decided are exempt from re-wrapping. - Loudness: loudnorm's linear mode silently reverts to the dynamic gain rider when the measured loudness range exceeds the target range or the gain would push the true peak over the ceiling, both routine for speech. The range target is now raised to the measured range, and peak-limited sources take a plain gain into a true-peak limiter instead. Both paths verified on the bundled ffmpeg: -14.0 LUFS linear, -14.5 LUFS / -1.9 dBTP limited. CLIPFORGE_DEBUG logs which path a render took. - Lazy reframe: a layout the user chose while a clip was pending (letterbox, a dragged focus slider, auto zoom off) is kept when the analysis lands; only the focus track and content type are always taken. Concurrent callers on one clip now refcount their aborts, so cancelling an export no longer strands an editor waiting on the same clip, and the run is cancelled only when every caller has gone. A failed analysis shows in the editor with a retry instead of looping or hanging on the spinner. - The "fit under N MB" field edits a local draft and commits on blur/Enter; bound to the saved value it could not be cleared and rewrote settings on every keystroke. The two layouts share the toggle, input and presets. - eval-clips uses the shared tail thresholds instead of literals. Release prep - Version 0.8.0; CHANGELOG section dated. - Merged origin/main (winget manifest). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XZCKJpD6vn9eFsymhmhHhg
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Start snap skips enclosing sentence
- When the model lands deep inside a long sentence, snapClipStart now word-snaps immediately instead of falling through to the next-sentence snap path.
You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 231653a. Configure here.
When the model points deep into a long sentence, fall back to word-snapping instead of continuing to the next-sentence snap path, which could jump forward to the following line when it starts within START_SNAP_SEC.
Regression test for the Bugbot finding on #58: a start in the last 2.5 s of a long sentence stays on that sentence at the nearest word instead of jumping to the next line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XZCKJpD6vn9eFsymhmhHhg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What does this change?
A quality pass over the five things the product lives on: where clips start and stop, captions matching the preview, audio mastering, speaker reframing throughput, and cleaner transcripts. Bumps the version to 0.8.0 with a dated CHANGELOG section so the release workflow can publish notes.
Clip boundaries
scripts/eval-clips.tsmeasures boundary quality on saved projects, and with--reruncompares against fresh detection so prompt changes can be checked.Captions
\Nwith\q2on caption events; the preview draws one non-wrapping block per line. Preview and export cannot wrap differently.Audio
Reframe throughput
Transcripts
Also
Why?
Making a long video into clips is judged in the first and last second of each clip and by whether the captions look like the editor promised. Each of these was leaking quality in a way you could see or hear: mid-sentence cuts, captions wrapping differently in the file, audio pumping on speech, an hour of face tracking before the first clip appeared.
How did you test it?
npm test(324 tests, up from 231; new coverage for sentence derivation, boundary snapping, caption layout and anchor, loudness parsing and both paths, tier selection and merge rules, hallucination filtering, word timing repair)npm run typechecknpm run lintAnything to watch out for?
Preview and export still share the planning code in
src/shared/; the caption layout now lives there too (captionLayout.ts), so both sides get identical lines by construction. The width estimate is deliberately conservative for unknown uploaded fonts, so lines break a little early rather than risk overflow.Older projects load unchanged: clips without a reframe status are treated as already analysed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XZCKJpD6vn9eFsymhmhHhg
Generated by Claude Code
Note
Medium Risk
Touches highlight detection, render audio, export IPC, and on-demand reframe on the critical path; behavior is heavily tested but regressions in framing or loudness would affect every export.
Overview
0.8.0 is a quality pass on clip boundaries, captions, export audio, reframe throughput, and transcript hygiene.
Clip picking now feeds the model one sentence per line (from word punctuation, with long unpunctuated runs split at pauses) instead of Whisper segments, and snaps starts/ends with
snapClipStart/snapClipEndso clips open and close on thoughts. Opening/ending reviews use the same sentence model. Tighten keeps more head/tail room so the export fade does not duck the last syllable.scripts/eval-clips.tsscores saved projects and can--rerundetection for prompt A/B checks.Speaker reframing runs eagerly only for the top 8–12 clips; others stay
reframeStatus: pendinguntil the editor opens them or export runsensureClipReframe, with merge rules so stale renderer saves do not wipe analysis and concurrent callers share one run.Captions share a width-based
captionLayoutBudgetincaptionLayout.tsfor preview and ASS (\N+\q2), middle-centreposto match preview, and a brief hold after each group.Exports measure then normalize loudness (linear loudnorm or gain+limiter) instead of single-pass dynamic loudnorm.
Transcription drops hallucinated Whisper segments and words, primes chunks with trusted text only, and normalizes word timings for karaoke.
Also fixes the fit under N MB settings field (draft until blur/Enter) and bumps version/docs.
Reviewed by Cursor Bugbot for commit 8b89e0a. Bugbot is set up for automated code reviews on this repo. Configure here.