0.26.0: render what the EDL says, find the repeat where whisper put it, and stop the cut eating the sentence - #77
Merged
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Six fixes measured against a 19-minute screencast, four of them found by running the tool rather than by reading it.
What lands
f2b8815commitcan render a round twice, somaster.wavfinally carries the deterministic cutsda8cd81silencesrejects a flag it does not read instead of answering with the default3d388barepeatedPhrasescounts a repetition inside one line, which is where whisper puts it64d4bdefindRestartscatches a retake whose attempts diverge at word three958ca8a984 tests, 0 fail. Each fix has a contrast run: revert the source, keep the test, confirm it fails.
#66 is the load-bearing one
commitfolded its deterministic cuts into the EDL and re-rendered over the path its own first render had written. The guard refused,commitexited 1, and the EDL shrank while the audio did not.Measured on a 1145s source:
master.wavedl.jsonDead 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/131then128/128segments agreeing.#71 was the reason detection looked fine and found nothing
semantic.tsrequired 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
measurementwording the seam fix changed. Both explained in their commits.repetitionCutstoday becauseMAX_FUMBLE_WORDS = 0declines every finding with speech between the readings, matching the issue's own "latent" framing.seamBetweenis tested directly. Raising that bound is a separate decision.