J.B-Lin commited on
Commit
0598330
·
1 Parent(s): ffe2c9f

Fix Gradio duplex audio playback clearing

Browse files
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 = 15.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,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 >= 2:
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": "running",
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": "running",
32
- "resultNote": "Backend verified; next validate Gradio microphone streaming and VAD loop.\nFunction-level VAD with synthetic tone triggered backend and appended chat text, but no audio path; next test uses real WAV chunks.\nAdded TTS merge cap to avoid abnormal huge wav_* outputs from non-speech/synthetic inputs during Gradio demo.",
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": "pending",
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("✅ 已就绪"), None
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("🎤 聆听中..."), None
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("🎤 聆听中..."), None
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("🎤 聆听中..."), None
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("🔊 正在听..."), None
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("🎤 聆听中..."), None
199
  else:
200
- return chat_history, _thinking_html("🎤 聆听中..."), None
201
  except Exception as e:
202
  traceback.print_exc()
203
- return chat_history, _thinking_html(f"错误: {str(e)[:50]}"), None
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: