12 KiB
status
| 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/transformers4.2.0 and 4.3.2 (npmlatest) ship onlymodels/parakeet/modeling_parakeet.js→ParakeetForCTC; the ASR pipeline routesmodel_type: 'parakeet_ctc'through the wav2vec2 CTC path (argmax, no timestamps, no chunking). No TDT/transducer decoder exists in either (greptdt|transducer|rnnt: no hits). The model dirs are identical between the two versions. ⇒ no native Parakeet TDT support →parakeet.js.parakeet.js1.4.4 pinsonnxruntime-web1.24.1 and dynamically imports bareonnxruntime-webinbackend.js(initOrt, CDNwasmPathsdefault). We never callfromUrls/fromHub/initOrt; we constructParakeetModeldirectly with ourort, our tokenizer, andJsPreprocessor. A Bun build plugin resolves parakeet.js's bareonnxruntime-webimport toonnxruntime-web/webgpuso 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 revisione5356bf6e24bdb7a269536b0e9e7869bc034b893. Files:encoder-model.int8.onnx652,183,999 B,decoder_joint-model.int8.onnx18,202,004 B,encoder-model.fp16.onnx1,238,960,452 B,decoder_joint-model.fp16.onnx36,266,140 B,vocab.txt102,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
fetchin the worker as today, ort.wasmfrom/ort/; colours as tokens;classifyErrorfor errors; TDD; docs duties in the same commit. - Decision ids:
D-WS7a(+D-WS7b…) placeholders, cited in code comments. - Heavy commands (
bun testreal-model, benches, downloads) underflock /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
- 90-minute file: windows are
subarrayviews (no copies of the whole file per window); only the word list grows. Test:planChunkson 90 min of samples returns ≥ 180 windows each ≤ 30 s, contiguous, total = length (Task 2). - Silence-only audio: empty transcript, progress reaches total, no throw (Task 2,
transcribeWindowswith a recognizer returning no words). - 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).
- Parakeet left selected, then phone UA: never offered, never loaded (Task 4
modelsForDevice; Task 6 App guard). - Cancel mid-download / mid-transcribe: worker termination (D-018) — the downloader only
puts 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/estimatePeakLoadBytesuse 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". -
Tests:
modelFamilydefault; 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. -
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; plusplanAudioChunks(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 shapeParakeetModelreads. -
parakeetFileName(part: "encoder" | "decoder", dtype: WhisperDtype): string(q8 →.int8.onnx, fp16 →.fp16.onnx, else throws) andparakeetFileUrl(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[] }>. -
Tests: window bounds (≤ 30 s, contiguous, cover all; short audio = one window; 90 min); quiet-gap cut at 27 s;
wordsToChunkssentence/gap/max splits and timestamps; vocab parse + decode (▁→ space, blank skipped, no space before punctuation); file names/URL;transcribeWindowsoffsets/progress/partials and silence-only. -
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>).
- Tests with fake cache/fetch: hit, miss+store+progress, HTTP error doesn't store, no Cache Storage ⇒ still fetches.
- 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.
- 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).
- Real-model test
tests/parakeet-real-model.test.ts(opt-inPARAKEET_REAL=1, reads files fromnode_modules/@huggingface/transformers/.cache/<id>/, runscreateParakeetEngineon 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. - Build check:
dist/inference.worker.jscontains the 1.26.0-dev ort version string and not1.24.1(no second runtime bundled); result recorded in D-WS7a. - Commit.
Task 6: UI
Files: src/App.tsx, src/components/ModelPicker.tsx, src/index.css if needed.
(impeccable for copy/layout.)
- Picker:
modelsForDevicefilters 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. - Typecheck, build, headless Chrome screenshots (desktop + phone UA, light/dark) on port 3170.
- Commit.
Task 7: Measurement + docs
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.- D-WS7a (runtime + engine path + numbers), Architecture.md (worker families), WorkBreakdown, README/AGENTS command for the opt-in test, CC-BY attribution note.
- Fast suite, typecheck, build; commit;
git rmthis plan; commit.