From 0632f7843b77f0e23984ef5ce6119db9c3c8c41c Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 30 Oct 2026 01:47:27 +0100 Subject: [PATCH] =?UTF-8?q?Renumber=20spec=20001=20decision=20placeholders?= =?UTF-8?q?=20to=20D-040=E2=80=93D-051;=20record=20the=20Danish=20publicat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 6 +-- README.md | 2 +- docs/Architecture.md | 40 ++++++++--------- docs/Decisions.md | 44 +++++++++++-------- docs/WorkBreakdown.md | 32 +++++++------- docs/plans/001-0-coordination.md | 2 +- scripts/bench-parakeet.ts | 2 +- scripts/bench-translate.ts | 2 +- scripts/build.ts | 4 +- scripts/measure-longform.ts | 2 +- scripts/ortAlias.ts | 2 +- server-py/sidecar.py | 16 +++---- src/App.tsx | 26 +++++------ src/components/AdvancedOptions.tsx | 4 +- src/components/ModelPicker.tsx | 2 +- src/components/StorageManager.tsx | 2 +- src/components/TranscriptionOptions.tsx | 4 +- src/components/TranslatePanel.tsx | 2 +- src/components/VocabularyHint.tsx | 4 +- src/index.css | 8 ++-- src/index.ts | 6 +-- src/lib/engine.ts | 6 +-- src/lib/errors.ts | 2 +- src/lib/hint.ts | 8 ++-- src/lib/longForm.ts | 4 +- src/lib/mobileMemoryGuard.ts | 2 +- src/lib/models.ts | 36 +++++++-------- src/lib/parakeet.ts | 2 +- src/lib/translation.ts | 4 +- src/server/live.ts | 4 +- src/server/transcribe.ts | 6 +-- src/worker/inference.worker.ts | 20 ++++----- src/worker/longform/index.ts | 2 +- src/worker/longform/pipeline.ts | 6 +-- src/worker/longform/quality.ts | 2 +- src/worker/longform/transformers.ts | 8 ++-- src/worker/longform/types.ts | 4 +- src/worker/longform/vad.ts | 2 +- src/worker/modelFiles.ts | 2 +- src/worker/opusMt.ts | 2 +- src/worker/ortShim.ts | 2 +- src/worker/parakeetEngine.ts | 2 +- src/worker/prompt.ts | 2 +- src/worker/protocol.ts | 16 +++---- src/worker/translate.worker.ts | 2 +- src/worker/translateProtocol.ts | 2 +- tests/engine.test.ts | 4 +- tests/errors.test.ts | 2 +- .../stub_fw/faster_whisper/__init__.py | 2 +- tests/hint-parity.test.ts | 2 +- tests/hint.test.ts | 4 +- tests/live-real-model.test.ts | 8 ++-- tests/longform-setting.test.ts | 2 +- tests/longform-transformers.test.ts | 2 +- tests/mobileMemoryGuard.test.ts | 2 +- tests/modelFiles.test.ts | 2 +- tests/models.test.ts | 16 +++---- tests/offline.md | 6 +-- tests/parakeet-real-model.test.ts | 2 +- tests/parakeet.test.ts | 2 +- tests/prompt.test.ts | 4 +- tests/protocol.test.ts | 6 +-- tests/server-engine.md | 6 +-- tests/server-live.test.ts | 4 +- tests/transcribe.test.ts | 24 +++++----- tests/transcriptionOptions.test.tsx | 2 +- tests/transformers-runtime.test.ts | 10 ++--- tests/translate-real-model.test.ts | 2 +- tests/translatePanel.test.tsx | 2 +- tests/translation.test.ts | 2 +- tests/workerBundle.test.ts | 2 +- 71 files changed, 244 insertions(+), 236 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1f07fed..16e5dbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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%2Ftransformers@4.3.2.patch` (D-027/D-028, D-WS1a) and kept out of runtime deps so the + `patches/@huggingface%2Ftransformers@4.3.2.patch` (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/`). diff --git a/README.md b/README.md index b1cf3e9..23a08fc 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/Architecture.md b/docs/Architecture.md index b5e768d..b74a2d4 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -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. diff --git a/docs/Decisions.md b/docs/Decisions.md index dcde821..2098ac9 100644 --- a/docs/Decisions.md +++ b/docs/Decisions.md @@ -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 `..`. **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 diff --git a/docs/WorkBreakdown.md b/docs/WorkBreakdown.md index 1b9cffd..b052419 100644 --- a/docs/WorkBreakdown.md +++ b/docs/WorkBreakdown.md @@ -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 diff --git a/docs/plans/001-0-coordination.md b/docs/plans/001-0-coordination.md index 9d25971..b2b992a 100644 --- a/docs/plans/001-0-coordination.md +++ b/docs/plans/001-0-coordination.md @@ -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 — …` - (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 `. Light commands diff --git a/scripts/bench-parakeet.ts b/scripts/bench-parakeet.ts index 7e645fd..9e6f4db 100644 --- a/scripts/bench-parakeet.ts +++ b/scripts/bench-parakeet.ts @@ -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 ...] * diff --git a/scripts/bench-translate.ts b/scripts/bench-translate.ts index 12f8ac6..f27e2f0 100644 --- a/scripts/bench-translate.ts +++ b/scripts/bench-translate.ts @@ -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…] * diff --git a/scripts/build.ts b/scripts/build.ts index 2855187..d1ef1a5 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -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", diff --git a/scripts/measure-longform.ts b/scripts/measure-longform.ts index 11d54b6..bc56b6c 100644 --- a/scripts/measure-longform.ts +++ b/scripts/measure-longform.ts @@ -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. * diff --git a/scripts/ortAlias.ts b/scripts/ortAlias.ts index 6dbdcfb..28ea748 100644 --- a/scripts/ortAlias.ts +++ b/scripts/ortAlias.ts @@ -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 diff --git a/server-py/sidecar.py b/server-py/sidecar.py index bb247c8..7a0f4d7 100644 --- a/server-py/sidecar.py +++ b/server-py/sidecar.py @@ -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 diff --git a/src/App.tsx b/src/App.tsx index b0fadc9..cdc0956 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(null); const [language, setLanguage] = useState(AUTO_LANGUAGE); const [task, setTask] = useState(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>(new Set()); @@ -207,7 +207,7 @@ export function App() { const auth = useAuth(); const [engineChoice, setEngineChoice] = useState(() => loadEngineChoice()); const [serverStatus, setServerStatus] = useState(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 && (

