Skip to content

transcribe backend: adapter contract, result schema, stack table, planner, orchestration #12

Description

@fyang0507

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.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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 hash0.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 seam with #11

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions