import type { TTSSynthesizeRequest, TTSResult, VoiceInfo, WordTimestamp, } from "@pipeline/shared"; import type { TTSProvider } from "../types.js"; import { registerProvider } from "../registry.js"; import { measureAudioDuration } from "../duration.js"; import { fetchWithRetry } from "../retry.js"; class OpenAITTSProvider implements TTSProvider { readonly name = "openai-tts"; async synthesize(request: TTSSynthesizeRequest): Promise { const apiKey = process.env.OPENAI_TTS_API_KEY; if (!apiKey) throw new Error("OPENAI_TTS_API_KEY not set"); const baseURL = process.env.OPENAI_TTS_BASE_URL || "https://new-api.corp.shuidi.tech/v1"; const model = process.env.OPENAI_TTS_MODEL || "seed-tts-1.1"; const response = await fetchWithRetry(`${baseURL}/audio/speech`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ input: request.text, model, response_format: request.format || "mp3", speed: request.speed ?? 1.0, voice: request.voiceId || "zh_female_linjianvhai_uranus_bigtts", }), }); if (!response.ok) { const body = await response.text(); throw new Error(`OpenAI TTS API error: ${response.status} ${body}`); } const audioBuffer = Buffer.from(await response.arrayBuffer()); const format = request.format || "mp3"; const outputPath = `${request.outputDir}/${request.filename}.${format}`; const { writeFile, mkdir } = await import("node:fs/promises"); await mkdir(request.outputDir, { recursive: true }); await writeFile(outputPath, audioBuffer); const durationSeconds = await measureAudioDuration(outputPath); return { audioFilePath: outputPath, durationSeconds, wordTimestamps: estimateWordTimestamps(request.text, durationSeconds), format, }; } async listVoices(): Promise { // Voice IDs are configured via Volcengine docs: // https://www.volcengine.com/docs/6561/1257544 return []; } async align(audio: Buffer, text: string): Promise { return estimateWordTimestamps(text, (audio.length * 8) / 128000); } } function estimateWordTimestamps( text: string, totalDuration: number ): WordTimestamp[] { const isChinese = /[一-鿿]/.test(text); const words = isChinese ? text.split("").filter((c) => c.trim()) : text.split(/\s+/).filter((w) => w); const durationPerWord = totalDuration / Math.max(words.length, 1); return words.map((word, i) => ({ word, startSeconds: i * durationPerWord, endSeconds: (i + 1) * durationPerWord, })); } registerProvider("openai-tts", () => new OpenAITTSProvider());