Browse Source

fix(tts+asr): parallel TTS with ordered enqueue + filter single-char ASR filler

- TTS: parallel tasks, enqueue in order to maintain sentence sequence
- ASR: filter results that are only punctuation/filler words (嗯啊哦)
- Simplify: _tts returns np.ndarray directly, no streaming
wenhongquan 3 weeks ago
parent
commit
0b3134495e
1 changed files with 29 additions and 29 deletions
  1. 29 29
      asr_agent/conversation_worker.py

+ 29 - 29
asr_agent/conversation_worker.py

@@ -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(