Add cancel + progress UI, EngineToggle, and Docker deployment

This commit is contained in:
Lucas Winther
2026-07-14 19:32:10 +02:00
parent be82ecfe57
commit 0084e0435a
16 changed files with 972 additions and 60 deletions
+41 -3
View File
@@ -19,6 +19,7 @@ import { resolveDefaultDtype } from "../lib/models";
import {
deviceAttempts,
isUiToWorkerMessage,
TranscribeProgressTracker,
type DeviceCapabilities,
type DevicePreference,
type InferenceDevice,
@@ -209,6 +210,12 @@ function load(message: LoadMessage): Promise<AutomaticSpeechRecognitionPipeline>
// Transcription
// ---------------------------------------------------------------------------
/** Audio sample rate the whole app standardizes on (D-012). */
const SAMPLE_RATE = 16_000;
/** Chunked-pipeline window / overlap, in seconds (Whisper's usual 30/5). */
const CHUNK_LENGTH_S = 30;
const STRIDE_LENGTH_S = 5;
async function transcribe(message: TranscribeMessage): Promise<void> {
if (active === null) {
post({
@@ -222,11 +229,39 @@ async function transcribe(message: TranscribeMessage): Promise<void> {
const asr = await active.promise;
const options: Record<string, unknown> = {
chunk_length_s: 30,
stride_length_s: 5,
chunk_length_s: CHUNK_LENGTH_S,
stride_length_s: STRIDE_LENGTH_S,
return_timestamps: true,
};
if (message.requestId === undefined) {
if (message.stream ?? message.requestId === undefined) {
// File-flow pass (id-less, or routed through BrowserEngine with an
// explicit stream flag, D-010): stream partial tokens and report audio
// progress. The streamer's chunk timestamps are window-relative; the
// tracker maps them to monotone absolute seconds (see protocol.ts).
// Live passes skip both — they resolve wholesale via "result" (D-016).
const totalSeconds = message.audio.length / SAMPLE_RATE;
const tracker = new TranscribeProgressTracker(
totalSeconds,
CHUNK_LENGTH_S,
CHUNK_LENGTH_S - 2 * STRIDE_LENGTH_S,
);
const postProgress = (processedSeconds: number | null) => {
if (processedSeconds === null) return;
post({
type: "transcribe-progress",
processedSeconds,
totalSeconds,
requestId: message.requestId,
});
};
// Immediate 0/total so the UI shows the audio length before the first
// (possibly slow) token arrives.
post({
type: "transcribe-progress",
processedSeconds: 0,
totalSeconds,
requestId: message.requestId,
});
options.streamer = new WhisperTextStreamer(
// The ASR pipeline for Whisper models always carries a WhisperTokenizer.
asr.tokenizer as ConstructorParameters<typeof WhisperTextStreamer>[0],
@@ -234,6 +269,9 @@ async function transcribe(message: TranscribeMessage): Promise<void> {
skip_prompt: true,
callback_function: (text: string) =>
post({ type: "partial", text, requestId: message.requestId }),
on_chunk_start: (time: number) => postProgress(tracker.timestamp(time)),
on_chunk_end: (time: number) => postProgress(tracker.timestamp(time)),
on_finalize: () => postProgress(tracker.finalizeWindow()),
},
);
}