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