Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
450 lines
21 KiB
Markdown
450 lines
21 KiB
Markdown
# Confucius4-R2T2
|
|
|
|
Confucius4-R2T2 (Real Real-Time Transcription) is NetEase Youdao's low-latency,
|
|
append-only streaming ASR model. It is a Qwen3-ASR-1.7B fine-tune that keeps the
|
|
same audio tower and thinker graph, and adds a Longest Stable Prefix (LSP)
|
|
decoding algorithm: every chunk re-decodes the whole accumulated audio with the
|
|
previously recognized text as a continuation prompt, and only the stable prefix
|
|
of the new result is committed downstream. Committed text is never revised,
|
|
which is what makes it suitable for live captioning and downstream agents.
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Family | `confucius4_r2t2` |
|
|
| HF checkpoint | `netease-youdao/Confucius4-R2T2` |
|
|
| Task | `asr` |
|
|
| Modes | `offline`, `streaming` |
|
|
| Input | 16 kHz speech WAV (other rates are resampled by the frontend) |
|
|
| Output | Transcript text |
|
|
| Streaming output | Append-only partial text plus the final transcript |
|
|
| Timestamps | Not supported |
|
|
| Context / hotwords | Optional `--text` system prompt |
|
|
|
|
## Independent implementation
|
|
|
|
`confucius4_r2t2` is a **standalone family** (a community port, `status: community` in
|
|
the spec): assets, Whisper log-mel frontend, windowed audio tower, tokenizer, LSP
|
|
session, and text post-processing live under
|
|
`src/community_models/confucius4_r2t2/` + `include/engine/community_models/confucius4_r2t2/` and
|
|
share no code with the `qwen3_asr` family, so the two can evolve independently.
|
|
The loader itself is the framework's **schema-v1 spec-backed loader** —
|
|
`model_specs/confucius4_r2t2.json` is the single source of truth for metadata,
|
|
capabilities, options and packages, and the factory lives next to the session
|
|
(`make_confucius4_r2t2_loader`), so there is no per-model `loader.{h,cpp}`. What it does
|
|
**not** duplicate is framework infrastructure:
|
|
|
|
* the thinker is a thin adapter over the shared greedy Qwen decoder runtime
|
|
(`runtime::GreedyQwenDecoderRuntime`), which owns the prefill/decode graphs,
|
|
the audio-embedding injection, the static KV cache, and greedy sampling;
|
|
* the audio tower builds on the shared modules (`Conv2dModule`, `GeluModule`,
|
|
`LinearModule`, `LayerNormModule`, `ScaledDotProductAttentionModule`) and the
|
|
shared layout/contiguity helper (`core::ensure_backend_addressable_layout`);
|
|
* long-audio chunking uses the shared audio chunk planner
|
|
(`engine::audio::plan_audio_chunks` + `slice_audio_buffer`);
|
|
* multichannel and off-rate input goes through the shared mono conversion and
|
|
resampling helper inside the frontend.
|
|
|
|
The family-specific pieces are:
|
|
|
|
* `session.cpp` — offline transcription plus the LSP streaming state machine,
|
|
* `text_postprocess.cpp` — `normalize_punct_by_context`, repetition repair,
|
|
`language X<asr_text>` parsing, Chinese spacing, and `|` truncation,
|
|
* `assets.cpp` — checkpoint resolution that keeps symlinked weight files
|
|
loadable, including supported quantized GGUF packages (see below),
|
|
* `thinker.cpp` — maps the R2T2 config and `thinker.*` tensor layout onto the
|
|
shared decoder, including the tied LM head the checkpoint ships without.
|
|
|
|
### Rope note
|
|
|
|
The R2T2 config declares interleaved mrope with `mrope_section [24, 20, 20]`, but
|
|
the model only ever sees audio and text, so all three position streams are
|
|
identical and mrope degenerates to standard NEOX RoPE. The shared decoder's
|
|
NEOX rope therefore reproduces the reference numerics, which the golden checks
|
|
in `tests/confucius4_r2t2/` confirm chunk by chunk.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
python3 tools/model_manager_v2.py install confucius4_r2t2_safetensors
|
|
```
|
|
|
|
Or point `--model` at any directory with the HF checkpoint layout (`config.json`,
|
|
`generation_config.json`, `preprocessor_config.json`, `model.safetensors`,
|
|
tokenizer files). Hugging Face cache snapshot directories work directly, even
|
|
though their `model.safetensors` is a symlink into the blob store: the family
|
|
resolves the checkpoint itself instead of going through the shared canonicalized
|
|
tensor path.
|
|
|
|
## Offline transcription
|
|
|
|
```bash
|
|
audiocpp_cli --task asr --family confucius4_r2t2 \
|
|
--model models/Confucius4-R2T2 --backend metal \
|
|
--audio speech_16k.wav --text-out transcript.txt
|
|
```
|
|
|
|
Audio longer than 30 seconds is split into 30-second chunks inside the session
|
|
and the chunk transcripts are joined with a single space. Pass `--language
|
|
Chinese` (or any supported language) to skip language detection, and `--text
|
|
"hotword, another term"` to bias recognition with a context prompt.
|
|
|
|
## Streaming transcription
|
|
|
|
```bash
|
|
audiocpp_cli --task asr --mode streaming --family confucius4_r2t2 \
|
|
--model models/Confucius4-R2T2 --backend metal \
|
|
--audio speech_16k.wav \
|
|
--session-option confucius4_r2t2.chunk_size_ms=320 \
|
|
--text-out transcript.txt
|
|
```
|
|
|
|
In streaming mode committed text appears as it stabilizes and the complete
|
|
transcript is returned when the stream ends. `--audio -` streams raw 16 kHz mono
|
|
PCM from stdin for live sources.
|
|
|
|
Chunk sizes from 80 ms to 2000 ms are supported. 320 ms is a good default on
|
|
Apple Silicon: 160 ms trades accuracy for latency and runs below real time with
|
|
eager Metal execution on smaller machines, while 2000 ms approaches offline
|
|
quality.
|
|
|
|
### Session options (use with `--session-option`)
|
|
|
|
| Option | Values | Default | Meaning |
|
|
|---|---|---:|---|
|
|
| `confucius4_r2t2.chunk_size_ms` | float > 0 | `320` | Streaming chunk duration in milliseconds; must be positive. |
|
|
| `confucius4_r2t2.unfixed_chunk_num` | integer | `2` | Leading chunks decoded without a stable-prefix prompt. |
|
|
| `confucius4_r2t2.unfixed_token_num` | integer | `5` | Tokens rolled back from the accumulated text before it is used as the prefix prompt. |
|
|
| `confucius4_r2t2.rollback_punctuation` | `true`, `false` | `false` | When punctuation is detected, set rollback to zero and retain the full decoded prefix. |
|
|
| `confucius4_r2t2.max_tokens` | integer | `32` | Greedy decode budget per chunk and for the final flush. |
|
|
| `confucius4_r2t2.audio_encoder_weight_type` | `native`, `f32`, `f16` | `native` | Audio tower weight storage. |
|
|
| `confucius4_r2t2.thinker_weight_type` (alias `confucius4_r2t2.weight_type`) | `native`, `f32`, `f16`, `bf16`, `q8_0` | `native` | Thinker weight storage. |
|
|
| `confucius4_r2t2.audio_encoder_graph_arena_mb` | MB | `128` | Audio tower graph arena. |
|
|
| `confucius4_r2t2.thinker_prefill_graph_arena_mb` | MB | `256` | Thinker prefill graph arena. |
|
|
| `confucius4_r2t2.thinker_decode_graph_arena_mb` | MB | `256` | Thinker decode graph arena. |
|
|
| `confucius4_r2t2.thinker_weight_context_mb` | MB | `64` | Thinker weight context. |
|
|
|
|
### Request options (use with `--request-option`)
|
|
|
|
| Option | Values | Default | Meaning |
|
|
|---|---|---:|---|
|
|
| `language` (or `--language`) | language name | empty | Force the recognition language; `Auto` keeps detection. |
|
|
| `max_tokens` (or `--max-tokens`) | integer | see description | Offline decode budget. Must be positive. Offline omission uses checkpoint configuration (512 fallback). |
|
|
|
|
## How the streaming state machine works
|
|
|
|
Each full chunk:
|
|
|
|
1. Appends the chunk to the accumulated audio (nothing is dropped, no padding).
|
|
2. Builds the prompt as `chat template + stable prefix`, where the stable prefix
|
|
is the accumulated transcript minus `unfixed_token_num` tokens. Before
|
|
`unfixed_chunk_num` chunks the prefix is empty. Decoding a prefix never cuts
|
|
a multi-byte token: the rollback grows until the decoded prefix contains no
|
|
U+FFFD replacement character.
|
|
3. Greedy-decodes with a `max_new_tokens` budget on the accumulated audio.
|
|
4. Normalizes punctuation by context, reparses the `language X<asr_text>text`
|
|
output, and re-derives the stable prefix (again minus
|
|
`unfixed_token_num` tokens).
|
|
5. Reports the growth of the stable prefix as committed text.
|
|
|
|
`finish_stream` flushes any tail shorter than one chunk with a fixed rollback,
|
|
then returns the complete transcript.
|
|
|
|
### Incremental compute
|
|
|
|
Re-decoding the whole accumulated audio is the reference algorithm, but not
|
|
the reference cost. Two properties keep each chunk bounded:
|
|
|
|
* The audio tower is block-diagonal per `n_window_infer` attention window
|
|
(4 s), with per-chunk convolutions and positions, so embeddings of completed
|
|
windows never change. They are cached, and only the trailing partial window
|
|
is re-encoded (together with the previous window when the tail is shorter
|
|
than one conv chunk, so the reusable graph is kept).
|
|
* The thinker prompt is `template head, audio tokens, template close, stable
|
|
prefix`. The K/V rows for the head and the cached audio tokens are retained
|
|
in the decode cache between chunks (`GreedyQwenDecoderRuntime::generate_incremental`),
|
|
so only the tail audio, the close, and the stable text prefix are prefilled.
|
|
The cache grows in 512-step buckets; crossing one falls back to a full
|
|
prefill for that chunk.
|
|
|
|
Per-chunk cost is therefore dominated by the stable text prefix and the greedy
|
|
tail decode, not by the utterance length.
|
|
|
|
Peak normalization and the log-mel floor are computed over all accumulated
|
|
audio, so a louder segment rewrites the features of earlier windows. Cached
|
|
windows are therefore reused only while their log-mel features are
|
|
bit-identical to the current ones; otherwise the encoder cache and the thinker
|
|
rows of those audio tokens are dropped and rebuilt, and that chunk costs a full
|
|
re-encode and prefill.
|
|
|
|
### Committed-text contract
|
|
|
|
Committed deltas contain transcript text only. In automatic language mode,
|
|
a rolled-back prefix is not publishable until it contains `<asr_text>`;
|
|
metadata fragments such as `language` neither emit a delta nor advance the
|
|
published offset. Forced-language prompts already supply the metadata, so
|
|
untagged decoded prefixes remain valid transcript text in that mode.
|
|
|
|
After metadata removal, growing stable prefixes emit only the suffix beyond
|
|
the previously published length, counted in Unicode code points. A shrinking
|
|
prefix emits nothing. The uncommitted tail is delivered in the final result
|
|
(`transcript.text.done` on the server), rather than as a final delta.
|
|
|
|
The reference also ships a rolling-window variant for unbounded streams ("no
|
|
reset": keep 16 s of audio, discard the oldest 8 s and the matching text). This
|
|
port implements the standard variant used by the upstream WebSocket server,
|
|
which bounds audio per utterance with VAD. Because the audio tower uses 1500
|
|
positions, a single unsegmented stream is limited to roughly 110 s of
|
|
accumulated audio; segment longer streams (as the reference server does) or add
|
|
the rolling-window variant.
|
|
|
|
### Streaming graph reuse
|
|
|
|
Streaming no longer rebuilds the major inference graphs on every chunk.
|
|
Opt-in via `encode(features, reuse_graph)` and
|
|
`R2T2ASRGenerationOptions::reuse_graphs` (the streaming session enables both;
|
|
the offline path keeps exact per-frame graphs):
|
|
|
|
* The audio encoder builds one graph sized to a capacity bucket (one to two
|
|
chunks exactly, then four-chunk steps) and refills the attention mask for
|
|
the valid token prefix on each run, so a growing stream reuses the graph
|
|
until the next bucket boundary.
|
|
* The thinker prefill runs through the shared Qwen chunked prefill runtime
|
|
(64-token blocks, one reserved graph) and the decode graph keeps a KV-cache
|
|
capacity grown in 128-token buckets up to `max_position_embeddings`; new
|
|
prompts clear KV on device.
|
|
* Prompt token embeddings reuse a fixed-width lookup graph instead of a
|
|
per-request build.
|
|
|
|
The Metal encoder avoids padding an input with fewer than 64 tokens to a
|
|
capacity of 64 tokens or more, and rebuilds on shrink across that boundary.
|
|
The bundled Metal backend switches attention value multiplication to a
|
|
half-input SIMD-group matrix kernel at that size. A larger bucket can therefore
|
|
change arithmetic precision, not just floating-point summation order.
|
|
|
|
Diagnostics on the M3 found identical convolution output, first-layer norm,
|
|
and Q/K/V for the tested inputs; the first difference appeared in attention
|
|
output when this boundary was crossed. Padding is not inherently inexact:
|
|
some padded shapes matched bit for bit. These findings describe the tested
|
|
backend and shapes, not a proof that all padding differences have one cause.
|
|
|
|
`test_confucius4_r2t2_graph_reuse` reads the actual retained graph capacity,
|
|
including after shrink. It checks the Metal precision boundary, unpadded
|
|
output equality, padded repeatability, and a backend-specific
|
|
relative-RMSE drift alarm on the synthetic fixture (2e-3 on Metal under
|
|
the 64-token guard, 2.5e-2 on CPU, where fp32 reduction-order noise is
|
|
larger). That alarm is not a transcript-quality guarantee.
|
|
Joint tests pass exact encoder output through the exact decoder and reused
|
|
encoder output through the reused decoder, requiring identical token IDs for
|
|
real English audio prefixes with automatic and forced-English prompts. They
|
|
also check nonempty output, bucket boundaries, and growth followed by shrink.
|
|
The separate decoder-only test continues to cover injected prompt lengths.
|
|
|
|
For a repeatable local benchmark, use the same checkpoint, backend, thread
|
|
count, and audio on both revisions. Count positive `graph.build_ms` entries
|
|
in `--log-file` output and compare `confucius4_r2t2.session.stream.wall_ms`,
|
|
CLI deltas, final text, and process memory measurements. The bundled English
|
|
sample is 14.0719 seconds (44 decode calls including the final flush at the
|
|
default 320 ms chunk size). A single timing comparison is not a general
|
|
performance guarantee; peak memory footprint and maximum resident set size
|
|
are different measurements.
|
|
|
|
The port still recomputes the accumulated audio and prompt on each chunk;
|
|
incremental encoder/prefill caching is a separate task.
|
|
|
|
## Known issue: final word needs trailing silence
|
|
|
|
When the audio ends right after the last word, the model often drops that
|
|
word. Push-to-talk dictation hits this when the key is released as the word
|
|
ends. Both modes are affected, so it is not a streaming chunk-boundary effect.
|
|
With v0.9.0, Metal, the Q8_0 GGUF and `--language English`, on short
|
|
16 kHz dictations:
|
|
|
|
| Audio | Offline | Streaming (320 ms) |
|
|
|---|---|---|
|
|
| As recorded (0.9 s) | `It seems` | `It seems` |
|
|
| Plus 300 ms of silence | `It seems fixed.` | `It seems fixed.` |
|
|
|
|
On the live server route, another 1.1 s clip gave `Thank you very` and
|
|
`Thank you very much.` with 200 ms or more of silence. This has not been
|
|
compared against the reference implementation yet.
|
|
|
|
Clients that end a stream when the user stops speaking should append about
|
|
300 ms of zero samples before closing the request body (or before EOF on
|
|
`--audio -`).
|
|
|
|
## Server usage
|
|
|
|
`server.local.json` declares `r2t2-asr` (offline) and `r2t2-asr-stream`
|
|
(streaming, 320 ms chunks):
|
|
|
|
```bash
|
|
curl http://127.0.0.1:8488/v1/audio/transcriptions \
|
|
-F model=r2t2-asr -F file=@speech.wav
|
|
```
|
|
|
|
```bash
|
|
# Decoding deltas of an uploaded file
|
|
curl -N http://127.0.0.1:8488/v1/audio/transcriptions \
|
|
-F model=r2t2-asr-stream -F stream=true -F file=@speech.wav
|
|
```
|
|
|
|
```bash
|
|
# Live PCM: deltas appear while the audio is still arriving
|
|
ffmpeg -f avfoundation -i ":0" -ar 16000 -ac 1 -f s16le - \
|
|
| curl -N -X POST -H 'Expect:' -T - \
|
|
'http://127.0.0.1:8488/v1/audio/transcriptions/live?model=r2t2-asr-stream&sample_rate=16000&channels=1&sample_format=s16le'
|
|
```
|
|
|
|
Add `&prompt=<url-encoded hotwords>` to bias the live stream the same way `--text` does on the CLI.
|
|
|
|
## GGUF checkpoints
|
|
|
|
GGUF is supported for `f32`, `f16`, `bf16`, `q8_0` and the k-quants `q2_k`,
|
|
`q4_k` and `q6_k`. `f16` and `q8_0` reproduce the reference transcripts
|
|
exactly; storage types outside that set (legacy `q4_0`, `q5_k`, ...) are
|
|
rejected at load time with an actionable error instead of silently decoding to
|
|
empty text, because this graph's kernels are not validated for them.
|
|
|
|
Verified conversions are published at
|
|
[`davidxifeng/Confucius4-R2T2-gguf`](https://huggingface.co/davidxifeng/Confucius4-R2T2-gguf)
|
|
(a quantized derivative work, distributed under the upstream NetEase Youdao
|
|
model license — see the repository's `LICENSE`, `LICENSE_zh`, and `NOTICE`):
|
|
|
|
| Package id | File | Quantization |
|
|
|---|---|---|
|
|
| `confucius4_r2t2_q8_0` (default) | `r2t2-q8_0.gguf` | Q8_0 |
|
|
| `confucius4_r2t2_f16` | `r2t2-f16.gguf` | F16 |
|
|
|
|
For current file sizes, SHA-256 checksums, runtime compatibility, and published
|
|
checkpoint validation, see the [Hugging Face model card](https://huggingface.co/davidxifeng/Confucius4-R2T2-gguf).
|
|
Release-specific artifact details are maintained there rather than duplicated
|
|
in this document.
|
|
|
|
### Community Q4_K_M quantization
|
|
|
|
A community Q4_K_M quantization by [NairoDorian](https://github.com/NairoDorian)
|
|
is published at
|
|
[`Nairod785/Confucius4-R2T2-Q4_K_M-GGUF`](https://huggingface.co/Nairod785/Confucius4-R2T2-Q4_K_M-GGUF)
|
|
and pinned to a fixed revision. It is a quantized derivative work under the
|
|
same NetEase Youdao model license (`LICENSE`, `LICENSE_zh`, `NOTICE` in that
|
|
repository); `QUANTIZATION.md` there documents the per-tensor recipe and the
|
|
measured speed/quality results.
|
|
|
|
| Package id | File | Quantization |
|
|
|---|---|---|
|
|
| `confucius4_r2t2_q4_k_m` | `r2t2-q4_k_m.gguf` | Q4_K_M (Q4_K with Q6_K MLP-down and Q2_K embeddings; BF16 audio tower) |
|
|
|
|
```bash
|
|
python3 tools/model_manager_v2.py install confucius4_r2t2_q4_k_m
|
|
```
|
|
|
|
```bash
|
|
python3 tools/model_manager_v2.py install confucius4_r2t2_q8_0 # or confucius4_r2t2_f16
|
|
```
|
|
|
|
These files are self-contained: tokenizer, processor/generation config, chat
|
|
template and the model spec are embedded, and the embedded spec carries the
|
|
**schema-v1 option contract**, so option validation comes from the file itself
|
|
rather than from a spec installed beside the runtime (a legacy embedded spec
|
|
triggers a `[warning][model_spec]` fallback instead).
|
|
|
|
### Converting a checkpoint yourself
|
|
|
|
Convert a real (non-symlinked) checkpoint directory:
|
|
|
|
```bash
|
|
audiocpp_gguf \
|
|
--input /path/to/Confucius4-R2T2/model.safetensors \
|
|
--root /path/to/Confucius4-R2T2 \
|
|
--family confucius4_r2t2 \
|
|
--output models/Confucius4-R2T2-GGUF/r2t2-q8_0.gguf \
|
|
--type q8_0
|
|
```
|
|
|
|
The converter resolves sidecars from a real directory, so run it against a
|
|
directory whose files are not symlinks (a plain `cp -r` of the checkpoint, or a
|
|
directory created by the model manager). Direct loading of a symlinked HF cache
|
|
snapshot is supported and does not need this step. The output embeds the
|
|
sidecars and the model spec, so the single `.gguf` is portable:
|
|
|
|
```bash
|
|
audiocpp_cli --task asr --family confucius4_r2t2 \
|
|
--model models/Confucius4-R2T2-GGUF/r2t2-q8_0.gguf --backend metal \
|
|
--audio speech_16k.wav
|
|
```
|
|
|
|
Loading accepts either the `.gguf` file, a directory holding one, or a directory
|
|
holding both formats — a GGUF checkpoint wins when both are present, matching
|
|
the Qwen-family convention.
|
|
|
|
The published checkpoint ties the LM head to the token embedding and therefore
|
|
contains no `lm_head.weight`; the family detects that and reuses the embedding,
|
|
so conversions need no special flags.
|
|
|
|
## Verification
|
|
|
|
The port is verified against the macOS MPS reference with golden traces:
|
|
|
|
```bash
|
|
# 1. Golden from the reference implementation (in the Confucius4-R2T2 repo)
|
|
cd /path/to/Confucius4-R2T2
|
|
PYTHONPATH=. uv run python /path/to/audio.cpp/tests/confucius4_r2t2/make_golden.py \
|
|
--model_path /path/to/audio.cpp/models/Confucius4-R2T2 \
|
|
--audio resources/test.wav \
|
|
--out /path/to/audio.cpp/tests/confucius4_r2t2/golden.json
|
|
|
|
# 2. Compare the C++ runtime (offline text, per-chunk committed text, final text)
|
|
cd /path/to/audio.cpp
|
|
python3 tests/confucius4_r2t2/compare.py \
|
|
--cli build/macos-metal-release/bin/audiocpp_cli \
|
|
--model models/Confucius4-R2T2 \
|
|
--audio /tmp/test.wav \
|
|
--golden tests/confucius4_r2t2/golden.json --backend metal
|
|
```
|
|
|
|
`compare.py` runs the CLI with trace logging, parses the per-chunk trace, and
|
|
diffs it against the golden. A repo-native smoke test runs both paths without
|
|
the Python environment:
|
|
|
|
```bash
|
|
build/macos-metal-release/bin/test_confucius4_r2t2_transcription --backend metal
|
|
```
|
|
|
|
(It skips with exit code 125 when `models/Confucius4-R2T2` or the audio asset is
|
|
absent.)
|
|
|
|
### Results on the reference machine
|
|
|
|
`compare.py` checks four things per golden: the offline transcript, the
|
|
**committed delta stream**, the final streaming transcript, and every per-chunk
|
|
`fixed_text`.
|
|
|
|
| Golden | Offline text | Committed stream | Final transcript | Per-chunk `fixed_text` |
|
|
|---|---|---|---|---|
|
|
| `golden.json` (upstream Chinese `resources/test.wav`) | exact | exact | exact | 21/21 exact |
|
|
| `golden_zh.json` (same audio, `--language Chinese`) | exact | exact | exact | 21/21 exact |
|
|
| `golden_sample16k.json` (`assets/resources/sample_16k.wav`, English) | exact | exact | exact | 41/43 exact |
|
|
| `golden_sample16k_bf16.json` (same audio, bf16 reference) | exact | exact | exact | 41/43 exact |
|
|
|
|
These are the original port's reference-parity results. The regression
|
|
comparison now filters metadata-only prefixes from the unmodified Python
|
|
goldens before checking committed deltas and per-chunk `fixed_text`. The C++
|
|
stream intentionally excludes the reference's `language` artifact; offline
|
|
and final transcript expectations are unchanged.
|
|
|
|
### The two English chunks that differ internally
|
|
|
|
On the 14 s English clip, chunks 37 and 42 differ in `fixed_text` by about one
|
|
token of the trailing numeric span (`22,00` vs `22,0`, and a trailing
|
|
` you.` predicted one chunk earlier). This is greedy-decoding sensitivity, not
|
|
port logic:
|
|
|
|
* both differences appear in the raw decoded text, before any R2T2
|
|
post-processing or rollback runs;
|
|
* the reference disagrees with **itself** at the same order of magnitude — its
|
|
fp16 and bf16 runs differ at chunks 13 and 37;
|
|
* the difference never reaches the wire, because `finish_stream` publishes no
|
|
delta and the final transcript is identical.
|
|
|
|
So per-chunk `fixed_text` equality is exact for the Chinese reference clip and
|
|
stable to within one token for long English audio, with metadata artifacts excluded from committed output.
|