2026-07-02 21:11:23 +08:00
|
|
|
|
import os
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
import requests
|
2026-07-02 21:26:34 +08:00
|
|
|
|
import base64
|
|
|
|
|
|
|
2026-07-02 21:11:23 +08:00
|
|
|
|
|
|
|
|
|
|
def synthesize_doubao(text, speed=1.0, pitch=0, volume=1.0, emotion='neutral'):
|
|
|
|
|
|
"""调用火山引擎豆包 Seed-ICL 2.0 语音合成 API"""
|
|
|
|
|
|
api_key = os.getenv('DOUBAO_API_KEY', '')
|
|
|
|
|
|
if not api_key:
|
|
|
|
|
|
raise ValueError('DOUBAO_API_KEY未设置')
|
|
|
|
|
|
|
|
|
|
|
|
url = 'https://openspeech.bytedance.com/api/v1/tts'
|
|
|
|
|
|
headers = {
|
|
|
|
|
|
'x-api-key': api_key,
|
2026-07-02 21:26:34 +08:00
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'X-Api-Resource-Id': 'seed-icl-2.0'
|
2026-07-02 21:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
'app': {
|
|
|
|
|
|
'cluster': 'volcano_icl'
|
|
|
|
|
|
},
|
|
|
|
|
|
'user': {
|
|
|
|
|
|
'uid': 'military_tech_voice'
|
|
|
|
|
|
},
|
|
|
|
|
|
'audio': {
|
2026-07-02 21:17:12 +08:00
|
|
|
|
'voice_type': 'S_LK0j18H72',
|
2026-07-02 21:11:23 +08:00
|
|
|
|
'encoding': 'mp3',
|
|
|
|
|
|
'speed_ratio': speed,
|
2026-07-02 21:34:49 +08:00
|
|
|
|
'pitch_ratio': max(0.1, min(3.0, 1.0 + pitch / 12)), # 前端-12~12映射到0.1~3.0
|
|
|
|
|
|
'volume_ratio': max(0.1, min(3.0, volume)), # 前端0~10,API 0.1~3.0
|
2026-07-02 21:26:34 +08:00
|
|
|
|
'bitrate': 128000,
|
2026-07-02 21:11:23 +08:00
|
|
|
|
},
|
|
|
|
|
|
'request': {
|
|
|
|
|
|
'reqid': str(uuid.uuid4()),
|
|
|
|
|
|
'text': text,
|
2026-07-02 21:26:34 +08:00
|
|
|
|
'operation': 'query',
|
|
|
|
|
|
'model_type': 4, # ICL 2.0 模型
|
2026-07-02 21:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response = requests.post(url, json=payload, headers=headers, timeout=60)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
|
|
|
|
|
raise Exception(f'豆包API错误: {response.status_code} - {response.text}')
|
|
|
|
|
|
|
|
|
|
|
|
result = response.json()
|
|
|
|
|
|
|
|
|
|
|
|
# 豆包返回格式: {"code": 3000, "data": "base64编码的音频"}
|
|
|
|
|
|
if result.get('code') == 3000 and result.get('data'):
|
|
|
|
|
|
audio_bytes = base64.b64decode(result['data'])
|
|
|
|
|
|
return audio_bytes
|
|
|
|
|
|
|
|
|
|
|
|
# 错误处理
|
|
|
|
|
|
error_msg = result.get('message', '未知错误')
|
|
|
|
|
|
error_code = result.get('code', 'unknown')
|
|
|
|
|
|
raise Exception(f'豆包返回错误 [{error_code}]: {error_msg}')
|