Skip to content

Commit c525c9b

Browse files
authored
Merge branch 'main' into fix/startup-and-tests
2 parents a10663d + 0682f31 commit c525c9b

17 files changed

Lines changed: 857 additions & 152 deletions

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ venv
88
node_modules/
99
frontend/dist/
1010
frontend/out/
11+
src/inputs/
1112

1213
# macOS
1314
.DS_Store
@@ -22,4 +23,7 @@ src/inputs/*.pdf
2223
.codex/
2324

2425
# Electron build artifacts
25-
frontend/release/
26+
frontend/release/
27+
28+
# Local Claude Code instructions
29+
CLAUDE.md

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ authors:
1212
- family-names: "Sans Domingo"
1313
given-names: "Jan"
1414
title: "FireForm"
15-
version: 1.0.0
15+
version: 1.1.0
1616
url: "https://github.com/juanalvv/FireForm"
1717
abstract: "FireForm is a Digital Public Good (DPG) designed to solve administrative overhead for first responders by automating PDF form filling using AI."
1818
keywords:

Makefile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
.PHONY: help build up down logs shell exec pull-model test clean fireform logs-app logs-ollama logs-frontend super-clean
22

3+
# The extraction model pulled into Ollama and used by src/llm.py. Override with
4+
# `make pull-model OLLAMA_MODEL=...`. A 1.5B model keeps per-field fills fast.
5+
OLLAMA_MODEL ?= qwen2.5:1.5b
6+
37
help:
48
@printf '%s\n' \
59
' ______ ______ ' \
@@ -21,13 +25,13 @@ help:
2125
@echo "make logs-ollama - View Ollama container logs"
2226
@echo "make shell - Open Python shell in app container"
2327
@echo "make exec - Execute Python script in container"
24-
@echo "make pull-model - Pull Mistral model into Ollama"
28+
@echo "make pull-model - Pull the extraction model ($(OLLAMA_MODEL)) into Ollama"
2529
@echo "make test - Run tests"
2630
@echo "make clean - Remove containers"
2731
@echo "make super-clean - [CAUTION] Use carefully. Cleans up ALL stopped containers, networks, build cache..."
2832

2933
# Fix #382 — pull-model is now part of the main setup flow
30-
# Mistral is pulled automatically before you need it
34+
# The extraction model is pulled automatically before you need it
3135
fireform: build up pull-model
3236
@echo ""
3337
@echo "✅ FireForm is ready!"
@@ -69,7 +73,7 @@ exec:
6973
docker compose exec app python3 src/main.py
7074

7175
pull-model:
72-
docker compose exec ollama ollama pull mistral
76+
docker compose exec ollama ollama pull $(OLLAMA_MODEL)
7377

7478
# Fix — correct test directory (was src/test/ which doesn't exist)
7579
test:

api/routes/forms.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
from fastapi import APIRouter, Depends
1+
import os
2+
3+
import requests
4+
from fastapi import APIRouter, Depends, File, UploadFile
25
from sqlmodel import Session
36
from 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+
)
513
from api.db.repositories import create_form, get_template
614
from api.db.models import FormSubmission
715
from 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)

api/schemas/forms.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
class FormFill(BaseModel):
44
template_id: int
55
input_text: str
6+
# Optional Ollama model override for this fill; falls back to OLLAMA_MODEL.
7+
# Not persisted (no DB column) — excluded before building FormSubmission.
8+
model: str | None = None
69

710
@field_validator("input_text")
811
def validate_input_text(cls, value):
@@ -18,4 +21,13 @@ class FormFillResponse(BaseModel):
1821
output_pdf_path: str
1922

2023
class Config:
21-
from_attributes = True
24+
from_attributes = True
25+
26+
27+
class TranscriptionResponse(BaseModel):
28+
text: str
29+
30+
31+
class ModelsResponse(BaseModel):
32+
models: list[str]
33+
default: str

docker-compose.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@ services:
1515
retries: 5
1616
start_period: 30s
1717

18+
whisper:
19+
# Multi-arch (arm64 + amd64) Whisper ASR service — runs natively on Apple
20+
# Silicon. Uses the faster-whisper (CTranslate2) engine and bundles ffmpeg,
21+
# so it accepts any audio the browser produces. Model is pulled from
22+
# Hugging Face on first request into the whisper_models volume.
23+
image: onerahmet/openai-whisper-asr-webservice:latest
24+
container_name: fireform-whisper
25+
environment:
26+
- ASR_ENGINE=faster_whisper
27+
- ASR_MODEL=small.en
28+
- ASR_MODEL_PATH=/data/whisper
29+
volumes:
30+
- whisper_models:/data/whisper
31+
ports:
32+
- "127.0.0.1:9000:9000"
33+
networks:
34+
- fireform-network
35+
healthcheck:
36+
test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9000/docs')\" || exit 1"]
37+
interval: 15s
38+
timeout: 5s
39+
retries: 5
40+
start_period: 60s
41+
1842
app:
1943
build:
2044
context: .
@@ -23,9 +47,14 @@ services:
2347
depends_on:
2448
ollama:
2549
condition: service_healthy
50+
whisper:
51+
condition: service_started
2652
command: /bin/sh -c "python3 -m api.db.init_db && python3 -m uvicorn api.main:app --host 0.0.0.0 --port 8000"
2753
volumes:
2854
- .:/app
55+
# Persist the SQLite DB (~/.fireform) across container rebuilds so created
56+
# templates aren't wiped each time the image is recreated.
57+
- fireform_db:/root/.fireform
2958
ports:
3059
- "8000:8000"
3160
environment:
@@ -35,6 +64,8 @@ services:
3564
- PYTHONPATH=/app
3665
- OLLAMA_HOST=http://ollama:11434
3766
- OLLAMA_TIMEOUT=300
67+
- OLLAMA_MODEL=qwen2.5:1.5b
68+
- WHISPER_HOST=http://whisper:9000
3869
networks:
3970
- fireform-network
4071

@@ -56,6 +87,10 @@ services:
5687
volumes:
5788
ollama_data:
5889
driver: local
90+
whisper_models:
91+
driver: local
92+
fireform_db:
93+
driver: local
5994

6095
networks:
6196
fireform-network:

0 commit comments

Comments
 (0)