Skip to content

0.26.0: render what the EDL says, find the repeat where whisper put it, and stop the cut eating the sentence - #77

Merged
Railly merged 6 commits into
mainfrom
issue-66-rerender-collision
Aug 17, 2026
Merged

0.26.0: render what the EDL says, find the repeat where whisper put it, and stop the cut eating the sentence#77
Railly merged 6 commits into
mainfrom
issue-66-rerender-collision

Conversation

@Railly

@Railly Railly commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Six fixes measured against a 19-minute screencast, four of them found by running the tool rather than by reading it.

What lands

commit issue what it changes
f2b8815 #66 commit can render a round twice, so master.wav finally carries the deterministic cuts
da8cd81 #67 silences rejects a flag it does not read instead of answering with the default
3d388ba #71 repeatedPhrases counts a repetition inside one line, which is where whisper puts it
64d4bde #72 new findRestarts catches a retake whose attempts diverge at word three
958ca8a #73 a repetition cut ends at the seam instead of eating the sentence

984 tests, 0 fail. Each fix has a contrast run: revert the source, keep the test, confirm it fails.

#66 is the load-bearing one

commit folded its deterministic cuts into the EDL and re-rendered over the path its own first render had written. The guard refused, commit exited 1, and the EDL shrank while the audio did not.

Measured on a 1145s source:

before after
exit code 1 0
master.wav 558.081s 417.029s
edl.json 445.5s 417.029s
divergence 112.6s 0.000s

Dead air at the deadAir parameters went from 10 pauses / 67.20s to zero, and the four largest formerly-dead spans now carry speech at the file's own level. Verified again over a full two-round run: audit 131/131 then 128/128 segments agreeing.

#71 was the reason detection looked fine and found nothing

semantic.ts required a repeat to cross two transcript lines. Whisper returns one cue per window 83% of the time at 16s, so the detector could not fire on most of the file. Measured over 639 windows at seven widths from 4s to 32s: 0 of 12 known repetitions found at every width. Counting intra-line repeats takes it to 10 of 12.

#72 ships partially, on purpose

The issue prescribed 2-word runs behind the content-word floor. Measured against the strings the issue itself names, the floor cannot separate the classes: real restart prefixes score 0,1,1,1,1,2,2 content words and the named false positives score 0,1,1. A structural rule shipped instead, and the issue stays open for the case it does not reach. Widening the bound needs a constant that could only be fitted to purpose-written fixtures.

Notes

