tts_client.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. # TTS 客户端(使用新增的同步合成端点)⭐
  2. import requests
  3. import tempfile
  4. import time
  5. from config import Config
  6. def synthesize(text: str, voice_id: str = None, speed: float = 1.0, emotion: str = None) -> str:
  7. """
  8. 调用 TTS 服务同步合成语音(立即返回音频 URL)
  9. 限制:<= 500 字
  10. """
  11. voice_id = voice_id or Config.TTS_VOICE_ID
  12. url = f'{Config.TTS_BASE_URL}/api/tts/synthesize'
  13. headers = {
  14. 'Authorization': f'Bearer {Config.TTS_API_KEY}',
  15. 'Content-Type': 'application/json',
  16. }
  17. payload = {
  18. 'text': text,
  19. 'voiceId': voice_id,
  20. 'voiceParams': {'speed': speed, 'pitch': 0, 'volume': 50},
  21. }
  22. if emotion:
  23. payload['emotion'] = emotion
  24. preview = text[:30] + ('...' if len(text) > 30 else '')
  25. print(f'TTS 同步合成: "{preview}"')
  26. response = requests.post(url, json=payload, headers=headers, timeout=15)
  27. if response.status_code != 200:
  28. raise Exception(f'TTS 失败: {response.status_code} {response.text}')
  29. data = response.json()
  30. if data.get('code') != 0:
  31. raise Exception(f'TTS 错误: {data.get("message")}')
  32. audio_url = data['data']['audioUrl']
  33. duration = data['data'].get('duration', 0)
  34. print(f'TTS 完成: 时长 {duration:.1f}s')
  35. return audio_url
  36. def download_audio(audio_url: str) -> str:
  37. """下载音频到本地临时文件"""
  38. print(f'下载音频: {audio_url}')
  39. response = requests.get(audio_url, timeout=30)
  40. response.raise_for_status()
  41. suffix = '.mp3' if '.mp3' in audio_url.lower() else '.wav'
  42. tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
  43. tmp_file.write(response.content)
  44. tmp_file.close()
  45. return tmp_file.name
  46. def synthesize_and_download(text: str, voice_id: str = None, emotion: str = None) -> str:
  47. """同步合成 + 下载,返回本地文件路径(一步到位)"""
  48. audio_url = synthesize(text, voice_id, emotion=emotion)
  49. local_path = download_audio(audio_url)
  50. return local_path
  51. def synthesize_async(text: str, voice_id: str = None, max_wait: int = 30) -> str:
  52. """
  53. 长文本异步合成(> 500 字)
  54. 回退方案:调 /tts/generate + 轮询 /tts/chapter-status
  55. """
  56. voice_id = voice_id or Config.TTS_VOICE_ID
  57. headers = {
  58. 'Authorization': f'Bearer {Config.TTS_API_KEY}',
  59. 'Content-Type': 'application/json',
  60. }
  61. # 1. 提交任务
  62. print(f'提交异步任务: "{text[:30]}..."')
  63. response = requests.post(
  64. f'{Config.TTS_BASE_URL}/api/tts/generate',
  65. json={
  66. 'text': text,
  67. 'voiceId': voice_id,
  68. 'voiceParams': {'speed': 1.0, 'pitch': 0, 'volume': 50},
  69. },
  70. headers=headers,
  71. timeout=10,
  72. )
  73. data = response.json()
  74. if data.get('code') != 0:
  75. raise Exception(f'任务提交失败: {data.get("message")}')
  76. chapter_id = data['data']['chapterId']
  77. # 2. 轮询
  78. status_url = f'{Config.TTS_BASE_URL}/api/tts/chapter-status/{chapter_id}'
  79. for i in range(max_wait):
  80. time.sleep(1)
  81. status_resp = requests.get(status_url, headers=headers, timeout=10)
  82. status_data = status_resp.json()['data']
  83. if status_data.get('status') == 'completed':
  84. return status_data['audioUrl']
  85. elif status_data.get('status') == 'failed':
  86. raise Exception('异步任务失败')
  87. print(f' 进度 {i+1}/{max_wait}s...')
  88. raise Exception(f'异步任务超时({max_wait}秒)')
  89. def synthesize_auto(text: str, voice_id: str = None) -> str:
  90. """
  91. 自动选择同步/异步合成
  92. - 文本 <= 500 字:同步(~800ms)
  93. - 文本 > 500 字:异步(3-5s)
  94. """
  95. if len(text) <= 500:
  96. return synthesize(text, voice_id)
  97. else:
  98. return synthesize_async(text, voice_id)
  99. if __name__ == '__main__':
  100. # 测试
  101. test_text = '你好,我是用 TTS 同步端点生成的语音。'
  102. local_path = synthesize_and_download(test_text)
  103. print(f'音频已保存到: {local_path}')