Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
working-directory: .
shell: bash
run: |
pyinstaller --name api-backend --onefile api/main.py
pyinstaller --name api-backend --onefile app/main.py
mkdir -p frontend/bin
cp dist/api-backend* frontend/bin/

Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ src/inputs/*.pdf
frontend/release/

# Local Claude Code instructions
CLAUDE.md
CLAUDE.md

*temp/
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .

# All imports use api.*, src.* which require the root to be on the path
# All imports use the app.* package, which requires the root on the path
ENV PYTHONPATH=/app

# Expose FastAPI port
EXPOSE 8000

# Start the FastAPI server (not tail -f /dev/null which does nothing)
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,8 @@ shell:

# Start the FastAPI server inside the running container
run:
docker compose exec app uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
docker compose exec app uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

exec:
docker compose exec app python3 src/main.py

pull-model:
docker compose exec ollama ollama pull $(OLLAMA_MODEL)
Expand Down
20 changes: 0 additions & 20 deletions api/db/database.py

This file was deleted.

42 changes: 0 additions & 42 deletions api/main.py

This file was deleted.

8 changes: 8 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""FireForm backend application package."""

import os

# Force CPU before any service module imports torch / rfdetr. Prevents PyTorch
# from probing for NVIDIA drivers on Mac Silicon and inside Docker. Runs here so
# it is guaranteed to execute before `app.main` imports the service layer.
os.environ["CUDA_VISIBLE_DEVICES"] = ""
File renamed without changes.
2 changes: 1 addition & 1 deletion api/deps.py → app/api/deps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from api.db.database import get_session
from app.db.database import get_session

def get_db():
yield from get_session()
12 changes: 12 additions & 0 deletions app/api/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Aggregates every route module into a single API router.

Add new feature routers here; main.py only mounts this one router.
"""

from fastapi import APIRouter

from app.api.routes import forms, templates

api_router = APIRouter()
api_router.include_router(templates.router)
api_router.include_router(forms.router)
File renamed without changes.
24 changes: 11 additions & 13 deletions api/routes/forms.py → app/api/routes/forms.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import os

import requests
from fastapi import APIRouter, Depends, File, UploadFile
from sqlmodel import Session
from api.deps import get_db
from api.schemas.forms import (

from app.api.deps import get_db
from app.api.schemas.forms import (
FormFill,
FormFillResponse,
ModelsResponse,
TranscriptionResponse,
)
from api.db.repositories import create_form, get_template
from api.db.models import FormSubmission
from api.errors.base import AppError
from src.controller import Controller
from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, WHISPER_HOST
from app.core.errors.base import AppError
from app.db.repositories import create_form, get_template
from app.models import FormSubmission
from app.services.controller import Controller

router = APIRouter(prefix="/forms", tags=["forms"])

Expand Down Expand Up @@ -48,12 +48,11 @@ def list_models():
"""List the Whisper-independent extraction models available in the local
Ollama instance, plus the configured default. Used by the Fill Form UI's
model picker. Falls back to just the default if Ollama is unreachable."""
default_model = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b")
ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/")
default_model = OLLAMA_MODEL

models: list[str] = []
try:
response = requests.get(f"{ollama_host}/api/tags", timeout=10)
response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=10)
response.raise_for_status()
models = [m["name"] for m in response.json().get("models", []) if m.get("name")]
except requests.exceptions.RequestException:
Expand All @@ -75,8 +74,7 @@ def transcribe(audio: UploadFile = File(...)):
audio is streamed straight through to the local STT service and never
persisted — no PII leaves the machine.
"""
whisper_host = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/")
whisper_url = f"{whisper_host}/asr"
whisper_url = f"{WHISPER_HOST}/asr"

files = {
"audio_file": (
Expand Down
15 changes: 8 additions & 7 deletions api/routes/templates.py → app/api/routes/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
from fastapi.responses import FileResponse
from sqlmodel import Session
from api.deps import get_db
from api.schemas.templates import (

from app.api.deps import get_db
from app.api.schemas.templates import (
TemplateCreate,
TemplateResponse,
TemplateUploadResponse,
MakeFillableRequest,
MakeFillableResponse,
)
from api.db.repositories import create_template, list_templates
from api.db.models import Template
from src.controller import Controller
from app.core.config import BASE_DIR, DEFAULT_TEMPLATE_DIR
from app.db.repositories import create_template, list_templates
from app.models import Template
from app.services.controller import Controller

router = APIRouter(prefix="/templates", tags=["templates"])
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_TEMPLATE_DIR = "src/inputs"
PROJECT_ROOT = BASE_DIR


def _resolve_target_directory(directory: str) -> Path:
Expand Down
File renamed without changes.
File renamed without changes.
1 change: 0 additions & 1 deletion api/schemas/forms.py → app/api/schemas/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ class FormFill(BaseModel):
template_id: int
input_text: str
# Optional Ollama model override for this fill; falls back to OLLAMA_MODEL.
# Not persisted (no DB column) — excluded before building FormSubmission.
model: str | None = None

@field_validator("input_text")
Expand Down
File renamed without changes.
File renamed without changes.
47 changes: 47 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Central configuration.

Single source of truth for paths, the database URL, external service hosts and
CORS. Read environment once here so the rest of the app imports settings instead
of calling os.getenv() in scattered places.
"""

import os
from pathlib import Path

# Repo root. config.py lives at app/core/config.py -> parents[2] is the repo root.
BASE_DIR = Path(__file__).resolve().parents[2]

# --- App metadata ---------------------------------------------------------
APP_TITLE = "FireForm API"
APP_VERSION = "1.1.0"

# --- Runtime data paths ---------------------------------------------------
# Uploaded templates and generated PDFs. Project-relative paths the API echoes
# back to the client are resolved against BASE_DIR (the "inside the project"
# guard in the templates routes). Override the data dir with FIREFORM_DATA_DIR.
DATA_DIR = Path(os.getenv("FIREFORM_DATA_DIR", BASE_DIR / "data")).resolve()

# Directory new uploads land in, as a project-relative string (was "src/inputs"
# before the restructure). Override with FIREFORM_TEMPLATE_DIR.
DEFAULT_TEMPLATE_DIR = os.getenv("FIREFORM_TEMPLATE_DIR", "data/inputs")

# --- Database -------------------------------------------------------------
# Keep the SQLite file in the user's home so it survives container rebuilds.
_APP_HOME = Path(os.path.expanduser("~")) / ".fireform"
_APP_HOME.mkdir(parents=True, exist_ok=True)
DB_PATH = Path(os.getenv("FIREFORM_DB_PATH", _APP_HOME / "fireform.db"))
DATABASE_URL = f"sqlite:///{DB_PATH}"
DB_ECHO = os.getenv("FIREFORM_DB_ECHO", "true").lower() == "true"

# --- External services ----------------------------------------------------
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b")
WHISPER_HOST = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/")

# --- CORS -----------------------------------------------------------------
_DEFAULT_ORIGINS = "http://127.0.0.1:5173,http://localhost:5173"
ALLOWED_ORIGINS = [
origin.strip()
for origin in os.getenv("FRONTEND_ORIGINS", _DEFAULT_ORIGINS).split(",")
if origin.strip()
]
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion api/errors/handlers.py → app/core/errors/handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from api.errors.base import AppError
from app.core.errors.base import AppError

def register_exception_handlers(app):
@app.exception_handler(AppError)
Expand Down
17 changes: 17 additions & 0 deletions app/core/lifespan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Application lifespan: startup and shutdown hooks."""

import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.db.init_db import init_db

logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Initializing database...")
init_db()
yield
14 changes: 14 additions & 0 deletions app/core/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Logging setup. Call setup_logging() once at app startup."""

import logging


def setup_logging(level: str = "INFO") -> None:
logging.basicConfig(
level=level,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
)


def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
File renamed without changes.
14 changes: 14 additions & 0 deletions app/db/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from sqlmodel import Session, create_engine

from app.core.config import DATABASE_URL, DB_ECHO

engine = create_engine(
DATABASE_URL,
echo=DB_ECHO,
connect_args={"check_same_thread": False},
)


def get_session():
with Session(engine) as session:
yield session
Loading