Merge feat/parakeet-engine: Parakeet TDT 0.6B v3 English engine (spec 001 workstream 7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	AGENTS.md
#	bun.lock
#	docs/Decisions.md
#	src/lib/models.ts
#	src/worker/inference.worker.ts
#	src/worker/protocol.ts
#	tests/models.test.ts
This commit is contained in:
Lucas Winther committed 2026-10-30 01:46:51 +01:00
commit 30969e86b7
29 files changed
+1741 -54

No files matched your search

+89 -6
View File
@@ -35,9 +35,32 @@ export interface DtypeDefault {
note: string;
}
/**
* Which inference path a catalog entry runs on (D-WS7a). 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). */
export interface ExactModelBytes {
encoder: Partial<Record<WhisperDtype, number>>;
decoder: Partial<Record<WhisperDtype, number>>;
/** Tokenizer/vocab/config files. */
aux: number;
}
export interface WhisperModel {
/** Hugging Face Hub id, e.g. "onnx-community/whisper-tiny.en". */
id: string;
/** Inference path (D-WS7a); absent ⇒ "whisper". */
family?: ModelFamily;
/**
* Real file sizes; when present they replace the parameter-count estimate
* in `estimateDownloadBytes` / `estimatePeakLoadBytes`.
*/
exactBytes?: ExactModelBytes;
/** Human-readable name for the picker. */
displayName: string;
/** False for `.en` (English-only) variants. */
@@ -63,8 +86,23 @@ 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
* answers 400 for them and the picker hides them while it is selected.
*/
fasterWhisperModel: string;
fasterWhisperModel?: string;
}
/** The entry's inference path; absent family (or unknown model) ⇒ "whisper". */
export function modelFamily(model: WhisperModel | undefined): ModelFamily {
return model?.family ?? "whisper";
}
/** Bytes of one part at one dtype: the exact file size when known, else the estimate. */
function partBytes(model: WhisperModel, part: "encoder" | "decoder", dtype: WhisperDtype): number {
const exact = model.exactBytes?.[part][dtype];
if (exact !== undefined) return exact;
const params = part === "encoder" ? model.encoderParamsM : model.decoderParamsM;
return params * 1e6 * BYTES_PER_PARAM[dtype];
}
// ---------------------------------------------------------------------------
@@ -90,9 +128,9 @@ export function estimateDownloadBytes(
const encDtype = typeof dtype === "string" ? dtype : dtype.encoder_model;
const decDtype = typeof dtype === "string" ? dtype : dtype.decoder_model_merged;
return Math.round(
model.encoderParamsM * 1e6 * BYTES_PER_PARAM[encDtype] +
model.decoderParamsM * 1e6 * BYTES_PER_PARAM[decDtype] +
AUX_FILES_BYTES,
partBytes(model, "encoder", encDtype) +
partBytes(model, "decoder", decDtype) +
(model.exactBytes?.aux ?? AUX_FILES_BYTES),
);
}
@@ -116,8 +154,8 @@ export function formatModelSize(bytes: number): string {
export function estimatePeakLoadBytes(model: WhisperModel, dtype: DtypeSpec): number {
const encDtype = typeof dtype === "string" ? dtype : dtype.encoder_model;
const decDtype = typeof dtype === "string" ? dtype : dtype.decoder_model_merged;
const encoderBytes = model.encoderParamsM * 1e6 * BYTES_PER_PARAM[encDtype];
const decoderBytes = model.decoderParamsM * 1e6 * BYTES_PER_PARAM[decDtype];
const encoderBytes = partBytes(model, "encoder", encDtype);
const decoderBytes = partBytes(model, "decoder", decDtype);
return Math.round(2 * Math.max(encoderBytes, decoderBytes));
}
@@ -209,6 +247,10 @@ 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). */
export const PARAKEET_MODEL_ID = "ysdede/parakeet-tdt-0.6b-v3-onnx";
export const PARAKEET_REVISION = "e5356bf6e24bdb7a269536b0e9e7869bc034b893";
export const MODELS: readonly WhisperModel[] = [
{
id: "onnx-community/whisper-tiny.en",
@@ -325,6 +367,47 @@ export const MODELS: readonly WhisperModel[] = [
decoderParamsM: 156,
defaults: { wasm: DANISH_WASM_Q4, webgpu: WEBGPU_FP16_Q4, webnn: WEBNN_FP16 },
},
{
// 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).
// 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
// the picker hides it on mobile instead of just warning.
id: PARAKEET_MODEL_ID,
family: "parakeet",
displayName: "Parakeet v3 (English)",
multilingual: false,
canTranslate: false,
// Parameter counts only feed fallbacks; the sizes below are exact.
encoderParamsM: 609,
decoderParamsM: 17,
exactBytes: {
encoder: { q8: 652_183_999, fp16: 1_238_960_452 },
decoder: { q8: 18_202_004, fp16: 36_266_140 },
aux: 102_132, // vocab.txt
},
defaults: {
wasm: {
dtype: "q8",
verified: false,
note: "int8 encoder + decoder: the measured WASM path (D-WS7a).",
},
// Only used when WebGPU is chosen explicitly — "auto" never picks it
// for Parakeet (D-WS7a): 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" },
verified: false,
note: "fp16 encoder on WebGPU (needs shader-f16) + int8 decoder on WASM.",
},
webnn: {
dtype: "q8",
verified: false,
note: "No WebNN path: runs the WASM int8 setup.",
},
},
},
];
export function getModel(id: string): WhisperModel | undefined {