Renumber spec 001 decision placeholders to D-040–D-051; record the Danish publication

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 01:47:27 +01:00
co-authored by Claude Opus 5.5
parent 30969e86b7
commit 0632f7843b
71 changed files with 244 additions and 236 deletions
+3 -3
View File
@@ -40,12 +40,12 @@ step with the commands above, in the same commit.
`Bun.$`. Bun API docs: `node_modules/bun-types/docs/`.
- **Two engines, two libraries (D-013).** Browser inference = Transformers.js, inside
`src/worker/inference.worker.ts` only (transcript translation: `src/worker/translate.worker.ts`,
D-WS4b) — never on the main thread. Server inference = faster-whisper
D-045) — never on the main thread. Server inference = faster-whisper
via `server-py/sidecar.py` — never Transformers.js on the server. The one exception is Parakeet
(`family: "parakeet"`, D-WS7a): parakeet.js in the same worker, on the **same** onnxruntime-web
(`family: "parakeet"`, D-051): parakeet.js in the same worker, on the **same** onnxruntime-web
instance — never call its `fromUrls`/`fromHub`/`initOrt` (own ort copy, CDN wasm path).
- **`@huggingface/transformers` is patched and a devDependency on purpose.** It is pinned by
`patches/@huggingface%[email protected]` (D-027/D-028, D-WS1a) and kept out of runtime deps so the
`patches/@huggingface%[email protected]` (D-027/D-028, D-040) and kept out of runtime deps so the
Docker image stays small (D-031). Upgrading or moving it means redoing the patch: ask first.
- **No cross-origin subresources.** Every response carries COOP/COEP `require-corp` (D-023,
`src/index.ts`); a CDN script/style/font will be blocked. Self-host it (onnxruntime is served from `/ort/`).
+1 -1
View File
@@ -13,7 +13,7 @@ On desktop browsers the on-device engine also offers **NVIDIA Parakeet TDT 0.6B
same onnxruntime-web. Model: [nvidia/parakeet-tdt-0.6b-v3](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3),
© NVIDIA, licensed [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/); ONNX
conversion [ysdede/parakeet-tdt-0.6b-v3-onnx](https://huggingface.co/ysdede/parakeet-tdt-0.6b-v3-onnx),
downloaded unmodified by the browser on first use (Decisions.md D-WS7a).
downloaded unmodified by the browser on first use (Decisions.md D-051).
See `docs/Architecture.md` for the design, `docs/Decisions.md` for the decision
log, and `PRODUCT.md` for the product spec.
+20 -20
View File
@@ -27,7 +27,7 @@ Logged-in users may optionally route inference to the server.
│ ├─ Device selection: WebNN → WebGPU → WASM fallback │
│ └─ Model weights cached via browser Cache API │
│ │
│ Translate Web Worker (D-WS4b) │
│ Translate Web Worker (D-045) │
│ └─ Transformers.js Opus-MT (MarianMT), WASM, on user request │
│ │
│ Service Worker (PWA) │
@@ -76,19 +76,19 @@ src/
parakeet.ts # pure Parakeet logic: ≤30 s window plan, words→chunks, vocab tokenizer, pinned URLs
modelCache.ts # "downloaded?" per model via Cache Storage (D-005; reused by M3 offline indicator)
engine.ts # engine abstraction: BrowserEngine | ServerEngine
translation.ts # transcript translation planning: Opus-MT catalog, units, sentence pieces (pure, D-WS4b)
hint.ts # "Names and terms" hint: normalize, localStorage, prompt truncation rule (D-WS2a)
translation.ts # transcript translation planning: Opus-MT catalog, units, sentence pieces (pure, D-045)
hint.ts # "Names and terms" hint: normalize, localStorage, prompt truncation rule (D-048)
worker/
inference.worker.ts # Transformers.js pipeline, WebNN/WebGPU/WASM selection; dispatches on catalog `family`
parakeetEngine.ts # Parakeet TDT path: parakeet.js ParakeetModel on the shared onnxruntime-web (D-WS7a)
parakeetEngine.ts # Parakeet TDT path: parakeet.js ParakeetModel on the shared onnxruntime-web (D-051)
modelFiles.ts # Cache-API weight download for non-Transformers.js paths (same cache, same progress msgs)
ortShim.ts # re-exports onnxruntime-web/webgpu; parakeet.js's own ort import is aliased here
protocol.ts # typed postMessage messages (shared with UI)
prompt.ts # decoder prompt (<|startofprev|> hint) via a generate() wrapper (D-WS2a)
translate.worker.ts # transcript translation (Opus-MT), its own worker + ort instance (D-WS4b)
prompt.ts # decoder prompt (<|startofprev|> hint) via a generate() wrapper (D-048)
translate.worker.ts # transcript translation (Opus-MT), its own worker + ort instance (D-045)
translateProtocol.ts # typed messages for the translate worker
opusMt.ts # Opus-MT loader shared by the worker, bench script and real-model test
longform/ # long-form file pipeline (D-WS3a): pure vad.ts / quality.ts / pipeline.ts
longform/ # long-form file pipeline (D-049): pure vad.ts / quality.ts / pipeline.ts
# behind transcribeLongForm(audio, deps, options); transformers.ts implements
# the deps (Silero VAD + Whisper decodes) on the same Transformers.js runtime
pwa/
@@ -158,17 +158,17 @@ environment). See `WorkBreakdown.md` for the itemized checklist.)
`AudioContext.decodeAudioData`, downmixes to mono, resamples to 16 kHz,
transfers the `Float32Array` to the worker. For multilingual models the
`transcribe` message also carries `language` (or auto-detect) and `task`
(transcribe | translate — translate only for models with `canTranslate`, D-WS4a).
(transcribe | translate — translate only for models with `canTranslate`, D-044).
For every model it may carry `prompt`: the optional "Names and terms" hint
(`VocabularyHint`, stored in localStorage by `lib/hint.ts`), raw as typed (D-WS2a).
(`VocabularyHint`, stored in localStorage by `lib/hint.ts`), raw as typed (D-048).
5. Worker runs the pipeline (chunked, with timestamps), streaming partial results
back via `{type:"partial"}` messages; final result as `{type:"result"}`.
A hint becomes `<|startofprev|> hint.` once per pass (`worker/prompt.ts`,
cut to a 120-token prompt budget at a whole word) and is prepended
to the decoder input of every 30 s window by a wrapper around the model's
`generate()`, which also strips it from the output again (D-WS2a).
`generate()`, which also strips it from the output again (D-048).
Live passes (4.1b) carry the same `prompt`.
6. **Long-form branch (D-WS3a/D-WS3b, "Advanced → Skip silence", off by default).**
6. **Long-form branch (D-049/D-050, "Advanced → Skip silence", off by default).**
With the option on, every load posted for a file run or the Download button carries
`vad: true`; once the Whisper pipeline is ready the worker loads Silero VAD
(`onnx-community/silero-vad`, 2.2 MB, Cache API like the weights; offline it is
@@ -183,7 +183,7 @@ environment). See `WorkBreakdown.md` for the itemized checklist.)
chunked path whenever the VAD model isn't loaded, and live passes never take
this branch (live keeps its RMS segmenter, D-016/D-024). A finished transcript
with no words shows "No speech was found in this recording." The names-and-terms
hint (`prompt`) is applied to every chunk's decode the same way (D-WS2a).
hint (`prompt`) is applied to every chunk's decode the same way (D-048).
### 4.1b Live microphone transcription (anonymous, browser engine — D-016)
1. `LiveControls` starts a session: `getUserMedia` → `AudioContext` at the
@@ -216,12 +216,12 @@ environment). See `WorkBreakdown.md` for the itemized checklist.)
only when signed in with the server engine available (`EngineToggle`);
anonymous/offline users always get the browser-engine path above.
### 4.1c Transcript translation (any engine — D-WS4b)
### 4.1c Transcript translation (any engine — D-045)
1. Any finished transcript (browser file, browser live, server engine) shows a
"Translate transcript…" disclosure (`TranslatePanel`). Whisper's own
"Translate to English" task stays in `TranscriptionOptions`, only for
models with `canTranslate` (D-WS4a).
models with `canTranslate` (D-044).
2. On **Translate**, the UI groups the transcript's chunks into sentence-ending
units (`buildTranslationUnits`, merged time spans) and posts their texts to
`translate.worker.js` — a separate worker with its own onnxruntime instance.
@@ -248,7 +248,7 @@ environment). See `WorkBreakdown.md` for the itemized checklist.)
- Model weights live in the Cache API (`transformers-cache`) via Transformers.js —
independent of the SW, which never touches that cache (D-005). `/api/*` is
network-only. The Silero VAD model of the long-form branch lives there too
(D-WS3b); offline, the worker checks that cache before loading it and never
(D-050); offline, the worker checks that cache before loading it and never
fetches, falling back to the chunked path if it is missing.
- Updates: `pwa/register.ts` surfaces a waiting SW as a toast; accepting posts
`SKIP_WAITING`, activation drops old `whisper-*` caches, and the page reloads.
@@ -266,11 +266,11 @@ environment). See `WorkBreakdown.md` for the itemized checklist.)
`src/server/transcribe.ts` bridges the sidecar's segment stream to the streamed
JSON-lines HTTP response (D-011). `language`, `task` and the vocabulary hint
(`prompt`) travel as query params (also on the live WebSocket) and are
validated by `parseLanguageAndTask` (D-032, D-WS2a); the sidecar cuts the
validated by `parseLanguageAndTask` (D-032, D-048); the sidecar cuts the
hint with the same rule as the browser and passes it to faster-whisper as
`initial_prompt` (D-WS2a). File requests (`mode: "file"`) decode with
`initial_prompt` (D-048). File requests (`mode: "file"`) decode with
faster-whisper's batched pipeline over Silero-VAD speech chunks; server-live
passes send `mode: "live"` and stay sequential (D-WS5a). The shared catalog maps model ids to
passes send `mode: "live"` and stay sequential (D-047). The shared catalog maps model ids to
`Systran/faster-whisper-*` CTranslate2 conversions. If the sidecar is absent,
the server engine reports unavailable; the browser/anonymous flow is unaffected.
4. Both engines implement the same `Engine` interface in `src/lib/engine.ts`, so the
@@ -283,7 +283,7 @@ environment). See `WorkBreakdown.md` for the itemized checklist.)
- Anonymous users are never blocked from browser inference — auth only unlocks the
server engine.
### 4.1c Parakeet (English-only, desktop-only browser model — D-WS7a)
### 4.1c Parakeet (English-only, desktop-only browser model — D-051)
Catalog entries carry `family?: "whisper" | "parakeet"` (absent ⇒ whisper). For
`family: "parakeet"` the worker skips the Transformers.js pipeline:
1. **Load:** `modelFiles.ts` fetches `vocab.txt`, the decoder_joint and the
@@ -316,7 +316,7 @@ Catalog entries carry `family?: "whisper" | "parakeet"` (absent ⇒ whisper). Fo
- **Anonymous-first:** every feature except the server engine works logged out.
- **Offline-first:** app shell + previously-downloaded models must work offline.
- **One browser inference runtime:** onnxruntime-web, driven by Transformers.js for
Whisper and by parakeet.js for Parakeet (D-WS7a) — the same ort instance, never a
Whisper and by parakeet.js for Parakeet (D-051) — the same ort instance, never a
second runtime; the server engine runs faster-whisper (D-013). The model catalog
stays shared across both engines (browser-only entries have no faster-whisper name).
- **Worker isolation:** inference never runs on the main thread.
+26 -18
View File
@@ -125,7 +125,7 @@ than ONNX Runtime under Bun; the server engine becomes genuinely useful rather t
slow convenience.
## D-014 — WASM sessions run at graphOptimizationLevel "basic"
**Status:** **Superseded by D-WS1c** (bug workaround, 2026-07-10; the ort-web
**Status:** **Superseded by D-043** (bug workaround, 2026-07-10; the ort-web
build in Transformers.js 4.3.2 no longer has the bug, so the cap is gone)
On the WASM backend, the inference worker passes
`session_options: { graphOptimizationLevel: "basic" }` to the Transformers.js
@@ -1226,7 +1226,7 @@ exists to catch.
**Consequence:** full `bun test` ≈ 19 min on CPU (en_alien ≈ 20 min of it
on a cold run); the fast suite excludes this file.
## D-WS1a — Transformers.js 4.2.0 → 4.3.2, sequential-session patch carried over
## D-040 — Transformers.js 4.2.0 → 4.3.2, sequential-session patch carried over
**Status:** Accepted (2026-10-29, spec 001 Goal 1)
`@huggingface/transformers` is now pinned to exactly `4.3.2` (latest; 4.3.0
added Safari 26+ WebGPU). It brings onnxruntime-web
@@ -1254,7 +1254,7 @@ TDT/RNN-T (transducer) decoder or joint-network loop anywhere in the
package, so **Parakeet TDT 0.6B v3 is not supported natively**; per the
spec, that path uses `parakeet.js`.
## D-WS1d — WebGPU-side effects of the 4.3.2 upgrade: inspected, not measured (needs the owner's browser check)
## D-041 — WebGPU-side effects of the 4.3.2 upgrade: inspected, not measured (needs the owner's browser check)
**Status:** Open — inspection only (2026-10-29)
This machine has no GPU and no browser WebGPU, so D-022, D-025 and D-026
cannot be re-measured here. What the upgrade changes on that side, from the
@@ -1294,7 +1294,7 @@ cannot be re-measured here. What the upgrade changes on that side, from the
5. **Safari 26 (macOS/iOS):** what the footer reports for "auto"; if WebGPU,
check speed and output.
## D-WS1b — D-021's `max_new_tokens: 440` is still needed on Transformers.js 4.3.2
## D-042 — D-021's `max_new_tokens: 440` is still needed on Transformers.js 4.3.2
**Status:** Accepted (2026-10-29) — re-test of D-021 part 3, which stands unchanged
**Source:** 4.3.2's `WhisperForConditionalGeneration.generate()` still takes
the seek loop on exactly D-021's condition,
@@ -1321,10 +1321,10 @@ version-to-version speed comparison.
**Decision:** keep `max_new_tokens: 440` in `src/worker/inference.worker.ts`
and in `tests/live-real-model.test.ts`.
## D-WS1c — WASM sessions back at the default graph optimization level (D-014's "basic" cap dropped)
## D-043 — WASM sessions back at the default graph optimization level (D-014's "basic" cap dropped)
**Status:** Accepted (2026-10-30) — supersedes D-014
**Re-test** of D-014's revisit condition after the Transformers.js 4.3.2 bump
(D-WS1a), which brings onnxruntime-web `1.31.0-dev.20260914-8d85527a0`.
(D-040), which brings onnxruntime-web `1.31.0-dev.20260914-8d85527a0`.
Measured under Bun with onnxruntime-web's own wasm EP (not onnxruntime-node),
1 thread, AMD Ryzen 9 9950X shared with other workstreams.
1. **Session creation**, `onnx-community/whisper-tiny`
@@ -1361,7 +1361,7 @@ check is re-run.
`ort-wasm-simd-threaded.*` files, but multithreaded under COOP/COEP) —
`tests/offline.md` §2's WASM step is the owner's browser check.
## D-WS4a — Whisper's "Translate to English" only on models trained for it (`canTranslate`)
## D-044 — Whisper's "Translate to English" only on models trained for it (`canTranslate`)
**Status:** Accepted (2026-10-29) · spec 001 Goal 4a
`WhisperModel` gains `canTranslate: boolean` (`src/lib/models.ts`): **true** for multilingual
tiny / base / small / medium, **false** for every `.en` model (no task token) and for
@@ -1376,13 +1376,13 @@ server's `parseLanguageAndTask` (D-032) still accepts "translate" for any model
sends it for turbo now, and a direct API caller asking faster-whisper to translate with turbo gets
what faster-whisper does; rejecting it server-side would be a behaviour change for API users with no
UI benefit. Speech in another language than English can still end up in English (or Danish) through
text translation of the finished transcript (D-WS4b), for every model.
text translation of the finished transcript (D-045), for every model.
## D-WS4b — Text translation of finished transcripts with Opus-MT, in its own worker
## D-045 — Text translation of finished transcripts with Opus-MT, in its own worker
**Status:** Accepted (2026-10-29) · spec 001 Goal 4b
**What:** any finished transcript — browser file, browser live, server engine — can be translated
da→en or en→da from a secondary "Translate transcript…" disclosure under the transcript
(`TranslatePanel`). Whisper's own task keeps its label "Translate to English" (D-WS4a). The
(`TranslatePanel`). Whisper's own task keeps its label "Translate to English" (D-044). The
translation renders as a second transcript card ("Translation (English)") with its own
`.txt` / `.srt` / `.txt+ts` exports named `<file>.<en|da>.<ext>`.
**Models:** `Xenova/opus-mt-da-en` and `Xenova/opus-mt-en-da` — Transformers.js ONNX exports of
@@ -1432,7 +1432,7 @@ terminates the translate worker only.
**Not done / follow-ups:** WebGPU for Opus-MT (unmeasurable here); more language pairs (the catalog
is two entries, `TRANSLATION_MODELS`); a translation of live text *while* recording.
## D-WS6a — Danish fine-tuned Whisper models: our own ONNX + CTranslate2 conversions, unpublished until the owner says go
## D-046 — Danish fine-tuned Whisper models: our own ONNX + CTranslate2 conversions; only whisper-small-danish published
**Status:** Proposed (2026-10-29, spec 001 Goal 6)
**Candidates checked (licence + architecture, from each repo's model card, config and notices):**
- `thorhojhus/whisper-small-danish` — MIT; plain `WhisperForConditionalGeneration`, whisper-small
@@ -1484,7 +1484,15 @@ use beam 5.
cards credit the original authors and carry MIT, or Apache-2.0 + NOTICE), then set
`DANISH_MODELS_HF_OWNER`. The integrator sets `canTranslate: false` on these entries at merge.
## D-WS5a — Server engine: batched faster-whisper + tuned Silero VAD for files; live passes stay sequential
**Publication (2026-10-30, owner's decision):** only the small fine-tune is
published — `lucasw89/whisper-small-danish-ONNX` (browser) and
`lucasw89/whisper-small-danish-ct2-int8` (server), public, model cards
crediting the `thorhojhus` account and carrying MIT. The turbo-danish and
edda-v0.2 conversions showed no clear gain over stock turbo on `da_festuge`, so
they stay unpublished (folders kept locally in `/home/user/hf-publish/`) and
their catalog entries were removed. `DANISH_MODELS_HF_OWNER = "lucasw89"`.
## D-047 — Server engine: batched faster-whisper + tuned Silero VAD for files; live passes stay sequential
**Status:** Accepted (2026-10-30, spec 001 Goal 5)
`server-py/sidecar.py` decoded every request with the sequential
`WhisperModel.transcribe`, no VAD.
@@ -1529,7 +1537,7 @@ long. Segments arrive in bursts of 8 chunks (≈ 4 min of speech) and the first
burst can take ~15–25 s on a long file; the D-021 heartbeat covers it, and
cancel lands at the next burst boundary. No new env vars.
## D-WS2a — "Names and terms" vocabulary hint as a Whisper decoder prompt, cut to 120 tokens
## D-048 — "Names and terms" vocabulary hint as a Whisper decoder prompt, cut to 120 tokens
**Status:** Accepted (2026-10-29) — spec 001, Goal 2. Placeholder id; the
integrator renumbers it.
**Context:** users want names and jargon spelled their way. Whisper takes a
@@ -1613,14 +1621,14 @@ test; revisit if a larger list is wanted on turbo).
long hint. WebGPU behaviour of the prompt path is unmeasured here (CPU
only) — needs the owner's browser check.
**Integration note (2026-10-30, merge with D-WS5a):** faster-whisper's
**Integration note (2026-10-30, merge with D-047):** faster-whisper's
`BatchedInferencePipeline` (server file mode) re-encodes `initial_prompt`
itself and accepts only text, so the sidecar passes it the budget-cut ids
decoded back to text (re-encoding gives the same ids, checked with the real
tokenizer incl. "Århus, Ærø"); server-live passes keep the ids. Pinned by the
stubbed-sidecar test "the vocabulary hint reaches the batched pipeline as text".
## D-WS3a — Long-form file pipeline (Silero VAD chunks, language pinned once) ships as an Advanced option, off by default
## D-049 — Long-form file pipeline (Silero VAD chunks, language pinned once) ships as an Advanced option, off by default
**Status:** Accepted (2026-10-30) — spec 001 Goal 3
Browser file transcription cuts audio into fixed 30 s windows with 5 s
overlap (Transformers.js's chunked pipeline), decoding silence and
@@ -1678,14 +1686,14 @@ bars. Whisper's `generate()` drops kwargs not in `forward_params`, so
instance; a Transformers.js upgrade must keep the "one encoder pass per
chunk" assertion green. WebGPU timings are unmeasured.
**Integration note (2026-10-30, merge with D-WS2a):** the long-form adapter
**Integration note (2026-10-30, merge with D-048):** the long-form adapter
(`worker/longform/transformers.ts`) applies the names-and-terms hint to every
chunk's decode with the same decoder-prompt prefix as the chunked path
(tokenized once per pass); before the merge the `prompt` option reached the
adapter but was ignored. Pinned by a unit test in
`tests/longform-transformers.test.ts`.
## D-WS3b — Silero VAD model: onnx-community/silero-vad from the Hub, cached like Whisper weights, never fetched offline
## D-050 — Silero VAD model: onnx-community/silero-vad from the Hub, cached like Whisper weights, never fetched offline
**Status:** Accepted (2026-10-30) — spec 001 Goal 3
**Decision:** the long-form pipeline's VAD is Silero VAD v5 as exported by
`onnx-community/silero-vad` (`onnx/model.onnx`, fp32, 2 243 022 bytes, MIT;
@@ -1704,7 +1712,7 @@ path (`choosePipeline`). Live mode keeps its energy gate (D-016).
off-by-default option); running it on the main thread (D-013).
**Consequence:** the first "Skip silence" run online downloads 2.2 MB.
## D-WS7a — Parakeet TDT 0.6B v3 as an English-only, desktop-only browser model via parakeet.js on the shared onnxruntime-web
## D-051 — Parakeet TDT 0.6B v3 as an English-only, desktop-only browser model via parakeet.js on the shared onnxruntime-web
**Status:** Accepted (2026-10-29, spec 001 Goal 7)
**Runtime decision (evidence).** `@huggingface/transformers` 4.2.0 (ours) and
4.3.2 (npm `latest`) were inspected from their published tarballs: both ship
+16 -16
View File
@@ -66,10 +66,10 @@ Notes (M1):
- [x] Handle model switching (dispose old pipeline, load new)
- [x] Language / task options for multilingual models (transcribe vs translate)
- [x] Whisper translate only on models trained for it (`canTranslate`; hidden for
large-v3-turbo and `.en`, stale "translate" resets to transcribe — D-WS4a)
large-v3-turbo and `.en`, stale "translate" resets to transcribe — D-044)
- [~] Verify quantized variants (q4/q8/fp16) on both backends; record working
combinations in `models.ts`
- [~] Danish fine-tunes (spec 001 Goal 6, D-WS6a): `whisper-small-danish`,
- [~] Danish fine-tunes (spec 001 Goal 6, D-046): `whisper-small-danish`,
`whisper-large-v3-turbo-danish`, `edda-v0.2`. We converted them to ONNX
(fp32/fp16/q8/q4) and CT2 int8 and verified them on CPU. The catalog
entries hang off `DANISH_MODELS_HF_OWNER`. Still pending: the owner's go
@@ -154,7 +154,7 @@ Notes (M4):
Bun (D-013 supersedes the Transformers.js-under-Bun plan); map the shared
model catalog to faster-whisper model ids in `src/lib/models.ts`
- [x] Batched inference + tuned Silero VAD for server-engine file
transcriptions, live passes sequential (spec 001 Goal 5, D-WS5a):
transcriptions, live passes sequential (spec 001 Goal 5, D-047):
≈ 2× faster on CPU int8 at equal-or-better WER; segments ≤ 30 s chunks
- [x] Degrade gracefully when Python/the sidecar is absent (server engine
unavailable; browser engine untouched — anonymous path deploys with Bun alone)
@@ -352,10 +352,10 @@ cached.
## M8 — Transcription quality and speed (spec 001)
- [x] Transformers.js 4.2.0 → 4.3.2, sequential-session patch regenerated
for 4.3.2 and guarded by `tests/transformers-runtime.test.ts` (D-WS1a).
for 4.3.2 and guarded by `tests/transformers-runtime.test.ts` (D-040).
- [x] WASM sessions back at the default graph optimization level: ort-web
1.31.0-dev loads the q8 decoders at every level (D-WS1c supersedes D-014).
- [x] Vocabulary hint "Names and terms" (D-WS2a) — optional field for every
1.31.0-dev loads the q8 decoders at every level (D-043 supersedes D-014).
- [x] Vocabulary hint "Names and terms" (D-048) — optional field for every
model, remembered in localStorage, sent as `prompt` on file and live
passes (browser and server engines); cut to 120 tokens at a whole word.
faster-whisper gets `initial_prompt` only (hotwords measured, no gain).
@@ -364,15 +364,15 @@ cached.
## Spec 001 — Translate (workstream 4)
- [x] `canTranslate` per model; Task control hidden where false; stale
"translate" resets to transcribe (D-WS4a)
"translate" resets to transcribe (D-044)
- [x] Text translation of any finished transcript, da→en / en→da, Opus-MT
in its own worker, sentence-chunked, timestamps kept for exports (D-WS4b)
in its own worker, sentence-chunked, timestamps kept for exports (D-045)
- [~] Real-browser checks: offline translation (`tests/offline.md` §5),
WebGPU-machine speed (owner)
---
## Spec 001 Goal 7 — Parakeet TDT 0.6B v3 (English, desktop, D-WS7a)
## Spec 001 Goal 7 — Parakeet TDT 0.6B v3 (English, desktop, D-051)
- [x] Runtime decision: Transformers.js 4.2.0/4.3.2 have CTC-only Parakeet → parakeet.js on the
shared onnxruntime-web (one runtime in the bundle, `tests/workerBundle.test.ts`).
- [x] Catalog `family` + exact sizes; worker engine path (≤ 30 s windows, word → chunk
@@ -419,27 +419,27 @@ Found while bootstrapping agent docs (D-036); all fixed on 2026-10-29.
— Light theme via tokens + self-hosted Inter (D-037). Also fixed: the
Backend select overflowed its panel on 390 px-wide phones.
- [ ] An English "Names and terms" hint can pull a short Danish live window
toward English words (one 2 s da_festuge snippet on turbo, D-WS2a).
toward English words (one 2 s da_festuge snippet on turbo, D-048).
Not addressed: suggest same-language hints if it shows up in use.
Found while building transcript translation (D-WS4b), 2026-10-29:
Found while building transcript translation (D-045), 2026-10-29:
- [ ] Transformers.js 4.2 batched Marian generation returns row 0's output for
every row of a padded batch, so translation runs one sentence per call
(~4× slower than batch 8 would be). Re-check after the 4.3 upgrade with
`bun scripts/bench-translate.ts` ("output identical" across batch sizes).
- [ ] The server API still accepts `task: "translate"` with large-v3-turbo
(only the UI hides it, D-WS4a).
(only the UI hides it, D-044).
Found in spec 001 workstream 6 (D-WS6a), open:
Found in spec 001 workstream 6 (D-046), open:
- [ ] Transformers.js 4.2 has no beam search: `num_beams > 1` in a model's
`generation_config.json` (edda-v0.2 ships 5) silently decodes greedily
and does a full-vocabulary top-k per step (D-WS6a). Not fixed: it would
and does a full-vocabulary top-k per step (D-046). Not fixed: it would
mean overriding `num_beams: 1` in the worker, or a patch.
- [ ] On `da_festuge`, neither Danish turbo fine-tune beats the stock turbo
(D-WS6a). Judging them needs a longer, harder Danish reference clip than
(D-046). Judging them needs a longer, harder Danish reference clip than
this repo has.
Found while building the long-form pipeline (spec 001 Goal 3, D-WS3a), 2026-10-29:
Found while building the long-form pipeline (spec 001 Goal 3, D-049), 2026-10-29:
- [ ] "Auto-detect" on the default (chunked) file path does not detect: with no
`language`, Transformers.js 4.2 logs "No language specified - defaulting to
English" and decodes Danish audio as an English translation (da_festuge WER
+1 -1
View File
@@ -53,7 +53,7 @@ The integrator merges in this order, rebasing/fixing up each onto the result of
- `AGENTS.md` rules apply in full (Bun only, COEP/no CDN, colour tokens, DI on server modules,
`classifyError`, runtime-env config, TDD, docs duties).
- **Decision ids:** don't take real `D-###` numbers. Write new entries as `## D-WS<n><letter> — …`
(e.g. `D-WS3a`) and cite the same placeholder in code comments; the integrator renumbers at merge.
(e.g. `D-049`) and cite the same placeholder in code comments; the integrator renumbers at merge.
- **Heavy jobs take turns.** This machine has 16 cores / 18 GB RAM shared by all workstreams. Wrap
every heavy command — real-model tests, the full `bun test`, benchmarks, model downloads,
conversions, `pip install` — in `flock /tmp/whisper-heavy.lock <command>`. Light commands
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Parakeet vs Whisper Large v3 Turbo benchmark (D-WS7a) — CPU only.
* Parakeet vs Whisper Large v3 Turbo benchmark (D-051) — CPU only.
*
* flock /tmp/whisper-heavy.lock bun scripts/bench-parakeet.ts [parakeet-node|parakeet-web|turbo ...]
*
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Transcript-translation speed on onnxruntime-node (CPU), D-WS4b.
* Transcript-translation speed on onnxruntime-node (CPU), D-045.
*
* flock /tmp/whisper-heavy.lock bun scripts/bench-translate.ts [batchSizes…]
*
+2 -2
View File
@@ -52,12 +52,12 @@ const workerBuild = await Bun.build({
target: "browser",
format: "esm",
minify: true,
// One onnxruntime in the bundle (D-WS7a).
// One onnxruntime in the bundle (D-051).
plugins: [ortAliasPlugin],
});
const workerScript = await workerBuild.outputs[0]!.text();
await Bun.write(join(DIST, "inference.worker.js"), workerScript);
// Transcript-translation worker (D-WS4b), same mechanism.
// Transcript-translation worker (D-045), same mechanism.
const translateWorkerBuild = await Bun.build({
entrypoints: [join(ROOT, "src/worker/translate.worker.ts")],
target: "browser",
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Before/after measurement for the long-form pipeline (D-WS3a): wall-clock,
* Before/after measurement for the long-form pipeline (D-049): wall-clock,
* WER against tests/audio_files/*.txt, generated tokens and decode calls, for
* the worker's current chunked path ("old") and long-form configurations.
*
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Bun build plugin: parakeet.js's bare `onnxruntime-web` import resolves to
* `src/worker/ortShim.ts` (= Transformers.js's `onnxruntime-web/webgpu`), so
* the inference worker bundles exactly one onnxruntime (D-WS7a). Used by the
* the inference worker bundles exactly one onnxruntime (D-051). Used by the
* production build (scripts/build.ts) and the dev server's worker bundle
* (src/index.ts). Without it the bundle carries parakeet.js's own 1.24.1
* copy as well — never executed, but ~0.5 MB of dead weight and a CDN
+8 -8
View File
@@ -11,7 +11,7 @@ stdio — one JSON object per line, no framing beyond that:
"audio_path": "/tmp/...", "language": "en" | null,
"task": "transcribe" | "translate",
"mode": "file" | "live", # optional, default "file"
"prompt": "Maxwell, Voices of the Void" | null} # optional (D-WS2a)
"prompt": "Maxwell, Voices of the Void" | null} # optional (D-048)
stdout <- {"type": "segment", "id": "1", "start": 0.0, "end": 4.2,
"text": " Hello world."} # 0..n of these
stdout <- {"type": "done", "id": "1", "cancelled": false}
@@ -26,7 +26,7 @@ container decoding happens here. Requests are processed strictly one at a
time; a reader thread keeps consuming stdin meanwhile so `cancel` can
interrupt the segment loop of the request in flight.
Decoding strategy by mode (Decisions.md D-WS5a):
Decoding strategy by mode (Decisions.md D-047):
"file" faster-whisper's BatchedInferencePipeline with its Silero VAD
(vad_filter=True, VAD_PARAMETERS): silence is dropped, speech is cut
into <= 30 s chunks and BATCH_SIZE chunks decode per forward pass.
@@ -62,18 +62,18 @@ import sys
import threading
SAMPLE_RATE = 16000
BATCH_SIZE = 8 # D-WS5a: VAD chunks decoded per batched forward pass
# D-WS5a: Silero VAD tuned for file mode. faster-whisper's batched default
BATCH_SIZE = 8 # D-047: VAD chunks decoded per batched forward pass
# D-047: Silero VAD tuned for file mode. faster-whisper's batched default
# (threshold 0.5, 400 ms padding) dropped quiet and overlapped speech —
# deletions nearly doubled on en_alien; 0.2 / 800 ms brought WER back to the
# sequential decoder's level. min_silence_duration_ms stays at VadOptions'
# 2000 ms default (a dict replaces the pipeline's 160 ms preset).
VAD_PARAMETERS = {"threshold": 0.2, "speech_pad_ms": 800}
# "Names and terms" vocabulary hint (D-WS2a). Twins of normalizeHint /
# "Names and terms" vocabulary hint (D-048). Twins of normalizeHint /
# fitHintToBudget in src/lib/hint.ts — change both together; parity is tested
# in tests/hint-parity.test.ts. Whisper would allow 223 tokens; 120 is the
# measured cap at which small models are not garbled (D-WS2a).
# measured cap at which small models are not garbled (D-048).
PROMPT_TOKEN_BUDGET = 120
_CONTROL_CHARS = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
@@ -215,7 +215,7 @@ def main():
)
if mode == "live":
if prompt_ids:
# D-WS2a: token ids, already cut to the budget by the
# D-048: token ids, already cut to the budget by the
# same rule as the browser engine.
options["initial_prompt"] = prompt_ids
segments, _info = cached_model.transcribe(
@@ -241,7 +241,7 @@ def main():
# `segments` is a generator: inference happens during iteration,
# so segments stream out (and cancel can cut in) while later ones
# are still being decoded — per segment in live mode, per batch of
# BATCH_SIZE speech chunks in file mode (D-WS5a).
# BATCH_SIZE speech chunks in file mode (D-047).
for segment in segments:
if request_id in cancelled:
was_cancelled = True
+13 -13
View File
@@ -80,10 +80,10 @@ interface AppErrorState {
}
const MODEL_IDS = MODELS.map((model) => model.id);
// Downloaded badges cover the Opus-MT models too (transcript translation, D-WS4b).
// Downloaded badges cover the Opus-MT models too (transcript translation, D-045).
const CACHED_MODEL_IDS = [...MODEL_IDS, ...TRANSLATION_MODEL_IDS];
/** Device signals for the mobile memory guard (D-027) and desktop-only models (D-WS7a). */
/** Device signals for the mobile memory guard (D-027) and desktop-only models (D-051). */
function memoryHint(): MobileMemoryHint {
const nav = typeof navigator !== "undefined" ? navigator : undefined;
return {
@@ -189,7 +189,7 @@ export function App() {
const [activeThreads, setActiveThreads] = useState<number | null>(null);
const [language, setLanguage] = useState(AUTO_LANGUAGE);
const [task, setTask] = useState<WhisperTask>(DEFAULT_TASK);
// "Names and terms" vocabulary hint (D-WS2a): raw text, remembered in this
// "Names and terms" vocabulary hint (D-048): raw text, remembered in this
// browser, sent with every pass for every model (`.en` included).
const [hint, setHint] = useState(() => loadHint());
const [downloaded, setDownloaded] = useState<ReadonlySet<string>>(new Set());
@@ -207,7 +207,7 @@ export function App() {
const auth = useAuth();
const [engineChoice, setEngineChoice] = useState<EngineKind>(() => loadEngineChoice());
const [serverStatus, setServerStatus] = useState<ServerEngineStatus | null>(null);
// "Skip silence" (long-form pipeline, D-WS3a): remembered per browser. A
// "Skip silence" (long-form pipeline, D-049): remembered per browser. A
// ref mirrors it for the worker-message handler set up once per worker.
const [longForm, setLongForm] = useState(() => loadLongFormChoice());
const longFormRef = useRef(longForm);
@@ -306,7 +306,7 @@ export function App() {
function confirmRiskyMobileLoad(forModelId: string, preference: DevicePreference): boolean {
const targetModel = getModel(forModelId);
if (!targetModel) return true;
// Desktop-only models are never offered on phones (D-WS7a); this guards
// Desktop-only models are never offered on phones (D-051); this guards
// any path that could still name one (e.g. a stale selection).
if (!isModelAllowedOnDevice(targetModel, memoryHint())) return false;
const device = guessDevice(preference);
@@ -521,7 +521,7 @@ export function App() {
// use) only where the peak-memory crash risk is real; everywhere else
// (desktop, low-risk models) the worker loads them in parallel.
sequentialSessionLoad: isMobileLoadRisk(nextModelId, guessDevice(device)),
// D-WS3b: the speech-detection model rides along with the same
// D-050: the speech-detection model rides along with the same
// user-initiated load when "Skip silence" is on.
vad: longFormRef.current,
});
@@ -532,7 +532,7 @@ export function App() {
if (busy || liveStatus !== "idle" || nextModelId === modelId) return;
setModelId(nextModelId);
// A "Translate to English" chosen on a model that can translate must not
// silently carry over to one that cannot (D-WS4a).
// silently carry over to one that cannot (D-044).
setTask((current) => taskForModel(nextModelId, current));
setActiveDevice(null);
setActiveThreads(null);
@@ -714,7 +714,7 @@ export function App() {
...(multilingual && { language, task: taskForModel(modelId, task) }),
prompt: hint,
// Browser runs only; the worker falls back to the chunked path when
// the VAD model isn't available (D-WS3a/D-WS3b).
// the VAD model isn't available (D-049/D-050).
...(!useServer && { longForm }),
onPartial: (fragment) => {
if (fileRun.current === run) setPartialText((text) => text + fragment);
@@ -797,7 +797,7 @@ export function App() {
language,
task: taskForModel(modelId, task),
}),
// Same hint for every live pass of the session (D-WS2a).
// Same hint for every live pass of the session (D-048).
...(hint && { prompt: hint }),
};
@@ -1027,7 +1027,7 @@ export function App() {
const serverSelected =
engineChoice === "server" && auth.user !== null && serverStatus?.available === true;
// Phones never see desktop-only models (Parakeet, D-WS7a); the server
// Phones never see desktop-only models (Parakeet, D-051); the server
// engine only offers what the sidecar runs.
const availableModels = modelsForDevice(
serverSelected
@@ -1087,7 +1087,7 @@ export function App() {
serverEngine={serverSelected}
/>
{/* Parakeet has no language or task choice (D-WS7a) — say so where
{/* Parakeet has no language or task choice (D-051) — say so where
those controls would be, rather than leaving their absence unexplained. */}
{parakeetSelected && (
<p className="model-language-note">
@@ -1109,7 +1109,7 @@ export function App() {
)}
{/* Hint and "Skip silence" are Whisper decode options; the Parakeet
path (D-WS7a) has neither, so they are hidden rather than ignored. */}
path (D-051) has neither, so they are hidden rather than ignored. */}
{!parakeetSelected && (
<>
<VocabularyHint
@@ -1249,7 +1249,7 @@ export function App() {
}
/>
{/* Text translation of the finished transcript (D-WS4b) — any engine,
{/* Text translation of the finished transcript (D-045) — any engine,
file or live; secondary to the transcript itself. */}
{result && !liveActive && (
<TranslatePanel
+2 -2
View File
@@ -7,8 +7,8 @@ interface AdvancedOptionsProps {
/**
* Technical file-transcription controls, collapsed by default (PRODUCT.md:
* every control stays available without crowding the primary flow).
* "Skip silence" turns on the long-form pipeline (D-WS3a), which needs a
* small speech-detection model downloaded once (D-WS3b).
* "Skip silence" turns on the long-form pipeline (D-049), which needs a
* small speech-detection model downloaded once (D-050).
*/
export function AdvancedOptions({
skipSilence,
+1 -1
View File
@@ -161,7 +161,7 @@ export function ModelPicker({
<span className="model-option-name">
{model.displayName}
{model.family === "parakeet" && (
// Not Whisper (D-WS7a): say what it is good for and
// Not Whisper (D-051): say what it is good for and
// what it can't do, in plain words.
<span className="model-option-note">
Not Whisper · English only · fast on long recordings
+1 -1
View File
@@ -16,7 +16,7 @@ interface StorageManagerProps {
onChanged: () => void;
}
// Whisper weights plus the Opus-MT translation models (D-WS4b).
// Whisper weights plus the Opus-MT translation models (D-045).
const MODEL_IDS = [...MODELS.map((model) => model.id), ...TRANSLATION_MODEL_IDS];
function displayName(modelId: string): string {
+2 -2
View File
@@ -8,7 +8,7 @@ interface TranscriptionOptionsProps {
onTaskChange: (task: WhisperTask) => void;
/**
* Whether the model supports Whisper's own speech → English translation
* (`WhisperModel.canTranslate`, D-WS4a). When false the Task control is not
* (`WhisperModel.canTranslate`, D-044). When false the Task control is not
* rendered at all — transcribing is the only thing such a model does.
*/
canTranslate: boolean;
@@ -18,7 +18,7 @@ interface TranscriptionOptionsProps {
/**
* Language + task controls for multilingual models. The parent renders this
* only when the selected model is multilingual — `.en` models take neither.
* The task control only appears where the model can translate (D-WS4a).
* The task control only appears where the model can translate (D-044).
*/
export function TranscriptionOptions({
language,
+1 -1
View File
@@ -65,7 +65,7 @@ interface TranslatePanelProps {
}
/**
* Text translation of a finished transcript with Opus-MT (D-WS4b): a
* Text translation of a finished transcript with Opus-MT (D-045): a
* secondary disclosure under the transcript, separate from Whisper's own
* "Translate to English" task. Runs on-device in its own worker; the model
* is downloaded only when the user clicks Translate (D-020).
+2 -2
View File
@@ -10,12 +10,12 @@ interface VocabularyHintProps {
/**
* Past this many characters the hint likely exceeds the 120-token prompt
* budget (a list of names runs ≈ 2.8 characters per token, measured) and gets
* cut at the end (D-WS2a) — say so rather than silently ignoring the tail.
* cut at the end (D-048) — say so rather than silently ignoring the tail.
*/
const LONG_HINT_CHARS = 300;
/**
* Optional "Names and terms" vocabulary hint (D-WS2a). Shown for every model;
* Optional "Names and terms" vocabulary hint (D-048). Shown for every model;
* the parent persists the value (src/lib/hint.ts) and sends it with each
* transcription as the decoder prompt.
*/
+4 -4
View File
@@ -423,7 +423,7 @@ body {
flex: 1;
}
/* One-line description under a model name (Parakeet, D-WS7a). */
/* One-line description under a model name (Parakeet, D-051). */
.model-option-note {
display: block;
color: var(--muted);
@@ -526,7 +526,7 @@ body {
flex-wrap: wrap;
}
/* --- VocabularyHint (D-WS2a) --- */
/* --- VocabularyHint (D-048) --- */
.vocabulary-hint {
display: flex;
@@ -580,7 +580,7 @@ body {
color: var(--muted);
}
/* --- AdvancedOptions (long-form "Skip silence", D-WS3a) --- */
/* --- AdvancedOptions (long-form "Skip silence", D-049) --- */
.advanced-options summary {
cursor: pointer;
color: var(--muted);
@@ -1098,7 +1098,7 @@ code {
}
/* --------------------------------------------------------------------------
Transcript translation (D-WS4b): a secondary disclosure under the
Transcript translation (D-045): a secondary disclosure under the
transcript, same panel language as .storage-manager.
-------------------------------------------------------------------------- */
+3 -3
View File
@@ -226,7 +226,7 @@ async function productionServeOptions(): Promise<ServeOptions> {
* with Bun 1.3: both `bun build` and the dev server leave the call untouched),
* so worker-like entrypoints are bundled at startup and served from stable
* URLs. The UI spawns `new Worker("/inference.worker.js")` (and
* "/translate.worker.js" for transcript translation, D-WS4b); the service
* "/translate.worker.js" for transcript translation, D-045); the service
* worker registers as "/sw.js".
*/
async function bundleEntrypoint(relativePath: string): Promise<string> {
@@ -237,7 +237,7 @@ async function bundleEntrypoint(relativePath: string): Promise<string> {
// Never minify: sw.ts relies on its placeholder string literals surviving
// bundling verbatim (src/pwa/precache.ts), and dev builds favor debuggability.
minify: false,
// One onnxruntime in the worker bundle (D-WS7a); harmless for sw.ts.
// One onnxruntime in the worker bundle (D-051); harmless for sw.ts.
plugins: [ortAliasPlugin],
});
const artifact = build.outputs[0];
@@ -254,7 +254,7 @@ async function developmentServeOptions(): Promise<ServeOptions> {
const { default: index } = await import("./index.html");
const workerScript = await bundleEntrypoint("./worker/inference.worker.ts");
// Transcript translation runs in its own worker (D-WS4b).
// Transcript translation runs in its own worker (D-045).
const translateWorkerScript = await bundleEntrypoint("./worker/translate.worker.ts");
// Dev SW: empty precache list — assets are unhashed here, so the shell is
// runtime-cached network-first instead (see src/pwa/sw.ts).
+3 -3
View File
@@ -38,7 +38,7 @@ export interface EngineTranscribeOptions {
/** transcribe | translate (multilingual models only). */
task?: WhisperTask;
/**
* "Names and terms" vocabulary hint, raw user text (D-WS2a). Applies to
* "Names and terms" vocabulary hint, raw user text (D-048). Applies to
* every Whisper model, `.en` ones included; empty/undefined = no prompt.
*/
prompt?: string;
@@ -61,7 +61,7 @@ export interface EngineTranscribeOptions {
signal?: AbortSignal;
/**
* Browser engine only: use the long-form pipeline when the worker has the
* VAD model (D-WS3a). The server engine runs faster-whisper's own pipeline.
* VAD model (D-049). The server engine runs faster-whisper's own pipeline.
*/
longForm?: boolean;
}
@@ -294,7 +294,7 @@ export class ServerEngine implements Engine {
params.set("language", options.language);
}
if (options.task !== undefined) params.set("task", options.task);
// Validated + normalized server-side (parseLanguageAndTask, D-WS2a).
// Validated + normalized server-side (parseLanguageAndTask, D-048).
if (options.prompt) params.set("prompt", options.prompt);
let response: Response;
+1 -1
View File
@@ -17,7 +17,7 @@
*/
/**
* `"translate"` covers text translation of a finished transcript (D-WS4b):
* `"translate"` covers text translation of a finished transcript (D-045):
* both its one-time model download and the translation pass, which run in
* their own worker — only the suggestions differ from the Whisper stages.
*/
+4 -4
View File
@@ -1,5 +1,5 @@
/**
* The optional "Names and terms" vocabulary hint (D-WS2a): free text the user
* The optional "Names and terms" vocabulary hint (D-048): free text the user
* types once, remembered in this browser, and fed to Whisper as a decoder
* prompt (`<|startofprev|> hint …`) so names and jargon come out spelled the
* way the user spells them.
@@ -27,7 +27,7 @@ export const HINT_MAX_CHARS = 1000;
* faster-whisper cap), but measured on en_alien a prompt that long garbles
* small models: base.en WER 0.339 → 0.476 with a realistic 218-token list and
* → 1.05 with a synthetic one, while ≤ 120 tokens stays at or below the
* no-hint WER (D-WS2a).
* no-hint WER (D-048).
*/
export const PROMPT_TOKEN_BUDGET = 120;
@@ -74,12 +74,12 @@ function defaultStorage(): StorageLike | null {
/**
* Token ids for the hint as a Whisper prompt (without `<|startofprev|>`),
* truncated predictably to `budget` tokens (D-WS2a):
* truncated predictably to `budget` tokens (D-048):
*
* 1. Prompt text = `" " + normalizeHint(hint)` (Whisper prompts start with a
* space), ending in a full stop unless it already ends in . ? or ! —
* measured: an unterminated list makes Whisper drop punctuation and, on
* smaller models, fall into repetition loops (D-WS2a).
* smaller models, fall into repetition loops (D-048).
* 2. Over budget → keep the longest prefix of whole space-separated words that
* fits (full stop included), a dangling "," / ";" removed: the start of the
* list is kept, the end dropped, never mid-word.
+2 -2
View File
@@ -1,6 +1,6 @@
/**
* The "Skip silence" file-transcription setting (long-form pipeline,
* D-WS3a): remembered in localStorage like the engine choice.
* D-049): remembered in localStorage like the engine choice.
*/
import type { StorageLike } from "./engine";
@@ -10,7 +10,7 @@ export const LONG_FORM_KEY = "whisper-long-form";
/**
* Off by default: the spec's rollout rule makes the long-form pipeline the
* default only if it is faster and not less accurate on every measurement
* clip — see D-WS3a for the numbers that decided this.
* clip — see D-049 for the numbers that decided this.
*/
export const LONG_FORM_DEFAULT = false;
+1 -1
View File
@@ -90,7 +90,7 @@ export function shouldWarnBeforeLoad(hint: MobileMemoryHint, peakBytes: number):
}
/**
* Desktop-only models (D-WS7a): Parakeet's ~1.3 GB load peak (2 × the
* Desktop-only models (D-051): Parakeet's ~1.3 GB load peak (2 × the
* 652 MB int8 encoder, D-027's arithmetic) exceeds every phone budget the
* D-029 table considers safe, even at deviceMemory 8 (800 MB). Unlike the
* Whisper models — where a confirm lets the user judge their device — it is
+18 -18
View File
@@ -36,14 +36,14 @@ export interface DtypeDefault {
}
/**
* Which inference path a catalog entry runs on (D-WS7a). Whisper models use
* Which inference path a catalog entry runs on (D-051). Whisper models use
* the Transformers.js ASR pipeline; Parakeet uses the worker's own TDT path
* (src/worker/parakeetEngine.ts). Absent ⇒ "whisper", so the Whisper entries
* carry no field at all.
*/
export type ModelFamily = "whisper" | "parakeet";
/** Exact weight-file sizes per dtype, for entries whose files we know (D-WS7a). */
/** Exact weight-file sizes per dtype, for entries whose files we know (D-051). */
export interface ExactModelBytes {
encoder: Partial<Record<WhisperDtype, number>>;
decoder: Partial<Record<WhisperDtype, number>>;
@@ -54,7 +54,7 @@ export interface ExactModelBytes {
export interface WhisperModel {
/** Hugging Face Hub id, e.g. "onnx-community/whisper-tiny.en". */
id: string;
/** Inference path (D-WS7a); absent ⇒ "whisper". */
/** Inference path (D-051); absent ⇒ "whisper". */
family?: ModelFamily;
/**
* Real file sizes; when present they replace the parameter-count estimate
@@ -67,11 +67,11 @@ export interface WhisperModel {
multilingual: boolean;
/**
* Whether Whisper's own `task: "translate"` (speech → English text) is
* offered (D-WS4a). False for every `.en` model (no task token) and for
* offered (D-044). False for every `.en` model (no task token) and for
* large-v3-turbo, which OpenAI did not train for translation — and for any
* turbo-based fine-tune not tested for it. The UI hides "Translate to
* English" where false; text translation of a finished transcript
* (D-WS4b) works for every model regardless.
* (D-045) works for every model regardless.
*/
canTranslate: boolean;
/** Encoder parameter count, in millions (drives size estimates). */
@@ -86,7 +86,7 @@ export interface WhisperModel {
* CTranslate2 conversions on the Hub (`Systran/faster-whisper-*`;
* large-v3-turbo → `mobiuslabsgmbh/faster-whisper-large-v3-turbo`), so the
* pinned library version stays the single authority on the exact repo.
* Absent for browser-only families (Parakeet, D-WS7a): the server engine
* Absent for browser-only families (Parakeet, D-051): the server engine
* answers 400 for them and the picker hides them while it is selected.
*/
fasterWhisperModel?: string;
@@ -212,12 +212,12 @@ const ACCEL_Q4: DtypeDefault = {
};
// ---------------------------------------------------------------------------
// Danish fine-tunes (D-WS6a)
// Danish fine-tunes (D-046)
// ---------------------------------------------------------------------------
/**
* Hugging Face account that hosts our conversions of the Danish Whisper
* fine-tunes (D-WS6a). Both the ONNX repo (browser engine) and the
* fine-tunes (D-046). Both the ONNX repo (browser engine) and the
* CTranslate2 int8 repo (server engine) hang off this one value. Only
* whisper-small-danish is published: the turbo fine-tunes (turbo-danish,
* edda-v0.2) showed no clear gain over stock turbo on our Danish clip.
@@ -225,14 +225,14 @@ const ACCEL_Q4: DtypeDefault = {
export const DANISH_MODELS_HF_OWNER = "lucasw89";
/**
* WASM default for the Danish conversions (D-WS6a): measured on our own files
* WASM default for the Danish conversions (D-046): measured on our own files
* (da_festuge, onnxruntime-node CPU), q4 matched or beat q8 on WER and was
* faster for all three — small included, unlike the stock small (q8).
*/
const DANISH_WASM_Q4: DtypeDefault = {
dtype: "q4",
verified: false,
note: "q4: best measured WER and fastest of q8/q4 on our Danish conversions (D-WS6a).",
note: "q4: best measured WER and fastest of q8/q4 on our Danish conversions (D-046).",
};
/** Catalog id + matching faster-whisper repo for one published conversion. */
@@ -247,7 +247,7 @@ function danishRepos(name: string): Pick<WhisperModel, "id" | "fasterWhisperMode
// The catalog (D-006: curated, not exhaustive)
// ---------------------------------------------------------------------------
/** Parakeet TDT 0.6B v3 ONNX repo and the revision its file sizes were taken from (D-WS7a). */
/** Parakeet TDT 0.6B v3 ONNX repo and the revision its file sizes were taken from (D-051). */
export const PARAKEET_MODEL_ID = "ysdede/parakeet-tdt-0.6b-v3-onnx";
export const PARAKEET_REVISION = "e5356bf6e24bdb7a269536b0e9e7869bc034b893";
@@ -347,13 +347,13 @@ export const MODELS: readonly WhisperModel[] = [
displayName: "Whisper Large v3 Turbo",
multilingual: true,
// Not trained for translation (OpenAI's turbo model card); its
// "translate" output is unreliable (D-WS4a).
// "translate" output is unreliable (D-044).
canTranslate: false,
encoderParamsM: 635,
decoderParamsM: 174,
defaults: { wasm: WASM_Q4, webgpu: WEBGPU_FP16_Q4, webnn: WEBNN_FP16 },
},
// Danish fine-tune (D-WS6a): unchanged Whisper architecture, so the
// Danish fine-tune (D-046): unchanged Whisper architecture, so the
// parameter counts are its base model's. Conversions by us, weights by
// the original authors (credited in each published repo's model card).
{
@@ -361,7 +361,7 @@ export const MODELS: readonly WhisperModel[] = [
...danishRepos("whisper-small-danish"),
displayName: "Whisper Small (Danish)",
multilingual: true,
// Translation untested on these fine-tunes (spec 001, D-WS4a).
// Translation untested on these fine-tunes (spec 001, D-044).
canTranslate: false,
encoderParamsM: 88,
decoderParamsM: 156,
@@ -369,7 +369,7 @@ export const MODELS: readonly WhisperModel[] = [
},
{
// NVIDIA Parakeet TDT 0.6B v3 (CC-BY-4.0), NeMo→ONNX conversion by
// ysdede, run through parakeet.js on our onnxruntime-web (D-WS7a).
// ysdede, run through parakeet.js on our onnxruntime-web (D-051).
// English-only in this app (the model itself knows 25 European
// languages, but only English is measured here). Desktop-only: its
// ~1.3 GB load peak is far past every phone budget (D-027–D-029), so
@@ -391,10 +391,10 @@ export const MODELS: readonly WhisperModel[] = [
wasm: {
dtype: "q8",
verified: false,
note: "int8 encoder + decoder: the measured WASM path (D-WS7a).",
note: "int8 encoder + decoder: the measured WASM path (D-051).",
},
// Only used when WebGPU is chosen explicitly — "auto" never picks it
// for Parakeet (D-WS7a): int8 on WebGPU is reported broken, and fp16
// for Parakeet (D-051): int8 on WebGPU is reported broken, and fp16
// is a separate 1.24 GB download. The decoder stays int8 on WASM.
webgpu: {
dtype: { encoder_model: "fp16", decoder_model_merged: "q8" },
@@ -424,7 +424,7 @@ export function getFasterWhisperModel(id: string): string | undefined {
}
/**
* The task a run on `modelId` should actually use (D-WS4a): a "translate"
* The task a run on `modelId` should actually use (D-044): a "translate"
* left over from a model that could translate falls back to "transcribe" on
* one that cannot (large-v3-turbo, `.en`, unknown ids). App applies it on
* every model switch and again when building each run's options.
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Pure logic of the Parakeet TDT engine path (Decisions.md D-WS7a): window
* Pure logic of the Parakeet TDT engine path (Decisions.md D-051): window
* planning, word → transcript-chunk mapping, the vocab tokenizer, and the
* pinned asset URLs. No runtime, no browser globals — the worker side
* (src/worker/parakeetEngine.ts) injects the actual recognizer.
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* Text translation of a finished transcript (D-WS4b) — the pure half.
* Text translation of a finished transcript (D-045) — the pure half.
*
* Opus-MT (MarianMT) runs in `src/worker/translate.worker.ts`; everything
* here is planning that needs no model: which checkpoint serves a language
@@ -237,7 +237,7 @@ export interface TranslateUnitsDeps extends SplitOptions {
* One piece per model call. Batching would be ~4x faster, but Transformers.js
* 4.2's batched Marian generation is broken: every row of a padded batch
* comes back as row 0's translation (measured 2026-10-29 on
* onnxruntime-node, q8 and fp32, any graph optimisation level — D-WS4b).
* onnxruntime-node, q8 and fp32, any graph optimisation level — D-045).
* Re-measure with `scripts/bench-translate.ts` after a Transformers.js
* upgrade before raising this.
*/
+2 -2
View File
@@ -25,7 +25,7 @@ export interface LiveSessionData {
model: string;
language?: string;
task: WhisperTask;
/** Normalized vocabulary hint, applied to every pass (D-WS2a). */
/** Normalized vocabulary hint, applied to every pass (D-048). */
prompt?: string;
tempDir: string;
}
@@ -62,7 +62,7 @@ export function createLiveWebSocket(deps: LiveWebSocketDeps) {
audioPath,
language: ws.data.language,
task: ws.data.task,
// Short passes: sequential decode, no batching/VAD (D-WS5a).
// Short passes: sequential decode, no batching/VAD (D-047).
mode: "live",
prompt: ws.data.prompt,
},
+3 -3
View File
@@ -50,11 +50,11 @@ export interface SidecarTranscribeRequest {
task?: WhisperTask;
/**
* "file" (default): whole uploads — batched inference over VAD speech
* chunks. "live": short server-live passes — sequential, no VAD (D-WS5a).
* chunks. "live": short server-live passes — sequential, no VAD (D-047).
*/
mode?: SidecarMode;
/**
* Normalized "Names and terms" hint (D-WS2a); the sidecar truncates it to
* Normalized "Names and terms" hint (D-048); the sidecar truncates it to
* Whisper's prompt budget with faster-whisper's own tokenizer.
*/
prompt?: string;
@@ -349,7 +349,7 @@ interface TranscribeParams {
* unvalidated string into the sidecar protocol just because it skips
* `parseParams` below (D-032). A string return is the 400 error text.
*
* `prompt` is the raw "Names and terms" hint (D-WS2a): at most
* `prompt` is the raw "Names and terms" hint (D-048): at most
* HINT_MAX_CHARS (the UI's textarea limit), normalized exactly as the browser
* worker does; empty after normalization = no prompt.
*/
+10 -10
View File
@@ -18,7 +18,7 @@ import {
} from "@huggingface/transformers";
// The very onnxruntime-web module Transformers.js imports (its web build
// imports "onnxruntime-web/webgpu"), so the Parakeet path shares one runtime,
// one wasm binary from /ort/, and one env (D-WS7a).
// one wasm binary from /ort/, and one env (D-051).
import * as ort from "onnxruntime-web/webgpu";
import { getModel, modelFamily, resolveDefaultDtype } from "../lib/models";
import { PARAKEET_VOCAB_FILE, parakeetFileName, parakeetFileUrl } from "../lib/parakeet";
@@ -222,7 +222,7 @@ async function probeCapabilities(
// Pipeline lifecycle (load, switch, dispose)
// ---------------------------------------------------------------------------
/** A loaded model of either family (D-WS7a): the worker dispatches on it. */
/** A loaded model of either family (D-051): the worker dispatches on it. */
type LoadedModel =
| { family: "whisper"; asr: AutomaticSpeechRecognitionPipeline }
| { family: "parakeet"; engine: ParakeetEngine };
@@ -297,7 +297,7 @@ async function createPipeline(
// graph optimization level. D-014's WASM "basic" cap worked around a
// level-2 QDQ bug in ort-web 1.26.0-dev that rejected the 8-bit
// decoders; the 1.31.0-dev build bundled with Transformers.js 4.3.2
// loads them at every level (D-WS1c, pinned in
// loads them at every level (D-043, pinned in
// tests/transformers-runtime.test.ts).
progress_callback: forwardProgress,
// D-027/D-029: sequential sub-model session loading trades load speed
@@ -324,7 +324,7 @@ async function createPipeline(
}
/**
* Parakeet load (D-WS7a): fetch vocab + decoder + encoder through the
* Parakeet load (D-051): fetch vocab + decoder + encoder through the
* Cache-API downloader (same cache and progress messages as Transformers.js,
* D-005), then create the sessions on our shared onnxruntime-web. Device
* policy is `parakeetDeviceAttempts` (WASM unless WebGPU was chosen
@@ -412,7 +412,7 @@ function load(message: LoadMessage): Promise<LoadedModel> {
}
/**
* Silero VAD for long-form file passes (D-WS3a/D-WS3b). Loaded on request
* Silero VAD for long-form file passes (D-049/D-050). Loaded on request
* (`load.vad`) once the Whisper pipeline is up — after it, so the Whisper
* attempt decides onnxruntime's thread pool (D-023/D-025). Offline it is
* used only if already cached: no network request (Review Focus 3). A failed
@@ -452,7 +452,7 @@ const CHUNK_LENGTH_S = 30;
const STRIDE_LENGTH_S = 5;
/**
* Parakeet pass (D-WS7a). English-only: `language`/`task` are ignored (the UI
* Parakeet pass (D-051). English-only: `language`/`task` are ignored (the UI
* never sends them for a non-multilingual model). File-flow passes stream one
* `partial` + one `transcribe-progress` per ≤ 30 s window — absolute seconds
* straight from the window plan, so no TranscribeProgressTracker is needed.
@@ -513,7 +513,7 @@ async function transcribe(message: TranscribeMessage): Promise<void> {
return_timestamps: true,
// With return_timestamps and no explicit max_new_tokens, Transformers.js
// 4.2–4.3 Whisper generate() takes a seek-loop path (D-021; re-measured
// on 4.3.2, D-WS1b) that re-decodes each window's tail (1–3 generate
// on 4.3.2, D-042) that re-decodes each window's tail (1–3 generate
// passes per window). Under this pipeline's own 30 s/5 s chunking that
// is pure overhead: ~1.4× slower on long
// files, duplicated boundary text, on_finalize firing once per pass
@@ -575,7 +575,7 @@ async function transcribe(message: TranscribeMessage): Promise<void> {
if (message.task !== undefined) {
options.task = message.task;
}
// "Names and terms" hint (D-WS2a): one prefix per pass, applied by the
// "Names and terms" hint (D-048): one prefix per pass, applied by the
// generate() wrapper to every 30 s window (Transformers.js ignores
// `prompt_ids`, see src/worker/prompt.ts). The wrapper is idempotent.
const promptPrefix = buildPromptPrefix(
@@ -603,7 +603,7 @@ async function transcribe(message: TranscribeMessage): Promise<void> {
}
/**
* A file pass through the long-form pipeline (D-WS3a): same partial /
* A file pass through the long-form pipeline (D-049): same partial /
* transcribe-progress / result messages as the chunked path.
*/
async function transcribeLongFormPass(
@@ -625,7 +625,7 @@ async function transcribeLongFormPass(
...LONG_FORM_DEFAULTS,
...(multilingual && message.language !== undefined && { language: message.language }),
...(multilingual && message.task !== undefined && { task: message.task }),
// Names-and-terms hint on every chunk, like the chunked path (D-WS2a).
// Names-and-terms hint on every chunk, like the chunked path (D-048).
...(message.prompt && { prompt: message.prompt }),
onPartial: (text) => post({ type: "partial", text, requestId: message.requestId }),
onProgress: (processedSeconds, totalSeconds) =>
+1 -1
View File
@@ -1,4 +1,4 @@
/** Long-form file transcription pipeline (D-WS3a). See pipeline.ts. */
/** Long-form file transcription pipeline (D-049). See pipeline.ts. */
export { DEFAULT_GUARD, LONG_FORM_DEFAULTS, transcribeLongForm } from "./pipeline";
export { DEFAULT_FALLBACK, type FallbackOptions } from "./quality";
export type {
+3 -3
View File
@@ -1,5 +1,5 @@
/**
* Long-form file transcription (D-WS3a), mirroring faster-whisper's batched
* Long-form file transcription (D-049), mirroring faster-whisper's batched
* pipeline without the batching: Silero VAD drops silence and cuts the audio
* on speech boundaries into chunks ≤ 30 s; each chunk is one Whisper decode
* (no overlapping windows to stitch); the language is detected once on the
@@ -34,10 +34,10 @@ import {
/** Whisper's window: no chunk is longer. */
const MAX_CHUNK_SECONDS = 30;
/** No guard: Whisper's own decoding (the measured default, D-WS3a). */
/** No guard: Whisper's own decoding (the measured default, D-049). */
export const DEFAULT_GUARD: RepetitionGuard = { noRepeatNgramSize: 0, repetitionPenalty: 1 };
/** The tuning the worker runs with — chosen by measurement (D-WS3a). */
/** The tuning the worker runs with — chosen by measurement (D-049). */
export const LONG_FORM_DEFAULTS: Pick<LongFormOptions, "guard" | "fallback" | "vad"> = {
guard: DEFAULT_GUARD,
fallback: false,
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Per-chunk decode quality checks for the long-form pipeline's temperature
* fallback (D-WS3a) — faster-whisper's `generate_with_fallback` criteria.
* fallback (D-049) — faster-whisper's `generate_with_fallback` criteria.
*/
export interface FallbackOptions {
+4 -4
View File
@@ -1,5 +1,5 @@
/**
* `LongFormDeps` over the existing Transformers.js runtime (D-WS3a/D-WS3b):
* `LongFormDeps` over the existing Transformers.js runtime (D-049/D-050):
* Silero VAD runs as a plain ONNX model on the same onnxruntime instance the
* Whisper pipeline uses, and Whisper decodes go through the already-loaded
* ASR pipeline — no second inference runtime. Free of worker globals so
@@ -30,7 +30,7 @@ import { SAMPLE_RATE, VAD_WINDOW_SAMPLES } from "./vad";
/**
* Silero VAD v5, ONNX export by onnx-community (MIT; `onnx/model.onnx`,
* fp32, 2.2 MB). Fetched from the Hub like Whisper weights — CORS-enabled,
* so COEP allows it — and kept in the same Cache API store (D-005, D-WS3b).
* so COEP allows it — and kept in the same Cache API store (D-005, D-050).
*/
export const VAD_MODEL_ID = "onnx-community/silero-vad";
@@ -60,7 +60,7 @@ async function vadInBrowserCache(): Promise<boolean> {
export async function loadSileroVad(options: LoadSileroVadOptions): Promise<SileroVad> {
if (options.localOnly && !(await (options.isCached ?? vadInBrowserCache)())) {
throw new Error("The speech-detection model is not downloaded (D-WS3b).");
throw new Error("The speech-detection model is not downloaded (D-050).");
}
const model = await AutoModel.from_pretrained(VAD_MODEL_ID, {
// The repo ships no config.json: the model is a bare ONNX graph.
@@ -200,7 +200,7 @@ export function createWhisperDeps(
if (request.task !== undefined) generate.task = request.task;
}
if (request.prompt) {
// Same decoder-prompt mechanism as the chunked path (D-WS2a,
// Same decoder-prompt mechanism as the chunked path (D-048,
// src/worker/prompt.ts); the wrapper strips the prompt from the output.
const prefix = promptPrefixFor(request.prompt);
if (prefix.length > 0) generate[PROMPT_PREFIX_KWARG] = prefix;
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* The long-form pipeline's interface (D-WS3a, coordination plan 001-0). The
* The long-form pipeline's interface (D-049, coordination plan 001-0). The
* pipeline is model-agnostic: everything that runs a model comes in through
* `LongFormDeps`, so a different implementation (a future Rust→WASM build,
* or batched decoding) can replace either side without touching the other.
@@ -46,7 +46,7 @@ export interface LongFormDeps {
detectLanguage?(audio: Float32Array): Promise<string>;
}
/** Decoder-side repetition guard (D-035's proposal; D-WS3a measured it). */
/** Decoder-side repetition guard (D-035's proposal; D-049 measured it). */
export interface RepetitionGuard {
noRepeatNgramSize: number;
repetitionPenalty: number;
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Speech segmentation for the long-form pipeline (D-WS3a). Pure: turns
* Speech segmentation for the long-form pipeline (D-049). Pure: turns
* per-window Silero VAD speech probabilities into speech spans, and packs the
* spans into ≤ 30 s chunks cut on speech boundaries. A port of faster-whisper
* 1.2.1 `vad.py` (`get_speech_timestamps` after the model call, and the
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Weight-file download + cache for engine paths that don't go through
* Transformers.js's own loader (Parakeet, D-WS7a).
* Transformers.js's own loader (Parakeet, D-051).
*
* Files land in the same Cache Storage cache Transformers.js uses
* (`transformers-cache`, D-005), keyed by their Hub URL, so the downloaded
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Opus-MT (MarianMT) through Transformers.js — the model half of transcript
* translation (D-WS4b). Shared by the browser translate worker and the
* translation (D-045). Shared by the browser translate worker and the
* onnxruntime-node measurement script / real-model test, so all three run
* the exact same pipeline options.
*/
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Stand-in for the bare `onnxruntime-web` import inside parakeet.js
* (D-WS7a). parakeet.js depends on its own onnxruntime-web 1.24.1, imported
* (D-051). parakeet.js depends on its own onnxruntime-web 1.24.1, imported
* only by its `initOrt` helper, which this app never calls — but a bundler
* would still pull that second runtime into the worker. `scripts/ortAlias.ts`
* redirects the import here, to the one runtime Transformers.js uses.
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Parakeet TDT engine (Decisions.md D-WS7a): the non-Whisper inference path
* Parakeet TDT engine (Decisions.md D-051): the non-Whisper inference path
* of the browser engine, used by the inference worker for catalog entries
* with `family: "parakeet"`.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Whisper decoder prompts (the "Names and terms" hint, D-WS2a) on top of the
* Whisper decoder prompts (the "Names and terms" hint, D-048) on top of the
* Transformers.js ASR pipeline.
*
* Transformers.js 4.2.0 (and 4.3.2) declare `prompt_ids` in the Whisper
+8 -8
View File
@@ -59,7 +59,7 @@ export interface LoadMessage {
sequentialSessionLoad?: boolean;
/**
* Also make the Silero VAD model (2.2 MB) available for long-form file
* transcription (D-WS3a/D-WS3b): cache-first, fetched from the Hub only
* transcription (D-049/D-050): cache-first, fetched from the Hub only
* when online — offline it is used only if already cached. Sent with the
* same user-initiated loads as the Whisper weights (D-020).
*/
@@ -96,14 +96,14 @@ export interface TranscribeMessage {
stream?: boolean;
/**
* Optional "Names and terms" vocabulary hint, raw as the user typed it
* (D-WS2a). The worker normalizes it, truncates it to Whisper's prompt
* (D-048). The worker normalizes it, truncates it to Whisper's prompt
* budget and feeds it as a decoder prompt to every window of this pass
* (src/worker/prompt.ts). Empty/omitted = no prompt.
*/
prompt?: string;
/**
* Run this file pass through the long-form pipeline (VAD-cut chunks,
* language pinned once, D-WS3a). Honoured only when the VAD model loaded;
* language pinned once, D-049). Honoured only when the VAD model loaded;
* otherwise the classic 30 s/5 s chunked path runs (`choosePipeline`).
*/
longForm?: boolean;
@@ -288,7 +288,7 @@ export function deviceAttempts(
}
/**
* Which file-transcription path a pass takes (D-WS3a/D-WS3b): long-form only
* Which file-transcription path a pass takes (D-049/D-050): long-form only
* when the pass asked for it and the VAD model is loaded. A missing VAD model
* (offline, never downloaded) silently keeps the classic chunked path — it is
* a fallback, never an error.
@@ -301,7 +301,7 @@ export function choosePipeline(input: {
}
/**
* Device attempts for the Parakeet engine path (D-WS7a). Narrower than
* Device attempts for the Parakeet engine path (D-051). Narrower than
* `deviceAttempts`: "auto" always means WASM int8 — the only measured path —
* because WebGPU int8 is reported broken and WebGPU needs the separate
* 1.24 GB fp16 encoder, so auto-selecting it would silently double the
@@ -496,7 +496,7 @@ export function applyLoadProgress(
/**
* One generate pass's slice of the audio, for
* `TranscribeProgressTracker.forWindows` (variable-length chunks, D-WS3a).
* `TranscribeProgressTracker.forWindows` (variable-length chunks, D-049).
*/
export interface ProgressWindow {
/** Window-relative seconds → absolute seconds on the file's timeline. */
@@ -521,7 +521,7 @@ export interface ProgressWindow {
* ends. This tracker turns those into absolute seconds that are monotone
* (windows overlap, so a later window's early timestamps can precede an
* earlier window's late ones — regressions are swallowed) and clamped to
* [0, totalSeconds]. The long-form pipeline (D-WS3a) uses `forWindows`
* [0, totalSeconds]. The long-form pipeline (D-049) uses `forWindows`
* instead: its windows are variable-length speech chunks with skipped
* silence between them.
*/
@@ -551,7 +551,7 @@ export class TranscribeProgressTracker {
/**
* Variable-length windows — the long-form pipeline's speech chunks
* (D-WS3a): each maps its own relative time onto the file's timeline, and
* (D-049): each maps its own relative time onto the file's timeline, and
* skipped silence between chunks counts as processed once a chunk ends.
*/
static forWindows(
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Transcript-translation Web Worker (D-WS4b): Opus-MT through
* Transcript-translation Web Worker (D-045): Opus-MT through
* Transformers.js, off the main thread (D-003).
*
* A dedicated worker rather than a message type on the Whisper worker:
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Typed postMessage protocol between the UI and the transcript-translation
* worker (`src/worker/translate.worker.ts`, D-WS4b). Kept apart from the
* worker (`src/worker/translate.worker.ts`, D-045). Kept apart from the
* Whisper worker's protocol: separate worker, separate lifetime. Free of
* browser globals so `bun test` covers the guards.
*/
+2 -2
View File
@@ -77,7 +77,7 @@ describe("BrowserEngine", () => {
expect("modelId" in message).toBe(false);
});
test("forwards the vocabulary hint as `prompt`, and omits an empty one (D-WS2a)", () => {
test("forwards the vocabulary hint as `prompt`, and omits an empty one (D-048)", () => {
const worker = new FakeWorker();
const engine = new BrowserEngine(worker);
void engine.transcribe(new Float32Array(4), { prompt: "Maxwell\nVoid" });
@@ -88,7 +88,7 @@ describe("BrowserEngine", () => {
expect("prompt" in worker.posted[2]!.message).toBe(false);
});
test("forwards the long-form request only when set (D-WS3a)", () => {
test("forwards the long-form request only when set (D-049)", () => {
const worker = new FakeWorker();
const engine = new BrowserEngine(worker);
void engine.transcribe(new Float32Array(4), { longForm: true });
+1 -1
View File
@@ -99,7 +99,7 @@ describe("classifyError", () => {
});
});
describe("classifyError — transcript translation (D-WS4b)", () => {
describe("classifyError — transcript translation (D-045)", () => {
test("out of memory while translating suggests closing tabs, not a smaller Whisper model", () => {
const classified = classifyError("RangeError: Out of memory", { stage: "translate" });
expect(classified.kind).toBe("out-of-memory");
+1 -1
View File
@@ -1,7 +1,7 @@
"""faster_whisper stand-in that reports how server-py/sidecar.py called it.
Lets tests/transcribe.test.ts check the real sidecar's request routing
(D-WS5a: file → BatchedInferencePipeline + VAD, live → sequential) without
(D-047: file → BatchedInferencePipeline + VAD, live → sequential) without
CTranslate2 or model weights. Every segment's text describes the call:
" seq model=<name> loads=<n> <sorted kwargs>" WhisperModel.transcribe
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import { join } from "node:path";
import { fitHintToBudget, normalizeHint, PROMPT_TOKEN_BUDGET } from "../src/lib/hint";
// D-WS2a: the server engine truncates the hint in Python (server-py/sidecar.py,
// D-048: the server engine truncates the hint in Python (server-py/sidecar.py,
// with faster-whisper's tokenizer) and the browser engine in TypeScript
// (src/lib/hint.ts, with the Transformers.js tokenizer). Both must apply the
// same rule; with the same deterministic fake tokenizer they must agree
+2 -2
View File
@@ -67,13 +67,13 @@ describe("hint storage", () => {
});
});
describe("fitHintToBudget (D-WS2a truncation rule)", () => {
describe("fitHintToBudget (D-048 truncation rule)", () => {
test("empty hint means no prompt", () => {
expect(fitHintToBudget(" \n", charEncode)).toEqual([]);
});
test("a hint within budget is encoded as a space-led sentence ending in a full stop", () => {
// Measured (D-WS2a): an unterminated list prompt suppresses punctuation
// Measured (D-048): an unterminated list prompt suppresses punctuation
// and triggers repetition loops on smaller models; a full stop does not.
expect(fitHintToBudget("Maxwell\nVoices of the Void", charEncode)).toEqual(
charEncode(" Maxwell, Voices of the Void."),
+4 -4
View File
@@ -329,7 +329,7 @@ describe("live pipeline accuracy (real model, real recordings)", () => {
});
// ---------------------------------------------------------------------------
// Vocabulary hint (D-WS2a): the worker's real prompt code on real weights
// Vocabulary hint (D-048): the worker's real prompt code on real weights
// ---------------------------------------------------------------------------
const HINT = "Maxwell, Voices of the Void, Slenderman, RuneScape, UFO";
@@ -372,7 +372,7 @@ async function transcribeWithHint(
return single.text ?? "";
}
describe("vocabulary hint (real model, D-WS2a)", () => {
describe("vocabulary hint (real model, D-048)", () => {
test.skipIf(!available)(
"a hint keeps the transcript intact, keeps the hinted names, and never leaks into the text",
async () => {
@@ -429,7 +429,7 @@ describe("vocabulary hint (real model, D-WS2a)", () => {
});
// ---------------------------------------------------------------------------
// File-mode accuracy of the long-form pipeline (D-WS3a): Silero VAD chunks,
// File-mode accuracy of the long-form pipeline (D-049): Silero VAD chunks,
// one decode per ≤ 30 s chunk — the path behind "Skip silence". Same
// per-clip bars as the live and chunked file paths above.
// ---------------------------------------------------------------------------
@@ -462,7 +462,7 @@ describe("long-form file pipeline accuracy (real model, real recordings)", () =>
if (clip.language !== "en") {
// "Auto-detect" in the UI: the chunked path forces English here
// (Transformers.js 4.2 has no detection); long-form detects once
// on the first speech chunk and pins it (D-WS3a).
// on the first speech chunk and pins it (D-049).
// Detection must not cost an extra encoder pass: the first
// chunk's decode reuses the encoder output detection computed.
const model = asr.model as unknown as Record<string, unknown>;
+1 -1
View File
@@ -18,7 +18,7 @@ function memoryStorage(initial: Record<string, string> = {}) {
};
}
describe("long-form setting persistence (D-WS3a)", () => {
describe("long-form setting persistence (D-049)", () => {
test("defaults when nothing is stored or storage is unavailable", () => {
expect(loadLongFormChoice(memoryStorage())).toBe(LONG_FORM_DEFAULT);
expect(loadLongFormChoice(null)).toBe(LONG_FORM_DEFAULT);
+1 -1
View File
@@ -75,7 +75,7 @@ describe.skipIf(!available)("Silero VAD on the Transformers.js runtime (real mod
});
});
describe("createWhisperDeps decode with the names-and-terms hint (D-WS2a × D-WS3a)", () => {
describe("createWhisperDeps decode with the names-and-terms hint (D-048 × D-049)", () => {
/** A fake ASR pipeline that records the generate options of each call. */
function fakeAsr() {
const calls: Record<string, unknown>[] = [];
+1 -1
View File
@@ -106,7 +106,7 @@ describe("shouldWarnBeforeLoad", () => {
});
});
describe("desktop-only models (D-WS7a)", () => {
describe("desktop-only models (D-051)", () => {
const parakeet = MODELS.find((model) => model.id === PARAKEET_MODEL_ID)!;
const phone = { userAgent: ANDROID_UA, maxTouchPoints: 5, deviceMemoryGB: 8 };
const ipad = { userAgent: IPAD_DESKTOP_UA, maxTouchPoints: 5, deviceMemoryGB: undefined };
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import { fetchModelFile, type ModelFileCacheStorage } from "../src/worker/modelFiles";
import type { LoadProgressMessage } from "../src/worker/protocol";
// Cache-API-backed weight download for the Parakeet path (D-WS7a, D-005).
// Cache-API-backed weight download for the Parakeet path (D-051, D-005).
const URL_A = "https://huggingface.co/org/model/resolve/rev/encoder.onnx";
+8 -8
View File
@@ -39,7 +39,7 @@ describe("MODELS catalog", () => {
"onnx-community/whisper-medium-ONNX",
"onnx-community/whisper-large-v3-turbo",
`${DANISH_MODELS_HF_OWNER}/whisper-small-danish-ONNX`,
// Not Whisper: the D-WS7a Parakeet engine path, last so it never
// Not Whisper: the D-051 Parakeet engine path, last so it never
// becomes anyone's accidental default.
"ysdede/parakeet-tdt-0.6b-v3-onnx",
]);
@@ -80,7 +80,7 @@ describe("MODELS catalog", () => {
});
test("every Whisper model maps to a faster-whisper name for the server engine (D-013)", () => {
// The Danish conversions (D-WS6a) have no faster-whisper alias; their
// The Danish conversions (D-046) have no faster-whisper alias; their
// server-engine name is a Hub repo id — covered by the Danish suite below.
for (const model of WHISPER_MODELS.filter((m) => !m.id.startsWith(`${DANISH_MODELS_HF_OWNER}/`))) {
// The onnx-community ids and faster-whisper's aliases share the same
@@ -98,7 +98,7 @@ describe("MODELS catalog", () => {
});
});
describe("Danish fine-tune conversions (D-WS6a)", () => {
describe("Danish fine-tune conversions (D-046)", () => {
const owner = `${DANISH_MODELS_HF_OWNER}/`;
const danish = MODELS.filter((model) => model.id.startsWith(owner));
@@ -131,7 +131,7 @@ describe("Danish fine-tune conversions (D-WS6a)", () => {
expect(smallDa!.decoderParamsM).toBe(small.decoderParamsM);
});
test("default to the dtypes measured on the converted files (D-WS6a)", () => {
test("default to the dtypes measured on the converted files (D-046)", () => {
// da_festuge on onnxruntime-node: q4 matched or beat q8 and was faster for
// all three, small included; fp16 encoders did not overflow (D-022 check).
for (const model of danish) {
@@ -141,7 +141,7 @@ describe("Danish fine-tune conversions (D-WS6a)", () => {
decoder_model_merged: "q4",
});
expect(resolveDefaultDtype(model.id, "webnn")).toBe("fp16");
expect(model.defaults.wasm.note).toContain("D-WS6a");
expect(model.defaults.wasm.note).toContain("D-046");
}
});
});
@@ -270,7 +270,7 @@ describe("WHISPER_LANGUAGES", () => {
});
});
describe("canTranslate (D-WS4a)", () => {
describe("canTranslate (D-044)", () => {
test("Whisper translate is offered only where OpenAI trained it", () => {
const translating = MODELS.filter((model) => model.canTranslate).map((model) => model.id);
// large-v3-turbo was not trained for translation (OpenAI model card);
@@ -290,7 +290,7 @@ describe("canTranslate (D-WS4a)", () => {
});
});
describe("taskForModel (D-WS4a)", () => {
describe("taskForModel (D-044)", () => {
test("a stale translate resets to transcribe on a model that cannot translate", () => {
expect(taskForModel("onnx-community/whisper-large-v3-turbo", "translate")).toBe("transcribe");
expect(taskForModel("onnx-community/whisper-small.en", "translate")).toBe("transcribe");
@@ -306,7 +306,7 @@ describe("taskForModel (D-WS4a)", () => {
});
});
describe("Parakeet catalog entry (D-WS7a)", () => {
describe("Parakeet catalog entry (D-051)", () => {
const parakeet = getModel(PARAKEET_MODEL_ID)!;
test("family defaults to whisper when absent", () => {
+3 -3
View File
@@ -69,7 +69,7 @@ The worker now caps WASM sessions at `graphOptimizationLevel: "basic"`
outside the browser. **Re-run this step with Backend = WASM to confirm in a
real browser.**
**Update (2026-10-30, D-WS1c):** the Transformers.js 4.3.2 bump brings an
**Update (2026-10-30, D-043):** the Transformers.js 4.3.2 bump brings an
onnxruntime-web build without that bug, so the cap is gone and WASM sessions
run at the default level again. Re-run this step with Backend = WASM (Whisper
Tiny and Tiny.en) to confirm the session still loads in a real browser.
@@ -111,7 +111,7 @@ Result: Works.
Result: Works
## 5. Transcript translation offline (D-WS4b)
## 5. Transcript translation offline (D-045)
1. Online: transcribe anything, open **Translate transcript…**, pick a
direction, click **Translate**. The hint said "Downloads a translation
@@ -129,7 +129,7 @@ Result: Works
Result: _pending (needs a real browser)_
## 6. "Skip silence" (long-form pipeline, D-WS3a/D-WS3b)
## 6. "Skip silence" (long-form pipeline, D-049/D-050)
1. Online, open **Advanced**, tick **Skip silence in uploaded files**, and
transcribe a file. DevTools → Network shows one request for
+1 -1
View File
@@ -15,7 +15,7 @@ import {
import { wordErrorRate } from "./dsp";
/**
* Real-model accuracy of the Parakeet engine path (D-WS7a), opt-in:
* Real-model accuracy of the Parakeet engine path (D-051), opt-in:
*
* PARAKEET_REAL=1 bun test tests/parakeet-real-model.test.ts
*
+1 -1
View File
@@ -12,7 +12,7 @@ import {
} from "../src/lib/parakeet";
import { transcriptToSrt, transcriptToTimestampedTxt } from "../src/lib/export";
// Pure Parakeet engine logic (D-WS7a) — no model, no runtime.
// Pure Parakeet engine logic (D-051) — no model, no runtime.
const RATE = 16_000;
+2 -2
View File
@@ -35,7 +35,7 @@ function tokens(output: unknown): number[] {
return (output as Tensor).tolist()[0]!.map(Number);
}
describe("installPromptSupport (D-WS2a)", () => {
describe("installPromptSupport (D-048)", () => {
test("without a prompt kwarg, generate is called exactly as before", async () => {
const model = fakeModel([50258, 50259, 50360]);
installPromptSupport(model);
@@ -117,7 +117,7 @@ describe("buildPromptPrefix", () => {
]);
});
test("a huge hint stays within the 120-token budget (D-WS2a)", () => {
test("a huge hint stays within the 120-token budget (D-048)", () => {
const prefix = buildPromptPrefix(tokenizer, "Maxwell, ".repeat(500));
expect(prefix.length).toBeLessThanOrEqual(121);
});
+3 -3
View File
@@ -83,7 +83,7 @@ describe("isUiToWorkerMessage", () => {
});
});
describe("choosePipeline (D-WS3a/D-WS3b)", () => {
describe("choosePipeline (D-049/D-050)", () => {
test("long-form only when asked for and the VAD model loaded", () => {
expect(choosePipeline({ longForm: true, vadReady: true })).toBe("longform");
// Offline / never downloaded: the old path, never an error (Review Focus 3).
@@ -248,7 +248,7 @@ describe("deviceAttempts (D-004, D-017)", () => {
});
});
describe("parakeetDeviceAttempts (D-WS7a)", () => {
describe("parakeetDeviceAttempts (D-051)", () => {
const everything = { webnn: true, webgpu: true, webgpuF16: true };
test("auto never picks an accelerator for Parakeet", () => {
@@ -322,7 +322,7 @@ describe("TranscribeProgressTracker", () => {
expect(tracker.finalizeWindow()).toBeNull(); // 20 clamps to 8, not an advance
});
describe("forWindows (variable-length VAD chunks, D-WS3a)", () => {
describe("forWindows (variable-length VAD chunks, D-049)", () => {
// Two speech chunks with a skipped 15 s silence between them: [0–20 s]
// and [35–60 s] of a 70 s file.
const windows = [
+3 -3
View File
@@ -22,7 +22,7 @@ FW_INTEGRATION=1 bun test tests/transcribe.test.ts
Runs the real sidecar end to end (spawn → ready handshake → `tiny.en` load →
transcribe → done), then the first 60 s of `tests/audio_files/en_alien.opus`
(needs `ffmpeg`) in both decoding modes (D-WS5a): `file` (faster-whisper's
(needs `ffmpeg`) in both decoding modes (D-047): `file` (faster-whisper's
`BatchedInferencePipeline` + Silero VAD, what `POST /api/transcribe` uses) and
`live` (sequential, what server-live passes use). First run downloads ~75 MB
of weights from the Hub. Point `SIDECAR_PYTHON` at a virtualenv's python if
@@ -52,11 +52,11 @@ burst, segments in bursts — and cancels a batched run after its first burst.
5. Transcribe the same clip through the server engine.
- [ ] Segments stream in as JSON lines (not one blob at the end). With the
batched pipeline (D-WS5a) they arrive in bursts — one burst per 8
batched pipeline (D-047) they arrive in bursts — one burst per 8
speech chunks (≈ up to 4 min of audio) — with blank heartbeat lines
every 5 s in between; on a clip under ~30 s of speech, everything
arrives as a single segment at the end. Each segment covers one VAD
speech chunk (up to 30 s), not one sentence — expected (D-WS5a).
speech chunk (up to 30 s), not one sentence — expected (D-047).
- [ ] Cancelling a long server transcription (abort / page refresh) stops
the sidecar within one burst; the next request then starts normally.
- [ ] Both transcripts agree in content (minor punctuation/casing drift
+2 -2
View File
@@ -125,7 +125,7 @@ describe("createLiveWebSocket", () => {
// wait for the payload itself, or a loaded machine races past it.
await waitFor(() => sidecar.audioPayloads.length > 0);
expect(sidecar.requests[0]).toEqual(
// Live passes are short: sequential decoding, no batching/VAD (D-WS5a).
// Live passes are short: sequential decoding, no batching/VAD (D-047).
expect.objectContaining({ model: "base", language: "da", task: "translate", mode: "live" }),
);
// The bytes written to the temp file are exactly the committed segment's
@@ -142,7 +142,7 @@ describe("createLiveWebSocket", () => {
await waitFor(async () => (await readdir(tempDir)).length === 0);
});
test("every pass of a session carries the session's vocabulary hint (D-WS2a)", async () => {
test("every pass of a session carries the session's vocabulary hint (D-048)", async () => {
const tempDir = await makeTempDir();
const sidecar = fakeSidecar();
const handler = createLiveWebSocket({ sidecar });
+12 -12
View File
@@ -168,7 +168,7 @@ describe("parseLanguageAndTask", () => {
});
});
describe("parseLanguageAndTask: vocabulary hint (D-WS2a)", () => {
describe("parseLanguageAndTask: vocabulary hint (D-048)", () => {
const parse = (query: string) => parseLanguageAndTask(new URL(`http://localhost/x${query}`));
test("normalizes the hint like the worker does", () => {
@@ -357,7 +357,7 @@ describe("POST /api/transcribe", () => {
});
test("heartbeat blank lines keep a quiet stream alive before the first segment (D-021)", async () => {
// The batched pipeline (D-WS5a) runs VAD over the whole file and decodes a
// The batched pipeline (D-047) runs VAD over the whole file and decodes a
// full batch before its first segment, so the quiet stretch is longer.
const handlers = createTranscribeHandlers({
sidecar: fakeSidecar({ delayMs: 200 }),
@@ -375,7 +375,7 @@ describe("POST /api/transcribe", () => {
expect(text).toContain('"type":"result"');
});
test("passes the vocabulary hint to the sidecar; 400 for an over-long one (D-WS2a)", async () => {
test("passes the vocabulary hint to the sidecar; 400 for an over-long one (D-048)", async () => {
const sidecar = fakeSidecar();
const handlers = createTranscribeHandlers({
sidecar,
@@ -477,7 +477,7 @@ describe("createSidecar (fake python sidecar)", () => {
expect(segments[1]!.text).toContain("model=ok bytes=12 lang=da task=translate");
expect(segments[1]!.text).not.toContain("prompt=");
// The vocabulary hint crosses the JSON-lines protocol intact (D-WS2a).
// The vocabulary hint crosses the JSON-lines protocol intact (D-048).
const prompted: SidecarSegment[] = [];
await sidecar.transcribe(
{ model: "ok", audioPath, prompt: "Århus, Voices of the Void" },
@@ -554,11 +554,11 @@ describe("createSidecar (degradation, D-013)", () => {
// ---------------------------------------------------------------------------
// The real server-py/sidecar.py against a stubbed faster_whisper/numpy
// (tests/fixtures/stub_fw): request routing, model cache, cancel (D-WS5a).
// (tests/fixtures/stub_fw): request routing, model cache, cancel (D-047).
// `-S` keeps real site-packages out, so this runs the same everywhere.
// ---------------------------------------------------------------------------
describe("server-py/sidecar.py (stubbed faster-whisper, D-WS5a)", () => {
describe("server-py/sidecar.py (stubbed faster-whisper, D-047)", () => {
const STUB_DIR = join(import.meta.dir, "fixtures/stub_fw");
const stubbedSidecar = () =>
createSidecar(["env", `PYTHONPATH=${STUB_DIR}`, "python3", "-S", REAL_SIDECAR]);
@@ -590,7 +590,7 @@ describe("server-py/sidecar.py (stubbed faster-whisper, D-WS5a)", () => {
expect(explicit.texts[0]).toContain("batched model=m1");
expect(explicit.texts[0]).toContain("vad_filter=True");
expect(explicit.texts[0]).toContain("batch_size=8");
// Tuned Silero VAD: the default (0.5 / 400 ms) dropped quiet speech (D-WS5a).
// Tuned Silero VAD: the default (0.5 / 400 ms) dropped quiet speech (D-047).
expect(explicit.texts[0]).toContain("vad_parameters={'threshold': 0.2, 'speech_pad_ms': 800}");
expect(explicit.texts[0]).toContain("language=da");
expect(explicit.texts[0]).toContain("task=transcribe");
@@ -599,7 +599,7 @@ describe("server-py/sidecar.py (stubbed faster-whisper, D-WS5a)", () => {
expect(implicit.texts[0]).toContain("batched model=m1");
});
test("the vocabulary hint reaches the batched pipeline as text, and live passes as ids (D-WS2a, D-WS5a)", async () => {
test("the vocabulary hint reaches the batched pipeline as text, and live passes as ids (D-048, D-047)", async () => {
const sidecar = stubbedSidecar();
const file = await run(sidecar, { model: "m1", mode: "file", prompt: "Maxwell, Voices of the Void" });
expect(file.texts).toHaveLength(1);
@@ -611,7 +611,7 @@ describe("server-py/sidecar.py (stubbed faster-whisper, D-WS5a)", () => {
expect(live.texts[0]).toContain("initial_prompt=[32, 77, 97, 120, 119, 101, 108, 108, 46]");
});
test("live mode stays sequential without VAD (short passes, D-WS5a)", async () => {
test("live mode stays sequential without VAD (short passes, D-047)", async () => {
const sidecar = stubbedSidecar();
const { texts } = await run(sidecar, { model: "m1", mode: "live", task: "translate" });
expect(texts[0]).toContain("seq model=m1");
@@ -681,7 +681,7 @@ describe.skipIf(!process.env.FW_INTEGRATION)("real faster-whisper sidecar", () =
);
test(
"file mode (batched + VAD) and live mode both transcribe real speech (D-WS5a)",
"file mode (batched + VAD) and live mode both transcribe real speech (D-047)",
async () => {
const sidecar = createSidecar();
// First 60 s of en_alien as raw float32 16 kHz mono — what the browser uploads.
@@ -718,7 +718,7 @@ describe.skipIf(!process.env.FW_INTEGRATION)("real faster-whisper sidecar", () =
}
test(
"a long file streams in bursts behind heartbeats over HTTP (D-021, D-WS5a)",
"a long file streams in bursts behind heartbeats over HTTP (D-021, D-047)",
async () => {
const handlers = createTranscribeHandlers({
sidecar: createSidecar(),
@@ -756,7 +756,7 @@ describe.skipIf(!process.env.FW_INTEGRATION)("real faster-whisper sidecar", () =
expect(segmentLines.at(-1)!.at - segmentLines[0]!.at).toBeGreaterThan(100);
expect(lines.at(-1)!.line).toContain('"type":"result"');
console.log(
"[D-WS5a] segment arrival (ms):",
"[D-047] segment arrival (ms):",
segmentLines.map(({ at }) => Math.round(at)).join(" "),
);
},
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { TranscriptionOptions } from "../src/components/TranscriptionOptions";
// D-WS4a: Whisper's own "Translate to English" only where the model can do it.
// D-044: Whisper's own "Translate to English" only where the model can do it.
function render(canTranslate: boolean): string {
return renderToStaticMarkup(
<TranscriptionOptions
+5 -5
View File
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
/**
* Tripwires for the patched, pinned Transformers.js runtime (D-WS1a).
* Tripwires for the patched, pinned Transformers.js runtime (D-040).
* `@huggingface/transformers` carries a `bun patch` (D-027/D-028) keyed by
* its exact version; a bump that forgets to regenerate the patch would leave
* the app working but silently bring back the mobile OOM. These checks fail
@@ -48,22 +48,22 @@ describe("@huggingface/transformers runtime", () => {
expect(body).toContain("for (const name of Object.keys(names))");
});
test("Whisper generate still takes the seek loop when max_new_tokens is unset (D-021, D-WS1b)", () => {
test("Whisper generate still takes the seek loop when max_new_tokens is unset (D-021, D-042)", () => {
// The worker's `max_new_tokens: 440` exists only to dodge this branch.
// If upstream changes the condition, re-measure D-021 before touching it.
const source = read(join(PKG_DIR, "src/models/whisper/modeling_whisper.js"));
expect(source).toContain("if (generation_config.return_timestamps && !kwargs.max_new_tokens)");
});
test("onnxruntime-web is the build D-WS1c re-tested 8-bit decoders on at the default optimization level", () => {
test("onnxruntime-web is the build D-043 re-tested 8-bit decoders on at the default optimization level", () => {
// D-014's "basic" cap was dropped because this build loads the q8 merged
// decoders at every graphOptimizationLevel. A different build needs the
// D-WS1c session-creation check re-run before it ships.
// D-043 session-creation check re-run before it ships.
const ortWeb = JSON.parse(read(join(ROOT, "node_modules/onnxruntime-web/package.json"))).version;
expect(ortWeb).toBe("1.31.0-dev.20260914-8d85527a0");
});
test("worker leaves WASM sessions at the default graph optimization level (D-WS1c)", () => {
test("worker leaves WASM sessions at the default graph optimization level (D-043)", () => {
const worker = read(join(ROOT, "src/worker/inference.worker.ts"));
expect(worker).not.toContain("graphOptimizationLevel");
});
+1 -1
View File
@@ -9,7 +9,7 @@ import {
import { loadOpusMt, type OpusMtTranslator } from "../src/worker/opusMt";
/**
* Real Opus-MT translation on onnxruntime-node (D-WS4b), opt-in:
* Real Opus-MT translation on onnxruntime-node (D-045), opt-in:
*
* MT_INTEGRATION=1 flock /tmp/whisper-heavy.lock bun test tests/translate-real-model.test.ts
*
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { TranslatePanel, translateStatusText } from "../src/components/TranslatePanel";
// D-WS4b: the panel's copy, before any worker exists (static render).
// D-045: the panel's copy, before any worker exists (static render).
const danish = { text: "Det er ikke noget med at vi skal på ferie.", chunks: [] };
function render(downloaded: string[] = [], hint?: string) {
+1 -1
View File
@@ -12,7 +12,7 @@ import {
} from "../src/lib/translation";
import type { TranscriptChunk } from "../src/worker/protocol";
// Pure planning for transcript translation (D-WS4b) — no model, no network.
// Pure planning for transcript translation (D-045) — no model, no network.
describe("translateUnits", () => {
test("translates every piece in order, batched, and rejoins per unit", async () => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import { join } from "node:path";
import { ortAliasPlugin } from "../scripts/ortAlias";
// D-WS7a: the Parakeet path must not bring a second onnxruntime into the
// D-051: the Parakeet path must not bring a second onnxruntime into the
// inference worker. parakeet.js depends on its own onnxruntime-web (1.24.1);
// the app runs the one Transformers.js pins. Unminified bundles carry a
// `// node_modules/<path>` comment per bundled module, so count runtimes.