Add cancel + progress UI, EngineToggle, and Docker deployment
This commit is contained in:
+86
-1
@@ -69,6 +69,14 @@ export interface TranscribeMessage {
|
||||
* responses to segment passes; the file flow omits it and is unchanged.
|
||||
*/
|
||||
requestId?: string;
|
||||
/**
|
||||
* Stream partial tokens and transcribe-progress for this request. Defaults
|
||||
* to `requestId === undefined` (the historical rule: id-less file-flow
|
||||
* passes stream, live segment passes resolve wholesale via "result").
|
||||
* `BrowserEngine` sets it explicitly so a file pass routed through the
|
||||
* engine (which correlates via requestId, D-010) still streams.
|
||||
*/
|
||||
stream?: boolean;
|
||||
}
|
||||
|
||||
export type UiToWorkerMessage = LoadMessage | TranscribeMessage;
|
||||
@@ -108,6 +116,22 @@ export interface PartialMessage {
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* How far a file-flow pass has progressed through the audio, in seconds.
|
||||
* Posted once at pass start (0 / total) and then on every Whisper segment
|
||||
* boundary, so slow models/backends show movement long before the first
|
||||
* partial token. Values are monotone and clamped to [0, totalSeconds]
|
||||
* (see TranscribeProgressTracker). Live passes (requestId set) don't report
|
||||
* progress — the live UI has its own committed/tentative feedback (D-016).
|
||||
*/
|
||||
export interface TranscribeProgressMessage {
|
||||
type: "transcribe-progress";
|
||||
processedSeconds: number;
|
||||
totalSeconds: number;
|
||||
/** Echo of the originating transcribe request's id, if it carried one. */
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
/** One timestamped segment of the final transcript. */
|
||||
export interface TranscriptChunk {
|
||||
text: string;
|
||||
@@ -134,6 +158,7 @@ export interface ErrorMessage {
|
||||
export type WorkerToUiMessage =
|
||||
| LoadProgressMessage
|
||||
| PartialMessage
|
||||
| TranscribeProgressMessage
|
||||
| ResultMessage
|
||||
| ErrorMessage;
|
||||
|
||||
@@ -234,7 +259,8 @@ export function isUiToWorkerMessage(value: unknown): value is UiToWorkerMessage
|
||||
(value.task === undefined ||
|
||||
value.task === "transcribe" ||
|
||||
value.task === "translate") &&
|
||||
isOptionalString(value.requestId)
|
||||
isOptionalString(value.requestId) &&
|
||||
(value.stream === undefined || typeof value.stream === "boolean")
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
@@ -264,6 +290,12 @@ export function isWorkerToUiMessage(value: unknown): value is WorkerToUiMessage
|
||||
}
|
||||
case "partial":
|
||||
return typeof value.text === "string" && isOptionalString(value.requestId);
|
||||
case "transcribe-progress":
|
||||
return (
|
||||
typeof value.processedSeconds === "number" &&
|
||||
typeof value.totalSeconds === "number" &&
|
||||
isOptionalString(value.requestId)
|
||||
);
|
||||
case "result":
|
||||
return (
|
||||
typeof value.text === "string" &&
|
||||
@@ -331,3 +363,56 @@ export function applyLoadProgress(
|
||||
}
|
||||
return { ...state, files: { ...state.files, [message.file]: next } };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transcription progress accumulation (pure; drives the in-progress UI)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Maps the streamer's window-relative timestamps to absolute audio progress.
|
||||
*
|
||||
* The chunked ASR pipeline slices audio into windows of `windowSeconds`,
|
||||
* advancing by `jumpSeconds` (window − 2·stride) per window, and runs one
|
||||
* generate pass per window. Whisper's timestamp tokens — surfaced via
|
||||
* `WhisperTextStreamer`'s `on_chunk_start`/`on_chunk_end` — are relative to
|
||||
* the current window's start, and `on_finalize` fires when a window's pass
|
||||
* ends. This tracker turns those into absolute seconds that are monotone
|
||||
* (windows overlap, so a later window's early timestamps can precede an
|
||||
* earlier window's late ones — regressions are swallowed) and clamped to
|
||||
* [0, totalSeconds].
|
||||
*/
|
||||
export class TranscribeProgressTracker {
|
||||
private windowIndex = 0;
|
||||
private reported = 0;
|
||||
|
||||
constructor(
|
||||
private readonly totalSeconds: number,
|
||||
private readonly windowSeconds: number,
|
||||
private readonly jumpSeconds: number,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Absolute progress for a window-relative timestamp, or null when it does
|
||||
* not advance past what was already reported.
|
||||
*/
|
||||
timestamp(withinWindowSeconds: number): number | null {
|
||||
const clamped = Math.min(Math.max(withinWindowSeconds, 0), this.windowSeconds);
|
||||
return this.advance(this.windowIndex * this.jumpSeconds + clamped);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current window's generate pass finished; progress moves to the start
|
||||
* of the next window (the amount of audio no later window revisits).
|
||||
*/
|
||||
finalizeWindow(): number | null {
|
||||
this.windowIndex += 1;
|
||||
return this.advance(this.windowIndex * this.jumpSeconds);
|
||||
}
|
||||
|
||||
private advance(processedSeconds: number): number | null {
|
||||
const clamped = Math.min(processedSeconds, this.totalSeconds);
|
||||
if (clamped <= this.reported) return null;
|
||||
this.reported = clamped;
|
||||
return clamped;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user