Let the transcribe progress tracker follow variable-length windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0a4b2796f
commit
221ac594eb
2 files changed
+86
-7
No files matched your search
+55
-7
@@ -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 {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in new issue
Block a user