|
|
@@ -182,10 +182,17 @@ async def _asr_transcribe(audio_data: np.ndarray, asr_engine=None) -> str:
|
|
|
async for chunk, is_done in _asr_mimo_stream(audio_data):
|
|
|
if chunk:
|
|
|
text += chunk
|
|
|
- return text.strip()
|
|
|
- loop = asyncio.get_running_loop()
|
|
|
- result = await loop.run_in_executor(None, asr_engine.transcribe_array, audio_data)
|
|
|
- return result.get("text", "").strip()
|
|
|
+ text = text.strip()
|
|
|
+ else:
|
|
|
+ loop = asyncio.get_running_loop()
|
|
|
+ result = await loop.run_in_executor(None, asr_engine.transcribe_array, audio_data)
|
|
|
+ text = result.get("text", "").strip()
|
|
|
+ # Filter single-char filler results (common ASR noise)
|
|
|
+ fillers = {"嗯", "啊", "哦", "呃", "咦", "噢", "唔", "哎"}
|
|
|
+ if text and len(text) <= 2 and all(c in "嗯啊哦呃咦噢唔哎?!。,、~·~" for c in text):
|
|
|
+ logger.info("ASR filtered filler: %s", text)
|
|
|
+ return ""
|
|
|
+ return text
|
|
|
|
|
|
|
|
|
async def _asr_mimo_stream(audio_data: np.ndarray):
|
|
|
@@ -240,11 +247,10 @@ async def _asr_mimo_stream(audio_data: np.ndarray):
|
|
|
# TTS
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
-async def _tts_stream(text: str, voice: str = "Chloe"):
|
|
|
- """Mimo TTS. Non-streaming to avoid chunk-boundary artifacts."""
|
|
|
+async def _tts(text: str, voice: str = "Chloe") -> np.ndarray | None:
|
|
|
+ """Mimo TTS. Returns int16 PCM at 24kHz mono, or None on failure."""
|
|
|
if not MIMO_KEY:
|
|
|
- yield np.array([], dtype=np.int16), 24000, True
|
|
|
- return
|
|
|
+ return None
|
|
|
if voice.startswith("data:"):
|
|
|
model = "mimo-v2.5-tts-voiceclone"
|
|
|
else:
|
|
|
@@ -270,12 +276,10 @@ async def _tts_stream(text: str, voice: str = "Chloe"):
|
|
|
result = await r.json()
|
|
|
b64 = result.get("choices", [{}])[0].get("message", {}).get("audio", {}).get("data", "")
|
|
|
if b64:
|
|
|
- pcm = np.frombuffer(base64.b64decode(b64), dtype=np.int16)
|
|
|
- if len(pcm) > 0:
|
|
|
- yield pcm, 24000, False
|
|
|
+ return np.frombuffer(base64.b64decode(b64), dtype=np.int16)
|
|
|
except Exception:
|
|
|
logger.exception("TTS failed")
|
|
|
- yield np.array([], dtype=np.int16), 24000, True
|
|
|
+ return None
|
|
|
|
|
|
|
|
|
def _clean_tts_text(text: str) -> str:
|
|
|
@@ -623,10 +627,19 @@ class Worker:
|
|
|
logger.exception("LLM stream failed")
|
|
|
reply_full = "抱歉,我暂时无法回答。"
|
|
|
|
|
|
- # Ordered TTS: generate and enqueue in sequence
|
|
|
- for s in tts_segments:
|
|
|
- logger.info("TTS seg(%d): %s", len(s), s[:40])
|
|
|
- await self._tts_enqueue(s, play_q, gen)
|
|
|
+ # Ordered TTS: generate concurrently but enqueue in order
|
|
|
+ if tts_segments:
|
|
|
+ tasks = [asyncio.create_task(_tts(s)) for s in tts_segments]
|
|
|
+ for i, task in enumerate(tasks):
|
|
|
+ try:
|
|
|
+ pcm = await asyncio.wait_for(task, timeout=15)
|
|
|
+ if pcm is not None and len(pcm) > 0:
|
|
|
+ logger.info("TTS seg(%d/%d): %s", i+1, len(tasks), tts_segments[i][:40])
|
|
|
+ await play_q.put((gen, pcm))
|
|
|
+ except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
+ break
|
|
|
+ except Exception:
|
|
|
+ logger.exception("TTS seg %d failed", i+1)
|
|
|
|
|
|
if reply_full:
|
|
|
await self._send({"type": "reply", "text": reply_full})
|
|
|
@@ -636,19 +649,6 @@ class Worker:
|
|
|
])
|
|
|
self.hist[:] = self.hist[-20:]
|
|
|
|
|
|
- async def _tts_enqueue(self, text: str, play_q, gen: int):
|
|
|
- """Generate TTS audio and enqueue for playback."""
|
|
|
- try:
|
|
|
- async for pcm, sr, is_final in _tts_stream(text):
|
|
|
- if len(pcm) > 0:
|
|
|
- await play_q.put((gen, pcm))
|
|
|
- if is_final:
|
|
|
- break
|
|
|
- except asyncio.CancelledError:
|
|
|
- pass
|
|
|
- except Exception:
|
|
|
- logger.exception("TTS failed")
|
|
|
-
|
|
|
async def _send(self, msg: dict):
|
|
|
try:
|
|
|
await self.room.local_participant.publish_data(
|