Skip to content

Place cuts with Silero rather than frame energy - #13

Merged
bojieli merged 3 commits into
mainfrom
silero-streaming-boundaries
Aug 25, 2026
Merged

Place cuts with Silero rather than frame energy#13
bojieli merged 3 commits into
mainfrom
silero-streaming-boundaries

Conversation

@bojieli

@bojieli bojieli commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Answering "why did you not ship this?" — the blocker was real but solvable, and this solves it.

Why energy boundaries were worth replacing

Silero has been loaded and run on every recording since it replaced the noise-floor heuristic, but only ever as a yes/no gate. Where to cut was still frame energy against a 2nd-percentile floor, which fragments a long pause the moment a breath or keyboard tap crosses it:

pause ≥ energy Silero
1.00 s 686 847
2.00 s 226 451

Measured on the 60 retained recordings past the splitting threshold:

energy Silero
median pause a cut lands in 0.76 s 2.14 s
cuts landing in a pause ≥ 1 s 40% 77%
final chunk, p50 17.9 s 27.7 s

The Swift implementation reproduces the offline prediction exactly — median 2.14 s across 54 cuts on the real corpus.

The longer final chunk is near-free on the offline path: request latency barely tracks chunk length (60 s → 2.43 s, 120 s → 2.98 s).

Why it wasn't done before

The live segmenter asks for a boundary every 200 ms once a minute is pending. Re-running the model over the pending buffer costs ~0.19 s of CPU per call at that size — roughly a whole core, sustained, for as long as no qualifying pause appears.

Silero is recurrent and built to stream, so the state and 64-sample context are now carried across calls and only new samples are analysed:

streaming: 300 s of audio in 1.62 s (185x real time)

fed in the 85 ms pieces the recorder actually delivers. A test holds that bound, to catch a regression back to re-scanning.

Two edges that are load-bearing

A still-open speech run counts when finding gaps. During capture the newest gap — the one worth cutting at — is always followed by a run that has not ended. Only its start is used, and Silero does not report a start until 250 ms of speech confirms it, which is exactly the evidence that the pause is over. I had this wrong first: excluding the open run made the newest pause invisible and the segmenter never cut. The live-session test caught it.

Fewer than two finalised runs hands the decision back to the energy finder, which needs no model and cannot fail. Audio Silero cannot parse still gets split rather than growing without bound — which matters more now, since an unbounded chunk is exactly the long-request case that truncates (#10).

Tests

SpeechStreamTests covers the risky part, the recurrent carry, on real speech:

  • identical segments at 85 ms, 1 s and 4 s arrivals — arrival size cannot change the answer
  • deliberately unaligned sizes (999/1237 bytes vs the 1024-byte window), so a dropped partial window is caught
  • an open run is not finalised
  • pause midpoints never land inside a speech run
  • the throughput bound above

530 Swift tests, zero failures.

Scope

Swift only, so macOS and iOS. Android keeps energy boundaries — unchanged behaviour, no regression — and porting it needs the same streaming treatment. Windows has no live session. Worth a follow-up rather than bundling here.

Supersedes the "measured, not yet shipped" note in #12; that line needs updating once both land.

🤖 Generated with Claude Code

https://claude.ai/code/session_015XKX6zEAGiZFE5wAxJgYbV

bojieli and others added 3 commits August 25, 2026 17:30
Silero has been loaded and run on every recording since it replaced the
noise-floor heuristic, but only ever as a yes/no gate on whether a chunk
contains speech. Where to cut was still decided by frame energy against a
2nd-percentile floor, which fragments a long pause the moment a breath or a
keyboard tap crosses it: 226 pauses of two seconds or more across 147 real
recordings, against 451 that Silero finds in the same audio.

Taking the gaps between finalised speech runs instead, measured on the 60
retained recordings past the splitting threshold: the median pause a cut
lands in goes from 0.76 s to 2.14 s and cuts landing in a pause of a second
or more from 40% to 77%. The Swift implementation reproduces the offline
prediction exactly — median 2.14 s on 54 cuts across the corpus.

The cost is a longer final chunk, p50 17.9 s to 27.7 s. On the offline path
that is close to free, because request latency barely tracks chunk length:
60 s costs 2.43 s and 120 s costs 2.98 s.

The reason this was not done when Silero arrived is that the live segmenter
asks for a boundary every 200 ms once a minute of audio is pending, and
re-running the model over the pending buffer costs about 0.19 s of CPU per
call at that size — roughly a whole core, sustained, for as long as no
qualifying pause appears. Silero is recurrent and designed to stream, so
the state and the 64-sample context are now carried across calls and only
new samples are analysed: 300 s of audio in 1.62 s, fed in the 85 ms pieces
the recorder actually delivers, and a test holds that bound.

Two edges are load-bearing. A run still open when the boundary is wanted
counts, because during capture the newest gap is always followed by one —
only its start is used, and Silero does not report a start until 250 ms of
speech confirms it, which is exactly the evidence the pause has ended.
Excluding it made the newest pause invisible and the segmenter never cut.
And fewer than two runs hands the decision back to the energy finder, which
needs no model and cannot fail, so audio Silero cannot parse still gets
split rather than growing without bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XKX6zEAGiZFE5wAxJgYbV
…daries

# Conflicts:
#	Sources/DoNotTypeCore/AudioChunker.swift
@bojieli
bojieli merged commit e6ee547 into main Aug 25, 2026
13 checks passed
@bojieli
bojieli deleted the silero-streaming-boundaries branch August 25, 2026 10:37
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