Keep Parakeet on WASM unless WebGPU is chosen explicitly, and never offer it on phones
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
827e9a8dae
commit
c5a57cd33a
@@ -19,6 +19,8 @@
|
||||
* advance, so a benchmark probe would cost more than it saves.
|
||||
*/
|
||||
|
||||
import type { WhisperModel } from "./models";
|
||||
|
||||
export interface MobileMemoryHint {
|
||||
/** `navigator.userAgent`, or undefined outside a browser (tests, SSR). */
|
||||
userAgent: string | undefined;
|
||||
@@ -86,3 +88,26 @@ export function shouldWarnBeforeLoad(hint: MobileMemoryHint, peakBytes: number):
|
||||
}
|
||||
return peakBytes > MOBILE_FALLBACK_RISK_THRESHOLD_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop-only models (D-WS7a): 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
|
||||
* not offered at all on phones and tablets: no amount of RAM a phone reports
|
||||
* makes it safe, so a "Continue anyway?" would only invite the crash.
|
||||
*/
|
||||
export function isModelAllowedOnDevice(
|
||||
model: Pick<WhisperModel, "family">,
|
||||
hint: Pick<MobileMemoryHint, "userAgent" | "maxTouchPoints">,
|
||||
): boolean {
|
||||
return model.family !== "parakeet" || !isLikelyMobile(hint);
|
||||
}
|
||||
|
||||
/** The catalog as offered on this device (order preserved). */
|
||||
export function modelsForDevice<T extends Pick<WhisperModel, "family">>(
|
||||
models: readonly T[],
|
||||
hint: Pick<MobileMemoryHint, "userAgent" | "maxTouchPoints">,
|
||||
): T[] {
|
||||
return models.filter((model) => isModelAllowedOnDevice(model, hint));
|
||||
}
|
||||
|
||||
@@ -267,6 +267,25 @@ export function deviceAttempts(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Device attempts for the Parakeet engine path (D-WS7a). 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
|
||||
* download on an unverified backend. An explicit WebGPU choice is honoured
|
||||
* only where shader-f16 exists (fp16 encoder), still falling back to WASM;
|
||||
* there is no WebNN path.
|
||||
*/
|
||||
export function parakeetDeviceAttempts(
|
||||
preference: DevicePreference,
|
||||
capabilities: DeviceCapabilities,
|
||||
): InferenceDevice[] {
|
||||
if (preference === "webgpu" && capabilities.webgpu && capabilities.webgpuF16) {
|
||||
return ["webgpu", "wasm"];
|
||||
}
|
||||
return ["wasm"];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runtime guards (postMessage data arrives untyped)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { isLikelyMobile, shouldWarnBeforeLoad } from "../src/lib/mobileMemoryGuard";
|
||||
import {
|
||||
isLikelyMobile,
|
||||
isModelAllowedOnDevice,
|
||||
modelsForDevice,
|
||||
shouldWarnBeforeLoad,
|
||||
} from "../src/lib/mobileMemoryGuard";
|
||||
import { MODELS, PARAKEET_MODEL_ID } from "../src/lib/models";
|
||||
|
||||
// Pure UA/heuristic logic (D-027) — no browser, no navigator.
|
||||
|
||||
@@ -99,3 +105,27 @@ describe("shouldWarnBeforeLoad", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("desktop-only models (D-WS7a)", () => {
|
||||
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 };
|
||||
const desktop = { userAgent: DESKTOP_WINDOWS_UA, maxTouchPoints: 0, deviceMemoryGB: 8 };
|
||||
|
||||
test("Parakeet is never offered on phones or tablets, even high-memory ones", () => {
|
||||
expect(isModelAllowedOnDevice(parakeet, phone)).toBe(false);
|
||||
expect(isModelAllowedOnDevice(parakeet, ipad)).toBe(false);
|
||||
expect(isModelAllowedOnDevice(parakeet, desktop)).toBe(true);
|
||||
});
|
||||
|
||||
test("Whisper models stay allowed everywhere (they get the D-027 confirm instead)", () => {
|
||||
for (const model of MODELS.filter((m) => m.id !== PARAKEET_MODEL_ID)) {
|
||||
expect(isModelAllowedOnDevice(model, phone)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("modelsForDevice filters the list, keeping order", () => {
|
||||
expect(modelsForDevice(MODELS, phone)).toEqual(MODELS.filter((m) => m !== parakeet));
|
||||
expect(modelsForDevice(MODELS, desktop)).toEqual([...MODELS]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, test, expect } from "bun:test";
|
||||
import {
|
||||
applyLoadProgress,
|
||||
deviceAttempts,
|
||||
parakeetDeviceAttempts,
|
||||
dtypeRequiresF16,
|
||||
initialLoadProgress,
|
||||
isUiToWorkerMessage,
|
||||
@@ -224,6 +225,29 @@ describe("deviceAttempts (D-004, D-017)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parakeetDeviceAttempts (D-WS7a)", () => {
|
||||
const everything = { webnn: true, webgpu: true, webgpuF16: true };
|
||||
|
||||
test("auto never picks an accelerator for Parakeet", () => {
|
||||
expect(parakeetDeviceAttempts("auto", everything)).toEqual(["wasm"]);
|
||||
});
|
||||
|
||||
test("explicit WebGPU is tried (then WASM) only with shader-f16", () => {
|
||||
expect(parakeetDeviceAttempts("webgpu", everything)).toEqual(["webgpu", "wasm"]);
|
||||
expect(parakeetDeviceAttempts("webgpu", { ...everything, webgpuF16: false })).toEqual([
|
||||
"wasm",
|
||||
]);
|
||||
expect(parakeetDeviceAttempts("webgpu", { ...everything, webgpu: false })).toEqual([
|
||||
"wasm",
|
||||
]);
|
||||
});
|
||||
|
||||
test("WebNN and WASM preferences run WASM", () => {
|
||||
expect(parakeetDeviceAttempts("webnn", everything)).toEqual(["wasm"]);
|
||||
expect(parakeetDeviceAttempts("wasm", everything)).toEqual(["wasm"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TranscribeProgressTracker", () => {
|
||||
// Production shape: 30 s windows advancing by 20 s (30 − 2·5, the worker's
|
||||
// chunk/stride settings), like the chunked ASR pipeline.
|
||||
|
||||
Reference in New Issue
Block a user