openai-tts.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import type {
  2. TTSSynthesizeRequest,
  3. TTSResult,
  4. VoiceInfo,
  5. WordTimestamp,
  6. } from "@pipeline/shared";
  7. import type { TTSProvider } from "../types.js";
  8. import { registerProvider } from "../registry.js";
  9. import { measureAudioDuration } from "../duration.js";
  10. class OpenAITTSProvider implements TTSProvider {
  11. readonly name = "openai-tts";
  12. async synthesize(request: TTSSynthesizeRequest): Promise<TTSResult> {
  13. const apiKey = process.env.OPENAI_TTS_API_KEY;
  14. if (!apiKey) throw new Error("OPENAI_TTS_API_KEY not set");
  15. const baseURL =
  16. process.env.OPENAI_TTS_BASE_URL ||
  17. "https://new-api.corp.shuidi.tech/v1";
  18. const model = process.env.OPENAI_TTS_MODEL || "seed-tts-1.1";
  19. const response = await fetch(`${baseURL}/audio/speech`, {
  20. method: "POST",
  21. headers: {
  22. "Content-Type": "application/json",
  23. Authorization: `Bearer ${apiKey}`,
  24. },
  25. body: JSON.stringify({
  26. input: request.text,
  27. model,
  28. response_format: request.format || "mp3",
  29. speed: request.speed ?? 1.0,
  30. voice: request.voiceId || "zh_female_linjianvhai_uranus_bigtts",
  31. }),
  32. });
  33. if (!response.ok) {
  34. const body = await response.text();
  35. throw new Error(`OpenAI TTS API error: ${response.status} ${body}`);
  36. }
  37. const audioBuffer = Buffer.from(await response.arrayBuffer());
  38. const format = request.format || "mp3";
  39. const outputPath = `${request.outputDir}/${request.filename}.${format}`;
  40. const { writeFile, mkdir } = await import("node:fs/promises");
  41. await mkdir(request.outputDir, { recursive: true });
  42. await writeFile(outputPath, audioBuffer);
  43. const durationSeconds = await measureAudioDuration(outputPath);
  44. return {
  45. audioFilePath: outputPath,
  46. durationSeconds,
  47. wordTimestamps: estimateWordTimestamps(request.text, durationSeconds),
  48. format,
  49. };
  50. }
  51. async listVoices(): Promise<VoiceInfo[]> {
  52. // Voice IDs are configured via Volcengine docs:
  53. // https://www.volcengine.com/docs/6561/1257544
  54. return [];
  55. }
  56. async align(audio: Buffer, text: string): Promise<WordTimestamp[]> {
  57. return estimateWordTimestamps(text, (audio.length * 8) / 128000);
  58. }
  59. }
  60. function estimateWordTimestamps(
  61. text: string,
  62. totalDuration: number
  63. ): WordTimestamp[] {
  64. const isChinese = /[一-鿿]/.test(text);
  65. const words = isChinese
  66. ? text.split("").filter((c) => c.trim())
  67. : text.split(/\s+/).filter((w) => w);
  68. const durationPerWord = totalDuration / Math.max(words.length, 1);
  69. return words.map((word, i) => ({
  70. word,
  71. startSeconds: i * durationPerWord,
  72. endSeconds: (i + 1) * durationPerWord,
  73. }));
  74. }
  75. registerProvider("openai-tts", () => new OpenAITTSProvider());