Skip to content

ASR word timestamps share an edge ~90% of the time, so the best take is the hardest one to cut

TL;DR.

Measured across 4 talking-head clips (Deepgram nova-3): the median inter-word gap is 0.000s and 81-92% of adjacent word pairs share a timestamp exactly, so a cut boundary read off word timestamps is on an unsafe shared edge by default. Enumerate the gap inventory first (6-9 usable points per minute) and snap semantic intent to it. The inversion that follows: the most fluent take has the fewest places to cut, so role-based cross-take splicing is quietly betting on the takes being bad.

Building a transcript-driven video cutter, you read cut boundaries off ASR word timestamps and then check whether each one is safe. Invert that. The unsafe case is not the exception, it is ~90% of all boundaries, and the takes a human actually picks are the ones with almost nowhere to cut.

Measured on 4 real talking-head clips (one speaker, Deepgram nova-3 / general-nova-3 v2025-07-31.0), computing every inter-word gap as words[i+1].start - words[i].end:

fluent take 1 41.5s 101 89 (89.0%) 6 5
fluent take 2 (the pick) 28.6s 75 68 (91.9%) 3 0
fluent take 3 27.0s 60 54 (91.5%) 3 2
stumbling take (11s reset) 66.7s 97 78 (81.2%) 8 6

The median inter-word gap is 0.000s in every single clip.

Why this breaks the obvious algorithm

The obvious flow is: pick a semantic boundary (end of the hook, start of the argument), read its timestamp off the word list, cut there. But when words[i].end == words[i+1].start exactly, that shared number is an ASR decision, not a measurement of silence. The true transition is somewhere within a few tens of ms either side and there is no safe value between: cutting there either bleeds the next word's attack in or eats the previous word's tail. A leftover consonant is worse than a whole extra word — 20ms of a trailing /s/ fused onto the following word and re-transcribed as a phantom word that was never spoken.

Since ~90% of boundaries are shared, "pick semantically, then validate" fails its validation almost every time and degenerates into hand-nudging every boundary. Enumerate the gap inventory first — it is small, cheap and complete — then snap semantic intent to the nearest real gap:

gaps = [(ws[i]["end"], round(ws[i+1]["start"] - ws[i]["end"], 3), ws[i]["word"], ws[i+1]["word"])
        for i in range(len(ws) - 1)]
cuttable = [g for g in gaps if g[1] >= 0.30]   # everything else is a shared edge

That came out at 6-9 usable points per minute across all four clips, independent of speaker fluency and clip length. A 28.6s take offered exactly three internal cut points in the whole take. You can print them all and choose by eye; there is no search problem here, only a scarcity problem.

The inversion, which is the part that surprised me

Take fluency and cut-point availability are inversely correlated. The take the human picked — tightest delivery, no restarts, no flubs, complete argument in 28.6s — had zero gaps of 0.60s or more anywhere in it. The bad take, the one with a false start and an 11.18s reset, had six.

Obvious in hindsight (a pause is a delivery defect, and a defect is an editing handle) with a consequence that is not obvious at all: a workflow that decomposes each take into roles (hook / story / CTA) so a human can mix a hook from take A with a body from take B is quietly betting on the takes being bad. On fluent takes the role boundaries land precisely on shared edges — all six hook/story boundaries across my three takes were exact ties (3.92/3.92, 3.52/3.52, 3.92/3.92), and one take's CTA butted straight onto the last word of the body at 25.38 with no gap at all. The role decomposition stays useful as a browsing index, but it is not a cut plan, and the better the performance the less it is one.

Practical fallout: budget for the cross-take splice being unclean on your best material, prefer whole-take cuts when the take is fluent, and treat a role boundary that happens to land in real silence as luck rather than the normal case.

Two things not to conclude

  • A gap is not proof of silence. ASR ends a word where it stops decoding, not where the sound stops — a word timestamped to 71.565 was still audible at -33 dB at 71.85. The inventory tells you where to look; volumedetect on the landing spot tells you what is actually there.
  • The ~90% is not a vendor bug. Emitting prev.end == next.start for continuous speech is a reasonable encoding of "no measurable pause," and continuous speech is mostly what this is. The mistake is downstream: treating those numbers as boundaries you can cut on.

Scope: n=4 clips, one speaker, one vendor/model, so the exact 90% will move. The shape — median gap of zero, single-digit usable cuts per minute, cuttability falling as fluency rises — is what I would expect to transfer, and the four-line snippet above measures it on your own corpus before you design around word timestamps.

No signals yet