Better model download management
This commit is contained in:
@@ -107,8 +107,11 @@ are the remaining M5 work; see `WorkBreakdown.md`.)
|
||||
|
||||
### 4.1 Browser transcription (anonymous, default)
|
||||
1. User picks a model in `ModelPicker` (curated catalog from `lib/models.ts`, with
|
||||
size estimates and a Cache-API-backed "downloaded" badge); UI posts
|
||||
`{type:"load", modelId, device}` where `device` is a preference
|
||||
size estimates and a Cache-API-backed "downloaded" badge). Selection alone
|
||||
downloads nothing (D-020): the UI posts `{type:"load", modelId, device}` when a
|
||||
browser-engine transcription starts (file drop / live start) or when the user
|
||||
clicks the explicit **Download** button on the selected model; server-engine
|
||||
runs never post a load. `device` is a preference
|
||||
(`"auto" | "webnn" | "webgpu" | "wasm"`, default `"auto"`). An explicit `dtype`
|
||||
override is expressible in the message but normally omitted.
|
||||
2. Worker resolves the device (D-004, D-017): for `"auto"` it probes
|
||||
|
||||
@@ -272,3 +272,21 @@ and its loaded pipeline are left untouched in that path.
|
||||
**Why:** M7 future want. Enables logged-in users on weak devices to transcribe live microphone audio using the server engine.
|
||||
**Shape:** The client opens a WebSocket connection to `/api/transcribe/live`. The client streams 16kHz Float32 PCM chunks (from `MicCapture`) over the WebSocket. The server runs `LiveTranscriber` and `LiveSegmenter` (from `src/lib/live.ts`, which is pure and runs perfectly in Node/Bun). For each segment/pass, the server's `LiveTranscriber` writes the audio to a temporary file and invokes the Python sidecar. The server sends `LiveUpdate` objects back to the client over the WebSocket.
|
||||
**Consequence:** The client does zero ML and minimal buffering, acting only as a mic-streamer and UI-renderer for live passes on the server.
|
||||
|
||||
## D-020 — Model downloads are user-initiated, never a side effect of selection
|
||||
**Status:** Accepted (2026-07-14)
|
||||
Selecting a model in `ModelPicker` (or changing the backend preference) no longer
|
||||
starts the weight download. Weights load exactly when they are needed or asked for:
|
||||
- **Lazy:** a file drop or live start on the **browser** engine posts the
|
||||
(idempotent) load, as both paths already did — the download overlaps decoding /
|
||||
mic startup.
|
||||
- **Explicit:** a **Download** button on the selected, not-yet-cached model in
|
||||
`ModelPicker` starts the load immediately — this keeps D-005's "prepare for
|
||||
offline use" story alive now that selection alone caches nothing.
|
||||
- **Server engine:** server runs never touch the browser pipeline, so with the
|
||||
toggle on "server" nothing downloads. (Previously the eager selection load
|
||||
downloaded browser weights even when every transcription would run server-side.)
|
||||
**Consequence:** model/backend switches just reset the load UI state
|
||||
(`loadStarted`, progress); the next load starts from clean progress. The explicit
|
||||
download is a full pipeline load, so it also warms up inference and surfaces the
|
||||
"Running on …" backend badge.
|
||||
|
||||
@@ -242,6 +242,11 @@ Notes (M5):
|
||||
models, versioned/cleaned by the SW itself) is deliberately not
|
||||
surfaced — deleting it per-model makes no sense and the SW would
|
||||
just re-fetch it.
|
||||
- [x] Model downloads are user-initiated (D-020): selecting a model/backend no
|
||||
longer auto-downloads weights; the load starts on browser-engine
|
||||
transcription start (file drop / live start) or via an explicit
|
||||
per-model Download button in `ModelPicker` — and never when the engine
|
||||
toggle is set to "server".
|
||||
- [ ] Basic a11y pass (keyboard, focus, aria on progress/live transcript)
|
||||
- [ ] Production build + `bun start` deployment check; README for deployers
|
||||
|
||||
|
||||
+17
-4
@@ -283,16 +283,27 @@ export function App() {
|
||||
setPartialText("");
|
||||
setTranscribeProgress(null);
|
||||
setResult(null);
|
||||
startLoad(nextModelId, devicePreference);
|
||||
// Selection never downloads (D-020): the load starts when a transcription
|
||||
// needs it (file drop / live start on the browser engine) or when the
|
||||
// user explicitly clicks Download. Reset the load state so the next load
|
||||
// reports fresh progress for this model.
|
||||
setLoadStarted(false);
|
||||
setLoadProgress(initialLoadProgress);
|
||||
}
|
||||
|
||||
function handleDevicePreference(preference: DevicePreference) {
|
||||
if (busy || liveStatus !== "idle" || preference === devicePreference) return;
|
||||
setDevicePreference(preference);
|
||||
setActiveDevice(null);
|
||||
// Only reload eagerly if a load was already in motion; otherwise the next
|
||||
// file drop picks up the new preference.
|
||||
if (loadStarted) startLoad(modelId, preference);
|
||||
// No eager reload (D-020) — the next load picks up the new preference.
|
||||
setLoadStarted(false);
|
||||
setLoadProgress(initialLoadProgress);
|
||||
}
|
||||
|
||||
/** Explicit model download (D-020) — pre-caches weights for offline use. */
|
||||
function handleDownloadModel() {
|
||||
if (busy || liveStatus !== "idle" || loadStarted) return;
|
||||
startLoad(modelId, devicePreference);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -631,6 +642,8 @@ export function App() {
|
||||
downloaded={downloaded}
|
||||
offlineReady={swControlled}
|
||||
disabled={busy || liveActive}
|
||||
onDownloadSelected={handleDownloadModel}
|
||||
downloading={loadStarted && !modelReady}
|
||||
/>
|
||||
|
||||
{model?.multilingual && (
|
||||
|
||||
@@ -19,6 +19,10 @@ interface ModelPickerProps {
|
||||
offlineReady: boolean;
|
||||
/** True while a transcription is running — switching is disabled then. */
|
||||
disabled?: boolean;
|
||||
/** Explicitly download the selected model's weights (D-020). */
|
||||
onDownloadSelected: () => void;
|
||||
/** True while a model load/download is in flight. */
|
||||
downloading: boolean;
|
||||
}
|
||||
|
||||
const DEVICE_OPTIONS: readonly { value: DevicePreference; label: string }[] = [
|
||||
@@ -50,6 +54,8 @@ export function ModelPicker({
|
||||
downloaded,
|
||||
offlineReady,
|
||||
disabled = false,
|
||||
onDownloadSelected,
|
||||
downloading,
|
||||
}: 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
|
||||
@@ -116,7 +122,7 @@ export function ModelPicker({
|
||||
{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."
|
||||
: "The app is cached for offline use. Download a model (or run one on-device transcription) 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) => (
|
||||
@@ -153,6 +159,17 @@ export function ModelPicker({
|
||||
</span>
|
||||
)}
|
||||
<span className="model-option-size">~{size}</span>
|
||||
{isSelected && !isDownloaded && (
|
||||
<button
|
||||
type="button"
|
||||
className="model-download-button"
|
||||
disabled={disabled || downloading}
|
||||
title="Download this model now so it is available offline. Otherwise it downloads automatically when an on-device transcription starts."
|
||||
onClick={onDownloadSelected}
|
||||
>
|
||||
{downloading ? "Downloading…" : "Download"}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
@@ -306,6 +306,29 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Explicit weight download for the selected model (D-020). */
|
||||
.model-download-button {
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-radius: 999px;
|
||||
padding: 0.1rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.model-download-button:hover:enabled {
|
||||
border-color: #3d6b4f;
|
||||
color: #9ad6ae;
|
||||
}
|
||||
|
||||
.model-download-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* --- PWA / offline (M3) --- */
|
||||
.offline-status {
|
||||
margin: 0.5rem 0 0.75rem;
|
||||
|
||||
Reference in New Issue
Block a user