Let the transcribe progress tracker follow variable-length windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-29 21:44:19 +01:00
1 parent e0a4b2796f
commit 221ac594eb
2 files changed
+86 -7

No files matched your search

+55 -7
View File
@@ -435,6 +435,22 @@ export function applyLoadProgress(
// Transcription progress accumulation (pure; drives the in-progress UI)
// ---------------------------------------------------------------------------
/**
* One generate pass's slice of the audio, for
* `TranscribeProgressTracker.forWindows` (variable-length chunks, D-WS3a).
*/
export interface ProgressWindow {
/** Window-relative seconds → absolute seconds on the file's timeline. */
toAbsolute(seconds: number): number;
/** Absolute end of the audio this window covers; timestamps clamp here. */
endSeconds: number;
/**
* Absolute progress once this window's pass finished: the next window's
* start, or the total for the last window.
*/
nextStartSeconds: number;
}
/**
* Maps the streamer's window-relative timestamps to absolute audio progress.
*
@@ -446,25 +462,56 @@ export function applyLoadProgress(
* 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].
* [0, totalSeconds]. The long-form pipeline (D-WS3a) uses `forWindows`
* instead: its windows are variable-length speech chunks with skipped
* silence between them.
*/
export class TranscribeProgressTracker {
private windowIndex = 0;
private reported = 0;
private readonly window: (index: number) => ProgressWindow | undefined;
/**
* Fixed-stride windows, as the chunked ASR pipeline produces:
* `windowSeconds` long, advancing by `jumpSeconds` (window − 2·stride).
*/
constructor(
private readonly totalSeconds: number,
private readonly windowSeconds: number,
private readonly jumpSeconds: number,
) {}
windowSeconds: number,
jumpSeconds: number,
windows?: readonly ProgressWindow[],
) {
this.window = windows
? (index) => windows[index]
: (index) => ({
toAbsolute: (t) => index * jumpSeconds + Math.min(t, windowSeconds),
endSeconds: index * jumpSeconds + windowSeconds,
nextStartSeconds: (index + 1) * jumpSeconds,
});
}
/**
* Variable-length windows — the long-form pipeline's speech chunks
* (D-WS3a): each maps its own relative time onto the file's timeline, and
* skipped silence between chunks counts as processed once a chunk ends.
*/
static forWindows(
totalSeconds: number,
windows: readonly ProgressWindow[],
): TranscribeProgressTracker {
return new TranscribeProgressTracker(totalSeconds, 0, 0, windows);
}
/**
* 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);
const window = this.window(this.windowIndex);
if (!window) return null;
return this.advance(
Math.min(window.toAbsolute(Math.max(withinWindowSeconds, 0)), window.endSeconds),
);
}
/**
@@ -472,8 +519,9 @@ export class TranscribeProgressTracker {
* of the next window (the amount of audio no later window revisits).
*/
finalizeWindow(): number | null {
const window = this.window(this.windowIndex);
this.windowIndex += 1;
return this.advance(this.windowIndex * this.jumpSeconds);
return this.advance(window ? window.nextStartSeconds : this.totalSeconds);
}
private advance(processedSeconds: number): number | null {
+31
View File
@@ -274,6 +274,37 @@ describe("TranscribeProgressTracker", () => {
expect(tracker.timestamp(8)).toBe(8);
expect(tracker.finalizeWindow()).toBeNull(); // 20 clamps to 8, not an advance
});
describe("forWindows (variable-length VAD chunks, D-WS3a)", () => {
// Two speech chunks with a skipped 15 s silence between them: [0–20 s]
// and [35–60 s] of a 70 s file.
const windows = [
{ toAbsolute: (t: number) => t, endSeconds: 20, nextStartSeconds: 35 },
{ toAbsolute: (t: number) => 35 + t, endSeconds: 60, nextStartSeconds: 70 },
];
test("maps chunk-relative times through each window", () => {
const tracker = TranscribeProgressTracker.forWindows(70, windows);
expect(tracker.timestamp(5)).toBe(5);
// Finishing a chunk counts the skipped silence up to the next one.
expect(tracker.finalizeWindow()).toBe(35);
expect(tracker.timestamp(10)).toBe(45);
expect(tracker.timestamp(2)).toBeNull(); // 37 < 45: no regression
expect(tracker.finalizeWindow()).toBe(70);
expect(tracker.finalizeWindow()).toBeNull(); // past the last window
});
test("clamps a stray timestamp to the window's end", () => {
const tracker = TranscribeProgressTracker.forWindows(70, windows);
expect(tracker.timestamp(400)).toBe(20);
});
test("no windows (no speech) → nothing to report but the end", () => {
const tracker = TranscribeProgressTracker.forWindows(70, []);
expect(tracker.timestamp(3)).toBeNull();
expect(tracker.finalizeWindow()).toBe(70);
});
});
});
describe("applyLoadProgress", () => {