@@ -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 && ( <> - {/* 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 && ( {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. Not Whisper · English only · fast on long recordings diff --git a/src/components/StorageManager.tsx b/src/components/StorageManager.tsx index 152f767..28870c7 100644 --- a/src/components/StorageManager.tsx +++ b/src/components/StorageManager.tsx @@ -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 { diff --git a/src/components/TranscriptionOptions.tsx b/src/components/TranscriptionOptions.tsx index 58b9823..4915646 100644 --- a/src/components/TranscriptionOptions.tsx +++ b/src/components/TranscriptionOptions.tsx @@ -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, diff --git a/src/components/TranslatePanel.tsx b/src/components/TranslatePanel.tsx index 539c305..2f6735e 100644 --- a/src/components/TranslatePanel.tsx +++ b/src/components/TranslatePanel.tsx @@ -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). diff --git a/src/components/VocabularyHint.tsx b/src/components/VocabularyHint.tsx index 83a8279..dafac23 100644 --- a/src/components/VocabularyHint.tsx +++ b/src/components/VocabularyHint.tsx @@ -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. */ diff --git a/src/index.css b/src/index.css index 56b2af0..6a0adeb 100644 --- a/src/index.css +++ b/src/index.css @@ -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. -------------------------------------------------------------------------- */ diff --git a/src/index.ts b/src/index.ts index 446c5e7..df44aa1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -226,7 +226,7 @@ async function productionServeOptions(): Promise { * 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 { @@ -237,7 +237,7 @@ async function bundleEntrypoint(relativePath: string): Promise { // 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 { 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). diff --git a/src/lib/engine.ts b/src/lib/engine.ts index 3614710..24b2fcb 100644 --- a/src/lib/engine.ts +++ b/src/lib/engine.ts @@ -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; diff --git a/src/lib/errors.ts b/src/lib/errors.ts index a0cc89a..7f49ab9 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -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. */ diff --git a/src/lib/hint.ts b/src/lib/hint.ts index 911df39..4824124 100644 --- a/src/lib/hint.ts +++ b/src/lib/hint.ts @@ -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. diff --git a/src/lib/longForm.ts b/src/lib/longForm.ts index 417e910..6de0c5e 100644 --- a/src/lib/longForm.ts +++ b/src/lib/longForm.ts @@ -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; diff --git a/src/lib/mobileMemoryGuard.ts b/src/lib/mobileMemoryGuard.ts index ae8429f..d72ed3f 100644 --- a/src/lib/mobileMemoryGuard.ts +++ b/src/lib/mobileMemoryGuard.ts @@ -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 diff --git a/src/lib/models.ts b/src/lib/models.ts index 8f34da1..37dd641 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -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>; decoder: Partial>; @@ -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 { } /** - * 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 { 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 { 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 { } /** - * 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) => diff --git a/src/worker/longform/index.ts b/src/worker/longform/index.ts index 8113b68..7b6e0de 100644 --- a/src/worker/longform/index.ts +++ b/src/worker/longform/index.ts @@ -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 { diff --git a/src/worker/longform/pipeline.ts b/src/worker/longform/pipeline.ts index b3f43c6..dafd6c1 100644 --- a/src/worker/longform/pipeline.ts +++ b/src/worker/longform/pipeline.ts @@ -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 = { guard: DEFAULT_GUARD, fallback: false, diff --git a/src/worker/longform/quality.ts b/src/worker/longform/quality.ts index 11aa2b8..40e409c 100644 --- a/src/worker/longform/quality.ts +++ b/src/worker/longform/quality.ts @@ -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 { diff --git a/src/worker/longform/transformers.ts b/src/worker/longform/transformers.ts index f08108c..af6c5d6 100644 --- a/src/worker/longform/transformers.ts +++ b/src/worker/longform/transformers.ts @@ -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 { export async function loadSileroVad(options: LoadSileroVadOptions): Promise { 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; diff --git a/src/worker/longform/types.ts b/src/worker/longform/types.ts index b052486..305d08c 100644 --- a/src/worker/longform/types.ts +++ b/src/worker/longform/types.ts @@ -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; } -/** 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; diff --git a/src/worker/longform/vad.ts b/src/worker/longform/vad.ts index ef18447..f7758b6 100644 --- a/src/worker/longform/vad.ts +++ b/src/worker/longform/vad.ts @@ -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 diff --git a/src/worker/modelFiles.ts b/src/worker/modelFiles.ts index 72a1109..1e081a0 100644 --- a/src/worker/modelFiles.ts +++ b/src/worker/modelFiles.ts @@ -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 diff --git a/src/worker/opusMt.ts b/src/worker/opusMt.ts index 8bbee00..08913d3 100644 --- a/src/worker/opusMt.ts +++ b/src/worker/opusMt.ts @@ -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. */ diff --git a/src/worker/ortShim.ts b/src/worker/ortShim.ts index 824e043..deee58f 100644 --- a/src/worker/ortShim.ts +++ b/src/worker/ortShim.ts @@ -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. diff --git a/src/worker/parakeetEngine.ts b/src/worker/parakeetEngine.ts index c3cf196..f1050e4 100644 --- a/src/worker/parakeetEngine.ts +++ b/src/worker/parakeetEngine.ts @@ -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"`. * diff --git a/src/worker/prompt.ts b/src/worker/prompt.ts index a964f83..daa5245 100644 --- a/src/worker/prompt.ts +++ b/src/worker/prompt.ts @@ -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 diff --git a/src/worker/protocol.ts b/src/worker/protocol.ts index 955698b..5a2d5ee 100644 --- a/src/worker/protocol.ts +++ b/src/worker/protocol.ts @@ -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( diff --git a/src/worker/translate.worker.ts b/src/worker/translate.worker.ts index 6e0ba4e..b39f2a1 100644 --- a/src/worker/translate.worker.ts +++ b/src/worker/translate.worker.ts @@ -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: diff --git a/src/worker/translateProtocol.ts b/src/worker/translateProtocol.ts index e26c105..4dbe1da 100644 --- a/src/worker/translateProtocol.ts +++ b/src/worker/translateProtocol.ts @@ -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. */ diff --git a/tests/engine.test.ts b/tests/engine.test.ts index c8201e6..81fe255 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -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 }); diff --git a/tests/errors.test.ts b/tests/errors.test.ts index 9fe8a52..cb76439 100644 --- a/tests/errors.test.ts +++ b/tests/errors.test.ts @@ -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"); diff --git a/tests/fixtures/stub_fw/faster_whisper/__init__.py b/tests/fixtures/stub_fw/faster_whisper/__init__.py index 8701d88..917de6c 100644 --- a/tests/fixtures/stub_fw/faster_whisper/__init__.py +++ b/tests/fixtures/stub_fw/faster_whisper/__init__.py @@ -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= loads= " WhisperModel.transcribe diff --git a/tests/hint-parity.test.ts b/tests/hint-parity.test.ts index 0a94e40..8ecbdd6 100644 --- a/tests/hint-parity.test.ts +++ b/tests/hint-parity.test.ts @@ -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 diff --git a/tests/hint.test.ts b/tests/hint.test.ts index 3ef7649..a3b66a0 100644 --- a/tests/hint.test.ts +++ b/tests/hint.test.ts @@ -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."), diff --git a/tests/live-real-model.test.ts b/tests/live-real-model.test.ts index c572dcd..dc12389 100644 --- a/tests/live-real-model.test.ts +++ b/tests/live-real-model.test.ts @@ -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; diff --git a/tests/longform-setting.test.ts b/tests/longform-setting.test.ts index 4c52374..53aee14 100644 --- a/tests/longform-setting.test.ts +++ b/tests/longform-setting.test.ts @@ -18,7 +18,7 @@ function memoryStorage(initial: Record = {}) { }; } -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); diff --git a/tests/longform-transformers.test.ts b/tests/longform-transformers.test.ts index 61ff9d0..3eaa905 100644 --- a/tests/longform-transformers.test.ts +++ b/tests/longform-transformers.test.ts @@ -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[] = []; diff --git a/tests/mobileMemoryGuard.test.ts b/tests/mobileMemoryGuard.test.ts index 859cbd4..2e76e9e 100644 --- a/tests/mobileMemoryGuard.test.ts +++ b/tests/mobileMemoryGuard.test.ts @@ -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 }; diff --git a/tests/modelFiles.test.ts b/tests/modelFiles.test.ts index 1b3bfcb..9e74cbb 100644 --- a/tests/modelFiles.test.ts +++ b/tests/modelFiles.test.ts @@ -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"; diff --git a/tests/models.test.ts b/tests/models.test.ts index ac3df68..cf8ccca 100644 --- a/tests/models.test.ts +++ b/tests/models.test.ts @@ -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", () => { diff --git a/tests/offline.md b/tests/offline.md index edce92d..c9363fc 100644 --- a/tests/offline.md +++ b/tests/offline.md @@ -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 diff --git a/tests/parakeet-real-model.test.ts b/tests/parakeet-real-model.test.ts index 0b14314..51071ac 100644 --- a/tests/parakeet-real-model.test.ts +++ b/tests/parakeet-real-model.test.ts @@ -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 * diff --git a/tests/parakeet.test.ts b/tests/parakeet.test.ts index 7711c83..6f42efd 100644 --- a/tests/parakeet.test.ts +++ b/tests/parakeet.test.ts @@ -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; diff --git a/tests/prompt.test.ts b/tests/prompt.test.ts index 4bd20ac..53a5b1b 100644 --- a/tests/prompt.test.ts +++ b/tests/prompt.test.ts @@ -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); }); diff --git a/tests/protocol.test.ts b/tests/protocol.test.ts index c96f07f..b20b07e 100644 --- a/tests/protocol.test.ts +++ b/tests/protocol.test.ts @@ -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 = [ diff --git a/tests/server-engine.md b/tests/server-engine.md index 6ac7ba9..eeedfa1 100644 --- a/tests/server-engine.md +++ b/tests/server-engine.md @@ -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 diff --git a/tests/server-live.test.ts b/tests/server-live.test.ts index f819756..35f0b50 100644 --- a/tests/server-live.test.ts +++ b/tests/server-live.test.ts @@ -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 }); diff --git a/tests/transcribe.test.ts b/tests/transcribe.test.ts index 7e507b0..b766ac6 100644 --- a/tests/transcribe.test.ts +++ b/tests/transcribe.test.ts @@ -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(" "), ); }, diff --git a/tests/transcriptionOptions.test.tsx b/tests/transcriptionOptions.test.tsx index a9ada77..c70bc5a 100644 --- a/tests/transcriptionOptions.test.tsx +++ b/tests/transcriptionOptions.test.tsx @@ -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( { 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"); }); diff --git a/tests/translate-real-model.test.ts b/tests/translate-real-model.test.ts index b2b156b..71b92a0 100644 --- a/tests/translate-real-model.test.ts +++ b/tests/translate-real-model.test.ts @@ -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 * diff --git a/tests/translatePanel.test.tsx b/tests/translatePanel.test.tsx index bd5186f..88e0a25 100644 --- a/tests/translatePanel.test.tsx +++ b/tests/translatePanel.test.tsx @@ -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) { diff --git a/tests/translation.test.ts b/tests/translation.test.ts index 06e3ca2..59fbd42 100644 --- a/tests/translation.test.ts +++ b/tests/translation.test.ts @@ -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 () => { diff --git a/tests/workerBundle.test.ts b/tests/workerBundle.test.ts index eda8384..d294517 100644 --- a/tests/workerBundle.test.ts +++ b/tests/workerBundle.test.ts @@ -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/` comment per bundled module, so count runtimes.