You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TRANSCRIBE_DESIGN_HANDOFF.md opens the design work as seven
items. This issue takes the backend half — handoff open items 1 (module and adapter
boundaries), 2 (stage orchestration), 4 (partial results and resume), 6 (word and
segment identity), and 7 (where the stack table lives). #11 takes provisioning and the
environment layout; the cue segmenter stays parked in #10.
The agent-facing surface is settled and signed off. VOCABULARY.md is the
naming contract, TRANSCRIBE_CONTRACT.md is the specification, TRANSCRIBE_HAPPY_PATH.md is the diff target, and tests/test_spec_docs.py already enforces the documents' internal consistency — it is also a
worked list of the invariants this implementation has to satisfy. Nothing underneath is built: src/audio_cli/ has no abstraction for a backend, and vad.py is the only backend-shaped
file in it, written before the vocabulary existed.
Design questions this issue answers
Where the stack table lives. Capability availability, cost, and notes are prose in two
documents today. They have to become data, and that data is what capabilities
serialises. VOCABULARY §Resolution's derivation table has exactly four legal cell forms — native, native (<stage>), + <package>, exit 2: <code> — and a test parametrized
over it must accept those four and keep the two refusal codes distinct. The handoff calls
this the first real design decision.
The normalized result schema and the adapter contract. One hard constraint: no
model-specific object travels past the adapter. Everything else is open. Build the
anti-fabrication assertions with the schema, not after — this is where fabrication becomes
possible.
The planner. Ideally a pure function from (stack, input metadata, requirements) to a
plan, with capabilities and plan as two serialisations of it. sample_output is
generated by running a placeholder result through the same serializer run uses, never
hand-written.
Stage orchestration. Strictly sequential, one model resident at a time. Enforcing that
across environments needs the transport environments and packages: provisioning layout, locks, registry, and audio packages #11 defines; the ordering, the residency
guarantee, and the observed-cost accounting are here. Recorded figures came from fresh
subprocesses — in-process loading is outside what was measured.
Partial results and resume. Exit 4 writes a conforming result with a coverage ledger: covered_through_seconds, plus explicit covered and missing intervals. The recorded Qwen
runner processes turns in duration-bucketed order, so completion is genuinely
non-contiguous and the watermark is not "the last thing that finished". --range resumes
against the original input; the canonical-timeline floor is why it is a range and not a
pre-clipped file.
Word and segment identity. Document-scoped, explicitly not stable across runs. If the
Observation Store later needs run-stable identity it cannot key on a timestamp — FireRed
reproduces timestamps only to 1 ms.
Suggested order
From the handoff, risk earliest: schema and adapter contract → stack table and planner → qwen-1.7b behind an adapter (most recorded evidence, cheapest runs, no native anything, so
it forces the derived paths first) → firered (exercises vad, punctuator, lid, and the
punctuation invariant) → vibevoice last (slowest to iterate, least recoverable) → export.
Nothing here can be exercised end to end before #11's pull works.
Risks that shape the design
vibevoice cannot resume. Whole media, one generate call, so a failure at minute 40
of 41 yields nothing. It is also the most likely to fail: 20.28 GiB live MPS on thirty
minutes, OOM under a strict 16 GiB cap. failure_recovery.partial_results is none there.
Do not design a recovery story that quietly assumes partitioning.
A long Qwen run truncates silently today. The recorded runner carries a global
generation budget and stops between turns when it runs out; at the recorded token rate it
exhausts near 1.6 hours of comparable audio. Every recorded run finished, so this has
never been observed — exit 4 exists for it. Make the budget declared configuration, not a
constant nobody notices.
Two Qwen decode paths disagree. Public generate() and private _generate_chunks_batched agree on every word and differ by two Chinese commas. The
private path is the one that batches turns and the one the timing figures came from, and it
returns the model's language English<asr_text> scaffold inside its text, which the adapter
must strip. api_path is in the plan for this reason. Lexical evidence transfers between
the paths; punctuation does not, and punctuation is what cue splitting breaks on.
The mlx-audio private API is pinned by source hash — 0.4.5, sha256 c082690575eedcd28fb76207d032cefd7eac2f9ce5d36df5a7a06575bc45d250. An upgrade is a
breaking change, not a bump.
16 GiB is not validated. RSS, MLX, PyTorch, and Core ML counters have different scopes
and must not be summed. Ship the product-demo route and warn from the plan; do not turn
that into a claim.
Unmeasured — design around the gap
Boundary MAE/P95 for FireRed's native word times and for the forced aligner (so "switch
stacks for better timing" is unsupported in either direction); filler recall on any stack;
cross-process determinism for the Qwen batched path; package byte sizes beyond the two Qwen
checkpoints and the Core ML model; licenses beyond FluidAudio's SDK and speaker-diarization-coreml; any accuracy figure outside Cantonese. Where a decision depends
on one of these, say so rather than assuming a value.
Acceptance
Against real command output, not mocks:
Sample-output key set: a plan's sample_output key set equals a real run's, and no key
exists for a capability that was not requested. Parametrized over VOCABULARY's derivation
table, including both refusal codes.
Punctuation invariant per stack: a sentence's text, stripped of punctuation and
whitespace, equals the concatenation of its word texts, compared case-insensitively
(FireRedPunc recases; 234 characters differ across the recorded artifacts). Skip segments
with no word stream rather than asserting they have one — the forced-aligner artifact has
two, both VibeVoice non-speech event tags.
Adapter normalization per stack, one case each: Qwen's language <label><asr_text>
scaffold is stripped; FireRed's lang: null, lang_confidence: 0 are dropped when LID did
not run; VibeVoice's Speaker: "N/A" becomes an absent key, never a speaker id.
A plan carries no outcomes; a run's provenance does.
Exit codes end to end, including 3 before pull, 4 on a truncated run, and that a partial
result is a conforming document rather than a debug dump.
uv run --extra dev pytest stays green; the deterministic render-from-original enhance
flow is not regressed.
The interface is (stack, requirements) → [package]. This issue produces it — capability
availability, satisfaction, evidence, notes, and the planner that reads them. #11 consumes it and owns the
package/environment columns: package id, environment, kind, revision, digest, bytes, license, requires_tool. One table, two owners. If #11 ships a stubbed version of this function, it
must be replaced rather than paralleled.
Note #11's open experiment: whether vibevoice-asr-7b, qwen3-forcedaligner, and firered-asr2s share one torch environment is unverified, and the as-built evidence
suggests they may not. If torch splits, the number of environments a request spans changes,
and so does what a plan reports.
Traps
A claim about what a backend produces must cite a runner source or a recorded artifact,
never a summary document. Three research documents said FireRed emits per-word
confidence. It does not — every word object is exactly {start_ms, end_ms, text} across
12,370 recorded tokens — and the claim survived four review passes because every summary
repeated it. Artifacts: model_tests/benchmark_runs/ (untracked, local), model_tests/benchmark/results/ (tracked), runners in model_tests/benchmark/.
Do not add structure nobody dispatches on. The capabilities report went through an evidence object, a seven-key cost object, a timing_precision object holding one null
and one string, and four sibling free-text fields before all of it collapsed into one
sentence per capability. Every one seemed justified when added. The test is whether a caller
branches on it.
Do not describe machinery that does not exist. The punctuation floor spent four review
passes instructing the adapter to reattach marks and drop their bounds — for a stage that
emits no per-mark bounds at all. The rule was inert and its test passed vacuously. When you
write an invariant, write the assertion that would fail if it were violated, and check that
it can fail.
Context
TRANSCRIBE_DESIGN_HANDOFF.md opens the design work as seven
items. This issue takes the backend half — handoff open items 1 (module and adapter
boundaries), 2 (stage orchestration), 4 (partial results and resume), 6 (word and
segment identity), and 7 (where the stack table lives).
#11 takes provisioning and the
environment layout; the cue segmenter stays parked in
#10.
The agent-facing surface is settled and signed off. VOCABULARY.md is the
naming contract, TRANSCRIBE_CONTRACT.md is the specification,
TRANSCRIBE_HAPPY_PATH.md is the diff target, and
tests/test_spec_docs.pyalready enforces the documents' internal consistency — it is also aworked list of the invariants this implementation has to satisfy. Nothing underneath is built:
src/audio_cli/has no abstraction for a backend, andvad.pyis the only backend-shapedfile in it, written before the vocabulary existed.
Design questions this issue answers
documents today. They have to become data, and that data is what
capabilitiesserialises. VOCABULARY §Resolution's derivation table has exactly four legal cell forms —
native,native (<stage>),+ <package>,exit 2: <code>— and a test parametrizedover it must accept those four and keep the two refusal codes distinct. The handoff calls
this the first real design decision.
model-specific object travels past the adapter. Everything else is open. Build the
anti-fabrication assertions with the schema, not after — this is where fabrication becomes
possible.
plan, with
capabilitiesandplanas two serialisations of it.sample_outputisgenerated by running a placeholder result through the same serializer
runuses, neverhand-written.
across environments needs the transport environments and packages: provisioning layout, locks, registry, and
audio packages#11 defines; the ordering, the residencyguarantee, and the observed-cost accounting are here. Recorded figures came from fresh
subprocesses — in-process loading is outside what was measured.
covered_through_seconds, plus explicit covered and missing intervals. The recorded Qwenrunner processes turns in duration-bucketed order, so completion is genuinely
non-contiguous and the watermark is not "the last thing that finished".
--rangeresumesagainst the original input; the canonical-timeline floor is why it is a range and not a
pre-clipped file.
Observation Store later needs run-stable identity it cannot key on a timestamp — FireRed
reproduces timestamps only to 1 ms.
Suggested order
From the handoff, risk earliest: schema and adapter contract → stack table and planner →
qwen-1.7bbehind an adapter (most recorded evidence, cheapest runs, no native anything, soit forces the derived paths first) →
firered(exercisesvad,punctuator,lid, and thepunctuation invariant) →
vibevoicelast (slowest to iterate, least recoverable) →export.Nothing here can be exercised end to end before #11's
pullworks.Risks that shape the design
vibevoicecannot resume. Whole media, onegeneratecall, so a failure at minute 40of 41 yields nothing. It is also the most likely to fail: 20.28 GiB live MPS on thirty
minutes, OOM under a strict 16 GiB cap.
failure_recovery.partial_resultsisnonethere.Do not design a recovery story that quietly assumes partitioning.
generation budget and stops between turns when it runs out; at the recorded token rate it
exhausts near 1.6 hours of comparable audio. Every recorded run finished, so this has
never been observed — exit 4 exists for it. Make the budget declared configuration, not a
constant nobody notices.
generate()and private_generate_chunks_batchedagree on every word and differ by two Chinese commas. Theprivate path is the one that batches turns and the one the timing figures came from, and it
returns the model's
language English<asr_text>scaffold inside its text, which the adaptermust strip.
api_pathis in the plan for this reason. Lexical evidence transfers betweenthe paths; punctuation does not, and punctuation is what cue splitting breaks on.
mlx-audioprivate API is pinned by source hash —0.4.5, sha256c082690575eedcd28fb76207d032cefd7eac2f9ce5d36df5a7a06575bc45d250. An upgrade is abreaking change, not a bump.
and must not be summed. Ship the product-demo route and warn from the plan; do not turn
that into a claim.
Unmeasured — design around the gap
Boundary MAE/P95 for FireRed's native word times and for the forced aligner (so "switch
stacks for better timing" is unsupported in either direction); filler recall on any stack;
cross-process determinism for the Qwen batched path; package byte sizes beyond the two Qwen
checkpoints and the Core ML model; licenses beyond FluidAudio's SDK and
speaker-diarization-coreml; any accuracy figure outside Cantonese. Where a decision dependson one of these, say so rather than assuming a value.
Acceptance
Against real command output, not mocks:
sample_outputkey set equals a real run's, and no keyexists for a capability that was not requested. Parametrized over VOCABULARY's derivation
table, including both refusal codes.
whitespace, equals the concatenation of its word texts, compared case-insensitively
(FireRedPunc recases; 234 characters differ across the recorded artifacts). Skip segments
with no word stream rather than asserting they have one — the forced-aligner artifact has
two, both VibeVoice non-speech event tags.
language <label><asr_text>scaffold is stripped; FireRed's
lang: null, lang_confidence: 0are dropped when LID didnot run; VibeVoice's
Speaker: "N/A"becomes an absent key, never a speaker id.outcomes; a run's provenance does.pull, 4 on a truncated run, and that a partialresult is a conforming document rather than a debug dump.
uv run --extra dev pyteststays green; the deterministic render-from-originalenhanceflow is not regressed.
The seam with #11
The interface is
(stack, requirements) → [package]. This issue produces it — capabilityavailability, satisfaction, evidence, notes, and the planner that reads them.
#11 consumes it and owns the
package/environment columns: package id, environment, kind, revision, digest, bytes, license,
requires_tool. One table, two owners. If #11 ships a stubbed version of this function, itmust be replaced rather than paralleled.
Note #11's open experiment: whether
vibevoice-asr-7b,qwen3-forcedaligner, andfirered-asr2sshare onetorchenvironment is unverified, and the as-built evidencesuggests they may not. If
torchsplits, the number of environments a request spans changes,and so does what a plan reports.
Traps
never a summary document. Three research documents said FireRed emits per-word
confidence. It does not — every word object is exactly
{start_ms, end_ms, text}across12,370 recorded tokens — and the claim survived four review passes because every summary
repeated it. Artifacts:
model_tests/benchmark_runs/(untracked, local),model_tests/benchmark/results/(tracked), runners inmodel_tests/benchmark/.evidenceobject, a seven-keycostobject, atiming_precisionobject holding one nulland one string, and four sibling free-text fields before all of it collapsed into one
sentence per capability. Every one seemed justified when added. The test is whether a caller
branches on it.
passes instructing the adapter to reattach marks and drop their bounds — for a stage that
emits no per-mark bounds at all. The rule was inert and its test passed vacuously. When you
write an invariant, write the assertion that would fail if it were violated, and check that
it can fail.