Skip to content

Core quality pass: sentence-cut clips, exact captions, two-pass loudness, lazy reframe (0.8.0) - #58

Merged
JeremySNR merged 7 commits into
mainfrom
claude/core-components-quality-lsnovg
Sep 6, 2026
Merged

Core quality pass: sentence-cut clips, exact captions, two-pass loudness, lazy reframe (0.8.0)#58
JeremySNR merged 7 commits into
mainfrom
claude/core-components-quality-lsnovg

Conversation

@JeremySNR

@JeremySNR JeremySNR commented Sep 6, 2026

Copy link
Copy Markdown
Owner

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

  • The model reads the transcript as one sentence per line (derived from word punctuation, unpunctuated rambles split at pauses) instead of Whisper segments, which break mid-sentence, and is told to start and end every clip on a line. The ending and opening reviews use the same sentences.
  • A start inside a sentence opens that sentence rather than jumping to the nearest boundary; an end inside a sentence completes it.
  • Tightened clips keep 0.7 s after the last word so the export fade no longer ducks the final syllable.
  • New scripts/eval-clips.ts measures boundary quality on saved projects, and with --rerun compares against fresh detection so prompt changes can be checked.

Captions

  • Groups are laid out into explicit lines from a per-style em budget (measured glyph widths, aspect-aware), capped at two lines. The ASS export carries the breaks as \N with \q2 on caption events; the preview draws one non-wrapping block per line. Preview and export cannot wrap differently.
  • Caption events are anchored middle-centre on the style line, matching the preview. Two-line captions used to sit half a block higher in the export.
  • Finished groups hold for up to 1.5 s instead of blanking the frame on every pause.

Audio

  • Two-pass loudness: measure, then one linear gain. Through loudnorm linear mode when peaks allow (range target raised to the measured range, which that mode requires), otherwise a plain gain into a true-peak limiter. Single-pass dynamic only when the source cannot be measured.

Reframe throughput

  • Face tracking plus active speaker detection runs for the top tier only (eight clips, twelve for scores of 80+). The rest are analysed on demand when opened or exported, with refcounted cancellation and an editor status line with retry. Layout choices made while pending are kept.

Transcripts

  • Whisper segments it flags as hallucinations are dropped with their words and never primed into the next chunk. Word timings are made monotonic with a minimum on-screen duration.

Also

  • The "fit under N MB" field can be cleared and retyped; it commits on blur/Enter.

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 typecheck
  • npm run lint
  • Checked the change in the running app (Xvfb smoke test with screenshots)
  • Offline pipeline and quality scripts (real renders through the new audio chain)
  • A scripted-model test drives the whole highlight pass end to end through a stubbed network layer: sentence-per-line prompt, mid-sentence model boundaries landing on sentence boundaries, the ending review extending a clip, the opening review advancing one, dedupe and clamping.
  • An integration test runs the real on-demand reframe analysis on a synthetic video under a stubbed Electron, covering persistence, shared runs, cancellation refcounting and merging with concurrent edits.
  • Loudness verified on the bundled ffmpeg: a wide-range source lands at -14.0 LUFS in linear mode; a peak-limited source lands at -14.5 LUFS with the true peak at -1.9 dBTP through the limiter.
  • Rendered 9:16 frames for three caption styles and a long hook title and checked them visually: blocks centred on their anchors, explicit two-line breaks, the hook wrapping inside its margins.

Anything 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 / snapClipEnd so 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.ts scores saved projects and can --rerun detection for prompt A/B checks.

Speaker reframing runs eagerly only for the top 8–12 clips; others stay reframeStatus: pending until the editor opens them or export runs ensureClipReframe, with merge rules so stale renderer saves do not wipe analysis and concurrent callers share one run.

Captions share a width-based captionLayoutBudget in captionLayout.ts for preview and ASS (\N + \q2), middle-centre pos to 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.

…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

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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.

Comment thread src/main/pipeline/highlights.ts
cursoragent and others added 2 commits September 6, 2026 13:04
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
@JeremySNR
JeremySNR merged commit 35814e5 into main Sep 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants