Remove the workstream 7 plan now that the Parakeet engine path is done

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-29 23:52:39 +01:00
co-authored by Claude Opus 5.5
parent fdc9b500c0
commit b66ba7e577
-187
View File
@@ -1,187 +0,0 @@
---
status: in-progress
---
# 001-7 — Parakeet TDT 0.6B v3 engine path: implementation plan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (inline, no subagents —
> coordination plan). Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** NVIDIA Parakeet TDT 0.6B v3 as an English-only, desktop-only browser-engine model choice
(file + live), WASM int8 baseline, opt-in WebGPU, measured against Whisper Large v3 Turbo.
**Architecture:** The catalog gains `family?: "whisper" | "parakeet"`. The worker dispatches on it:
Whisper keeps the Transformers.js pipeline; Parakeet runs `parakeet.js`'s `ParakeetModel` (TDT greedy
decoder + JS mel preprocessor) on **the same onnxruntime-web instance Transformers.js imports**
(`onnxruntime-web/webgpu`), with weights fetched by our own Cache-API downloader into the
`transformers-cache` cache (so D-005 badges / storage panel / delete work unchanged). Audio is cut into
≤ 30 s windows at the quietest point near each boundary; word timestamps become sentence chunks in the
app's `TranscriptChunk` shape, so TranscriptView, export, progress, and live work unchanged.
**Tech Stack:** Bun, TypeScript, React 19, onnxruntime-web 1.26.0-dev (via Transformers.js 4.2.0),
`parakeet.js` 1.4.4 (MIT, devDependency, bundled into the worker only).
**Spec:** `docs/specs/001-transcription-quality-and-speed.md` Goal 7; coordination
`docs/plans/001-0-coordination.md`.
## Runtime decision (Step 1 — done before planning; evidence for D-WS7a)
- `@huggingface/transformers` 4.2.0 **and** 4.3.2 (npm `latest`) ship only
`models/parakeet/modeling_parakeet.js` → `ParakeetForCTC`; the ASR pipeline routes
`model_type: 'parakeet_ctc'` through the wav2vec2 CTC path (argmax, no timestamps, no chunking).
No TDT/transducer decoder exists in either (grep `tdt|transducer|rnnt`: no hits). The model dirs are
identical between the two versions. ⇒ **no native Parakeet TDT support → `parakeet.js`.**
- `parakeet.js` 1.4.4 pins `onnxruntime-web` 1.24.1 and dynamically imports bare `onnxruntime-web` in
`backend.js` (`initOrt`, CDN `wasmPaths` default). We never call `fromUrls`/`fromHub`/`initOrt`; we
construct `ParakeetModel` directly with our `ort`, our tokenizer, and `JsPreprocessor`. A Bun build
plugin resolves parakeet.js's bare `onnxruntime-web` import to `onnxruntime-web/webgpu` so the bundle
holds one ort (no second runtime, no CDN path).
- Model repo: `ysdede/parakeet-tdt-0.6b-v3-onnx` (CC-BY-4.0, NeMo conversion), pinned revision
`e5356bf6e24bdb7a269536b0e9e7869bc034b893`. Files: `encoder-model.int8.onnx` 652,183,999 B,
`decoder_joint-model.int8.onnx` 18,202,004 B, `encoder-model.fp16.onnx` 1,238,960,452 B,
`decoder_joint-model.fp16.onnx` 36,266,140 B, `vocab.txt` 102,132 B.
- Spike (ort-node CPU, 12 threads, naive 30 s cuts): en_aiproblem WER 0.030 in 0.34 s; en_alien
WER 0.116 in 18.1 s. onnxruntime-web WASM in Bun: en_alien WER 0.104 in 45.2 s. Turbo: 0.261 /
420 s (D-039).
## Global Constraints
- AGENTS.md rules: Bun only; no cross-origin subresources (COEP require-corp) — weights from
huggingface.co via `fetch` in the worker as today, ort `.wasm` from `/ort/`; colours as tokens;
`classifyError` for errors; TDD; docs duties in the same commit.
- Decision ids: `D-WS7a` (+ `D-WS7b` …) placeholders, cited in code comments.
- Heavy commands (`bun test` real-model, benches, downloads) under `flock /tmp/whisper-heavy.lock`.
- Ports 3170–3179. No push/publish. Plan `git rm`'d last.
- `family?` absent ⇒ `"whisper"`; existing catalog entries unedited.
- D-020: selecting never downloads; D-005: cached via Cache API, shown with real size.
- Mobile (D-027–D-029): Parakeet hidden in the picker and blocked from loading on `isLikelyMobile`.
- English only: no language/task controls; plain copy that translation isn't available.
- WebGPU: never auto-selected for Parakeet; explicit WebGPU = fp16 encoder on WebGPU + int8 decoder
on WASM, only with `shader-f16`, falling back to WASM (D-025 restart). Owner's browser check.
- Measurements are CPU numbers (ort-node and ort-web WASM in Bun); say so.
## Review Focus
1. **90-minute file:** windows are `subarray` views (no copies of the whole file per window); only
the word list grows. Test: `planChunks` on 90 min of samples returns ≥ 180 windows each ≤ 30 s,
contiguous, total = length (Task 2).
2. **Silence-only audio:** empty transcript, progress reaches total, no throw (Task 2,
`transcribeWindows` with a recognizer returning no words).
3. **Speech crossing a window boundary:** cut lands in the quietest 100 ms frame of the last 6 s —
test with a silent gap planted at 27 s (Task 2).
4. **Parakeet left selected, then phone UA:** never offered, never loaded (Task 4 `modelsForDevice`;
Task 6 App guard).
5. **Cancel mid-download / mid-transcribe:** worker termination (D-018) — the downloader only `put`s a
complete response, so a half download never shows as "available offline" (Task 3 test).
---
### Task 1: Catalog `family`, exact sizes, Parakeet entry
**Files:** Modify `src/lib/models.ts`, `tests/models.test.ts`.
**Interfaces — Produces:**
- `type ModelFamily = "whisper" | "parakeet"`; `WhisperModel.family?: ModelFamily`;
`modelFamily(model: WhisperModel | undefined): ModelFamily` (absent ⇒ "whisper").
- `WhisperModel.fasterWhisperModel?: string` (absent for browser-only families).
- `WhisperModel.exactBytes?: { encoder: Partial<Record<WhisperDtype, number>>; decoder: Partial<Record<WhisperDtype, number>>; aux: number }`
— when present, `estimateDownloadBytes` / `estimatePeakLoadBytes` use it.
- `PARAKEET_MODEL_ID = "ysdede/parakeet-tdt-0.6b-v3-onnx"`, `PARAKEET_REVISION`.
- Entry: displayName "Parakeet v3 (English)", `multilingual: false`, `family: "parakeet"`,
defaults wasm `"q8"`, webgpu `{ encoder_model: "fp16", decoder_model_merged: "q8" }`, webnn `"q8"`.
- [x] Tests: `modelFamily` default; Parakeet entry family/multilingual/no fasterWhisperModel;
wasm download bytes = 652,183,999 + 18,202,004 + 102,132; webgpu = fp16 encoder + int8 decoder +
aux; peak = 2 × encoder bytes; existing fasterWhisperModel test restricted to Whisper family.
- [x] Fail → implement → pass → typecheck → commit.
### Task 2: Pure Parakeet logic (`src/lib/parakeet.ts`)
**Files:** Create `src/lib/parakeet.ts`, `tests/parakeet.test.ts`.
**Produces:**
- `planChunks(length: number, energyAt: (start: number, end: number) => number, opts?: { sampleRate?: number; maxSeconds?: number; searchSeconds?: number; frameSeconds?: number }): Array<[number, number]>`
— defaults 16 000 / 30 / 6 / 0.1; plus `planAudioChunks(audio: Float32Array): Array<[number, number]>`
using mean-square energy.
- `wordsToChunks(words: ParakeetWord[], opts?: { gapSeconds?: number; maxSegmentSeconds?: number }): TranscriptChunk[]`
— break after `.?!` (optionally followed by quotes/brackets), on gap ≥ 1.0 s, or once a segment
reaches 12 s; chunk text `" " + words.join(" ")` (Whisper convention: leading space).
- `ParakeetWord = { text: string; start_time: number; end_time: number }`.
- `parseVocab(text: string): string[]`; `class ParakeetVocabTokenizer` (`id2token`, `blankId`,
`blankToken`, `sanitizedTokens`, `decode(ids)`) — the shape `ParakeetModel` reads.
- `parakeetFileName(part: "encoder" | "decoder", dtype: WhisperDtype): string` (q8 → `.int8.onnx`,
fp16 → `.fp16.onnx`, else throws) and `parakeetFileUrl(file: string): string`
(`https://huggingface.co/<id>/resolve/<rev>/<file>`).
- `transcribeWindows(audio: Float32Array, recognize: (window: Float32Array, offsetSeconds: number) => Promise<ParakeetWord[]>, callbacks?: { onWindow?: (chunks: TranscriptChunk[], processedSeconds: number, totalSeconds: number) => void }): Promise<{ text: string; chunks: TranscriptChunk[] }>`.
- [x] Tests: window bounds (≤ 30 s, contiguous, cover all; short audio = one window; 90 min);
quiet-gap cut at 27 s; `wordsToChunks` sentence/gap/max splits and timestamps; vocab parse +
decode (`▁` → space, blank skipped, no space before punctuation); file names/URL;
`transcribeWindows` offsets/progress/partials and silence-only.
- [x] Fail → implement → pass → commit.
### Task 3: Cache-API model file downloader (`src/worker/modelFiles.ts`)
**Produces:** `fetchModelFile(url: string, file: string, deps: { caches: CacheStorageLike | undefined; fetch: typeof fetch; onProgress: (message: LoadProgressMessage) => void }): Promise<Uint8Array>`
— cache name `TRANSFORMERS_CACHE_NAME`; hit ⇒ `initiate`/`done`, no fetch; miss ⇒ streamed
`progress` (loaded/total/progress), `put` only after the full body, then `done`; non-OK ⇒ throws
`Could not download <file> (HTTP <status>)`.
- [x] Tests with fake cache/fetch: hit, miss+store+progress, HTTP error doesn't store, no Cache
Storage ⇒ still fetches.
- [x] Fail → implement → pass → commit.
### Task 4: Device policy + mobile filter
**Files:** `src/worker/protocol.ts` (+ `tests/protocol.test.ts`), `src/lib/mobileMemoryGuard.ts`
(+ `tests/mobileMemoryGuard.test.ts`).
**Produces:** `parakeetDeviceAttempts(preference: DevicePreference, capabilities: DeviceCapabilities): InferenceDevice[]`
— `"webgpu"` with `webgpu && webgpuF16` ⇒ `["webgpu","wasm"]`; everything else ⇒ `["wasm"]`
(auto never picks WebGPU, WebNN unsupported). `isModelAllowedOnDevice(model, hint): boolean` /
`modelsForDevice(models, hint)` — false for `family === "parakeet"` when `isLikelyMobile`.
- [x] Tests → fail → implement → pass → commit.
### Task 5: Worker engine path
**Files:** Create `src/worker/parakeetEngine.ts`; modify `src/worker/inference.worker.ts`,
`scripts/build.ts`, `src/index.ts` (shared `ortAliasPlugin` in `scripts/ortAlias.ts`).
**Produces:** `createParakeetEngine(ort: OrtLike, assets: { encoder: Uint8Array; decoder: Uint8Array; vocab: string }, opts: { encoderOptions: object; decoderOptions: object }): Promise<ParakeetEngine>`;
`ParakeetEngine.transcribe(audio, callbacks) → { text, chunks }`, `.dispose()`.
Worker: `load` dispatches on `modelFamily(getModel(id))`; Parakeet load = fetch files (Task 3) →
sessions on the attempt device (Task 4, D-025 restart reused) → `ready`. `transcribe` dispatches;
Parakeet ignores language/task, posts `transcribe-progress`/`partial` per window when streaming,
`result` always (live works through the same message).
- [x] Real-model test `tests/parakeet-real-model.test.ts` (opt-in `PARAKEET_REAL=1`, reads files from
`node_modules/@huggingface/transformers/.cache/<id>/`, runs `createParakeetEngine` on
onnxruntime-web WASM): whole-file and live WER per clip ≤ the turbo bars (0.25; en_alien 0.35),
chunks span the clip. Run under the lock.
- [x] Build check: `dist/inference.worker.js` contains the 1.26.0-dev ort version string and **not**
`1.24.1` (no second runtime bundled); result recorded in D-WS7a.
- [x] Commit.
### Task 6: UI
**Files:** `src/App.tsx`, `src/components/ModelPicker.tsx`, `src/index.css` if needed.
(impeccable for copy/layout.)
- Picker: `modelsForDevice` filters Parakeet out on phones; Parakeet row gets a one-line note
"English only · much faster on long recordings · no translation" (final copy via impeccable).
- App: if the selected model isn't allowed on this device, fall back to `DEFAULT_MODEL_ID`; guard
load paths; the WASM "slower than real time" live hint only for Whisper family.
- [x] Typecheck, build. (Screenshots taken in /tmp before the pause; re-verify after resume.)
- [ ] Headless Chrome screenshots (desktop + phone UA, light/dark) on port 3170, re-taken on the
final build; headless-Chrome end-to-end file run (progress, transcript, .txt/.srt export).
- [x] Commit.
### Task 7: Measurement + docs
- [x] `scripts/bench-parakeet.ts`: per clip WER + wall-clock for Parakeet (ort-node, ort-web WASM)
and Turbo (ort-node, worker options) — run under the lock.
- [x] D-WS7a (runtime + engine path + numbers), Architecture.md (worker families), WorkBreakdown,
README/AGENTS command for the opt-in test, CC-BY attribution note.
- [x] Docs committed (9cd92b4).
- [ ] Fast suite, typecheck, build; commit; `git rm` this plan; commit.