Debdeep30 commited on
Commit
a375b99
·
verified ·
1 Parent(s): 6b18cd3

fix: replace streaming VAD with record→send voice chat; remove upload/share buttons from Chatbot and Image; fix portrait note

Browse files
Files changed (1) hide show
  1. app.py +61 -94
app.py CHANGED
@@ -64,7 +64,7 @@ AVATAR_IMAGES = {
64
  for tag in ("smile", "nod", "concerned", "gentle", "laugh")
65
  }
66
 
67
- # VAD tuning (live streaming)
68
  SILENCE_THRESHOLD = 0.015
69
  SILENCE_CHUNKS = 6
70
  MIN_SPEECH_CHUNKS = 3
@@ -171,96 +171,64 @@ def text_chat(message: str, history: list[dict], profile_id: str):
171
 
172
 
173
  # ---------------------------------------------------------------------------
174
- # Gradio handlers — Live Voice (streaming VAD)
175
  # ---------------------------------------------------------------------------
176
 
177
- _LIVE_STATE_DEFAULT: dict = {
178
- "buffer": [],
179
- "sr": 16000,
180
- "silent_chunks": 0,
181
- "is_speaking": False,
182
- "history": [],
183
- "profile_id": DEFAULT_PROFILE_ID,
184
- }
185
-
186
-
187
- def _rms(audio: np.ndarray) -> float:
188
- return float(np.sqrt(np.mean((audio.astype(np.float32) / 32768.0) ** 2)))
189
-
190
-
191
- def _save_wav(chunks: list[np.ndarray], sr: int) -> str:
192
- audio = np.concatenate(chunks)
193
- tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
194
- sf.write(tmp.name, audio, sr)
195
  tmp.close()
196
  return tmp.name
197
 
198
 
199
- def live_stream(chunk, state: dict, profile_id: str):
200
  """
201
- Called on every ~250 ms audio chunk.
202
- Returns: (new_state, video_path, audio_path, chatbot, status, avatar_img)
203
  """
204
- state = dict(state)
205
- state["profile_id"] = profile_id
206
- no_change = (state, None, None, state["history"], "🎙 Listening…", AVATAR_IMAGES["smile"])
207
 
208
- if chunk is None:
209
  return no_change
210
 
211
- sr, audio = chunk
212
- state["sr"] = int(sr)
213
- energy = _rms(audio)
214
- state["buffer"] = state["buffer"] + [audio]
215
-
216
- if energy > SILENCE_THRESHOLD:
217
- state["is_speaking"] = True
218
- state["silent_chunks"] = 0
219
- return state, None, None, state["history"], "🗣 Listening…", AVATAR_IMAGES["nod"]
220
-
221
- state["silent_chunks"] = state["silent_chunks"] + 1
222
-
223
- if not state["is_speaking"] or state["silent_chunks"] < SILENCE_CHUNKS:
224
- return state, None, None, state["history"], "🎙 Listening…", AVATAR_IMAGES["smile"]
225
-
226
- # ── End of utterance ────────────────────────────────────────────────────
227
- speech_chunks = state["buffer"][: -state["silent_chunks"]]
228
- state["buffer"] = []
229
- state["silent_chunks"] = 0
230
- state["is_speaking"] = False
231
-
232
- if len(speech_chunks) < MIN_SPEECH_CHUNKS:
233
- return state, None, None, state["history"], "🎙 Listening…", AVATAR_IMAGES["smile"]
234
-
235
  # STT
236
- wav_path = _save_wav(speech_chunks, state["sr"])
237
  user_text = ""
238
  try:
239
- user_text = transcribe(wav_path) if STT_URL else "[STT not configured]"
240
  except Exception as e:
241
  print(f"[STT] {e}")
242
  finally:
243
- try:
244
- os.unlink(wav_path)
245
- except OSError:
246
- pass
 
247
 
248
  if not user_text:
249
- return state, None, None, state["history"], "🎙 Listening…", AVATAR_IMAGES["smile"]
250
 
251
  # Scam check
252
  is_scam, deflection = check_and_deflect(user_text)
253
  if is_scam:
254
  audio_path, video_path = _tts_and_video(deflection, profile_id)
255
- new_history = state["history"] + [
256
  {"role": "user", "content": f"🎤 {user_text}"},
257
  {"role": "assistant", "content": deflection},
258
  ]
259
- state["history"] = new_history
260
- return state, video_path, audio_path, new_history, "🎙 Listening…", AVATAR_IMAGES["gentle"]
261
 
262
- # LLM + TTS + SadTalker
263
- messages = _build_messages(state["history"]) + [{"role": "user", "content": user_text}]
264
  response_text = ""
265
  audio_path = None
266
  video_path = None
@@ -275,18 +243,17 @@ def live_stream(chunk, state: dict, profile_id: str):
275
 
276
  facts = extract_facts_from_response(raw)
277
  if facts:
278
- save_session(PATIENT_ID, facts, "unknown", "unknown", "Live voice session")
279
  except Exception as e:
280
  print(f"[LLM/TTS] {e}")
281
- response_text = "I'm sorry, I had a little trouble just then. Could you say that again?"
282
  audio_path, video_path = _tts_and_video(response_text, profile_id)
283
 
284
- new_history = state["history"] + [
285
  {"role": "user", "content": f"🎤 {user_text}"},
286
  {"role": "assistant", "content": response_text},
287
  ]
288
- state["history"] = new_history
289
- return state, video_path, audio_path, new_history, "🎙 Listening…", avatar
290
 
291
 
292
  # ---------------------------------------------------------------------------
@@ -358,6 +325,8 @@ with gr.Blocks(title="Lumi — Voice Companion", theme=gr.themes.Soft(), css=CSS
358
  show_label=False,
359
  elem_id="avatar-img",
360
  interactive=False,
 
 
361
  width=200,
362
  height=200,
363
  visible=True,
@@ -382,7 +351,8 @@ with gr.Blocks(title="Lumi — Voice Companion", theme=gr.themes.Soft(), css=CSS
382
 
383
  # Tab 1 — Text Chat ──────────────────────────────────────────────────
384
  with gr.Tab("💬 Text Chat"):
385
- chatbot1 = gr.Chatbot(height=400, label="Conversation", type="messages")
 
386
  with gr.Row():
387
  msg1 = gr.Textbox(
388
  placeholder="Type a message to Lumi…", scale=7, container=False
@@ -408,36 +378,34 @@ with gr.Blocks(title="Lumi — Voice Companion", theme=gr.themes.Soft(), css=CSS
408
  [chatbot1, avatar_img],
409
  ).then(lambda: "", None, msg1)
410
 
411
- # Tab 2 — Live Voice Chat ────────────────────────────────────────────
412
- with gr.Tab("🎤 Live Voice Chat"):
413
  gr.Markdown(
414
- "**Click the microphone button once to start.** "
415
- "Speak naturally Lumi listens and responds when you pause. "
416
- "Click again to stop."
417
- )
418
- chatbot2 = gr.Chatbot(height=280, label="Conversation", type="messages")
419
-
420
- mic_live = gr.Audio(
421
- sources=["microphone"],
422
- streaming=True,
423
- type="numpy",
424
- label="🎙 Microphone",
425
- waveform_options={"show_recording_waveform": False},
426
  )
 
 
 
 
 
 
 
 
 
 
 
427
  audio_live_out = gr.Audio(label="Lumi's voice", autoplay=True, visible=True)
428
  video_live_out = gr.Video(
429
  label="", show_label=False, autoplay=True,
430
  elem_id="avatar-video", visible=False,
431
  )
432
- status_live = gr.Markdown("🎙 Listening…", elem_classes=["status-badge"])
433
-
434
- live_state = gr.State(dict(_LIVE_STATE_DEFAULT))
435
 
436
- mic_live.stream(
437
- live_stream,
438
- inputs=[mic_live, live_state, profile_state],
439
- outputs=[live_state, video_live_out, audio_live_out,
440
- chatbot2, status_live, avatar_img],
441
  )
442
 
443
  # When a video comes back: hide static avatar, show video
@@ -467,9 +435,8 @@ with gr.Blocks(title="Lumi — Voice Companion", theme=gr.themes.Soft(), css=CSS
467
  f"{get_profile(DEFAULT_PROFILE_ID)['description']}",
468
  )
469
  gr.Markdown(
470
- "> **Note:** Portrait images must be placed in `portraits/<id>.jpg` "
471
- "for the talking head animation to work. "
472
- "See `pipeline/profiles.py` for the full list of IDs.",
473
  visible=True,
474
  )
475
 
 
64
  for tag in ("smile", "nod", "concerned", "gentle", "laugh")
65
  }
66
 
67
+ # VAD tuning (streaming mode — kept for reference)
68
  SILENCE_THRESHOLD = 0.015
69
  SILENCE_CHUNKS = 6
70
  MIN_SPEECH_CHUNKS = 3
 
171
 
172
 
173
  # ---------------------------------------------------------------------------
174
+ # Gradio handlers — Voice Chat (record → submit)
175
  # ---------------------------------------------------------------------------
176
 
177
+ def _save_wav_np(audio_tuple) -> str | None:
178
+ """Save (sr, numpy_array) tuple to a temp WAV file. Handles float32 and int16."""
179
+ if audio_tuple is None:
180
+ return None
181
+ sr, audio = audio_tuple
182
+ tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
183
+ # Gradio 5.x may give float32 [-1,1] or int16 — normalise to float32 for soundfile
184
+ if audio.dtype == np.int16:
185
+ audio = audio.astype(np.float32) / 32768.0
186
+ elif audio.dtype != np.float32:
187
+ audio = audio.astype(np.float32)
188
+ sf.write(tmp.name, audio, int(sr))
 
 
 
 
 
 
189
  tmp.close()
190
  return tmp.name
191
 
192
 
193
+ def voice_submit(audio_tuple, history: list[dict], profile_id: str):
194
  """
195
+ Called when the user finishes recording and clicks Send (or stops recording).
196
+ Returns: (chatbot, audio_out, video_out, avatar_img, status)
197
  """
198
+ no_change = (history, None, None, AVATAR_IMAGES["smile"], "🎤 Record then click Send")
 
 
199
 
200
+ if audio_tuple is None:
201
  return no_change
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  # STT
204
+ wav_path = _save_wav_np(audio_tuple)
205
  user_text = ""
206
  try:
207
+ user_text = transcribe(wav_path) if STT_URL else "[STT not configured — set STT_URL secret]"
208
  except Exception as e:
209
  print(f"[STT] {e}")
210
  finally:
211
+ if wav_path:
212
+ try:
213
+ os.unlink(wav_path)
214
+ except OSError:
215
+ pass
216
 
217
  if not user_text:
218
+ return history, None, None, AVATAR_IMAGES["smile"], " Couldn't hear that — try again"
219
 
220
  # Scam check
221
  is_scam, deflection = check_and_deflect(user_text)
222
  if is_scam:
223
  audio_path, video_path = _tts_and_video(deflection, profile_id)
224
+ new_history = history + [
225
  {"role": "user", "content": f"🎤 {user_text}"},
226
  {"role": "assistant", "content": deflection},
227
  ]
228
+ return new_history, audio_path, video_path, AVATAR_IMAGES["gentle"], "🎤 Record then click Send"
 
229
 
230
+ # LLM + TTS + (optionally) SadTalker
231
+ messages = _build_messages(history) + [{"role": "user", "content": user_text}]
232
  response_text = ""
233
  audio_path = None
234
  video_path = None
 
243
 
244
  facts = extract_facts_from_response(raw)
245
  if facts:
246
+ save_session(PATIENT_ID, facts, "unknown", "unknown", "Voice session")
247
  except Exception as e:
248
  print(f"[LLM/TTS] {e}")
249
+ response_text = "I'm sorry, I had a little trouble. Could you try again?"
250
  audio_path, video_path = _tts_and_video(response_text, profile_id)
251
 
252
+ new_history = history + [
253
  {"role": "user", "content": f"🎤 {user_text}"},
254
  {"role": "assistant", "content": response_text},
255
  ]
256
+ return new_history, audio_path, video_path, avatar, "🎤 Record then click Send"
 
257
 
258
 
259
  # ---------------------------------------------------------------------------
 
325
  show_label=False,
326
  elem_id="avatar-img",
327
  interactive=False,
328
+ show_download_button=False,
329
+ show_fullscreen_button=False,
330
  width=200,
331
  height=200,
332
  visible=True,
 
351
 
352
  # Tab 1 — Text Chat ──────────────────────────────────────────────────
353
  with gr.Tab("💬 Text Chat"):
354
+ chatbot1 = gr.Chatbot(height=400, label="Conversation", type="messages",
355
+ show_copy_button=False, show_share_button=False)
356
  with gr.Row():
357
  msg1 = gr.Textbox(
358
  placeholder="Type a message to Lumi…", scale=7, container=False
 
378
  [chatbot1, avatar_img],
379
  ).then(lambda: "", None, msg1)
380
 
381
+ # Tab 2 — Voice Chat (record → Send) ──────────────────────────────────
382
+ with gr.Tab("🎤 Voice Chat"):
383
  gr.Markdown(
384
+ "**Record your message, then click Send.** "
385
+ "Lumi will transcribe, think, and speak back."
 
 
 
 
 
 
 
 
 
 
386
  )
387
+ chatbot2 = gr.Chatbot(height=280, label="Conversation", type="messages",
388
+ show_copy_button=False, show_share_button=False)
389
+
390
+ with gr.Row():
391
+ mic_input = gr.Audio(
392
+ sources=["microphone"],
393
+ type="numpy",
394
+ label="🎙 Record your message",
395
+ )
396
+ voice_send = gr.Button("🎤 Send", variant="primary", scale=0)
397
+
398
  audio_live_out = gr.Audio(label="Lumi's voice", autoplay=True, visible=True)
399
  video_live_out = gr.Video(
400
  label="", show_label=False, autoplay=True,
401
  elem_id="avatar-video", visible=False,
402
  )
403
+ status_live = gr.Markdown("🎤 Record then click Send", elem_classes=["status-badge"])
 
 
404
 
405
+ voice_send.click(
406
+ voice_submit,
407
+ inputs=[mic_input, chatbot2, profile_state],
408
+ outputs=[chatbot2, audio_live_out, video_live_out, avatar_img, status_live],
 
409
  )
410
 
411
  # When a video comes back: hide static avatar, show video
 
435
  f"{get_profile(DEFAULT_PROFILE_ID)['description']}",
436
  )
437
  gr.Markdown(
438
+ "> **Tip:** The selected profile's voice and portrait apply to both "
439
+ "text chat and voice chat. Switch anytime — it takes effect on the next message.",
 
440
  visible=True,
441
  )
442