Download app.py from build-small-hackathon/patient-virtuel-dentiste: direct link, hf CLI and curl.
- Browser
- Download file 6.68 kB
-
https://huggingface.co/spaces/build-small-hackathon/patient-virtuel-dentiste/resolve/b05542b6cc08dc3eaa013120cc6ab932aedd31af/app.py
- Command line
-
hf download hf://spaces/build-small-hackathon/patient-virtuel-dentiste@b05542b6cc08dc3eaa013120cc6ab932aedd31af/app.py
-
curl -L -o app.py https://huggingface.co/spaces/build-small-hackathon/patient-virtuel-dentiste/resolve/b05542b6cc08dc3eaa013120cc6ab932aedd31af/app.py
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) | |