Skip to content

Stop the boundary scorer's distance penalty overruling a better pause - #11

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

Stop the boundary scorer's distance penalty overruling a better pause#11
bojieli merged 4 commits into
mainfrom
silero-boundaries

Conversation

@bojieli

@bojieli bojieli commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The scorer bug

boundaryScore subtracted the raw distance from the target:

preferredBonus + min(2, duration) * 4 + min(20, depth) / 10 - abs(seconds - target)

That term is linear, unbounded, and in the same units as nothing else in the score, so it dominated every quality term. On the shipping policy a clean 1.3 s sentence break ten seconds early scored below a 0.4 s breath sitting on the target — and the splitter took the breath.

Normalising by the width of the acceptable window fixes the units. Distance still breaks ties, so chunks stay evenly sized, but it can no longer overrule a much better pause inside the range the policy already called acceptable.

Measured over the 60 retained recordings past the splitting threshold:

today with the fix
median pause a cut lands in 0.76 s 1.32 s
cuts landing in a pause ≥ 1 s 40% 60%
chunks per recording 3.1 3.2
final chunk, p50 17.9 s 17.0 s

No trade to tune — better seams, same chunk count, slightly shorter final chunk. The old form was wrong rather than differently weighted.

The worse bug the test found

C# record structs ignore their primary constructor's defaults for new() and zero-initialise instead:

new P()         -> min=0 target=0 horizon=0
new P(45,60,75) -> min=45 target=60 horizon=75

AudioChunker.DefaultPolicy = new() therefore meant the Windows core ran with every field zero: no minimum chunk length, a zero target, a zero horizon that emptied the preferred set on every call, and a minimum pause of zero that made a single 20 ms dip a legal cut. That client has been splitting long recordings at the first quiet frame while Swift and Kotlin aimed at 60 seconds.

Every existing test passed. They assert that cuts land in silence and that no audio is lost — both stay true when the chunks are tiny. Fixed with an explicit parameterless constructor, and the policy is now pinned field-for-field in all three cores so it cannot drift silently again.

BoundaryPolicy is the only affected type; TokenUsage is a record class, where new() does apply defaults.

Tests

New behavioural tests go through bestBoundary rather than the private scorer, because the outcome is the contract and the formula is not. Verified they actually discriminate: reverting to the old form fails exactly the one test that captures the change, and passes the three invariants (tie-breaking, eligibility before the minimum, no-cut-without-a-pause) — which is what those are for.

One fixture note worth keeping: a synthetic clip whose only quiet is the pause under test estimates its 2nd-percentile floor from speech itself and then finds no speech at all. Real dictation is 39–86% pause, so the fixtures include ordinary background pauses.

529 Swift, 457 C#, zero failures. Kotlin runs in CI.

Not in this PR

Using Silero for boundary placement (rather than only as a speech gate) measures better again — median cut pause 2.14 s and 77% landing in a pause ≥ 1 s — but it needs incremental streaming state to avoid re-running the model over the same buffer every 200 ms in the live path. Separate change.

🤖 Generated with Claude Code

https://claude.ai/code/session_015XKX6zEAGiZFE5wAxJgYbV

bojieli and others added 4 commits August 25, 2026 17:04
`boundaryScore` subtracted the raw distance from the target. Linear,
unbounded, and in the same units as nothing else in the score, so it
dominated every quality term: on the shipping policy a clean 1.3 s sentence
break ten seconds early scored below a 0.4 s breath sitting on the target,
and the splitter took the breath.

Normalising by the width of the acceptable window fixes the units.
Distance still breaks ties, so chunks stay evenly sized, but it can no
longer overrule a much better pause inside the range the policy already
called acceptable.

Measured over the 60 retained recordings past the splitting threshold: the
median pause a cut lands in goes from 0.76 s to 1.32 s and cuts landing in
a pause of a second or more from 40% to 60%, with the same number of chunks
and a slightly shorter final chunk. There is nothing to tune here — the old
form was wrong rather than differently weighted.

Writing the test found a second, worse bug. A record *struct* ignores its
primary constructor's defaults for `new()` and zero-initialises instead, so
the C# `DefaultPolicy` was every field zero: no minimum chunk length, a
zero target, a zero horizon that emptied the preferred set on every call,
and a minimum pause of zero that made a single 20 ms dip a legal cut. That
client has been splitting long recordings at the first quiet frame while
Swift and Kotlin aimed at 60 seconds. Every existing test passed, because
they assert that cuts land in silence and that no audio is lost, and both
stay true when the chunks are tiny. Fixed with an explicit parameterless
constructor and pinned in all three cores.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XKX6zEAGiZFE5wAxJgYbV
The same mistake as the two files fixed in #18, caught before merging this
time: kotlin.test is not on this project's test classpath. JUnit4 also
wants a delta on a double comparison and puts the message first, so those
are corrected too. Verified with ./gradlew --offline :app:testDebugUnitTest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XKX6zEAGiZFE5wAxJgYbV
@bojieli
bojieli merged commit d4a22b0 into main Aug 25, 2026
12 checks passed
@bojieli
bojieli deleted the silero-boundaries branch August 25, 2026 10:12
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