import json import logging import datetime import spaces import gradio as gr from config import Config, VIPS_CATEGORIES from gdpr_filter import apply_gdpr_filter from models import WhisperASR, MistralClient from vips_classifier import classify_all, format_vips_for_display logger = logging.getLogger(__name__) asr_model = WhisperASR() mistral_client = None def format_vips_output(vips_dict) -> str: if isinstance(vips_dict, dict): return format_vips_for_display(vips_dict) if not vips_dict or not str(vips_dict).strip(): return "No output generated" return str(vips_dict).strip() def _get_clients(): global mistral_client if mistral_client is None: mistral_client = MistralClient() return mistral_client @spaces.GPU def run_pipeline_audio(audio): try: swedish_text = asr_model.transcribe(audio) if not swedish_text or not swedish_text.strip(): return ("Transkriptionen ar tom.", "", "", "", "", "") except Exception as e: logger.exception("ASR failed") return (f"[FEL ASR]: {e}", "", "", "", "", "") return _run_common(swedish_text) def run_pipeline_text(text_input): if not text_input or not text_input.strip(): return ("Ingen text angiven.", "", "", "", "", "") return _run_common(text_input.strip()) def _run_common(swedish_text): logger.info("Running GDPR filter...") anonymized_sv = apply_gdpr_filter(swedish_text) try: mc = _get_clients() except Exception as e: logger.exception("Client init failed") return (swedish_text, anonymized_sv, f"[FEL]: {e}", "", "", "") logger.info("Running Scaleway LLM...") try: all_results = classify_all(anonymized_sv, mc) logger.info("Scaleway classification complete") except Exception as e: logger.exception("LLM failed") err = f"[FEL LLM]: {e}" return (swedish_text, anonymized_sv, err, err, err, err) zero_text = format_vips_output(all_results["zero_shot"]) few_text = format_vips_output(all_results["few_shot"]) cot_text = format_vips_output(all_results["chain_of_thought"]) logger.info("Returning results to UI") return (swedish_text, anonymized_sv, zero_text, few_text, cot_text) def run_pipeline(audio, text_input): if audio is not None: return run_pipeline_audio(audio) return run_pipeline_text(text_input) PROMPT_CHOICES = ["Zero-shot", "Few-shot", "Chain-of-Thought"] NASA_SCALE_STR = ["1", "2", "3", "4", "5", "6", "7"] custom_css = """ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&display=swap'); * { font-family: 'DM Sans', sans-serif !important; } .gradio-container { background: #f0f4f8 !important; max-width: 1400px !important; margin: 0 auto; } .header-banner { background: linear-gradient(135deg, #1a5276 0%, #2980b9 100%); border-radius: 16px; padding: 32px 40px; margin-bottom: 8px; } .header-banner h1 { color: white !important; font-size: 2rem !important; font-weight: 600 !important; margin: 0 0 6px 0 !important; } .header-banner p { color: rgba(255,255,255,0.85) !important; font-size: 0.9rem !important; margin: 0 !important; } .section-card { background: white; border-radius: 14px; padding: 28px; margin-bottom: 16px; border: 1px solid #e8ecf0; } .section-label { font-size: 0.7rem !important; font-weight: 600 !important; letter-spacing: 0.12em !important; text-transform: uppercase !important; color: #2980b9 !important; margin-bottom: 16px !important; } .vips-col-zero { border-top: 3px solid #e74c3c !important; border-radius: 10px; padding: 16px; } .vips-col-few { border-top: 3px solid #2980b9 !important; border-radius: 10px; padding: 16px; } .vips-col-cot { border-top: 3px solid #27ae60 !important; border-radius: 10px; padding: 16px; } .gr-button-primary { background: linear-gradient(135deg, #1a5276, #2980b9) !important; border: none !important; border-radius: 10px !important; font-weight: 600 !important; } footer, .footer, .gradio-container > footer, a[href*="gradio.app"], a[href*="/?view=api"] { display: none !important; visibility: hidden !important; } """ with gr.Blocks(title="VoiceNote AI") as demo: gr.HTML(f"""

{Config.APP_NAME}

VIPS-journalgenerering | Whisper KBLab -> GDPR -> Scaleway

""") with gr.Group(elem_classes="section-card"): gr.Markdown("##### INMATNING", elem_classes="section-label") with gr.Row(equal_height=True): audio_input = gr.Audio(sources=["microphone", "upload"], type="filepath", label="Ljud", scale=1) text_input = gr.Textbox(label="Eller text", lines=5, scale=1, placeholder="Klistra in patientsamtalet har...") process_btn = gr.Button("Generera journalanteckning", variant="primary", size="lg") with gr.Group(elem_classes="section-card"): gr.Markdown("##### RESULTAT", elem_classes="section-label") with gr.Accordion("Pipeline-detaljer", open=False): with gr.Row(): transcription_out = gr.Textbox(label="Transkription (SV)", lines=5, interactive=True) anonymized_out = gr.Textbox(label="Anonymiserad (SV)", lines=5, interactive=False) gr.Markdown("##### VIPS - TRE PROMPTSTRATEGIER", elem_classes="section-label") with gr.Row(): with gr.Column(elem_classes="vips-col-zero"): gr.HTML("

Zero-shot

") zero_out = gr.Textbox(label="", lines=10, interactive=True) with gr.Column(elem_classes="vips-col-few"): gr.HTML("

Few-shot

") few_out = gr.Textbox(label="", lines=10, interactive=True) with gr.Column(elem_classes="vips-col-cot"): gr.HTML("

Chain-of-Thought

") cot_out = gr.Textbox(label="", lines=10, interactive=True) with gr.Group(elem_classes="section-card"): gr.Markdown("##### UTVARDERING", elem_classes="section-label") gr.Markdown("**Del 1 - Jamforelse av promptstrategier**") with gr.Row(): with gr.Column(): eval_complete = gr.Radio(choices=PROMPT_CHOICES, label="1. Mest fullstandig?") eval_hallucination = gr.Radio(choices=PROMPT_CHOICES, label="2. Undvek bast att hitta pa information?") with gr.Column(): eval_structure = gr.Radio(choices=PROMPT_CHOICES, label="3. Foljde VIPS-strukturen bast?") eval_clinical = gr.Radio(choices=PROMPT_CHOICES, label="4. Skulle valjas i klinisk praktik?") eval_comment = gr.Textbox(label="5. Kommentar", lines=3) gr.Markdown("---\n**Del 2 - NASA-TLX** | *1 = lag, 7 = hog*") with gr.Row(): with gr.Column(): tlx_mental = gr.Radio(choices=NASA_SCALE_STR, label="Mental") tlx_physical = gr.Radio(choices=NASA_SCALE_STR, label="Fysisk") tlx_temporal = gr.Radio(choices=NASA_SCALE_STR, label="Tidsbrist") with gr.Column(): tlx_performance = gr.Radio(choices=NASA_SCALE_STR, label="Prestation") tlx_effort = gr.Radio(choices=NASA_SCALE_STR, label="Anstrangning") tlx_frustration = gr.Radio(choices=NASA_SCALE_STR, label="Frustration") with gr.Row(): save_btn = gr.Button("Spara utvardering & ladda ner", variant="primary", scale=2) clear_btn = gr.Button("Rensa all data fran granssnittet", variant="secondary", scale=1) eval_status = gr.Textbox(label="", interactive=False, placeholder="Status visas har efter sparning...") download_file = gr.File( label="Komplett resultat + utvardering (JSON) - klicka for att ladda ner", interactive=False, ) process_btn.click( fn=run_pipeline, inputs=[audio_input, text_input], outputs=[transcription_out, anonymized_out, zero_out, few_out, cot_out], ) def on_save(c, h, s, cl, cm, m, p, t, pe, e, f, transcription, zero, few, cot): if not any([c, h, s, cl]): return "Fyll i minst ett svar i Del 1.", None filled = [int(x) for x in [m, p, t, pe, e, f] if x] entry = { "timestamp": datetime.datetime.now().isoformat(), "system": f"{Config.APP_NAME} v{Config.APP_VERSION}", "pipeline_results": { "transcription": transcription, "vips": { "zero_shot": zero, "few_shot": few, "chain_of_thought": cot, }, }, "prompt_evaluation": { "most_complete": c, "least_hallucination": h, "best_structure": s, "clinical_choice": cl, "comment": cm or "", }, "nasa_tlx": { "mental": m, "physical": p, "temporal": t, "performance": pe, "effort": e, "frustration": f, "total_avg": round(sum(filled)/len(filled), 2) if filled else None, }, } timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"/tmp/voicenote_utvardering_{timestamp}.json" with open(filename, "w", encoding="utf-8") as fh: json.dump(entry, fh, ensure_ascii=False, indent=2) return "Utvardering sparad! Fil klar for nedladdning nedan.", filename save_btn.click( fn=on_save, inputs=[eval_complete, eval_hallucination, eval_structure, eval_clinical, eval_comment, tlx_mental, tlx_physical, tlx_temporal, tlx_performance, tlx_effort, tlx_frustration, transcription_out, zero_out, few_out, cot_out], outputs=[eval_status, download_file], ) def clear_all(): return ( None, "", "", "", "", "", "", None, None, None, None, "", None, None, None, None, None, None, "All data rensad fran granssnittet.", None, ) clear_btn.click( fn=clear_all, inputs=[], outputs=[ audio_input, text_input, transcription_out, anonymized_out, zero_out, few_out, cot_out, eval_complete, eval_hallucination, eval_structure, eval_clinical, eval_comment, tlx_mental, tlx_physical, tlx_temporal, tlx_performance, tlx_effort, tlx_frustration, eval_status, download_file, ], ) if __name__ == "__main__": demo.launch(css=custom_css)