Agent completed work jubii

This commit is contained in:
Lucas Winther
2026-07-09 08:07:32 +02:00
parent 6466d3a45c
commit eb72a2e646
29 changed files with 2051 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
import { useEffect, useRef, useState } from "react";
import "./index.css";
import { FileDropzone } from "./components/FileDropzone";
import { ModelLoadProgress } from "./components/ModelLoadProgress";
import { TranscriptView } from "./components/TranscriptView";
import { decodeAudioFile } from "./lib/audio";
import { DEFAULT_DEVICE, DEFAULT_MODEL_ID } from "./lib/models";
import {
applyLoadProgress,
initialLoadProgress,
isWorkerToUiMessage,
type TranscriptChunk,
type UiToWorkerMessage,
} from "./worker/protocol";
interface TranscriptResult {
text: string;
chunks: TranscriptChunk[];
}
export function App() {
const workerRef = useRef<Worker | null>(null);
const [busy, setBusy] = useState(false);
const [decoding, setDecoding] = useState(false);
const [loadStarted, setLoadStarted] = useState(false);
const [loadProgress, setLoadProgress] = useState(initialLoadProgress);
const [partialText, setPartialText] = useState("");
const [result, setResult] = useState<TranscriptResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
// Terminate the worker when the app unmounts (also keeps HMR tidy).
useEffect(() => {
return () => {
workerRef.current?.terminate();
workerRef.current = null;
};
}, []);
function ensureWorker(): Worker {
if (workerRef.current) return workerRef.current;
// Inference always runs in a dedicated Web Worker (D-003), never on the
// main thread (Architecture.md §5). The worker script is bundled by the
// server at startup and served at a stable URL (see src/index.ts) because
// Bun's bundler does not follow `new Worker(new URL(...))` graphs yet.
const worker = new Worker("/inference.worker.js", { type: "module" });
worker.addEventListener("message", (event: MessageEvent<unknown>) => {
const message = event.data;
if (!isWorkerToUiMessage(message)) return;
switch (message.type) {
case "load-progress":
setLoadProgress((state) => applyLoadProgress(state, message));
break;
case "partial":
setPartialText((text) => text + message.text);
break;
case "result":
setResult({ text: message.text, chunks: message.chunks });
setBusy(false);
break;
case "error":
setError(message.message);
setBusy(false);
break;
}
});
worker.addEventListener("error", (event) => {
setError(event.message || "The inference worker crashed unexpectedly.");
setBusy(false);
});
workerRef.current = worker;
return worker;
}
function postToWorker(worker: Worker, message: UiToWorkerMessage, transfer?: Transferable[]) {
worker.postMessage(message, transfer ?? []);
}
async function handleFile(file: File) {
if (busy) return;
setBusy(true);
setError(null);
setPartialText("");
setResult(null);
setFileName(file.name);
// Kick off the (idempotent) model load immediately so the download runs
// while we decode the audio locally.
const worker = ensureWorker();
postToWorker(worker, {
type: "load",
modelId: DEFAULT_MODEL_ID,
device: DEFAULT_DEVICE,
});
setLoadStarted(true);
setDecoding(true);
try {
// Decoded entirely on-device via Web Audio (D-012); the audio never
// touches the network (Architecture.md §5).
const audio = await decodeAudioFile(file);
// Transfer the buffer to the worker — zero copy.
postToWorker(worker, { type: "transcribe", audio }, [audio.buffer as ArrayBuffer]);
} catch (decodeError) {
setError(decodeError instanceof Error ? decodeError.message : String(decodeError));
setBusy(false);
} finally {
setDecoding(false);
}
}
const modelReady = loadProgress.ready;
const showModelProgress = loadStarted && !modelReady && !error;
const transcribing = busy && !decoding && modelReady;
return (
<main className="app">
<header className="app-header">
<h1>Whisper Web</h1>
<p className="tagline">
Speech-to-text that runs entirely in your browser. Your audio never
leaves this device.
</p>
</header>
<FileDropzone onFile={handleFile} disabled={busy} />
{fileName && (
<p className="file-name">
File: <strong>{fileName}</strong>
{decoding && " — decoding audio…"}
</p>
)}
{error && (
<p className="error" role="alert">
{error}
</p>
)}
{showModelProgress && (
<ModelLoadProgress modelId={DEFAULT_MODEL_ID} progress={loadProgress} />
)}
<TranscriptView
partialText={partialText}
result={result}
transcribing={transcribing}
/>
<footer className="app-footer">
<p>
Model: <code>{DEFAULT_MODEL_ID}</code> · Engine: WASM (on-device)
</p>
</footer>
</main>
);
}
export default App;