Context
Decomposes #12, phase E of
TRANSCRIBE_IMPLEMENTATION_PLAN.md.
vibevoice is last because it is the slowest to iterate against and the least recoverable. It is
also the stack whose adapter has the two sharpest absence rules and the only truncation story in
the tool.
Scope
The vibevoice stage script and adapter. Native diarization and segment_timestamps, so no
diarizer; word_timestamps still adds the aligner, in mlx, which is why this request spans two
environments and still loads one model at a time.
Two forms of "no speaker", and the docs name one
The floors name Speaker: "N/A". The artifacts show both forms:
model_tests/vibevoice/VibeVoice/result_vibevoice_multispeaker.json — "Speaker": "N/A" on both
[Environmental Sounds] segments, on the 139.284 s clip.
vibe_mps_bf16_logitskeep_spice30m_participant.json — the Speaker key is absent on
[Silence] segments in the model's own raw_text, and "N/A" appears nowhere in any tracked
artifact.
Both are the absence of a label and both become an absent key. Only the first is reachable for a
test, and it is reachable, so the test is not vacuous.
Non-speech event tags
[Environmental Sounds], [Silence], [Human Sounds] arrive as segment text with real bounds
and no words: the aligner is not run on a segment with no speech to align. That is neither a failure
nor an abstention. words is therefore absent on some segments while word_timestamps reports
produced, and observed.segments_without_words records how many.
Truncation loses everything upstream
post_process_transcription finds the closing bracket by counting
(vibevoice/processor/vibevoice_asr_processor.py:514-524, commit 94da20d). On an unterminated
array the count never returns to zero, json_str becomes empty, json.loads raises, and the
handler logs a warning and returns [] (:559-562). So a truncated forty-minute decode yields
nothing through the upstream parser — the transcript is lost, not merely trimmed.
The adapter therefore owns a salvage parse: take complete objects up to the truncation, discard the
trailing partial, and take the coverage watermark from the last complete segment's end. The detector
is hit_max_new_tokens (generated == cap with no EOS), and the observable symptom is an empty
segment list beside a non-empty raw_text.
The cap is reachable on the target use case: the 30-minute run generated 11,345 tokens for 1800 s =
6.30 tok/s, so the recorded 16,384-token cap exhausts near 43 minutes of comparable audio.
That is a rate extrapolation, not an observed truncation, and the catalog says so. max_new_tokens
is declared configuration.
failure_recovery
partial_results becomes prefix_only (added in phase 0, replacing none, which is retired
because vibevoice was its only holder). The note carries what each failure yields: a truncation
leaves a prefix and --range addresses the rest; an out-of-memory at model load leaves nothing. The
field says what the partition permits, not what every failure produces — the same reading per_unit
already has.
Memory
Measured 20.28 GiB live MPS on the 30-minute fixture; a strict 16 GiB cap OOMs at model load on
a 27.8 s probe while an 18 GiB cap passes it. The plan warns from
measured_peak_exceeds_target and does not block — an accepted product decision — and no 16 GiB
claim is made for this route. VibeVoice and the aligner have never been measured co-resident and
this plan does not make them so. Its acoustic tokenizer samples a Gaussian latent, so the seed is
fixed at 1234 and recorded in provenance.
Out of scope
export. The MLX 8-bit alternative is
#13 — adopting it replaces every
recorded VibeVoice figure, so it is a re-measurement decision and not this issue's business.
Acceptance
Depends on
- #21 — the transport and the aligner stage, which this stack reuses unchanged.
- #18 —
prefix_only replaces none there.
Context
Decomposes #12, phase E of
TRANSCRIBE_IMPLEMENTATION_PLAN.md.
vibevoiceis last because it is the slowest to iterate against and the least recoverable. It isalso the stack whose adapter has the two sharpest absence rules and the only truncation story in
the tool.
Scope
The
vibevoicestage script and adapter. Nativediarizationandsegment_timestamps, so nodiarizer;
word_timestampsstill adds the aligner, inmlx, which is why this request spans twoenvironments and still loads one model at a time.
Two forms of "no speaker", and the docs name one
The floors name
Speaker: "N/A". The artifacts show both forms:model_tests/vibevoice/VibeVoice/result_vibevoice_multispeaker.json—"Speaker": "N/A"on both[Environmental Sounds]segments, on the 139.284 s clip.vibe_mps_bf16_logitskeep_spice30m_participant.json— theSpeakerkey is absent on[Silence]segments in the model's ownraw_text, and"N/A"appears nowhere in any trackedartifact.
Both are the absence of a label and both become an absent key. Only the first is reachable for a
test, and it is reachable, so the test is not vacuous.
Non-speech event tags
[Environmental Sounds],[Silence],[Human Sounds]arrive as segmenttextwith real boundsand no words: the aligner is not run on a segment with no speech to align. That is neither a failure
nor an abstention.
wordsis therefore absent on some segments whileword_timestampsreportsproduced, andobserved.segments_without_wordsrecords how many.Truncation loses everything upstream
post_process_transcriptionfinds the closing bracket by counting(
vibevoice/processor/vibevoice_asr_processor.py:514-524, commit94da20d). On an unterminatedarray the count never returns to zero,
json_strbecomes empty,json.loadsraises, and thehandler logs a warning and returns
[](:559-562). So a truncated forty-minute decode yieldsnothing through the upstream parser — the transcript is lost, not merely trimmed.
The adapter therefore owns a salvage parse: take complete objects up to the truncation, discard the
trailing partial, and take the coverage watermark from the last complete segment's end. The detector
is
hit_max_new_tokens(generated == cap with no EOS), and the observable symptom is an emptysegment list beside a non-empty
raw_text.The cap is reachable on the target use case: the 30-minute run generated 11,345 tokens for 1800 s =
6.30 tok/s, so the recorded 16,384-token cap exhausts near 43 minutes of comparable audio.
That is a rate extrapolation, not an observed truncation, and the catalog says so.
max_new_tokensis declared configuration.
failure_recoverypartial_resultsbecomesprefix_only(added in phase 0, replacingnone, which is retiredbecause
vibevoicewas its only holder). The note carries what each failure yields: a truncationleaves a prefix and
--rangeaddresses the rest; an out-of-memory at model load leaves nothing. Thefield says what the partition permits, not what every failure produces — the same reading
per_unitalready has.
Memory
Measured 20.28 GiB live MPS on the 30-minute fixture; a strict 16 GiB cap OOMs at model load on
a 27.8 s probe while an 18 GiB cap passes it. The plan warns from
measured_peak_exceeds_targetand does not block — an accepted product decision — and no 16 GiBclaim is made for this route. VibeVoice and the aligner have never been measured co-resident and
this plan does not make them so. Its acoustic tokenizer samples a Gaussian latent, so the seed is
fixed at 1234 and recorded in provenance.
Out of scope
export. The MLX 8-bit alternative is#13 — adopting it replaces every
recorded VibeVoice figure, so it is a re-measurement decision and not this issue's business.
Acceptance
Speaker: "N/A"produces nospeakerkey, and the test fails ifthat string reaches the document — asserted anywhere a speaker label can appear, including turns.
Speakerkey produces the same absent key, from the tracked 30-minute artifact.observed.segments_without_wordscounts the event-tag segments whileword_timestampsreports
produced.raw_texttruncated at an arbitrary offset yields every complete segment before thecut, exit 4, and a watermark at the last complete segment's end — and yields
[]throughpost_process_transcription, which is the reason the salvage exists. Both halves asserted.diarizationandsegment_timestampsresolvenativewith no diarizer role and no add-onpackage;
--diarizer fluidaudiois refused withpin_conflicts_with_native_capability.uv run --extra dev pytestgreen.Depends on
prefix_onlyreplacesnonethere.