WIP: M3 PWA/offline implementation, M4 auth (backend + frontend), D-013 faster-whisper
- M3: hand-rolled service worker (precache + runtime /ort caching), precache manifest injection, SW registration/update toast, manifest + generated icons, production build script, offline indicator in ModelPicker (D-005/D-007) - M4 backend: bun:sqlite users/sessions (D-009), Passport local strategy behind connect-compat adapter (D-008), /api/auth routes wired into dev+prod serving, Bun.password Argon2id, 38 auth tests - M4 frontend: AuthForms + useAuth + framework-free authClient, session-aware header, anonymous-first fail-silent offline handling, 18 client tests - D-013: server engine (M5) will use faster-whisper via Python sidecar; browser side stays Transformers.js (CTranslate2 has no WASM build) - bun test: 83 pass / tsc --noEmit clean - Note: M3 doc closeout (WorkBreakdown/Architecture/tests/offline.md) still in flight from a background agent; follow-up commit will finish docs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eb72a2e646
commit
9f39a82982
38 files changed
+3857
-246
No files matched your search
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
estimateDefaultDownloadBytes,
|
||||
formatModelSize,
|
||||
type WhisperModel,
|
||||
} from "../lib/models";
|
||||
import type { DevicePreference, InferenceDevice } from "../worker/protocol";
|
||||
|
||||
interface ModelPickerProps {
|
||||
models: readonly WhisperModel[];
|
||||
selectedId: string;
|
||||
onSelect: (modelId: string) => void;
|
||||
devicePreference: DevicePreference;
|
||||
onDevicePreferenceChange: (preference: DevicePreference) => void;
|
||||
/** Backend the worker actually selected; null until the first load (D-004). */
|
||||
activeDevice: InferenceDevice | null;
|
||||
/** Model ids with weights already in the browser cache (D-005). */
|
||||
downloaded: ReadonlySet<string>;
|
||||
/** True when the service worker controls the page — app shell cached (M3). */
|
||||
offlineReady: boolean;
|
||||
/** True while a transcription is running — switching is disabled then. */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DEVICE_OPTIONS: readonly { value: DevicePreference; label: string }[] = [
|
||||
{ value: "auto", label: "Auto (WebGPU if available)" },
|
||||
{ value: "webgpu", label: "WebGPU" },
|
||||
{ value: "wasm", label: "WASM (CPU)" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Curated model list (D-006): size estimate before download, English-only vs
|
||||
* multilingual grouping, downloaded badge from the Cache API (D-005), and a
|
||||
* device preference select (default "auto", D-004).
|
||||
*/
|
||||
export function ModelPicker({
|
||||
models,
|
||||
selectedId,
|
||||
onSelect,
|
||||
devicePreference,
|
||||
onDevicePreferenceChange,
|
||||
activeDevice,
|
||||
downloaded,
|
||||
offlineReady,
|
||||
disabled = false,
|
||||
}: ModelPickerProps) {
|
||||
// Size estimates depend on the dtype the worker will pick, which depends on
|
||||
// the device. Until the worker has reported one, assume WASM unless the
|
||||
// user explicitly asked for WebGPU.
|
||||
const estimateDevice: InferenceDevice =
|
||||
activeDevice ?? (devicePreference === "webgpu" ? "webgpu" : "wasm");
|
||||
|
||||
const groups: { title: string; items: WhisperModel[] }[] = [
|
||||
{
|
||||
title: "English-only",
|
||||
items: models.filter((model) => !model.multilingual),
|
||||
},
|
||||
{
|
||||
title: "Multilingual",
|
||||
items: models.filter((model) => model.multilingual),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="model-picker" aria-label="Model selection">
|
||||
<div className="model-picker-header">
|
||||
<h2>Model</h2>
|
||||
<label className="device-select">
|
||||
Backend
|
||||
<select
|
||||
value={devicePreference}
|
||||
disabled={disabled}
|
||||
onChange={(event) =>
|
||||
onDevicePreferenceChange(event.target.value as DevicePreference)
|
||||
}
|
||||
>
|
||||
{DEVICE_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{disabled && (
|
||||
<p className="model-picker-hint">
|
||||
Model switching is disabled while a transcription is running.
|
||||
</p>
|
||||
)}
|
||||
<p
|
||||
className={`offline-status${offlineReady && downloaded.size > 0 ? " offline-status-ready" : ""}`}
|
||||
>
|
||||
{offlineReady
|
||||
? downloaded.size > 0
|
||||
? "Offline ready — the app and the models marked “available offline” work without a network connection."
|
||||
: "The app is cached for offline use. Load a model once to make it available offline too."
|
||||
: "Offline support is not active yet (the service worker is still installing, or this browser does not support it)."}
|
||||
</p>
|
||||
{groups.map((group) => (
|
||||
<fieldset key={group.title} className="model-group" disabled={disabled}>
|
||||
<legend>{group.title}</legend>
|
||||
<ul>
|
||||
{group.items.map((model) => {
|
||||
const size = formatModelSize(
|
||||
estimateDefaultDownloadBytes(model, estimateDevice),
|
||||
);
|
||||
const isDownloaded = downloaded.has(model.id);
|
||||
const isSelected = model.id === selectedId;
|
||||
return (
|
||||
<li key={model.id}>
|
||||
<label
|
||||
className={`model-option${isSelected ? " model-option-selected" : ""}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="model"
|
||||
value={model.id}
|
||||
checked={isSelected}
|
||||
disabled={disabled}
|
||||
onChange={() => onSelect(model.id)}
|
||||
/>
|
||||
<span className="model-option-name">{model.displayName}</span>
|
||||
<span className="model-option-meta">
|
||||
{isDownloaded && (
|
||||
<span
|
||||
className="model-badge"
|
||||
title="Weights are cached in this browser — this model loads and transcribes without a network connection."
|
||||
>
|
||||
available offline
|
||||
</span>
|
||||
)}
|
||||
<span className="model-option-size">~{size}</span>
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</fieldset>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user