Offer Parakeet in the picker as English-only, hide it on phones, and explain the missing language controls

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-29 22:08:08 +01:00
co-authored by Claude Opus 5.5
parent 708fae5778
commit 12e0f6678b
3 changed files with 62 additions and 16 deletions
+43 -14
View File
@@ -36,9 +36,15 @@ import {
estimatePeakLoadBytes,
formatModelSize,
getModel,
modelFamily,
resolveDefaultDtype,
} from "./lib/models";
import { shouldWarnBeforeLoad, type MobileMemoryHint } from "./lib/mobileMemoryGuard";
import {
isModelAllowedOnDevice,
modelsForDevice,
shouldWarnBeforeLoad,
type MobileMemoryHint,
} from "./lib/mobileMemoryGuard";
import {
applyLoadProgress,
initialLoadProgress,
@@ -68,6 +74,16 @@ interface AppErrorState {
const MODEL_IDS = MODELS.map((model) => model.id);
/** Device signals for the mobile memory guard (D-027) and desktop-only models (D-WS7a). */
function memoryHint(): MobileMemoryHint {
const nav = typeof navigator !== "undefined" ? navigator : undefined;
return {
userAgent: nav?.userAgent,
maxTouchPoints: nav?.maxTouchPoints,
deviceMemoryGB: (nav as (Navigator & { deviceMemory?: number }) | undefined)?.deviceMemory,
};
}
export function App() {
const workerRef = useRef<Worker | null>(null);
@@ -260,15 +276,7 @@ export function App() {
const targetModel = getModel(forModelId);
if (!targetModel) return false;
const peakBytes = estimatePeakLoadBytes(targetModel, resolveDefaultDtype(forModelId, device));
const hint: MobileMemoryHint = {
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
maxTouchPoints: typeof navigator !== "undefined" ? navigator.maxTouchPoints : undefined,
deviceMemoryGB:
typeof navigator !== "undefined"
? (navigator as Navigator & { deviceMemory?: number }).deviceMemory
: undefined,
};
return shouldWarnBeforeLoad(hint, peakBytes);
return shouldWarnBeforeLoad(memoryHint(), peakBytes);
}
/**
@@ -281,6 +289,9 @@ export function App() {
function confirmRiskyMobileLoad(forModelId: string, preference: DevicePreference): boolean {
const targetModel = getModel(forModelId);
if (!targetModel) return true;
// Desktop-only models are never offered on phones (D-WS7a); this guards
// any path that could still name one (e.g. a stale selection).
if (!isModelAllowedOnDevice(targetModel, memoryHint())) return false;
const device = guessDevice(preference);
const key = overrideKey(forModelId, device);
if (mobileWarningAcknowledged.current.has(key)) return true;
@@ -979,9 +990,15 @@ export function App() {
const serverSelected =
engineChoice === "server" && auth.user !== null && serverStatus?.available === true;
const availableModels = serverSelected
? MODELS.filter((m) => m.fasterWhisperModel === "large-v3-turbo" || m.fasterWhisperModel === "medium.en")
: MODELS;
// Phones never see desktop-only models (Parakeet, D-WS7a); the server
// engine only offers what the sidecar runs.
const availableModels = modelsForDevice(
serverSelected
? MODELS.filter((m) => m.fasterWhisperModel === "large-v3-turbo" || m.fasterWhisperModel === "medium.en")
: MODELS,
memoryHint(),
);
const parakeetSelected = modelFamily(model) === "parakeet";
useEffect(() => {
if (serverSelected) {
@@ -1032,6 +1049,15 @@ export function App() {
serverEngine={serverSelected}
/>
{/* Parakeet has no language or task choice (D-WS7a) — say so where
those controls would be, rather than leaving their absence unexplained. */}
{parakeetSelected && (
<p className="model-language-note">
Parakeet understands English only, so there is no language or translation
choice. For other languages, pick a Multilingual Whisper model.
</p>
)}
{model?.multilingual && (
<TranscriptionOptions
language={language}
@@ -1069,7 +1095,10 @@ export function App() {
models — every pass pads to a full 30 s Whisper window (D-024), so
tentative text lags and the stop drain grows. Say so instead of
looking frozen. */}
{liveActive && activeDevice === "wasm" && (model?.encoderParamsM ?? 0) >= 300 && (
{liveActive &&
activeDevice === "wasm" &&
!parakeetSelected &&
(model?.encoderParamsM ?? 0) >= 300 && (
<p className="live-hint" role="status">
{model?.displayName} runs slower than real time on the WASM backend, so
live text will lag behind speech. A smaller model (Tiny, Base or Small)
+10 -1
View File
@@ -158,7 +158,16 @@ export function ModelPicker({
disabled={disabled}
onChange={() => onSelect(model.id)}
/>
<span className="model-option-name">{model.displayName}</span>
<span className="model-option-name">
{model.displayName}
{model.family === "parakeet" && (
// Not Whisper (D-WS7a): say what it is good for and
// what it can't do, in plain words.
<span className="model-option-note">
Not Whisper · English only · fast on long recordings
</span>
)}
</span>
<span className="model-option-meta">
{isDownloaded && (
<span
+9 -1
View File
@@ -423,6 +423,13 @@ body {
flex: 1;
}
/* One-line description under a model name (Parakeet, D-WS7a). */
.model-option-note {
display: block;
color: var(--muted);
font-size: 0.8rem;
}
.model-option-meta {
display: flex;
align-items: center;
@@ -640,7 +647,8 @@ body {
}
}
.live-hint {
.live-hint,
.model-language-note {
margin: 0;
color: var(--muted);
font-size: 0.9rem;