feat(voice): rework TTS/ASR stack and unify tool/skill config schema

This commit is contained in:
zhayujie
2026-05-21 16:00:54 +08:00
parent 2b90f377e6
commit b8333e351c
31 changed files with 1551 additions and 335 deletions

View File

@@ -1,8 +1,7 @@
# encoding:utf-8
"""
MiniMax TTS voice service
"""
"""MiniMax TTS via /v1/t2a_v2 (SSE stream, hex-encoded mp3 chunks)."""
import datetime
import json
import random
import requests
@@ -12,24 +11,12 @@ from config import conf
from voice.voice import Voice
MINIMAX_TTS_VOICES = [
"English_Graceful_Lady",
"English_Insightful_Speaker",
"English_radiant_girl",
"English_Persuasive_Man",
"English_Lucky_Robot",
"English_expressive_narrator",
"Chinese_Warm_Woman",
"Chinese_Gentle_Man",
]
class MinimaxVoice(Voice):
def __init__(self):
self.api_key = conf().get("minimax_api_key")
self.api_base = conf().get("minimax_api_base") or "https://api.minimax.io"
# Strip trailing /v1 if present so we can always append /v1/t2a_v2
self.api_base = self.api_base.rstrip("/")
# Mainland endpoint matches `sk-api-0-...` keys; override via
# `minimax_api_base` for international (api.minimax.io) workspaces.
self.api_base = (conf().get("minimax_api_base") or "https://api.minimaxi.com").rstrip("/")
if self.api_base.endswith("/v1"):
self.api_base = self.api_base[:-3]
@@ -68,12 +55,14 @@ class MinimaxVoice(Voice):
response = requests.post(url, headers=headers, json=payload, stream=True, timeout=60)
response.raise_for_status()
# Parse SSE stream and collect hex-encoded audio chunks
# MiniMax returns HTTP 200 even on errors; capture base_resp for diagnostics.
audio_chunks = []
buffer = ""
last_base_resp = None
event_count = 0
for raw in response.iter_lines():
if not raw:
continue
event_count += 1
line = raw.decode("utf-8") if isinstance(raw, bytes) else raw
if not line.startswith("data:"):
continue
@@ -81,16 +70,31 @@ class MinimaxVoice(Voice):
if not json_str or json_str == "[DONE]":
continue
try:
import json
event_data = json.loads(json_str)
audio_hex = event_data.get("data", {}).get("audio")
if audio_hex:
audio_chunks.append(bytes.fromhex(audio_hex))
except Exception:
continue
base_resp = event_data.get("base_resp") or {}
if base_resp:
last_base_resp = base_resp
audio_hex = (event_data.get("data") or {}).get("audio")
if audio_hex:
try:
audio_chunks.append(bytes.fromhex(audio_hex))
except Exception as e:
logger.warning(f"[MINIMAX] skip bad audio hex chunk: {e}")
if not audio_chunks:
logger.error("[MINIMAX] TTS returned no audio data")
ct = response.headers.get("Content-Type", "")
if last_base_resp and last_base_resp.get("status_code") not in (None, 0):
logger.error(
f"[MINIMAX] TTS failed: status_code={last_base_resp.get('status_code')}, "
f"status_msg={last_base_resp.get('status_msg')}, model={model}, voice_id={voice_id}"
)
else:
logger.error(
f"[MINIMAX] TTS returned no audio data, model={model}, voice_id={voice_id}, "
f"url={url}, http={response.status_code}, content_type={ct!r}, events={event_count}"
)
return Reply(ReplyType.ERROR, "语音合成失败,未获取到音频数据")
audio_data = b"".join(audio_chunks)