Railly added 6 commits August 17, 2026 01:16
…ter carries the deterministic cuts (#66)

commit folds the deterministic pass's cuts into the EDL and renders again,
but the second render reused the round's own renderOptions and so targeted
the path the first render already wrote. The existing-output guard refused
it and commit exited 1: edl.json shrank, master.wav did not, and rounds 2+
re-measured a render that no longer matched the EDL.

RenderOptions gains allowExisting, set only by that second call. The guard
stays for every other caller. It is safe because the render already writes
to a temp sibling and only renames it into place after probeOutput passes,
so a failed second render leaves the first file standing.

Measured on a 1145.2s source: exit 0, master.wav 417.029s against edl.json
417.029s, where before it was 558.081s against 445.5s. Dead air at the
deadAir parameters went from 10 pauses / 67.20s to zero, and the four
largest formerly-dead spans now carry speech at the file's own level.
…wering with the default (#67)

`vcut silences --threshold -80 --min-silence 800` ran, exited 0, and reported
thresholdDb -30 and minSilenceMs 250 back: the defaults, shaped exactly like a
measurement. The real flags are --noise and --min. Nothing in the output told
a caller the threshold it asked for was never seen, and every number derived
from that run inherited the error silently.

rejectUnknownFlags names every unrecognised flag at once and exits 2, the same
silent-acceptance bug requireJson and requireRawOutput each close for one flag,
widened to the flags a command declares itself. Values are skipped rather than
validated, so a command's own parser still owns what its flags mean, and a
negative dB value reads as a value rather than as an unknown flag.

Wired into silences, the verb where it was measured. The other nine verbs with
their own flags share the same exposure and are filed separately.
…sper puts it (#71)

repeatedPhrases skipped a run it had already seen in the current line, so a
repetition had to straddle two transcript lines to be counted. Whisper does not
transcribe that way: measured across 639 windows over 417s of real material at
seven widths from 4s to 32s, it returns one cue per window 83% of the time at
the shipped 16s sweep width. The detector found 0 of 12 known repetitions at
every width, and the window size was never the cause.

A real 16s cue it discarded, verbatim: "Podemos comenzar a probar el MCP en
normal. Entonces, para usarlo, simplemente lo taggeamos, arroba normal.
Entonces, para usarlo, lo taggeamos."

Counting occurrences inside a line takes recall to 10 of 12. False positives go
0 to 8, all of them at 2 or more content words, so MIN_CONTENT_WORDS still does
its job and no threshold moved. Occurrences are counted non-overlapping, which
is the narrower guard the old rule was reaching for: "eh eh eh eh" is one
hesitation seen through two sliding probes, not two sayings of it.

count now means occurrences rather than lines. survivingRepeats compares it
against occurrences in the rendered master's own text, so the line count made a
phrase said twice inside one line read as answered by a render still saying it
twice.

verify.ts's findRepeatedPhrases never shared the defect: it scans a window's raw
text and already counts within it.

The test asserting a line never repeats itself is inverted rather than deleted.
It encoded the defect as intent, and its own example ("un honor grande un honor
grande") is exactly the shape #71 measured in production.
A speaker who backs up mid-clause and restarts with a different destination
leaves a shared prefix and a divergence: "para simplemente cifrar, para
simplemente detectar". Six annotated defects have this shape and every
RUN_LENGTH scan is blind to it. Not because the width is wrong: normalised, the
only runs occurring twice are bigrams, so no trigram repeats at all and
findRepeatedPhrases returns zero findings on the real text.

Dropping to a bare 2-word run is not the fix, and the issue's own named strings
prove it. Real restart prefixes score 0, 1, 1, 1, 1, 2, 2 content words; the
false positives it names score 0, 1, 1. The distributions overlap completely, so
no content-word floor separates them. Scoring the grown span or the prefix plus
both destinations separates them no better: "voy a mostrar / cerrar" scores 3,
higher than two of the real restarts.

findRestarts takes the escape findStackedOpeners already took for the identical
problem, structure instead of a lexicon. The prefix must repeat and then
diverge, the abandoned attempt must be shorter than the shared prefix, and the
prefix must carry a content word. growMatch already computed that divergence
index as its loop's exit condition and threw it away; for this class the index
is the answer.

No constant is picked. The bound on the abandoned attempt is the repeat's own
width, the same self-referential shape MAX_RETAKE_GAP_MS derives from the
sweep's window rather than inventing a number.

Measured: fires on "la ia tendria" and "para simplemente", silent on all three
connectives the issue names and on the "le damos clic a Create / a Sign in"
reuse #63 forbids cutting.

Stated limit: it does not fire on "abrimos la guia para vincular / abrimos la
opcion para vincular", whose abandoned attempt runs three words against a
two-word prefix. A wider bound would be fitted to fixtures, since the corpus
behind the 8-to-64 false-positive figure is not in this repo. Documented at
RESTART_PROBE_LENGTH rather than papered over.
…currence (#73)

The span ran from the first occurrence's first word to the SECOND occurrence's
first word, which removes occurrence 1 plus every word spoken between the two
readings. Those words are not only the trailing-off: when the speaker got
further into the first attempt than the repeated phrase itself, they are the
continuation the surviving sentence was going to complete.

Measured over one master, 5 of 5 repetition cuts ate real words. One removed
"cual es la diferencia" outright.

The cut now ends at the seam, the widest measured gap between two consecutive
words in that stretch, which is the micro-pause a speaker leaves when they stop
one attempt and start another. Everything after the seam survives.

No threshold is introduced. The seam is the largest gap present rather than the
first gap over some minimum, so this asks where the break is rather than whether
a break is big enough. Two readings butting straight up against each other fall
back to the retake's own start, which is where the old span already ended and is
correct when there is nothing in between to preserve.

Verified by transcribing 4s around every junction in source and render: 9
junctions identical, 6 differing only by ASR variance (click/clic, aprobar/a
probar), 0 words lost.

Nothing shipped this damage yet because MAX_FUMBLE_WORDS is 0 and the pass
declines nearly everything with speech between the readings, which is why
seamBetween is tested directly. That latency is the reason to fix the geometry
now: #71 and #72 both raise the sensitivity, and whoever raises it further
inherits a cutter that eats speech.
…ut eating the sentence

The repeat detector had been returning zero on real material for a structural
reason nobody had measured: it required a repetition to straddle two transcript
lines, and whisper returns one cue per window 83% of the time at the shipped
sweep width. Across 639 windows over 417s at seven widths, 0 of 12 known
repetitions. Counting inside a line takes that to 10 of 12 without moving a
threshold.

The restart that diverges at word three is a second blind spot and needed a
different instrument, not a smaller window. The content-word floor cannot
separate a real restart from a connective at two words: measured on the issue's
own strings, both distributions run 0 to 2. Structure separates them, so
findRestarts requires a shared prefix that diverges and an abandoned attempt
shorter than the prefix itself, which borrows no constant.

The third is latent damage rather than a miss. A repetition cut spanned both
occurrences, so it removed the words the speaker said in the failed attempt that
belong to the sentence that survives: 5 of 5 cuts ate real speech on one master.
Nothing shipped it because the pass declines almost everything today, and the
first two changes are exactly what would have exposed it.
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vcut Ready Ready Preview Aug 17, 2026 8:34pm

@Railly
Railly merged commit 2142cc5 into main Aug 17, 2026
5 checks passed
@Railly
Railly deleted the issue-66-rerender-collision branch August 17, 2026 20:39
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.

1 participant