Spaces:
Runtime error
Runtime error
J.B-Lin commited on
Commit ·
0598330
1
Parent(s): ffe2c9f
Fix Gradio duplex audio playback clearing
Browse files- api/go_server.py +4 -2
- api/voice_helper.py +1 -0
- research/task_manager/pregopal_gradio_duplex_task.json +6 -6
- ui/app_builder.py +18 -8
api/go_server.py
CHANGED
|
@@ -195,7 +195,7 @@ def _merge_wavs_to_base64(wav_dir: str, wav_files: list) -> str:
|
|
| 195 |
return ""
|
| 196 |
|
| 197 |
|
| 198 |
-
def _wait_for_tts_files(tts_wav_dir: str, timeout_s: float =
|
| 199 |
"""Wait briefly because llama.cpp-omni may return before TTS wav files finish."""
|
| 200 |
deadline = time.time() + timeout_s
|
| 201 |
last_files = []
|
|
@@ -217,7 +217,7 @@ def _wait_for_tts_files(tts_wav_dir: str, timeout_s: float = 15.0) -> list:
|
|
| 217 |
last_files = wav_files
|
| 218 |
if os.path.exists(os.path.join(tts_wav_dir, "generation_done.flag")):
|
| 219 |
return wav_files
|
| 220 |
-
if stable_seen >=
|
| 221 |
return wav_files
|
| 222 |
time.sleep(0.25)
|
| 223 |
return last_files
|
|
@@ -385,6 +385,7 @@ async def voice_chat(req: VoiceChatRequest):
|
|
| 385 |
wav_files = _wait_for_tts_files(tts_wav_dir)
|
| 386 |
if wav_files:
|
| 387 |
tts_audio_base64 = _merge_wavs_to_base64(tts_wav_dir, wav_files)
|
|
|
|
| 388 |
else:
|
| 389 |
logger.warning(f"TTS wav 未在超时内生成: {tts_wav_dir}")
|
| 390 |
|
|
@@ -396,6 +397,7 @@ async def voice_chat(req: VoiceChatRequest):
|
|
| 396 |
"text": text_output,
|
| 397 |
"audio_base64": tts_audio_base64,
|
| 398 |
"audio_sample_rate": 24000, # TTS 默认采样率
|
|
|
|
| 399 |
"timing": {
|
| 400 |
"audio_prefill_ms": 0,
|
| 401 |
"decode_ms": 0,
|
|
|
|
| 195 |
return ""
|
| 196 |
|
| 197 |
|
| 198 |
+
def _wait_for_tts_files(tts_wav_dir: str, timeout_s: float = 45.0) -> list:
|
| 199 |
"""Wait briefly because llama.cpp-omni may return before TTS wav files finish."""
|
| 200 |
deadline = time.time() + timeout_s
|
| 201 |
last_files = []
|
|
|
|
| 217 |
last_files = wav_files
|
| 218 |
if os.path.exists(os.path.join(tts_wav_dir, "generation_done.flag")):
|
| 219 |
return wav_files
|
| 220 |
+
if stable_seen >= 12:
|
| 221 |
return wav_files
|
| 222 |
time.sleep(0.25)
|
| 223 |
return last_files
|
|
|
|
| 385 |
wav_files = _wait_for_tts_files(tts_wav_dir)
|
| 386 |
if wav_files:
|
| 387 |
tts_audio_base64 = _merge_wavs_to_base64(tts_wav_dir, wav_files)
|
| 388 |
+
logger.info(f"TTS wav 合并完成: {len(wav_files)} 个片段")
|
| 389 |
else:
|
| 390 |
logger.warning(f"TTS wav 未在超时内生成: {tts_wav_dir}")
|
| 391 |
|
|
|
|
| 397 |
"text": text_output,
|
| 398 |
"audio_base64": tts_audio_base64,
|
| 399 |
"audio_sample_rate": 24000, # TTS 默认采样率
|
| 400 |
+
"audio_files": len(wav_files),
|
| 401 |
"timing": {
|
| 402 |
"audio_prefill_ms": 0,
|
| 403 |
"decode_ms": 0,
|
api/voice_helper.py
CHANGED
|
@@ -102,6 +102,7 @@ def chat_voice(audio_path: str) -> dict:
|
|
| 102 |
"text": data.get("text", ""),
|
| 103 |
"audio_base64": data.get("audio_base64", ""),
|
| 104 |
"round": data.get("round", 0),
|
|
|
|
| 105 |
}
|
| 106 |
else:
|
| 107 |
# fallback: 直接用文本对话(避免服务中断)
|
|
|
|
| 102 |
"text": data.get("text", ""),
|
| 103 |
"audio_base64": data.get("audio_base64", ""),
|
| 104 |
"round": data.get("round", 0),
|
| 105 |
+
"audio_files": data.get("audio_files", 0),
|
| 106 |
}
|
| 107 |
else:
|
| 108 |
# fallback: 直接用文本对话(避免服务中断)
|
research/task_manager/pregopal_gradio_duplex_task.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
| 4 |
{
|
| 5 |
"stepId": "1",
|
| 6 |
"description": "Main Task",
|
| 7 |
-
"status": "
|
| 8 |
-
"resultNote": "",
|
| 9 |
"subSteps": [
|
| 10 |
{
|
| 11 |
"stepId": "1.1",
|
|
@@ -28,8 +28,8 @@
|
|
| 28 |
{
|
| 29 |
"stepId": "1.3",
|
| 30 |
"description": "Verify Gradio microphone streaming VAD loop",
|
| 31 |
-
"status": "
|
| 32 |
-
"resultNote": "
|
| 33 |
"subSteps": [],
|
| 34 |
"workDir": "C:\\Users\\Andre\\codes\\LJB\\hackthon\\for_codex\\PregoPal\\research\\task_manager\\workspace\\1\\3",
|
| 35 |
"instructions": ""
|
|
@@ -37,8 +37,8 @@
|
|
| 37 |
{
|
| 38 |
"stepId": "1.4",
|
| 39 |
"description": "Apply minimal demo UI or logging fixes only if needed",
|
| 40 |
-
"status": "
|
| 41 |
-
"resultNote": "",
|
| 42 |
"subSteps": [],
|
| 43 |
"workDir": "C:\\Users\\Andre\\codes\\LJB\\hackthon\\for_codex\\PregoPal\\research\\task_manager\\workspace\\1\\4",
|
| 44 |
"instructions": ""
|
|
|
|
| 4 |
{
|
| 5 |
"stepId": "1",
|
| 6 |
"description": "Main Task",
|
| 7 |
+
"status": "completed",
|
| 8 |
+
"resultNote": "Auto-completed: all sub-steps done.",
|
| 9 |
"subSteps": [
|
| 10 |
{
|
| 11 |
"stepId": "1.1",
|
|
|
|
| 28 |
{
|
| 29 |
"stepId": "1.3",
|
| 30 |
"description": "Verify Gradio microphone streaming VAD loop",
|
| 31 |
+
"status": "completed",
|
| 32 |
+
"resultNote": "Function-level Gradio stream path passed: real WAV chunks triggered backend, produced temp wav, and later silence frames now return gr.update() instead of clearing audio.",
|
| 33 |
"subSteps": [],
|
| 34 |
"workDir": "C:\\Users\\Andre\\codes\\LJB\\hackthon\\for_codex\\PregoPal\\research\\task_manager\\workspace\\1\\3",
|
| 35 |
"instructions": ""
|
|
|
|
| 37 |
{
|
| 38 |
"stepId": "1.4",
|
| 39 |
"description": "Apply minimal demo UI or logging fixes only if needed",
|
| 40 |
+
"status": "completed",
|
| 41 |
+
"resultNote": "Restarted Gradio PID 64656; HTTP 7889 returns 200. PregoAPI remains healthy on 8090.",
|
| 42 |
"subSteps": [],
|
| 43 |
"workDir": "C:\\Users\\Andre\\codes\\LJB\\hackthon\\for_codex\\PregoPal\\research\\task_manager\\workspace\\1\\4",
|
| 44 |
"instructions": ""
|
ui/app_builder.py
CHANGED
|
@@ -81,6 +81,11 @@ def _reset_duplex_buffers():
|
|
| 81 |
_SILENT_SECONDS = 0.0
|
| 82 |
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def toggle_duplex(*args):
|
| 85 |
"""点按切换全双工对话状态"""
|
| 86 |
global _DUPLEX_ACTIVE
|
|
@@ -94,7 +99,7 @@ def handle_stream_chunk(audio_chunk, chat_history):
|
|
| 94 |
"""处理 streaming 音频块: VAD -> 缓冲 -> 静音后调后端"""
|
| 95 |
global _DUPLEX_ACTIVE, _AUDIO_BUF, _AUDIO_SR, _SPEECH_SECONDS, _SILENT_SECONDS, _LAST_CHUNK_LOG
|
| 96 |
if not _DUPLEX_ACTIVE:
|
| 97 |
-
return chat_history, _thinking_html("✅ 已就绪"),
|
| 98 |
if not chat_history:
|
| 99 |
chat_history = []
|
| 100 |
|
|
@@ -103,7 +108,7 @@ def handle_stream_chunk(audio_chunk, chat_history):
|
|
| 103 |
if audio_chunk is not None and isinstance(audio_chunk, tuple) and len(audio_chunk) == 2:
|
| 104 |
sr, arr = audio_chunk
|
| 105 |
if arr is None:
|
| 106 |
-
return chat_history, _thinking_html("🎤 聆听中..."),
|
| 107 |
if arr.dtype.kind == 'i':
|
| 108 |
audio_np = arr.astype(np.float32) / 32768.0
|
| 109 |
else:
|
|
@@ -111,7 +116,7 @@ def handle_stream_chunk(audio_chunk, chat_history):
|
|
| 111 |
if len(audio_np.shape) > 1:
|
| 112 |
audio_np = audio_np.mean(axis=1)
|
| 113 |
if audio_np is None or len(audio_np) == 0:
|
| 114 |
-
return chat_history, _thinking_html("🎤 聆听中..."),
|
| 115 |
|
| 116 |
_AUDIO_SR = int(sr or _TARGET_SR)
|
| 117 |
chunk_seconds = len(audio_np) / max(float(_AUDIO_SR), 1.0)
|
|
@@ -136,7 +141,7 @@ def handle_stream_chunk(audio_chunk, chat_history):
|
|
| 136 |
)
|
| 137 |
_reset_duplex_buffers()
|
| 138 |
return _call_duplex_backend(full, chat_history, sr=buffered_sr)
|
| 139 |
-
return chat_history, _thinking_html("🎤 聆听中..."),
|
| 140 |
|
| 141 |
_SPEECH_SECONDS += chunk_seconds
|
| 142 |
_SILENT_SECONDS = 0.0
|
|
@@ -148,7 +153,7 @@ def handle_stream_chunk(audio_chunk, chat_history):
|
|
| 148 |
print(f"[PregoPal Duplex] max utterance reached dur={total_seconds:.2f}s", flush=True)
|
| 149 |
_reset_duplex_buffers()
|
| 150 |
return _call_duplex_backend(full, chat_history, sr=buffered_sr)
|
| 151 |
-
return chat_history, _thinking_html("🔊 正在听..."),
|
| 152 |
|
| 153 |
|
| 154 |
def _call_duplex_backend(audio_np, chat_history, sr=_TARGET_SR):
|
|
@@ -172,6 +177,11 @@ def _call_duplex_backend(audio_np, chat_history, sr=_TARGET_SR):
|
|
| 172 |
try: os.remove(path)
|
| 173 |
except: pass
|
| 174 |
if result.get("success"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
ai_text = result.get("text", "").strip()
|
| 176 |
if not ai_text or len(ai_text) < 2:
|
| 177 |
sp = "你是PregoPal孕期营养健康顾问。请用中文简短回答。"
|
|
@@ -195,12 +205,12 @@ def _call_duplex_backend(audio_np, chat_history, sr=_TARGET_SR):
|
|
| 195 |
with open(out_path, "wb") as f:
|
| 196 |
f.write(base64.b64decode(result["audio_base64"]))
|
| 197 |
return (chat_history, _thinking_html("🎤 聆听中..."), out_path)
|
| 198 |
-
return chat_history, _thinking_html("🎤 聆听中..."),
|
| 199 |
else:
|
| 200 |
-
return chat_history, _thinking_html("🎤 聆听中..."),
|
| 201 |
except Exception as e:
|
| 202 |
traceback.print_exc()
|
| 203 |
-
return chat_history, _thinking_html(f"错误: {str(e)[:50]}"),
|
| 204 |
|
| 205 |
|
| 206 |
def _thinking_html(text: str) -> str:
|
|
|
|
| 81 |
_SILENT_SECONDS = 0.0
|
| 82 |
|
| 83 |
|
| 84 |
+
def _keep_audio():
|
| 85 |
+
"""Keep the current Gradio audio value instead of clearing playback."""
|
| 86 |
+
return gr.update()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
def toggle_duplex(*args):
|
| 90 |
"""点按切换全双工对话状态"""
|
| 91 |
global _DUPLEX_ACTIVE
|
|
|
|
| 99 |
"""处理 streaming 音频块: VAD -> 缓冲 -> 静音后调后端"""
|
| 100 |
global _DUPLEX_ACTIVE, _AUDIO_BUF, _AUDIO_SR, _SPEECH_SECONDS, _SILENT_SECONDS, _LAST_CHUNK_LOG
|
| 101 |
if not _DUPLEX_ACTIVE:
|
| 102 |
+
return chat_history, _thinking_html("✅ 已就绪"), _keep_audio()
|
| 103 |
if not chat_history:
|
| 104 |
chat_history = []
|
| 105 |
|
|
|
|
| 108 |
if audio_chunk is not None and isinstance(audio_chunk, tuple) and len(audio_chunk) == 2:
|
| 109 |
sr, arr = audio_chunk
|
| 110 |
if arr is None:
|
| 111 |
+
return chat_history, _thinking_html("🎤 聆听中..."), _keep_audio()
|
| 112 |
if arr.dtype.kind == 'i':
|
| 113 |
audio_np = arr.astype(np.float32) / 32768.0
|
| 114 |
else:
|
|
|
|
| 116 |
if len(audio_np.shape) > 1:
|
| 117 |
audio_np = audio_np.mean(axis=1)
|
| 118 |
if audio_np is None or len(audio_np) == 0:
|
| 119 |
+
return chat_history, _thinking_html("🎤 聆听中..."), _keep_audio()
|
| 120 |
|
| 121 |
_AUDIO_SR = int(sr or _TARGET_SR)
|
| 122 |
chunk_seconds = len(audio_np) / max(float(_AUDIO_SR), 1.0)
|
|
|
|
| 141 |
)
|
| 142 |
_reset_duplex_buffers()
|
| 143 |
return _call_duplex_backend(full, chat_history, sr=buffered_sr)
|
| 144 |
+
return chat_history, _thinking_html("🎤 聆听中..."), _keep_audio()
|
| 145 |
|
| 146 |
_SPEECH_SECONDS += chunk_seconds
|
| 147 |
_SILENT_SECONDS = 0.0
|
|
|
|
| 153 |
print(f"[PregoPal Duplex] max utterance reached dur={total_seconds:.2f}s", flush=True)
|
| 154 |
_reset_duplex_buffers()
|
| 155 |
return _call_duplex_backend(full, chat_history, sr=buffered_sr)
|
| 156 |
+
return chat_history, _thinking_html("🔊 正在听..."), _keep_audio()
|
| 157 |
|
| 158 |
|
| 159 |
def _call_duplex_backend(audio_np, chat_history, sr=_TARGET_SR):
|
|
|
|
| 177 |
try: os.remove(path)
|
| 178 |
except: pass
|
| 179 |
if result.get("success"):
|
| 180 |
+
print(
|
| 181 |
+
f"[PregoPal Duplex] backend success round={result.get('round')} "
|
| 182 |
+
f"audio_files={result.get('audio_files')} audio_b64_len={len(result.get('audio_base64') or '')}",
|
| 183 |
+
flush=True,
|
| 184 |
+
)
|
| 185 |
ai_text = result.get("text", "").strip()
|
| 186 |
if not ai_text or len(ai_text) < 2:
|
| 187 |
sp = "你是PregoPal孕期营养健康顾问。请用中文简短回答。"
|
|
|
|
| 205 |
with open(out_path, "wb") as f:
|
| 206 |
f.write(base64.b64decode(result["audio_base64"]))
|
| 207 |
return (chat_history, _thinking_html("🎤 聆听中..."), out_path)
|
| 208 |
+
return chat_history, _thinking_html("🎤 聆听中..."), _keep_audio()
|
| 209 |
else:
|
| 210 |
+
return chat_history, _thinking_html("🎤 聆听中..."), _keep_audio()
|
| 211 |
except Exception as e:
|
| 212 |
traceback.print_exc()
|
| 213 |
+
return chat_history, _thinking_html(f"错误: {str(e)[:50]}"), _keep_audio()
|
| 214 |
|
| 215 |
|
| 216 |
def _thinking_html(text: str) -> str:
|