feat(providers): add streaming stt providers

This commit is contained in:
Peter Steinberger
2026-04-23 03:05:44 +01:00
parent 5b68092351
commit 51ed22e608
32 changed files with 2399 additions and 16 deletions

View File

@@ -0,0 +1,77 @@
const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
const DEFAULT_ELEVENLABS_VOICE_ID = "pMsXgVXv3BLzUgSXRplE";
const DEFAULT_ELEVENLABS_TTS_MODEL_ID = "eleven_multilingual_v2";
export function normalizeTranscriptForMatch(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
}
export async function waitForLiveExpectation(expectation: () => void, timeoutMs = 30_000) {
const started = Date.now();
let lastError: unknown;
while (Date.now() - started < timeoutMs) {
try {
expectation();
return;
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
throw lastError;
}
export async function synthesizeElevenLabsLiveSpeech(params: {
text: string;
apiKey: string;
outputFormat: "mp3_44100_128" | "ulaw_8000";
timeoutMs?: number;
}): Promise<Buffer> {
const baseUrl = process.env.ELEVENLABS_BASE_URL?.trim() || DEFAULT_ELEVENLABS_BASE_URL;
const voiceId = process.env.ELEVENLABS_LIVE_VOICE_ID?.trim() || DEFAULT_ELEVENLABS_VOICE_ID;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), params.timeoutMs ?? 30_000);
try {
const url = new URL(`${baseUrl.replace(/\/+$/, "")}/v1/text-to-speech/${voiceId}`);
url.searchParams.set("output_format", params.outputFormat);
const response = await fetch(url, {
method: "POST",
headers: {
"xi-api-key": params.apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: params.text,
model_id: DEFAULT_ELEVENLABS_TTS_MODEL_ID,
voice_settings: {
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
speed: 1,
},
}),
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`ElevenLabs live TTS failed (${response.status})`);
}
return Buffer.from(await response.arrayBuffer());
} finally {
clearTimeout(timeout);
}
}
export async function streamAudioForLiveTest(params: {
audio: Buffer;
sendAudio: (chunk: Buffer) => void;
chunkSize?: number;
delayMs?: number;
}) {
const chunkSize = params.chunkSize ?? 160;
const delayMs = params.delayMs ?? 5;
for (let offset = 0; offset < params.audio.byteLength; offset += chunkSize) {
params.sendAudio(params.audio.subarray(offset, offset + chunkSize));
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}