refactor(whisper): implement dynamic timeout configuration for alignment and decoding
Replace static timeout constants with functions that derive alignment and ffmpeg decode timeouts from compute timeout config. This enables adaptive timeout behavior based on runtime configuration, improving robustness under varying resource constraints. Update error handling and internal APIs to propagate timeout values accordingly. Remove unused Node.js engine constraint from package.json for broader compatibility.
This commit is contained in:
parent
7cdd89b26d
commit
661af71fa4
2 changed files with 32 additions and 14 deletions
|
|
@ -10,6 +10,7 @@ import JSZip from 'jszip';
|
||||||
import type { TTSAudioBuffer, TTSAudioBytes, TTSSentenceAlignment } from '../types/tts';
|
import type { TTSAudioBuffer, TTSAudioBytes, TTSSentenceAlignment } from '../types/tts';
|
||||||
import { getFFmpegPath } from '../runtime/ffmpeg';
|
import { getFFmpegPath } from '../runtime/ffmpeg';
|
||||||
import { getOnnxThreadsPerJob } from '../runtime/cpu-budget';
|
import { getOnnxThreadsPerJob } from '../runtime/cpu-budget';
|
||||||
|
import { getComputeTimeoutConfig } from '../runtime/timeout-config';
|
||||||
import {
|
import {
|
||||||
mapWordsToSentenceOffsets,
|
mapWordsToSentenceOffsets,
|
||||||
type WhisperWord,
|
type WhisperWord,
|
||||||
|
|
@ -92,8 +93,10 @@ const alignmentCache = state.alignmentCache;
|
||||||
const alignmentInFlight = state.alignmentInFlight;
|
const alignmentInFlight = state.alignmentInFlight;
|
||||||
const ALIGNMENT_CACHE_MAX_ENTRIES = 256;
|
const ALIGNMENT_CACHE_MAX_ENTRIES = 256;
|
||||||
const MAX_DECODE_STEPS_CAP = 128;
|
const MAX_DECODE_STEPS_CAP = 128;
|
||||||
const ALIGNMENT_TIMEOUT_MS = 25000;
|
const ALIGNMENT_TIMEOUT_BUFFER_MS = 2_000;
|
||||||
const FFMPEG_DECODE_TIMEOUT_MS = 10000;
|
const MIN_ALIGNMENT_TIMEOUT_MS = 5_000;
|
||||||
|
const MIN_FFMPEG_DECODE_TIMEOUT_MS = 10_000;
|
||||||
|
const MAX_FFMPEG_DECODE_TIMEOUT_MS = 60_000;
|
||||||
|
|
||||||
const SAMPLE_RATE = 16000;
|
const SAMPLE_RATE = 16000;
|
||||||
const N_FFT = 400;
|
const N_FFT = 400;
|
||||||
|
|
@ -284,7 +287,22 @@ function computeLogMelSpectrogram(audioSamples: Float32Array): ort.Tensor {
|
||||||
return new ort.Tensor('float32', flattened, [1, N_MELS, frameCount]);
|
return new ort.Tensor('float32', flattened, [1, N_MELS, frameCount]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAlignmentTimeoutMs(): number {
|
||||||
|
const whisperTimeoutMs = getComputeTimeoutConfig().whisperTimeoutMs;
|
||||||
|
return Math.max(MIN_ALIGNMENT_TIMEOUT_MS, whisperTimeoutMs - ALIGNMENT_TIMEOUT_BUFFER_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFfmpegDecodeTimeoutMs(): number {
|
||||||
|
const whisperTimeoutMs = getComputeTimeoutConfig().whisperTimeoutMs;
|
||||||
|
const halfBudgetMs = Math.floor(whisperTimeoutMs * 0.5);
|
||||||
|
return Math.max(
|
||||||
|
MIN_FFMPEG_DECODE_TIMEOUT_MS,
|
||||||
|
Math.min(MAX_FFMPEG_DECODE_TIMEOUT_MS, halfBudgetMs),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function decodeToPcm16(inputPath: string, outputPath: string): Promise<void> {
|
async function decodeToPcm16(inputPath: string, outputPath: string): Promise<void> {
|
||||||
|
const ffmpegDecodeTimeoutMs = getFfmpegDecodeTimeoutMs();
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const ffmpeg = spawn(getFFmpegPath(), [
|
const ffmpeg = spawn(getFFmpegPath(), [
|
||||||
'-y',
|
'-y',
|
||||||
|
|
@ -304,7 +322,7 @@ async function decodeToPcm16(inputPath: string, outputPath: string): Promise<voi
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
timedOut = true;
|
timedOut = true;
|
||||||
ffmpeg.kill('SIGKILL');
|
ffmpeg.kill('SIGKILL');
|
||||||
}, FFMPEG_DECODE_TIMEOUT_MS);
|
}, ffmpegDecodeTimeoutMs);
|
||||||
ffmpeg.stderr.on('data', (data) => {
|
ffmpeg.stderr.on('data', (data) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
|
|
@ -317,7 +335,7 @@ async function decodeToPcm16(inputPath: string, outputPath: string): Promise<voi
|
||||||
ffmpeg.on('close', (code) => {
|
ffmpeg.on('close', (code) => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
reject(new Error(`ffmpeg decode timed out after ${FFMPEG_DECODE_TIMEOUT_MS}ms`));
|
reject(new Error(`ffmpeg decode timed out after ${ffmpegDecodeTimeoutMs}ms`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
|
|
@ -368,9 +386,9 @@ function computeAdaptiveDecodeStepLimit(maxDecodeSteps: number, textHint?: strin
|
||||||
return adaptive;
|
return adaptive;
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertWithinDeadline(deadlineMs: number): void {
|
function assertWithinDeadline(deadlineMs: number, timeoutMs: number): void {
|
||||||
if (Date.now() > deadlineMs) {
|
if (Date.now() > deadlineMs) {
|
||||||
throw new Error(`Whisper alignment timed out after ${ALIGNMENT_TIMEOUT_MS}ms`);
|
throw new Error(`Whisper alignment timed out after ${timeoutMs}ms`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -657,8 +675,9 @@ async function runWhisperOnnx(
|
||||||
opts: WhisperAlignmentOptions,
|
opts: WhisperAlignmentOptions,
|
||||||
numFrames: number,
|
numFrames: number,
|
||||||
deadlineMs: number,
|
deadlineMs: number,
|
||||||
|
timeoutMs: number,
|
||||||
): Promise<WhisperWord[]> {
|
): Promise<WhisperWord[]> {
|
||||||
assertWithinDeadline(deadlineMs);
|
assertWithinDeadline(deadlineMs, timeoutMs);
|
||||||
const runtime = await getRuntime();
|
const runtime = await getRuntime();
|
||||||
const decodeStepLimit = computeAdaptiveDecodeStepLimit(runtime.maxDecodeSteps, opts.textHint);
|
const decodeStepLimit = computeAdaptiveDecodeStepLimit(runtime.maxDecodeSteps, opts.textHint);
|
||||||
const mel = computeLogMelSpectrogram(audioSamples);
|
const mel = computeLogMelSpectrogram(audioSamples);
|
||||||
|
|
@ -740,7 +759,7 @@ async function runWhisperOnnx(
|
||||||
...emptyPastFeeds,
|
...emptyPastFeeds,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
assertWithinDeadline(deadlineMs);
|
assertWithinDeadline(deadlineMs, timeoutMs);
|
||||||
outputs = await runtime.decoderMerged.run(prefillFeeds, runtime.prefillFetches);
|
outputs = await runtime.decoderMerged.run(prefillFeeds, runtime.prefillFetches);
|
||||||
} finally {
|
} finally {
|
||||||
disposeTensor(prefillInputIds);
|
disposeTensor(prefillInputIds);
|
||||||
|
|
@ -756,7 +775,7 @@ async function runWhisperOnnx(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let step = 0; step < decodeStepLimit; step += 1) {
|
for (let step = 0; step < decodeStepLimit; step += 1) {
|
||||||
assertWithinDeadline(deadlineMs);
|
assertWithinDeadline(deadlineMs, timeoutMs);
|
||||||
if (!outputs) break;
|
if (!outputs) break;
|
||||||
const logits = outputs.logits;
|
const logits = outputs.logits;
|
||||||
const logitsData = logits.data as Float32Array;
|
const logitsData = logits.data as Float32Array;
|
||||||
|
|
@ -791,7 +810,7 @@ async function runWhisperOnnx(
|
||||||
};
|
};
|
||||||
let nextOutputs: Record<string, ort.Tensor>;
|
let nextOutputs: Record<string, ort.Tensor>;
|
||||||
try {
|
try {
|
||||||
assertWithinDeadline(deadlineMs);
|
assertWithinDeadline(deadlineMs, timeoutMs);
|
||||||
nextOutputs = await runtime.decoderWithPast.run(stepFeeds, runtime.stepFetches);
|
nextOutputs = await runtime.decoderWithPast.run(stepFeeds, runtime.stepFetches);
|
||||||
} finally {
|
} finally {
|
||||||
disposeTensor(stepInputIds);
|
disposeTensor(stepInputIds);
|
||||||
|
|
@ -924,7 +943,8 @@ export async function alignAudioWithText(
|
||||||
|
|
||||||
state.pendingAlignments += 1;
|
state.pendingAlignments += 1;
|
||||||
const run = (async (): Promise<TTSSentenceAlignment[]> => {
|
const run = (async (): Promise<TTSSentenceAlignment[]> => {
|
||||||
const deadlineMs = Date.now() + ALIGNMENT_TIMEOUT_MS;
|
const alignmentTimeoutMs = getAlignmentTimeoutMs();
|
||||||
|
const deadlineMs = Date.now() + alignmentTimeoutMs;
|
||||||
const previous = state.alignMutex;
|
const previous = state.alignMutex;
|
||||||
let release!: () => void;
|
let release!: () => void;
|
||||||
state.alignMutex = new Promise<void>((resolve) => {
|
state.alignMutex = new Promise<void>((resolve) => {
|
||||||
|
|
@ -966,6 +986,7 @@ export async function alignAudioWithText(
|
||||||
{ ...opts, textHint: text },
|
{ ...opts, textHint: text },
|
||||||
effectiveFrameCount,
|
effectiveFrameCount,
|
||||||
deadlineMs,
|
deadlineMs,
|
||||||
|
alignmentTimeoutMs,
|
||||||
);
|
);
|
||||||
const alignment = mapWordsToSentenceOffsets(text, words);
|
const alignment = mapWordsToSentenceOffsets(text, words);
|
||||||
const result: TTSSentenceAlignment[] = [alignment];
|
const result: TTSSentenceAlignment[] = [alignment];
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
"name": "openreader",
|
"name": "openreader",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
|
||||||
"node": "22.x"
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@10.33.4",
|
"packageManager": "pnpm@10.33.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/openreader-entrypoint.mjs -- pnpm dev:raw",
|
"dev": "node scripts/openreader-entrypoint.mjs -- pnpm dev:raw",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue