carlosduplar
build-small-hackathon: initial Gradio + Modal app scaffold
b05542b
Raw History Blame
6.68 kB
import re
import gradio as gr
from prompts import SYSTEM_PROMPT, PHASE_SWITCH_REMINDER
from parse_feedback import parse_feedback, render_feedback_table, strip_markdown
from stt_engine import transcribe
from llm_engine import chat as llm_chat
from tts_engine import synthesize
TERMINATE_RE = re.compile(r"(fin\s+de\s+(la\s+)?séance|session\s+terminée)", re.IGNORECASE)
# 7 outputs: chatbot, audio_output, state, feedback_intro, feedback_table, feedback_panel, status
def _idle_feedback():
return "", [], gr.update(open=False)
def _show_feedback(state, clean):
entries = parse_feedback(clean)
table = render_feedback_table(entries) if entries else []
intro = clean
if "Disse:" in intro:
intro = intro.split("Disse:")[0].strip()
return intro, table, gr.update(open=True)
def _chat_val(state):
return state["messages"]
def _make_audio(audio_bytes):
return audio_bytes if audio_bytes else None
def process_turn(audio_path, state):
state = dict(state)
if not audio_path:
yield _chat_val(state), None, state, *_idle_feedback(), ""
return
# 1. STT
yield _chat_val(state), None, state, *_idle_feedback(), "🎙 Transcription…"
user_text = transcribe(audio_path)
if not user_text or len(user_text.strip()) < 2:
yield _chat_val(state), None, state, *_idle_feedback(), "⛔ Parlez plus fort ou plus longtemps."
return
state["messages"].append({"role": "user", "content": user_text.strip()})
if TERMINATE_RE.search(user_text):
yield from _end_session(state)
return
# 2. LLM
yield _chat_val(state), None, state, *_idle_feedback(), "🧠 Réflexion…"
response = llm_chat(state["messages"])
if not response:
yield _chat_val(state), None, state, *_idle_feedback(), "⛔ Erreur du modèle. Réessayez."
return
clean = strip_markdown(response)
state["messages"].append({"role": "assistant", "content": clean})
# 3. TTS
yield _chat_val(state), None, state, *_idle_feedback(), "🔊 Synthèse vocale…"
audio_bytes = synthesize(clean)
yield _chat_val(state), _make_audio(audio_bytes), state, *_idle_feedback(), ""
def _end_session(state):
state["messages"].append({"role": "user", "content": PHASE_SWITCH_REMINDER})
yield _chat_val(state), None, state, *_idle_feedback(), "📝 Génération du récapitulatif…"
response = llm_chat(state["messages"])
if not response:
yield _chat_val(state), None, state, *_idle_feedback(), "⛔ Erreur lors de la génération du bilan."
return
clean = strip_markdown(response)
state["messages"].append({"role": "assistant", "content": clean})
state["phase"] = 2
intro, table, accordion = _show_feedback(state, clean)
audio_bytes = synthesize(intro)
yield _chat_val(state), _make_audio(audio_bytes), state, intro, table, accordion, ""
def end_session_click(state):
state = dict(state)
yield from _end_session(state)
def reset_session():
state = {"messages": [], "phase": 1, "turn_count": 0}
state["messages"].append({"role": "system", "content": SYSTEM_PROMPT})
return [], None, state, *_idle_feedback(), ""
# ---- Init state ----
initial_messages = [{"role": "system", "content": SYSTEM_PROMPT}]
initial_state = {"messages": list(initial_messages), "phase": 1, "turn_count": 0}
# ---- Gradio UI ----
custom_css = open("style.css", encoding="utf-8").read()
with gr.Blocks(
css=custom_css,
title="Patient Virtuel · Hygiéniste Pro",
theme=gr.themes.Soft(primary_hue="orange"),
) as demo:
gr.HTML('<div class="atmosphere"></div>')
gr.Markdown(
'<h1 class="app-title" style="text-align:center; font-weight:400; '
'font-family:Cormorant Garamond,serif; color:white; margin-bottom:0; '
'font-size:28px; letter-spacing:0.02em;">'
"Patient Virtuel · Hygiéniste Pro</h1>"
)
state = gr.State(initial_state)
with gr.Row():
with gr.Column(scale=1, min_width=280):
audio_input = gr.Audio(
sources=["microphone"],
type="filepath",
show_label=False,
show_download_button=False,
waveform_options={"waveform_color": "#ff4e00", "show_controls": False},
)
gr.Markdown(
'<p style="font-size:13px; color:#888; text-align:center; '
'margin-top:4px;">Appuyez pour parler, relâchez pour envoyer</p>'
)
with gr.Row():
btn_end = gr.Button("🟠 Terminer la séance", variant="stop", scale=2)
btn_clear = gr.Button("🗑 Nouvelle", variant="secondary", scale=1)
status = gr.Markdown("", elem_id="status-bar")
with gr.Column(scale=2):
chatbot = gr.Chatbot(
value=list(initial_messages),
type="messages",
label="Conversation",
height=480,
avatar_images=(None, "🤖"),
show_copy_button=False,
sanitize_html=True,
render_markdown=False,
)
audio_output = gr.Audio(
label="Réponse audio",
autoplay=True,
show_download_button=False,
interactive=False,
waveform_options={"waveform_color": "#ff4e00", "show_controls": False},
)
with gr.Row():
feedback_panel = gr.Accordion(
label="📋 Récapitulatif de la séance",
open=False,
)
with feedback_panel:
feedback_intro = gr.Markdown("")
feedback_table = gr.Dataframe(
headers=["Disse", "Correction", "Explication"],
datatype=["str", "str", "str"],
wrap=True,
interactive=False,
label="Erreurs relevées",
show_label=False,
)
gr.Markdown(
'<p style="font-size:11px; color:#555; text-align:center; margin-top:16px;">'
"License CC-BY-NC 4.0 (Voxtral TTS) — démonstration non-commerciale. "
"Modèle: Qwen/Qwen3.6-27B, STT: faster-whisper.</p>"
)
# ---- Event wiring ----
outputs = [chatbot, audio_output, state, feedback_intro, feedback_table, feedback_panel, status]
audio_input.change(fn=process_turn, inputs=[audio_input, state], outputs=outputs)
btn_end.click(fn=end_session_click, inputs=[state], outputs=outputs)
btn_clear.click(fn=reset_session, inputs=[], outputs=outputs)
# ---- Launch ----
if __name__ == "__main__":
demo.launch(show_api=False)