Files
xiaozhi-esp32-server/main/manager-api-fastapi/app/integrations/voice_clone.py
T

97 lines
3.3 KiB
Python

from __future__ import annotations
import base64
import json
from dataclasses import dataclass
from typing import Any
import httpx
@dataclass(slots=True)
class VoiceCloneProviderError(Exception):
code: int
message: str
def __str__(self) -> str:
return self.message
class VoiceCloneIntegration:
ENDPOINT = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload"
def __init__(
self,
*,
timeout_seconds: float,
client: httpx.AsyncClient | None = None,
endpoint: str | None = None,
):
self.timeout_seconds = timeout_seconds
self.client = client
self.endpoint = endpoint or self.ENDPOINT
async def train_huoshan(
self,
*,
appid: str,
access_token: str,
voice: bytes,
speaker_id: str,
) -> str:
request_body: dict[str, Any] = {
"appid": appid,
"audios": [
{
"audio_bytes": base64.b64encode(voice).decode("ascii"),
"audio_format": "wav",
}
],
"source": 2,
"language": 0,
"model_type": 1,
"speaker_id": speaker_id,
}
owns_client = self.client is None
client = self.client or httpx.AsyncClient()
try:
response = await client.post(
self.endpoint,
content=json.dumps(request_body, ensure_ascii=False, separators=(",", ":")).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer;{access_token}",
"Resource-Id": "seed-icl-1.0",
},
timeout=self.timeout_seconds,
)
try:
payload = response.json()
except (json.JSONDecodeError, ValueError) as exc:
raise VoiceCloneProviderError(10157, str(exc)) from exc
except httpx.HTTPError as exc:
raise VoiceCloneProviderError(10157, str(exc)) from exc
finally:
if owns_client:
await client.aclose()
if not isinstance(payload, dict):
raise VoiceCloneProviderError(10156, "响应格式错误,缺少BaseResp字段")
base_response = payload.get("BaseResp")
if isinstance(base_response, dict):
raw_status = base_response.get("StatusCode")
try:
status_code = int(raw_status) if raw_status is not None else None
except (TypeError, ValueError):
status_code = None
returned_speaker = payload.get("speaker_id")
if status_code == 0 and isinstance(returned_speaker, str) and returned_speaker.strip():
return returned_speaker
status_message = base_response.get("StatusMessage")
message = str(status_message) if status_message not in (None, "") else "训练失败"
raise VoiceCloneProviderError(500, message)
payload_message = payload.get("message")
if payload_message not in (None, ""):
raise VoiceCloneProviderError(500, str(payload_message))
raise VoiceCloneProviderError(10156, "响应格式错误,缺少BaseResp字段")