1- from fastapi import APIRouter , Depends
1+ import os
2+
3+ import requests
4+ from fastapi import APIRouter , Depends , File , UploadFile
25from sqlmodel import Session
36from api .deps import get_db
4- from api .schemas .forms import FormFill , FormFillResponse
7+ from api .schemas .forms import (
8+ FormFill ,
9+ FormFillResponse ,
10+ ModelsResponse ,
11+ TranscriptionResponse ,
12+ )
513from api .db .repositories import create_form , get_template
614from api .db .models import FormSubmission
715from api .errors .base import AppError
@@ -23,9 +31,77 @@ def fill_form(form: FormFill, db: Session = Depends(get_db)):
2331 user_input = form .input_text ,
2432 fields = fetched_template .fields ,
2533 pdf_form_path = fetched_template .pdf_path ,
34+ model = form .model ,
35+ )
36+
37+ # `model` is a runtime override, not a column — keep it out of the DB row.
38+ submission = FormSubmission (
39+ ** form .model_dump (exclude = {"model" }), output_pdf_path = path
2640 )
27-
28- submission = FormSubmission (** form .model_dump (), output_pdf_path = path )
2941 return create_form (db , submission )
3042 except Exception as e :
3143 raise AppError (str (e ), status_code = 500 )
44+
45+
46+ @router .get ("/models" , response_model = ModelsResponse )
47+ def list_models ():
48+ """List the Whisper-independent extraction models available in the local
49+ Ollama instance, plus the configured default. Used by the Fill Form UI's
50+ model picker. Falls back to just the default if Ollama is unreachable."""
51+ default_model = os .getenv ("OLLAMA_MODEL" , "qwen2.5:1.5b" )
52+ ollama_host = os .getenv ("OLLAMA_HOST" , "http://localhost:11434" ).rstrip ("/" )
53+
54+ models : list [str ] = []
55+ try :
56+ response = requests .get (f"{ ollama_host } /api/tags" , timeout = 10 )
57+ response .raise_for_status ()
58+ models = [m ["name" ] for m in response .json ().get ("models" , []) if m .get ("name" )]
59+ except requests .exceptions .RequestException :
60+ models = []
61+
62+ # Always surface the configured default, even if Ollama hasn't pulled it yet.
63+ if default_model not in models :
64+ models .insert (0 , default_model )
65+
66+ return ModelsResponse (models = models , default = default_model )
67+
68+
69+ @router .post ("/transcribe" , response_model = TranscriptionResponse )
70+ def transcribe (audio : UploadFile = File (...)):
71+ """Forward recorded audio to the local Whisper ASR sidecar and return text.
72+
73+ Mirrors the Ollama wiring: WHISPER_HOST points at the whisper service
74+ (http://whisper:9000 inside Docker, http://localhost:9000 otherwise). The
75+ audio is streamed straight through to the local STT service and never
76+ persisted — no PII leaves the machine.
77+ """
78+ whisper_host = os .getenv ("WHISPER_HOST" , "http://localhost:9000" ).rstrip ("/" )
79+ whisper_url = f"{ whisper_host } /asr"
80+
81+ files = {
82+ "audio_file" : (
83+ audio .filename or "audio.wav" ,
84+ audio .file .read (),
85+ audio .content_type or "audio/wav" ,
86+ )
87+ }
88+ params = {"task" : "transcribe" , "output" : "json" , "encode" : "true" }
89+
90+ try :
91+ response = requests .post (whisper_url , params = params , files = files , timeout = 120 )
92+ response .raise_for_status ()
93+ except requests .exceptions .ConnectionError :
94+ raise AppError (
95+ f"Could not connect to the speech-to-text service at { whisper_url } . "
96+ "Please ensure the whisper service is running." ,
97+ status_code = 503 ,
98+ )
99+ except requests .exceptions .RequestException as e :
100+ raise AppError (f"Transcription failed: { e } " , status_code = 502 )
101+
102+ try :
103+ text = (response .json ().get ("text" ) or "" ).strip ()
104+ except ValueError :
105+ text = response .text .strip ()
106+
107+ return TranscriptionResponse (text = text )
0 commit comments