Add a model family field and the Parakeet v3 catalog entry with exact file sizes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-29 21:48:39 +01:00
1 parent c3a490548a
commit d554162a2c
2 files changed
+136 -9

No files matched your search

+88 -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. */
@@ -54,8 +77,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];
}
// ---------------------------------------------------------------------------
@@ -81,9 +119,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),
);
}
@@ -107,8 +145,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));
}
@@ -168,6 +206,10 @@ const ACCEL_Q4: DtypeDefault = {
// 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",
@@ -259,6 +301,46 @@ export const MODELS: readonly WhisperModel[] = [
decoderParamsM: 174,
defaults: { wasm: 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,
// 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 {
+48 -3
View File
@@ -9,6 +9,8 @@ import {
formatModelSize,
getFasterWhisperModel,
getModel,
modelFamily,
PARAKEET_MODEL_ID,
resolveDefaultDtype,
} from "../src/lib/models";
@@ -19,6 +21,8 @@ function stripRepoDecorations(id: string): string {
return id.replace(/(_timestamped|-ONNX)$/, "");
}
const WHISPER_MODELS = MODELS.filter((model) => modelFamily(model) === "whisper");
describe("MODELS catalog", () => {
test("contains the curated D-006 lineup from the onnx-community org", () => {
const ids = MODELS.map((model) => model.id);
@@ -32,12 +36,15 @@ describe("MODELS catalog", () => {
"onnx-community/whisper-medium.en_timestamped",
"onnx-community/whisper-medium-ONNX",
"onnx-community/whisper-large-v3-turbo",
// Not Whisper: the D-WS7a Parakeet engine path, last so it never
// becomes anyone's accidental default.
"ysdede/parakeet-tdt-0.6b-v3-onnx",
]);
expect(new Set(ids).size).toBe(ids.length);
});
test(".en variants are English-only; the rest are multilingual", () => {
for (const model of MODELS) {
for (const model of WHISPER_MODELS) {
// Hub repo names may carry decorations (e.g. "_timestamped", "-ONNX")
// after the size suffix — judge English-ness on the undecorated name.
expect(model.multilingual).toBe(!stripRepoDecorations(model.id).endsWith(".en"));
@@ -69,8 +76,8 @@ describe("MODELS catalog", () => {
expect(getModel("onnx-community/whisper-nonexistent")).toBeUndefined();
});
test("every model maps to a faster-whisper name for the server engine (D-013)", () => {
for (const model of MODELS) {
test("every Whisper model maps to a faster-whisper name for the server engine (D-013)", () => {
for (const model of WHISPER_MODELS) {
// The onnx-community ids and faster-whisper's aliases share the same
// size suffix ("tiny.en", …, "large-v3-turbo") once repo-name
// decorations ("_timestamped", "-ONNX") are stripped; keeping them
@@ -209,3 +216,41 @@ describe("WHISPER_LANGUAGES", () => {
expect(WHISPER_LANGUAGES.some((entry) => entry.code === "auto")).toBe(false);
});
});
describe("Parakeet catalog entry (D-WS7a)", () => {
const parakeet = getModel(PARAKEET_MODEL_ID)!;
test("family defaults to whisper when absent", () => {
expect(modelFamily(getModel(DEFAULT_MODEL_ID))).toBe("whisper");
expect(modelFamily(undefined)).toBe("whisper");
expect(modelFamily(parakeet)).toBe("parakeet");
});
test("is English-only and browser-only (no faster-whisper name)", () => {
expect(parakeet.multilingual).toBe(false);
expect(parakeet.fasterWhisperModel).toBeUndefined();
expect(getFasterWhisperModel(PARAKEET_MODEL_ID)).toBeUndefined();
});
test("WASM runs int8 for both parts; WebGPU is fp16 encoder + int8 decoder", () => {
expect(resolveDefaultDtype(PARAKEET_MODEL_ID, "wasm")).toBe("q8");
expect(resolveDefaultDtype(PARAKEET_MODEL_ID, "webgpu")).toEqual({
encoder_model: "fp16",
decoder_model_merged: "q8",
});
});
test("download size is the real file sizes, not a parameter estimate", () => {
expect(estimateDefaultDownloadBytes(parakeet, "wasm")).toBe(
652_183_999 + 18_202_004 + 102_132,
);
expect(estimateDefaultDownloadBytes(parakeet, "webgpu")).toBe(
1_238_960_452 + 18_202_004 + 102_132,
);
expect(formatModelSize(estimateDefaultDownloadBytes(parakeet, "wasm"))).toBe("670 MB");
});
test("peak load memory is twice the larger weight file", () => {
expect(estimatePeakLoadBytes(parakeet, "q8")).toBe(2 * 652_183_999);
});
});