Skip to content

transcribe phase C: transport, orchestration, and the Qwen stacks end to end #21

Description

@fyang0507

Context

Decomposes #12, phase C of
TRANSCRIBE_IMPLEMENTATION_PLAN.md.

The first phase where a model runs. qwen-1.7b goes first because it has the most recorded
evidence, the cheapest runs, and no native anything, so it forces every derived path — five of
the seven roles, and the two exit codes only run can return.

Scope

transport.py, orchestrator.py, the decode, silero, diarizer, qwen and aligner
adapters with their stage scripts, and audio transcribe run for qwen-1.7b and qwen-0.6b.

Transport

One fresh subprocess per stage, chosen per environment, request JSON in and result JSON out, exit
code as the signal, progress on stderr. ENVIRONMENTS.md settles this, and the
recorded end-to-end measurement is what it describes
(model_tests/benchmark/run_interview_pipeline.py:175, "strictly sequential fresh subprocesses").

Environment How a stage runs
core in-process SileroOnnxVad. A declared exception: a 2.3 MB hash-pinned ONNX file in the tool's own environment, already normalized to SpeechRegion, with no model object to leak. Normalization here is by code, not by construction.
mlx <root>/envs/mlx/bin/python <stage script> <request> <result>
swift the built product directly

Stage scripts ship in the wheel, are passed by absolute path, and import no audio_cli
installing it into a provisioned environment would drag onnxruntime and a conflicting numpy into
each one.

Adapters run in core, over the stage's raw JSON. The stage emits the backend's own JSON-safe
shape minus anything unbounded (FluidAudio's 256-float per-segment embeddings are dropped at the
boundary); normalization happens in the tool. That is what lets every adapter test run against the
recorded artifacts with no provisioned environment. The floor is still structural: no
model-specific object can cross a process boundary.

Decode

One canonical render per run — ffmpeg -ac 1 -ar 16000 -c:a pcm_s16le to a temp file every later
stage reads. The source is never modified.

ASR

_generate_chunks_batched for both the diarized and the fixed-chunk case: one declared
api_path, and it is the only path that reports per-unit completion, without which
failure_recovery: per_unit and exit 4 are not implementable. The public generate() chunks
internally and reports nothing per unit. The two paths agree on every word and differ by two
Chinese commas, so the choice is declared rather than hidden.

max_tokens is declared configuration, not a constant nobody notices: it is the exit-4
trigger. The 60-minute stress run consumed 10,169 of 16,384 tokens, so at that rate the budget
exhausts near 1.6 hours of comparable audio.

The language English<asr_text> scaffold is stripped unconditionally via extract_language
(qwen3_asr.py:899). It is only reachable with no language hint: the batched path leaks it at
language=None (run_qwen_verbatim_probe.py:545,553-558) and does not leak it in 0 of 195
segments
with a hint (mlx_qwen3_asr_1.7b_8bit_fluidaudio_turns_spice30m_mix_batch1_clearcache.json),
so a test written against the hinted configuration passes vacuously.

Aligner

Per segment: clip, text, and a language that selects a tokenizer, with bounds re-offset onto the
source timeline. --language does not reach it. qwen3_forced_aligner.py:256-266 branches on
japanese, korean, chinese, and everything else to tokenize_space_lang, while its checkpoint
declares Cantonese as supported — so passing Cantonese through would space-split CJK into one
"word" per segment and silently degenerate word_timestamps, on the exact configuration every
recorded Cantonese figure used. roles.aligner.config.language_rule declares the recorded CJK rule
verbatim (run_mlx_forced_aligner_probe.py:55,94), which is what produced the 246 token-identical
alignments.

Diarizer

fluidaudiocli process --mode offline with the five values the measured runs passed, plus
batch_size, and --overlapping-segments only when overlapped_speech is requested. No
--num-speakers: the offline options are "all optional" and the diarizer estimates. Both cited
figures came from the opposite settings, which the catalog notes now say (phase 0).

The turn reconciliation is deterministic glue, not a role: an exact partition of the timeline, with
the three thresholds from run_turn_attributed_mlx_asr.py:78-84 and the abstention ledger they
produce.

Partial results and resume

Exit 4 writes a conforming document with complete: false and a coverage ledger. --range resumes
against the original input and re-decodes and re-diarizes the whole file, then transcribes only
units intersecting the range. That is not belt-and-braces: three separate runs of the 30-minute
fixture, two weeks apart, produced identical normalized_output_sha256 (3b6639…, 589 segments),
while the same 30 minutes in two positions of a 60-minute concat produced 588 against 549
intervals. Whole-file reproduces the partition and the anonymous labels exactly, so the two results
merge; range-only would not.

Out of scope

firered, vibevoice, export.

Acceptance

  • A real run's provenance key set equals its plan's sample_output key set, parametrized
    over the derivation table, compared against the real embedded plan and not the elided print.
  • Exit 3 before pull, with the missing payload and the pull line as its fix.
  • Exit 4 on a budget forced low enough to truncate, writing a document that satisfies every
    floor, whose covered_intervals and missing_intervals partition the source exactly.
  • A resumed run's turn bounds equal the first run's for the units they share, and its speaker
    labels agree.
  • The scaffold strip is asserted on the no-hint configuration, and the assertion fails when
    the strip is removed.
  • peak_rss_bytes is the maximum of the per-stage peaks; the sum never appears.
  • No "N/A", no 0.0 synthesized bound, no key for an unrequested capability, in real output.
  • A stage double can represent a truncated run and an exhausted budget — a double that cannot
    reach the state under test makes the test pass without the code running, which has happened here
    twice.
  • uv run --extra dev pytest green, and the deterministic render-from-original enhance flow
    is not regressed.

Depends on

  • #20run executes a plan, so the planner comes first.
  • #11 — nothing here runs end to end without audio packages pull.

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