- Add bin/api/ package with generate_image() and synthesize_tts() - Refactor bin/commands/image.py to call bin.api.image.generate_image - Refactor bin/commands/voice.py to call bin.api.voice.synthesize_tts - API layer is free of Click CLI dependencies, usable by external scripts
94 lines
2.6 KiB
Python
94 lines
2.6 KiB
Python
"""Voice / TTS API client via Volcengine (ByteDance) WebSocket API."""
|
|
|
|
import asyncio
|
|
import json
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
from bin.commands.protocols import EventType, MsgType, full_client_request, receive_message
|
|
|
|
_RESOURCE_ID = "volc.service_type.10029"
|
|
_ENDPOINT = "wss://openspeech.bytedance.com/api/v3/tts/unidirectional/stream"
|
|
|
|
_VOICE_CHOICES = [
|
|
"zh_female_cancan_mars_bigtts",
|
|
"zh_female_shuangkuaisisi_moon_bigtts",
|
|
"zh_male_wennuanahu_moon_bigtts",
|
|
"zh_male_sunwukong_moon_bigtts",
|
|
]
|
|
|
|
_FORMAT_CHOICES = ["mp3", "wav", "pcm"]
|
|
|
|
|
|
def synthesize_tts(
|
|
appid: str,
|
|
token: str,
|
|
text: str,
|
|
speaker: str = "zh_male_wennuanahu_moon_bigtts",
|
|
fmt: str = "mp3",
|
|
speech_rate: int = 0,
|
|
output_path: str = "output.mp3",
|
|
) -> str:
|
|
"""Synchronous TTS synthesis via Volcengine WebSocket API.
|
|
|
|
Returns absolute path to the output audio file.
|
|
"""
|
|
try:
|
|
import websockets
|
|
except ImportError as e:
|
|
raise RuntimeError(f"Missing dependency: {e}. Run: pip install websockets") from e
|
|
|
|
headers = {
|
|
"X-Api-App-Id": appid,
|
|
"X-Api-Access-Key": token,
|
|
"X-Api-Resource-Id": _RESOURCE_ID,
|
|
"X-Api-Connect-Id": str(uuid.uuid4()),
|
|
}
|
|
|
|
async def _run() -> bytearray:
|
|
ws = await websockets.connect(
|
|
_ENDPOINT,
|
|
additional_headers=headers,
|
|
max_size=10 * 1024 * 1024,
|
|
open_timeout=30,
|
|
)
|
|
|
|
request = {
|
|
"user": {"uid": str(uuid.uuid4())},
|
|
"req_params": {
|
|
"speaker": speaker,
|
|
"audio_params": {
|
|
"format": fmt,
|
|
"sample_rate": 24000,
|
|
"speech_rate": speech_rate,
|
|
},
|
|
"text": text,
|
|
},
|
|
}
|
|
|
|
await full_client_request(ws, json.dumps(request).encode())
|
|
|
|
audio_data = bytearray()
|
|
while True:
|
|
msg = await receive_message(ws)
|
|
if msg.type == MsgType.FullServerResponse and msg.event == EventType.SessionFinished:
|
|
break
|
|
elif msg.type == MsgType.AudioOnlyServer:
|
|
audio_data.extend(msg.payload)
|
|
elif msg.type == MsgType.Error:
|
|
await ws.close()
|
|
raise RuntimeError(
|
|
f"TTS service error: {msg.payload.decode('utf-8', errors='replace')}"
|
|
)
|
|
|
|
await ws.close()
|
|
return audio_data
|
|
|
|
audio_data = asyncio.run(_run())
|
|
if not audio_data:
|
|
raise RuntimeError("No audio data received from TTS service")
|
|
|
|
out = Path(output_path)
|
|
out.write_bytes(audio_data)
|
|
return str(out.resolve())
|