From 72b03d43f18170363fa031c82cfd721afc141d00 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 30 May 2026 23:32:48 +0530 Subject: [PATCH 01/85] refactor: restructure backend into layered app/ package --- .gitignore | 4 +- api/db/database.py | 20 -------- api/main.py | 42 ----------------- app/__init__.py | 8 ++++ {api => app/api}/__init__.py | 0 {api => app/api}/deps.py | 2 +- app/api/router.py | 12 +++++ {api => app/api}/routes/__init__.py | 0 {api => app/api}/routes/forms.py | 24 +++++----- {api => app/api}/routes/templates.py | 15 +++--- {api/db => app/api/schemas}/__init__.py | 0 {api => app/api}/schemas/common.py | 0 {api => app/api}/schemas/forms.py | 1 - {api => app/api}/schemas/templates.py | 0 {api/errors => app/core}/__init__.py | 0 app/core/config.py | 47 +++++++++++++++++++ {api/schemas => app/core/errors}/__init__.py | 0 {api => app/core}/errors/base.py | 0 {api => app/core}/errors/handlers.py | 2 +- app/core/lifespan.py | 17 +++++++ app/core/logging.py | 14 ++++++ {src => app/db}/__init__.py | 0 app/db/database.py | 14 ++++++ {api => app}/db/init_db.py | 34 +++++++++----- {api => app}/db/repositories.py | 2 +- app/main.py | 34 ++++++++++++++ app/models/__init__.py | 5 ++ {api/db => app/models}/models.py | 0 app/services/__init__.py | 0 {src => app/services}/controller.py | 2 +- {src => app/services}/file_manipulator.py | 4 +- {src => app/services}/filler.py | 2 +- {src => app/services}/inputs/input.txt | 0 {src => app/services}/llm.py | 7 +-- .../services}/outputs/test_output_1.json | 0 {src => app/services}/prompt.txt | 0 .../services}/test/example_template.json | 0 {src => app/services}/test/test_output_1.json | 0 examples/pipeline_demo.py | 27 +++++++++++ src/main.py | 42 ----------------- tests/conftest.py | 10 ++-- tests/test_api.py | 2 +- {src/test => tests}/test_model.py | 0 43 files changed, 238 insertions(+), 155 deletions(-) delete mode 100644 api/db/database.py delete mode 100644 api/main.py create mode 100644 app/__init__.py rename {api => app/api}/__init__.py (100%) rename {api => app/api}/deps.py (51%) create mode 100644 app/api/router.py rename {api => app/api}/routes/__init__.py (100%) rename {api => app/api}/routes/forms.py (85%) rename {api => app/api}/routes/templates.py (95%) rename {api/db => app/api/schemas}/__init__.py (100%) rename {api => app/api}/schemas/common.py (100%) rename {api => app/api}/schemas/forms.py (90%) rename {api => app/api}/schemas/templates.py (100%) rename {api/errors => app/core}/__init__.py (100%) create mode 100644 app/core/config.py rename {api/schemas => app/core/errors}/__init__.py (100%) rename {api => app/core}/errors/base.py (100%) rename {api => app/core}/errors/handlers.py (88%) create mode 100644 app/core/lifespan.py create mode 100644 app/core/logging.py rename {src => app/db}/__init__.py (100%) create mode 100644 app/db/database.py rename {api => app}/db/init_db.py (65%) rename {api => app}/db/repositories.py (93%) create mode 100644 app/main.py create mode 100644 app/models/__init__.py rename {api/db => app/models}/models.py (100%) create mode 100644 app/services/__init__.py rename {src => app/services}/controller.py (87%) rename {src => app/services}/file_manipulator.py (96%) rename {src => app/services}/filler.py (97%) rename {src => app/services}/inputs/input.txt (100%) rename {src => app/services}/llm.py (94%) rename {src => app/services}/outputs/test_output_1.json (100%) rename {src => app/services}/prompt.txt (100%) rename {src => app/services}/test/example_template.json (100%) rename {src => app/services}/test/test_output_1.json (100%) create mode 100644 examples/pipeline_demo.py delete mode 100644 src/main.py rename {src/test => tests}/test_model.py (100%) diff --git a/.gitignore b/.gitignore index b5a9a904..268a0ff2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ src/inputs/*.pdf frontend/release/ # Local Claude Code instructions -CLAUDE.md \ No newline at end of file +CLAUDE.md + +*temp/ \ No newline at end of file diff --git a/api/db/database.py b/api/db/database.py deleted file mode 100644 index 97e6e622..00000000 --- a/api/db/database.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -from sqlmodel import create_engine, Session - -# Define path to the database in the user's home directory -HOME_DIR = os.path.expanduser("~") -APP_DIR = os.path.join(HOME_DIR, ".fireform") -os.makedirs(APP_DIR, exist_ok=True) -DB_PATH = os.path.join(APP_DIR, "fireform.db") - -DATABASE_URL = f"sqlite:///{DB_PATH}" - -engine = create_engine( - DATABASE_URL, - echo=True, - connect_args={"check_same_thread": False}, -) - -def get_session(): - with Session(engine) as session: - yield session \ No newline at end of file diff --git a/api/main.py b/api/main.py deleted file mode 100644 index 0c30bc5e..00000000 --- a/api/main.py +++ /dev/null @@ -1,42 +0,0 @@ -from contextlib import asynccontextmanager -import os - -# Disable CUDA to prevent PyTorch from trying to find NVIDIA drivers on Mac Silicon / Docker -os.environ["CUDA_VISIBLE_DEVICES"] = "" - -from fastapi import FastAPI -from api.routes import templates, forms -from api.db.init_db import init_db -from api.errors.handlers import register_exception_handlers -from fastapi.middleware.cors import CORSMiddleware -from api.routes import forms, templates - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup: Initialize the database and seed it if necessary - print("Initializing database...") - init_db() - yield - # Shutdown logic goes here if needed - -app = FastAPI(lifespan=lifespan) - -register_exception_handlers(app) - -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() -] - -app.add_middleware( - CORSMiddleware, - allow_origins=allowed_origins, - allow_credentials=False, - allow_methods=["*"], - allow_headers=["*"], -) - -app.include_router(templates.router) -app.include_router(forms.router) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..de9379be --- /dev/null +++ b/app/__init__.py @@ -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"] = "" diff --git a/api/__init__.py b/app/api/__init__.py similarity index 100% rename from api/__init__.py rename to app/api/__init__.py diff --git a/api/deps.py b/app/api/deps.py similarity index 51% rename from api/deps.py rename to app/api/deps.py index c2a0b748..4aa9614b 100644 --- a/api/deps.py +++ b/app/api/deps.py @@ -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() \ No newline at end of file diff --git a/app/api/router.py b/app/api/router.py new file mode 100644 index 00000000..ba9c0b56 --- /dev/null +++ b/app/api/router.py @@ -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) diff --git a/api/routes/__init__.py b/app/api/routes/__init__.py similarity index 100% rename from api/routes/__init__.py rename to app/api/routes/__init__.py diff --git a/api/routes/forms.py b/app/api/routes/forms.py similarity index 85% rename from api/routes/forms.py rename to app/api/routes/forms.py index 74069eaf..7af5da62 100644 --- a/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -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"]) @@ -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: @@ -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": ( diff --git a/api/routes/templates.py b/app/api/routes/templates.py similarity index 95% rename from api/routes/templates.py rename to app/api/routes/templates.py index 64db2e05..cc98370d 100644 --- a/api/routes/templates.py +++ b/app/api/routes/templates.py @@ -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: diff --git a/api/db/__init__.py b/app/api/schemas/__init__.py similarity index 100% rename from api/db/__init__.py rename to app/api/schemas/__init__.py diff --git a/api/schemas/common.py b/app/api/schemas/common.py similarity index 100% rename from api/schemas/common.py rename to app/api/schemas/common.py diff --git a/api/schemas/forms.py b/app/api/schemas/forms.py similarity index 90% rename from api/schemas/forms.py rename to app/api/schemas/forms.py index ca996515..6a833c8d 100644 --- a/api/schemas/forms.py +++ b/app/api/schemas/forms.py @@ -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") diff --git a/api/schemas/templates.py b/app/api/schemas/templates.py similarity index 100% rename from api/schemas/templates.py rename to app/api/schemas/templates.py diff --git a/api/errors/__init__.py b/app/core/__init__.py similarity index 100% rename from api/errors/__init__.py rename to app/core/__init__.py diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 00000000..055bbadd --- /dev/null +++ b/app/core/config.py @@ -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() +] diff --git a/api/schemas/__init__.py b/app/core/errors/__init__.py similarity index 100% rename from api/schemas/__init__.py rename to app/core/errors/__init__.py diff --git a/api/errors/base.py b/app/core/errors/base.py similarity index 100% rename from api/errors/base.py rename to app/core/errors/base.py diff --git a/api/errors/handlers.py b/app/core/errors/handlers.py similarity index 88% rename from api/errors/handlers.py rename to app/core/errors/handlers.py index 903e744b..0285ddb1 100644 --- a/api/errors/handlers.py +++ b/app/core/errors/handlers.py @@ -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) diff --git a/app/core/lifespan.py b/app/core/lifespan.py new file mode 100644 index 00000000..7f842739 --- /dev/null +++ b/app/core/lifespan.py @@ -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 diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 00000000..48d28a8a --- /dev/null +++ b/app/core/logging.py @@ -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) diff --git a/src/__init__.py b/app/db/__init__.py similarity index 100% rename from src/__init__.py rename to app/db/__init__.py diff --git a/app/db/database.py b/app/db/database.py new file mode 100644 index 00000000..0437afd1 --- /dev/null +++ b/app/db/database.py @@ -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 diff --git a/api/db/init_db.py b/app/db/init_db.py similarity index 65% rename from api/db/init_db.py rename to app/db/init_db.py index ea77cb62..fc171471 100644 --- a/api/db/init_db.py +++ b/app/db/init_db.py @@ -1,9 +1,15 @@ -import json import datetime -from sqlmodel import SQLModel, Session, select -from api.db.database import engine -from api.db import models -from api.db.models import Template +import logging + +from sqlmodel import Session, SQLModel, select + +from app.core.config import DEFAULT_TEMPLATE_DIR +from app.db.database import engine + +from app.models import FormSubmission, Template # noqa: F401 + +logger = logging.getLogger(__name__) + def seed_db(): with Session(engine) as session: @@ -14,9 +20,9 @@ def seed_db(): except Exception: # Table might not exist yet if called at a weird time results = None - + if not results: - print("Seeding database with default template...") + logger.info("Seeding database with default template...") fields = { "Employee's name": "string", "Employee's job title": "string", @@ -24,24 +30,26 @@ def seed_db(): "Employee's phone number": "string", "Employee's email": "string", "Signature": "string", - "Date": "string" + "Date": "string", } - + # Using ID 2 as agreed to avoid any ID 1 corruption default_template = Template( id=2, name="Manual Test Template", fields=fields, - pdf_path="src/inputs/file_template_manual.pdf", - created_at=datetime.datetime.now() + pdf_path=f"{DEFAULT_TEMPLATE_DIR}/file_template_manual.pdf", + created_at=datetime.datetime.now(), ) session.add(default_template) session.commit() - print("Database seeded successfully.") + logger.info("Database seeded successfully.") + def init_db(): SQLModel.metadata.create_all(engine) seed_db() + if __name__ == "__main__": - init_db() \ No newline at end of file + init_db() diff --git a/api/db/repositories.py b/app/db/repositories.py similarity index 93% rename from api/db/repositories.py rename to app/db/repositories.py index a9ac3cfa..ebb80de6 100644 --- a/api/db/repositories.py +++ b/app/db/repositories.py @@ -1,5 +1,5 @@ from sqlmodel import Session, select -from api.db.models import Template, FormSubmission +from app.models import Template, FormSubmission # Templates def create_template(session: Session, template: Template) -> Template: diff --git a/app/main.py b/app/main.py new file mode 100644 index 00000000..3d3bdad8 --- /dev/null +++ b/app/main.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.router import api_router +from app.core import config +from app.core.errors.handlers import register_exception_handlers +from app.core.lifespan import lifespan +from app.core.logging import setup_logging + + +def create_app() -> FastAPI: + setup_logging() + + app = FastAPI( + title=config.APP_TITLE, + version=config.APP_VERSION, + lifespan=lifespan, + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=config.ALLOWED_ORIGINS, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], + ) + + register_exception_handlers(app) + app.include_router(api_router) + + return app + + +app = create_app() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 00000000..f46e17dc --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,5 @@ +"""ORM models. Import from here: `from app.models import Template`.""" + +from app.models.models import FormSubmission, Template + +__all__ = ["Template", "FormSubmission"] diff --git a/api/db/models.py b/app/models/models.py similarity index 100% rename from api/db/models.py rename to app/models/models.py diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/controller.py b/app/services/controller.py similarity index 87% rename from src/controller.py rename to app/services/controller.py index 0e19290a..49c10e48 100644 --- a/src/controller.py +++ b/app/services/controller.py @@ -1,4 +1,4 @@ -from src.file_manipulator import FileManipulator +from app.services.file_manipulator import FileManipulator class Controller: def __init__(self): diff --git a/src/file_manipulator.py b/app/services/file_manipulator.py similarity index 96% rename from src/file_manipulator.py rename to app/services/file_manipulator.py index 8d1f3a0a..357356e2 100644 --- a/src/file_manipulator.py +++ b/app/services/file_manipulator.py @@ -1,6 +1,6 @@ import os -from src.filler import Filler -from src.llm import LLM +from app.services.filler import Filler +from app.services.llm import LLM class FileManipulator: diff --git a/src/filler.py b/app/services/filler.py similarity index 97% rename from src/filler.py rename to app/services/filler.py index 7f738c24..aec97560 100644 --- a/src/filler.py +++ b/app/services/filler.py @@ -1,5 +1,5 @@ from pdfrw import PdfReader, PdfWriter -from src.llm import LLM +from app.services.llm import LLM from datetime import datetime diff --git a/src/inputs/input.txt b/app/services/inputs/input.txt similarity index 100% rename from src/inputs/input.txt rename to app/services/inputs/input.txt diff --git a/src/llm.py b/app/services/llm.py similarity index 94% rename from src/llm.py rename to app/services/llm.py index 053b8836..29983182 100644 --- a/src/llm.py +++ b/app/services/llm.py @@ -3,6 +3,8 @@ import requests from requests.exceptions import Timeout, RequestException +from app.core.config import OLLAMA_HOST, OLLAMA_MODEL + class LLM: def __init__(self, transcript_text: str=None, target_fields: list=None, json_dict: dict=None, model: str=None): @@ -31,9 +33,8 @@ def main_loop(self): total_fields = len(self._target_fields) for i, (field, field_type) in enumerate(self._target_fields.items(), 1): prompt = self.build_prompt(field, field_type if isinstance(field_type, str) else "string") - ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") - ollama_url = f"{ollama_host}/api/generate" - ollama_model = self._model or os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") + ollama_url = f"{OLLAMA_HOST}/api/generate" + ollama_model = self._model or OLLAMA_MODEL payload = { "model": ollama_model, diff --git a/src/outputs/test_output_1.json b/app/services/outputs/test_output_1.json similarity index 100% rename from src/outputs/test_output_1.json rename to app/services/outputs/test_output_1.json diff --git a/src/prompt.txt b/app/services/prompt.txt similarity index 100% rename from src/prompt.txt rename to app/services/prompt.txt diff --git a/src/test/example_template.json b/app/services/test/example_template.json similarity index 100% rename from src/test/example_template.json rename to app/services/test/example_template.json diff --git a/src/test/test_output_1.json b/app/services/test/test_output_1.json similarity index 100% rename from src/test/test_output_1.json rename to app/services/test/test_output_1.json diff --git a/examples/pipeline_demo.py b/examples/pipeline_demo.py new file mode 100644 index 00000000..dbad305c --- /dev/null +++ b/examples/pipeline_demo.py @@ -0,0 +1,27 @@ +"""Manual, end-to-end demo of the fill pipeline (not part of the package). + +Run from the repo root with a real PDF path. This is a developer convenience +for exercising the services layer without the API; it is not imported anywhere. +""" + +from commonforms import prepare_form +from pypdf import PdfReader + +from app.services.controller import Controller + +if __name__ == "__main__": + file = "./data/inputs/file.pdf" # update to a real input PDF + user_input = ( + "Hi. The employee's name is John Doe. His job title is managing director. " + "His department supervisor is Jane Doe. His phone number is 123456. " + "His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005" + ) + prepared_pdf = "temp_outfile.pdf" + prepare_form(file, prepared_pdf) + + reader = PdfReader(prepared_pdf) + fields = reader.get_fields() + num_fields = len(fields) if fields else 0 + + controller = Controller() + controller.fill_form(user_input, fields, file) diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 630d262a..00000000 --- a/src/main.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -os.environ["CUDA_VISIBLE_DEVICES"] = "" - -# Monkey patch rfdetr to force CPU usage on Mac Silicon / Docker -try: - import rfdetr.detr - original_ensure = rfdetr.detr._ensure_model_on_device - def patched_ensure(model_ctx): - model_ctx.device = "cpu" - original_ensure(model_ctx) - rfdetr.detr._ensure_model_on_device = patched_ensure -except ImportError: - pass - -from commonforms import prepare_form -from pypdf import PdfReader -from controller import Controller - -if __name__ == "__main__": - file = "./src/inputs/file.pdf" - user_input = "Hi. The employee's name is John Doe. His job title is managing director. His department supervisor is Jane Doe. His phone number is 123456. His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005" - fields = [ - "Employee's name", - "Employee's job title", - "Employee's department supervisor", - "Employee's phone number", - "Employee's email", - "Signature", - "Date", - ] - prepared_pdf = "temp_outfile.pdf" - prepare_form(file, prepared_pdf) - - reader = PdfReader(prepared_pdf) - fields = reader.get_fields() - if fields: - num_fields = len(fields) - else: - num_fields = 0 - - controller = Controller() - controller.fill_form(user_input, fields, file) diff --git a/tests/conftest.py b/tests/conftest.py index 5f1eb3fa..56dea60d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,9 +12,9 @@ from sqlalchemy.pool import StaticPool from sqlmodel import SQLModel, Session, create_engine -from api.main import app -from api.deps import get_db -from api.db.models import Template, FormSubmission # noqa: F401 — registers tables +from app.main import app +from app.api.deps import get_db +from app.models import Template, FormSubmission # noqa: F401 — registers tables # --------------------------------------------------------------------------- # In-memory database @@ -85,8 +85,8 @@ def pdf_upload(pdf_bytes): @pytest.fixture def mock_controller(): """Patch Controller so create_template / fill_form don't touch the FS or LLM.""" - with patch("api.routes.templates.Controller") as tpl_cls, \ - patch("api.routes.forms.Controller") as form_cls: + with patch("app.api.routes.templates.Controller") as tpl_cls, \ + patch("app.api.routes.forms.Controller") as form_cls: tpl_instance = MagicMock() tpl_instance.create_template.return_value = "src/inputs/test_template.pdf" tpl_cls.return_value = tpl_instance diff --git a/tests/test_api.py b/tests/test_api.py index 89a9b25a..66696b94 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,7 +7,7 @@ import pytest from sqlmodel import select -from api.db.models import Template, FormSubmission +from app.models import Template, FormSubmission # ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/test/test_model.py b/tests/test_model.py similarity index 100% rename from src/test/test_model.py rename to tests/test_model.py From f4ee6fe909c6a0c45b9e17f9938cdf6a611b169f Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 30 May 2026 23:34:39 +0530 Subject: [PATCH 02/85] Updated the change of code structure layer in docker, makefile and workflows --- .github/workflows/release.yml | 2 +- Dockerfile | 4 ++-- Makefile | 4 +--- docker-compose.yml | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6d1f835..9ddeaf3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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/ diff --git a/Dockerfile b/Dockerfile index 282eb251..b7b00cef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/Makefile b/Makefile index 026a721a..3f155338 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml index a9e8e6ed..45ad0341 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,7 @@ services: condition: service_healthy whisper: condition: service_started - command: /bin/sh -c "python3 -m api.db.init_db && python3 -m uvicorn api.main:app --host 0.0.0.0 --port 8000" + command: /bin/sh -c "python3 -m app.db.init_db && python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000" volumes: - .:/app # Persist the SQLite DB (~/.fireform) across container rebuilds so created From e260df9e86dc2d1502c8872830e8515a47cbf07d Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 2 Jun 2026 23:59:24 +0530 Subject: [PATCH 03/85] fix: lint target src/ -> app/ and remove stale electron release workflow --- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 59 ----------------------------------- 2 files changed, 1 insertion(+), 60 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 93d41fbb..1652a2b7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,4 +26,4 @@ jobs: - name: Run linter run: | - ruff check src/ --output-format=github \ No newline at end of file + ruff check app/ --output-format=github \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 9ddeaf3e..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release Electron App - -on: - push: - tags: ["v*"] - -jobs: - build: - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - defaults: - run: - working-directory: frontend - - permissions: - contents: write - - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install Python dependencies - working-directory: . - shell: bash - run: | - python -m pip install --upgrade pip - if [ "$RUNNER_OS" == "macOS" ]; then - pip install pyinstaller - pip install -r requirements.txt - else - pip install torch --index-url https://download.pytorch.org/whl/cpu - pip install pyinstaller - pip install -r requirements.txt - fi - - - name: Build Python backend with PyInstaller - working-directory: . - shell: bash - run: | - pyinstaller --name api-backend --onefile app/main.py - mkdir -p frontend/bin - cp dist/api-backend* frontend/bin/ - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - run: npm ci - - - name: Build & publish - run: npx electron-builder --publish always - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From fa7188f2d048f97fc30e2f8d41919887e5440d3a Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 3 Jun 2026 00:05:22 +0530 Subject: [PATCH 04/85] fix: explicit re-export of templates and forms in routes __init__. Fixes lint errors --- app/api/routes/__init__.py | 2 ++ app/api/schemas/common.py | 2 +- tests/test_api.py | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index e8fe8f45..2264aee0 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1 +1,3 @@ from . import templates, forms + +__all__ = ["templates", "forms"] diff --git a/app/api/schemas/common.py b/app/api/schemas/common.py index 578cd2c0..8d20a24b 100644 --- a/app/api/schemas/common.py +++ b/app/api/schemas/common.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Any, Optional +from typing import Any class SuccessResponse(BaseModel): success: bool = True diff --git a/tests/test_api.py b/tests/test_api.py index 66696b94..33b2c620 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,7 +4,6 @@ All heavy dependencies (LLM, commonforms, filesystem) are mocked via conftest. """ -import pytest from sqlmodel import select from app.models import Template, FormSubmission From 93a5b3ac1630e1cc66664ae9eee58886bd438e69 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 3 Jun 2026 00:16:29 +0530 Subject: [PATCH 05/85] chore: remove frontend directory, as it's migrated to fireform-frontend repo --- frontend/app.js | 1362 ------------ frontend/electron.js | 64 - frontend/index.html | 165 -- frontend/package-lock.json | 4025 ------------------------------------ frontend/package.json | 44 - frontend/preload.js | 3 - frontend/styles.css | 658 ------ 7 files changed, 6321 deletions(-) delete mode 100644 frontend/app.js delete mode 100644 frontend/electron.js delete mode 100644 frontend/index.html delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/preload.js delete mode 100644 frontend/styles.css diff --git a/frontend/app.js b/frontend/app.js deleted file mode 100644 index 4fd15ad5..00000000 --- a/frontend/app.js +++ /dev/null @@ -1,1362 +0,0 @@ -const STORAGE_TEMPLATES_KEY = "fireform.templates.v1"; -const STORAGE_LAST_OUTPUT_KEY = "fireform.lastOutputPath.v1"; -// Where uploaded template PDFs are copied. Fixed for now; longer term this -// should be user-configurable behind a Settings button (see note below). -const DEFAULT_TEMPLATE_DIRECTORY = "src/inputs"; -const API_BASE_URL = "http://127.0.0.1:8000"; - -// UI label <-> stored type-string mapping. The stored values stay backward -// compatible with the existing default "string" type. -const FIELD_TYPES = [ - { label: "Text", value: "string" }, - { label: "Long Text", value: "long_text" }, - { label: "Number", value: "number" }, - { label: "Date", value: "date" }, - { label: "Time", value: "time" }, - { label: "Email", value: "email" }, - { label: "Phone", value: "phone" }, - { label: "Signature", value: "signature" }, - { label: "Checkbox", value: "checkbox" }, - { label: "List", value: "list" }, -]; -const TYPE_VALUE_TO_LABEL = Object.fromEntries(FIELD_TYPES.map((t) => [t.value, t.label])); -const DEFAULT_FIELD_ROWS = [{ name: "", type: "string" }]; - -const elements = { - tabs: Array.from(document.querySelectorAll(".tab")), - panels: Array.from(document.querySelectorAll(".panel")), - templateForm: document.getElementById("templateForm"), - templateName: document.getElementById("templateName"), - templatePdfFile: document.getElementById("templatePdfFile"), - pdfDropZone: document.getElementById("pdfDropZone"), - selectedFileMeta: document.getElementById("selectedFileMeta"), - changePdfBtn: document.getElementById("changePdfBtn"), - makeFillableBtn: document.getElementById("makeFillableBtn"), - makeFillableHelpBtn: document.getElementById("makeFillableHelpBtn"), - makeFillableHelp: document.getElementById("makeFillableHelp"), - fieldsBuilder: document.getElementById("fieldsBuilder"), - fieldCountBadge: document.getElementById("fieldCountBadge"), - addFieldBtn: document.getElementById("addFieldBtn"), - templateFormMessage: document.getElementById("templateFormMessage"), - templateFormResponse: document.getElementById("templateFormResponse"), - fillForm: document.getElementById("fillForm"), - fillModel: document.getElementById("fillModel"), - fillTemplateTiles: document.getElementById("fillTemplateTiles"), - fillSelectionHint: document.getElementById("fillSelectionHint"), - fillSubmitBtn: document.getElementById("fillSubmitBtn"), - inputText: document.getElementById("inputText"), - sttControls: document.getElementById("sttControls"), - sttRecordBtn: document.getElementById("sttRecordBtn"), - sttPauseBtn: document.getElementById("sttPauseBtn"), - sttStopBtn: document.getElementById("sttStopBtn"), - sttStatus: document.getElementById("sttStatus"), - fillFormMessage: document.getElementById("fillFormMessage"), - fillFormResponse: document.getElementById("fillFormResponse"), - templatesEmpty: document.getElementById("templatesEmpty"), - templatesList: document.getElementById("templatesList"), - localPdfFile: document.getElementById("localPdfFile"), - serverPdfPath: document.getElementById("serverPdfPath"), - previewPathBtn: document.getElementById("previewPathBtn"), - previewStatus: document.getElementById("previewStatus"), - pdfFrame: document.getElementById("pdfFrame"), -}; - -let templates = loadTemplates(); -let activeObjectUrl = null; -let selectedTemplateFile = null; -// Field rows are scratch state for building one template — they start empty -// each session and are not persisted. -let fieldRows = DEFAULT_FIELD_ROWS.map((row) => ({ ...row })); -let dragSourceIndex = null; -let uploadedPath = null; -let uploadedFieldCount = null; -// Template ids currently selected in the Fill Form tab (multi-select). -let selectedFillIds = new Set(); - -// Speech-to-text recording state. The MediaRecorder captures compressed audio -// in the renderer; on stop we POST it straight to /forms/transcribe (the local -// Whisper service handles decoding). -let mediaRecorder = null; -let recordedChunks = []; -let recordingStream = null; - -waitForBackend().then(initialize); - -async function waitForBackend() { - const loadingScreen = document.getElementById("loadingScreen"); - let isReady = false; - - while (!isReady) { - try { - const response = await fetch(`${API_BASE_URL}/templates`); - if (response.ok) { - isReady = true; - } - } catch (e) { - // Ignore error and try again - } - - if (!isReady) { - await new Promise(r => setTimeout(r, 500)); - } - } - - if (loadingScreen) { - loadingScreen.classList.add("hidden"); - } -} - -async function initialize() { - bindEvents(); - renderFieldRows(); - renderTemplates(); - renderFillTemplates(); - restorePreviewState(); - updateSelectedFileMeta(); - loadModels(); - await refreshTemplatesFromApi(); -} - -function bindEvents() { - elements.tabs.forEach((tab) => { - tab.addEventListener("click", () => activateSection(tab.dataset.target)); - }); - - elements.templateForm.addEventListener("submit", handleTemplateSubmit); - elements.templatePdfFile.addEventListener("change", handleTemplateFileInput); - elements.pdfDropZone.addEventListener("click", () => elements.templatePdfFile.click()); - elements.pdfDropZone.addEventListener("keydown", handleDropZoneKeyDown); - elements.changePdfBtn.addEventListener("click", () => elements.templatePdfFile.click()); - elements.addFieldBtn.addEventListener("click", handleAddFieldClick); - elements.makeFillableBtn.addEventListener("click", handleMakeFillableClick); - elements.makeFillableHelpBtn.addEventListener("click", toggleMakeFillableHelp); - bindDropZoneDragEvents(); - elements.fillForm.addEventListener("submit", handleFillSubmit); - elements.fillTemplateTiles.addEventListener("click", handleTileClick); - elements.fillTemplateTiles.addEventListener("keydown", handleTileKeydown); - elements.sttRecordBtn.addEventListener("click", startRecording); - elements.sttPauseBtn.addEventListener("click", togglePauseRecording); - elements.sttStopBtn.addEventListener("click", stopRecording); - elements.templatesList.addEventListener("click", handleTemplateActionClick); - elements.localPdfFile.addEventListener("change", handleLocalFilePreview); - elements.previewPathBtn.addEventListener("click", () => - previewFromPath(elements.serverPdfPath.value, { switchToPreview: true }) - ); -} - -function activateSection(targetId) { - switchSection(targetId); -} - -async function refreshTemplatesFromApi() { - try { - const response = await fetch(`${API_BASE_URL}/templates`); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - if (Array.isArray(body)) { - templates = body.map((template) => ({ - id: template.id, - name: template.name || "", - pdf_path: template.pdf_path || "", - fields: template.fields || {}, - })); - saveTemplates(); - // Drop selections for templates that no longer exist. - const liveIds = new Set(templates.map((t) => Number(t.id))); - selectedFillIds.forEach((id) => { if (!liveIds.has(id)) selectedFillIds.delete(id); }); - renderTemplates(); - renderFillTemplates(); - } - } catch (error) { - setStatus( - elements.templateFormMessage, - `Could not refresh templates from API: ${error.message}`, - "error" - ); - } -} - -function bindDropZoneDragEvents() { - ["dragenter", "dragover"].forEach((eventName) => { - elements.pdfDropZone.addEventListener(eventName, (event) => { - event.preventDefault(); - event.stopPropagation(); - elements.pdfDropZone.classList.add("active"); - }); - }); - - ["dragleave", "dragend", "drop"].forEach((eventName) => { - elements.pdfDropZone.addEventListener(eventName, (event) => { - event.preventDefault(); - event.stopPropagation(); - elements.pdfDropZone.classList.remove("active"); - }); - }); - - elements.pdfDropZone.addEventListener("drop", (event) => { - const file = event.dataTransfer?.files?.[0]; - setSelectedTemplateFile(file); - }); -} - -function handleDropZoneKeyDown(event) { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - elements.templatePdfFile.click(); - } -} - -function handleTemplateFileInput(event) { - const file = event.target.files && event.target.files[0]; - setSelectedTemplateFile(file); -} - -function setSelectedTemplateFile(file) { - if (!file) { - return; - } - - if (!isPdfFile(file)) { - selectedTemplateFile = null; - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - setStatus(elements.templateFormMessage, "Please select a PDF file.", "error"); - updateSelectedFileMeta(); - return; - } - - selectedTemplateFile = file; - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - clearJson(elements.templateFormResponse); - setStatus(elements.templateFormMessage, ""); - updateSelectedFileMeta(); - // Eager upload so the user gets a live field-count comparison while building rows. - uploadSelectedFileSilently(); -} - -async function uploadSelectedFileSilently() { - if (!selectedTemplateFile) return; - const directory = DEFAULT_TEMPLATE_DIRECTORY; - - const fileAtUploadStart = selectedTemplateFile; - try { - const upload = await uploadTemplatePdf(fileAtUploadStart, directory); - // Guard against the user picking a different file mid-upload. - if (fileAtUploadStart !== selectedTemplateFile) return; - uploadedPath = upload.pdf_path; - uploadedFieldCount = - typeof upload.field_count === "number" ? upload.field_count : null; - maybeSeedFieldRows(upload.fields); - renderFieldCountBadge(); - } catch (_error) { - // Silent failure — the explicit Create / Make Fillable paths surface errors. - } -} - -// Auto-add a row per field the PDF already defines — same as clicking "+ Add -// Field" for each — filling in its description and type so the user can edit. -// If the list already has rows the user typed, warn before replacing them. -function maybeSeedFieldRows(fields) { - if (!Array.isArray(fields) || !fields.length) return; - syncFieldRowsFromDom(); - - if (fieldRows.some((row) => row.name.trim())) { - const replace = window.confirm( - `This PDF has ${fields.length} fillable field${fields.length === 1 ? "" : "s"}.\n\n` + - "Replace your current form fields with them? Your existing entries will be lost." - ); - if (!replace) { - setStatus(elements.templateFormMessage, "Kept your existing form fields.", "info"); - return; - } - } - - fieldRows = fields.map((f) => ({ - name: f.description || f.name || "", - type: normalizeFieldType(f.type), - })); - renderFieldRows(); - setStatus( - elements.templateFormMessage, - `Loaded ${fieldRows.length} field${fieldRows.length === 1 ? "" : "s"} from the PDF — edit the descriptions as needed.`, - "info" - ); -} - -function setMakeFillableButtonState() { - if (!elements.makeFillableBtn) return; - elements.makeFillableBtn.disabled = !selectedTemplateFile; - elements.makeFillableBtn.textContent = "Make this PDF fillable"; -} - -function renderFieldCountBadge() { - const badge = elements.fieldCountBadge; - if (!badge) return; - - if (!selectedTemplateFile || uploadedFieldCount === null) { - badge.classList.add("hidden"); - badge.classList.remove("match", "mismatch"); - badge.textContent = ""; - return; - } - - const expected = uploadedFieldCount; - const actual = fieldRows.length; - const noun = (n) => `${n} fillable field${n === 1 ? "" : "s"}`; - const rowNoun = (n) => `${n} row${n === 1 ? "" : "s"}`; - - badge.classList.remove("hidden", "match", "mismatch"); - if (expected === actual) { - badge.classList.add("match"); - badge.textContent = `PDF has ${noun(expected)} — your ${rowNoun(actual)} match.`; - } else { - badge.classList.add("mismatch"); - badge.textContent = `PDF has ${noun(expected)} — you have ${rowNoun(actual)}.`; - } -} - -function isPdfFile(file) { - const name = String(file?.name || "").toLowerCase(); - return name.endsWith(".pdf"); -} - -function updateSelectedFileMeta() { - // Once a file is chosen, swap the drop zone for a compact "change" control. - const hasFile = !!selectedTemplateFile; - elements.pdfDropZone.classList.toggle("hidden", hasFile); - elements.changePdfBtn.classList.toggle("hidden", !hasFile); - - if (!hasFile) { - elements.selectedFileMeta.textContent = "No PDF selected."; - return; - } - - const destinationPath = `${DEFAULT_TEMPLATE_DIRECTORY}/${selectedTemplateFile.name}`; - - elements.selectedFileMeta.textContent = `Selected: ${selectedTemplateFile.name} (${formatBytes( - selectedTemplateFile.size - )}) - destination: ${destinationPath}`; -} - -function formatBytes(bytes) { - if (!Number.isFinite(bytes) || bytes <= 0) { - return "0 B"; - } - - const units = ["B", "KB", "MB", "GB"]; - let value = bytes; - let unitIndex = 0; - - while (value >= 1024 && unitIndex < units.length - 1) { - value /= 1024; - unitIndex += 1; - } - - return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; -} - -function switchSection(targetId) { - elements.panels.forEach((panel) => { - panel.classList.toggle("hidden", panel.id !== targetId); - }); - elements.tabs.forEach((tab) => { - tab.classList.toggle("active", tab.dataset.target === targetId); - }); -} - -function setStatus(target, message, type = "info") { - target.textContent = message || ""; - target.className = "status"; - if (type) { - target.classList.add(type); - } -} - -function showJson(preElement, payload) { - preElement.textContent = JSON.stringify(payload, null, 2); - preElement.classList.remove("hidden"); -} - -function clearJson(preElement) { - preElement.textContent = ""; - preElement.classList.add("hidden"); -} - -function collectFieldRows() { - syncFieldRowsFromDom(); - - if (fieldRows.length === 0) { - return { error: "Add at least one field before creating the template." }; - } - - const dict = {}; - const seen = new Set(); - for (const row of fieldRows) { - const name = row.name.trim(); - if (!name) { - return { error: "Every field needs a name." }; - } - const key = name.toLowerCase(); - if (seen.has(key)) { - return { error: `Field names must be unique ("${name}" appears more than once).` }; - } - seen.add(key); - dict[name] = row.type || "string"; - } - return { value: dict }; -} - -async function handleTemplateSubmit(event) { - event.preventDefault(); - clearJson(elements.templateFormResponse); - setStatus(elements.templateFormMessage, ""); - - const name = elements.templateName.value.trim(); - const templateDirectory = DEFAULT_TEMPLATE_DIRECTORY; - const collected = collectFieldRows(); - - if (!name || !selectedTemplateFile) { - setStatus( - elements.templateFormMessage, - "Name and PDF file are required.", - "error" - ); - return; - } - - if (collected.error) { - setStatus(elements.templateFormMessage, collected.error, "error"); - return; - } - - try { - let activePdfPath = uploadedPath; - if (!activePdfPath) { - setStatus(elements.templateFormMessage, "Copying PDF into project directory...", "info"); - const upload = await uploadTemplatePdf(selectedTemplateFile, templateDirectory); - activePdfPath = upload.pdf_path; - uploadedPath = upload.pdf_path; - } - - const payload = { - name, - pdf_path: activePdfPath, - fields: collected.value, - }; - - setStatus(elements.templateFormMessage, "Creating template...", "info"); - const response = await fetch(`${API_BASE_URL}/templates/create`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - upsertTemplate(body); - if (body.id != null) { - selectedFillIds.add(Number(body.id)); - } - await refreshTemplatesFromApi(); - elements.serverPdfPath.value = body.pdf_path || ""; - - const expected = body.field_count; - const actual = Object.keys(collected.value).length; - let mismatchNote = ""; - let statusLevel = "success"; - if (typeof expected === "number" && expected !== actual) { - mismatchNote = ` Heads up — the PDF has ${expected} fillable field${expected === 1 ? "" : "s"}, but you added ${actual} row${actual === 1 ? "" : "s"}. Fills may be incomplete or misaligned.`; - statusLevel = "error"; - } - - setStatus( - elements.templateFormMessage, - `Template created (id: ${body.id}). PDF saved at ${activePdfPath}.${mismatchNote}`, - statusLevel - ); - showJson(elements.templateFormResponse, body); - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - } catch (error) { - setStatus(elements.templateFormMessage, error.message, "error"); - } -} - -async function uploadTemplatePdf(file, directory) { - const formData = new FormData(); - formData.append("file", file, file.name); - formData.append("directory", directory); - - const response = await fetch(`${API_BASE_URL}/templates/upload`, { - method: "POST", - body: formData, - }); - - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - return body; -} - -// ───────────────────────── Fill Form: model + template tiles ────────────── - -// "1 field" / "3 forms" — keeps the count-and-label logic in one place. -function pluralize(count, noun) { - return `${count} ${noun}${count === 1 ? "" : "s"}`; -} - -// Look up a template by id (ids may arrive as strings from dataset attributes). -function findTemplate(id) { - return templates.find((template) => Number(template.id) === Number(id)); -} - -// Populate the model picker from the local Ollama models the API reports. -async function loadModels() { - const select = elements.fillModel; - try { - const response = await fetch(`${API_BASE_URL}/forms/models`); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - select.innerHTML = ""; - const models = body.models || []; - models.forEach((name) => { - const isDefault = name === body.default; - const option = document.createElement("option"); - option.value = name; - option.textContent = isDefault ? `${name} (default)` : name; - option.selected = isDefault; - select.append(option); - }); - } catch (_error) { - // Ollama unreachable — leave one placeholder so the picker isn't empty. - if (!select.options.length) { - const option = document.createElement("option"); - option.value = ""; - option.textContent = "(default model)"; - select.append(option); - } - } -} - -// Build one selectable tile. Whether it's selected is shown purely through the -// tile's highlighted styling (.selected) — there's no separate checkbox. -function createTemplateTile(template) { - const id = Number(template.id); - const selected = selectedFillIds.has(id); - - const tile = document.createElement("div"); - tile.className = selected ? "template-tile selected" : "template-tile"; - tile.dataset.templateId = String(id); - // Behaves like a toggle button for keyboard and screen-reader users. - tile.setAttribute("role", "button"); - tile.setAttribute("tabindex", "0"); - tile.setAttribute("aria-pressed", String(selected)); - - const title = document.createElement("span"); - title.className = "tile-title"; - title.textContent = template.name || "Untitled"; - - const fieldCount = template.fields ? Object.keys(template.fields).length : 0; - const meta = document.createElement("span"); - meta.className = "tile-meta"; - meta.textContent = pluralize(fieldCount, "field"); - - const body = document.createElement("div"); - body.className = "tile-body"; - body.append(title, meta); - - // Preview must not toggle selection, so it carries its own id and the click - // handler stops the event from bubbling up to the tile. - const previewButton = document.createElement("button"); - previewButton.type = "button"; - previewButton.className = "tile-preview-btn"; - previewButton.dataset.previewId = String(id); - previewButton.textContent = "Preview"; - - tile.append(body, previewButton); - return tile; -} - -function renderFillTemplates() { - const container = elements.fillTemplateTiles; - container.innerHTML = ""; - - if (!templates.length) { - const empty = document.createElement("p"); - empty.className = "empty-state"; - empty.textContent = "No templates yet — create one in the Create Template tab."; - container.append(empty); - updateFillButtonState(); - return; - } - - templates.forEach((template) => container.append(createTemplateTile(template))); - updateFillButtonState(); -} - -function handleTileClick(event) { - // A click on the Preview button previews the PDF without toggling selection. - const previewButton = event.target.closest(".tile-preview-btn"); - if (previewButton) { - event.stopPropagation(); - const template = findTemplate(previewButton.dataset.previewId); - if (template) { - elements.serverPdfPath.value = template.pdf_path || ""; - previewFromPath(template.pdf_path || "", { switchToPreview: true }); - } - return; - } - - // A click anywhere else on the tile toggles it on/off for filling. - const tile = event.target.closest(".template-tile"); - if (tile) { - toggleFillSelection(Number(tile.dataset.templateId)); - } -} - -function handleTileKeydown(event) { - // Enter/Space activate the focused tile, matching its role="button". - if (event.key !== "Enter" && event.key !== " ") { - return; - } - const tile = event.target.closest(".template-tile"); - if (tile) { - event.preventDefault(); - toggleFillSelection(Number(tile.dataset.templateId)); - } -} - -function toggleFillSelection(id) { - if (selectedFillIds.has(id)) { - selectedFillIds.delete(id); - } else { - selectedFillIds.add(id); - } - renderFillTemplates(); -} - -function updateFillButtonState() { - const count = selectedFillIds.size; - const nothingSelected = count === 0; - - // Greyed out (but still clickable) until at least one form is chosen. - elements.fillSubmitBtn.classList.toggle("is-disabled", nothingSelected); - elements.fillSubmitBtn.textContent = count > 1 ? `Fill ${count} Forms` : "Fill Form"; - - elements.fillSelectionHint.classList.remove("error"); - elements.fillSelectionHint.textContent = nothingSelected - ? "Select one or more forms to fill." - : `${pluralize(count, "form")} selected.`; -} - -// A human-readable label for a template, used in the success/error summary. -function templateLabel(id) { - const template = findTemplate(id); - return template && template.name ? template.name : `id ${id}`; -} - -// Fill a single template and return its submission. Throws on failure so the -// caller can note which form failed and still continue with the others. -async function fillOneTemplate(id, inputText, model) { - const response = await fetch(`${API_BASE_URL}/forms/fill`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ template_id: id, input_text: inputText, model }), - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - return body; -} - -// Summarize "N filled, M failed" into the status line, choosing the right tone: -// all-good = success, some failed but some worked = info, nothing worked = error. -function reportFillOutcome(results, errors) { - const parts = []; - if (results.length) parts.push(`${results.length} filled`); - if (errors.length) parts.push(`${errors.length} failed`); - - let level = "success"; - if (errors.length) { - level = results.length ? "info" : "error"; - } - - const detail = errors.length ? ` ${errors.join("; ")}` : ""; - setStatus(elements.fillFormMessage, `${parts.join(", ")}.${detail}`, level); -} - -async function handleFillSubmit(event) { - event.preventDefault(); - clearJson(elements.fillFormResponse); - setStatus(elements.fillFormMessage, ""); - - const ids = Array.from(selectedFillIds); - if (!ids.length) { - // The button looks disabled but stays clickable, so prompt the user here. - elements.fillSelectionHint.classList.add("error"); - elements.fillSelectionHint.textContent = "Select at least one form to fill."; - setStatus(elements.fillFormMessage, "Select at least one form to fill.", "error"); - return; - } - - const inputText = elements.inputText.value.trim(); - if (!inputText) { - setStatus(elements.fillFormMessage, "Input text is required.", "error"); - return; - } - - // An empty picker value means "let the server use its default model". - const model = elements.fillModel.value || undefined; - setStatus(elements.fillFormMessage, `Filling ${pluralize(ids.length, "form")}…`, "info"); - - // Fill each selected form independently so one failure doesn't stop the rest. - const results = []; - const errors = []; - for (const id of ids) { - try { - results.push(await fillOneTemplate(id, inputText, model)); - } catch (error) { - errors.push(`${templateLabel(id)}: ${error.message}`); - } - } - - const lastResult = results[results.length - 1]; - if (lastResult) { - showJson(elements.fillFormResponse, results.length === 1 ? lastResult : results); - if (lastResult.output_pdf_path) { - localStorage.setItem(STORAGE_LAST_OUTPUT_KEY, lastResult.output_pdf_path); - elements.serverPdfPath.value = lastResult.output_pdf_path; - } - } - - reportFillOutcome(results, errors); - - // Preview the most recently filled PDF. - if (lastResult && lastResult.output_pdf_path) { - await previewFromPath(lastResult.output_pdf_path, { switchToPreview: true }); - } -} - -// ───────────────────────── Speech-to-text (local Whisper) ───────────────── - -function setSttStatus(message) { - if (elements.sttStatus) { - elements.sttStatus.textContent = message || ""; - } -} - -async function startRecording() { - if (mediaRecorder) { - return; - } - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - setSttStatus("Microphone capture is not available in this environment."); - return; - } - - try { - recordingStream = await navigator.mediaDevices.getUserMedia({ audio: true }); - } catch (error) { - setSttStatus("Microphone permission denied."); - return; - } - - recordedChunks = []; - mediaRecorder = new MediaRecorder(recordingStream); - mediaRecorder.addEventListener("dataavailable", (event) => { - if (event.data && event.data.size > 0) { - recordedChunks.push(event.data); - } - }); - mediaRecorder.addEventListener("stop", handleRecordingStop); - mediaRecorder.start(); - - elements.sttControls.classList.add("is-recording"); - elements.sttControls.classList.remove("is-paused"); - elements.sttRecordBtn.disabled = true; - elements.sttPauseBtn.disabled = false; - elements.sttStopBtn.disabled = false; - elements.sttPauseBtn.textContent = "Pause"; - setSttStatus("Recording…"); -} - -function togglePauseRecording() { - if (!mediaRecorder) { - return; - } - if (mediaRecorder.state === "recording") { - mediaRecorder.pause(); - elements.sttControls.classList.add("is-paused"); - elements.sttControls.classList.remove("is-recording"); - elements.sttPauseBtn.textContent = "Resume"; - setSttStatus("Paused."); - } else if (mediaRecorder.state === "paused") { - mediaRecorder.resume(); - elements.sttControls.classList.add("is-recording"); - elements.sttControls.classList.remove("is-paused"); - elements.sttPauseBtn.textContent = "Pause"; - setSttStatus("Recording…"); - } -} - -function stopRecording() { - if (!mediaRecorder) { - return; - } - // Lock the controls while we finalize capture and transcribe. - elements.sttPauseBtn.disabled = true; - elements.sttStopBtn.disabled = true; - setSttStatus("Finishing capture…"); - mediaRecorder.stop(); -} - -async function handleRecordingStop() { - elements.sttControls.classList.remove("is-recording", "is-paused"); - stopRecordingStream(); - - const chunks = recordedChunks; - const recorder = mediaRecorder; - recordedChunks = []; - mediaRecorder = null; - - const blob = new Blob(chunks, { type: (recorder && recorder.mimeType) || "audio/webm" }); - if (!blob.size) { - resetSttControls(); - setSttStatus("Nothing was recorded."); - return; - } - - try { - setSttStatus("Transcribing…"); - const text = await transcribeAudio(blob); - appendTranscribedText(text); - setSttStatus(text ? "Transcription added." : "No speech detected."); - } catch (error) { - setSttStatus(`Transcription failed: ${error.message}`); - } finally { - resetSttControls(); - } -} - -function resetSttControls() { - elements.sttRecordBtn.disabled = false; - elements.sttPauseBtn.disabled = true; - elements.sttStopBtn.disabled = true; - elements.sttPauseBtn.textContent = "Pause"; - elements.sttControls.classList.remove("is-recording", "is-paused"); -} - -function stopRecordingStream() { - if (recordingStream) { - recordingStream.getTracks().forEach((track) => track.stop()); - recordingStream = null; - } -} - -function appendTranscribedText(text) { - if (!text) { - return; - } - const existing = elements.inputText.value.trim(); - elements.inputText.value = existing ? `${existing} ${text}` : text; - // Let any listeners (and the required-field check) see the new value. - elements.inputText.dispatchEvent(new Event("input")); -} - -// "audio/webm;codecs=opus" -> "webm". Just gives the upload a sensible filename; -// the server decodes by content, not extension. -function audioExtension(mimeType) { - const subtype = (mimeType || "").split("/")[1] || ""; - const withoutCodecs = subtype.split(";")[0].trim(); - return withoutCodecs || "webm"; -} - -// The Whisper ASR service decodes audio with ffmpeg, so we post the recording -// as-is (typically webm/opus) — no client-side transcoding needed. -async function transcribeAudio(blob) { - const formData = new FormData(); - formData.append("audio", blob, `recording.${audioExtension(blob.type)}`); - - const response = await fetch(`${API_BASE_URL}/forms/transcribe`, { - method: "POST", - body: formData, - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - return (body.text || "").trim(); -} - -function handleTemplateActionClick(event) { - const button = event.target.closest("button[data-action]"); - if (!button) { - return; - } - - const id = Number(button.dataset.templateId); - const template = templates.find((item) => Number(item.id) === id); - if (!template) { - return; - } - - if (button.dataset.action === "preview") { - elements.serverPdfPath.value = template.pdf_path || ""; - previewFromPath(template.pdf_path || "", { switchToPreview: true }); - return; - } - - if (button.dataset.action === "use-fill") { - selectedFillIds.add(Number(template.id)); - renderFillTemplates(); - activateSection("fillFormSection"); - setStatus( - elements.fillFormMessage, - `"${template.name || "Template"}" selected for filling.`, - "info" - ); - } -} - -function handleLocalFilePreview(event) { - const file = event.target.files && event.target.files[0]; - if (!file) { - return; - } - - if (activeObjectUrl) { - URL.revokeObjectURL(activeObjectUrl); - } - - activeObjectUrl = URL.createObjectURL(file); - elements.pdfFrame.src = activeObjectUrl; - switchSection("pdfPreviewerSection"); - setStatus(elements.previewStatus, `Previewing local file: ${file.name}`, "success"); -} - -function resolvePreviewCandidates(pathInput) { - const raw = String(pathInput || "").trim(); - if (!raw) { - return []; - } - - if (/^https?:\/\//i.test(raw)) { - return [raw]; - } - - return [`${API_BASE_URL}/templates/preview?path=${encodeURIComponent(raw)}`]; -} - -async function previewFromPath(pathInput, options = {}) { - if (options.switchToPreview) { - switchSection("pdfPreviewerSection"); - } - - const raw = String(pathInput || "").trim(); - if (!raw) { - setStatus(elements.previewStatus, "Enter a PDF path or URL first.", "error"); - return false; - } - - const candidates = resolvePreviewCandidates(raw); - if (!candidates.length) { - setStatus(elements.previewStatus, "Unable to parse preview path.", "error"); - return false; - } - - setStatus(elements.previewStatus, "Attempting to preview path...", "info"); - let lastReason = "unknown error"; - - for (const candidate of candidates) { - try { - const response = await fetch(candidate, { method: "HEAD" }); - if (response.ok || response.status === 405) { - elements.pdfFrame.src = candidate; - setStatus(elements.previewStatus, `Previewing path: ${candidate}`, "success"); - return true; - } - lastReason = `${response.status} ${response.statusText}`.trim(); - } catch (error) { - lastReason = error.message; - } - } - - const likelyServerLocal = - !/^https?:\/\//i.test(raw) && !raw.startsWith("/"); - - if (likelyServerLocal) { - setStatus( - elements.previewStatus, - `Could not preview "${raw}". It looks like a server-local path and may not be web-accessible.`, - "error" - ); - } else { - setStatus( - elements.previewStatus, - `Could not preview path. Last error: ${lastReason}`, - "error" - ); - } - - return false; -} - -function renderTemplates() { - elements.templatesList.innerHTML = ""; - - if (!templates.length) { - elements.templatesEmpty.classList.remove("hidden"); - return; - } - - elements.templatesEmpty.classList.add("hidden"); - templates.forEach((template) => { - const card = document.createElement("article"); - card.className = "template-card"; - - const title = document.createElement("h3"); - title.textContent = `${template.name || "Untitled"} (id: ${template.id ?? "n/a"})`; - - const path = document.createElement("p"); - path.className = "template-meta"; - path.textContent = `pdf_path: ${template.pdf_path || ""}`; - - const fields = buildFieldsTable(template.fields || {}); - - const actions = document.createElement("div"); - actions.className = "card-actions"; - - const previewButton = document.createElement("button"); - previewButton.type = "button"; - previewButton.dataset.action = "preview"; - previewButton.dataset.templateId = String(template.id); - previewButton.textContent = "Preview This Template"; - - const useFillButton = document.createElement("button"); - useFillButton.type = "button"; - useFillButton.dataset.action = "use-fill"; - useFillButton.dataset.templateId = String(template.id); - useFillButton.textContent = "Use in Fill Form"; - - actions.append(previewButton, useFillButton); - card.append(title, path, fields, actions); - elements.templatesList.append(card); - }); -} - -function buildFieldsTable(fieldsDict) { - const table = document.createElement("table"); - table.className = "fields-table"; - - const thead = document.createElement("thead"); - thead.innerHTML = "FieldType"; - table.appendChild(thead); - - const tbody = document.createElement("tbody"); - const entries = Object.entries(fieldsDict || {}); - if (!entries.length) { - const row = document.createElement("tr"); - const cell = document.createElement("td"); - cell.colSpan = 2; - cell.textContent = "No fields."; - row.appendChild(cell); - tbody.appendChild(row); - } else { - for (const [name, type] of entries) { - const row = document.createElement("tr"); - const nameCell = document.createElement("td"); - nameCell.textContent = name; - const typeCell = document.createElement("td"); - typeCell.textContent = TYPE_VALUE_TO_LABEL[type] || "Text"; - row.append(nameCell, typeCell); - tbody.appendChild(row); - } - } - table.appendChild(tbody); - return table; -} - -function normalizeFieldType(value) { - return TYPE_VALUE_TO_LABEL[value] ? value : "string"; -} - -function syncFieldRowsFromDom() { - const rowEls = Array.from(elements.fieldsBuilder.querySelectorAll(".field-row")); - fieldRows = rowEls.map((rowEl) => ({ - name: rowEl.querySelector(".field-name").value, - type: rowEl.querySelector(".field-type").value, - })); -} - -function renderFieldRows() { - const fragment = document.createDocumentFragment(); - fieldRows.forEach((row, index) => { - fragment.appendChild(buildFieldRow(row, index)); - }); - elements.fieldsBuilder.innerHTML = ""; - elements.fieldsBuilder.appendChild(fragment); - renderFieldCountBadge(); -} - -function buildFieldRow(row, index) { - const rowEl = document.createElement("div"); - rowEl.className = "field-row"; - rowEl.draggable = true; - rowEl.dataset.index = String(index); - - const handle = document.createElement("span"); - handle.className = "field-drag-handle"; - handle.setAttribute("aria-hidden", "true"); - handle.textContent = "⋮⋮"; // two-column dots — reads as a grip handle - - const nameInput = document.createElement("input"); - nameInput.type = "text"; - nameInput.className = "field-name"; - nameInput.placeholder = "Give description here"; - nameInput.value = row.name || ""; - nameInput.addEventListener("input", () => { - syncFieldRowsFromDom(); - }); - - const typeSelect = document.createElement("select"); - typeSelect.className = "field-type"; - FIELD_TYPES.forEach((t) => { - const opt = document.createElement("option"); - opt.value = t.value; - opt.textContent = t.label; - typeSelect.appendChild(opt); - }); - typeSelect.value = normalizeFieldType(row.type); - typeSelect.addEventListener("change", () => { - syncFieldRowsFromDom(); - }); - - const deleteBtn = document.createElement("button"); - deleteBtn.type = "button"; - deleteBtn.className = "field-delete-btn"; - deleteBtn.setAttribute("aria-label", "Remove field"); - deleteBtn.textContent = "✕"; // ✕ - deleteBtn.addEventListener("click", () => { - syncFieldRowsFromDom(); - const rowIndex = Number(rowEl.dataset.index); - fieldRows.splice(rowIndex, 1); - renderFieldRows(); - }); - - rowEl.addEventListener("dragstart", handleRowDragStart); - rowEl.addEventListener("dragover", handleRowDragOver); - rowEl.addEventListener("dragleave", handleRowDragLeave); - rowEl.addEventListener("drop", handleRowDrop); - rowEl.addEventListener("dragend", handleRowDragEnd); - - rowEl.append(handle, nameInput, typeSelect, deleteBtn); - return rowEl; -} - -function toggleMakeFillableHelp() { - const willShow = elements.makeFillableHelp.classList.contains("hidden"); - elements.makeFillableHelp.classList.toggle("hidden", !willShow); - elements.makeFillableHelpBtn.setAttribute("aria-expanded", String(willShow)); -} - -async function handleMakeFillableClick() { - if (!selectedTemplateFile) { - setStatus(elements.templateFormMessage, "Select a PDF first.", "error"); - return; - } - - const templateDirectory = DEFAULT_TEMPLATE_DIRECTORY; - - elements.makeFillableBtn.disabled = true; - const previousLabel = elements.makeFillableBtn.textContent; - elements.makeFillableBtn.textContent = "Working..."; - setStatus( - elements.templateFormMessage, - "Uploading PDF and running fillable-field detection (this can take a minute)...", - "info" - ); - - try { - if (!uploadedPath) { - const upload = await uploadTemplatePdf(selectedTemplateFile, templateDirectory); - uploadedPath = upload.pdf_path; - } - - const response = await fetch(`${API_BASE_URL}/templates/make-fillable`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ pdf_path: uploadedPath }), - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - uploadedPath = body.pdf_path; - const count = typeof body.field_count === "number" ? body.field_count : null; - uploadedFieldCount = count; - renderFieldCountBadge(); - setStatus( - elements.templateFormMessage, - count !== null - ? `Fillable PDF created — ${count} field${count === 1 ? "" : "s"} detected.` - : "Fillable PDF created.", - "success" - ); - elements.makeFillableBtn.textContent = "Re-detect fields"; - elements.makeFillableBtn.disabled = false; - } catch (error) { - setStatus(elements.templateFormMessage, error.message, "error"); - elements.makeFillableBtn.textContent = previousLabel; - elements.makeFillableBtn.disabled = false; - } -} - -function handleAddFieldClick() { - syncFieldRowsFromDom(); - fieldRows.push({ name: "", type: "string" }); - renderFieldRows(); - const rows = elements.fieldsBuilder.querySelectorAll(".field-row .field-name"); - if (rows.length) { - rows[rows.length - 1].focus(); - } -} - -function handleRowDragStart(event) { - const rowEl = event.currentTarget; - dragSourceIndex = Number(rowEl.dataset.index); - rowEl.classList.add("is-dragging"); - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", String(dragSourceIndex)); - } -} - -function handleRowDragOver(event) { - event.preventDefault(); - if (event.dataTransfer) { - event.dataTransfer.dropEffect = "move"; - } - event.currentTarget.classList.add("drag-over"); -} - -function handleRowDragLeave(event) { - event.currentTarget.classList.remove("drag-over"); -} - -function handleRowDrop(event) { - event.preventDefault(); - const rowEl = event.currentTarget; - rowEl.classList.remove("drag-over"); - const targetIndex = Number(rowEl.dataset.index); - if (dragSourceIndex === null || dragSourceIndex === targetIndex) { - return; - } - syncFieldRowsFromDom(); - const [moved] = fieldRows.splice(dragSourceIndex, 1); - fieldRows.splice(targetIndex, 0, moved); - dragSourceIndex = null; - renderFieldRows(); -} - -function handleRowDragEnd(event) { - event.currentTarget.classList.remove("is-dragging"); - elements.fieldsBuilder - .querySelectorAll(".field-row.drag-over") - .forEach((el) => el.classList.remove("drag-over")); - dragSourceIndex = null; -} - -function loadTemplates() { - try { - const raw = localStorage.getItem(STORAGE_TEMPLATES_KEY); - if (!raw) { - return []; - } - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch (_error) { - return []; - } -} - -function saveTemplates() { - localStorage.setItem(STORAGE_TEMPLATES_KEY, JSON.stringify(templates)); -} - -function upsertTemplate(template) { - const normalized = { - id: template.id, - name: template.name || "", - pdf_path: template.pdf_path || "", - fields: template.fields || {}, - }; - - const index = templates.findIndex((item) => Number(item.id) === Number(template.id)); - if (index >= 0) { - templates[index] = normalized; - } else { - templates.unshift(normalized); - } - - saveTemplates(); -} - -function restorePreviewState() { - const lastPath = localStorage.getItem(STORAGE_LAST_OUTPUT_KEY); - if (lastPath) { - elements.serverPdfPath.value = lastPath; - } -} - -async function parseJsonResponse(response) { - const text = await response.text(); - if (!text) { - return {}; - } - try { - return JSON.parse(text); - } catch (_error) { - return { raw: text }; - } -} - -function extractErrorMessage(responseBody, statusCode) { - if (responseBody && typeof responseBody === "object") { - if (typeof responseBody.error === "string") { - return responseBody.error; - } - if (Array.isArray(responseBody.detail)) { - const first = responseBody.detail[0]; - if (first && typeof first.msg === "string") { - return first.msg; - } - } - if (typeof responseBody.detail === "string") { - return responseBody.detail; - } - if (typeof responseBody.raw === "string") { - return responseBody.raw; - } - } - return `Request failed with status ${statusCode}.`; -} diff --git a/frontend/electron.js b/frontend/electron.js deleted file mode 100644 index 9fc7e198..00000000 --- a/frontend/electron.js +++ /dev/null @@ -1,64 +0,0 @@ -const { app, BrowserWindow } = require("electron"); -const path = require("path"); -const { spawn } = require("child_process"); - -let backendProcess = null; - -function startBackend() { - if (app.isPackaged) { - const ext = process.platform === 'win32' ? '.exe' : ''; - const backendPath = path.join(process.resourcesPath, "bin", `api-backend${ext}`); - - backendProcess = spawn(backendPath, [], { - stdio: 'ignore' - }); - - backendProcess.on('error', (err) => { - console.error('Failed to start backend process.', err); - }); - } else { - console.log("Running in development mode. Assuming backend is running via Docker Desktop."); - } -} - -function createWindow() { - const win = new BrowserWindow({ - width: 1100, - height: 800, - webPreferences: { - preload: path.join(__dirname, "preload.js"), - }, - }); - - // Allow microphone capture for the local speech-to-text recorder. Audio is - // only ever sent to the local Whisper service — nothing leaves the machine. - win.webContents.session.setPermissionRequestHandler( - (webContents, permission, callback) => { - callback(permission === "media"); - } - ); - - win.loadFile("index.html"); - if (!app.isPackaged) { - win.webContents.openDevTools(); - } -} - -app.whenReady().then(() => { - startBackend(); - createWindow(); -}); - -app.on("will-quit", () => { - if (backendProcess) { - backendProcess.kill(); - } -}); - -app.on("window-all-closed", () => { - if (process.platform !== "darwin") app.quit(); -}); - -app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); -}); diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index d525015e..00000000 --- a/frontend/index.html +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - FireForm - - - -
-
-

Starting up...

-

Initializing FireForm backend

-
-
-
-

FireForm

-

- Create templates, fill forms, and preview PDFs from one place. -

-
- - - -
-

Create Template

-
- - - - - -
- Drag and drop a PDF here - or click to select a file -
-

No PDF selected.

- - -
- - -
- - - -

- What information should be filled in? Add one row per field. Fields are filled - into your PDF in the order shown — drag to reorder. -

- -
- - - -
-

- -
- - - - - - -
- - - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 86ed6800..00000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,4025 +0,0 @@ -{ - "name": "fireform", - "version": "1.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "fireform", - "version": "1.1.0", - "devDependencies": { - "electron": "^35.0.0", - "electron-builder": "^26.0.0" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/fuses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.1", - "fs-extra": "^9.0.1", - "minimist": "^1.2.5" - }, - "bin": { - "electron-fuses": "dist/bin.js" - } - }, - "node_modules/@electron/fuses/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/fuses/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/fuses/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/rebuild": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^12.2.0", - "read-binary-file-arch": "^1.0.6" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", - "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/asar": "3.4.1", - "@electron/fuses": "^1.8.0", - "@electron/get": "^3.0.0", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "^4.0.3", - "@electron/universal": "2.0.3", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chromium-pickle-js": "^0.2.0", - "ci-info": "4.3.1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "26.8.1", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "isbinaryfile": "^5.0.0", - "jiti": "^2.4.2", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.0.3", - "plist": "3.1.0", - "proper-lockfile": "^4.1.2", - "resedit": "^1.7.0", - "semver": "~7.7.3", - "tar": "^7.5.7", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0", - "which": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "26.8.1", - "electron-builder-squirrel-windows": "26.8.1" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", - "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.6", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "js-yaml": "^4.1.0", - "sanitize-filename": "^1.6.3", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " - } - }, - "node_modules/dir-compare/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dmg-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", - "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "35.7.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz", - "integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", - "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "dmg-builder": "26.8.1", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "electron-winstaller": "5.4.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-publish": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", - "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.29.0.tgz", - "integrity": "sha512-bGc7hHz6lrdpMqH3XqfiHc5PKzEhjgUj6OLpTXynkLi9JZKyMByI/tdpm4Liu6O2BjtE1lakBWXjOQS1EnSQLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", - "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/tiny-async-pool": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.5.0" - } - }, - "node_modules/tiny-async-pool/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 468e34ce..00000000 --- a/frontend/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "fireform", - "version": "1.1.0", - "description": "FireForm — report once, file everywhere", - "repository": "fireform-core/FireForm", - "main": "electron.js", - "scripts": { - "start": "electron .", - "dist": "electron-builder --publish never" - }, - "devDependencies": { - "electron": "^35.0.0", - "electron-builder": "^26.0.0" - }, - "build": { - "appId": "com.fireform.app", - "productName": "FireForm", - "files": [ - "index.html", - "app.js", - "styles.css", - "electron.js", - "preload.js" - ], - "extraResources": [ - { - "from": "bin", - "to": "bin", - "filter": [ - "**/*" - ] - } - ], - "mac": { - "target": "dmg" - }, - "win": { - "target": "nsis" - }, - "linux": { - "target": "AppImage" - } - } -} diff --git a/frontend/preload.js b/frontend/preload.js deleted file mode 100644 index 4e99198a..00000000 --- a/frontend/preload.js +++ /dev/null @@ -1,3 +0,0 @@ -// Preload script — placeholder for future Node.js bridge APIs. -// Use contextBridge.exposeInMainWorld() here to safely expose -// Node functionality to the renderer process when needed. diff --git a/frontend/styles.css b/frontend/styles.css deleted file mode 100644 index c5b4a694..00000000 --- a/frontend/styles.css +++ /dev/null @@ -1,658 +0,0 @@ -:root { - --bg: linear-gradient(145deg, #f7f4ec 0%, #eef4f7 100%); - --panel: #ffffff; - --panel-border: #d7dde3; - --text: #1f2a36; - --muted: #576779; - --primary: #125a86; - --primary-strong: #0b4568; - --success: #1a7f4b; - --error: #a43434; - --radius: 14px; - --shadow: 0 10px 28px rgba(16, 44, 70, 0.09); -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-height: 100vh; - font-family: "Avenir Next", "Trebuchet MS", "Segoe UI", sans-serif; - color: var(--text); - background: var(--bg); -} - -.app-shell { - max-width: 980px; - margin: 0 auto; - padding: 24px 18px 48px; -} - -.app-header { - margin-bottom: 16px; -} - -.eyebrow { - margin: 0 0 8px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--muted); - font-size: 0.78rem; -} - -h1 { - margin: 0; - font-size: clamp(1.8rem, 3vw, 2.5rem); - line-height: 1.15; -} - -h2 { - margin: 0 0 14px; - font-size: 1.25rem; -} - -.subtitle { - margin: 8px 0 0; - color: var(--muted); -} - -.card { - background: var(--panel); - border: 1px solid var(--panel-border); - border-radius: var(--radius); - box-shadow: var(--shadow); - padding: 18px; -} - -.api-config { - display: grid; - gap: 8px; - margin-bottom: 14px; -} - -.tabs { - display: flex; - gap: 8px; - margin-bottom: 14px; - flex-wrap: wrap; -} - -.tab { - border: 1px solid var(--panel-border); - background: #f9fbfc; - border-radius: 999px; - padding: 8px 14px; - cursor: pointer; - font-weight: 600; - color: var(--text); -} - -.tab.active { - border-color: var(--primary); - color: #fff; - background: var(--primary); -} - -.panel { - display: grid; - gap: 14px; -} - -.stacked-form { - display: grid; - gap: 10px; -} - -.dropzone { - border: 2px dashed #9ab1c7; - border-radius: 12px; - padding: 20px 14px; - display: grid; - gap: 4px; - text-align: center; - cursor: pointer; - background: #f7fbff; - color: #2f4a63; - transition: border-color 0.2s ease, background-color 0.2s ease; -} - -.dropzone strong { - font-size: 1.02rem; - color: #1f3a53; -} - -.dropzone span { - font-size: 0.92rem; - color: #4e657b; -} - -.dropzone:hover, -.dropzone:focus-visible, -.dropzone.active { - border-color: var(--primary); - background: #eef7ff; - outline: none; -} - -label { - font-weight: 600; -} - -input, -textarea, -select, -button { - font: inherit; -} - -input, -textarea, -select { - width: 100%; - border: 1px solid #bcc8d6; - border-radius: 10px; - padding: 10px 12px; - background: #fff; - color: var(--text); -} - -select { - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml;utf8,"); - background-repeat: no-repeat; - background-position: right 12px center; - padding-right: 32px; -} - -input:focus, -textarea:focus, -select:focus { - outline: 2px solid var(--primary); - outline-offset: 1px; - border-color: var(--primary); -} - -textarea { - resize: vertical; -} - -button { - border: 0; - border-radius: 10px; - background: var(--primary); - color: #fff; - cursor: pointer; - padding: 10px 14px; - font-weight: 700; -} - -button:hover { - background: var(--primary-strong); -} - -.helper { - margin: 0; - color: var(--muted); - font-size: 0.95rem; -} - -.status { - margin: 0; - min-height: 1.4em; - color: var(--muted); -} - -.status.success { - color: var(--success); -} - -.status.error { - color: var(--error); -} - -.json-output { - margin: 0; - white-space: pre-wrap; - word-break: break-word; - border-radius: 10px; - border: 1px solid #d7dde3; - background: #f7fafc; - padding: 10px 12px; - max-height: 280px; - overflow: auto; - font-family: "IBM Plex Mono", "Menlo", "Consolas", monospace; - font-size: 0.88rem; -} - -.hidden { - display: none; -} - -.secondary-btn { - background: #f1f5f9; - color: var(--primary-strong); - border: 1px solid var(--panel-border); - font-weight: 600; - padding: 8px 14px; - justify-self: start; -} - -.secondary-btn:hover { - background: #e5ecf3; -} - -.stt-controls { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -} - -.stt-btn { - display: inline-flex; - align-items: center; - gap: 7px; - padding: 8px 14px; -} - -.stt-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.stt-dot { - width: 9px; - height: 9px; - border-radius: 50%; - background: #c2ccd6; - flex-shrink: 0; -} - -.stt-controls.is-recording .stt-dot { - background: var(--error); - animation: stt-pulse 1.1s ease-in-out infinite; -} - -.stt-controls.is-paused .stt-dot { - background: var(--primary); -} - -@keyframes stt-pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.35; transform: scale(0.78); } -} - -.stt-status { - color: var(--muted); - font-size: 0.92rem; - min-height: 1.2em; -} - -/* Fill Form: model picker, template tiles, view toggle */ -.helper.error { - color: var(--error); - font-weight: 600; -} - -.template-tiles { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); - gap: 14px; -} - -.template-tile { - position: relative; - display: flex; - flex-direction: column; - gap: 6px; - min-height: 112px; - border: 1px solid var(--panel-border); - border-radius: 12px; - background: #fbfcfe; - padding: 16px; - cursor: pointer; - transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; -} - -.template-tile:hover { - border-color: #9ab1c7; -} - -.template-tile:focus-visible { - outline: 2px solid var(--primary); - outline-offset: 1px; -} - -.template-tile.selected { - border-color: var(--primary); - background: #eef7ff; - box-shadow: inset 0 0 0 1px var(--primary); -} - -.tile-body { - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; -} - -.tile-title { - font-weight: 700; - color: var(--text); - /* Uniform tiles: clamp long titles to two lines, then ellipsis. */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; -} - -.tile-meta { - font-size: 0.85rem; - color: var(--muted); -} - -.tile-preview-btn { - margin-top: auto; /* pin to the bottom so every tile's button lines up */ - align-self: flex-start; - background: transparent; - color: var(--primary-strong); - border: 1px solid var(--panel-border); - border-radius: 8px; - padding: 5px 10px; - font-size: 0.82rem; - font-weight: 600; -} - -.tile-preview-btn:hover { - background: #e5ecf3; -} - -/* Greyed-but-clickable: looks disabled, still fires click to prompt the user. */ -button.is-disabled, -button.is-disabled:hover { - background: #c2ccd6; - cursor: not-allowed; -} - -.help-trigger { - background: #e5ecf3; - color: var(--primary-strong); - border: 1px solid #c8d2dd; - border-radius: 50%; - width: 24px; - height: 24px; - padding: 0; - font-weight: 700; - font-size: 0.9rem; - line-height: 1; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - flex-shrink: 0; -} - -.help-trigger:hover { - background: #d8e2eb; -} - -.help-trigger[aria-expanded="true"] { - background: var(--primary); - color: #fff; - border-color: var(--primary); -} - -.field-count-badge { - margin: 0; - padding: 6px 10px; - border-radius: 8px; - background: #f1f5f9; - border: 1px solid var(--panel-border); - color: var(--muted); - font-size: 0.92rem; - display: inline-block; - justify-self: start; -} - -.field-count-badge.hidden { - display: none; -} - -.field-count-badge.match { - background: #e8f3ec; - border-color: #b9d8c4; - color: var(--success); -} - -.field-count-badge.mismatch { - background: #fbe9e9; - border-color: #f3d3d3; - color: var(--error); -} - -.fields-builder { - display: grid; - gap: 8px; -} - -.field-row { - display: grid; - grid-template-columns: 24px 1fr 170px 36px; - gap: 10px; - align-items: center; - padding: 8px 10px; - border: 1px solid var(--panel-border); - border-radius: 12px; - background: #fbfcfe; -} - -.field-row.is-dragging { - opacity: 0.4; -} - -.field-row.drag-over { - border-color: var(--primary); - background: #eef7ff; -} - -.field-drag-handle { - cursor: grab; - color: var(--muted); - text-align: center; - user-select: none; - font-size: 1rem; - line-height: 1; -} - -.field-drag-handle:active { - cursor: grabbing; -} - -.field-delete-btn { - background: transparent; - color: var(--muted); - border: 1px solid transparent; - padding: 0; - font-size: 1rem; - font-weight: 600; - line-height: 1; - border-radius: 8px; - width: 32px; - height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.field-delete-btn:hover { - background: #fbe9e9; - color: var(--error); - border-color: #f3d3d3; -} - -.fields-table { - width: 100%; - border-collapse: collapse; - border: 1px solid var(--panel-border); - border-radius: 10px; - overflow: hidden; - background: #fff; - font-size: 0.92rem; -} - -.fields-table th, -.fields-table td { - text-align: left; - padding: 8px 10px; - border-bottom: 1px solid var(--panel-border); -} - -.fields-table tr:last-child td { - border-bottom: 0; -} - -.fields-table th { - background: #f1f5f9; - font-weight: 600; - color: var(--muted); -} - -@media (max-width: 540px) { - .field-row { - grid-template-columns: 22px 1fr 36px; - grid-template-areas: - "handle name delete" - ". select select"; - } - - .field-drag-handle { grid-area: handle; } - .field-row input[type="text"] { grid-area: name; } - .field-row select { grid-area: select; } - .field-delete-btn { grid-area: delete; } -} - -.divider { - width: 100%; - border: 0; - border-top: 1px solid #d7dde3; - margin: 4px 0; -} - -.template-list { - display: grid; - gap: 12px; -} - -.template-card { - border: 1px solid #d7dde3; - border-radius: 12px; - padding: 14px; - background: #fbfcfe; -} - -.template-meta { - margin: 0; -} - -.card-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 10px; -} - -.card-actions button { - padding: 8px 12px; - font-weight: 600; -} - -.empty-state { - padding: 14px; - border-radius: 10px; - border: 1px dashed #bcc8d6; - color: var(--muted); - background: #f8fafb; -} - -.preview-controls { - display: grid; - gap: 8px; -} - -.inline-actions { - display: grid; - grid-template-columns: 1fr auto; - gap: 8px; - align-items: center; -} - -#pdfFrame { - width: 100%; - min-height: 560px; - border: 1px solid #d7dde3; - border-radius: 10px; - background: #fff; -} - -@media (max-width: 720px) { - .app-shell { - padding: 16px 12px 28px; - } - - .card { - padding: 14px; - } - - .inline-actions { - grid-template-columns: 1fr; - } - - #pdfFrame { - min-height: 420px; - } -} - -.loading-screen { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: var(--bg); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 9999; - transition: opacity 0.5s ease, visibility 0.5s ease; -} - -.loading-screen.hidden { - opacity: 0; - visibility: hidden; - pointer-events: none; -} - -.loading-screen h2 { - margin-top: 24px; - color: var(--primary); -} - -.spinner { - width: 50px; - height: 50px; - border: 5px solid rgba(18, 90, 134, 0.2); - border-top-color: var(--primary); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} From e26a0fff8ab4a981ab5f0d0440cb9cb54411d70c Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 3 Jun 2026 00:37:22 +0530 Subject: [PATCH 06/85] chore: move scripts to scripts/ directory, fixes #517 --- container-init.sh => scripts/container-init.sh | 0 setup-dockers-env.sh => scripts/setup-dockers-env.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename container-init.sh => scripts/container-init.sh (100%) rename setup-dockers-env.sh => scripts/setup-dockers-env.sh (100%) diff --git a/container-init.sh b/scripts/container-init.sh similarity index 100% rename from container-init.sh rename to scripts/container-init.sh diff --git a/setup-dockers-env.sh b/scripts/setup-dockers-env.sh similarity index 100% rename from setup-dockers-env.sh rename to scripts/setup-dockers-env.sh From b6527b7ea109b6360a10bbb11bc7f022f5c9db2c Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 4 Jun 2026 00:57:31 +0530 Subject: [PATCH 07/85] chore: created docker file and compose for development envirnment --- docker/dev/Dockerfile | 26 +++++++++++++ docker/dev/compose.yml | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 docker/dev/Dockerfile create mode 100644 docker/dev/compose.yml diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile new file mode 100644 index 00000000..ad4bfd4b --- /dev/null +++ b/docker/dev/Dockerfile @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:1 +FROM python:3.11-slim + +WORKDIR /app + +# Use apt cache mount to speed up system package installation across builds +RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt \ + apt-get update && apt-get install -y \ + curl \ + libgl1 \ + libglib2.0-0 \ + libxcb1 + +COPY requirements.txt . + +# Use pip cache mount so it remembers downloaded wheels +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install -r requirements.txt + +ENV PYTHONPATH=/app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml new file mode 100644 index 00000000..5b7c2f04 --- /dev/null +++ b/docker/dev/compose.yml @@ -0,0 +1,83 @@ +services: + ollama: + image: ollama/ollama:latest + container_name: fireform-ollama + ports: + - "127.0.0.1:11434:11434" + volumes: + - ollama_data:/root/.ollama + networks: + - fireform-network + healthcheck: + test: ["CMD-SHELL", "ollama list || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + whisper: + image: onerahmet/openai-whisper-asr-webservice:latest + container_name: fireform-whisper + environment: + - ASR_ENGINE=faster_whisper + - ASR_MODEL=${WHISPER_MODEL:-small.en} + - ASR_MODEL_PATH=/data/whisper + volumes: + - whisper_models:/data/whisper + ports: + - "127.0.0.1:9000:9000" + networks: + - fireform-network + healthcheck: + test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9000/docs')\" || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + + app: + build: + context: ../.. + dockerfile: docker/dev/Dockerfile + container_name: fireform-app + depends_on: + ollama: + condition: service_healthy + whisper: + condition: service_started + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + volumes: + - ../..:/app + - fireform_db:/data/db + - fireform_uploads:/data/uploads + ports: + - "${APP_PORT:-8000}:8000" + environment: + - PYTHONUNBUFFERED=1 + - CUDA_VISIBLE_DEVICES= + - PYTHONPATH=/app + - OLLAMA_HOST=${OLLAMA_HOST:-http://ollama:11434} + - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT:-300} + - OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:1.5b} + - WHISPER_HOST=${WHISPER_HOST:-http://whisper:9000} + - FIREFORM_DB_PATH=/data/db/fireform.db + - FIREFORM_DATA_DIR=/data/uploads + - FIREFORM_TEMPLATE_DIR=/data/uploads + - FIREFORM_DB_ECHO=${FIREFORM_DB_ECHO:-true} + - FRONTEND_ORIGINS=${FRONTEND_ORIGINS:-http://localhost:5173,http://127.0.0.1:5173} + networks: + - fireform-network + +volumes: + ollama_data: + driver: local + whisper_models: + driver: local + fireform_db: + driver: local + fireform_uploads: + driver: local + +networks: + fireform-network: + driver: bridge From 79a1ce05ef82009893de5e3a30e059354db6502d Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 4 Jun 2026 01:10:06 +0530 Subject: [PATCH 08/85] chore: docker file and docker compose for prod envirnment --- docker/prod/Dockerfile | 37 +++++++++++++++++ docker/prod/compose.yml | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docker/prod/Dockerfile create mode 100644 docker/prod/compose.yml diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile new file mode 100644 index 00000000..8ad8deab --- /dev/null +++ b/docker/prod/Dockerfile @@ -0,0 +1,37 @@ +FROM python:3.11-slim AS builder + +WORKDIR /build + +COPY requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + + +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + curl \ + libgl1 \ + libglib2.0-0 \ + libxcb1 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /install /usr/local + +# Copy only app code, not data/ temp/ tests/ docs/ etc. +COPY app/ ./app/ +COPY requirements.txt . + +ENV PYTHONPATH=/app + +# Data dirs created here; actual storage comes from mounted volumes at runtime. +RUN mkdir -p /data/db /data/uploads && chmod +x /entrypoint.sh + +EXPOSE 8000 + +CMD ["gunicorn", "app.main:app", \ + "--worker-class", "uvicorn.workers.UvicornWorker", \ + "--bind", "0.0.0.0:8000", \ + "--access-logfile", "-", \ + "--error-logfile", "-"] diff --git a/docker/prod/compose.yml b/docker/prod/compose.yml new file mode 100644 index 00000000..edd2fa49 --- /dev/null +++ b/docker/prod/compose.yml @@ -0,0 +1,90 @@ +services: + ollama: + image: ollama/ollama:latest + container_name: fireform-ollama + ports: + - "127.0.0.1:11434:11434" + volumes: + - ollama_data:/root/.ollama + networks: + - fireform-network + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "ollama list || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + whisper: + image: onerahmet/openai-whisper-asr-webservice:latest + container_name: fireform-whisper + environment: + - ASR_ENGINE=faster_whisper + - ASR_MODEL=${WHISPER_MODEL} + - ASR_MODEL_PATH=/data/whisper + volumes: + - whisper_models:/data/whisper + ports: + - "127.0.0.1:9000:9000" + networks: + - fireform-network + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9000/docs')\" || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + + app: + build: + context: ../.. + dockerfile: docker/prod/Dockerfile + container_name: fireform-app + depends_on: + ollama: + condition: service_healthy + whisper: + condition: service_started + command: ["gunicorn", "app.main:app", + "--worker-class", "uvicorn.workers.UvicornWorker", + "--bind", "0.0.0.0:8000", + "--access-logfile", "-", + "--error-logfile", "-"] + volumes: + - fireform_db:/data/db + - fireform_uploads:/data/uploads + ports: + - "${APP_PORT}:8000" + environment: + - PYTHONUNBUFFERED=1 + - CUDA_VISIBLE_DEVICES= + - PYTHONPATH=/app + - OLLAMA_HOST=${OLLAMA_HOST} + - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT} + - OLLAMA_MODEL=${OLLAMA_MODEL} + - WHISPER_HOST=${WHISPER_HOST} + - FIREFORM_DB_PATH=/data/db/fireform.db + - FIREFORM_DATA_DIR=/data/uploads + - FIREFORM_TEMPLATE_DIR=/data/uploads + - FIREFORM_DB_ECHO=false + - FRONTEND_ORIGINS=${FRONTEND_ORIGINS} + - WEB_CONCURRENCY=${GUNICORN_WORKERS} + networks: + - fireform-network + restart: unless-stopped + +volumes: + ollama_data: + driver: local + whisper_models: + driver: local + fireform_db: + driver: local + fireform_uploads: + driver: local + +networks: + fireform-network: + driver: bridge From 0bc991a5cade6d7cdd085d7211b2728049057209 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 4 Jun 2026 15:43:36 +0530 Subject: [PATCH 09/85] Updated Makefile: added make init command to run a project initilization setup. added init-env, select ollama model scripts. Updated docker compose path proposed in #526 and #527 --- Makefile | 102 +++++++++++++++++++++----------------- scripts/check-deps.sh | 51 +++++++++++++++++++ scripts/init-env.sh | 35 +++++++++++++ scripts/select-model.sh | 107 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 44 deletions(-) create mode 100755 scripts/check-deps.sh create mode 100755 scripts/init-env.sh create mode 100755 scripts/select-model.sh diff --git a/Makefile b/Makefile index 3f155338..13ea3665 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ -.PHONY: help build up down logs shell exec pull-model test clean fireform logs-app logs-ollama logs-frontend super-clean +.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean -# The extraction model pulled into Ollama and used by src/llm.py. Override with -# `make pull-model OLLAMA_MODEL=...`. A 1.5B model keeps per-field fills fast. -OLLAMA_MODEL ?= qwen2.5:1.5b +COMPOSE = docker compose -f docker/dev/compose.yml --env-file docker/.env.dev +ENV_DEV = docker/.env.dev + +# Read OLLAMA_MODEL from .env.dev at runtime; fall back to default if file absent. +OLLAMA_MODEL = $(shell grep -E '^OLLAMA_MODEL=' $(ENV_DEV) 2>/dev/null | cut -d= -f2 | tr -d '[:space:]' || echo qwen2.5:1.5b) help: @printf '%s\n' \ @@ -13,72 +15,84 @@ help: '/_/ /_//_/ \___/ /_/ \____/_/ /_/ /_/ /_/ ' \ '' @echo "" - @echo "Fireform Development Commands" + @echo "FireForm Development Commands" @echo "==============================" - @echo "make fireform - Build and start containers, then open a shell" + @echo "make init - First-time setup: check deps, create .env.dev, pick model" + @echo "make fireform - Build images, start containers, pull Ollama model" @echo "make build - Build Docker images" - @echo "make up - Start all containers" + @echo "make up - Start all containers (detached)" @echo "make down - Stop all containers" - @echo "make logs - View container logs" - @echo "make logs-app - View API container logs" - @echo "make logs-frontend - View frontend container logs" - @echo "make logs-ollama - View Ollama container logs" - @echo "make shell - Open Python shell in app container" - @echo "make exec - Execute Python script in container" - @echo "make pull-model - Pull the extraction model ($(OLLAMA_MODEL)) into Ollama" - @echo "make test - Run tests" - @echo "make clean - Remove containers" - @echo "make super-clean - [CAUTION] Use carefully. Cleans up ALL stopped containers, networks, build cache..." - -# Fix #382 — pull-model is now part of the main setup flow -# The extraction model is pulled automatically before you need it -fireform: build up pull-model + @echo "make logs - Stream all container logs" + @echo "make logs-app - Stream app container logs" + @echo "make logs-ollama - Stream Ollama container logs" + @echo "make shell - Open shell in running app container" + @echo "make pull-model - Pull Ollama model from .env.dev ($(OLLAMA_MODEL))" + @echo "make test - Run test suite" + @echo "make clean - Stop containers (preserves volumes)" + @echo "make super-clean - [CAUTION] Stop containers, delete volumes, prune Docker" + +init: + @chmod +x scripts/check-deps.sh scripts/init-env.sh scripts/select-model.sh + @sh scripts/check-deps.sh + @sh scripts/init-env.sh + @sh scripts/select-model.sh + @printf "Build containers and pull model now? [y/N] "; \ + read answer; \ + case "$$answer" in \ + [yY]*) $(MAKE) fireform ;; \ + *) echo "Run 'make fireform' when ready." ;; \ + esac + +fireform: build up + @printf "Waiting for Ollama to be ready..." + @until $(COMPOSE) exec -T ollama ollama list > /dev/null 2>&1; do \ + printf '.'; sleep 2; \ + done + @echo " ready." + @if $(COMPOSE) exec -T ollama ollama list 2>/dev/null | grep -q "^$(OLLAMA_MODEL)"; then \ + echo " Model $(OLLAMA_MODEL) already pulled."; \ + else \ + echo " Pulling $(OLLAMA_MODEL)..."; \ + $(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL); \ + fi @echo "" - @echo "✅ FireForm is ready!" - @echo " Frontend: http://localhost:5173" + @echo "FireForm is ready!" @echo " API: http://localhost:8000" @echo " API Docs: http://localhost:8000/docs" @echo "" @echo "Run 'make logs' to view live logs, 'make down' to stop." build: - docker compose build + @$(COMPOSE) build --progress=quiet up: - docker compose up -d + @$(COMPOSE) up -d down: - docker compose down + @$(COMPOSE) down --remove-orphans logs: - docker compose logs -f + @$(COMPOSE) logs -f logs-app: - docker compose logs -f app + @$(COMPOSE) logs -f app logs-ollama: - docker compose logs -f ollama - -logs-frontend: - docker compose logs -f frontend + @$(COMPOSE) logs -f ollama shell: - docker compose exec app /bin/bash - -# Start the FastAPI server inside the running container -run: - docker compose exec app uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload - + @$(COMPOSE) exec app /bin/sh pull-model: - docker compose exec ollama ollama pull $(OLLAMA_MODEL) + @$(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL) -# Fix — correct test directory (was src/test/ which doesn't exist) test: - docker compose exec app python3 -m pytest tests/ -v + @$(COMPOSE) exec -T app python3 -m pytest tests/ -v clean: - docker compose down -v + @$(COMPOSE) down + super-clean: - docker compose down -v - docker system prune + @echo "WARNING: this will delete all volumes (database, uploads, model weights)." + @$(COMPOSE) down -v + @docker system prune -f diff --git a/scripts/check-deps.sh b/scripts/check-deps.sh new file mode 100755 index 00000000..3c4484e7 --- /dev/null +++ b/scripts/check-deps.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -e + +PASS=0 +FAIL=1 +errors=0 + +check() { + label="$1" + shift + if "$@" > /dev/null 2>&1; then + echo " [ok] $label" + else + echo " [!!] $label" + errors=$((errors + 1)) + fi +} + +echo "" +echo "Checking dependencies..." +echo "========================" + +# Docker daemon running +check "Docker daemon is running" docker info + +# docker compose v2 (plugin form, not legacy docker-compose) +check "docker compose v2 available" docker compose version + +# Minimum Docker version: 24 (BuildKit cache mounts stable) +DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1) +if [ -n "$DOCKER_VERSION" ] && [ "$DOCKER_VERSION" -ge 24 ] 2>/dev/null; then + echo " [ok] Docker version >= 24 (found $(docker version --format '{{.Server.Version}}' 2>/dev/null))" +else + echo " [!!] Docker version >= 24 required (found $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo 'unknown'))" + echo " BuildKit cache mounts in docker/dev/Dockerfile require Docker 24+." + errors=$((errors + 1)) +fi + +echo "" + +if [ "$errors" -gt 0 ]; then + echo "$errors check(s) failed. Fix the above before continuing." + echo "" + echo " Install Docker: https://docs.docker.com/get-docker/" + echo " Upgrade Docker Desktop: https://docs.docker.com/desktop/release-notes/" + echo "" + exit 1 +fi + +echo "All checks passed." +echo "" diff --git a/scripts/init-env.sh b/scripts/init-env.sh new file mode 100755 index 00000000..c2c08040 --- /dev/null +++ b/scripts/init-env.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e + +ENV_EXAMPLE="docker/.env.example" +ENV_DEV="docker/.env.dev" + +echo "" +echo "Setting up environment..." +echo "=========================" + +if [ ! -f "$ENV_EXAMPLE" ]; then + echo "Error: $ENV_EXAMPLE not found. Are you running from the repo root?" + exit 1 +fi + +if [ -f "$ENV_DEV" ]; then + printf " %s already exists. Overwrite? [y/N] " "$ENV_DEV" + read -r answer + case "$answer" in + [yY]*) + cp "$ENV_EXAMPLE" "$ENV_DEV" + echo " Overwritten." + ;; + *) + echo " Kept existing $ENV_DEV." + ;; + esac +else + cp "$ENV_EXAMPLE" "$ENV_DEV" + echo " Created $ENV_DEV from $ENV_EXAMPLE." +fi + +echo "" +echo " Review docker/.env.dev and adjust values if needed before running 'make fireform'." +echo "" diff --git a/scripts/select-model.sh b/scripts/select-model.sh new file mode 100755 index 00000000..4b3c9d36 --- /dev/null +++ b/scripts/select-model.sh @@ -0,0 +1,107 @@ +#!/bin/sh +set -e + +ENV_DEV="docker/.env.dev" +COMPOSE="docker compose -f docker/dev/compose.yml --env-file $ENV_DEV" + +# model name | approx size +MODELS="qwen2.5:1.5b|~1GB qwen2.5:3b|~2GB qwen2.5:7b|~4GB llama3.2:3b|~2GB mistral:7b|~4GB" + +current_model="" +if [ -f "$ENV_DEV" ]; then + current_model=$(grep -E '^OLLAMA_MODEL=' "$ENV_DEV" | cut -d= -f2 | tr -d '[:space:]') +fi + +echo "" +echo "Select Ollama model" +echo "===================" +[ -n "$current_model" ] && echo " Current: $current_model" +echo "" + +i=1 +for entry in $MODELS; do + name=$(echo "$entry" | cut -d'|' -f1) + size=$(echo "$entry" | cut -d'|' -f2) + if [ "$name" = "${current_model}" ]; then + echo " $i) $name $size [current]" + else + echo " $i) $name $size" + fi + i=$((i + 1)) +done +echo " $i) Enter custom model name" +i=$((i + 1)) +echo " $i) Keep current" +echo "" +printf "Choice [1-$i]: " +read -r choice + +total=$(echo "$MODELS" | wc -w | tr -d '[:space:]') +keep_choice=$((total + 2)) +custom_choice=$((total + 1)) + +if [ "$choice" = "$keep_choice" ] || [ -z "$choice" ]; then + echo " Keeping current model: ${current_model:-qwen2.5:1.5b}" + echo "" + exit 0 +fi + +if [ "$choice" = "$custom_choice" ]; then + printf " Enter model name (e.g. phi3:mini): " + read -r selected + if [ -z "$selected" ]; then + echo " No model entered. Keeping current." + echo "" + exit 0 + fi + selected_size="unknown size" +else + # Validate numeric choice in range + if ! echo "$choice" | grep -qE '^[0-9]+$' || [ "$choice" -lt 1 ] || [ "$choice" -gt "$total" ]; then + echo " Invalid choice. Keeping current model." + echo "" + exit 0 + fi + i=1 + for entry in $MODELS; do + if [ "$i" = "$choice" ]; then + selected=$(echo "$entry" | cut -d'|' -f1) + selected_size=$(echo "$entry" | cut -d'|' -f2) + fi + i=$((i + 1)) + done +fi + +# Patch OLLAMA_MODEL in .env.dev +tmp=$(mktemp) +sed "s|^OLLAMA_MODEL=.*|OLLAMA_MODEL=$selected|" "$ENV_DEV" > "$tmp" +mv "$tmp" "$ENV_DEV" +echo "" +echo " OLLAMA_MODEL set to: $selected" + +# If container is running, check and optionally pull immediately. +# If not running, the caller (make init) handles the pull prompt. +if $COMPOSE ps ollama 2>/dev/null | grep -q "running"; then + if $COMPOSE exec -T ollama ollama list 2>/dev/null | grep -q "^$selected"; then + echo " Model already pulled. Nothing to download." + echo "" + exit 0 + fi + + echo "" + printf " Model not yet downloaded ($selected_size). Pull now? [y/N] " + read -r pull_answer + case "$pull_answer" in + [yY]*) + echo "" + $COMPOSE exec -T ollama ollama pull "$selected" + echo "" + echo " Model ready." + ;; + *) + echo " Skipped. Run 'make pull-model' when ready." + ;; + esac +fi + +echo "" From e2e066265130f51d93eec4e8491b9bf1b9f48257 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 6 Jun 2026 03:44:31 +0530 Subject: [PATCH 10/85] remove unwanted html files under docs/ which is already migrated to fireform-core/fireform-website --- docs/dpg.html | 203 ------------------- docs/index.html | 176 ----------------- docs/privacy.html | 97 --------- docs/styles.css | 488 ---------------------------------------------- docs/terms.html | 152 --------------- 5 files changed, 1116 deletions(-) delete mode 100644 docs/dpg.html delete mode 100644 docs/index.html delete mode 100644 docs/privacy.html delete mode 100644 docs/styles.css delete mode 100644 docs/terms.html diff --git a/docs/dpg.html b/docs/dpg.html deleted file mode 100644 index 47371bc6..00000000 --- a/docs/dpg.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - FireForm - Digital Public Good & SDG Relevance - - - - - - - -
-
-
-
- - - -
-
-
-
Digital Public Good
-

SDG Relevance

-

- FireForm is proud to be an open-source Digital Public Good, dedicated to advancing the United Nations' Sustainable Development Goals (SDGs). By drastically reducing the administrative burden on first responders, we enable emergency services to operate more efficiently, safely, and transparently. -

-
-
-
- -
-
-

Relevant Sustainable Development Goals

- -
- -
-
🏢
-

SDG 16: Peace, Justice and Strong Institutions

-

Target 16.6: Develop effective, accountable and transparent institutions at all levels.

-

How FireForm contributes: By unifying and digitizing the reporting structure for firefighters and other emergency responders, FireForm builds stronger, more accountable local institutions. Emergency services can seamlessly share critical incident data across county lines, sheriff departments, and EMS, ensuring transparency and highly effective public service administration without the overhead of repetitive paperwork.

-
- -
-
🏙️
-

SDG 11: Sustainable Cities and Communities

-

Target 11.b: By 2020, substantially increase the number of cities and human settlements adopting and implementing integrated policies and plans towards inclusion, resource efficiency, mitigation and adaptation to climate change, resilience to disasters...

-

How FireForm contributes: FireForm directly enhances a city's resilience to disasters by giving first responders hours of their time back. Instead of doing administrative work, firefighters can focus on training, disaster mitigation, and responding to emergencies, ultimately creating safer and more resilient communities.

-
- -
-
💡
-

SDG 9: Industry, Innovation and Infrastructure

-

Target 9.4: By 2030, upgrade infrastructure and retrofit industries to make them sustainable, with increased resource-use efficiency and greater adoption of clean and environmentally sound technologies and industrial processes...

-

How FireForm contributes: FireForm modernizes the outdated infrastructure of first responder reporting. By leveraging open-source AI locally, it serves as an innovative digital infrastructure that makes emergency management more resource-efficient and environmentally sound (eliminating physical paperwork and reducing digital redundancies).

-
- -
- -
-

Clear Ownership & Accountability

-
-

- In accordance with the Digital Public Goods Alliance requirements for Open Software, the ownership and accountability of the FireForm project and all its produced assets are clearly defined. -

-

- Accountable Entity: The intellectual property and copyright of the software code and content are owned by the original core creators: Juan Álvarez Sánchez, Manuel Carriedo Garrido, Vincent Harkins, Marc Vergés, and Jan Sans. -

-

- This ownership is publicly documented and verifiable across our project's assets: -

-
    -
  • Software License: Detailed in our public MIT License.
  • -
  • Source Repository: Explicitly stated in our GitHub README page.
  • -
  • Public Website: Documented here on our official project website.
  • -
-
-
- -
-

Platform Independence

-
-

- In accordance with the Digital Public Goods Alliance requirements for Open Software and Open AI Systems, FireForm guarantees platform independence. We do not rely on mandatory proprietary dependencies or closed components that would restrict our MIT License. -

-

- Core Dependencies: -

-
    -
  • Frontend: React, Electron, Node.js. (Declared in frontend/package.json)
  • -
  • Backend: Python, FastAPI, SQLite. (Declared in requirements.txt)
  • -
  • AI System (Optional): Ollama running open-weight models like Mistral. All inference runs locally. Note: The AI features are optional and not core to the main functionality of FireForm (which operates as a digital form and template manager). The AI extraction can be disabled in the application settings. Furthermore, this local Ollama dependency can be swapped with any other LLM service.
  • -
-

- Dependency Graph (SBOM): All components and their versions in our software supply chain are automatically tracked by our source repository. You can view our full dependency graph and SBOM directly on our GitHub repository. If any proprietary components are ever integrated, they will be strictly optional and replaceable with open alternatives without modifying the core solution. -

-
-
- -
-

Mechanism for Extracting Data

-
-

- Digital public goods must ensure that data and content can be extracted or imported in a non-proprietary format. FireForm embraces this requirement at the core of its architecture: -

-
    -
  • Standard Format (JSON): All data extracted by the LLM—whether non-PII or PII—is formatted and exported natively as a non-proprietary JSON object. This makes it instantly compatible with any modern software or data pipeline without vendor lock-in.
  • -
  • Open Storage (SQLite): Metadata, templates, and local configuration are stored using SQLite, an open, serverless database engine. The data is saved locally in a fireform.db file, which can be easily extracted, backed up, and read by hundreds of open-source tools.
  • -
  • API Access: Our local Python FastAPI backend exposes these JSON objects and database entries, allowing automated systems to securely extract the data.
  • -
-

- By standardizing on JSON and SQLite, FireForm guarantees that emergency departments retain full ownership and accessibility of their data at all times. -

-
-
- -
-

Privacy & Applicable Laws

-
-

- FireForm is designed from the ground up with a privacy-first, local-only architecture. Because emergency incident reports frequently contain sensitive Personally Identifiable Information (PII), we guarantee that no data ever leaves the user's device. -

-

- By eliminating cloud servers and third-party APIs, FireForm enables first responder organizations to easily comply with the world's most stringent data protection laws, including: -

-
    -
  • HIPAA (Health Insurance Portability and Accountability Act): Ensures EMS medical data remains entirely offline and secure.
  • -
  • CCPA (California Consumer Privacy Act): Ensures no consumer data is shared or sold.
  • -
  • GDPR (General Data Protection Regulation): Enforces strict data minimization and localizes data subjects' rights.
  • -
-

- For full details on our consent management procedures, data minimization practices, and how the solution was designed to comply with HIPAA, CCPA, and GDPR, please read our official Privacy Policy. -

-
-
- -
-

Do No Harm by Design

-
-

- FireForm is committed to anticipating and preventing harm by design, strictly adhering to the DPGA's framework: -

-
    -
  • 9A. Data Privacy & Security: Because our solution handles sensitive incident data (PII), we mitigate risk by strictly operating offline. Data is never exposed to public networks, avoiding data breaches. (See our Privacy Policy).
  • -
  • 9B. Inappropriate & Illegal Content: FireForm is an enterprise productivity tool for emergency responders, not a social platform. It does not host public user-generated content, completely mitigating the risk of distributing illegal or misleading public content.
  • -
  • 9C. Protection from Harassment: There is no public user-to-user interaction within the app. For our open-source contributor community, we strictly enforce our Code of Conduct to protect against harassment and abuse.
  • -
-
-
- -
-

Standards & Best Practices

-
-

- FireForm strictly aligns with globally recognized standards and best practices curated by the DPGA to ensure high-quality, interoperable, and sustainable software: -

-

Adhered Standards:

-
    -
  • JSON & UTF-8 (Data Interchange): All incident data extracted by our AI system is structured purely as JSON with UTF-8 encoding.
  • -
  • OpenAPI & REST (Data Exchange): Our Python backend is built with FastAPI, which automatically generates interactive OpenAPI specifications and follows RESTful architectural patterns.
  • -
-

Adhered Best Practices:

-
    -
  • Community: We maintain a welcoming environment through our Code of Conduct and clear Contribution Guidelines.
  • -
  • Lifecycle Management: All codebase modifications use Git for Change Management. We track progress with Tagged Releases and strictly adhere to Semantic Versioning.
  • -
  • Interoperability & Architecture: We prioritize Open Standards and File Formats, modularized Programmatic APIs, and strict Dependency Management.
  • -
-
-
- -
-

Public Communications & Mission

-

- Our mission is to build software that protects the people who protect us. FireForm was originally conceived and won 1st place at the Reboot the Earth hackathon, hosted by the UN and UC Santa Cruz, specifically targeting solutions for a better, more sustainable future. -

-

- We believe that modern technology like Local LLMs should be leveraged as public goods, accessible to any department regardless of budget, to help them better serve their communities. -

-
-
-
- -
-
-

FireForm is a Digital Public Good. Licensed under MIT.

-
-
- - diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index d01e95ec..00000000 --- a/docs/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - FireForm - Report Once, File Everywhere - - - - - - - -
-
-
-
- - - -
-
-
-
Reboot the Earth Hackathon Winner
-

Report Once,
File Everywhere.

-

- An open-source, AI-powered system built to solve administrative overhead for first responders. Save hundreds of hours by eliminating redundant paperwork. -

- - -
- -
-

Digital Public Good

-
-
-

FireForm is proudly recognized as a Digital Public Good (DPG). We are committed to advancing the United Nations' Sustainable Development Goals (SDGs).

-
- Learn about our SDG Relevance → -
-
- -
-

Current Features

-
-
-
📝
-

Smart PDF Conversion

-

Automatically turn any non-fillable PDF into a fillable, standardized template.

-
-
-
💾
-

Local Template Database

-

Store your templates in a local DB. Recover past templates and quickly generate new forms.

-
-
-
🎙️
-

Voice or Text Input

-

Provide input via text or voice transcription. A single input drives everything.

-
-
-
🤖
-

Automated LLM Filling

-

Our local LLM extracts relevant information and automatically populates the documents.

-
-
-
- -
-

Future Roadmap

-
-
-
-
-
🌍
-

External Data APIs

-

Connect to APIs to automatically fetch climate, location, and other external data.

-
-
-
-
-
-
📊
-

Insightful Dashboard

-

Visualize your reports with comprehensive graphics and statistics.

-
-
-
-
-
-
🧠
-

ML Field Detection

-

Machine learning models for advanced, automatic form field detection.

-
-
-
-
-
-
📑
-

Advanced PDF Features

-

More powerful PDF manipulation, merging, and management tools.

-
-
-
-
- -
-

About the Project

-
-
-

FireForm was born at a Silicon Valley hackathon organized by the United Nations, University of California, and CalFire.

-

After winning first place, the project was awarded a 6-month mentorship by Salesforce. We are also proud to be part of Google Summer of Code, welcoming new collaborators to scale our impact.

-
- -
-
-
-
- - - - diff --git a/docs/privacy.html b/docs/privacy.html deleted file mode 100644 index 461a3703..00000000 --- a/docs/privacy.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - FireForm - Privacy Policy - - - - - - - -
-
-
-
- - - -
-
-
-

Privacy Policy

-

- FireForm is built on the principle of privacy by design. Our architecture guarantees that your data, including Personally Identifiable Information (PII), never leaves your device. -

-
-
-
- -
-
- -
-

1. Local-First Architecture

-

- FireForm is a completely offline, local-first application. All processing, including voice transcription and AI-based information extraction, is performed on your local hardware using local Large Language Models (LLMs) via Ollama. -

-

- We do not use cloud APIs, we do not have central servers that collect your data, and we do not track telemetry. Therefore, we do not collect, process, or share any PII with third parties. -

-
- -
-

2. Data Collection and Usage

-

- The only data collected by FireForm is the information you explicitly input (voice memos, text, and PDF templates) to generate incident reports. This data is stored locally on your device in a local SQLite database and standard JSON files. You maintain full ownership and control over this data, and can delete it or export it at any time. -

-

- Because all data remains on your device, consent management and data subject requests are fully within the control of the user or the deploying organization (e.g., the fire department). -

-
- -
-

3. Compliance with Applicable Laws

-

- By guaranteeing that data never leaves the host machine, FireForm enables deploying organizations to easily comply with the strictest data protection and privacy laws worldwide. We specifically support compliance with: -

-
    -
  • Health Insurance Portability and Accountability Act (HIPAA): By ensuring EMS and healthcare-related incident data remains entirely offline and secure on the local device.
  • -
  • California Consumer Privacy Act (CCPA) / CPRA: No consumer data is shared or sold. The local architecture defaults to maximum privacy.
  • -
  • General Data Protection Regulation (GDPR): Complete data minimization and localized processing ensures that no unauthorized cross-border data transfers occur, and data subjects' rights are easily managed locally.
  • -
-
- -
-

4. Contact Us

-

- If you have any questions about this Privacy Policy or how FireForm handles data, please contact the core maintainers via our GitHub Repository. -

-
- -
-
- - - - diff --git a/docs/styles.css b/docs/styles.css deleted file mode 100644 index 4f3dae70..00000000 --- a/docs/styles.css +++ /dev/null @@ -1,488 +0,0 @@ -:root { - --bg-dark: #0f0a0a; - --bg-card: rgba(20, 10, 10, 0.6); - --bg-card-hover: rgba(40, 15, 15, 0.8); - --text-primary: #ffffff; - --text-secondary: #ffb8b8; - --accent-red: #ff3b30; - --accent-orange: #ff9500; - --border-color: rgba(255, 59, 48, 0.2); - --glow-color: rgba(255, 59, 48, 0.4); - --font-main: 'Inter', system-ui, -apple-system, sans-serif; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - background-color: var(--bg-dark); - color: var(--text-primary); - font-family: var(--font-main); - line-height: 1.6; - overflow-x: hidden; - min-height: 100vh; - display: flex; - flex-direction: column; -} - -.container { - width: 100%; - max-width: 1200px; - margin: 0 auto; - padding: 0 2rem; -} - -/* Background Effects */ -.background-effects { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - z-index: -1; - overflow: hidden; - pointer-events: none; -} - -.glow { - position: absolute; - width: 60vw; - height: 60vw; - border-radius: 50%; - filter: blur(100px); - opacity: 0.15; - animation: pulse 10s ease-in-out infinite alternate; -} - -.red-glow { - top: -20%; - right: -10%; - background: var(--accent-red); -} - -.orange-glow { - bottom: -20%; - left: -10%; - background: var(--accent-orange); - animation-delay: -5s; -} - -@keyframes pulse { - 0% { transform: scale(1) translate(0, 0); opacity: 0.15; } - 100% { transform: scale(1.1) translate(-5%, 5%); opacity: 0.25; } -} - -/* Navigation */ -.navbar { - padding: 1.5rem 0; - border-bottom: 1px solid var(--border-color); - background: rgba(15, 10, 10, 0.8); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - position: sticky; - top: 0; - z-index: 100; -} - -.navbar .container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.logo { - font-size: 1.5rem; - font-weight: 800; - letter-spacing: -0.5px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.fire-icon { - font-size: 1.8rem; - filter: drop-shadow(0 0 8px var(--accent-orange)); -} - -.github-link { - color: var(--text-primary); - text-decoration: none; - font-weight: 600; - padding: 0.5rem 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; - transition: all 0.3s ease; -} - -.github-link:hover { - background: var(--border-color); - box-shadow: 0 0 15px var(--glow-color); -} - -/* Hero Section */ -.hero { - flex: 1; - display: flex; - align-items: center; - padding: 6rem 0; -} - -.hero-content { - text-align: center; - max-width: 800px; - margin: 0 auto; -} - -.badge { - display: inline-block; - padding: 0.4rem 1rem; - background: rgba(255, 149, 0, 0.1); - border: 1px solid var(--accent-orange); - color: var(--accent-orange); - border-radius: 20px; - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 1.5rem; - box-shadow: 0 0 15px rgba(255, 149, 0, 0.2); -} - -.hero-title { - font-size: 4rem; - font-weight: 800; - line-height: 1.1; - margin-bottom: 1.5rem; - letter-spacing: -1px; -} - -.gradient-text { - background: linear-gradient(135deg, var(--accent-red), var(--accent-orange)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.hero-subtitle { - font-size: 1.25rem; - color: var(--text-secondary); - margin-bottom: 3rem; - font-weight: 300; -} - -/* Download Section */ -.download-section { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 24px; - padding: 3rem 2rem; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - box-shadow: 0 20px 40px rgba(0,0,0,0.4), inset 0 0 0 1px rgba(255,255,255,0.05); -} - -.download-heading { - font-size: 1.5rem; - margin-bottom: 2rem; - font-weight: 600; -} - -.button-group { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.btn { - display: flex; - align-items: center; - gap: 1rem; - padding: 1.25rem; - border-radius: 16px; - text-decoration: none; - transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); - background: rgba(25, 15, 15, 0.8); - border: 1px solid var(--border-color); - position: relative; - overflow: hidden; -} - -.btn::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(135deg, rgba(255,59,48,0.1), rgba(255,149,0,0.1)); - opacity: 0; - transition: opacity 0.3s ease; -} - -.btn:hover { - transform: translateY(-5px) scale(1.02); - border-color: var(--accent-orange); - box-shadow: 0 15px 30px rgba(255, 59, 48, 0.2); -} - -.btn:hover::before { - opacity: 1; -} - -.os-icon { - font-size: 2rem; - z-index: 1; -} - -.btn-text { - display: flex; - flex-direction: column; - align-items: flex-start; - z-index: 1; -} - -.os-name { - color: var(--text-primary); - font-weight: 600; - font-size: 1.1rem; -} - -.file-type { - color: var(--text-secondary); - font-size: 0.85rem; -} - -.view-all-link { - color: var(--accent-orange); - text-decoration: none; - font-size: 0.95rem; - font-weight: 600; - transition: color 0.2s ease; -} - -.view-all-link:hover { - color: var(--text-primary); -} - -/* Features Sections */ -.features-section { - margin-top: 5rem; -} - -.section-heading { - text-align: center; - font-size: 2.2rem; - margin-bottom: 2.5rem; - color: var(--text-primary); - font-weight: 800; -} - -.section-heading::after { - content: ''; - display: block; - width: 60px; - height: 4px; - background: linear-gradient(135deg, var(--accent-red), var(--accent-orange)); - margin: 1rem auto 0; - border-radius: 2px; -} - -.features-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 2rem; -} - -/* Timeline Styles */ -.timeline { - position: relative; - max-width: 800px; - margin: 0 auto; - padding: 2rem 0; -} -.timeline::before { - content: ''; - position: absolute; - left: 50%; - top: 0; - bottom: 0; - width: 2px; - background: linear-gradient(to bottom, var(--accent-red), var(--accent-orange)); - transform: translateX(-50%); -} -.timeline-item { - position: relative; - width: 50%; - padding: 1.5rem 3rem; -} -.timeline-item:nth-child(odd) { - left: 0; - text-align: right; -} -.timeline-item:nth-child(even) { - left: 50%; - text-align: left; -} -.timeline-dot { - position: absolute; - top: 50%; - width: 20px; - height: 20px; - background: var(--bg-dark); - border: 4px solid var(--accent-orange); - border-radius: 50%; - transform: translateY(-50%); - box-shadow: 0 0 10px var(--accent-orange); - z-index: 2; -} -.timeline-item:nth-child(odd) .timeline-dot { - right: -10px; -} -.timeline-item:nth-child(even) .timeline-dot { - left: -10px; -} -.timeline-content { - background: rgba(15, 10, 10, 0.4); - border: 1px dashed rgba(255, 149, 0, 0.3); - padding: 1.5rem; - border-radius: 16px; - transition: all 0.3s ease; -} -.timeline-item:hover .timeline-content { - background: rgba(255, 149, 0, 0.05); - border-style: solid; - border-color: var(--accent-orange); - transform: translateY(-5px); -} - -.feature-card { - background: rgba(15, 10, 10, 0.4); - border: 1px solid rgba(255, 255, 255, 0.05); - padding: 2rem; - border-radius: 16px; - text-align: left; - transition: transform 0.3s ease, border-color 0.3s ease; -} - -.feature-card:hover { - transform: translateY(-5px); - border-color: var(--border-color); -} - -.feature-icon { - font-size: 2.5rem; - margin-bottom: 1rem; -} - -.feature-card h3 { - margin-bottom: 0.5rem; - font-size: 1.2rem; -} - -.feature-card p { - color: var(--text-secondary); - font-size: 0.95rem; -} - -/* About Section Styles */ -.about-section { - margin-top: 5rem; - padding-bottom: 3rem; -} -.about-card { - background: linear-gradient(135deg, rgba(20, 10, 10, 0.8), rgba(40, 15, 15, 0.6)); - border: 1px solid var(--border-color); - border-radius: 24px; - padding: 3rem; - display: flex; - flex-direction: column; - gap: 2rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,59,48,0.1); -} -.about-content { - font-size: 1.1rem; - color: var(--text-secondary); -} -.about-content p { - margin-bottom: 1rem; -} -.about-content strong { - color: var(--text-primary); -} -.contact-links { - margin-top: 1rem; - padding-top: 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.05); -} -.contact-links h3 { - margin-bottom: 1rem; - font-size: 1.2rem; -} -.social-badges { - display: flex; - gap: 1rem; - flex-wrap: wrap; -} -.badge-link { - display: inline-flex; - align-items: center; - padding: 0.5rem 1rem; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - color: var(--text-primary); - text-decoration: none; - border-radius: 8px; - font-size: 0.95rem; - transition: all 0.2s ease; -} -.badge-link:hover { - background: rgba(255, 59, 48, 0.1); - border-color: var(--accent-red); - transform: translateY(-2px); -} - -/* Footer */ -footer { - text-align: center; - padding: 2rem 0; - border-top: 1px solid rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.4); - font-size: 0.9rem; -} - -/* Responsive */ -@media (max-width: 768px) { - .hero-title { - font-size: 2.8rem; - } - - .hero { - padding: 3rem 0; - } - - .download-section { - padding: 2rem 1.5rem; - } - - .timeline::before { - left: 20px; - } - .timeline-item { - width: 100%; - padding-left: 50px; - padding-right: 0; - } - .timeline-item:nth-child(odd), .timeline-item:nth-child(even) { - left: 0; - text-align: left; - } - .timeline-item:nth-child(odd) .timeline-dot, .timeline-item:nth-child(even) .timeline-dot { - left: 10px; - right: auto; - } - .about-card { - padding: 2rem 1.5rem; - } -} diff --git a/docs/terms.html b/docs/terms.html deleted file mode 100644 index 557c1277..00000000 --- a/docs/terms.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - FireForm - Terms of Service - - - - - - - -
-
-
-
- - - -
-
-
-

Terms of Service

-

- By downloading, installing, or using FireForm you agree to the following terms. Please read them carefully. If you do not agree, do not use the software. -

-

- Last updated: May 2026 -

-
-
-
- -
-
- -
-

1. Acceptance of Terms

-

- These Terms of Service ("Terms") govern your access to and use of the FireForm software application, source code, documentation, and related materials (collectively, the "Software"). By using the Software you confirm that you are at least 18 years old (or the age of majority in your jurisdiction) and have the legal authority to accept these Terms on behalf of yourself or the organization you represent. -

-
- -
-

2. Open-Source License

-

- FireForm is released under the MIT License. You are free to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software subject to the conditions stated in the LICENSE file included with every distribution and available in our GitHub repository. -

-

- Nothing in these Terms restricts rights granted to you by the MIT License; they are intended solely to clarify responsibilities and limitations not addressed by that license. -

-
- -
-

3. Permitted Use

-

You may use FireForm for any lawful purpose, including:

-
    -
  • Internal use by emergency services, public safety agencies, and fire departments.
  • -
  • Research, academic, and non-commercial projects.
  • -
  • Commercial deployments, provided you comply with the MIT License attribution requirements.
  • -
  • Developing derivative works or integrations, subject to the MIT License terms.
  • -
-
- -
-

4. Prohibited Use

-

You must not use FireForm to:

-
    -
  • Violate any applicable law or regulation, including but not limited to laws governing data protection, privacy, and public safety reporting.
  • -
  • Intentionally generate false, fraudulent, or misleading incident reports.
  • -
  • Infringe the intellectual property rights of any third party.
  • -
  • Introduce malware, exploits, or other harmful code into the Software or its dependencies.
  • -
  • Use the Software in a manner that endangers the health or safety of any person.
  • -
-
- -
-

5. No Warranty

-

- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THE FIREFORM CONTRIBUTORS DO NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE, UNINTERRUPTED, SECURE, OR THAT ANY DEFECTS WILL BE CORRECTED. USE OF THE SOFTWARE FOR CRITICAL PUBLIC-SAFETY OPERATIONS IS ENTIRELY AT YOUR OWN RISK AND YOUR ORGANIZATION'S DISCRETION. -

-
- -
-

6. Limitation of Liability

-

- TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE FIREFORM CONTRIBUTORS, MAINTAINERS, SPONSORS, OR AFFILIATED ORGANIZATIONS BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES — INCLUDING LOSS OF DATA, REVENUE, GOODWILL, OR LIFE — ARISING OUT OF OR IN CONNECTION WITH THE USE OR INABILITY TO USE THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN JURISDICTIONS THAT DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR LIMITATIONS OF LIABILITY, OUR LIABILITY IS LIMITED TO THE FULLEST EXTENT PERMITTED BY LAW. -

-
- -
-

7. User-Generated Content and Data

-

- FireForm processes data exclusively on your local device. You retain full ownership of all incident reports, templates, voice recordings, and any other data you create or import. We do not access, store, or transmit your data. -

-

- You are solely responsible for the accuracy and legality of the data you enter into FireForm and for complying with any reporting obligations imposed by your jurisdiction or regulatory authority. -

-
- -
-

8. Third-Party Components

-

- FireForm integrates with third-party software, including Ollama for local LLM inference and Whisper for voice transcription. Your use of those components is governed by their respective licenses and terms. We make no representations or warranties regarding third-party software and are not responsible for any issues arising from their use. -

-
- -
-

9. Contributions

-

- Contributions to FireForm (pull requests, issues, documentation, etc.) are subject to our Contributing Guidelines and Code of Conduct. By submitting a contribution you confirm that you have the right to license it under the MIT License and that you grant the FireForm project a perpetual, worldwide, royalty-free license to use, reproduce, and distribute your contribution. -

-
- -
-

10. Changes to These Terms

-

- We may update these Terms from time to time. When we do, we will revise the "Last updated" date at the top of this page and, for material changes, post a notice in our GitHub repository. Continued use of FireForm after changes are posted constitutes your acceptance of the revised Terms. -

-
- -
-

11. Contact

-

- If you have questions about these Terms, please reach out to the core maintainers via our GitHub Repository. We welcome feedback from deploying organizations and community members. -

-
- -
-
- - - - From a318ff976ab668e8cc0b6792c35fc4e2e93aafb5 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sun, 7 Jun 2026 20:33:01 +0530 Subject: [PATCH 11/85] added entrypoint, readme and updated docker file and compose --- .gitignore | 3 ++- docker/.env.example | 27 +++++++++++++++++++++++++++ docker/README.md | 31 +++++++++++++++++++++++++++++++ docker/dev/compose.yml | 1 + docker/entrypoint.sh | 10 ++++++++++ docker/prod/Dockerfile | 2 ++ 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 docker/.env.example create mode 100644 docker/README.md create mode 100755 docker/entrypoint.sh diff --git a/.gitignore b/.gitignore index 268a0ff2..1515bf38 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ frontend/release/ # Local Claude Code instructions CLAUDE.md -*temp/ \ No newline at end of file +*temp/ +*.env* \ No newline at end of file diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 00000000..c4321ac7 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,27 @@ +# Copy this file to .env.dev or .env.prod and fill in values. +# Never commit .env.dev or .env.prod — they are gitignored. + +# --- App ------------------------------------------------------------------ +APP_PORT=8000 + +# --- Ollama --------------------------------------------------------------- +# Ollama runs in Docker with port mapped to host. App runs on host. +# Override to point at an external Ollama instance (e.g. a GPU server). +OLLAMA_HOST=http://localhost:11434 +OLLAMA_MODEL=qwen2.5:1.5b +OLLAMA_TIMEOUT=300 + +# --- Whisper -------------------------------------------------------------- +# Whisper runs in Docker with port mapped to host. App runs on host. +# Override to point at an external Whisper endpoint. +WHISPER_HOST=http://localhost:9000 +WHISPER_MODEL=small.en + +# --- Gunicorn (prod only) ------------------------------------------------- +GUNICORN_WORKERS=2 + +# --- CORS ----------------------------------------------------------------- +# Comma-separated list of allowed frontend origins. +# Dev: http://localhost:5173,http://127.0.0.1:5173 +# Prod: https://your-domain.com +FRONTEND_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..e238c640 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,31 @@ +# Docker + +``` +docker/ + dev/ + Dockerfile # uvicorn --reload, source-mounted for hot reload + compose.yml + prod/ + Dockerfile # multi-stage build, gunicorn, no source mount + compose.yml + .env.example # template — copy to .env.dev or .env.prod + .env.dev # gitignored, dev values + .env.prod # gitignored, prod values + entrypoint.sh # runs DB migrations then exec's the server command + README.md +``` + +## Env vars + +See `.env.example` for the full list with descriptions. + +## Volumes + +`docker compose down` never removes volumes. Use `docker compose down -v` only to intentionally wipe all data. + +| Volume | Path in container | Purpose | +| ------------------ | ---------------------- | ----------------------------------- | +| `fireform_db` | `/data/db/fireform.db` | SQLite database | +| `fireform_uploads` | `/data/uploads` | Uploaded templates + generated PDFs | +| `ollama_data` | `/root/.ollama` | Ollama model weights | +| `whisper_models` | `/data/whisper` | Whisper model cache | diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml index 5b7c2f04..cdb94ccd 100644 --- a/docker/dev/compose.yml +++ b/docker/dev/compose.yml @@ -45,6 +45,7 @@ services: condition: service_healthy whisper: condition: service_started + entrypoint: ["sh", "docker/entrypoint.sh"] command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] volumes: - ../..:/app diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 00000000..84a81429 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +# Ensure data directories exist (volumes may be empty on first run) +mkdir -p /data/db /data/uploads + +# Run DB migrations / init before starting the server +python3 -m app.db.init_db + +exec "$@" diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 8ad8deab..f97d6dd5 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -22,6 +22,7 @@ COPY --from=builder /install /usr/local # Copy only app code, not data/ temp/ tests/ docs/ etc. COPY app/ ./app/ COPY requirements.txt . +COPY docker/entrypoint.sh /entrypoint.sh ENV PYTHONPATH=/app @@ -30,6 +31,7 @@ RUN mkdir -p /data/db /data/uploads && chmod +x /entrypoint.sh EXPOSE 8000 +ENTRYPOINT ["/entrypoint.sh"] CMD ["gunicorn", "app.main:app", \ "--worker-class", "uvicorn.workers.UvicornWorker", \ "--bind", "0.0.0.0:8000", \ From eb20a9570b70559ad6dd92f665378ab53a8b4ad2 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sun, 7 Jun 2026 20:44:30 +0530 Subject: [PATCH 12/85] fixed tests, replaced api -> app.api following our current code structure --- tests/test_api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 33b2c620..e98d16fd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -125,7 +125,7 @@ def test_upload_pdf(self, client, pdf_upload, tmp_path, monkeypatch): # Point the upload directory inside tmp_path (which is inside the project # for the path-safety check — we monkeypatch the check). monkeypatch.setattr( - "api.routes.templates.PROJECT_ROOT", + "app.api.routes.templates.PROJECT_ROOT", tmp_path, ) resp = client.post( @@ -234,7 +234,7 @@ def fake_post(url, params=None, files=None, timeout=None): captured["files"] = files return fake_response - monkeypatch.setattr("api.routes.forms.requests.post", fake_post) + monkeypatch.setattr("app.api.routes.forms.requests.post", fake_post) audio = ("audio", ("recording.wav", io.BytesIO(b"RIFFfake"), "audio/wav")) resp = client.post("/forms/transcribe", files=[audio]) @@ -252,7 +252,7 @@ def test_list_models(self, client, monkeypatch): fake_response = MagicMock() fake_response.json.return_value = {"models": [{"name": "qwen2.5:3b"}, {"name": "mistral:latest"}]} fake_response.raise_for_status.return_value = None - monkeypatch.setattr("api.routes.forms.requests.get", lambda *a, **k: fake_response) + monkeypatch.setattr("app.api.routes.forms.requests.get", lambda *a, **k: fake_response) monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:1.5b") resp = client.get("/forms/models") @@ -269,7 +269,7 @@ def test_list_models_ollama_down(self, client, monkeypatch): def boom(*a, **k): raise requests.exceptions.ConnectionError("down") - monkeypatch.setattr("api.routes.forms.requests.get", boom) + monkeypatch.setattr("app.api.routes.forms.requests.get", boom) monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:1.5b") resp = client.get("/forms/models") @@ -296,7 +296,7 @@ def test_transcribe_service_unavailable(self, client, monkeypatch): def fake_post(*args, **kwargs): raise requests.exceptions.ConnectionError("no service") - monkeypatch.setattr("api.routes.forms.requests.post", fake_post) + monkeypatch.setattr("app.api.routes.forms.requests.post", fake_post) audio = ("audio", ("recording.wav", io.BytesIO(b"data"), "audio/wav")) resp = client.post("/forms/transcribe", files=[audio]) @@ -315,7 +315,7 @@ class TestE2EPipeline: def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypatch, db): # -- Step 1: Upload a PDF -- - monkeypatch.setattr("api.routes.templates.PROJECT_ROOT", tmp_path) + monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path) upload_resp = client.post( "/templates/upload", files=[pdf_upload], From b4f6c709f3b3e044b37a041a4ac24ef1356849b5 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 9 Jun 2026 03:46:45 +0530 Subject: [PATCH 13/85] added API contracts --- contracts/openapi.yaml | 98 +++ contracts/path/extraction.yaml | 313 ++++++++ contracts/path/forms.yaml | 348 ++++++++ contracts/path/incidents.yaml | 313 ++++++++ contracts/path/input.yaml | 247 ++++++ contracts/path/jobs.yaml | 79 ++ contracts/path/reporting.yaml | 191 +++++ contracts/path/system.yaml | 152 ++++ contracts/path/templates.yaml | 267 +++++++ contracts/schemas/canonical-incident.yaml | 930 ++++++++++++++++++++++ contracts/schemas/common.yaml | 129 +++ contracts/schemas/enums.yaml | 130 +++ contracts/schemas/extraction-record.yaml | 134 ++++ contracts/schemas/form-record.yaml | 198 +++++ contracts/schemas/incident-record.yaml | 150 ++++ contracts/schemas/input-record.yaml | 138 ++++ contracts/schemas/reporting.yaml | 112 +++ contracts/schemas/system.yaml | 101 +++ contracts/schemas/template.yaml | 144 ++++ 19 files changed, 4174 insertions(+) create mode 100644 contracts/openapi.yaml create mode 100644 contracts/path/extraction.yaml create mode 100644 contracts/path/forms.yaml create mode 100644 contracts/path/incidents.yaml create mode 100644 contracts/path/input.yaml create mode 100644 contracts/path/jobs.yaml create mode 100644 contracts/path/reporting.yaml create mode 100644 contracts/path/system.yaml create mode 100644 contracts/path/templates.yaml create mode 100644 contracts/schemas/canonical-incident.yaml create mode 100644 contracts/schemas/common.yaml create mode 100644 contracts/schemas/enums.yaml create mode 100644 contracts/schemas/extraction-record.yaml create mode 100644 contracts/schemas/form-record.yaml create mode 100644 contracts/schemas/incident-record.yaml create mode 100644 contracts/schemas/input-record.yaml create mode 100644 contracts/schemas/reporting.yaml create mode 100644 contracts/schemas/system.yaml create mode 100644 contracts/schemas/template.yaml diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml new file mode 100644 index 00000000..3b19ea3c --- /dev/null +++ b/contracts/openapi.yaml @@ -0,0 +1,98 @@ +openapi: 3.0.0 + +info: + title: FireForm API + version: 1.0.0 + description: | + Proposed API contract for FireForm as GSoC project 2026 by [chetanr25.in](https://chetanr25.in) + contact: + name: Chetan R + url: https://github.com/chetanr25 + email: chetan250204@gmail.com + +servers: + - url: http://localhost:8080 + description: Local FireForm instance (default) + +tags: + - name: input + description: Submit voice or text incident narratives + - name: extraction + description: AI-powered data extraction from narratives into canonical JSON + - name: forms + description: Generate agency-specific PDF forms from extracted data + - name: incidents + description: Manage incident records linking inputs, extractions, and forms + - name: reporting + description: Aggregate statistics and periodic report generation + - name: templates + description: Form template configuration and management + - name: system + description: Health checks, schema introspection, and system status + - name: jobs + description: Cross-cutting async job status polling + +paths: + # ── Layer 1: Input ────────────────────────────────────────────── + /api/v1/input/voice: + $ref: "path/input.yaml#/voice" + /api/v1/input/text: + $ref: "path/input.yaml#/text" + /api/v1/input/{input_id}: + $ref: "path/input.yaml#/input_by_id" + + # ── Layer 2: AI Extraction ───────────────────────────────────── + /api/v1/extract/{input_id}: + $ref: "path/extraction.yaml#/extract_by_input" + /api/v1/extract/{extract_id}: + $ref: "path/extraction.yaml#/extract_by_id" + /api/v1/extract/{extract_id}/validate: + $ref: "path/extraction.yaml#/validate" + + # ── Layer 3: Form Generation ─────────────────────────────────── + /api/v1/forms/generate/all: + $ref: "path/forms.yaml#/generate_all" + /api/v1/forms/generate/{form_type}: + $ref: "path/forms.yaml#/generate_single" + /api/v1/forms/{form_id}: + $ref: "path/forms.yaml#/form_by_id" + /api/v1/forms/{form_id}/pdf: + $ref: "path/forms.yaml#/form_pdf" + /api/v1/forms/{form_id}/json: + $ref: "path/forms.yaml#/form_json" + /api/v1/forms/batch/{batch_id}: + $ref: "path/forms.yaml#/batch_by_id" + + # ── Layer 4: Incident Management ─────────────────────────────── + /api/v1/incidents: + $ref: "path/incidents.yaml#/incidents" + /api/v1/incidents/{incident_id}: + $ref: "path/incidents.yaml#/incident_by_id" + + # ── Layer 5: Reporting & Analytics ───────────────────────────── + /api/v1/reports/summary: + $ref: "path/reporting.yaml#/summary" + /api/v1/reports/generate: + $ref: "path/reporting.yaml#/generate" + /api/v1/reports/{report_id}: + $ref: "path/reporting.yaml#/report_by_id" + + # ── Layer 6: Templates & Configuration ───────────────────────── + /api/v1/templates: + $ref: "path/templates.yaml#/templates" + /api/v1/templates/{template_id}: + $ref: "path/templates.yaml#/template_by_id" + /api/v1/templates/{template_id}/fields: + $ref: "path/templates.yaml#/template_fields" + + # ── Layer 7: System ──────────────────────────────────────────── + /api/v1/health: + $ref: "path/system.yaml#/health" + /api/v1/schema/incident: + $ref: "path/system.yaml#/schema_incident" + /api/v1/schema/incident/versions: + $ref: "path/system.yaml#/schema_versions" + + # ── Layer 8: Async Jobs ──────────────────────────────────────── + /api/v1/jobs/{job_id}: + $ref: "path/jobs.yaml#/job_by_id" diff --git a/contracts/path/extraction.yaml b/contracts/path/extraction.yaml new file mode 100644 index 00000000..0e11309b --- /dev/null +++ b/contracts/path/extraction.yaml @@ -0,0 +1,313 @@ +# Layer 2 AI Extraction Endpoints +# POST /api/v1/extract/{input_id} +# GET /api/v1/extract/{extract_id} +# PATCH /api/v1/extract/{extract_id} +# POST /api/v1/extract/{extract_id}/validate + +extract_by_input: + post: + operationId: createExtraction + summary: Start AI extraction from input narrative + description: | + Sends the narrative (from a previously submitted input) to the local Ollama + LLM with a structured prompt to extract all incident fields into the canonical + FireForm JSON schema. This is an asynchronous operation the LLM may take + 30–120 seconds. Returns an extract_id and job_id for polling. + tags: + - extraction + parameters: + - name: input_id + in: path + required: true + description: ID of the input record to extract from + schema: + type: string + format: uuid + requestBody: + required: false + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ExtractionRequest" + example: + model_override: "llama3:8b" + extraction_hints: + incident_type: "wildland_fire" + state: "CA" + responses: + "202": + description: Extraction job queued successfully + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/AsyncJobResponse" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "processing" + input_id: "550e8400-e29b-41d4-a716-446655440001" + queued_at: "2024-07-15T14:30:00Z" + estimated_seconds: 60 + poll_url: "/api/v1/extract/550e8400-e29b-41d4-a716-446655440020" + "404": + description: Input ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INPUT_NOT_FOUND" + message: "Input with ID 550e8400-e29b-41d4-a716-446655440099 not found" + "409": + description: Conflict input not ready or extraction already exists + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + not_ready: + summary: Input still transcribing + value: + error_code: "INPUT_NOT_READY" + message: "Input is in 'transcribing' state. Wait until status is 'ready'." + detail: + current_status: "transcribing" + already_exists: + summary: Extraction already initiated for this input + value: + error_code: "EXTRACTION_EXISTS" + message: "An extraction already exists for this input" + detail: + existing_extract_id: "550e8400-e29b-41d4-a716-446655440020" + "503": + description: Ollama LLM service unavailable + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "LLM_UNAVAILABLE" + message: "Ollama LLM service is not available" + retry_after_seconds: 30 + detail: + ollama_status: "connection_refused" + +extract_by_id: + get: + operationId: getExtraction + summary: Get extraction result by ID + description: | + Returns the full canonical FireForm JSON when extraction is complete, or + the current job status while still processing. When status is "completed", + the response body contains the entire canonical incident schema. When still + processing, includes a retry_after_seconds hint for polling. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction + schema: + type: string + format: uuid + responses: + "200": + description: Extraction record (completed or in-progress) + content: + application/json: + schema: + oneOf: + - $ref: "../schemas/extraction-record.yaml#/ExtractionCompleted" + - $ref: "../schemas/extraction-record.yaml#/ExtractionProcessing" + examples: + completed: + summary: Extraction completed with canonical JSON + value: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "completed" + completed_at: "2024-07-15T14:31:05Z" + canonical_incident: + schema_version: "1.1.0" + schema_name: "fireform_canonical_incident" + extraction_metadata: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + confidence_score: 0.91 + incident: + name: "Bear Creek Wildfire" + processing: + summary: Extraction still in progress + value: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "processing" + started_at: "2024-07-15T14:30:00Z" + retry_after_seconds: 5 + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_NOT_FOUND" + message: "Extraction with ID 550e8400-e29b-41d4-a716-446655440099 not found" + + patch: + operationId: updateExtraction + summary: Manually correct extracted fields + description: | + Allows a responder to correct any field in the canonical JSON after LLM + extraction. Uses JSON Merge Patch (RFC 7396) only send the fields that + changed. The server records an audit trail of all changes vs the original + LLM output and recalculates completeness scores and applicable_forms. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction to update + schema: + type: string + format: uuid + requestBody: + required: true + description: JSON Merge Patch (RFC 7396) only include changed fields + content: + application/merge-patch+json: + schema: + $ref: "../schemas/canonical-incident.yaml#/CanonicalIncident" + example: + fire: + estimated_damage_usd: 250000 + casualties: + total_responder_injuries: 2 + responses: + "200": + description: Extraction updated returns full updated canonical JSON + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ExtractionCompleted" + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Extraction is locked (already submitted) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_LOCKED" + message: "Cannot modify extraction incident report has been submitted" + detail: + report_status: "submitted" + submitted_at: "2024-07-15T18:00:00Z" + "422": + description: Invalid field path or value + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Invalid field path or value in patch" + validation_errors: + - field: "fire.cause_certainty" + issue: "Must be one of: confirmed, probable, suspected, undetermined" + value: "maybe" + +validate: + post: + operationId: validateExtraction + summary: Validate extraction against a form's requirements + description: | + Validates the canonical JSON against a specific form type's field requirements. + Returns whether the extraction has all required fields, which recommended + fields are missing, and any warnings. Useful for checking "can I generate + a NERIS report with what I have?" before triggering form generation. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction to validate + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - form_type + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + example: + form_type: "neris" + responses: + "200": + description: Validation result + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ValidationResult" + example: + valid: true + form_type: "neris" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + missing_required: [] + missing_recommended: + - "fire.detector_present" + - "fire.detector_operated" + warnings: + - "fire.estimated_damage_usd is null NERIS recommends providing damage estimates" + field_coverage_percent: 94 + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Unknown form type + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "UNKNOWN_FORM_TYPE" + message: "Form type 'xyz' is not recognized" + detail: + valid_form_types: + - neris + - nemsis_epcr + - nibrs + - nfirs_basic + - nfirs_fire + - nfirs_structure + - nfirs_wildland + - nfirs_ems + - nfirs_hazmat + - nfirs_apparatus + - nfirs_personnel + - nfirs_arson + - nfirs_casualty_civilian + - nfirs_casualty_responder + - cal_fire_ics209 + - osha_301 + - un_ssirs + - state_georgia + - state_california + - state_new_york diff --git a/contracts/path/forms.yaml b/contracts/path/forms.yaml new file mode 100644 index 00000000..b920b41a --- /dev/null +++ b/contracts/path/forms.yaml @@ -0,0 +1,348 @@ +# Layer 3 Form Generation Endpoints +# POST /api/v1/forms/generate/all +# POST /api/v1/forms/generate/{form_type} +# GET /api/v1/forms/{form_id} +# GET /api/v1/forms/{form_id}/pdf +# GET /api/v1/forms/{form_id}/json +# GET /api/v1/forms/batch/{batch_id} + +generate_all: + post: + operationId: generateAllForms + summary: Generate all applicable forms from an extraction + description: | + Triggers batch generation of ALL forms listed in the extraction's + extraction_metadata.applicable_forms. This is an async batch job. + Use skip_incomplete to skip forms that fail validation, or force_partial + to generate forms with blank fields where data is missing. + tags: + - forms + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/GenerateAllRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + options: + skip_incomplete: true + force_partial: false + responses: + "202": + description: Batch form generation job accepted + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/BatchGenerateResponse" + example: + batch_id: "550e8400-e29b-41d4-a716-446655440030" + status: "processing" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + forms_queued: + - "neris" + - "nfirs_basic" + - "nfirs_wildland" + forms_skipped: + - form_type: "nemsis_epcr" + reason: "Missing required fields: ems.patients[0].date_of_birth" + estimated_seconds: 45 + poll_url: "/api/v1/forms/batch/550e8400-e29b-41d4-a716-446655440030" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Extraction not yet completed + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_NOT_COMPLETED" + message: "Extraction is still processing. Wait until status is 'completed'." + detail: + current_status: "processing" + "422": + description: No applicable forms found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "NO_APPLICABLE_FORMS" + message: "No forms are applicable for this extraction's incident type" + +generate_single: + post: + operationId: generateSingleForm + summary: Generate one specific form type + description: | + Generates a single agency-specific form from the canonical extraction data. + The form_type path parameter specifies which form to generate. If the extraction + is missing required fields for this form, returns 422 unless force_partial is true. + tags: + - forms + parameters: + - name: form_type + in: path + required: true + description: Type of form to generate + schema: + $ref: "../schemas/enums.yaml#/FormType" + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/GenerateSingleRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + options: + output_format: "pdf" + force_partial: false + responses: + "202": + description: Form generation job accepted + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormGenerateResponse" + example: + form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "processing" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + job_id: "550e8400-e29b-41d4-a716-446655440097" + estimated_seconds: 15 + poll_url: "/api/v1/forms/550e8400-e29b-41d4-a716-446655440040" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Validation failure or form not in applicable list + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "FORM_VALIDATION_FAILED" + message: "Extraction is missing required fields for NERIS form" + detail: + form_type: "neris" + missing_fields: + - "incident.types[0].neris_code" + - "location.coordinates" + validation_errors: + - field: "incident.types[0].neris_code" + issue: "Required field is null" + +form_by_id: + get: + operationId: getForm + summary: Get form metadata and status + description: | + Returns the form record including generation status, associated extract and + incident IDs, and a field_mapping_summary showing how canonical fields were + mapped to the form's agency-specific fields (for audit/transparency). + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the generated form + schema: + type: string + format: uuid + responses: + "200": + description: Form record found + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormRecord" + example: + form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_id: "550e8400-e29b-41d4-a716-446655440050" + created_at: "2024-07-15T14:32:00Z" + completed_at: "2024-07-15T14:32:12Z" + pdf_ready: true + json_ready: true + field_mapping_summary: + total_form_fields: 85 + fields_filled: 72 + fields_blank: 13 + coverage_percent: 84.7 + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +form_pdf: + get: + operationId: downloadFormPdf + summary: Download filled PDF form + description: | + Returns the filled PDF binary file for the specified form. If the form + generation is still in progress, returns 202 Accepted with a retry_after + hint. The Content-Disposition header is set for browser download. + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the form to download + schema: + type: string + format: uuid + responses: + "200": + description: Filled PDF file + content: + application/pdf: + schema: + type: string + format: binary + headers: + Content-Disposition: + description: Attachment filename for download + schema: + type: string + example: 'attachment; filename="NERIS_FF-2024-CA-0157.pdf"' + "202": + description: Form generation still in progress + content: + application/json: + schema: + type: object + properties: + message: + type: string + status: + type: string + retry_after_seconds: + type: integer + example: + message: "Form generation is still in progress" + status: "processing" + retry_after_seconds: 5 + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "500": + description: PDF generation failed + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "PDF_GENERATION_FAILED" + message: "Failed to generate PDF for form" + detail: + reason: "Template file corrupted or missing" + +form_json: + get: + operationId: getFormJson + summary: Get form-specific field-mapped JSON + description: | + Returns the form-specific JSON with fields mapped to the agency's expected + format not the canonical FireForm schema, but the actual field names and + structure that the target agency system expects. Useful for future direct + API submission to agency systems. + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the form + schema: + type: string + format: uuid + responses: + "200": + description: Form-specific mapped JSON + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormMappedJson" + example: + form_type: "neris" + form_version: "2.0" + form_id: "550e8400-e29b-41d4-a716-446655440040" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + agency_fields: + incident_type: "wildland-fire" + incident_date: "2024-07-10" + alarm_time: "13:52" + acres_burned: 1247 + cause: "natural" + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +batch_by_id: + get: + operationId: getBatchStatus + summary: Get batch form generation status + description: | + Returns the status of a batch form generation job including progress + for each individual form. Poll this endpoint after POST /forms/generate/all. + tags: + - forms + parameters: + - name: batch_id + in: path + required: true + description: Unique identifier of the batch job + schema: + type: string + format: uuid + responses: + "200": + description: Batch job status + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/BatchStatus" + example: + batch_id: "550e8400-e29b-41d4-a716-446655440030" + status: "completed" + total: 3 + completed: 3 + failed: 0 + forms: + - form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + - form_id: "550e8400-e29b-41d4-a716-446655440041" + form_type: "nfirs_basic" + status: "completed" + - form_id: "550e8400-e29b-41d4-a716-446655440042" + form_type: "nfirs_wildland" + status: "completed" + "404": + description: Batch job not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/path/incidents.yaml b/contracts/path/incidents.yaml new file mode 100644 index 00000000..810a311a --- /dev/null +++ b/contracts/path/incidents.yaml @@ -0,0 +1,313 @@ +# Layer 4 Incident Management Endpoints +# POST /api/v1/incidents +# GET /api/v1/incidents +# GET /api/v1/incidents/{incident_id} +# PATCH /api/v1/incidents/{incident_id} +# DELETE /api/v1/incidents/{incident_id} + +incidents: + post: + operationId: createIncident + summary: Create a full incident record + description: | + Creates a permanent incident record that links input, extraction, and + generated forms into a single coherent unit. Assigns a permanent incident_id + and stores the complete incident for future retrieval and reporting. + tags: + - incidents + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/CreateIncidentRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_number: "CA-SQF-2024-0421" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + responses: + "201": + description: Incident record created + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecord" + example: + incident_id: "550e8400-e29b-41d4-a716-446655440050" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_number: "CA-SQF-2024-0421" + status: "draft" + forms_generated: + - form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + created_at: "2024-07-15T14:35:00Z" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Duplicate incident number + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "DUPLICATE_INCIDENT_NUMBER" + message: "Incident number CA-SQF-2024-0421 already exists" + detail: + existing_incident_id: "550e8400-e29b-41d4-a716-446655440049" + + get: + operationId: listIncidents + summary: List incidents with filtering + description: | + Returns a paginated list of incident records with optional filtering by + date range, incident type, and status. Supports sorting by date. + Maximum 100 results per page. + tags: + - incidents + parameters: + - name: date_from + in: query + description: Start date filter (inclusive, ISO 8601) + schema: + type: string + format: date + - name: date_to + in: query + description: End date filter (inclusive, ISO 8601) + schema: + type: string + format: date + - name: incident_type + in: query + description: Filter by incident category + schema: + $ref: "../schemas/enums.yaml#/IncidentCategory" + - name: status + in: query + description: Filter by report status + schema: + $ref: "../schemas/enums.yaml#/ReportStatus" + - name: page + in: query + description: Page number (1-based) + schema: + type: integer + minimum: 1 + default: 1 + - name: per_page + in: query + description: Items per page (max 100) + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: sort + in: query + description: Sort order + schema: + type: string + enum: + - date_asc + - date_desc + default: date_desc + responses: + "200": + description: Paginated list of incidents + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentListResponse" + example: + data: + - incident_id: "550e8400-e29b-41d4-a716-446655440050" + incident_number: "CA-SQF-2024-0421" + status: "draft" + incident_name: "Bear Creek Wildfire" + incident_type: "fire" + incident_date: "2024-07-10" + forms_count: 3 + created_at: "2024-07-15T14:35:00Z" + pagination: + total: 42 + page: 1 + per_page: 20 + total_pages: 3 + has_next: true + has_prev: false + "422": + description: Invalid query parameter format + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Invalid date format for date_from" + validation_errors: + - field: "date_from" + issue: "Must be ISO 8601 date format (YYYY-MM-DD)" + value: "15/07/2024" + +incident_by_id: + get: + operationId: getIncident + summary: Get full incident record + description: | + Returns the complete incident record including the linked canonical + extraction, all generated forms and their statuses, submission log, + and audit trail. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident + schema: + type: string + format: uuid + responses: + "200": + description: Full incident record + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecordFull" + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + + patch: + operationId: updateIncident + summary: Update incident metadata + description: | + Updates incident metadata such as status, tags, incident number, and notes. + Does NOT allow changing the underlying extraction data use PATCH /extract + for that. Submitted incidents cannot be modified. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident to update + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/UpdateIncidentRequest" + example: + status: "approved" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + - "reviewed" + notes: "Reviewed by BC Wilson. Ready for submission." + responses: + "200": + description: Incident updated + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecord" + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Cannot modify a submitted incident + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INCIDENT_SUBMITTED" + message: "Cannot modify an incident that has been submitted" + detail: + submitted_at: "2024-07-15T18:00:00Z" + + delete: + operationId: deleteIncident + summary: Soft-delete an incident + description: | + Performs a soft delete sets a deleted_at timestamp but never removes data. + Submitted incidents cannot be deleted. Soft-deleted incidents are excluded + from list queries by default but can be recovered. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident to delete + schema: + type: string + format: uuid + responses: + "200": + description: Incident soft-deleted + content: + application/json: + schema: + type: object + properties: + incident_id: + type: string + format: uuid + deleted_at: + type: string + format: date-time + recoverable: + type: boolean + example: + incident_id: "550e8400-e29b-41d4-a716-446655440050" + deleted_at: "2024-07-16T10:00:00Z" + recoverable: true + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Cannot delete already deleted or submitted + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + already_deleted: + summary: Incident already soft-deleted + value: + error_code: "ALREADY_DELETED" + message: "Incident has already been deleted" + detail: + deleted_at: "2024-07-16T09:00:00Z" + submitted: + summary: Submitted incidents cannot be deleted + value: + error_code: "INCIDENT_SUBMITTED" + message: "Submitted incidents cannot be deleted" diff --git a/contracts/path/input.yaml b/contracts/path/input.yaml new file mode 100644 index 00000000..a2d6a57a --- /dev/null +++ b/contracts/path/input.yaml @@ -0,0 +1,247 @@ +# Layer 1 Input Endpoints +# POST /api/v1/input/voice, POST /api/v1/input/text, GET /api/v1/input/{input_id} + +voice: + post: + operationId: submitVoiceInput + summary: Submit a voice recording for transcription + description: | + Accepts an audio file (voice memo) from a first responder along with optional + incident metadata. The audio is saved and queued for transcription via Whisper + running on the local Ollama instance. Returns an input_id immediately the + transcription runs asynchronously. Poll GET /api/v1/input/{input_id} for status. + tags: + - input + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - audio_file + properties: + audio_file: + type: string + format: binary + description: Audio file in wav, mp3, m4a, ogg, or webm format. Max 500MB. + station_id: + type: string + description: Station identifier (e.g. "STA-045") + responder_badge: + type: string + description: Badge number of the responder submitting the report + incident_date_hint: + type: string + format: date + description: Approximate date of the incident to aid extraction + example: + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-15" + responses: + "201": + description: Voice input accepted and queued for transcription + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/VoiceInputResponse" + example: + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "queued" + input_type: "voice" + estimated_processing_seconds: 30 + created_at: "2024-07-15T14:25:00Z" + job_id: "550e8400-e29b-41d4-a716-446655440099" + poll_url: "/api/v1/input/550e8400-e29b-41d4-a716-446655440001" + "400": + description: Malformed request (missing audio file or invalid form data) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "413": + description: Audio file exceeds 500MB limit + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "FILE_TOO_LARGE" + message: "Audio file exceeds maximum size of 500MB" + detail: + max_size_bytes: 524288000 + received_size_bytes: 600000000 + "415": + description: Unsupported audio format + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "UNSUPPORTED_FORMAT" + message: "Audio format not supported. Accepted formats: wav, mp3, m4a, ogg, webm" + detail: + accepted_formats: + - wav + - mp3 + - m4a + - ogg + - webm + "503": + description: Ollama/Whisper service unavailable + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "LLM_UNAVAILABLE" + message: "Ollama transcription service is not available" + retry_after_seconds: 30 + +text: + post: + operationId: submitTextInput + summary: Submit a text narrative for processing + description: | + Accepts a free-text incident narrative from a first responder. This is + synchronous the text is stored immediately and the input is marked as + "ready" for extraction. No transcription step needed. + tags: + - input + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/TextInputRequest" + example: + narrative: > + Responded to a wildfire at Bear Creek Trailhead around 1:45 PM on July 10th. + Lightning strike ignited timber litter in mixed conifer and chaparral. Fire + spread rapidly northeast driven by 15-25 mph SW winds. We deployed 18 engines, + 6 water tenders, and 3 helicopters. 247 personnel total. One firefighter + treated for heat exhaustion, returned to duty July 12. 1247 acres burned across + federal, state, and private land. 47 structures threatened, 3 damaged, none + destroyed. Fire contained July 14, controlled July 15. + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-10" + responses: + "201": + description: Text input accepted and ready for extraction + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/TextInputResponse" + example: + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "ready" + input_type: "text" + character_count: 612 + word_count: 98 + created_at: "2024-07-15T14:25:00Z" + "400": + description: Malformed JSON request body + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "413": + description: Narrative exceeds 50,000 character limit + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "NARRATIVE_TOO_LONG" + message: "Narrative exceeds maximum length of 50,000 characters" + detail: + max_characters: 50000 + received_characters: 52000 + "422": + description: Validation error (empty or too-short narrative) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Narrative is too short to extract meaningful incident data" + validation_errors: + - field: "narrative" + issue: "Must contain at least 10 words" + value: "Fire happened" + +input_by_id: + get: + operationId: getInput + summary: Get input record by ID + description: | + Returns the full input record including the original text or transcript, + processing status for voice inputs, and metadata. For voice inputs, poll + this endpoint until status transitions from "queued"/"transcribing" to "ready". + tags: + - input + parameters: + - name: input_id + in: path + required: true + description: Unique identifier of the input record + schema: + type: string + format: uuid + responses: + "200": + description: Input record found + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/InputRecord" + examples: + voice_ready: + summary: Voice input with completed transcription + value: + input_id: "550e8400-e29b-41d4-a716-446655440001" + input_type: "voice" + status: "ready" + transcript: "Responded to a wildfire at Bear Creek..." + original_filename: "incident_memo.m4a" + audio_duration_seconds: 180 + character_count: 612 + word_count: 98 + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-10" + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:30Z" + voice_processing: + summary: Voice input still transcribing + value: + input_id: "550e8400-e29b-41d4-a716-446655440001" + input_type: "voice" + status: "transcribing" + transcript: null + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:10Z" + retry_after_seconds: 5 + text_ready: + summary: Text input ready for extraction + value: + input_id: "550e8400-e29b-41d4-a716-446655440002" + input_type: "text" + status: "ready" + transcript: "Responded to a wildfire at Bear Creek..." + character_count: 612 + word_count: 98 + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:00Z" + "404": + description: Input record not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INPUT_NOT_FOUND" + message: "Input with ID 550e8400-e29b-41d4-a716-446655440099 not found" diff --git a/contracts/path/jobs.yaml b/contracts/path/jobs.yaml new file mode 100644 index 00000000..2ac042f6 --- /dev/null +++ b/contracts/path/jobs.yaml @@ -0,0 +1,79 @@ +# Layer 8 Async Job Status (Cross-cutting) +# GET /api/v1/jobs/{job_id} + +job_by_id: + get: + operationId: getJobStatus + summary: Get async job status + description: | + Universal job status endpoint for any asynchronous operation in FireForm — + transcription, LLM extraction, form generation, and report generation. + Returns the current status, progress percentage, and a result URL when + the job completes. Poll this endpoint for long-running operations. + tags: + - jobs + parameters: + - name: job_id + in: path + required: true + description: Unique identifier of the async job + schema: + type: string + format: uuid + responses: + "200": + description: Job status + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/Job" + examples: + queued: + summary: Job waiting in queue + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "queued" + progress_percent: 0 + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:30:00Z" + processing: + summary: Job currently processing + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "processing" + progress_percent: 45 + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:30:30Z" + completed: + summary: Job completed successfully + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "completed" + progress_percent: 100 + result_url: "/api/v1/extract/550e8400-e29b-41d4-a716-446655440020" + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:31:05Z" + failed: + summary: Job failed with error + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "failed" + progress_percent: 23 + error: + error_code: "LLM_TIMEOUT" + message: "Ollama did not respond within 120 seconds" + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:32:00Z" + "404": + description: Job not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "JOB_NOT_FOUND" + message: "Job with ID 550e8400-e29b-41d4-a716-446655440099 not found" diff --git a/contracts/path/reporting.yaml b/contracts/path/reporting.yaml new file mode 100644 index 00000000..ad101723 --- /dev/null +++ b/contracts/path/reporting.yaml @@ -0,0 +1,191 @@ +# Layer 5 Reporting & Analytics Endpoints +# GET /api/v1/reports/summary +# POST /api/v1/reports/generate +# GET /api/v1/reports/{report_id} + +summary: + get: + operationId: getReportSummary + summary: Get aggregate incident statistics + description: | + Returns aggregate statistics for a dashboard view total incidents, + breakdown by type and status, forms generated, average completeness + scores, and average processing times. Supports filtering by date range + and grouping by time period or incident type. + tags: + - reporting + parameters: + - name: date_from + in: query + description: Start date (inclusive, ISO 8601) + schema: + type: string + format: date + - name: date_to + in: query + description: End date (inclusive, ISO 8601) + schema: + type: string + format: date + - name: group_by + in: query + description: Group results by time period or incident type + schema: + type: string + enum: + - day + - week + - month + - incident_type + default: month + responses: + "200": + description: Aggregate statistics + content: + application/json: + schema: + $ref: "../schemas/reporting.yaml#/ReportSummary" + example: + date_from: "2024-01-01" + date_to: "2024-07-15" + total_incidents: 157 + by_type: + fire: 42 + ems: 68 + rescue: 12 + hazardous_conditions: 8 + service_call: 15 + good_intent: 7 + false_alarm: 5 + by_status: + draft: 3 + under_review: 2 + approved: 12 + submitted: 140 + forms_generated: 489 + avg_completeness_score: 88.3 + avg_processing_time_seconds: 47.2 + groups: + - period: "2024-07" + incident_count: 23 + forms_generated: 71 + +generate: + post: + operationId: generateReport + summary: Generate a periodic report + description: | + Generates a periodic (monthly, quarterly, or annual) aggregate report + as PDF or JSON. This includes statistics, incident summaries, and + compliance metrics required by agencies like USFA/NERIS. This is an + async operation returns a report_id for polling. + + Note: USFA encourages monthly submission but requires quarterly at minimum. + This endpoint supports generating reports aligned with those cadences. + tags: + - reporting + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/reporting.yaml#/GenerateReportRequest" + example: + period_type: "monthly" + year: 2024 + month: 7 + format: "pdf" + responses: + "202": + description: Report generation job accepted + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/AsyncJobResponse" + example: + job_id: "550e8400-e29b-41d4-a716-446655440096" + job_type: "report_generation" + status: "processing" + estimated_seconds: 30 + poll_url: "/api/v1/reports/550e8400-e29b-41d4-a716-446655440060" + report_id: "550e8400-e29b-41d4-a716-446655440060" + "422": + description: Invalid period or future date + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + future_period: + summary: Future period requested + value: + error_code: "FUTURE_PERIOD" + message: "Cannot generate a report for a future period" + invalid_quarter: + summary: Invalid quarter value + value: + error_code: "VALIDATION_ERROR" + message: "Invalid quarter value" + validation_errors: + - field: "quarter" + issue: "Must be 1, 2, 3, or 4" + value: 5 + +report_by_id: + get: + operationId: getReport + summary: Retrieve a generated report + description: | + Returns a previously generated periodic report. If the report is still + being generated, returns 202 Accepted with a retry hint. When complete, + returns the report in the originally requested format (PDF binary or JSON). + tags: + - reporting + parameters: + - name: report_id + in: path + required: true + description: Unique identifier of the generated report + schema: + type: string + format: uuid + responses: + "200": + description: Generated report (PDF or JSON depending on original request) + content: + application/pdf: + schema: + type: string + format: binary + application/json: + schema: + $ref: "../schemas/reporting.yaml#/PeriodicReport" + headers: + Content-Disposition: + description: Attachment filename for PDF downloads + schema: + type: string + example: 'attachment; filename="FireForm_Monthly_2024-07.pdf"' + "202": + description: Report still being generated + content: + application/json: + schema: + type: object + properties: + message: + type: string + status: + type: string + retry_after_seconds: + type: integer + example: + message: "Report generation is still in progress" + status: "processing" + retry_after_seconds: 10 + "404": + description: Report not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/path/system.yaml b/contracts/path/system.yaml new file mode 100644 index 00000000..cd00901c --- /dev/null +++ b/contracts/path/system.yaml @@ -0,0 +1,152 @@ +# Layer 7 System Endpoints +# GET /api/v1/health +# GET /api/v1/schema/incident +# GET /api/v1/schema/incident/versions + +health: + get: + operationId: getHealth + summary: System health check + description: | + Returns the health status of all FireForm components — database, Ollama LLM + (including loaded models, GPU status, and current load), Whisper transcription, + and file storage. Returns 200 even when degraded (so load balancers don't kill + it), 503 only if the system is truly unhealthy and cannot serve any requests. + tags: + - system + responses: + "200": + description: System is healthy or degraded + content: + application/json: + schema: + $ref: "../schemas/system.yaml#/HealthStatus" + examples: + healthy: + summary: All systems operational + value: + status: "healthy" + version: "1.0.0" + uptime_seconds: 86400 + components: + database: + status: "healthy" + response_time_ms: 2 + ollama: + status: "healthy" + response_time_ms: 15 + model_loaded: "llama3:8b" + ollama_version: "0.3.0" + models_available: + - name: "llama3:8b" + size_gb: 4.7 + quantization: "Q4_K_M" + loaded: true + - name: "mistral:7b" + size_gb: 4.1 + quantization: "Q4_0" + loaded: false + current_load: + active_requests: 1 + queued_requests: 0 + whisper: + status: "healthy" + response_time_ms: 10 + storage: + status: "healthy" + disk_free_gb: 120.5 + degraded: + summary: Some components have issues + value: + status: "degraded" + version: "1.0.0" + uptime_seconds: 3600 + components: + database: + status: "healthy" + response_time_ms: 2 + ollama: + status: "degraded" + response_time_ms: 5000 + detail: "High latency model may be loading" + current_load: + active_requests: 3 + queued_requests: 2 + whisper: + status: "unhealthy" + detail: "Whisper model not loaded" + storage: + status: "healthy" + disk_free_gb: 120.5 + "503": + description: System is unhealthy cannot serve requests + content: + application/json: + schema: + $ref: "../schemas/system.yaml#/HealthStatus" + example: + status: "unhealthy" + version: "1.0.0" + uptime_seconds: 10 + components: + database: + status: "unhealthy" + detail: "Connection refused" + ollama: + status: "unhealthy" + detail: "Connection refused" + +schema_incident: + get: + operationId: getIncidentSchema + summary: Get the canonical FireForm JSON Schema + description: | + Returns the full canonical FireForm incident JSON Schema (JSON Schema + draft-07). Clients can use this to validate incident data locally before + submitting. This is a schema-as-API pattern for interoperability. + tags: + - system + responses: + "200": + description: Full JSON Schema document + content: + application/json: + schema: + type: object + description: JSON Schema draft-07 document for the canonical incident model + example: + $schema: "http://json-schema.org/draft-07/schema#" + title: "FireForm Canonical Incident" + type: "object" + properties: + schema_version: + type: "string" + +schema_versions: + get: + operationId: getSchemaVersions + summary: Get schema version history + description: | + Returns the version history of the canonical FireForm incident schema, + including changelogs and whether each version introduced breaking changes. + Useful for clients tracking schema evolution across FireForm updates. + tags: + - system + responses: + "200": + description: Schema version history + content: + application/json: + schema: + type: array + items: + $ref: "../schemas/system.yaml#/SchemaVersion" + example: + - version: "1.1.0" + released_at: "2026-02-01T00:00:00Z" + changelog: "Added NERIS fields replacing legacy NFIRS. Added wildland.aerial_operations. Updated incident.types to include neris_code." + breaking_changes: true + - version: "1.0.0" + released_at: "2024-01-01T00:00:00Z" + changelog: "Initial canonical schema with NFIRS support." + breaking_changes: false diff --git a/contracts/path/templates.yaml b/contracts/path/templates.yaml new file mode 100644 index 00000000..dab9dca5 --- /dev/null +++ b/contracts/path/templates.yaml @@ -0,0 +1,267 @@ +# Layer 6 Template & Configuration Endpoints +# GET /api/v1/templates +# GET /api/v1/templates/{template_id} +# POST /api/v1/templates +# PUT /api/v1/templates/{template_id} +# GET /api/v1/templates/{template_id}/fields + +templates: + get: + operationId: listTemplates + summary: List all available form templates + description: | + Returns all registered form templates including built-in standard templates + (NERIS, NEMSIS, NIBRS, NFIRS modules, OSHA, etc.) and any custom templates + added for specific jurisdictions. Each template defines the fields, validation + rules, and mapping from the canonical FireForm schema. + tags: + - templates + responses: + "200": + description: List of all templates + content: + application/json: + schema: + type: array + items: + $ref: "../schemas/template.yaml#/TemplateSummary" + example: + - template_id: "550e8400-e29b-41d4-a716-446655440070" + form_type: "neris" + display_name: "NERIS Incident Report" + jurisdiction: "US-Federal" + agency_type: "fire_department" + version: "2.0" + last_updated: "2026-02-01" + field_count: 85 + status: "active" + - template_id: "550e8400-e29b-41d4-a716-446655440071" + form_type: "nemsis_epcr" + display_name: "NEMSIS Electronic Patient Care Report" + jurisdiction: "US-Federal" + agency_type: "ems" + version: "3.5" + last_updated: "2024-01-15" + field_count: 142 + status: "active" + - template_id: "550e8400-e29b-41d4-a716-446655440072" + form_type: "nfirs_basic" + display_name: "NFIRS Basic Module (Legacy)" + jurisdiction: "US-Federal" + agency_type: "fire_department" + version: "5.0" + last_updated: "2015-01-01" + field_count: 65 + status: "legacy" + + post: + operationId: createTemplate + summary: Register a new custom form template + description: | + Registers a new form template for a jurisdiction or agency not yet supported. + This is how FireForm extends to new states, countries, or custom agency forms + without code changes. The template defines all fields, their types, validation + rules, and how each maps from the canonical FireForm schema. + tags: + - templates + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/CreateTemplateRequest" + example: + form_type: "state_texas" + display_name: "Texas State Fire Marshal Incident Report" + jurisdiction: "US-TX" + agency_type: "fire_department" + fields: + - field_name: "incident_number" + field_type: "string" + required: true + max_length: 20 + description: "State-assigned incident number" + canonical_mapping: "report_metadata.incident_number" + - field_name: "fire_cause" + field_type: "enum" + required: true + allowed_values: + - "accidental" + - "natural" + - "intentional" + - "undetermined" + canonical_mapping: "fire.cause_category" + field_mappings_from_canonical: + "report_metadata.incident_number": "incident_number" + "fire.cause_category": "fire_cause" + responses: + "201": + description: Template created + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "409": + description: Template with this form_type already exists + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Invalid template definition + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +template_by_id: + get: + operationId: getTemplate + summary: Get full template schema + description: | + Returns the complete template definition including all fields, their types, + required/optional status, validation rules, and the mapping from canonical + FireForm schema fields. Includes the source standard reference where applicable. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template + schema: + type: string + format: uuid + responses: + "200": + description: Full template definition + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + + put: + operationId: updateTemplate + summary: Update an existing template + description: | + Replaces an existing template definition. Used when an upstream standard + changes (e.g., NERIS schema update). Templates used by submitted incidents + cannot be modified create a new version instead. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template to update + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/CreateTemplateRequest" + responses: + "200": + description: Template updated + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Template in use by submitted incidents cannot modify + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "TEMPLATE_IN_USE" + message: "Template is referenced by submitted incidents. Create a new version instead." + detail: + submitted_incident_count: 15 + recommendation: "POST /api/v1/templates to create a new version" + +template_fields: + get: + operationId: getTemplateFields + summary: Get template field definitions + description: | + Returns just the fields list for a template with type, required/optional + status, validation rules, and canonical schema mapping. Useful for building + validation checklists and for the validate endpoint to determine requirements. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template + schema: + type: string + format: uuid + - name: required_only + in: query + description: If true, return only required fields + schema: + type: boolean + default: false + responses: + "200": + description: List of template fields + content: + application/json: + schema: + type: object + properties: + template_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + total_fields: + type: integer + required_fields: + type: integer + optional_fields: + type: integer + fields: + type: array + items: + $ref: "../schemas/template.yaml#/TemplateField" + example: + template_id: "550e8400-e29b-41d4-a716-446655440070" + form_type: "neris" + total_fields: 85 + required_fields: 32 + optional_fields: 53 + fields: + - field_name: "incident_type" + field_type: "enum" + required: true + description: "Primary incident type code" + canonical_mapping: "incident.types[0].neris_code" + - field_name: "incident_date" + field_type: "date" + required: true + description: "Date of incident" + canonical_mapping: "incident.start_datetime" + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/schemas/canonical-incident.yaml b/contracts/schemas/canonical-incident.yaml new file mode 100644 index 00000000..a11d1b90 --- /dev/null +++ b/contracts/schemas/canonical-incident.yaml @@ -0,0 +1,930 @@ +# Canonical FireForm Incident Schema +# This is the master superset schema single source of truth for all downstream forms + +CanonicalIncident: + type: object + description: | + The canonical FireForm incident data model. This is the superset schema containing + every field any downstream form could need. Form-specific mappers select only the + relevant fields for each agency template. + properties: + schema_version: + type: string + description: Schema version identifier + example: "1.1.0" + schema_name: + type: string + description: Schema name identifier + enum: + - fireform_canonical_incident + + extraction_metadata: + $ref: "#/ExtractionMetadata" + report_metadata: + $ref: "#/ReportMetadata" + incident: + $ref: "#/Incident" + location: + $ref: "#/Location" + fire: + $ref: "#/Fire" + wildland: + $ref: "#/Wildland" + structure: + $ref: "#/Structure" + casualties: + $ref: "#/Casualties" + ems: + $ref: "#/EMS" + hazmat: + $ref: "#/Hazmat" + arson: + $ref: "#/Arson" + responding_agencies: + $ref: "#/RespondingAgencies" + resources_deployed: + $ref: "#/ResourcesDeployed" + weather: + $ref: "#/Weather" + environmental_impact: + $ref: "#/EnvironmentalImpact" + infrastructure_impact: + $ref: "#/InfrastructureImpact" + near_miss_and_safety: + $ref: "#/NearMissAndSafety" + lessons_learned: + $ref: "#/LessonsLearned" + follow_up: + $ref: "#/FollowUp" + periodic_reporting: + $ref: "#/PeriodicReporting" + attachments: + $ref: "#/Attachments" + +# --- Sub-schemas --- + +ExtractionMetadata: + type: object + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + input_type: + type: string + enum: [voice, text] + extracted_at: + type: string + format: date-time + llm_model: + type: string + example: "llama3:8b" + confidence_score: + type: number + minimum: 0 + maximum: 1 + description: Overall confidence score from the LLM extraction (0.0–1.0) + completeness: + $ref: "#/Completeness" + applicable_forms: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + +Completeness: + type: object + description: | + Tells the system which forms can be fully auto-generated vs which need + manual review. Recalculated server-side after every PATCH /extract. + properties: + overall_percent: + type: integer + minimum: 0 + maximum: 100 + missing_fields: + type: array + items: + type: string + description: JSON paths of fields that have no value + low_confidence_fields: + type: array + items: + type: string + description: JSON paths of fields where LLM confidence is low + inferred_fields: + type: array + items: + type: string + description: JSON paths of fields that were inferred (not explicitly stated) + forms_fully_generatable: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_needing_review: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_missing_data: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + +ReportMetadata: + type: object + properties: + report_id: + type: string + example: "FF-2024-CA-0157" + incident_number: + type: string + example: "CA-SQF-2024-0421" + report_date: + type: string + format: date + report_time: + type: string + format: time + report_status: + $ref: "../schemas/enums.yaml#/ReportStatus" + reporting_unit: + $ref: "#/ReportingUnit" + prepared_by: + type: array + items: + $ref: "#/Personnel" + reviewed_by: + type: array + items: + $ref: "#/Reviewer" + submission_log: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + submitted_at: + type: string + format: date-time + submitted_to: + type: string + +ReportingUnit: + type: object + properties: + station_name: + type: string + station_id: + type: string + agency_name: + type: string + agency_id: + type: string + format: uuid + agency_type: + type: string + +Personnel: + type: object + properties: + name: + type: string + badge_number: + type: string + rank: + type: string + role: + type: string + contact_number: + type: string + signature_captured: + type: boolean + +Reviewer: + type: object + properties: + name: + type: string + badge_number: + type: string + rank: + type: string + role: + type: string + reviewed_at: + type: string + format: date-time + approved: + type: boolean + +Incident: + type: object + properties: + name: + type: string + description: Human-readable incident name + types: + type: array + items: + $ref: "#/IncidentType" + start_datetime: + type: string + format: date-time + alarm_datetime: + type: string + format: date-time + first_arrival_datetime: + type: string + format: date-time + containment_datetime: + type: string + format: date-time + nullable: true + controlled_datetime: + type: string + format: date-time + nullable: true + cleared_datetime: + type: string + format: date-time + nullable: true + total_duration_hours: + type: number + nullable: true + narrative: + type: string + description: Free text summary of the incident + raw_transcript: + type: string + description: Original voice or text input verbatim + +IncidentType: + type: object + properties: + primary: + type: boolean + category: + $ref: "../schemas/enums.yaml#/IncidentCategory" + subcategory: + type: string + neris_code: + type: string + description: NERIS incident type code (replaces NFIRS as of Feb 2026) + nfirs_code: + type: string + description: Legacy NFIRS incident type code + +Location: + type: object + properties: + address: + type: string + nullable: true + nearest_landmark: + type: string + nullable: true + nearest_town: + type: string + nullable: true + county: + type: string + nullable: true + state: + type: string + nullable: true + country: + type: string + nullable: true + postal_code: + type: string + nullable: true + coordinates: + $ref: "#/Coordinates" + ignition_point_coordinates: + $ref: "#/CoordinatesBasic" + elevation_range_ft: + type: string + nullable: true + legal_description: + type: string + nullable: true + jurisdiction: + type: object + properties: + federal: + type: boolean + state: + type: boolean + private: + type: boolean + tribal: + type: boolean + property_type: + type: string + nullable: true + property_use: + type: string + nullable: true + dispatch_center: + type: string + nullable: true + +Coordinates: + type: object + properties: + latitude: + type: number + longitude: + type: number + accuracy_meters: + type: number + +CoordinatesBasic: + type: object + properties: + latitude: + type: number + longitude: + type: number + +Fire: + type: object + properties: + cause_category: + type: string + nullable: true + cause_specific: + type: string + nullable: true + cause_certainty: + $ref: "../schemas/enums.yaml#/CauseCertainty" + arson_suspected: + type: boolean + material_first_ignited: + type: string + nullable: true + fuel_types: + type: array + items: + type: string + fire_spread_directions: + type: array + items: + type: string + rate_of_spread: + $ref: "../schemas/enums.yaml#/RateOfSpread" + flame_lengths_ft: + type: string + nullable: true + spotting_distance_miles: + type: number + nullable: true + unusual_behaviors: + type: array + items: + type: string + detector_present: + type: boolean + nullable: true + detector_operated: + type: boolean + nullable: true + suppression_system_present: + type: boolean + nullable: true + suppression_system_operated: + type: boolean + nullable: true + estimated_damage_usd: + type: number + nullable: true + contents_loss_usd: + type: number + nullable: true + +Wildland: + type: object + properties: + is_wildland_incident: + type: boolean + total_acres_burned: + type: number + nullable: true + land_ownership_breakdown: + type: object + properties: + federal_acres: + type: number + state_acres: + type: number + private_acres: + type: number + tribal_acres: + type: number + percent_contained: + type: integer + minimum: 0 + maximum: 100 + fire_lines: + type: object + properties: + primary_line_miles: + type: number + secondary_line_miles: + type: number + dozer_line_miles: + type: number + hand_line_miles: + type: number + aerial_operations: + type: object + properties: + water_drops_gallons: + type: integer + retardant_drops_gallons: + type: integer + total_flight_hours: + type: number + containment_strategies: + type: array + items: + type: string + +Structure: + type: object + properties: + is_structure_involved: + type: boolean + structures_threatened: + type: integer + nullable: true + structures_damaged: + type: integer + nullable: true + structures_destroyed: + type: integer + nullable: true + structures_protected: + type: integer + nullable: true + construction_type: + type: string + nullable: true + stories: + type: integer + nullable: true + area_sqft: + type: number + nullable: true + occupancy_at_time: + type: integer + nullable: true + +Casualties: + type: object + properties: + civilian: + type: array + items: + $ref: "#/CivilianCasualty" + responder: + type: array + items: + $ref: "#/ResponderCasualty" + total_civilian_injuries: + type: integer + total_civilian_fatalities: + type: integer + total_responder_injuries: + type: integer + total_responder_fatalities: + type: integer + +CivilianCasualty: + type: object + properties: + age: + type: integer + nullable: true + sex: + type: string + nullable: true + injury_type: + type: string + severity: + $ref: "../schemas/enums.yaml#/InjurySeverity" + cause: + type: string + nullable: true + location_at_time: + type: string + nullable: true + transported: + type: boolean + hospital: + type: string + nullable: true + +ResponderCasualty: + type: object + properties: + personnel_id: + type: string + agency: + type: string + role: + type: string + injury_type: + type: string + severity: + $ref: "../schemas/enums.yaml#/InjurySeverity" + treatment: + type: string + nullable: true + transported: + type: boolean + hospital: + type: string + nullable: true + return_to_duty_date: + type: string + format: date + nullable: true + osha_recordable: + type: boolean + nfirs_5_required: + type: boolean + +EMS: + type: object + properties: + ems_response_required: + type: boolean + patients: + type: array + items: + $ref: "#/EMSPatient" + total_patients: + type: integer + ems_agency_responded: + type: string + nullable: true + nemsis_report_required: + type: boolean + nemsis_report_ids: + type: array + items: + type: string + +EMSPatient: + type: object + properties: + patient_ref_id: + type: string + age_approx: + type: integer + nullable: true + sex: + type: string + nullable: true + chief_complaint: + type: string + nullable: true + disposition: + type: string + nullable: true + transported: + type: boolean + date_of_birth: + type: string + format: date + nullable: true + nemsis_data_captured: + type: boolean + +Hazmat: + type: object + properties: + involved: + type: boolean + materials: + type: array + items: + type: object + properties: + name: + type: string + un_number: + type: string + nullable: true + quantity: + type: string + nullable: true + epa_reportable_quantity_exceeded: + type: boolean + spill_size_gallons: + type: number + nullable: true + +Arson: + type: object + properties: + suspected: + type: boolean + confirmed: + type: boolean + law_enforcement_notified: + type: boolean + investigation_required: + type: boolean + investigation_agency: + type: string + nullable: true + evidence_collected: + type: boolean + nibrs_report_required: + type: boolean + notes: + type: string + nullable: true + +RespondingAgencies: + type: object + properties: + primary_agency: + type: string + all_agencies: + type: array + items: + $ref: "#/RespondingAgency" + mutual_aid_activated: + type: boolean + mutual_aid_agencies: + type: array + items: + type: string + unified_command: + type: boolean + +RespondingAgency: + type: object + properties: + agency_name: + type: string + agency_type: + type: string + role: + type: string + personnel_count: + type: integer + +ResourcesDeployed: + type: object + properties: + total_personnel: + type: integer + personnel_breakdown: + type: object + properties: + firefighters: + type: integer + crew_supervisors: + type: integer + engineers: + type: integer + incident_command: + type: integer + support_staff: + type: integer + apparatus: + type: array + items: + $ref: "#/Apparatus" + crew_types: + type: array + items: + type: string + +Apparatus: + type: object + properties: + type: + type: string + count: + type: integer + +Weather: + type: object + properties: + on_arrival: + $ref: "#/WeatherReading" + worst_conditions: + $ref: "#/WeatherReadingExtended" + factors_influencing_fire: + type: array + items: + type: string + +WeatherReading: + type: object + properties: + datetime: + type: string + format: date-time + temperature_f: + type: number + relative_humidity_percent: + type: number + wind_speed_mph: + type: number + wind_direction: + type: string + haines_index: + type: integer + nullable: true + +WeatherReadingExtended: + type: object + properties: + datetime: + type: string + format: date-time + temperature_f: + type: number + relative_humidity_percent: + type: number + wind_speed_mph: + type: number + wind_gusts_mph: + type: number + nullable: true + +EnvironmentalImpact: + type: object + properties: + wildlife_habitat_affected_acres: + type: number + nullable: true + watershed_impact: + type: string + nullable: true + soil_erosion_risk: + type: string + nullable: true + sensitive_species_affected: + type: array + items: + type: string + air_quality_impact: + type: string + nullable: true + water_body_affected: + type: boolean + nullable: true + +InfrastructureImpact: + type: object + properties: + items: + type: array + items: + $ref: "#/InfrastructureItem" + +InfrastructureItem: + type: object + properties: + type: + type: string + unit: + type: string + quantity: + type: number + severity: + type: string + +NearMissAndSafety: + type: object + properties: + near_miss_events: + type: array + items: + type: object + properties: + description: + type: string + date: + type: string + format: date + contributing_factors: + type: array + items: + type: string + lessons_learned: + type: string + nullable: true + corrective_action: + type: string + nullable: true + safety_breaches: + type: integer + weather_related_risks: + type: array + items: + type: string + +LessonsLearned: + type: object + properties: + successful_tactics: + type: array + items: + type: string + areas_for_improvement: + type: array + items: + type: string + recommendations: + type: array + items: + type: string + +FollowUp: + type: object + properties: + mop_up: + type: object + properties: + percent_complete: + type: integer + estimated_completion_date: + type: string + format: date + personnel_assigned: + type: integer + rehabilitation: + type: object + properties: + erosion_control_acres: + type: number + reseeding_acres: + type: number + hazard_tree_removal_required: + type: boolean + next_inspection_date: + type: string + format: date + nullable: true + investigation_ongoing: + type: boolean + +PeriodicReporting: + type: object + properties: + contributes_to_monthly_report: + type: boolean + contributes_to_quarterly_report: + type: boolean + contributes_to_annual_report: + type: boolean + neris_submitted: + type: boolean + neris_submitted_at: + type: string + format: date-time + nullable: true + state_submitted: + type: boolean + state_submitted_at: + type: string + format: date-time + nullable: true + +Attachments: + type: object + properties: + maps: + type: boolean + photos_count: + type: integer + weather_charts: + type: boolean + resource_tracking_logs: + type: boolean + incident_action_plans: + type: boolean + attachment_refs: + type: array + items: + type: object + properties: + ref_id: + type: string + format: uuid + filename: + type: string + content_type: + type: string + size_bytes: + type: integer diff --git a/contracts/schemas/common.yaml b/contracts/schemas/common.yaml new file mode 100644 index 00000000..0798f85d --- /dev/null +++ b/contracts/schemas/common.yaml @@ -0,0 +1,129 @@ +# Common schemas used across multiple endpoints + +ErrorResponse: + type: object + required: + - error_code + - message + properties: + error_code: + type: string + description: Machine-readable error code + example: "EXTRACT_NOT_FOUND" + message: + type: string + description: Human-readable error message + example: "Extraction with ID xyz not found" + detail: + type: object + description: Additional context specific to the error type + additionalProperties: true + retry_after_seconds: + type: integer + description: Present on 503 errors seconds to wait before retrying + validation_errors: + type: array + description: Present on 422 errors list of field-level validation issues + items: + $ref: "#/ValidationError" + +ValidationError: + type: object + properties: + field: + type: string + description: JSON path of the invalid field + example: "fire.cause_certainty" + issue: + type: string + description: Description of the validation issue + example: "Must be one of: confirmed, probable, suspected, undetermined" + value: + description: The invalid value that was provided + +Pagination: + type: object + properties: + total: + type: integer + description: Total number of items across all pages + page: + type: integer + description: Current page number (1-based) + per_page: + type: integer + description: Items per page + total_pages: + type: integer + description: Total number of pages + has_next: + type: boolean + description: Whether there is a next page + has_prev: + type: boolean + description: Whether there is a previous page + +AsyncJobResponse: + type: object + required: + - job_id + - status + properties: + job_id: + type: string + format: uuid + description: Unique job identifier for polling + job_type: + type: string + description: Type of async operation + enum: + - transcription + - extraction + - form_generation + - batch_form_generation + - report_generation + status: + $ref: "enums.yaml#/JobStatus" + estimated_seconds: + type: integer + description: Estimated time to completion in seconds + poll_url: + type: string + description: Full URL to poll for job status + +Job: + type: object + required: + - job_id + - job_type + - status + properties: + job_id: + type: string + format: uuid + job_type: + type: string + enum: + - transcription + - extraction + - form_generation + - batch_form_generation + - report_generation + status: + $ref: "enums.yaml#/JobStatus" + progress_percent: + type: integer + minimum: 0 + maximum: 100 + description: Progress percentage (0–100) + result_url: + type: string + description: URL of the completed result (present when status is "completed") + error: + $ref: "#/ErrorResponse" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time diff --git a/contracts/schemas/enums.yaml b/contracts/schemas/enums.yaml new file mode 100644 index 00000000..b79bedae --- /dev/null +++ b/contracts/schemas/enums.yaml @@ -0,0 +1,130 @@ +# Shared enums used across the FireForm API + +InputStatus: + type: string + enum: + - queued + - transcribing + - ready + - failed + description: Status of an input record + +ExtractionStatus: + type: string + enum: + - processing + - completed + - failed + - needs_review + description: Status of an AI extraction job + +FormStatus: + type: string + enum: + - queued + - generating + - completed + - failed + description: Status of a form generation job + +ReportStatus: + type: string + enum: + - draft + - under_review + - approved + - submitted + description: Status of an incident report in the approval workflow + +JobStatus: + type: string + enum: + - queued + - processing + - completed + - failed + description: Status of any async job + +FormType: + type: string + enum: + - neris + - nemsis_epcr + - nibrs + - nfirs_basic + - nfirs_fire + - nfirs_structure + - nfirs_wildland + - nfirs_ems + - nfirs_hazmat + - nfirs_apparatus + - nfirs_personnel + - nfirs_arson + - nfirs_casualty_civilian + - nfirs_casualty_responder + - cal_fire_ics209 + - osha_301 + - un_ssirs + - state_georgia + - state_california + - state_new_york + description: | + Stable string identifier for a form type. Includes the new NERIS standard + (replacing NFIRS as of Feb 2026), legacy NFIRS modules, NEMSIS, NIBRS, + OSHA, state-specific, and international (UN SSIRS) forms. + +IncidentCategory: + type: string + enum: + - fire + - ems + - rescue + - hazardous_conditions + - service_call + - good_intent + - false_alarm + - law_enforcement + description: High-level incident category + +CauseCertainty: + type: string + enum: + - confirmed + - probable + - suspected + - undetermined + description: Certainty level of fire cause determination + +InjurySeverity: + type: string + enum: + - minor + - moderate + - severe + - fatal + description: Severity classification for injuries + +RateOfSpread: + type: string + enum: + - slow + - moderate + - rapid + - extreme + description: Rate of fire spread classification + +PeriodType: + type: string + enum: + - monthly + - quarterly + - annual + description: Reporting period type + +OutputFormat: + type: string + enum: + - pdf + - json + - both + description: Output format for generated forms diff --git a/contracts/schemas/extraction-record.yaml b/contracts/schemas/extraction-record.yaml new file mode 100644 index 00000000..aef74426 --- /dev/null +++ b/contracts/schemas/extraction-record.yaml @@ -0,0 +1,134 @@ +# Extraction-related schemas + +ExtractionRequest: + type: object + properties: + model_override: + type: string + description: Override the default LLM model (e.g. "llama3:70b") + extraction_hints: + type: object + description: Optional hints to improve extraction accuracy + properties: + incident_type: + type: string + description: Hint about the incident type (e.g. "wildland_fire", "structure_fire") + state: + type: string + description: US state code to apply state-specific extraction rules + agency_type: + type: string + description: Agency type hint for form selection + additionalProperties: true + +ExtractionCompleted: + type: object + required: + - extract_id + - input_id + - status + - canonical_incident + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + status: + type: string + enum: + - completed + completed_at: + type: string + format: date-time + model_used: + type: string + description: LLM model that performed the extraction + processing_time_seconds: + type: number + canonical_incident: + $ref: "canonical-incident.yaml#/CanonicalIncident" + corrections: + type: array + description: Audit trail of manual corrections applied via PATCH + items: + type: object + properties: + field_path: + type: string + original_value: {} + corrected_value: {} + corrected_at: + type: string + format: date-time + corrected_by: + type: string + +ExtractionProcessing: + type: object + required: + - extract_id + - input_id + - status + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + status: + type: string + enum: + - processing + - failed + started_at: + type: string + format: date-time + retry_after_seconds: + type: integer + description: Polling hint for clients + error_type: + type: string + nullable: true + description: Present when status is "failed" + error_detail: + type: string + nullable: true + partial_result: + $ref: "canonical-incident.yaml#/CanonicalIncident" + +ValidationResult: + type: object + required: + - valid + - form_type + - extract_id + properties: + valid: + type: boolean + description: Whether all required fields for this form type are present + form_type: + $ref: "enums.yaml#/FormType" + extract_id: + type: string + format: uuid + missing_required: + type: array + items: + type: string + description: JSON paths of required fields that are missing + missing_recommended: + type: array + items: + type: string + description: JSON paths of recommended fields that are missing + warnings: + type: array + items: + type: string + description: Human-readable warnings about data quality + field_coverage_percent: + type: number + description: Percentage of form fields that have values diff --git a/contracts/schemas/form-record.yaml b/contracts/schemas/form-record.yaml new file mode 100644 index 00000000..a83ec41a --- /dev/null +++ b/contracts/schemas/form-record.yaml @@ -0,0 +1,198 @@ +# Form generation related schemas + +GenerateAllRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + options: + type: object + properties: + skip_incomplete: + type: boolean + default: true + description: Skip forms that fail validation + force_partial: + type: boolean + default: false + description: Generate forms even with missing fields (leaving blanks) + +GenerateSingleRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + options: + type: object + properties: + output_format: + $ref: "../schemas/enums.yaml#/OutputFormat" + force_partial: + type: boolean + default: false + force: + type: boolean + default: false + description: Allow generation even if form_type is not in applicable_forms + +BatchGenerateResponse: + type: object + required: + - batch_id + - status + - extract_id + properties: + batch_id: + type: string + format: uuid + status: + type: string + enum: [processing] + extract_id: + type: string + format: uuid + forms_queued: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_skipped: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + reason: + type: string + estimated_seconds: + type: integer + poll_url: + type: string + +FormGenerateResponse: + type: object + required: + - form_id + - form_type + - status + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + type: string + enum: [processing, completed] + extract_id: + type: string + format: uuid + job_id: + type: string + format: uuid + estimated_seconds: + type: integer + poll_url: + type: string + +FormRecord: + type: object + required: + - form_id + - form_type + - status + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" + extract_id: + type: string + format: uuid + incident_id: + type: string + format: uuid + nullable: true + created_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + nullable: true + pdf_ready: + type: boolean + json_ready: + type: boolean + field_mapping_summary: + type: object + properties: + total_form_fields: + type: integer + fields_filled: + type: integer + fields_blank: + type: integer + coverage_percent: + type: number + +FormMappedJson: + type: object + required: + - form_type + - form_id + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + form_version: + type: string + form_id: + type: string + format: uuid + extract_id: + type: string + format: uuid + agency_fields: + type: object + additionalProperties: true + description: Agency-specific field names and values as the target system expects + +BatchStatus: + type: object + required: + - batch_id + - status + properties: + batch_id: + type: string + format: uuid + status: + type: string + enum: [processing, completed, failed] + total: + type: integer + completed: + type: integer + failed: + type: integer + forms: + type: array + items: + type: object + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" diff --git a/contracts/schemas/incident-record.yaml b/contracts/schemas/incident-record.yaml new file mode 100644 index 00000000..932425cc --- /dev/null +++ b/contracts/schemas/incident-record.yaml @@ -0,0 +1,150 @@ +# Incident management schemas + +CreateIncidentRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + incident_number: + type: string + description: Optional org-assigned incident number + tags: + type: array + items: + type: string + +UpdateIncidentRequest: + type: object + properties: + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + tags: + type: array + items: + type: string + incident_number: + type: string + notes: + type: string + +IncidentRecord: + type: object + required: + - incident_id + - extract_id + - status + properties: + incident_id: + type: string + format: uuid + extract_id: + type: string + format: uuid + incident_number: + type: string + nullable: true + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + incident_name: + type: string + nullable: true + incident_type: + type: string + nullable: true + incident_date: + type: string + format: date + nullable: true + forms_generated: + type: array + items: + type: object + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" + tags: + type: array + items: + type: string + notes: + type: string + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + deleted_at: + type: string + format: date-time + nullable: true + +IncidentRecordFull: + description: Full incident record with linked extraction and forms + allOf: + - $ref: "#/IncidentRecord" + - type: object + properties: + canonical_incident: + $ref: "canonical-incident.yaml#/CanonicalIncident" + forms: + type: array + items: + $ref: "form-record.yaml#/FormRecord" + submission_log: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + submitted_at: + type: string + format: date-time + submitted_to: + type: string + status: + type: string + +IncidentListResponse: + type: object + properties: + data: + type: array + items: + type: object + properties: + incident_id: + type: string + format: uuid + incident_number: + type: string + nullable: true + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + incident_name: + type: string + nullable: true + incident_type: + type: string + nullable: true + incident_date: + type: string + format: date + nullable: true + forms_count: + type: integer + created_at: + type: string + format: date-time + pagination: + $ref: "common.yaml#/Pagination" diff --git a/contracts/schemas/input-record.yaml b/contracts/schemas/input-record.yaml new file mode 100644 index 00000000..65dd2055 --- /dev/null +++ b/contracts/schemas/input-record.yaml @@ -0,0 +1,138 @@ +# Input-related schemas + +TextInputRequest: + type: object + required: + - narrative + properties: + narrative: + type: string + description: Free-text incident narrative (10+ words, max 50,000 characters) + minLength: 20 + maxLength: 50000 + station_id: + type: string + description: Station identifier + responder_badge: + type: string + description: Badge number of the reporting responder + incident_date_hint: + type: string + format: date + description: Approximate date of the incident + +VoiceInputResponse: + type: object + required: + - input_id + - status + - input_type + properties: + input_id: + type: string + format: uuid + status: + type: string + enum: + - queued + input_type: + type: string + enum: + - voice + estimated_processing_seconds: + type: integer + created_at: + type: string + format: date-time + job_id: + type: string + format: uuid + description: Job ID for polling transcription status + poll_url: + type: string + description: URL to poll for input status + +TextInputResponse: + type: object + required: + - input_id + - status + - input_type + properties: + input_id: + type: string + format: uuid + status: + type: string + enum: + - ready + input_type: + type: string + enum: + - text + character_count: + type: integer + word_count: + type: integer + created_at: + type: string + format: date-time + +InputRecord: + type: object + required: + - input_id + - input_type + - status + properties: + input_id: + type: string + format: uuid + input_type: + type: string + enum: + - voice + - text + status: + $ref: "enums.yaml#/InputStatus" + transcript: + type: string + nullable: true + description: Transcribed text (for voice) or original narrative (for text). Null while transcribing. + original_filename: + type: string + nullable: true + description: Original audio filename (voice inputs only) + audio_duration_seconds: + type: number + nullable: true + description: Duration of audio in seconds (voice inputs only) + character_count: + type: integer + nullable: true + word_count: + type: integer + nullable: true + station_id: + type: string + nullable: true + responder_badge: + type: string + nullable: true + incident_date_hint: + type: string + format: date + nullable: true + error_detail: + type: string + nullable: true + description: Error message if transcription failed + retry_after_seconds: + type: integer + description: Polling hint present when status is queued or transcribing + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time diff --git a/contracts/schemas/reporting.yaml b/contracts/schemas/reporting.yaml new file mode 100644 index 00000000..f352e709 --- /dev/null +++ b/contracts/schemas/reporting.yaml @@ -0,0 +1,112 @@ +# Reporting & Analytics schemas + +ReportSummary: + type: object + properties: + date_from: + type: string + format: date + date_to: + type: string + format: date + total_incidents: + type: integer + by_type: + type: object + additionalProperties: + type: integer + description: Incident counts grouped by category + by_status: + type: object + additionalProperties: + type: integer + description: Incident counts grouped by report status + forms_generated: + type: integer + avg_completeness_score: + type: number + avg_processing_time_seconds: + type: number + groups: + type: array + items: + type: object + properties: + period: + type: string + description: Period label (e.g. "2024-07" for monthly, "fire" for by type) + incident_count: + type: integer + forms_generated: + type: integer + +GenerateReportRequest: + type: object + required: + - period_type + - year + properties: + period_type: + $ref: "../schemas/enums.yaml#/PeriodType" + year: + type: integer + minimum: 2000 + maximum: 2100 + month: + type: integer + minimum: 1 + maximum: 12 + description: Required when period_type is "monthly" + quarter: + type: integer + minimum: 1 + maximum: 4 + description: Required when period_type is "quarterly" + format: + $ref: "../schemas/enums.yaml#/OutputFormat" + +PeriodicReport: + type: object + properties: + report_id: + type: string + format: uuid + period_type: + $ref: "../schemas/enums.yaml#/PeriodType" + period_label: + type: string + description: Human-readable period (e.g. "July 2024", "Q3 2024", "2024") + generated_at: + type: string + format: date-time + summary: + $ref: "#/ReportSummary" + incidents: + type: array + items: + type: object + properties: + incident_id: + type: string + format: uuid + incident_number: + type: string + incident_date: + type: string + format: date + incident_type: + type: string + status: + type: string + forms_generated: + type: integer + compliance: + type: object + properties: + neris_submission_rate: + type: number + description: Percentage of incidents with NERIS submitted + average_submission_delay_days: + type: number + overdue_incidents: + type: integer diff --git a/contracts/schemas/system.yaml b/contracts/schemas/system.yaml new file mode 100644 index 00000000..8c256932 --- /dev/null +++ b/contracts/schemas/system.yaml @@ -0,0 +1,101 @@ +# System & health check schemas + +HealthStatus: + type: object + required: + - status + - version + properties: + status: + type: string + enum: + - healthy + - degraded + - unhealthy + version: + type: string + description: FireForm API version + uptime_seconds: + type: integer + components: + type: object + properties: + database: + $ref: "#/ComponentHealth" + ollama: + $ref: "#/ComponentHealth" + whisper: + $ref: "#/ComponentHealth" + storage: + $ref: "#/ComponentHealth" + +ComponentHealth: + type: object + required: + - status + properties: + status: + type: string + enum: + - healthy + - degraded + - unhealthy + response_time_ms: + type: integer + nullable: true + detail: + type: string + nullable: true + model_loaded: + type: string + nullable: true + description: Currently loaded model (ollama component) + disk_free_gb: + type: number + nullable: true + description: Free disk space (storage component) + ollama_version: + type: string + nullable: true + description: Ollama server version (ollama component) + models_available: + type: array + nullable: true + description: All models pulled and available on the Ollama server (ollama component) + items: + type: object + properties: + name: + type: string + size_gb: + type: number + quantization: + type: string + nullable: true + loaded: + type: boolean + current_load: + type: object + nullable: true + description: Current processing load (ollama component) + properties: + active_requests: + type: integer + queued_requests: + type: integer + +SchemaVersion: + type: object + required: + - version + - released_at + properties: + version: + type: string + released_at: + type: string + format: date-time + changelog: + type: string + breaking_changes: + type: boolean diff --git a/contracts/schemas/template.yaml b/contracts/schemas/template.yaml new file mode 100644 index 00000000..45c71f77 --- /dev/null +++ b/contracts/schemas/template.yaml @@ -0,0 +1,144 @@ +# Template & Configuration schemas + +TemplateSummary: + type: object + properties: + template_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + display_name: + type: string + jurisdiction: + type: string + description: Jurisdiction code (e.g. "US-Federal", "US-CA", "US-GA") + agency_type: + type: string + version: + type: string + last_updated: + type: string + format: date + field_count: + type: integer + status: + type: string + enum: + - active + - legacy + - draft + +CreateTemplateRequest: + type: object + required: + - form_type + - display_name + - jurisdiction + - fields + - field_mappings_from_canonical + properties: + form_type: + type: string + description: Unique form type identifier + display_name: + type: string + jurisdiction: + type: string + agency_type: + type: string + fields: + type: array + items: + $ref: "#/TemplateField" + field_mappings_from_canonical: + type: object + additionalProperties: + type: string + description: | + Mapping from canonical FireForm JSON paths to this template's field names. + Key = canonical path (e.g. "fire.cause_category"), value = template field name. + source_standard: + type: string + nullable: true + description: Reference to the source standard (e.g. "NERIS v2.0", "NFIRS 5.0") + pdf_template_ref: + type: string + nullable: true + description: Reference to the PDF template file + +Template: + allOf: + - type: object + properties: + template_id: + type: string + format: uuid + version: + type: string + last_updated: + type: string + format: date + field_count: + type: integer + status: + type: string + enum: [active, legacy, draft] + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + - $ref: "#/CreateTemplateRequest" + +TemplateField: + type: object + required: + - field_name + - field_type + - required + properties: + field_name: + type: string + description: Field identifier within this template + field_type: + type: string + enum: + - string + - integer + - number + - boolean + - date + - datetime + - time + - enum + - text + - array + description: Data type of the field + required: + type: boolean + description: + type: string + nullable: true + max_length: + type: integer + nullable: true + min_value: + type: number + nullable: true + max_value: + type: number + nullable: true + allowed_values: + type: array + items: + type: string + nullable: true + description: Valid values for enum fields + canonical_mapping: + type: string + description: JSON path in canonical FireForm schema this field maps from + default_value: + nullable: true + description: Default value if canonical field is null From 38253221f5b4795856b410624bb0f93478df71a6 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 9 Jun 2026 11:41:46 +0530 Subject: [PATCH 14/85] renamed incident-contract --- .../schemas/{canonical-incident.yaml => incident-contract.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/schemas/{canonical-incident.yaml => incident-contract.yaml} (100%) diff --git a/contracts/schemas/canonical-incident.yaml b/contracts/schemas/incident-contract.yaml similarity index 100% rename from contracts/schemas/canonical-incident.yaml rename to contracts/schemas/incident-contract.yaml From 7b354ea39ea790fefa39adbb6cce84de3d5c69f8 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 9 Jun 2026 11:54:45 +0530 Subject: [PATCH 15/85] updated incident-contracts in all references --- contracts/path/extraction.yaml | 6 +++--- contracts/schemas/extraction-record.yaml | 8 ++++---- contracts/schemas/incident-contract.yaml | 4 ++-- contracts/schemas/incident-record.yaml | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/path/extraction.yaml b/contracts/path/extraction.yaml index 0e11309b..865e20c4 100644 --- a/contracts/path/extraction.yaml +++ b/contracts/path/extraction.yaml @@ -129,9 +129,9 @@ extract_by_id: input_id: "550e8400-e29b-41d4-a716-446655440001" status: "completed" completed_at: "2024-07-15T14:31:05Z" - canonical_incident: + incident_contract: schema_version: "1.1.0" - schema_name: "fireform_canonical_incident" + schema_name: "fireform_incident_contract" extraction_metadata: extract_id: "550e8400-e29b-41d4-a716-446655440020" confidence_score: 0.91 @@ -179,7 +179,7 @@ extract_by_id: content: application/merge-patch+json: schema: - $ref: "../schemas/canonical-incident.yaml#/CanonicalIncident" + $ref: "../schemas/incident-contract.yaml#/IncidentContract" example: fire: estimated_damage_usd: 250000 diff --git a/contracts/schemas/extraction-record.yaml b/contracts/schemas/extraction-record.yaml index aef74426..ae0fd487 100644 --- a/contracts/schemas/extraction-record.yaml +++ b/contracts/schemas/extraction-record.yaml @@ -27,7 +27,7 @@ ExtractionCompleted: - extract_id - input_id - status - - canonical_incident + - incident_contract properties: extract_id: type: string @@ -47,8 +47,8 @@ ExtractionCompleted: description: LLM model that performed the extraction processing_time_seconds: type: number - canonical_incident: - $ref: "canonical-incident.yaml#/CanonicalIncident" + incident_contract: + $ref: "incident-contract.yaml#/IncidentContract" corrections: type: array description: Audit trail of manual corrections applied via PATCH @@ -97,7 +97,7 @@ ExtractionProcessing: type: string nullable: true partial_result: - $ref: "canonical-incident.yaml#/CanonicalIncident" + $ref: "incident-contract.yaml#/IncidentContract" ValidationResult: type: object diff --git a/contracts/schemas/incident-contract.yaml b/contracts/schemas/incident-contract.yaml index a11d1b90..1a129df1 100644 --- a/contracts/schemas/incident-contract.yaml +++ b/contracts/schemas/incident-contract.yaml @@ -1,7 +1,7 @@ # Canonical FireForm Incident Schema # This is the master superset schema single source of truth for all downstream forms -CanonicalIncident: +IncidentContract: type: object description: | The canonical FireForm incident data model. This is the superset schema containing @@ -16,7 +16,7 @@ CanonicalIncident: type: string description: Schema name identifier enum: - - fireform_canonical_incident + - fireform_incident_contract extraction_metadata: $ref: "#/ExtractionMetadata" diff --git a/contracts/schemas/incident-record.yaml b/contracts/schemas/incident-record.yaml index 932425cc..686e7ce8 100644 --- a/contracts/schemas/incident-record.yaml +++ b/contracts/schemas/incident-record.yaml @@ -94,8 +94,8 @@ IncidentRecordFull: - $ref: "#/IncidentRecord" - type: object properties: - canonical_incident: - $ref: "canonical-incident.yaml#/CanonicalIncident" + incident_contract: + $ref: "incident-contract.yaml#/IncidentContract" forms: type: array items: From 0529cfd90f7abc39b6a9006674091375af1b13cf Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 11 Jun 2026 23:43:17 +0530 Subject: [PATCH 16/85] added setup docs, updated contributing.md --- CONTRIBUTING.md | 23 +++--------- docs/SETUP.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 docs/SETUP.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 154daf8a..7d297fb3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,25 +39,12 @@ If you have a great idea for FireForm, we'd love to hear it! Please open an issu 4. Make sure your code lints. 5. Issue that pull request! -## 🛠️ Local Development Setup - -FireForm uses Docker and Docker Compose for the backend to ensure a consistent environment. - -### Prerequisites +**Issues are not formally assigned.** You are free to pick any open issue, work on it, and raise a PR directly. If multiple PRs address the same issue, the first one that actually fixes it generally gets preference. To avoid duplicating someone else's work, coordinate with other contributors on our [Discord](https://discord.gg/nBv5b6kF68) before starting. -- [Docker](https://docs.docker.com/get-docker/) -- [Docker Compose](https://docs.docker.com/compose/install/) -- `make` (optional, but recommended) -- [Node.js](https://nodejs.org/) 20+ (only needed for the desktop app) +## 💬 Community -### Desktop App Development +Join our Discord server to ask questions, discuss issues, and coordinate work with other contributors: https://discord.gg/nBv5b6kF68 -The frontend is a vanilla HTML/CSS/JS app wrapped in Electron. To run it locally: - -```bash -cd frontend -npm install # one-time setup -npm start # launches the Electron desktop window -``` +## 🛠️ Local Development Setup -The backend (API + Ollama) must be running separately via Docker — see `make fireform`. +See the [Setup Guide](docs/SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 00000000..216bc24e --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,98 @@ +# Setup Guide + +This guide gets the FireForm backend running locally with Docker. It assumes you are comfortable with git and a terminal, but new to this project. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) **24 or newer**, with the Docker daemon running +- Docker Compose v2 (bundled with Docker Desktop; verify with `docker compose version`) +- `make` +- ~3 GB of free disk space (Docker images + LLM model weights) + +## Setup + +### 1. Clone the repository + +```bash +git clone https://github.com/fireform-core/FireForm.git +cd FireForm +``` + +### 2. Run first-time setup + +```bash +make init +``` + +This will: + +1. Check that Docker meets the requirements above +2. Create `docker/.env.dev` from `docker/.env.example` (gitignored; defaults work out of the box) +3. Prompt you to pick an Ollama model (the default, `qwen2.5:1.5b`, is the smallest and fine for development) +4. Offer to build and start everything answer `y`, or run `make fireform` later + +### 3. Build and start (if you skipped it in step 2) + +```bash +make fireform +``` + +This builds the Docker images, starts the containers, waits for Ollama, and pulls the LLM model. The first run takes several minutes (image build + model download); later runs are fast. + +When it finishes you'll see: + +``` +FireForm is ready! + API: http://localhost:8000 + API Docs: http://localhost:8000/docs +``` + +### 4. Verify it works + +Open **http://localhost:8000/docs** in your browser. This is the interactive Swagger UI you can explore and test every API endpoint directly from there (expand an endpoint, click _Try it out_, then _Execute_). + +## Day-to-day commands + +| Command | What it does | +| ------------ | ------------------------------------------------------------------------ | +| `make up` | Start containers | +| `make down` | Stop containers (data is preserved) | +| `make logs` | Stream all container logs (`make logs-app` / `make logs-ollama` for one) | +| `make shell` | Open a shell inside the app container | +| `make test` | Run the test suite | +| `make help` | List all commands | + +The dev container mounts the source code, so code changes reload automatically — no rebuild needed. Rebuild (`make build`) only when dependencies in `requirements.txt` or the Dockerfile change. + +## Frontend (optional) + +The desktop/web frontend lives in a separate repository: + +```bash +git clone https://github.com/fireform-core/fireform-frontend.git +``` + +Follow the README in that repository to run it. The backend from this guide must be running for the frontend to work. + +## Troubleshooting + +**`make init` fails dependency checks** +Docker isn't running or is too old. Start Docker Desktop (or the Docker daemon) and confirm `docker version` reports 24+. + +**Port 8000 already in use** +Another process is bound to the port. Either stop it, or change `APP_PORT` in `docker/.env.dev` and run `make down && make up`. + +**Model pull is slow or times out** +The first `make fireform` downloads the LLM weights (~1 GB for the default model). On a slow connection just wait, or re-run `make pull-model` it resumes safely. + +**Containers start but the API doesn't respond** +Check `make logs-app` for the actual error. The entrypoint runs database migrations on startup, so the API takes a few seconds after the container starts. + +**Want a clean slate** +`make super-clean` stops everything and **deletes all volumes** database, uploads, and downloaded model weights. Only use it when you intend to wipe all local data. + +## Where to go next + +- **Join our [Discord](https://discord.gg/nBv5b6kF68)** — ask questions and coordinate with other contributors +- [CONTRIBUTING.md](../CONTRIBUTING.md) - how to contribute +- [docker/README.md](../docker/README.md) - Docker layout, env vars, and volumes in detail From c131ee14d479a806c8037d9c8ab5e20d0cb5d795 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 12 Jun 2026 03:13:11 +0530 Subject: [PATCH 17/85] added project structure --- CONTRIBUTING.md | 2 + docs/{SETUP.md => 1. SETUP.md} | 1 + docs/2. PROJECT_STRUCTURE.md | 87 ++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) rename docs/{SETUP.md => 1. SETUP.md} (97%) create mode 100644 docs/2. PROJECT_STRUCTURE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d297fb3..6cf0be75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,3 +48,5 @@ Join our Discord server to ask questions, discuss issues, and coordinate work wi ## 🛠️ Local Development Setup See the [Setup Guide](docs/SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. + +Before writing code, read the [Project Structure](docs/PROJECT_STRUCTURE.md) guide — it explains how the codebase is organized and where new code should go. diff --git a/docs/SETUP.md b/docs/1. SETUP.md similarity index 97% rename from docs/SETUP.md rename to docs/1. SETUP.md index 216bc24e..5b4de340 100644 --- a/docs/SETUP.md +++ b/docs/1. SETUP.md @@ -95,4 +95,5 @@ Check `make logs-app` for the actual error. The entrypoint runs database migrati - **Join our [Discord](https://discord.gg/nBv5b6kF68)** — ask questions and coordinate with other contributors - [CONTRIBUTING.md](../CONTRIBUTING.md) - how to contribute +- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) - how the codebase is organized and where new code goes - [docker/README.md](../docker/README.md) - Docker layout, env vars, and volumes in detail diff --git a/docs/2. PROJECT_STRUCTURE.md b/docs/2. PROJECT_STRUCTURE.md new file mode 100644 index 00000000..cee9c40f --- /dev/null +++ b/docs/2. PROJECT_STRUCTURE.md @@ -0,0 +1,87 @@ +# Project Structure + +This document explains how the FireForm repository is organized and, when you add new code, where it should go. + +## Top-level layout + +``` +FireForm/ +├── app/ # Backend source code (FastAPI application) +├── tests/ # Test suite (pytest) +├── docker/ # Dockerfiles, compose files, env templates +├── scripts/ # Shell scripts +├── docs/ # Project documentation +├── examples/ # Standalone demo scripts +├── data/ # Local runtime data (gitignored contents) +├── Makefile # Entry point for all dev commands (`make help`) +└── requirements.txt # Python dependencies +``` + +| You want to… | Go to | +| -------------------------------- | ------------------------------------------------------- | +| Change backend behavior | `app/` | +| Add or fix tests | `tests/` | +| Change container setup, env vars | `docker/` (see [docker/README.md](../docker/README.md)) | +| Change setup/bootstrap scripts | `scripts/` | +| Add documentation | `docs/` | +| Add dev commands | `Makefile` | + +## Inside `app/` + +The backend follows a layered structure: **routes → services → repositories → database**. Requests enter through the API layer, business logic lives in services, and all database access goes through repositories. + +``` +app/ +├── main.py # App factory: creates FastAPI app, wires middleware and routers +├── api/ # HTTP layer - request/response handling only, no business logic +│ ├── router.py # Aggregates all route modules into one router; main.py mounts this +│ ├── deps.py # Shared FastAPI dependencies (e.g. DB session injection) +│ ├── routes/ # One module per feature (forms.py, templates.py, …) +│ └── schemas/ # Pydantic request/response models, one module per feature +├── core/ # App-wide infrastructure +│ ├── config.py # All settings and env var reading nothing else reads os.environ +│ ├── lifespan.py # Startup/shutdown logic (DB init, etc.) +│ ├── logging.py # Logging configuration +│ └── errors/ # Custom exception classes (base.py) and handlers (handlers.py) +├── services/ # Business logic — LLM extraction, PDF filling, orchestration +├── db/ # Database engine/session (database.py) and repositories.py +├── models/ # SQLAlchemy ORM models +└── utils/ # Small generic helpers with no business logic +``` + +### Where does my new code go? + +**Adding a new API endpoint:** + +1. `app/api/schemas/<...>.py` - Pydantic models for the request and response bodies +2. `app/api/routes/<...>.py` - the route handlers; keep them thin, delegate to a service +3. `app/api/router.py` - register the new router (one `include_router` line) +4. Business logic goes in `app/services/`, not in the route handler + +**Adding business logic:** `app/services/`. A service should not know about HTTP (no FastAPI imports) it takes plain data in and returns plain data, so it can be tested and reused independently. + +**Adding a database table:** define the ORM model in `app/models/models.py`, and add its query/persistence functions to `app/db/repositories.py`. Services call repositories; routes never touch the database directly. + +**Adding a setting or env var:** declare it in `app/core/config.py` and document it in `docker/.env.example`. Code elsewhere imports from `config`, never reads `os.environ` itself. + +**Adding a custom error:** subclass in `app/core/errors/base.py`; map it to an HTTP response in `app/core/errors/handlers.py`. + +## Tests + +Tests live in `tests/`, run with `make test` (pytest inside the app container). `conftest.py` holds shared fixtures. Name files `test_.py` and mirror what you're testing: API endpoint tests alongside `test_api.py`, model/DB tests alongside `test_model.py`. +Note: Tests will be undergoing many changes hence the docs can be outdated, Please raise issue if tests or docs are outdated. + +## Docker & scripts + +- `docker/dev/` - development image (hot reload, source mounted) and its compose file. This is what `make up` runs. +- `docker/prod/` - production image (multi-stage build, gunicorn, no source mount). +- `docker/.env.example` - template for env vars; copied to `.env.dev` by `make init`. +- `scripts/` - shell scripts invoked by `make init` (dependency checks, env file creation, model selection) and by container startup. Not meant to be run directly. + +Full details: [docker/README.md](../docker/README.md). + +## Everything else + +- `examples/` - runnable demo scripts showing the pipeline end to end; not imported by the app. +- `data/` - runtime working data on your machine; contents are not part of the codebase. +- `src/`, `temp/` - scratch/legacy directories slated for cleanup; don't add new code here. From 6ea00393f865393b2a3aee3aa5c93942a17e5fc0 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 12 Jun 2026 19:17:47 +0530 Subject: [PATCH 18/85] Updated renamed file name --- CONTRIBUTING.md | 4 ++-- docs/1. SETUP.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cf0be75..d30e9b93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,6 @@ Join our Discord server to ask questions, discuss issues, and coordinate work wi ## 🛠️ Local Development Setup -See the [Setup Guide](docs/SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. +See the [Setup Guide](docs/1.%20SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. -Before writing code, read the [Project Structure](docs/PROJECT_STRUCTURE.md) guide — it explains how the codebase is organized and where new code should go. +Before writing code, read the [Project Structure](docs/2.%20PROJECT_STRUCTURE.md) guide — it explains how the codebase is organized and where new code should go. diff --git a/docs/1. SETUP.md b/docs/1. SETUP.md index 5b4de340..30d2aeee 100644 --- a/docs/1. SETUP.md +++ b/docs/1. SETUP.md @@ -95,5 +95,5 @@ Check `make logs-app` for the actual error. The entrypoint runs database migrati - **Join our [Discord](https://discord.gg/nBv5b6kF68)** — ask questions and coordinate with other contributors - [CONTRIBUTING.md](../CONTRIBUTING.md) - how to contribute -- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) - how the codebase is organized and where new code goes +- [PROJECT_STRUCTURE.md](2.%20PROJECT_STRUCTURE.md) - how the codebase is organized and where new code goes - [docker/README.md](../docker/README.md) - Docker layout, env vars, and volumes in detail From b59006987b0401a4129031ac7bafbde0b4e831cb Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 17 Jun 2026 14:27:36 +0530 Subject: [PATCH 19/85] Fixed whisper and ollama URL in .env.example. Fix #537 --- docker/.env.example | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index c4321ac7..4532e0d7 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1,5 +1,4 @@ -# Copy this file to .env.dev or .env.prod and fill in values. -# Never commit .env.dev or .env.prod — they are gitignored. +# Copy this file to .env.dev and fill in values. # --- App ------------------------------------------------------------------ APP_PORT=8000 @@ -7,14 +6,14 @@ APP_PORT=8000 # --- Ollama --------------------------------------------------------------- # Ollama runs in Docker with port mapped to host. App runs on host. # Override to point at an external Ollama instance (e.g. a GPU server). -OLLAMA_HOST=http://localhost:11434 +OLLAMA_HOST=http://ollama:11434 OLLAMA_MODEL=qwen2.5:1.5b OLLAMA_TIMEOUT=300 # --- Whisper -------------------------------------------------------------- # Whisper runs in Docker with port mapped to host. App runs on host. # Override to point at an external Whisper endpoint. -WHISPER_HOST=http://localhost:9000 +WHISPER_HOST=http://whisper:9000 WHISPER_MODEL=small.en # --- Gunicorn (prod only) ------------------------------------------------- @@ -22,6 +21,4 @@ GUNICORN_WORKERS=2 # --- CORS ----------------------------------------------------------------- # Comma-separated list of allowed frontend origins. -# Dev: http://localhost:5173,http://127.0.0.1:5173 -# Prod: https://your-domain.com FRONTEND_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 From 86d232f72bddabcf310da33b3be3cfba6659312f Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 19 Jun 2026 01:44:13 +0530 Subject: [PATCH 20/85] updated dev-docker compose to pull posgres docker image and updated .env.example --- docker/.env.example | 4 ++++ docker/dev/compose.yml | 28 ++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 4532e0d7..026a2318 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -3,6 +3,10 @@ # --- App ------------------------------------------------------------------ APP_PORT=8000 +# --- Database ------------------------------------------------------------- +# Dev: postgres runs inside Docker Compose, no config needed. +DATABASE_URL=postgresql://fireform:fireform@localhost:5432/fireform + # --- Ollama --------------------------------------------------------------- # Ollama runs in Docker with port mapped to host. App runs on host. # Override to point at an external Ollama instance (e.g. a GPU server). diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml index cdb94ccd..83de9243 100644 --- a/docker/dev/compose.yml +++ b/docker/dev/compose.yml @@ -1,4 +1,23 @@ services: + postgres: + image: postgres:17-alpine + container_name: fireform-postgres + environment: + POSTGRES_USER: fireform + POSTGRES_PASSWORD: fireform + POSTGRES_DB: fireform + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" + networks: + - fireform-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fireform"] + interval: 10s + timeout: 5s + retries: 5 + ollama: image: ollama/ollama:latest container_name: fireform-ollama @@ -41,6 +60,8 @@ services: dockerfile: docker/dev/Dockerfile container_name: fireform-app depends_on: + postgres: + condition: service_healthy ollama: condition: service_healthy whisper: @@ -49,7 +70,6 @@ services: command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] volumes: - ../..:/app - - fireform_db:/data/db - fireform_uploads:/data/uploads ports: - "${APP_PORT:-8000}:8000" @@ -57,11 +77,11 @@ services: - PYTHONUNBUFFERED=1 - CUDA_VISIBLE_DEVICES= - PYTHONPATH=/app + - DATABASE_URL=postgresql://fireform:fireform@postgres:5432/fireform - OLLAMA_HOST=${OLLAMA_HOST:-http://ollama:11434} - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT:-300} - OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:1.5b} - WHISPER_HOST=${WHISPER_HOST:-http://whisper:9000} - - FIREFORM_DB_PATH=/data/db/fireform.db - FIREFORM_DATA_DIR=/data/uploads - FIREFORM_TEMPLATE_DIR=/data/uploads - FIREFORM_DB_ECHO=${FIREFORM_DB_ECHO:-true} @@ -70,12 +90,12 @@ services: - fireform-network volumes: + postgres_data: + driver: local ollama_data: driver: local whisper_models: driver: local - fireform_db: - driver: local fireform_uploads: driver: local From 3d20567fa8a1098bc3772f9ccea744c43ae16bb7 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 19 Jun 2026 01:45:31 +0530 Subject: [PATCH 21/85] removed sqlite db path from getting created through entrypoint script --- docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 84a81429..8694d9ff 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,7 +2,7 @@ set -e # Ensure data directories exist (volumes may be empty on first run) -mkdir -p /data/db /data/uploads +mkdir -p /data/uploads # Run DB migrations / init before starting the server python3 -m app.db.init_db From bd645432324709974d1841103d31ca550b661882 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 19 Jun 2026 01:53:09 +0530 Subject: [PATCH 22/85] removed unwanted variables in config.py, Updated fallback of database_url --- app/core/config.py | 9 ++++----- app/db/database.py | 6 +----- requirements.txt | 1 + 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index 055bbadd..d6f723df 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -26,11 +26,10 @@ 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}" +DATABASE_URL = os.getenv( + "DATABASE_URL", + "postgresql://fireform:fireform@localhost:5432/fireform", +) DB_ECHO = os.getenv("FIREFORM_DB_ECHO", "true").lower() == "true" # --- External services ---------------------------------------------------- diff --git a/app/db/database.py b/app/db/database.py index 0437afd1..08f95293 100644 --- a/app/db/database.py +++ b/app/db/database.py @@ -2,11 +2,7 @@ from app.core.config import DATABASE_URL, DB_ECHO -engine = create_engine( - DATABASE_URL, - echo=DB_ECHO, - connect_args={"check_same_thread": False}, -) +engine = create_engine(DATABASE_URL, echo=DB_ECHO) def get_session(): diff --git a/requirements.txt b/requirements.txt index a22229d3..5f57f064 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ fastapi uvicorn pydantic sqlmodel +psycopg2-binary pytest httpx numpy<2 From 271e4ba59e822dae666fa8023ad37d0ac96456e5 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 19 Jun 2026 01:59:05 +0530 Subject: [PATCH 23/85] added a seperate docker compose for isolated postgres db instance. Removed sqlite references and updated with database_url --- docker/prod/compose.postgres.yml | 22 ++++++++++++++++++++++ docker/prod/compose.yml | 5 +---- 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 docker/prod/compose.postgres.yml diff --git a/docker/prod/compose.postgres.yml b/docker/prod/compose.postgres.yml new file mode 100644 index 00000000..30e13dc5 --- /dev/null +++ b/docker/prod/compose.postgres.yml @@ -0,0 +1,22 @@ +services: + postgres: + image: postgres:17-alpine + container_name: fireform-postgres-prod + environment: + POSTGRES_USER: ${POSTGRES_USER:-fireform} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-fireform} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-fireform}"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local diff --git a/docker/prod/compose.yml b/docker/prod/compose.yml index edd2fa49..9ac8f39f 100644 --- a/docker/prod/compose.yml +++ b/docker/prod/compose.yml @@ -53,7 +53,6 @@ services: "--access-logfile", "-", "--error-logfile", "-"] volumes: - - fireform_db:/data/db - fireform_uploads:/data/uploads ports: - "${APP_PORT}:8000" @@ -61,11 +60,11 @@ services: - PYTHONUNBUFFERED=1 - CUDA_VISIBLE_DEVICES= - PYTHONPATH=/app + - DATABASE_URL=${DATABASE_URL} - OLLAMA_HOST=${OLLAMA_HOST} - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT} - OLLAMA_MODEL=${OLLAMA_MODEL} - WHISPER_HOST=${WHISPER_HOST} - - FIREFORM_DB_PATH=/data/db/fireform.db - FIREFORM_DATA_DIR=/data/uploads - FIREFORM_TEMPLATE_DIR=/data/uploads - FIREFORM_DB_ECHO=false @@ -80,8 +79,6 @@ volumes: driver: local whisper_models: driver: local - fireform_db: - driver: local fireform_uploads: driver: local From 89239c828d59a46dce42f62b6228cdfa27741f7c Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Fri, 19 Jun 2026 02:04:23 +0530 Subject: [PATCH 24/85] Removed unwanted dockerfile and compose. --- Dockerfile | 28 ------------- docker-compose.yml | 97 ---------------------------------------------- 2 files changed, 125 deletions(-) delete mode 100644 Dockerfile delete mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b7b00cef..00000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ - -FROM python:3.11-slim - -WORKDIR /app - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - curl \ - libgl1 \ - libglib2.0-0 \ - libxcb1 \ - && rm -rf /var/lib/apt/lists/* - -# Copy and install Python dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Copy application code -COPY . . - -# 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", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 45ad0341..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,97 +0,0 @@ -services: - ollama: - image: ollama/ollama:latest - container_name: fireform-ollama - ports: - - "127.0.0.1:11434:11434" - volumes: - - ollama_data:/root/.ollama - networks: - - fireform-network - healthcheck: - test: ["CMD-SHELL", "ollama list || exit 1"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - - whisper: - # Multi-arch (arm64 + amd64) Whisper ASR service — runs natively on Apple - # Silicon. Uses the faster-whisper (CTranslate2) engine and bundles ffmpeg, - # so it accepts any audio the browser produces. Model is pulled from - # Hugging Face on first request into the whisper_models volume. - image: onerahmet/openai-whisper-asr-webservice:latest - container_name: fireform-whisper - environment: - - ASR_ENGINE=faster_whisper - - ASR_MODEL=small.en - - ASR_MODEL_PATH=/data/whisper - volumes: - - whisper_models:/data/whisper - ports: - - "127.0.0.1:9000:9000" - networks: - - fireform-network - healthcheck: - test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9000/docs')\" || exit 1"] - interval: 15s - timeout: 5s - retries: 5 - start_period: 60s - - app: - build: - context: . - dockerfile: Dockerfile - container_name: fireform-app - depends_on: - ollama: - condition: service_healthy - whisper: - condition: service_started - command: /bin/sh -c "python3 -m app.db.init_db && python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000" - volumes: - - .:/app - # Persist the SQLite DB (~/.fireform) across container rebuilds so created - # templates aren't wiped each time the image is recreated. - - fireform_db:/root/.fireform - ports: - - "8000:8000" - environment: - - PYTHONUNBUFFERED=1 - - CUDA_VISIBLE_DEVICES="" - # Fix #118 #116 — correct PYTHONPATH to project root - - PYTHONPATH=/app - - OLLAMA_HOST=http://ollama:11434 - - OLLAMA_TIMEOUT=300 - - OLLAMA_MODEL=qwen2.5:1.5b - - WHISPER_HOST=http://whisper:9000 - networks: - - fireform-network - - frontend: - image: python:3.11-slim - container_name: fireform-frontend - working_dir: /app - command: python3 -m http.server 5173 --directory frontend - volumes: - - .:/app - ports: - - "5173:5173" - depends_on: - app: - condition: service_started - networks: - - fireform-network - -volumes: - ollama_data: - driver: local - whisper_models: - driver: local - fireform_db: - driver: local - -networks: - fireform-network: - driver: bridge From d60ce42c0fcdf158605894753cd9f8909d7888bc Mon Sep 17 00:00:00 2001 From: marcvergees Date: Fri, 19 Jun 2026 15:20:48 -0700 Subject: [PATCH 25/85] feat: :sparkles: first simple approach introducing fetching data --- Makefile | 2 +- app/api/router.py | 3 +- app/api/routes/weather.py | 19 ++++++ app/services/controller.py | 8 ++- app/services/external_apis_coordinator.py | 70 +++++++++++++++++++++++ requirements.txt | 4 ++ 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 app/api/routes/weather.py create mode 100644 app/services/external_apis_coordinator.py diff --git a/Makefile b/Makefile index 13ea3665..7d1d3575 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ fireform: build up @echo "Run 'make logs' to view live logs, 'make down' to stop." build: - @$(COMPOSE) build --progress=quiet + @$(COMPOSE) build up: @$(COMPOSE) up -d diff --git a/app/api/router.py b/app/api/router.py index ba9c0b56..b66875b2 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -5,8 +5,9 @@ from fastapi import APIRouter -from app.api.routes import forms, templates +from app.api.routes import forms, templates, weather api_router = APIRouter() api_router.include_router(templates.router) api_router.include_router(forms.router) +api_router.include_router(weather.router) \ No newline at end of file diff --git a/app/api/routes/weather.py b/app/api/routes/weather.py new file mode 100644 index 00000000..babc254a --- /dev/null +++ b/app/api/routes/weather.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter + +from app.core.errors.base import AppError +from app.services.controller import Controller + +router = APIRouter(prefix="/weather", tags=["weather"]) + + +@router.get("/forecast") +def get_weather_forecast(latitude: float, longitude: float): + """ + Fetch weather forecast data for the given coordinates. + """ + controller = Controller() + try: + weather_data = controller.get_weather(latitude, longitude) + return weather_data + except Exception as e: + raise AppError(str(e), status_code=500) \ No newline at end of file diff --git a/app/services/controller.py b/app/services/controller.py index 49c10e48..28eae503 100644 --- a/app/services/controller.py +++ b/app/services/controller.py @@ -1,11 +1,17 @@ from app.services.file_manipulator import FileManipulator +from app.services.external_apis_coordinator import ExternalAPIsCoordinator + class Controller: def __init__(self): self.file_manipulator = FileManipulator() + self.external_apis_coordinator = ExternalAPIsCoordinator() def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str = None): return self.file_manipulator.fill_form(user_input, fields, pdf_form_path, model=model) def prepare_fillable(self, pdf_path: str): - return self.file_manipulator.prepare_fillable(pdf_path) \ No newline at end of file + return self.file_manipulator.prepare_fillable(pdf_path) + + def get_weather(self, latitude: float, longitude: float): + return self.external_apis_coordinator.get_weather(latitude, longitude) \ No newline at end of file diff --git a/app/services/external_apis_coordinator.py b/app/services/external_apis_coordinator.py new file mode 100644 index 00000000..984368c9 --- /dev/null +++ b/app/services/external_apis_coordinator.py @@ -0,0 +1,70 @@ +import openmeteo_requests + +import pandas as pd +import requests_cache +from retry_requests import retry + +class ExternalAPIsCoordinator: + def __init__(self): + pass + + def get_weather(self, latitude: float, longitude: float): + """ + Get weather data for a given latitude and longitude. + + Args: + latitude (float): The latitude of the location. + longitude (float): The longitude of the location. + + Returns: + dict: A dictionary containing the weather data. + """ + + # Setup the Open-Meteo API client with cache and retry on error + cache_session = requests_cache.CachedSession('.cache', expire_after = 3600) + retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2) + openmeteo = openmeteo_requests.Client(session = retry_session) + + # Make sure all required weather variables are listed here + # The order of variables in hourly or daily is important to assign them correctly below + url = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": latitude, + "longitude": longitude, + "hourly": "temperature_2m", + } + responses = openmeteo.weather_api(url, params = params) + + # Process first location. Add a for-loop for multiple locations or weather models + response = responses[0] + print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E") + print(f"Elevation: {response.Elevation()} m asl") + print(f"Timezone difference to GMT+0: {response.UtcOffsetSeconds()}s") + + # Process hourly data. The order of variables needs to be the same as requested. + hourly = response.Hourly() + hourly_temperature_2m = hourly.Variables(0).ValuesAsNumpy() + + hourly_data = { + "date": pd.date_range( + start = pd.to_datetime(hourly.Time(), unit = "s", utc = True), + end = pd.to_datetime(hourly.TimeEnd(), unit = "s", utc = True), + freq = pd.Timedelta(seconds = hourly.Interval()), + inclusive = "left" + ) + } + + hourly_data["temperature_2m"] = hourly_temperature_2m.tolist() + + # Convert pandas Timestamp objects to ISO strings for JSON serialization + hourly_data["date"] = [d.isoformat() for d in hourly_data["date"]] + + return { + "latitude": response.Latitude(), + "longitude": response.Longitude(), + "elevation": response.Elevation(), + "timezone": response.Timezone(), + "timezone_abbreviation": response.TimezoneAbbreviation(), + "utc_offset_seconds": response.UtcOffsetSeconds(), + "hourly": hourly_data + } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5f57f064..fb23c427 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,7 @@ numpy<2 ollama pypdf python-multipart +openmeteo-requests +requests-cache +retry-requests +pandas \ No newline at end of file From 4994cfb2ae2a8b0fae39c76740690b586d9bd8aa Mon Sep 17 00:00:00 2001 From: marcvergees Date: Fri, 19 Jun 2026 16:38:52 -0700 Subject: [PATCH 26/85] feat: :sparkles: multiple weather fields can be fetched, fetched data graphically shown and incorporated to the LLM input --- app/api/routes/weather.py | 27 ++++++++++- app/services/controller.py | 4 +- app/services/external_apis_coordinator.py | 58 +++++++++++++++-------- 3 files changed, 64 insertions(+), 25 deletions(-) diff --git a/app/api/routes/weather.py b/app/api/routes/weather.py index babc254a..89c5bfd4 100644 --- a/app/api/routes/weather.py +++ b/app/api/routes/weather.py @@ -5,15 +5,38 @@ router = APIRouter(prefix="/weather", tags=["weather"]) +ALL_HOURLY_FIELDS = [ + "temperature_2m", + "relative_humidity_2m", + "rain", + "apparent_temperature", + "precipitation_probability", + "precipitation", + "wind_direction_10m", + "wind_speed_10m", + "soil_temperature_0cm", + "uv_index", +] + @router.get("/forecast") -def get_weather_forecast(latitude: float, longitude: float): +def get_weather_forecast( + latitude: float, + longitude: float, + fields: str | None = None, +): """ Fetch weather forecast data for the given coordinates. + + Query params: + - latitude, longitude: required floats + - fields: optional comma-separated list of hourly variables to include. + Defaults to all supported fields when omitted. """ controller = Controller() try: - weather_data = controller.get_weather(latitude, longitude) + requested = [f.strip() for f in fields.split(",") if f.strip()] if fields else [] + weather_data = controller.get_weather(latitude, longitude, hourly_fields=requested) return weather_data except Exception as e: raise AppError(str(e), status_code=500) \ No newline at end of file diff --git a/app/services/controller.py b/app/services/controller.py index 28eae503..248c7c70 100644 --- a/app/services/controller.py +++ b/app/services/controller.py @@ -13,5 +13,5 @@ def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: st def prepare_fillable(self, pdf_path: str): return self.file_manipulator.prepare_fillable(pdf_path) - def get_weather(self, latitude: float, longitude: float): - return self.external_apis_coordinator.get_weather(latitude, longitude) \ No newline at end of file + def get_weather(self, latitude: float, longitude: float, hourly_fields: list = None): + return self.external_apis_coordinator.get_weather(latitude, longitude, hourly_fields=hourly_fields) \ No newline at end of file diff --git a/app/services/external_apis_coordinator.py b/app/services/external_apis_coordinator.py index 984368c9..407029c2 100644 --- a/app/services/external_apis_coordinator.py +++ b/app/services/external_apis_coordinator.py @@ -4,58 +4,74 @@ import requests_cache from retry_requests import retry +# All supported hourly fields exposed by this service +ALL_HOURLY_FIELDS = [ + "temperature_2m", + "relative_humidity_2m", + "rain", + "apparent_temperature", + "precipitation_probability", + "precipitation", + "wind_direction_10m", + "wind_speed_10m", + "soil_temperature_0cm", + "uv_index", +] + + class ExternalAPIsCoordinator: def __init__(self): pass - - def get_weather(self, latitude: float, longitude: float): + + def get_weather(self, latitude: float, longitude: float, hourly_fields: list[str] | None = None): """ Get weather data for a given latitude and longitude. Args: latitude (float): The latitude of the location. longitude (float): The longitude of the location. + hourly_fields (list[str] | None): Subset of hourly variables to request. + Defaults to ALL_HOURLY_FIELDS when None or empty. Returns: dict: A dictionary containing the weather data. """ - + requested = [f for f in (hourly_fields or []) if f in ALL_HOURLY_FIELDS] + if not requested: + requested = list(ALL_HOURLY_FIELDS) + # Setup the Open-Meteo API client with cache and retry on error - cache_session = requests_cache.CachedSession('.cache', expire_after = 3600) - retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2) - openmeteo = openmeteo_requests.Client(session = retry_session) + cache_session = requests_cache.CachedSession('.cache', expire_after=3600) + retry_session = retry(cache_session, retries=5, backoff_factor=0.2) + openmeteo = openmeteo_requests.Client(session=retry_session) - # Make sure all required weather variables are listed here - # The order of variables in hourly or daily is important to assign them correctly below url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": latitude, "longitude": longitude, - "hourly": "temperature_2m", + "hourly": requested, } - responses = openmeteo.weather_api(url, params = params) + responses = openmeteo.weather_api(url, params=params) - # Process first location. Add a for-loop for multiple locations or weather models response = responses[0] print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E") print(f"Elevation: {response.Elevation()} m asl") print(f"Timezone difference to GMT+0: {response.UtcOffsetSeconds()}s") - # Process hourly data. The order of variables needs to be the same as requested. hourly = response.Hourly() - hourly_temperature_2m = hourly.Variables(0).ValuesAsNumpy() - hourly_data = { + hourly_data: dict = { "date": pd.date_range( - start = pd.to_datetime(hourly.Time(), unit = "s", utc = True), - end = pd.to_datetime(hourly.TimeEnd(), unit = "s", utc = True), - freq = pd.Timedelta(seconds = hourly.Interval()), - inclusive = "left" + start=pd.to_datetime(hourly.Time(), unit="s", utc=True), + end=pd.to_datetime(hourly.TimeEnd(), unit="s", utc=True), + freq=pd.Timedelta(seconds=hourly.Interval()), + inclusive="left", ) } - hourly_data["temperature_2m"] = hourly_temperature_2m.tolist() - + for i, field in enumerate(requested): + hourly_data[field] = hourly.Variables(i).ValuesAsNumpy().tolist() + # Convert pandas Timestamp objects to ISO strings for JSON serialization hourly_data["date"] = [d.isoformat() for d in hourly_data["date"]] @@ -66,5 +82,5 @@ def get_weather(self, latitude: float, longitude: float): "timezone": response.Timezone(), "timezone_abbreviation": response.TimezoneAbbreviation(), "utc_offset_seconds": response.UtcOffsetSeconds(), - "hourly": hourly_data + "hourly": hourly_data, } \ No newline at end of file From 83cdddddb18ddde423d0b84a0a0107cbe3799029 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Fri, 19 Jun 2026 18:07:49 -0700 Subject: [PATCH 27/85] feat: :sparkles: start and end date --- app/api/routes/weather.py | 4 +- app/services/controller.py | 4 +- app/services/external_apis/weather_api.py | 87 ++++++++++++++++++++++ app/services/external_apis_coordinator.py | 88 ++--------------------- 4 files changed, 97 insertions(+), 86 deletions(-) create mode 100644 app/services/external_apis/weather_api.py diff --git a/app/api/routes/weather.py b/app/api/routes/weather.py index 89c5bfd4..2e5b493b 100644 --- a/app/api/routes/weather.py +++ b/app/api/routes/weather.py @@ -23,6 +23,8 @@ def get_weather_forecast( latitude: float, longitude: float, + start_date: str, + end_date: str, fields: str | None = None, ): """ @@ -36,7 +38,7 @@ def get_weather_forecast( controller = Controller() try: requested = [f.strip() for f in fields.split(",") if f.strip()] if fields else [] - weather_data = controller.get_weather(latitude, longitude, hourly_fields=requested) + weather_data = controller.get_weather(latitude, longitude, start_date, end_date, hourly_fields=requested) return weather_data except Exception as e: raise AppError(str(e), status_code=500) \ No newline at end of file diff --git a/app/services/controller.py b/app/services/controller.py index 248c7c70..ae79ecc8 100644 --- a/app/services/controller.py +++ b/app/services/controller.py @@ -13,5 +13,5 @@ def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: st def prepare_fillable(self, pdf_path: str): return self.file_manipulator.prepare_fillable(pdf_path) - def get_weather(self, latitude: float, longitude: float, hourly_fields: list = None): - return self.external_apis_coordinator.get_weather(latitude, longitude, hourly_fields=hourly_fields) \ No newline at end of file + def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list = None): + return self.external_apis_coordinator.get_weather(latitude, longitude, start_date, end_date, hourly_fields) \ No newline at end of file diff --git a/app/services/external_apis/weather_api.py b/app/services/external_apis/weather_api.py new file mode 100644 index 00000000..92489bc7 --- /dev/null +++ b/app/services/external_apis/weather_api.py @@ -0,0 +1,87 @@ +import openmeteo_requests + +import pandas as pd +import requests_cache +from retry_requests import retry + +# All supported hourly fields exposed by this service +ALL_HOURLY_FIELDS = [ + "temperature_2m", + "relative_humidity_2m", + "rain", + "apparent_temperature", + "precipitation_probability", + "precipitation", + "wind_direction_10m", + "wind_speed_10m", + "soil_temperature_0cm", + "uv_index", +] + +class WeatherAPI: + def __init__(self): + pass + + def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list[str] | None = None): + """ + Get weather data for a given latitude and longitude. + + Args: + latitude (float): The latitude of the location. + longitude (float): The longitude of the location. + hourly_fields (list[str] | None): Subset of hourly variables to request. + Defaults to ALL_HOURLY_FIELDS when None or empty. + + Returns: + dict: A dictionary containing the weather data. + """ + requested = [f for f in (hourly_fields or []) if f in ALL_HOURLY_FIELDS] + if not requested: + requested = list(ALL_HOURLY_FIELDS) + + # Setup the Open-Meteo API client with cache and retry on error + cache_session = requests_cache.CachedSession('.cache', expire_after=3600) + retry_session = retry(cache_session, retries=5, backoff_factor=0.2) + openmeteo = openmeteo_requests.Client(session=retry_session) + + url = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": latitude, + "longitude": longitude, + "hourly": requested, + "start_date": start_date, + "end_date": end_date, + } + responses = openmeteo.weather_api(url, params=params) + + response = responses[0] + print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E") + print(f"Elevation: {response.Elevation()} m asl") + print(f"Timezone difference to GMT+0: {response.UtcOffsetSeconds()}s") + + hourly = response.Hourly() + + hourly_data: dict = { + "date": pd.date_range( + start=pd.to_datetime(hourly.Time(), unit="s", utc=True), + end=pd.to_datetime(hourly.TimeEnd(), unit="s", utc=True), + freq=pd.Timedelta(seconds=hourly.Interval()), + inclusive="left", + ) + } + + for i, field in enumerate(requested): + hourly_data[field] = hourly.Variables(i).ValuesAsNumpy().tolist() + + # Convert pandas Timestamp objects to ISO strings for JSON serialization + hourly_data["date"] = [d.isoformat() for d in hourly_data["date"]] + + return { + "latitude": response.Latitude(), + "longitude": response.Longitude(), + "elevation": response.Elevation(), + "timezone": response.Timezone(), + "timezone_abbreviation": response.TimezoneAbbreviation(), + "utc_offset_seconds": response.UtcOffsetSeconds(), + "hourly": hourly_data, + } \ No newline at end of file diff --git a/app/services/external_apis_coordinator.py b/app/services/external_apis_coordinator.py index 407029c2..aca735b8 100644 --- a/app/services/external_apis_coordinator.py +++ b/app/services/external_apis_coordinator.py @@ -1,86 +1,8 @@ -import openmeteo_requests - -import pandas as pd -import requests_cache -from retry_requests import retry - -# All supported hourly fields exposed by this service -ALL_HOURLY_FIELDS = [ - "temperature_2m", - "relative_humidity_2m", - "rain", - "apparent_temperature", - "precipitation_probability", - "precipitation", - "wind_direction_10m", - "wind_speed_10m", - "soil_temperature_0cm", - "uv_index", -] - +from app.services.external_apis.weather_api import WeatherAPI class ExternalAPIsCoordinator: def __init__(self): - pass - - def get_weather(self, latitude: float, longitude: float, hourly_fields: list[str] | None = None): - """ - Get weather data for a given latitude and longitude. - - Args: - latitude (float): The latitude of the location. - longitude (float): The longitude of the location. - hourly_fields (list[str] | None): Subset of hourly variables to request. - Defaults to ALL_HOURLY_FIELDS when None or empty. - - Returns: - dict: A dictionary containing the weather data. - """ - requested = [f for f in (hourly_fields or []) if f in ALL_HOURLY_FIELDS] - if not requested: - requested = list(ALL_HOURLY_FIELDS) - - # Setup the Open-Meteo API client with cache and retry on error - cache_session = requests_cache.CachedSession('.cache', expire_after=3600) - retry_session = retry(cache_session, retries=5, backoff_factor=0.2) - openmeteo = openmeteo_requests.Client(session=retry_session) - - url = "https://api.open-meteo.com/v1/forecast" - params = { - "latitude": latitude, - "longitude": longitude, - "hourly": requested, - } - responses = openmeteo.weather_api(url, params=params) - - response = responses[0] - print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E") - print(f"Elevation: {response.Elevation()} m asl") - print(f"Timezone difference to GMT+0: {response.UtcOffsetSeconds()}s") - - hourly = response.Hourly() - - hourly_data: dict = { - "date": pd.date_range( - start=pd.to_datetime(hourly.Time(), unit="s", utc=True), - end=pd.to_datetime(hourly.TimeEnd(), unit="s", utc=True), - freq=pd.Timedelta(seconds=hourly.Interval()), - inclusive="left", - ) - } - - for i, field in enumerate(requested): - hourly_data[field] = hourly.Variables(i).ValuesAsNumpy().tolist() - - # Convert pandas Timestamp objects to ISO strings for JSON serialization - hourly_data["date"] = [d.isoformat() for d in hourly_data["date"]] - - return { - "latitude": response.Latitude(), - "longitude": response.Longitude(), - "elevation": response.Elevation(), - "timezone": response.Timezone(), - "timezone_abbreviation": response.TimezoneAbbreviation(), - "utc_offset_seconds": response.UtcOffsetSeconds(), - "hourly": hourly_data, - } \ No newline at end of file + self.weather_api = WeatherAPI() + + def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list[str] | None = None): + return self.weather_api.get_weather(latitude, longitude, start_date, end_date, hourly_fields) From 97d98a088e18b191cc42b57fd33f56958fa2a7b8 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Fri, 19 Jun 2026 21:01:49 -0700 Subject: [PATCH 28/85] feat: :sparkles: zipcode resolver --- app/api/router.py | 5 +-- app/api/routes/zipcode.py | 42 +++++++++++++++++++++++ app/services/controller.py | 8 ++++- app/services/external_apis/zipcode_api.py | 23 +++++++++++++ app/services/external_apis_coordinator.py | 8 +++++ examples/pgeocode_demo.py | 5 +++ requirements.txt | 3 +- 7 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 app/api/routes/zipcode.py create mode 100644 app/services/external_apis/zipcode_api.py create mode 100644 examples/pgeocode_demo.py diff --git a/app/api/router.py b/app/api/router.py index b66875b2..ea70e65d 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -5,9 +5,10 @@ from fastapi import APIRouter -from app.api.routes import forms, templates, weather +from app.api.routes import forms, templates, weather, zipcode api_router = APIRouter() api_router.include_router(templates.router) api_router.include_router(forms.router) -api_router.include_router(weather.router) \ No newline at end of file +api_router.include_router(weather.router) +api_router.include_router(zipcode.router) \ No newline at end of file diff --git a/app/api/routes/zipcode.py b/app/api/routes/zipcode.py new file mode 100644 index 00000000..10536ee2 --- /dev/null +++ b/app/api/routes/zipcode.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter + +from app.core.errors.base import AppError +from app.services.controller import Controller + +router = APIRouter(prefix="/zipcode", tags=["zipcode"]) + +@router.get("/postal-code") +def get_postal_code( + country: str, + postal_code: str, +): + """ + Fetch data for the given postal code. + + Query params: + - country: required string + - postal_code: required string + """ + controller = Controller() + try: + return controller.get_postal_code(country, postal_code) + except Exception as e: + raise AppError(str(e), status_code=500) + +@router.get("/location") +def get_location( + country: str, + city: str, +): + """ + Fetch data for the given location. + + Query params: + - country: required string + - city: required string + """ + controller = Controller() + try: + return controller.get_location(country, city) + except Exception as e: + raise AppError(str(e), status_code=500) diff --git a/app/services/controller.py b/app/services/controller.py index ae79ecc8..852b0e71 100644 --- a/app/services/controller.py +++ b/app/services/controller.py @@ -14,4 +14,10 @@ def prepare_fillable(self, pdf_path: str): return self.file_manipulator.prepare_fillable(pdf_path) def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list = None): - return self.external_apis_coordinator.get_weather(latitude, longitude, start_date, end_date, hourly_fields) \ No newline at end of file + return self.external_apis_coordinator.get_weather(latitude, longitude, start_date, end_date, hourly_fields) + + def get_postal_code(self, country: str, postal_code: str): + return self.external_apis_coordinator.get_postal_code(country, postal_code) + + def get_location(self, country: str, city: str): + return self.external_apis_coordinator.get_location(country, city) \ No newline at end of file diff --git a/app/services/external_apis/zipcode_api.py b/app/services/external_apis/zipcode_api.py new file mode 100644 index 00000000..b4bfe5da --- /dev/null +++ b/app/services/external_apis/zipcode_api.py @@ -0,0 +1,23 @@ +import pgeocode +import numpy as np +import pandas as pd + +class ZipCodeAPI: + def __init__(self): + pass + + def get_postal_code(self, country: str, postal_code: str): + res = pgeocode.Nominatim(country).query_postal_code(postal_code) + if isinstance(res, pd.Series): + res = res.replace({np.nan: None}).to_dict() + elif isinstance(res, pd.DataFrame): + res = res.replace({np.nan: None}).to_dict(orient='records') + return res + + def get_location(self, country: str, city: str): + res = pgeocode.Nominatim(country).query_location(city) + if isinstance(res, pd.Series): + res = res.replace({np.nan: None}).to_dict() + elif isinstance(res, pd.DataFrame): + res = res.replace({np.nan: None}).to_dict(orient='records') + return res \ No newline at end of file diff --git a/app/services/external_apis_coordinator.py b/app/services/external_apis_coordinator.py index aca735b8..f0c22fd9 100644 --- a/app/services/external_apis_coordinator.py +++ b/app/services/external_apis_coordinator.py @@ -1,8 +1,16 @@ +from app.services.external_apis.zipcode_api import ZipCodeAPI from app.services.external_apis.weather_api import WeatherAPI class ExternalAPIsCoordinator: def __init__(self): self.weather_api = WeatherAPI() + self.zipcode_api = ZipCodeAPI() def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list[str] | None = None): return self.weather_api.get_weather(latitude, longitude, start_date, end_date, hourly_fields) + + def get_postal_code(self, country: str, postal_code: str): + return self.zipcode_api.get_postal_code(country, postal_code) + + def get_location(self, country: str, city: str): + return self.zipcode_api.get_location(country, city) \ No newline at end of file diff --git a/examples/pgeocode_demo.py b/examples/pgeocode_demo.py new file mode 100644 index 00000000..10201a8c --- /dev/null +++ b/examples/pgeocode_demo.py @@ -0,0 +1,5 @@ +import pgeocode + +nomi = pgeocode.Nominatim('us') +response_pgeo = nomi.query_location("Santa Cruz", top_k=3) +print(response_pgeo) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fb23c427..08ad84db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,5 @@ python-multipart openmeteo-requests requests-cache retry-requests -pandas \ No newline at end of file +pandas +pgeocode \ No newline at end of file From 72303382b397867f38793b3a95e4aa8865794e82 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Fri, 19 Jun 2026 21:13:49 -0700 Subject: [PATCH 29/85] docs: :memo: openapi contracts of the new created endpoints --- contracts/openapi.yaml | 10 +++++++ contracts/path/weather.yaml | 47 ++++++++++++++++++++++++++++++ contracts/path/zipcode.yaml | 57 +++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 contracts/path/weather.yaml create mode 100644 contracts/path/zipcode.yaml diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml index 3b19ea3c..781c2217 100644 --- a/contracts/openapi.yaml +++ b/contracts/openapi.yaml @@ -31,6 +31,8 @@ tags: description: Health checks, schema introspection, and system status - name: jobs description: Cross-cutting async job status polling + - name: external-apis + description: Integrations with external APIs (Weather, Zipcode) paths: # ── Layer 1: Input ────────────────────────────────────────────── @@ -96,3 +98,11 @@ paths: # ── Layer 8: Async Jobs ──────────────────────────────────────── /api/v1/jobs/{job_id}: $ref: "path/jobs.yaml#/job_by_id" + + # ── Layer 9: External APIs ───────────────────────────────────── + /api/v1/weather/forecast: + $ref: "path/weather.yaml#/forecast" + /api/v1/zipcode/postal-code: + $ref: "path/zipcode.yaml#/postal_code" + /api/v1/zipcode/location: + $ref: "path/zipcode.yaml#/location" diff --git a/contracts/path/weather.yaml b/contracts/path/weather.yaml new file mode 100644 index 00000000..bf7c46b8 --- /dev/null +++ b/contracts/path/weather.yaml @@ -0,0 +1,47 @@ +forecast: + get: + summary: Fetch weather forecast + description: Fetch weather data for the given location and fields. + tags: + - external-apis + parameters: + - name: latitude + in: query + required: true + schema: + type: number + format: float + - name: longitude + in: query + required: true + schema: + type: number + format: float + - name: start_date + in: query + required: true + schema: + type: string + format: date + - name: end_date + in: query + required: true + schema: + type: string + format: date + - name: fields + in: query + required: false + schema: + type: string + description: Comma-separated list of hourly variables to request. + responses: + '200': + description: Weather data retrieved successfully + content: + application/json: + schema: + type: object + additionalProperties: true + '500': + description: Internal server error diff --git a/contracts/path/zipcode.yaml b/contracts/path/zipcode.yaml new file mode 100644 index 00000000..683b4ac4 --- /dev/null +++ b/contracts/path/zipcode.yaml @@ -0,0 +1,57 @@ +postal_code: + get: + summary: Resolve postal code + description: Fetch data for the given postal code and country. + tags: + - external-apis + parameters: + - name: country + in: query + required: true + schema: + type: string + - name: postal_code + in: query + required: true + schema: + type: string + responses: + '200': + description: Postal code data retrieved successfully + content: + application/json: + schema: + type: object + additionalProperties: true + '500': + description: Internal server error + +location: + get: + summary: Resolve location + description: Fetch data for the given city and country. + tags: + - external-apis + parameters: + - name: country + in: query + required: true + schema: + type: string + - name: city + in: query + required: true + schema: + type: string + responses: + '200': + description: Location data retrieved successfully + content: + application/json: + schema: + type: array + items: + type: object + additionalProperties: true + '500': + description: Internal server error From c6e3a108a699cb8ef2772c570a651e9173b5fde2 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sat, 20 Jun 2026 13:47:00 +0530 Subject: [PATCH 30/85] Add /api/v1/health and system schema endpoints (#542) --- app/api/router.py | 6 + app/api/schemas/system.py | 45 +++++ app/api/v1/__init__.py | 0 app/api/v1/router.py | 13 ++ app/api/v1/routes/__init__.py | 0 app/api/v1/routes/system.py | 184 ++++++++++++++++++++ tests/test_v1_system.py | 312 ++++++++++++++++++++++++++++++++++ 7 files changed, 560 insertions(+) create mode 100644 app/api/schemas/system.py create mode 100644 app/api/v1/__init__.py create mode 100644 app/api/v1/router.py create mode 100644 app/api/v1/routes/__init__.py create mode 100644 app/api/v1/routes/system.py create mode 100644 tests/test_v1_system.py diff --git a/app/api/router.py b/app/api/router.py index ba9c0b56..cba62424 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -1,12 +1,18 @@ """Aggregates every route module into a single API router. Add new feature routers here; main.py only mounts this one router. + +IMPORTANT: Never add prefix="/api/v1" to api_router itself — that would +silently move the existing /templates and /forms routes and break the frontend. +New v1 endpoints live in app/api/v1/ and are included via v1_router below. """ from fastapi import APIRouter from app.api.routes import forms, templates +from app.api.v1.router import v1_router api_router = APIRouter() api_router.include_router(templates.router) api_router.include_router(forms.router) +api_router.include_router(v1_router) diff --git a/app/api/schemas/system.py b/app/api/schemas/system.py new file mode 100644 index 00000000..bafa5cfc --- /dev/null +++ b/app/api/schemas/system.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel + + +class ModelInfo(BaseModel): + name: str + size_gb: float + quantization: str | None = None + loaded: bool + + +class CurrentLoad(BaseModel): + active_requests: int + queued_requests: int + + +class ComponentHealth(BaseModel): + status: str + response_time_ms: int | None = None + detail: str | None = None + model_loaded: str | None = None + ollama_version: str | None = None + models_available: list[ModelInfo] | None = None + current_load: CurrentLoad | None = None + disk_free_gb: float | None = None + + +class HealthComponents(BaseModel): + database: ComponentHealth + ollama: ComponentHealth + whisper: ComponentHealth + storage: ComponentHealth + + +class HealthStatus(BaseModel): + status: str + version: str + uptime_seconds: int | None = None + components: HealthComponents | None = None + + +class SchemaVersion(BaseModel): + version: str + released_at: str + changelog: str | None = None + breaking_changes: bool | None = None diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/api/v1/router.py b/app/api/v1/router.py new file mode 100644 index 00000000..da6a812c --- /dev/null +++ b/app/api/v1/router.py @@ -0,0 +1,13 @@ +"""Aggregates all /api/v1 route modules. + +This router is included ADDITIVELY in app/api/router.py alongside the +existing /templates and /forms routers. Never put prefix="/api/v1" on the +top-level api_router — that would silently move the existing routes. +""" + +from fastapi import APIRouter + +from app.api.v1.routes import system + +v1_router = APIRouter(prefix="/api/v1") +v1_router.include_router(system.router) diff --git a/app/api/v1/routes/__init__.py b/app/api/v1/routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/api/v1/routes/system.py b/app/api/v1/routes/system.py new file mode 100644 index 00000000..b31909c3 --- /dev/null +++ b/app/api/v1/routes/system.py @@ -0,0 +1,184 @@ +"""System endpoints: GET /api/v1/health, /schema/incident, /schema/incident/versions. + +Health contract: contracts/path/system.yaml + contracts/schemas/system.yaml +""" + +import shutil +import time + +import requests +from fastapi import APIRouter +from fastapi.responses import JSONResponse +from sqlalchemy import text + +from app.api.schemas.system import ( + ComponentHealth, + HealthComponents, + HealthStatus, + ModelInfo, +) +from app.core.config import APP_VERSION, DATA_DIR, OLLAMA_HOST, WHISPER_HOST +from app.db.database import engine + +router = APIRouter(tags=["system"]) + +_START_TIME = time.monotonic() + +# Tune-able thresholds. _SLOW_MS is patched in tests to trigger degraded +# without mocking wall-clock time. +_PROBE_TIMEOUT = 5 +_SLOW_MS = 5_000 + + +def _check_database() -> ComponentHealth: + t0 = time.monotonic() + try: + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + elapsed = int((time.monotonic() - t0) * 1000) + return ComponentHealth(status="healthy", response_time_ms=elapsed) + except Exception as exc: + return ComponentHealth(status="unhealthy", detail=str(exc)) + + +def _check_ollama() -> ComponentHealth: + t0 = time.monotonic() + try: + tags_resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=_PROBE_TIMEOUT) + tags_resp.raise_for_status() + elapsed = int((time.monotonic() - t0) * 1000) + + tags_data = tags_resp.json() + + ollama_version: str | None = None + try: + ver_resp = requests.get(f"{OLLAMA_HOST}/api/version", timeout=_PROBE_TIMEOUT) + ver_resp.raise_for_status() + ollama_version = ver_resp.json().get("version") + except Exception: + pass + + running_names: set[str] = set() + model_loaded: str | None = None + try: + ps_resp = requests.get(f"{OLLAMA_HOST}/api/ps", timeout=_PROBE_TIMEOUT) + ps_resp.raise_for_status() + running = ps_resp.json().get("models", []) + running_names = {m.get("name", "") for m in running} + if running: + model_loaded = running[0].get("name") + except Exception: + pass + + raw_models = tags_data.get("models", []) + models_available: list[ModelInfo] = [] + for m in raw_models: + name = m.get("name", "") + size_gb = round(m.get("size", 0) / (1024 ** 3), 2) + quantization = m.get("details", {}).get("quantization_level") + models_available.append( + ModelInfo(name=name, size_gb=size_gb, quantization=quantization, loaded=name in running_names) + ) + + status = "degraded" if elapsed > _SLOW_MS else "healthy" + + return ComponentHealth( + status=status, + response_time_ms=elapsed, + model_loaded=model_loaded, + ollama_version=ollama_version, + models_available=models_available if models_available else None, + # current_load is always None — Ollama exposes no queue-depth API; + # populating it would require fabricating data. + current_load=None, + ) + except requests.exceptions.RequestException as exc: + return ComponentHealth(status="unhealthy", detail=str(exc)) + + +def _check_whisper() -> ComponentHealth: + # The onerahmet/openai-whisper-asr-webservice image does not expose /health. + # Both compose files probe /docs (a FastAPI auto-endpoint always present). + t0 = time.monotonic() + try: + resp = requests.get(f"{WHISPER_HOST}/docs", timeout=_PROBE_TIMEOUT) + resp.raise_for_status() + elapsed = int((time.monotonic() - t0) * 1000) + return ComponentHealth(status="healthy", response_time_ms=elapsed) + except requests.exceptions.RequestException as exc: + return ComponentHealth(status="unhealthy", detail=str(exc)) + + +def _check_storage() -> ComponentHealth: + try: + usage = shutil.disk_usage(DATA_DIR) + disk_free_gb = round(usage.free / (1024 ** 3), 2) + return ComponentHealth(status="healthy", disk_free_gb=disk_free_gb) + except OSError as exc: + return ComponentHealth(status="unhealthy", detail=str(exc)) + + +@router.get( + "/health", + responses={ + 200: {"model": HealthStatus, "description": "System healthy or degraded"}, + 503: {"model": HealthStatus, "description": "System unhealthy, cannot serve requests"}, + }, + summary="System health check", +) +def get_health(): + database = _check_database() + ollama = _check_ollama() + whisper = _check_whisper() + storage = _check_storage() + + components = HealthComponents( + database=database, ollama=ollama, whisper=whisper, storage=storage + ) + + statuses = {database.status, ollama.status, whisper.status, storage.status} + + if database.status == "unhealthy": + overall = "unhealthy" + elif "unhealthy" in statuses or "degraded" in statuses: + overall = "degraded" + else: + overall = "healthy" + + body = HealthStatus( + status=overall, + version=APP_VERSION, + uptime_seconds=int(time.monotonic() - _START_TIME), + components=components, + ) + + http_status = 503 if overall == "unhealthy" else 200 + return JSONResponse( + content=body.model_dump(exclude_none=True), + status_code=http_status, + ) + + +@router.get("/schema/incident", summary="Get canonical incident JSON Schema") +def get_schema_incident(): + # PROVISIONAL: This 501 body shape is not canonical — it will be updated + # to match the standardized ErrorResponse envelope once #543 lands. + return JSONResponse( + status_code=501, + content={ + "error_code": "NOT_IMPLEMENTED", + "message": "Incident schema not yet finalised — see issue #555", + }, + ) + + +@router.get("/schema/incident/versions", summary="Get incident schema version history") +def get_schema_versions(): + # PROVISIONAL: Same as /schema/incident — shape will align with #543. + return JSONResponse( + status_code=501, + content={ + "error_code": "NOT_IMPLEMENTED", + "message": "Schema version history not yet available — see issue #555", + }, + ) diff --git a/tests/test_v1_system.py b/tests/test_v1_system.py new file mode 100644 index 00000000..a9903be3 --- /dev/null +++ b/tests/test_v1_system.py @@ -0,0 +1,312 @@ +"""Tests for GET /api/v1/health, /schema/incident, /schema/incident/versions. + +External dependencies (Ollama, Whisper, disk, DB) are mocked at the route-module +boundary. The health check uses engine.connect() directly (not the get_db +dependency) so we mock system_mod.engine explicitly in every test. +""" + +from unittest.mock import MagicMock + +import pytest +import requests as requests_lib + +import app.api.v1.routes.system as system_mod + + +# --------------------------------------------------------------------------- +# Low-level helpers +# --------------------------------------------------------------------------- + +def _mock_engine_healthy(): + """Engine whose connect() succeeds — supports the 'with engine.connect()' pattern.""" + mock = MagicMock() + # MagicMock already supports __enter__/__exit__ automatically + return mock + + +def _mock_engine_down(): + mock = MagicMock() + mock.connect.side_effect = Exception("Connection refused") + return mock + + +def _mock_engine_execute_fails(): + """connect() succeeds (context manager enters) but execute() raises — simulates + a connection acquired then lost, or a query timeout: the more realistic outage path. + """ + mock = MagicMock() + conn = MagicMock() + conn.execute.side_effect = Exception("query failed: timeout") + mock.connect.return_value.__enter__.return_value = conn + return mock + + +def _mock_shutil(free_bytes: int = 120 * 1024 ** 3, raise_oserror: bool = False): + """Return a shutil-shaped object whose disk_usage behaves as configured.""" + mock = MagicMock() + if raise_oserror: + mock.disk_usage.side_effect = OSError("disk error") + else: + usage = MagicMock() + usage.free = free_bytes + mock.disk_usage.return_value = usage + return mock + + +def _make_mock_response(json_data=None, status_code=200): + m = MagicMock() + m.status_code = status_code + m.json.return_value = json_data or {} + m.raise_for_status.return_value = None + return m + + +# --------------------------------------------------------------------------- +# Canned Ollama / Whisper responses +# --------------------------------------------------------------------------- + +_TAGS_RESPONSE = { + "models": [ + {"name": "llama3:8b", "size": 5_000_000_000, "details": {"quantization_level": "Q4_K_M"}}, + {"name": "mistral:7b", "size": 4_000_000_000, "details": {"quantization_level": "Q4_0"}}, + ] +} +_VERSION_RESPONSE = {"version": "0.3.0"} +_PS_RESPONSE = {"models": [{"name": "llama3:8b"}]} +_PS_EMPTY = {"models": []} + + +def _fake_get_all_healthy(url, timeout=None): + """All external services reachable and healthy.""" + if "/api/tags" in url: + return _make_mock_response(_TAGS_RESPONSE) + if "/api/version" in url: + return _make_mock_response(_VERSION_RESPONSE) + if "/api/ps" in url: + return _make_mock_response(_PS_RESPONSE) + if url.endswith("/docs"): + return _make_mock_response() + raise ValueError(f"Unexpected URL in test: {url}") + + +def _fake_get_ollama_down(url, timeout=None): + """Ollama is unreachable; Whisper is healthy.""" + if "/api/tags" in url or "/api/version" in url or "/api/ps" in url: + raise requests_lib.exceptions.ConnectionError("Ollama down") + if url.endswith("/docs"): + return _make_mock_response() + raise ValueError(f"Unexpected URL in test: {url}") + + +def _fake_get_whisper_down(url, timeout=None): + """Whisper is unreachable; Ollama is healthy.""" + if url.endswith("/docs"): + raise requests_lib.exceptions.ConnectionError("Whisper down") + if "/api/tags" in url: + return _make_mock_response(_TAGS_RESPONSE) + if "/api/version" in url: + return _make_mock_response(_VERSION_RESPONSE) + if "/api/ps" in url: + return _make_mock_response(_PS_EMPTY) + raise ValueError(f"Unexpected URL in test: {url}") + + +# --------------------------------------------------------------------------- +# Health endpoint +# --------------------------------------------------------------------------- + +class TestHealthEndpoint: + + def test_all_healthy(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "healthy" + assert body["version"] is not None + assert body["uptime_seconds"] >= 0 + + comps = body["components"] + assert comps["database"]["status"] == "healthy" + assert comps["ollama"]["status"] == "healthy" + assert comps["whisper"]["status"] == "healthy" + assert comps["storage"]["status"] == "healthy" + + def test_ollama_down_returns_200_degraded(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_ollama_down) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "degraded" + assert body["components"]["ollama"]["status"] == "unhealthy" + assert body["components"]["database"]["status"] == "healthy" + assert body["components"]["whisper"]["status"] == "healthy" + + def test_whisper_down_returns_200_degraded(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_whisper_down) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "degraded" + assert body["components"]["whisper"]["status"] == "unhealthy" + assert body["components"]["ollama"]["status"] == "healthy" + + def test_db_down_returns_503_unhealthy(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_down()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 503 + body = resp.json() + assert body["status"] == "unhealthy" + assert body["components"]["database"]["status"] == "unhealthy" + assert "Connection refused" in body["components"]["database"]["detail"] + + def test_db_execute_fails_returns_503_unhealthy(self, client, monkeypatch): + """connect() succeeds but execute() raises — the realistic outage path where + a connection is acquired then the query times out or the server drops it. + """ + monkeypatch.setattr(system_mod, "engine", _mock_engine_execute_fails()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 503 + body = resp.json() + assert body["status"] == "unhealthy" + assert body["components"]["database"]["status"] == "unhealthy" + assert "query failed" in body["components"]["database"]["detail"] + + def test_current_load_not_fabricated(self, client, monkeypatch): + """current_load must be absent or null — never a made-up value.""" + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + ollama_comp = resp.json()["components"]["ollama"] + assert ollama_comp.get("current_load") is None + + def test_ollama_slow_returns_degraded(self, client, monkeypatch): + """Patch _SLOW_MS to -1 so any measured elapsed time triggers degraded, + without depending on wall-clock timing in CI. + """ + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr(system_mod, "_SLOW_MS", -1) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "degraded" + assert body["components"]["ollama"]["status"] == "degraded" + + def test_models_available_and_loaded_flag(self, client, monkeypatch): + """models_available is built from /api/tags; loaded=True only for models in /api/ps.""" + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + models = resp.json()["components"]["ollama"]["models_available"] + assert len(models) == 2 + llama = next(m for m in models if m["name"] == "llama3:8b") + mistral = next(m for m in models if m["name"] == "mistral:7b") + assert llama["loaded"] is True + assert mistral["loaded"] is False + assert llama["quantization"] == "Q4_K_M" + assert llama["size_gb"] == pytest.approx(4.66, abs=0.1) + + def test_model_loaded_reflects_running_model(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + ollama = resp.json()["components"]["ollama"] + assert ollama["model_loaded"] == "llama3:8b" + + def test_ollama_version_present(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + ollama = resp.json()["components"]["ollama"] + assert ollama["ollama_version"] == "0.3.0" + + def test_storage_disk_free_present(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil(free_bytes=200 * 1024 ** 3)) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + storage = resp.json()["components"]["storage"] + assert storage["status"] == "healthy" + assert storage["disk_free_gb"] == pytest.approx(200.0, abs=0.1) + + def test_storage_unavailable_is_degraded(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil(raise_oserror=True)) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "degraded" + assert body["components"]["storage"]["status"] == "unhealthy" + + def test_uptime_seconds_is_non_negative(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + assert resp.json()["uptime_seconds"] >= 0 + + def test_existing_routes_not_displaced(self, client): + """Sanity: adding v1_router must not break the existing /templates endpoint.""" + resp = client.get("/templates") + assert resp.status_code == 200 + + +# --------------------------------------------------------------------------- +# Schema stub endpoints +# --------------------------------------------------------------------------- + +class TestSchemaStubEndpoints: + + def test_schema_incident_returns_501(self, client): + resp = client.get("/api/v1/schema/incident") + assert resp.status_code == 501 + body = resp.json() + assert body["error_code"] == "NOT_IMPLEMENTED" + assert "555" in body["message"] + + def test_schema_versions_returns_501(self, client): + resp = client.get("/api/v1/schema/incident/versions") + assert resp.status_code == 501 + body = resp.json() + assert body["error_code"] == "NOT_IMPLEMENTED" From 61586cd3af2766651596d080314a7a193fa11ced Mon Sep 17 00:00:00 2001 From: marcvergees Date: Sat, 20 Jun 2026 16:28:55 -0700 Subject: [PATCH 31/85] feat: :sparkles: Coordinate format documented, date format specified, fields parameter explained and zipcode approach changed --- app/api/routes/weather.py | 8 +- app/api/routes/zipcode.py | 38 +++------ app/services/controller.py | 7 +- app/services/external_apis/weather_api.py | 3 - app/services/external_apis/zipcode_api.py | 85 +++++++++++++++----- app/services/external_apis_coordinator.py | 7 +- contracts/path/weather.yaml | 67 +++++++++++++++- contracts/path/zipcode.yaml | 97 ++++++++++++++--------- requirements.txt | 2 +- 9 files changed, 209 insertions(+), 105 deletions(-) diff --git a/app/api/routes/weather.py b/app/api/routes/weather.py index 2e5b493b..5426dcdb 100644 --- a/app/api/routes/weather.py +++ b/app/api/routes/weather.py @@ -28,12 +28,10 @@ def get_weather_forecast( fields: str | None = None, ): """ - Fetch weather forecast data for the given coordinates. + Fetch hourly weather forecast data for the given coordinates and date range. - Query params: - - latitude, longitude: required floats - - fields: optional comma-separated list of hourly variables to include. - Defaults to all supported fields when omitted. + Coordinates must be supplied as **decimal degrees** — not degree/minute/second notation. E.g. -122.4194, 37.7749. + Dates must use the **YYYY-MM-DD** format (ISO 8601). """ controller = Controller() try: diff --git a/app/api/routes/zipcode.py b/app/api/routes/zipcode.py index 10536ee2..c54c2873 100644 --- a/app/api/routes/zipcode.py +++ b/app/api/routes/zipcode.py @@ -5,38 +5,24 @@ router = APIRouter(prefix="/zipcode", tags=["zipcode"]) -@router.get("/postal-code") -def get_postal_code( - country: str, - postal_code: str, -): - """ - Fetch data for the given postal code. - Query params: - - country: required string - - postal_code: required string +@router.get("/lookup-address") +def lookup_address(address: str): """ - controller = Controller() - try: - return controller.get_postal_code(country, postal_code) - except Exception as e: - raise AppError(str(e), status_code=500) + Resolve a free-form address to one or more geographic locations including + postal code, latitude, longitude, place name, state, county, and country. -@router.get("/location") -def get_location( - country: str, - city: str, -): - """ - Fetch data for the given location. + Returns a list of matching results ordered by relevance (most relevant first). + Each result is guaranteed to include `latitude` and `longitude`; `postal_code` + is present when the geocoding service can determine it for the given address. - Query params: - - country: required string - - city: required string + Example: + address = "1600 Amphitheatre Parkway, Mountain View, CA, US" """ controller = Controller() try: - return controller.get_location(country, city) + return controller.lookup_address(address) + except TimeoutError as e: + raise AppError(str(e), status_code=504) except Exception as e: raise AppError(str(e), status_code=500) diff --git a/app/services/controller.py b/app/services/controller.py index 852b0e71..cccb62c3 100644 --- a/app/services/controller.py +++ b/app/services/controller.py @@ -16,8 +16,5 @@ def prepare_fillable(self, pdf_path: str): def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list = None): return self.external_apis_coordinator.get_weather(latitude, longitude, start_date, end_date, hourly_fields) - def get_postal_code(self, country: str, postal_code: str): - return self.external_apis_coordinator.get_postal_code(country, postal_code) - - def get_location(self, country: str, city: str): - return self.external_apis_coordinator.get_location(country, city) \ No newline at end of file + def lookup_address(self, address: str) -> list[dict]: + return self.external_apis_coordinator.lookup_address(address) \ No newline at end of file diff --git a/app/services/external_apis/weather_api.py b/app/services/external_apis/weather_api.py index 92489bc7..61cd04f3 100644 --- a/app/services/external_apis/weather_api.py +++ b/app/services/external_apis/weather_api.py @@ -55,9 +55,6 @@ def get_weather(self, latitude: float, longitude: float, start_date: str, end_da responses = openmeteo.weather_api(url, params=params) response = responses[0] - print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E") - print(f"Elevation: {response.Elevation()} m asl") - print(f"Timezone difference to GMT+0: {response.UtcOffsetSeconds()}s") hourly = response.Hourly() diff --git a/app/services/external_apis/zipcode_api.py b/app/services/external_apis/zipcode_api.py index b4bfe5da..c1a043a3 100644 --- a/app/services/external_apis/zipcode_api.py +++ b/app/services/external_apis/zipcode_api.py @@ -1,23 +1,72 @@ -import pgeocode -import numpy as np -import pandas as pd +from geopy.geocoders import Nominatim +from geopy.exc import GeocoderTimedOut, GeocoderServiceError + +from app.core.logging import get_logger + +logger = get_logger(__name__) + +_GEOLOCATOR = Nominatim(user_agent="fireform", timeout=10) + class ZipCodeAPI: def __init__(self): pass - def get_postal_code(self, country: str, postal_code: str): - res = pgeocode.Nominatim(country).query_postal_code(postal_code) - if isinstance(res, pd.Series): - res = res.replace({np.nan: None}).to_dict() - elif isinstance(res, pd.DataFrame): - res = res.replace({np.nan: None}).to_dict(orient='records') - return res - - def get_location(self, country: str, city: str): - res = pgeocode.Nominatim(country).query_location(city) - if isinstance(res, pd.Series): - res = res.replace({np.nan: None}).to_dict() - elif isinstance(res, pd.DataFrame): - res = res.replace({np.nan: None}).to_dict(orient='records') - return res \ No newline at end of file + def lookup_address(self, address: str) -> list[dict]: + """ + Look up an address string via OpenStreetMap Nominatim and return a list + of matching locations with their postal codes, coordinates, and names. + + Args: + address: Full address string, e.g. "1600 Amphitheatre Parkway, Mountain View, CA, US" + + Returns: + A list of dicts, each containing: + - postal_code (str | None) + - place_name (str) + - latitude (float) + - longitude (float) + - display_name (str) + - country (str | None) + - state (str | None) + - county (str | None) + """ + try: + locations = _GEOLOCATOR.geocode( + address, + exactly_one=False, + addressdetails=True, + limit=10, + ) + except GeocoderTimedOut: + logger.error("Nominatim geocoding timed out for address: %s", address) + raise TimeoutError(f"Geocoding service timed out for address: '{address}'") + except GeocoderServiceError as exc: + logger.error("Nominatim geocoding service error: %s", exc) + raise RuntimeError(f"Geocoding service error: {exc}") + + if not locations: + logger.info("No results found for address: %s", address) + return [] + + results = [] + for loc in locations: + addr = loc.raw.get("address", {}) + results.append({ + "postal_code": addr.get("postcode"), + "place_name": addr.get("city") + or addr.get("town") + or addr.get("village") + or addr.get("hamlet") + or addr.get("municipality") + or "", + "latitude": loc.latitude, + "longitude": loc.longitude, + "display_name": loc.address, + "country": addr.get("country"), + "state": addr.get("state"), + "county": addr.get("county"), + }) + logger.debug("Geocoded result: %s", results[-1]) + + return results \ No newline at end of file diff --git a/app/services/external_apis_coordinator.py b/app/services/external_apis_coordinator.py index f0c22fd9..07952ab0 100644 --- a/app/services/external_apis_coordinator.py +++ b/app/services/external_apis_coordinator.py @@ -9,8 +9,5 @@ def __init__(self): def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list[str] | None = None): return self.weather_api.get_weather(latitude, longitude, start_date, end_date, hourly_fields) - def get_postal_code(self, country: str, postal_code: str): - return self.zipcode_api.get_postal_code(country, postal_code) - - def get_location(self, country: str, city: str): - return self.zipcode_api.get_location(country, city) \ No newline at end of file + def lookup_address(self, address: str) -> list[dict]: + return self.zipcode_api.lookup_address(address) \ No newline at end of file diff --git a/contracts/path/weather.yaml b/contracts/path/weather.yaml index bf7c46b8..ff56d935 100644 --- a/contracts/path/weather.yaml +++ b/contracts/path/weather.yaml @@ -1,40 +1,69 @@ forecast: get: summary: Fetch weather forecast - description: Fetch weather data for the given location and fields. + description: > + Fetch hourly weather data for the given location and date range. + Coordinates must be provided as decimal degrees (e.g. `40.7128` for latitude, `-74.0060` for longitude). tags: - external-apis parameters: - name: latitude in: query required: true + description: > + Latitude of the location in decimal degrees. Valid range: -90.0 to 90.0. + Example: `40.7128` (New York City). Do NOT use degree/minute/second notation. schema: type: number format: float + minimum: -90.0 + maximum: 90.0 + example: 40.7128 - name: longitude in: query required: true + description: > + Longitude of the location in decimal degrees. Valid range: -180.0 to 180.0. + Example: `-74.0060` (New York City). Do NOT use degree/minute/second notation. schema: type: number format: float + minimum: -180.0 + maximum: 180.0 + example: -74.0060 - name: start_date in: query required: true + description: > + Start date of the forecast period. Format: `YYYY-MM-DD` (ISO 8601). + Example: `2024-06-01`. schema: type: string format: date + example: "2024-06-01" - name: end_date in: query required: true + description: > + End date of the forecast period (inclusive). Format: `YYYY-MM-DD` (ISO 8601). + Example: `2024-06-07`. Must be >= start_date. schema: type: string format: date + example: "2024-06-07" - name: fields in: query required: false + description: > + Comma-separated list of hourly variables to include in the response. + When omitted, all supported variables are returned. + Supported values: `temperature_2m`, `relative_humidity_2m`, `rain`, + `apparent_temperature`, `precipitation_probability`, `precipitation`, + `wind_direction_10m`, `wind_speed_10m`, `soil_temperature_0cm`, `uv_index`. + Example: `temperature_2m,rain,wind_speed_10m`. schema: type: string - description: Comma-separated list of hourly variables to request. + example: "temperature_2m,rain,wind_speed_10m" responses: '200': description: Weather data retrieved successfully @@ -42,6 +71,38 @@ forecast: application/json: schema: type: object - additionalProperties: true + properties: + latitude: + type: number + description: Actual latitude used by the weather model (may differ slightly from input). + longitude: + type: number + description: Actual longitude used by the weather model. + elevation: + type: number + description: Elevation of the location in metres above sea level. + timezone: + type: string + description: Timezone name (e.g. "GMT"). + timezone_abbreviation: + type: string + hourly: + type: object + description: > + Object containing hourly time series. The `date` key holds an array + of ISO 8601 timestamps; all other keys correspond to the requested + fields and contain arrays of the same length. + properties: + date: + type: array + items: + type: string + format: date-time + additionalProperties: + type: array + items: + type: number + '422': + description: Validation error (e.g. coordinates out of range, invalid date format) '500': description: Internal server error diff --git a/contracts/path/zipcode.yaml b/contracts/path/zipcode.yaml index 683b4ac4..dcc22c40 100644 --- a/contracts/path/zipcode.yaml +++ b/contracts/path/zipcode.yaml @@ -1,57 +1,76 @@ -postal_code: +lookup_address: get: - summary: Resolve postal code - description: Fetch data for the given postal code and country. + summary: Resolve address to location + description: > + Geocode a free-form address string to one or more geographic locations. + Each result includes postal code (when available), latitude, longitude, + place name, state, county, and country. + The list is ordered by relevance (most relevant first, up to 10 results). tags: - external-apis parameters: - - name: country - in: query - required: true - schema: - type: string - - name: postal_code - in: query - required: true - schema: - type: string - responses: - '200': - description: Postal code data retrieved successfully - content: - application/json: - schema: - type: object - additionalProperties: true - '500': - description: Internal server error - -location: - get: - summary: Resolve location - description: Fetch data for the given city and country. - tags: - - external-apis - parameters: - - name: country - in: query - required: true - schema: - type: string - - name: city + - name: address in: query required: true + description: > + Full address string to geocode. Include as much detail as possible for accurate results. + Examples: "1600 Amphitheatre Parkway, Mountain View, CA, US" + or "100 Main St, Santa Cruz, CA". + City-only queries (e.g. "Santa Cruz") are supported but may return multiple ambiguous results. schema: type: string + example: "1600 Amphitheatre Parkway, Mountain View, CA, US" responses: '200': - description: Location data retrieved successfully + description: > + List of geocoded results ordered by relevance. An empty array means + no results were found for the given address. content: application/json: schema: type: array items: type: object - additionalProperties: true + properties: + postal_code: + type: string + nullable: true + description: Postal/ZIP code for the location, if determinable. + example: "94043" + place_name: + type: string + description: City, town, or village name. + example: "Mountain View" + latitude: + type: number + format: float + description: Latitude in decimal degrees. + example: 37.4224764 + longitude: + type: number + format: float + description: Longitude in decimal degrees. + example: -122.0842499 + display_name: + type: string + description: Full human-readable address as returned by the geocoder. + example: "1600 Amphitheatre Parkway, Mountain View, Santa Clara County, California, 94043, United States" + country: + type: string + nullable: true + description: Country name. + example: "United States" + state: + type: string + nullable: true + description: State or region name. + example: "California" + county: + type: string + nullable: true + description: County or district name. + example: "Santa Clara County" + '504': + description: Geocoding service timed out '500': description: Internal server error diff --git a/requirements.txt b/requirements.txt index 08ad84db..8781c970 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,4 @@ openmeteo-requests requests-cache retry-requests pandas -pgeocode \ No newline at end of file +geopy \ No newline at end of file From 6bf87d0c3a38293e36852e7f3c2095e257741cf8 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Sat, 20 Jun 2026 16:29:26 -0700 Subject: [PATCH 32/85] refactor: :recycle: changing outdated prints to logger --- app/services/file_manipulator.py | 15 ++++++++------- app/services/llm.py | 14 +++++++------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/services/file_manipulator.py b/app/services/file_manipulator.py index 357356e2..87142e13 100644 --- a/app/services/file_manipulator.py +++ b/app/services/file_manipulator.py @@ -1,6 +1,9 @@ import os from app.services.filler import Filler from app.services.llm import LLM +from app.core.logging import get_logger + +logger = get_logger(__name__) class FileManipulator: @@ -39,25 +42,23 @@ def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: st It receives the raw data, runs the PDF filling logic, and returns the path to the newly created file. """ - print("[1] Received request from frontend.") - print(f"[2] PDF template path: {pdf_form_path}") + logger.info("[1] Received request from frontend.") + logger.info("[2] PDF template path: %s", pdf_form_path) if not os.path.exists(pdf_form_path): raise FileNotFoundError(f"PDF template not found at {pdf_form_path}") - print("[3] Starting extraction and PDF filling process...") + logger.info("[3] Starting extraction and PDF filling process...") try: self.llm._target_fields = fields self.llm._transcript_text = user_input self.llm._model = model output_name = self.filler.fill_form(pdf_form=pdf_form_path, llm=self.llm) - print("\n----------------------------------") - print("✅ Process Complete.") - print(f"Output saved to: {output_name}") + logger.info("Process complete. Output saved to: %s", output_name) return output_name except Exception as e: - print(f"An error occurred during PDF generation: {e}") + logger.error("An error occurred during PDF generation: %s", e) raise e diff --git a/app/services/llm.py b/app/services/llm.py index 29983182..e2d1639d 100644 --- a/app/services/llm.py +++ b/app/services/llm.py @@ -4,6 +4,9 @@ from requests.exceptions import Timeout, RequestException from app.core.config import OLLAMA_HOST, OLLAMA_MODEL +from app.core.logging import get_logger + +logger = get_logger(__name__) class LLM: @@ -51,9 +54,9 @@ def main_loop(self): json_data = response.json() break except Timeout: - print(f"[LOG]: Ollama request timed out (attempt {attempt+1}) for field '{field}'. Retrying...") + logger.warning("Ollama request timed out (attempt %d) for field '%s'. Retrying...", attempt + 1, field) except RequestException as e: - print(f"[LOG]: Ollama request failed: {e}") + logger.error("Ollama request failed: %s", e) except requests.exceptions.ConnectionError: raise ConnectionError( f"Could not connect to Ollama at {ollama_url}. " @@ -67,12 +70,9 @@ def main_loop(self): else: parsed_response = json_data["response"] self.add_response_to_json(field, parsed_response) - print(f"[{i}/{total_fields}] Extracted data for field '{field}' successfully.") + logger.info("[%d/%d] Extracted data for field '%s' successfully.", i, total_fields, field) - print("----------------------------------") - print("\t[LOG] Resulting JSON created from the input text:") - print(json.dumps(self._json, indent=2)) - print("--------- extracted data ---------") + logger.info("Resulting JSON created from the input text:\n%s", json.dumps(self._json, indent=2)) return self From 1d47b7ab8e824ea9d30415d49c467ee643604285 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sun, 21 Jun 2026 12:59:18 +0530 Subject: [PATCH 33/85] Add common contract schemas and standardized error handling (#543) --- app/api/routes/forms.py | 7 ++- app/api/schemas/common.py | 59 ++++++++++++++++--- app/api/schemas/enums.py | 111 ++++++++++++++++++++++++++++++++++++ app/api/v1/routes/system.py | 3 - app/core/errors/base.py | 23 +++++++- app/core/errors/handlers.py | 35 +++++++++++- tests/test_api.py | 10 +++- 7 files changed, 226 insertions(+), 22 deletions(-) create mode 100644 app/api/schemas/enums.py diff --git a/app/api/routes/forms.py b/app/api/routes/forms.py index 7af5da62..9ecb46c8 100644 --- a/app/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -23,7 +23,7 @@ def fill_form(form: FormFill, db: Session = Depends(get_db)): fetched_template = get_template(db, form.template_id) if not fetched_template: - raise AppError("Template not found", status_code=404) + raise AppError("Template not found", status_code=404, error_code="TEMPLATE_NOT_FOUND") controller = Controller() try: @@ -40,7 +40,7 @@ def fill_form(form: FormFill, db: Session = Depends(get_db)): ) return create_form(db, submission) except Exception as e: - raise AppError(str(e), status_code=500) + raise AppError(str(e), status_code=500, error_code="FORM_FILL_ERROR") @router.get("/models", response_model=ModelsResponse) @@ -93,9 +93,10 @@ def transcribe(audio: UploadFile = File(...)): f"Could not connect to the speech-to-text service at {whisper_url}. " "Please ensure the whisper service is running.", status_code=503, + error_code="STT_UNAVAILABLE", ) except requests.exceptions.RequestException as e: - raise AppError(f"Transcription failed: {e}", status_code=502) + raise AppError(f"Transcription failed: {e}", status_code=502, error_code="TRANSCRIPTION_FAILED") try: text = (response.json().get("text") or "").strip() diff --git a/app/api/schemas/common.py b/app/api/schemas/common.py index 8d20a24b..8d29d318 100644 --- a/app/api/schemas/common.py +++ b/app/api/schemas/common.py @@ -1,14 +1,55 @@ -from pydantic import BaseModel +from __future__ import annotations + +from datetime import datetime from typing import Any -class SuccessResponse(BaseModel): - success: bool = True - data: Any +from pydantic import BaseModel, ConfigDict + +from app.api.schemas.enums import JobStatus, JobType + + +class ValidationErrorItem(BaseModel): + model_config = ConfigDict() + field: str | None = None + issue: str | None = None + value: Any = None -class ErrorDetail(BaseModel): - code: str - message: str class ErrorResponse(BaseModel): - success: bool = False - error: ErrorDetail \ No newline at end of file + model_config = ConfigDict() + error_code: str + message: str + detail: dict[str, Any] | None = None + retry_after_seconds: int | None = None + validation_errors: list[ValidationErrorItem] | None = None + + +class Pagination(BaseModel): + model_config = ConfigDict() + total: int + page: int + per_page: int + total_pages: int + has_next: bool + has_prev: bool + + +class AsyncJobResponse(BaseModel): + model_config = ConfigDict() + job_id: str + job_type: JobType | None = None + status: JobStatus + estimated_seconds: int | None = None + poll_url: str | None = None + + +class Job(BaseModel): + model_config = ConfigDict() + job_id: str + job_type: JobType + status: JobStatus + progress_percent: int | None = None + result_url: str | None = None + error: ErrorResponse | None = None + created_at: datetime | None = None + updated_at: datetime | None = None diff --git a/app/api/schemas/enums.py b/app/api/schemas/enums.py new file mode 100644 index 00000000..77c07c33 --- /dev/null +++ b/app/api/schemas/enums.py @@ -0,0 +1,111 @@ +from enum import Enum + + +class InputStatus(str, Enum): + queued = "queued" + transcribing = "transcribing" + ready = "ready" + failed = "failed" + + +class ExtractionStatus(str, Enum): + processing = "processing" + completed = "completed" + failed = "failed" + needs_review = "needs_review" + + +class FormStatus(str, Enum): + queued = "queued" + generating = "generating" + completed = "completed" + failed = "failed" + + +class ReportStatus(str, Enum): + draft = "draft" + under_review = "under_review" + approved = "approved" + submitted = "submitted" + + +class JobStatus(str, Enum): + queued = "queued" + processing = "processing" + completed = "completed" + failed = "failed" + + +class JobType(str, Enum): + transcription = "transcription" + extraction = "extraction" + form_generation = "form_generation" + batch_form_generation = "batch_form_generation" + report_generation = "report_generation" + + +class FormType(str, Enum): + neris = "neris" + nemsis_epcr = "nemsis_epcr" + nibrs = "nibrs" + nfirs_basic = "nfirs_basic" + nfirs_fire = "nfirs_fire" + nfirs_structure = "nfirs_structure" + nfirs_wildland = "nfirs_wildland" + nfirs_ems = "nfirs_ems" + nfirs_hazmat = "nfirs_hazmat" + nfirs_apparatus = "nfirs_apparatus" + nfirs_personnel = "nfirs_personnel" + nfirs_arson = "nfirs_arson" + nfirs_casualty_civilian = "nfirs_casualty_civilian" + nfirs_casualty_responder = "nfirs_casualty_responder" + cal_fire_ics209 = "cal_fire_ics209" + osha_301 = "osha_301" + un_ssirs = "un_ssirs" + state_georgia = "state_georgia" + state_california = "state_california" + state_new_york = "state_new_york" + + +class IncidentCategory(str, Enum): + fire = "fire" + ems = "ems" + rescue = "rescue" + hazardous_conditions = "hazardous_conditions" + service_call = "service_call" + good_intent = "good_intent" + false_alarm = "false_alarm" + law_enforcement = "law_enforcement" + + +class CauseCertainty(str, Enum): + confirmed = "confirmed" + probable = "probable" + suspected = "suspected" + undetermined = "undetermined" + + +class InjurySeverity(str, Enum): + minor = "minor" + moderate = "moderate" + severe = "severe" + fatal = "fatal" + + +class RateOfSpread(str, Enum): + slow = "slow" + moderate = "moderate" + rapid = "rapid" + extreme = "extreme" + + +class PeriodType(str, Enum): + monthly = "monthly" + quarterly = "quarterly" + annual = "annual" + + +class OutputFormat(str, Enum): + pdf = "pdf" + json = "json" + both = "both" diff --git a/app/api/v1/routes/system.py b/app/api/v1/routes/system.py index b31909c3..eadd1d9f 100644 --- a/app/api/v1/routes/system.py +++ b/app/api/v1/routes/system.py @@ -161,8 +161,6 @@ def get_health(): @router.get("/schema/incident", summary="Get canonical incident JSON Schema") def get_schema_incident(): - # PROVISIONAL: This 501 body shape is not canonical — it will be updated - # to match the standardized ErrorResponse envelope once #543 lands. return JSONResponse( status_code=501, content={ @@ -174,7 +172,6 @@ def get_schema_incident(): @router.get("/schema/incident/versions", summary="Get incident schema version history") def get_schema_versions(): - # PROVISIONAL: Same as /schema/incident — shape will align with #543. return JSONResponse( status_code=501, content={ diff --git a/app/core/errors/base.py b/app/core/errors/base.py index 1f81a082..d317736a 100644 --- a/app/core/errors/base.py +++ b/app/core/errors/base.py @@ -1,4 +1,23 @@ +def _default_error_code(status_code: int) -> str: + return { + 400: "BAD_REQUEST", + 404: "NOT_FOUND", + 422: "VALIDATION_ERROR", + 500: "INTERNAL_ERROR", + 502: "BAD_GATEWAY", + 503: "SERVICE_UNAVAILABLE", + }.get(status_code, "ERROR") + + class AppError(Exception): - def __init__(self, message: str, status_code: int = 400): + def __init__( + self, + message: str, + status_code: int = 400, + error_code: str | None = None, + detail: dict | None = None, + ): self.message = message - self.status_code = status_code \ No newline at end of file + self.status_code = status_code + self.error_code = error_code or _default_error_code(status_code) + self.detail = detail diff --git a/app/core/errors/handlers.py b/app/core/errors/handlers.py index 0285ddb1..446056da 100644 --- a/app/core/errors/handlers.py +++ b/app/core/errors/handlers.py @@ -1,11 +1,40 @@ from fastapi import Request +from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse + from app.core.errors.base import AppError +# Advisory retry hint on 503 responses — not a measured queue depth or backpressure +# value; just a safe default so clients know when to try again. +_RETRY_AFTER_SECONDS = 30 + + def register_exception_handlers(app): @app.exception_handler(AppError) async def app_error_handler(request: Request, exc: AppError): + body: dict = {"error_code": exc.error_code, "message": exc.message} + if exc.detail is not None: + body["detail"] = exc.detail + if exc.status_code == 503: + body["retry_after_seconds"] = _RETRY_AFTER_SECONDS + return JSONResponse(status_code=exc.status_code, content=body) + + @app.exception_handler(RequestValidationError) + async def validation_error_handler(request: Request, exc: RequestValidationError): + validation_errors = [] + for error in exc.errors(): + loc = error.get("loc", ()) + field = ".".join(str(x) for x in loc if x != "body") + validation_errors.append({ + "field": field or None, + "issue": error.get("msg"), + "value": error.get("input"), + }) return JSONResponse( - status_code=exc.status_code, - content={"error": exc.message}, - ) \ No newline at end of file + status_code=422, + content={ + "error_code": "VALIDATION_ERROR", + "message": "Request validation failed", + "validation_errors": validation_errors, + }, + ) diff --git a/tests/test_api.py b/tests/test_api.py index e98d16fd..c5c1bf0b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -210,12 +210,18 @@ def test_fill_form_template_file_not_found(self, client, mock_controller): "input_text": "some text", }) assert resp.status_code == 500 - assert "PDF template not found" in resp.json()["error"] + assert resp.json()["error_code"] == "FORM_FILL_ERROR" + assert "PDF template not found" in resp.json()["message"] def test_fill_form_validates_body(self, client): - """Missing required fields → 422.""" + """Missing required fields → 422 with contract envelope.""" resp = client.post("/forms/fill", json={}) assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + assert len(body["validation_errors"]) >= 1 + assert body["validation_errors"][0]["field"] is not None + assert body["validation_errors"][0]["issue"] is not None def test_transcribe_success(self, client, monkeypatch): """Audio is forwarded to the whisper sidecar and its text returned.""" From 83e0ec1b8d68bf8461842fe3f7b85117967c7b9c Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 20 Jun 2026 23:12:57 +0530 Subject: [PATCH 34/85] feat: add celery and redis to docker compose to pull celery & redis docker image, updated celery broker url to use redis endpoint. Added required packages. --- docker/.env.example | 4 ++++ docker/dev/compose.yml | 51 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 ++ 3 files changed, 57 insertions(+) diff --git a/docker/.env.example b/docker/.env.example index 026a2318..d5ccca58 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -20,6 +20,10 @@ OLLAMA_TIMEOUT=300 WHISPER_HOST=http://whisper:9000 WHISPER_MODEL=small.en +# --- Celery / Redis ------------------------------------------------------- +CELERY_BROKER_URL=redis://redis:6379/0 +CELERY_RESULT_BACKEND=redis://redis:6379/0 + # --- Gunicorn (prod only) ------------------------------------------------- GUNICORN_WORKERS=2 diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml index 83de9243..22f8a5d5 100644 --- a/docker/dev/compose.yml +++ b/docker/dev/compose.yml @@ -54,6 +54,21 @@ services: retries: 5 start_period: 60s + redis: + image: redis:7-alpine + container_name: fireform-redis + ports: + - "127.0.0.1:6379:6379" + volumes: + - redis_data:/data + networks: + - fireform-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + app: build: context: ../.. @@ -66,6 +81,8 @@ services: condition: service_healthy whisper: condition: service_started + redis: + condition: service_healthy entrypoint: ["sh", "docker/entrypoint.sh"] command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] volumes: @@ -86,6 +103,38 @@ services: - FIREFORM_TEMPLATE_DIR=/data/uploads - FIREFORM_DB_ECHO=${FIREFORM_DB_ECHO:-true} - FRONTEND_ORIGINS=${FRONTEND_ORIGINS:-http://localhost:5173,http://127.0.0.1:5173} + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + networks: + - fireform-network + + celery-worker: + build: + context: ../.. + dockerfile: docker/dev/Dockerfile + container_name: fireform-celery-worker + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + ollama: + condition: service_healthy + command: ["celery", "-A", "app.core.celery:celery_app", "worker", "--loglevel=info", "--concurrency=4"] + volumes: + - ../..:/app + - fireform_uploads:/data/uploads + environment: + - PYTHONUNBUFFERED=1 + - PYTHONPATH=/app + - DATABASE_URL=postgresql://fireform:fireform@postgres:5432/fireform + - OLLAMA_HOST=${OLLAMA_HOST:-http://ollama:11434} + - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT:-300} + - OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:1.5b} + - FIREFORM_DATA_DIR=/data/uploads + - FIREFORM_TEMPLATE_DIR=/data/uploads + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 networks: - fireform-network @@ -98,6 +147,8 @@ volumes: driver: local fireform_uploads: driver: local + redis_data: + driver: local networks: fireform-network: diff --git a/requirements.txt b/requirements.txt index 5f57f064..066e00b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ numpy<2 ollama pypdf python-multipart +celery[redis] +redis \ No newline at end of file From 2bdc700fe61b00b875dd020fad6571e65dc966b1 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 20 Jun 2026 23:39:37 +0530 Subject: [PATCH 35/85] configured celery to FireForm --- app/core/celery.py | 19 +++++++++++++++++++ app/core/config.py | 4 ++++ app/models/__init__.py | 4 ++-- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 app/core/celery.py diff --git a/app/core/celery.py b/app/core/celery.py new file mode 100644 index 00000000..4efe5dbc --- /dev/null +++ b/app/core/celery.py @@ -0,0 +1,19 @@ +from celery import Celery + +from app.core.config import CELERY_BROKER_URL, CELERY_RESULT_BACKEND + +celery_app = Celery( + "fireform", + broker=CELERY_BROKER_URL, + backend=CELERY_RESULT_BACKEND, +) + +celery_app.conf.update( + task_serializer="json", + result_serializer="json", + accept_content=["json"], + task_track_started=True, + result_expires=86400, +) + +celery_app.conf.include = ["app.tasks.fill"] diff --git a/app/core/config.py b/app/core/config.py index d6f723df..d89cea7f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -37,6 +37,10 @@ OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") WHISPER_HOST = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/") +# --- Celery / Redis ------------------------------------------------------- +CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") +CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/0") + # --- CORS ----------------------------------------------------------------- _DEFAULT_ORIGINS = "http://127.0.0.1:5173,http://localhost:5173" ALLOWED_ORIGINS = [ diff --git a/app/models/__init__.py b/app/models/__init__.py index f46e17dc..9c5433b6 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,5 +1,5 @@ """ORM models. Import from here: `from app.models import Template`.""" -from app.models.models import FormSubmission, Template +from app.models.models import FormSubmission, Job, Template -__all__ = ["Template", "FormSubmission"] +__all__ = ["Template", "FormSubmission", "Job"] From 53f36d9043ba27e4d574111198222228b1a4f0d3 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sat, 20 Jun 2026 23:55:24 +0530 Subject: [PATCH 36/85] added required schemas for asuncformfill and to track celery jobs --- app/api/schemas/forms.py | 38 ++++++++++++++++++++++++++++++++++++-- app/models/models.py | 15 ++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/api/schemas/forms.py b/app/api/schemas/forms.py index 6a833c8d..8be28822 100644 --- a/app/api/schemas/forms.py +++ b/app/api/schemas/forms.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, field_validator + class FormFill(BaseModel): template_id: int input_text: str - # Optional Ollama model override for this fill; falls back to OLLAMA_MODEL. model: str | None = None @field_validator("input_text") @@ -29,4 +29,38 @@ class TranscriptionResponse(BaseModel): class ModelsResponse(BaseModel): models: list[str] - default: str \ No newline at end of file + default: str + + +class AsyncFormFill(BaseModel): + template_ids: list[int] + input_text: str + model: str | None = None + + @field_validator("input_text") + def validate_input_text(cls, value): + if not value or not value.strip(): + raise ValueError("Input text cannot be empty") + return value + + @field_validator("template_ids") + def validate_template_ids(cls, value): + if not value: + raise ValueError("template_ids cannot be empty") + return value + + +class JobResponse(BaseModel): + id: int + celery_task_id: str + template_id: int + status: str + output_pdf_path: str | None = None + error: str | None = None + + class Config: + from_attributes = True + + +class AsyncFormFillResponse(BaseModel): + jobs: list[JobResponse] \ No newline at end of file diff --git a/app/models/models.py b/app/models/models.py index 8f826556..9afc2bf5 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -16,4 +16,17 @@ class FormSubmission(SQLModel, table=True): template_id: int = Field(foreign_key="template.id") input_text: str output_pdf_path: str - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) \ No newline at end of file + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Job(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + celery_task_id: str = Field(index=True) + template_id: int = Field(foreign_key="template.id") + input_text: str + status: str = Field(default="pending") + output_pdf_path: str | None = None + error: str | None = None + model: str | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) \ No newline at end of file From 436b15c12fc18c36ade92b119a1222b7aa0c4f2d Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sun, 21 Jun 2026 17:37:04 +0530 Subject: [PATCH 37/85] feat: add universal job tracking with UUID, progress, and structured errors --- app/api/router.py | 3 +- app/api/routes/jobs.py | 60 ++++++++++++++++++++++++++++++++++++++ app/api/schemas/forms.py | 23 +++++++++++---- app/db/repositories.py | 31 +++++++++++++++++++- app/models/models.py | 17 +++++++---- app/tasks/fill.py | 63 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 app/api/routes/jobs.py create mode 100644 app/tasks/fill.py diff --git a/app/api/router.py b/app/api/router.py index cba62424..27b7bcb0 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -9,10 +9,11 @@ from fastapi import APIRouter -from app.api.routes import forms, templates +from app.api.routes import forms, jobs, templates from app.api.v1.router import v1_router api_router = APIRouter() api_router.include_router(templates.router) api_router.include_router(forms.router) api_router.include_router(v1_router) +api_router.include_router(jobs.router) \ No newline at end of file diff --git a/app/api/routes/jobs.py b/app/api/routes/jobs.py new file mode 100644 index 00000000..5af90087 --- /dev/null +++ b/app/api/routes/jobs.py @@ -0,0 +1,60 @@ +from fastapi import APIRouter, Depends +from sqlmodel import Session + +from app.api.deps import get_db +from app.api.schemas.forms import ( + AsyncFormFill, + AsyncFormFillResponse, + AsyncJobSubmitResponse, + JobResponse, +) +from app.core.errors.base import AppError +from app.db.repositories import create_job, get_job_by_uuid, get_template +from app.models import Job +from app.tasks.fill import fill_form_task + +router = APIRouter(tags=["jobs"]) + + +@router.get("/jobs/{job_id}", response_model=JobResponse) +def get_job_status(job_id: str, db: Session = Depends(get_db)): + job = get_job_by_uuid(db, job_id) + if not job: + raise AppError("Job not found", status_code=404) + return JobResponse( + job_id=job.job_id, + job_type=job.job_type, + status=job.status, + progress_percent=job.progress_percent, + result_url=job.result_url, + error=job.error, + created_at=job.created_at.isoformat() if job.created_at else None, + updated_at=job.updated_at.isoformat() if job.updated_at else None, + ) + + +@router.post("/forms/jobs", response_model=AsyncFormFillResponse) +def submit_async_form_fill(form: AsyncFormFill, db: Session = Depends(get_db)): + for tid in form.template_ids: + if not get_template(db, tid): + raise AppError(f"Template {tid} not found", status_code=404) + + jobs: list[AsyncJobSubmitResponse] = [] + for tid in form.template_ids: + result = fill_form_task.delay(tid, form.input_text, form.model) + job = Job( + celery_task_id=result.id, + job_type="form_generation", + template_id=tid, + input_text=form.input_text, + status="queued", + model=form.model, + ) + job = create_job(db, job) + jobs.append(AsyncJobSubmitResponse( + job_id=job.job_id, + status=job.status, + poll_url=f"/api/v1/jobs/{job.job_id}", + )) + + return AsyncFormFillResponse(jobs=jobs) diff --git a/app/api/schemas/forms.py b/app/api/schemas/forms.py index 8be28822..155b14f0 100644 --- a/app/api/schemas/forms.py +++ b/app/api/schemas/forms.py @@ -51,16 +51,27 @@ def validate_template_ids(cls, value): class JobResponse(BaseModel): - id: int - celery_task_id: str - template_id: int + job_id: str + job_type: str + status: str + progress_percent: int = 0 + result_url: str | None = None + error: dict | None = None + created_at: str | None = None + updated_at: str | None = None + + class Config: + from_attributes = True + + +class AsyncJobSubmitResponse(BaseModel): + job_id: str status: str - output_pdf_path: str | None = None - error: str | None = None + poll_url: str class Config: from_attributes = True class AsyncFormFillResponse(BaseModel): - jobs: list[JobResponse] \ No newline at end of file + jobs: list[AsyncJobSubmitResponse] \ No newline at end of file diff --git a/app/db/repositories.py b/app/db/repositories.py index ebb80de6..8b6a2c4f 100644 --- a/app/db/repositories.py +++ b/app/db/repositories.py @@ -1,5 +1,5 @@ from sqlmodel import Session, select -from app.models import Template, FormSubmission +from app.models import Template, FormSubmission, Job # Templates def create_template(session: Session, template: Template) -> Template: @@ -22,3 +22,32 @@ def create_form(session: Session, form: FormSubmission) -> FormSubmission: session.commit() session.refresh(form) return form + + +# Jobs +def create_job(session: Session, job: Job) -> Job: + session.add(job) + session.commit() + session.refresh(job) + return job + + +def get_job(session: Session, job_id: int) -> Job | None: + return session.get(Job, job_id) + + +def get_job_by_uuid(session: Session, job_uuid: str) -> Job | None: + statement = select(Job).where(Job.job_id == job_uuid) + return session.exec(statement).first() + + +def get_job_by_celery_id(session: Session, celery_task_id: str) -> Job | None: + statement = select(Job).where(Job.celery_task_id == celery_task_id) + return session.exec(statement).first() + + +def update_job(session: Session, job: Job) -> Job: + session.add(job) + session.commit() + session.refresh(job) + return job diff --git a/app/models/models.py b/app/models/models.py index 9afc2bf5..c4ddf0bd 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,5 +1,7 @@ -from sqlmodel import SQLModel, Field +import uuid as uuid_mod + from sqlalchemy import Column, JSON +from sqlmodel import SQLModel, Field from datetime import datetime, timezone @@ -21,12 +23,15 @@ class FormSubmission(SQLModel, table=True): class Job(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) + job_id: str = Field(default_factory=lambda: str(uuid_mod.uuid4()), index=True, unique=True) celery_task_id: str = Field(index=True) - template_id: int = Field(foreign_key="template.id") - input_text: str - status: str = Field(default="pending") - output_pdf_path: str | None = None - error: str | None = None + job_type: str = Field(default="form_generation") + template_id: int | None = Field(default=None, foreign_key="template.id") + input_text: str | None = None + status: str = Field(default="queued") + progress_percent: int = Field(default=0) + result_url: str | None = None + error: dict | None = Field(default=None, sa_column=Column(JSON)) model: str | None = None created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) \ No newline at end of file diff --git a/app/tasks/fill.py b/app/tasks/fill.py new file mode 100644 index 00000000..fc7b3115 --- /dev/null +++ b/app/tasks/fill.py @@ -0,0 +1,63 @@ +import logging +from datetime import datetime, timezone + +from app.core.celery import celery_app +from app.db.database import get_session +from app.db.repositories import get_job_by_celery_id, get_template, update_job, create_form +from app.models import FormSubmission +from app.services.controller import Controller + +logger = logging.getLogger(__name__) + + +@celery_app.task(bind=True, name="fill_form") +def fill_form_task(self, template_id: int, input_text: str, model: str | None = None): + session = next(get_session()) + try: + job = get_job_by_celery_id(session, self.request.id) + if not job: + raise RuntimeError(f"No job row for celery task {self.request.id}") + + job.status = "processing" + job.progress_percent = 10 + job.updated_at = datetime.now(timezone.utc) + update_job(session, job) + + template = get_template(session, template_id) + if not template: + raise ValueError(f"Template {template_id} not found") + + controller = Controller() + path = controller.fill_form( + user_input=input_text, + fields=template.fields, + pdf_form_path=template.pdf_path, + model=model, + ) + + submission = FormSubmission( + template_id=template_id, + input_text=input_text, + output_pdf_path=path, + ) + create_form(session, submission) + + job.status = "completed" + job.progress_percent = 100 + job.result_url = f"/api/v1/forms/{submission.id}/download" + job.updated_at = datetime.now(timezone.utc) + update_job(session, job) + + return {"job_id": job.job_id, "result_url": job.result_url} + + except Exception as e: + logger.exception("fill_form_task failed") + job = get_job_by_celery_id(session, self.request.id) + if job: + job.status = "failed" + job.error = {"error_code": "TASK_FAILED", "message": str(e)} + job.updated_at = datetime.now(timezone.utc) + update_job(session, job) + raise + finally: + session.close() From 838b85ae9949aefc6e7230ca4949b5281bee1150 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sun, 21 Jun 2026 17:37:28 +0530 Subject: [PATCH 38/85] added make command to see logs for workers --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 13ea3665..0c8fa4ff 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ help: @echo "make logs - Stream all container logs" @echo "make logs-app - Stream app container logs" @echo "make logs-ollama - Stream Ollama container logs" + @echo "make logs-worker - Stream Celery worker logs" @echo "make shell - Open shell in running app container" @echo "make pull-model - Pull Ollama model from .env.dev ($(OLLAMA_MODEL))" @echo "make test - Run test suite" @@ -80,6 +81,9 @@ logs-app: logs-ollama: @$(COMPOSE) logs -f ollama +logs-worker: + @$(COMPOSE) logs -f celery-worker + shell: @$(COMPOSE) exec app /bin/sh From 77661fcac208eb33e310e85f6be3467a4a8d2cf2 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sun, 21 Jun 2026 17:37:56 +0530 Subject: [PATCH 39/85] added init.py for tasks --- app/tasks/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/tasks/__init__.py diff --git a/app/tasks/__init__.py b/app/tasks/__init__.py new file mode 100644 index 00000000..e69de29b From 60e1ce5cbd51f8951ca135170a3da0877884b6d2 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Sun, 21 Jun 2026 17:40:23 +0530 Subject: [PATCH 40/85] added tests for celery workers --- tests/conftest.py | 2 +- tests/test_jobs.py | 111 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/test_jobs.py diff --git a/tests/conftest.py b/tests/conftest.py index 56dea60d..2699127a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ from app.main import app from app.api.deps import get_db -from app.models import Template, FormSubmission # noqa: F401 — registers tables +from app.models import Template, FormSubmission, Job # noqa: F401 — registers tables # --------------------------------------------------------------------------- # In-memory database diff --git a/tests/test_jobs.py b/tests/test_jobs.py new file mode 100644 index 00000000..0355a7c9 --- /dev/null +++ b/tests/test_jobs.py @@ -0,0 +1,111 @@ +"""Tests for async job submission and status endpoints.""" + +from unittest.mock import patch, MagicMock + + +class TestJobEndpoints: + + def _seed_template(self, client): + resp = client.post("/templates/create", json={ + "name": "Test Template", + "pdf_path": "test.pdf", + "fields": {"name": "string"}, + }) + return resp.json()["id"] + + @patch("app.api.routes.jobs.fill_form_task") + def test_submit_async_single(self, mock_task, client): + mock_result = MagicMock() + mock_result.id = "celery-task-id-1" + mock_task.delay.return_value = mock_result + + tpl_id = self._seed_template(client) + resp = client.post("/forms/jobs", json={ + "template_ids": [tpl_id], + "input_text": "John Doe firefighter", + }) + assert resp.status_code == 200 + data = resp.json() + assert len(data["jobs"]) == 1 + assert data["jobs"][0]["status"] == "queued" + assert "job_id" in data["jobs"][0] + assert data["jobs"][0]["poll_url"].startswith("/api/v1/jobs/") + mock_task.delay.assert_called_once_with(tpl_id, "John Doe firefighter", None) + + @patch("app.api.routes.jobs.fill_form_task") + def test_submit_async_batch(self, mock_task, client): + mock_task.delay.side_effect = [ + MagicMock(id="task-1"), + MagicMock(id="task-2"), + ] + + t1 = self._seed_template(client) + t2 = self._seed_template(client) + resp = client.post("/forms/jobs", json={ + "template_ids": [t1, t2], + "input_text": "batch input", + }) + assert resp.status_code == 200 + jobs = resp.json()["jobs"] + assert len(jobs) == 2 + assert jobs[0]["job_id"] != jobs[1]["job_id"] + assert mock_task.delay.call_count == 2 + + @patch("app.api.routes.jobs.fill_form_task") + def test_submit_async_missing_template(self, mock_task, client): + resp = client.post("/forms/jobs", json={ + "template_ids": [9999], + "input_text": "some text", + }) + assert resp.status_code == 404 + mock_task.delay.assert_not_called() + + @patch("app.api.routes.jobs.fill_form_task") + def test_get_job_status(self, mock_task, client): + mock_task.delay.return_value = MagicMock(id="celery-abc") + + tpl_id = self._seed_template(client) + submit_resp = client.post("/forms/jobs", json={ + "template_ids": [tpl_id], + "input_text": "test input", + }) + job_id = submit_resp.json()["jobs"][0]["job_id"] + + resp = client.get(f"/jobs/{job_id}") + assert resp.status_code == 200 + data = resp.json() + assert data["job_id"] == job_id + assert data["job_type"] == "form_generation" + assert data["status"] == "queued" + assert data["progress_percent"] == 0 + + def test_get_job_not_found(self, client): + resp = client.get("/jobs/00000000-0000-0000-0000-000000000000") + assert resp.status_code == 404 + + @patch("app.api.routes.jobs.fill_form_task") + def test_submit_with_model_override(self, mock_task, client): + mock_task.delay.return_value = MagicMock(id="celery-xyz") + + tpl_id = self._seed_template(client) + resp = client.post("/forms/jobs", json={ + "template_ids": [tpl_id], + "input_text": "test", + "model": "mistral:latest", + }) + assert resp.status_code == 200 + mock_task.delay.assert_called_once_with(tpl_id, "test", "mistral:latest") + + def test_submit_empty_template_ids(self, client): + resp = client.post("/forms/jobs", json={ + "template_ids": [], + "input_text": "test", + }) + assert resp.status_code == 422 + + def test_submit_empty_input_text(self, client): + resp = client.post("/forms/jobs", json={ + "template_ids": [1], + "input_text": "", + }) + assert resp.status_code == 422 From ae8d8a150892eceaec72f09e078949e8e41d988b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 22 Jun 2026 10:54:32 +0530 Subject: [PATCH 41/85] Move retry-after constant to config.py (#543 review) --- app/core/config.py | 5 +++++ app/core/errors/handlers.py | 7 ++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index d6f723df..d08ccf3a 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -44,3 +44,8 @@ for origin in os.getenv("FRONTEND_ORIGINS", _DEFAULT_ORIGINS).split(",") if origin.strip() ] + +# --- Error handling ------------------------------------------------------- +# Advisory Retry-After sent to clients on 503 responses. This is a client +# hint, not a measured backpressure value — the app has no queue-depth signal. +RETRY_AFTER_SECONDS = 30 diff --git a/app/core/errors/handlers.py b/app/core/errors/handlers.py index 446056da..ce8988ad 100644 --- a/app/core/errors/handlers.py +++ b/app/core/errors/handlers.py @@ -2,12 +2,9 @@ from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse +from app.core.config import RETRY_AFTER_SECONDS from app.core.errors.base import AppError -# Advisory retry hint on 503 responses — not a measured queue depth or backpressure -# value; just a safe default so clients know when to try again. -_RETRY_AFTER_SECONDS = 30 - def register_exception_handlers(app): @app.exception_handler(AppError) @@ -16,7 +13,7 @@ async def app_error_handler(request: Request, exc: AppError): if exc.detail is not None: body["detail"] = exc.detail if exc.status_code == 503: - body["retry_after_seconds"] = _RETRY_AFTER_SECONDS + body["retry_after_seconds"] = RETRY_AFTER_SECONDS return JSONResponse(status_code=exc.status_code, content=body) @app.exception_handler(RequestValidationError) From 0ec204296170ebcd38a34a26cb4b9d10cb16fc6b Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Mon, 22 Jun 2026 23:26:41 +0530 Subject: [PATCH 42/85] feat: Add Alembic migration infrastructure --- alembic.ini | 37 ++++++++++++++++++ alembic/env.py | 50 +++++++++++++++++++++++++ alembic/script.py.mako | 27 +++++++++++++ alembic/versions/001_initial_schema.py | 52 ++++++++++++++++++++++++++ app/db/init_db.py | 24 +++++++++--- requirements.txt | 3 +- 6 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 alembic.ini create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/001_initial_schema.py diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..2788aaa8 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,37 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +path_separator = os + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 00000000..d503289d --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,50 @@ +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +from app.core.config import DATABASE_URL +from app.models.models import SQLModel +from app.models import Template, FormSubmission + +config = context.config + +if not config.get_main_option("sqlalchemy.url"): + config.set_main_option("sqlalchemy.url", DATABASE_URL) + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = SQLModel.metadata + + +def run_migrations_offline(): + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + connectable = config.attributes.get("connection", None) + + if connectable is None: + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + else: + context.configure(connection=connectable, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 00000000..6ce33510 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/001_initial_schema.py b/alembic/versions/001_initial_schema.py new file mode 100644 index 00000000..e6d9fade --- /dev/null +++ b/alembic/versions/001_initial_schema.py @@ -0,0 +1,52 @@ +"""Initial schema — Template and FormSubmission tables. + +Revision ID: 001 +Revises: +Create Date: 2026-06-22 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + +revision: str = "001" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "template", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("fields", sa.JSON, nullable=False), + sa.Column("pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column( + "created_at", + sa.DateTime, + nullable=False, + server_default=sa.text("now()"), + ), + ) + + op.create_table( + "formsubmission", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("template_id", sa.Integer, sa.ForeignKey("template.id"), nullable=False), + sa.Column("input_text", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("output_pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column( + "created_at", + sa.DateTime, + nullable=False, + server_default=sa.text("now()"), + ), + ) + + +def downgrade() -> None: + op.drop_table("formsubmission") + op.drop_table("template") diff --git a/app/db/init_db.py b/app/db/init_db.py index fc171471..ebfe09b2 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -1,24 +1,37 @@ import datetime import logging +from pathlib import Path -from sqlmodel import Session, SQLModel, select +from alembic import command +from alembic.config import Config +from sqlmodel import Session, select from app.core.config import DEFAULT_TEMPLATE_DIR from app.db.database import engine - from app.models import FormSubmission, Template # noqa: F401 logger = logging.getLogger(__name__) +ALEMBIC_INI = str(Path(__file__).resolve().parents[2] / "alembic.ini") + + +def _alembic_cfg() -> Config: + cfg = Config(ALEMBIC_INI) + return cfg + + +def run_migrations(): + logger.info("Running Alembic migrations...") + command.upgrade(_alembic_cfg(), "head") + logger.info("Migrations complete.") + def seed_db(): with Session(engine) as session: - # Check if we already have templates statement = select(Template) try: results = session.exec(statement).first() except Exception: - # Table might not exist yet if called at a weird time results = None if not results: @@ -33,7 +46,6 @@ def seed_db(): "Date": "string", } - # Using ID 2 as agreed to avoid any ID 1 corruption default_template = Template( id=2, name="Manual Test Template", @@ -47,7 +59,7 @@ def seed_db(): def init_db(): - SQLModel.metadata.create_all(engine) + run_migrations() seed_db() diff --git a/requirements.txt b/requirements.txt index 8781c970..25cb0a16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,5 @@ openmeteo-requests requests-cache retry-requests pandas -geopy \ No newline at end of file +geopy +alembic \ No newline at end of file From 512174a841a5787ddbd09c1875f13158e0a3a587 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Mon, 22 Jun 2026 23:27:02 +0530 Subject: [PATCH 43/85] Add migration tests for upgrade, downgrade, and schema validation --- tests/test_migrations.py | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/test_migrations.py diff --git a/tests/test_migrations.py b/tests/test_migrations.py new file mode 100644 index 00000000..3e920322 --- /dev/null +++ b/tests/test_migrations.py @@ -0,0 +1,85 @@ +"""Tests for Alembic migration infrastructure. + +Runs migrations against an in-memory SQLite database to verify: +- upgrade to head works +- downgrade to base works +- round-trip (up → down → up) works +- schema matches expected columns and foreign keys +""" + +import pytest +from alembic import command +from alembic.config import Config +from sqlalchemy import create_engine, inspect + +ALEMBIC_INI = "alembic.ini" + + +@pytest.fixture() +def alembic_engine(): + return create_engine("sqlite://") + + +@pytest.fixture() +def alembic_cfg(alembic_engine): + cfg = Config(ALEMBIC_INI) + cfg.set_main_option("sqlalchemy.url", "sqlite://") + cfg.attributes["connection"] = alembic_engine.connect() + return cfg + + +def test_upgrade_head(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + tables = inspector.get_table_names() + assert "template" in tables + assert "formsubmission" in tables + assert "alembic_version" in tables + + +def test_downgrade_base(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + command.downgrade(alembic_cfg, "base") + + inspector = inspect(alembic_engine) + tables = inspector.get_table_names() + assert "template" not in tables + assert "formsubmission" not in tables + + +def test_round_trip(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + command.downgrade(alembic_cfg, "-1") + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + tables = inspector.get_table_names() + assert "template" in tables + assert "formsubmission" in tables + + +def test_template_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("template")} + assert columns == {"id", "name", "fields", "pdf_path", "created_at"} + + +def test_formsubmission_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("formsubmission")} + assert columns == {"id", "template_id", "input_text", "output_pdf_path", "created_at"} + + +def test_formsubmission_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("formsubmission") + assert len(fks) == 1 + assert fks[0]["referred_table"] == "template" + assert fks[0]["referred_columns"] == ["id"] From 2ba207d44832a18c24bc74c3a28fce9b67aa0831 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Mon, 22 Jun 2026 23:27:54 +0530 Subject: [PATCH 44/85] Add alembic check to CI and migration commands to Makefile --- .github/workflows/tests.yml | 8 +++++++- Makefile | 10 +++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c90b98e..075b3e27 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,4 +27,10 @@ jobs: env: PYTHONPATH: . run: | - python -m pytest tests/ -v --tb=short \ No newline at end of file + python -m pytest tests/ -v --tb=short + + - name: Check for un-migrated model changes + env: + PYTHONPATH: . + run: | + python -m alembic check \ No newline at end of file diff --git a/Makefile b/Makefile index 7d1d3575..c81235e7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean +.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean migrate migration COMPOSE = docker compose -f docker/dev/compose.yml --env-file docker/.env.dev ENV_DEV = docker/.env.dev @@ -28,6 +28,8 @@ help: @echo "make shell - Open shell in running app container" @echo "make pull-model - Pull Ollama model from .env.dev ($(OLLAMA_MODEL))" @echo "make test - Run test suite" + @echo "make migrate - Run pending Alembic migrations" + @echo "make migration - Generate new migration (msg='description')" @echo "make clean - Stop containers (preserves volumes)" @echo "make super-clean - [CAUTION] Stop containers, delete volumes, prune Docker" @@ -89,6 +91,12 @@ pull-model: test: @$(COMPOSE) exec -T app python3 -m pytest tests/ -v +migrate: + @$(COMPOSE) exec -T app alembic upgrade head + +migration: + @$(COMPOSE) exec -T app alembic revision --autogenerate -m "$(msg)" + clean: @$(COMPOSE) down From 7e934aa270ca0d4fafb0967730979e351424b2fe Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 23 Jun 2026 17:48:10 +0530 Subject: [PATCH 45/85] Dockerfiles + compose: perf(docker): faster image builds and container startup --- docker/dev/Dockerfile | 19 ++++++++++++++----- docker/dev/compose.yml | 4 ++++ docker/prod/Dockerfile | 26 +++++++++++++++++++------- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index ad4bfd4b..a5a42c1b 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.11-slim WORKDIR /app -# Use apt cache mount to speed up system package installation across builds +# apt cache mounts keep downloaded .debs across builds RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt \ @@ -11,13 +11,22 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ curl \ libgl1 \ libglib2.0-0 \ - libxcb1 + libxcb1 \ + libpq-dev \ + build-essential \ + g++ + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY requirements.txt . -# Use pip cache mount so it remembers downloaded wheels -RUN --mount=type=cache,target=/root/.cache/pip \ - pip install -r requirements.txt +RUN --mount=type=cache,target=/root/.cache/uv \ + UV_TORCH_BACKEND=cpu \ + uv pip install --system -r requirements.txt + +# Bake a hash of the deps the image was built with. The entrypoint compares this +# against the live (bind-mounted) requirements.txt to decide whether to reinstall. +RUN sha256sum requirements.txt | cut -d' ' -f1 > /opt/req_hash ENV PYTHONPATH=/app diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml index 83de9243..58cb9d74 100644 --- a/docker/dev/compose.yml +++ b/docker/dev/compose.yml @@ -17,6 +17,8 @@ services: interval: 10s timeout: 5s retries: 5 + start_period: 30s + start_interval: 1s ollama: image: ollama/ollama:latest @@ -33,6 +35,7 @@ services: timeout: 5s retries: 5 start_period: 30s + start_interval: 2s whisper: image: onerahmet/openai-whisper-asr-webservice:latest @@ -53,6 +56,7 @@ services: timeout: 5s retries: 5 start_period: 60s + start_interval: 3s app: build: diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index f97d6dd5..ed65a283 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,32 +1,44 @@ +# ---- builder ---- FROM python:3.11-slim AS builder -WORKDIR /build +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential g++ libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +WORKDIR /build COPY requirements.txt . -RUN pip install --no-cache-dir --prefix=/install -r requirements.txt +RUN --mount=type=cache,target=/root/.cache/uv \ + UV_TORCH_BACKEND=cpu \ + uv pip install --system -r requirements.txt +# ---- runtime ---- FROM python:3.11-slim WORKDIR /app -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ libgl1 \ libglib2.0-0 \ libxcb1 \ - && rm -rf /var/lib/apt/lists/* + libpq5 && \ + rm -rf /var/lib/apt/lists/* -COPY --from=builder /install /usr/local +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin -# Copy only app code, not data/ temp/ tests/ docs/ etc. COPY app/ ./app/ COPY requirements.txt . COPY docker/entrypoint.sh /entrypoint.sh +# Bake deps hash so the entrypoint can skip the redundant install when unchanged. +RUN sha256sum requirements.txt | cut -d' ' -f1 > /opt/req_hash + ENV PYTHONPATH=/app -# Data dirs created here; actual storage comes from mounted volumes at runtime. RUN mkdir -p /data/db /data/uploads && chmod +x /entrypoint.sh EXPOSE 8000 From 9f088a8debb0a6459b8b37377206beb94ce053d8 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 23 Jun 2026 17:48:39 +0530 Subject: [PATCH 46/85] fast make workflow and runtime dependency sync --- Makefile | 36 +++++++++++++++++++++++------------- docker/entrypoint.sh | 19 +++++++++++++++++-- scripts/setup-dockers-env.sh | 6 +++++- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 7d1d3575..3b9a95f8 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean +.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean status ready-banner sync COMPOSE = docker compose -f docker/dev/compose.yml --env-file docker/.env.dev ENV_DEV = docker/.env.dev @@ -22,6 +22,8 @@ help: @echo "make build - Build Docker images" @echo "make up - Start all containers (detached)" @echo "make down - Stop all containers" + @echo "make sync - Fast-install new requirements.txt deps into running app (no rebuild)" + @echo "make status - Show compact container health summary" @echo "make logs - Stream all container logs" @echo "make logs-app - Stream app container logs" @echo "make logs-ollama - Stream Ollama container logs" @@ -43,30 +45,38 @@ init: *) echo "Run 'make fireform' when ready." ;; \ esac -fireform: build up - @printf "Waiting for Ollama to be ready..." - @until $(COMPOSE) exec -T ollama ollama list > /dev/null 2>&1; do \ - printf '.'; sleep 2; \ - done - @echo " ready." +fireform: + @$(COMPOSE) up -d --build @if $(COMPOSE) exec -T ollama ollama list 2>/dev/null | grep -q "^$(OLLAMA_MODEL)"; then \ echo " Model $(OLLAMA_MODEL) already pulled."; \ else \ echo " Pulling $(OLLAMA_MODEL)..."; \ $(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL); \ fi - @echo "" - @echo "FireForm is ready!" - @echo " API: http://localhost:8000" - @echo " API Docs: http://localhost:8000/docs" - @echo "" - @echo "Run 'make logs' to view live logs, 'make down' to stop." + @$(MAKE) --no-print-directory ready-banner build: @$(COMPOSE) build up: @$(COMPOSE) up -d + @$(MAKE) --no-print-directory ready-banner + +# Fast path for "I added a package": install the delta into the running container +# (no image rebuild, no 1.6GB layer re-export). uv installs only what's missing in +sync: + @$(COMPOSE) exec -T app sh -c "UV_TORCH_BACKEND=cpu uv pip install --system -r requirements.txt" + +status: + @$(COMPOSE) ps --format 'table {{.Service}}\t{{.Status}}' + +ready-banner: + @echo "" + @echo "FireForm is ready!" + @echo " API: http://localhost:8000" + @echo " API Docs: http://localhost:8000/docs" + @echo "" + @echo "Run 'make logs' to view live logs, 'make down' to stop." down: @$(COMPOSE) down --remove-orphans diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 8694d9ff..a4832be7 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,10 +1,25 @@ #!/bin/sh set -e -# Ensure data directories exist (volumes may be empty on first run) mkdir -p /data/uploads -# Run DB migrations / init before starting the server +# Reinstall deps only when the live requirements.txt differs from what the image +# was built with. The image bakes the hash at /opt/req_hash; in dev the live file +# comes from the bind mount. Matching hash => deps already baked in => skip (instant). +BAKED_HASH=$(cat /opt/req_hash 2>/dev/null || echo "none") +LIVE_HASH=$(sha256sum requirements.txt 2>/dev/null | cut -d' ' -f1 || echo "unknown") + +if [ "$BAKED_HASH" = "$LIVE_HASH" ]; then + echo "[entrypoint] dependencies up to date — skipping install" +else + echo "[entrypoint] requirements.txt changed since image build — syncing deps..." + if command -v uv > /dev/null 2>&1; then + UV_TORCH_BACKEND=cpu uv pip install --system -r requirements.txt + else + pip install -r requirements.txt + fi +fi + python3 -m app.db.init_db exec "$@" diff --git a/scripts/setup-dockers-env.sh b/scripts/setup-dockers-env.sh index 90adb5c4..17418ad9 100644 --- a/scripts/setup-dockers-env.sh +++ b/scripts/setup-dockers-env.sh @@ -1,4 +1,8 @@ #!/bin/bash source venv/bin/activate -pip install -r requirements.txt +if command -v uv > /dev/null 2>&1; then + uv pip install -r requirements.txt +else + pip install -r requirements.txt +fi From 501d837ebc16666ee59c5fd56993a3e11080fb40 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 23 Jun 2026 17:49:19 +0530 Subject: [PATCH 47/85] updated package in ci-cd --- .github/workflows/lint.yml | 7 ++++--- .github/workflows/tests.yml | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1652a2b7..97bd836b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,10 +19,11 @@ jobs: with: python-version: "3.11" + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install linter - run: | - python -m pip install --upgrade pip - pip install ruff + run: uv pip install --system ruff - name: Run linter run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c90b98e..44536145 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,10 +18,11 @@ jobs: with: python-version: "3.11" + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + run: uv pip install --system -r requirements.txt - name: Run tests env: From 0e3217f75f98f60c4a25e20958cfb5c710f188b9 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 23 Jun 2026 18:11:26 +0530 Subject: [PATCH 48/85] fixed redis late start time --- docker/dev/compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml index 83e1d821..820711d1 100644 --- a/docker/dev/compose.yml +++ b/docker/dev/compose.yml @@ -72,6 +72,8 @@ services: interval: 10s timeout: 5s retries: 5 + start_period: 30s + start_interval: 1s app: build: From d31ff2dc2e3b6b8ee939f826346b4ff3cfe09425 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Tue, 23 Jun 2026 07:16:27 -0700 Subject: [PATCH 49/85] refactor: :recycle: replaced Union and Sequence to newer accepted versions --- alembic/versions/001_initial_schema.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/alembic/versions/001_initial_schema.py b/alembic/versions/001_initial_schema.py index e6d9fade..59faa787 100644 --- a/alembic/versions/001_initial_schema.py +++ b/alembic/versions/001_initial_schema.py @@ -5,16 +5,16 @@ Create Date: 2026-06-22 """ -from typing import Sequence, Union +from collections.abc import Sequence from alembic import op import sqlalchemy as sa import sqlmodel revision: str = "001" -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: From 7f04672f48eed78130617e0d95e3cfa1ab7f6e6a Mon Sep 17 00:00:00 2001 From: marcvergees Date: Tue, 23 Jun 2026 07:33:10 -0700 Subject: [PATCH 50/85] refactor: :recycle: restructuring routing of v1 --- app/api/router.py | 27 ++++++++------------------- app/api/{v1 => }/routes/system.py | 0 app/api/v1/__init__.py | 0 app/api/v1/router.py | 13 ------------- app/api/v1/routes/__init__.py | 0 app/core/config.py | 3 +++ 6 files changed, 11 insertions(+), 32 deletions(-) rename app/api/{v1 => }/routes/system.py (100%) delete mode 100644 app/api/v1/__init__.py delete mode 100644 app/api/v1/router.py delete mode 100644 app/api/v1/routes/__init__.py diff --git a/app/api/router.py b/app/api/router.py index b8e3e29b..962a26ca 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -1,23 +1,12 @@ -"""Aggregates every route module into a single API router. - -Add new feature routers here; main.py only mounts this one router. - -IMPORTANT: Never add prefix="/api/v1" to api_router itself — that would -silently move the existing /templates and /forms routes and break the frontend. -New v1 endpoints live in app/api/v1/ and are included via v1_router below. -""" - from fastapi import APIRouter -from app.api.routes import forms, jobs, templates -from app.api.v1.router import v1_router -from app.api.routes import forms, templates, weather, zipcode +from app.api.routes import forms, templates, weather, zipcode, jobs, system +from app.core.config import API_PREFIX api_router = APIRouter() -api_router.include_router(templates.router) -api_router.include_router(forms.router) -api_router.include_router(v1_router) -api_router.include_router(jobs.router) - -api_router.include_router(weather.router) -api_router.include_router(zipcode.router) +api_router.include_router(templates.router, prefix=API_PREFIX) +api_router.include_router(forms.router, prefix=API_PREFIX) +api_router.include_router(system.router, prefix=API_PREFIX) +api_router.include_router(jobs.router, prefix=API_PREFIX) +api_router.include_router(weather.router, prefix=API_PREFIX) +api_router.include_router(zipcode.router, prefix=API_PREFIX) \ No newline at end of file diff --git a/app/api/v1/routes/system.py b/app/api/routes/system.py similarity index 100% rename from app/api/v1/routes/system.py rename to app/api/routes/system.py diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/api/v1/router.py b/app/api/v1/router.py deleted file mode 100644 index da6a812c..00000000 --- a/app/api/v1/router.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Aggregates all /api/v1 route modules. - -This router is included ADDITIVELY in app/api/router.py alongside the -existing /templates and /forms routers. Never put prefix="/api/v1" on the -top-level api_router — that would silently move the existing routes. -""" - -from fastapi import APIRouter - -from app.api.v1.routes import system - -v1_router = APIRouter(prefix="/api/v1") -v1_router.include_router(system.router) diff --git a/app/api/v1/routes/__init__.py b/app/api/v1/routes/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/core/config.py b/app/core/config.py index 40bed253..391c082f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -53,3 +53,6 @@ # Advisory Retry-After sent to clients on 503 responses. This is a client # hint, not a measured backpressure value — the app has no queue-depth signal. RETRY_AFTER_SECONDS = 30 + +# --- API Versioning ------------------------------------------------------- +API_PREFIX = "/api/v1" \ No newline at end of file From 08eb9f20ce0afdd59a0bc0091a23ccd0a7b3fb93 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Tue, 23 Jun 2026 11:23:58 -0700 Subject: [PATCH 51/85] feat: :sparkles: introducing analytics and form submission history --- app/api/routes/forms.py | 79 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/app/api/routes/forms.py b/app/api/routes/forms.py index 9ecb46c8..63366f46 100644 --- a/app/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -12,7 +12,7 @@ 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.models import FormSubmission, Template from app.services.controller import Controller router = APIRouter(prefix="/forms", tags=["forms"]) @@ -104,3 +104,80 @@ def transcribe(audio: UploadFile = File(...)): text = response.text.strip() return TranscriptionResponse(text=text) + + +@router.get("/submissions") +def get_submissions(db: Session = Depends(get_db)): + from sqlmodel import select + statement = ( + select(FormSubmission, Template.name) + .join(Template, FormSubmission.template_id == Template.id, isouter=True) + .order_by(FormSubmission.created_at.desc(), FormSubmission.id.desc()) + ) + results = db.exec(statement).all() + return [ + { + "id": sub.id, + "template_id": sub.template_id, + "template_name": name or "Unknown Template", + "input_text": sub.input_text, + "output_pdf_path": sub.output_pdf_path, + "created_at": sub.created_at.isoformat() if sub.created_at else None, + } + for sub, name in results + ] + + +@router.get("/submissions/analytics") +def get_submissions_analytics(db: Session = Depends(get_db)): + from collections import Counter + import re + from sqlmodel import select + + statement = select(FormSubmission, Template.name).join( + Template, FormSubmission.template_id == Template.id, isouter=True + ) + results = db.exec(statement).all() + + total_submissions = len(results) + + template_counts = Counter() + daily_counts = Counter() + words = [] + + stopwords = { + "the", "and", "a", "of", "to", "in", "is", "that", "it", "was", "for", "on", + "as", "with", "by", "at", "an", "be", "this", "are", "from", "or", "have", + "has", "had", "but", "not", "he", "she", "they", "we", "i", "you", "my", "his", + "her", "their", "our", "me", "him", "them", "us", "about", "there", "their", + "were", "been", "would", "could", "should", "will", "can", "no", "yes", "any", + "so", "very", "patient", "presents", "with", "reported", "history", "shows", + "left", "right", "pain", "due", "after", "before", "emergency", "department", + "medical", "clinical" + } + + for sub, name in results: + template_name = name or "Unknown Template" + template_counts[template_name] += 1 + + if sub.created_at: + date_str = sub.created_at.strftime("%Y-%m-%d") + daily_counts[date_str] += 1 + + if sub.input_text: + found_words = re.findall(r"\b[a-zA-Z]{3,15}\b", sub.input_text.lower()) + for w in found_words: + if w not in stopwords: + words.append(w) + + sorted_daily = [{"date": k, "count": v} for k, v in sorted(daily_counts.items())] + sorted_templates = [{"template_name": k, "count": v} for k, v in template_counts.most_common()] + common_terms = [{"word": k, "count": v} for k, v in Counter(words).most_common(12)] + + return { + "total_submissions": total_submissions, + "by_template": sorted_templates, + "by_date": sorted_daily, + "common_terms": common_terms, + } + From 2d20143f2fe5051d18cdfba8c8e4ef5095b1a876 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 23 Jun 2026 22:27:56 +0530 Subject: [PATCH 52/85] fix(db): add job table to migration 001 and align schema with models --- alembic/env.py | 5 ++-- alembic/versions/001_initial_schema.py | 38 +++++++++++++++++--------- app/models/models.py | 2 +- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/alembic/env.py b/alembic/env.py index d503289d..06639d87 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -3,9 +3,10 @@ from alembic import context from sqlalchemy import engine_from_config, pool +from sqlmodel import SQLModel + from app.core.config import DATABASE_URL -from app.models.models import SQLModel -from app.models import Template, FormSubmission +from app.models import FormSubmission, Job, Template config = context.config diff --git a/alembic/versions/001_initial_schema.py b/alembic/versions/001_initial_schema.py index 59faa787..12db15df 100644 --- a/alembic/versions/001_initial_schema.py +++ b/alembic/versions/001_initial_schema.py @@ -1,4 +1,4 @@ -"""Initial schema — Template and FormSubmission tables. +"""Initial schema — Template, FormSubmission, and Job tables. Revision ID: 001 Revises: @@ -24,12 +24,7 @@ def upgrade() -> None: sa.Column("name", sqlmodel.sql.sqltypes.AutoString, nullable=False), sa.Column("fields", sa.JSON, nullable=False), sa.Column("pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=False), - sa.Column( - "created_at", - sa.DateTime, - nullable=False, - server_default=sa.text("now()"), - ), + sa.Column("created_at", sa.DateTime, nullable=False), ) op.create_table( @@ -38,15 +33,32 @@ def upgrade() -> None: sa.Column("template_id", sa.Integer, sa.ForeignKey("template.id"), nullable=False), sa.Column("input_text", sqlmodel.sql.sqltypes.AutoString, nullable=False), sa.Column("output_pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=False), - sa.Column( - "created_at", - sa.DateTime, - nullable=False, - server_default=sa.text("now()"), - ), + sa.Column("created_at", sa.DateTime, nullable=False), ) + op.create_table( + "job", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("job_id", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("celery_task_id", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("job_type", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("template_id", sa.Integer, sa.ForeignKey("template.id"), nullable=True), + sa.Column("input_text", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("progress_percent", sa.Integer, nullable=False), + sa.Column("result_url", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("error", sa.JSON, nullable=True), + sa.Column("model", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + op.create_index("ix_job_job_id", "job", ["job_id"], unique=True) + op.create_index("ix_job_celery_task_id", "job", ["celery_task_id"], unique=False) + def downgrade() -> None: + op.drop_index("ix_job_celery_task_id", table_name="job") + op.drop_index("ix_job_job_id", table_name="job") + op.drop_table("job") op.drop_table("formsubmission") op.drop_table("template") diff --git a/app/models/models.py b/app/models/models.py index c4ddf0bd..fea977f1 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -8,7 +8,7 @@ class Template(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str - fields: dict = Field(sa_column=Column(JSON)) + fields: dict = Field(sa_column=Column(JSON, nullable=False)) pdf_path: str created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) From e76392163f1c94971bed528a1b2cc6d3bca83c4d Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 24 Jun 2026 00:02:13 +0530 Subject: [PATCH 53/85] updated teststo include job tables --- tests/test_migrations.py | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 3e920322..87a6149c 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -35,6 +35,7 @@ def test_upgrade_head(alembic_cfg, alembic_engine): tables = inspector.get_table_names() assert "template" in tables assert "formsubmission" in tables + assert "job" in tables assert "alembic_version" in tables @@ -46,6 +47,7 @@ def test_downgrade_base(alembic_cfg, alembic_engine): tables = inspector.get_table_names() assert "template" not in tables assert "formsubmission" not in tables + assert "job" not in tables def test_round_trip(alembic_cfg, alembic_engine): @@ -83,3 +85,44 @@ def test_formsubmission_fk(alembic_cfg, alembic_engine): assert len(fks) == 1 assert fks[0]["referred_table"] == "template" assert fks[0]["referred_columns"] == ["id"] + + +def test_job_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("job")} + assert columns == { + "id", + "job_id", + "celery_task_id", + "job_type", + "template_id", + "input_text", + "status", + "progress_percent", + "result_url", + "error", + "model", + "created_at", + "updated_at", + } + + +def test_job_indexes(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + indexes = {ix["name"]: ix for ix in inspector.get_indexes("job")} + assert indexes["ix_job_job_id"]["unique"] + assert not indexes["ix_job_celery_task_id"]["unique"] + + +def test_job_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("job") + assert len(fks) == 1 + assert fks[0]["referred_table"] == "template" + assert fks[0]["referred_columns"] == ["id"] From 638353688963ac4ae602e2d31099897fb7bd05e9 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Tue, 23 Jun 2026 19:36:34 -0700 Subject: [PATCH 54/85] feat: :bug: implement deletes, purges, apikey for security and updated readme.md --- README.md | 66 ++++++++- app/api/deps.py | 18 ++- app/api/routes/forms.py | 72 +++++++++- app/api/routes/templates.py | 47 ++++++- app/core/celery.py | 12 +- app/core/config.py | 8 +- app/db/repositories.py | 15 +++ app/tasks/purge.py | 90 +++++++++++++ tests/conftest.py | 1 + tests/test_api.py | 47 +++---- tests/test_deletion.py | 257 ++++++++++++++++++++++++++++++++++++ tests/test_jobs.py | 23 ++-- tests/test_v1_system.py | 32 ++--- 13 files changed, 624 insertions(+), 64 deletions(-) create mode 100644 app/tasks/purge.py create mode 100644 tests/test_deletion.py diff --git a/README.md b/README.md index 15a67a00..a07ea1f0 100644 --- a/README.md +++ b/README.md @@ -102,14 +102,74 @@ FireForm is designed from the ground up to ensure that all generated and collect ## 🔒 Privacy & Applicable Laws -FireForm is built on a local-first architecture, ensuring that all processing (including AI extraction) occurs locally on the user's hardware. We do not collect, process, or share any Personally Identifiable Information (PII) with third parties. By keeping data completely offline, FireForm natively assists deploying organizations in complying with strict data protection laws such as **HIPAA**, **CCPA**, and **GDPR**. +FireForm is built on a **local-first architecture**: all processing (including AI extraction) occurs locally on the operator's hardware and nothing is transmitted to external servers by default. However, operators should be aware of the following: -For complete details on our data handling, consent management procedures, and how the solution was designed to comply with HIPAA, CCPA, and GDPR, please review our public [Privacy Policy](https://fireform-core.github.io/FireForm/privacy.html). +- **PII is processed locally.** Incident descriptions, names, dates, and other personally identifiable information are extracted by the local LLM, written into filled PDF files, and persisted in the local database (`FormSubmission` records). These files and records remain on disk until explicitly deleted. +- **No external transmission.** Data does not leave the machine by default. Audio transcription (Whisper) and LLM inference (Ollama) are both local services. +- **Operator responsibility.** Because PII is stored locally, the security of that data depends entirely on the operator's host system, filesystem permissions, backup practices, and access controls. + +For complete details on data handling and compliance guidance, please review our public [Privacy Policy](https://fireform-core.github.io/FireForm/privacy.html). + +## 🗑️ Data Deletion & Retention + +FireForm provides API endpoints and an automated purge mechanism to manage stored PII. + +### Deleting individual records + +```bash +# Delete a single form submission (removes DB record + output PDF) +curl -X DELETE http://localhost:8000/api/v1/forms/{submission_id} \ + -H "X-API-Key: your-key" + +# Delete a template and all associated submissions +curl -X DELETE http://localhost:8000/api/v1/templates/{template_id} \ + -H "X-API-Key: your-key" +``` + +### Bulk purge + +```bash +# Purge all submissions older than 30 days +curl -X POST "http://localhost:8000/api/v1/forms/purge?days=30" \ + -H "X-API-Key: your-key" +``` + +### Automated retention + +Set `RETENTION_PERIOD_DAYS` in `docker/.env.dev` (default: `30`). Celery Beat will automatically run a purge job every night at 03:00 UTC: + +```env +RETENTION_PERIOD_DAYS=30 +``` + +To enable the scheduler: `celery -A app.core.celery beat -l info` + +## 🔑 Access Control + +Deletion and purge endpoints can be protected by setting an API key: + +```env +FIREFORM_API_KEY=your-strong-secret-key +``` + +When set, all `DELETE` and `POST /purge` requests must include one of: +- **Header:** `X-API-Key: your-strong-secret-key` +- **Bearer token:** `Authorization: Bearer your-strong-secret-key` + +Read-only endpoints (list, preview, fill) do **not** require authentication. + +## 🛡️ Operator Hardening Recommendations + +- **Filesystem permissions:** Restrict access to `data/inputs/` and `data/outputs/` to the application user only (`chmod 700`). +- **Backups:** Encrypt any backups containing output PDFs. +- **HTTPS:** Deploy behind a reverse proxy (e.g., nginx) with TLS enabled. +- **API key rotation:** Rotate `FIREFORM_API_KEY` regularly and never commit it to version control. +- **Retention policy:** Configure `RETENTION_PERIOD_DAYS` in line with applicable regulations (HIPAA, GDPR, CCPA). ## 🛡️ Do No Harm by Design FireForm is built to anticipate and prevent harm: -- **Data Privacy & Security (9A):** We handle sensitive incident data entirely offline. By never transmitting data to the cloud, we natively prevent online data breaches of PII. +- **Data Privacy & Security (9A):** We handle sensitive incident data entirely offline. By never transmitting data to the cloud, we natively prevent online data breaches of PII. Operators are responsible for securing local storage; see the Operator Hardening Recommendations above. - **Inappropriate Content (9B):** The application is an internal productivity tool without public social interactions or user-generated content hosting, completely mitigating the risks of public harassment or illegal content distribution. - **Protection from Harassment (9C):** For our open-source contributor community, we strictly enforce our [Code of Conduct](CODE_OF_CONDUCT.md) to ensure a safe, harassment-free environment for all contributors. diff --git a/app/api/deps.py b/app/api/deps.py index 4aa9614b..36ec257c 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -1,4 +1,20 @@ from app.db.database import get_session +from fastapi import Header, Request +from app.core.config import FIREFORM_API_KEY +from app.core.errors.base import AppError def get_db(): - yield from get_session() \ No newline at end of file + yield from get_session() + +def verify_api_key(request: Request, x_api_key: str | None = Header(None)): + if not FIREFORM_API_KEY: + return + + provided_key = x_api_key + if not provided_key: + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + provided_key = auth_header[len("Bearer "):] + + if not provided_key or provided_key != FIREFORM_API_KEY: + raise AppError("Unauthorized", status_code=401, error_code="UNAUTHORIZED") \ No newline at end of file diff --git a/app/api/routes/forms.py b/app/api/routes/forms.py index 9ecb46c8..87e4a165 100644 --- a/app/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -1,20 +1,40 @@ +from datetime import datetime, timedelta, timezone +from pathlib import Path import requests -from fastapi import APIRouter, Depends, File, UploadFile -from sqlmodel import Session +from fastapi import APIRouter, Depends, File, UploadFile, Query +from sqlmodel import Session, select -from app.api.deps import get_db +from app.api.deps import get_db, verify_api_key from app.api.schemas.forms import ( FormFill, FormFillResponse, ModelsResponse, TranscriptionResponse, ) -from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, WHISPER_HOST +from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, WHISPER_HOST, BASE_DIR, RETENTION_PERIOD_DAYS from app.core.errors.base import AppError -from app.db.repositories import create_form, get_template +from app.db.repositories import create_form, get_template, get_form_submission, delete_form_submission from app.models import FormSubmission from app.services.controller import Controller +PROJECT_ROOT = BASE_DIR + +def _resolve_project_file(file_path: str) -> Path: + raw_path = (file_path or "").strip() + if not raw_path: + raise AppError("Path is required", status_code=400) + + candidate = Path(raw_path) + if not candidate.is_absolute(): + candidate = (PROJECT_ROOT / candidate).resolve() + else: + candidate = candidate.resolve() + + if candidate != PROJECT_ROOT and PROJECT_ROOT not in candidate.parents: + raise AppError("Path must be inside the project", status_code=400) + + return candidate + router = APIRouter(prefix="/forms", tags=["forms"]) @@ -104,3 +124,45 @@ def transcribe(audio: UploadFile = File(...)): text = response.text.strip() return TranscriptionResponse(text=text) + + +@router.delete("/{submission_id}", dependencies=[Depends(verify_api_key)]) +def delete_submission_endpoint(submission_id: int, db: Session = Depends(get_db)): + sub = get_form_submission(db, submission_id) + if not sub: + raise AppError("Submission not found", status_code=404, error_code="SUBMISSION_NOT_FOUND") + + if sub.output_pdf_path: + try: + resolved_out = _resolve_project_file(sub.output_pdf_path) + if resolved_out.exists() and resolved_out.is_file(): + resolved_out.unlink() + except Exception: + pass + + delete_form_submission(db, sub) + return {"status": "success", "message": "Submission and associated output file deleted"} + + +@router.post("/purge", dependencies=[Depends(verify_api_key)]) +def purge_submissions_endpoint(days: int = Query(default=None), db: Session = Depends(get_db)): + retention_days = days if days is not None else RETENTION_PERIOD_DAYS + cutoff_date = datetime.now(timezone.utc) - timedelta(days=retention_days) + + statement = select(FormSubmission).where(FormSubmission.created_at < cutoff_date) + submissions = list(db.exec(statement)) + + purged_count = 0 + for sub in submissions: + if sub.output_pdf_path: + try: + resolved_out = _resolve_project_file(sub.output_pdf_path) + if resolved_out.exists() and resolved_out.is_file(): + resolved_out.unlink() + except Exception: + pass + delete_form_submission(db, sub) + purged_count += 1 + + return {"status": "success", "purged_count": purged_count, "retention_days_used": retention_days} + diff --git a/app/api/routes/templates.py b/app/api/routes/templates.py index cc98370d..ee7189aa 100644 --- a/app/api/routes/templates.py +++ b/app/api/routes/templates.py @@ -6,7 +6,7 @@ from fastapi.responses import FileResponse from sqlmodel import Session -from app.api.deps import get_db +from app.api.deps import get_db, verify_api_key from app.api.schemas.templates import ( TemplateCreate, TemplateResponse, @@ -15,9 +15,10 @@ MakeFillableResponse, ) 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.db.repositories import create_template, list_templates, get_template, delete_template +from app.models import Template, FormSubmission, Job from app.services.controller import Controller +from sqlmodel import select router = APIRouter(prefix="/templates", tags=["templates"]) PROJECT_ROOT = BASE_DIR @@ -197,3 +198,43 @@ def make_fillable(req: MakeFillableRequest): pdf_path=relative_path, field_count=_count_pdf_widgets(relative_path), ) + + +@router.delete("/{template_id}", dependencies=[Depends(verify_api_key)]) +def delete_template_endpoint(template_id: int, db: Session = Depends(get_db)): + template = get_template(db, template_id) + if not template: + raise HTTPException(status_code=404, detail="Template not found") + + # 1. Clean up associated submissions and their generated PDFs + sub_stmt = select(FormSubmission).where(FormSubmission.template_id == template_id) + submissions = list(db.exec(sub_stmt)) + for sub in submissions: + if sub.output_pdf_path: + try: + resolved_out = _resolve_project_file(sub.output_pdf_path) + if resolved_out.exists() and resolved_out.is_file(): + resolved_out.unlink() + except Exception: + pass + db.delete(sub) + + # 2. Clean up associated jobs + job_stmt = select(Job).where(Job.template_id == template_id) + jobs = list(db.exec(job_stmt)) + for job in jobs: + db.delete(job) + + # 3. Delete template PDF file + if template.pdf_path: + try: + resolved_pdf = _resolve_project_file(template.pdf_path) + if resolved_pdf.exists() and resolved_pdf.is_file(): + resolved_pdf.unlink() + except Exception: + pass + + # 4. Delete the template itself + delete_template(db, template) + return {"status": "success", "message": "Template and all associated data deleted"} + diff --git a/app/core/celery.py b/app/core/celery.py index 4efe5dbc..f7637025 100644 --- a/app/core/celery.py +++ b/app/core/celery.py @@ -16,4 +16,14 @@ result_expires=86400, ) -celery_app.conf.include = ["app.tasks.fill"] +celery_app.conf.include = ["app.tasks.fill", "app.tasks.purge"] + +# Optional Celery Beat schedule — runs purge_old_submissions once a day. +# Enable by running: celery -A app.core.celery beat +from celery.schedules import crontab # noqa: E402 +celery_app.conf.beat_schedule = { + "daily-submission-purge": { + "task": "purge_old_submissions", + "schedule": crontab(hour=3, minute=0), # 03:00 UTC daily + }, +} diff --git a/app/core/config.py b/app/core/config.py index 391c082f..38db4813 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -55,4 +55,10 @@ RETRY_AFTER_SECONDS = 30 # --- API Versioning ------------------------------------------------------- -API_PREFIX = "/api/v1" \ No newline at end of file +API_PREFIX = "/api/v1" + +# --- Security & Access Control --------------------------------------------- +FIREFORM_API_KEY = os.getenv("FIREFORM_API_KEY", "") + +# --- Data Retention -------------------------------------------------------- +RETENTION_PERIOD_DAYS = int(os.getenv("RETENTION_PERIOD_DAYS", "30")) \ No newline at end of file diff --git a/app/db/repositories.py b/app/db/repositories.py index 8b6a2c4f..d7445ff6 100644 --- a/app/db/repositories.py +++ b/app/db/repositories.py @@ -51,3 +51,18 @@ def update_job(session: Session, job: Job) -> Job: session.commit() session.refresh(job) return job + + +def delete_template(session: Session, template: Template) -> None: + session.delete(template) + session.commit() + + +def get_form_submission(session: Session, submission_id: int) -> FormSubmission | None: + return session.get(FormSubmission, submission_id) + + +def delete_form_submission(session: Session, submission: FormSubmission) -> None: + session.delete(submission) + session.commit() + diff --git a/app/tasks/purge.py b/app/tasks/purge.py new file mode 100644 index 00000000..83256751 --- /dev/null +++ b/app/tasks/purge.py @@ -0,0 +1,90 @@ +"""Celery beat task: purge form submissions older than RETENTION_PERIOD_DAYS. + +Runs automatically if Celery Beat is configured. Can also be triggered manually +via the API endpoint POST /api/v1/forms/purge. +""" + +import logging +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from app.core.celery import celery_app +from app.core.config import BASE_DIR, RETENTION_PERIOD_DAYS +from app.db.database import get_session +from app.db.repositories import delete_form_submission +from app.models import FormSubmission +from sqlmodel import select + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = BASE_DIR + + +def _safe_delete_file(file_path: str) -> bool: + """Delete a project-relative or absolute file safely. Returns True if deleted.""" + try: + candidate = Path(file_path) + if not candidate.is_absolute(): + candidate = (PROJECT_ROOT / candidate).resolve() + else: + candidate = candidate.resolve() + + # Safety: must stay inside project root + if candidate != PROJECT_ROOT and PROJECT_ROOT not in candidate.parents: + logger.warning("Skipping file outside project root: %s", candidate) + return False + + if candidate.exists() and candidate.is_file(): + candidate.unlink() + return True + except Exception as exc: + logger.error("Failed to delete file %s: %s", file_path, exc) + return False + + +@celery_app.task(name="purge_old_submissions") +def purge_old_submissions(retention_days: int | None = None) -> dict: + """Delete form submissions (and associated output PDFs) older than retention_days. + + Args: + retention_days: Number of days to retain data. Defaults to the + RETENTION_PERIOD_DAYS environment variable (default: 30). + + Returns: + dict with purged_count and retention_days_used. + """ + days = retention_days if retention_days is not None else RETENTION_PERIOD_DAYS + cutoff = datetime.now(timezone.utc) - timedelta(days=days) + logger.info("Purging submissions older than %s days (cutoff: %s)", days, cutoff) + + session = next(get_session()) + purged_count = 0 + files_deleted = 0 + + try: + statement = select(FormSubmission).where(FormSubmission.created_at < cutoff) + submissions = list(session.exec(statement)) + + for sub in submissions: + if sub.output_pdf_path: + if _safe_delete_file(sub.output_pdf_path): + files_deleted += 1 + delete_form_submission(session, sub) + purged_count += 1 + + logger.info( + "Purge complete: removed %d submissions and %d output files", + purged_count, + files_deleted, + ) + return { + "purged_count": purged_count, + "files_deleted": files_deleted, + "retention_days_used": days, + } + + except Exception as exc: + logger.exception("purge_old_submissions task failed: %s", exc) + raise + finally: + session.close() diff --git a/tests/conftest.py b/tests/conftest.py index 2699127a..8c25516c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from app.main import app from app.api.deps import get_db +from app.core.config import API_PREFIX # single source of truth for all tests from app.models import Template, FormSubmission, Job # noqa: F401 — registers tables # --------------------------------------------------------------------------- diff --git a/tests/test_api.py b/tests/test_api.py index c5c1bf0b..32104ae7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,6 +7,7 @@ from sqlmodel import select from app.models import Template, FormSubmission +from app.core.config import API_PREFIX # ═══════════════════════════════════════════════════════════════════════════ @@ -83,7 +84,7 @@ def test_list_templates_ordering(self, db): class TestTemplateEndpoints: def test_list_templates_empty(self, client): - resp = client.get("/templates") + resp = client.get(f"{API_PREFIX}/templates") assert resp.status_code == 200 assert resp.json() == [] @@ -97,7 +98,7 @@ def test_create_template(self, client, mock_controller): "Location": "string", }, } - resp = client.post("/templates/create", json=payload) + resp = client.post(f"{API_PREFIX}/templates/create", json=payload) assert resp.status_code == 200 data = resp.json() @@ -110,12 +111,12 @@ def test_create_template(self, client, mock_controller): def test_create_then_list(self, client, mock_controller): """Creating a template should make it appear in the list.""" - client.post("/templates/create", json={ + client.post(f"{API_PREFIX}/templates/create", json={ "name": "T1", "pdf_path": "a.pdf", "fields": {"f": "string"}, }) - resp = client.get("/templates") + resp = client.get(f"{API_PREFIX}/templates") assert resp.status_code == 200 assert len(resp.json()) == 1 assert resp.json()[0]["name"] == "T1" @@ -129,7 +130,7 @@ def test_upload_pdf(self, client, pdf_upload, tmp_path, monkeypatch): tmp_path, ) resp = client.post( - "/templates/upload", + f"{API_PREFIX}/templates/upload", files=[pdf_upload], data={"directory": str(tmp_path)}, ) @@ -141,19 +142,19 @@ def test_upload_pdf(self, client, pdf_upload, tmp_path, monkeypatch): def test_upload_non_pdf_rejected(self, client): import io bad_file = ("file", ("notes.txt", io.BytesIO(b"hello"), "text/plain")) - resp = client.post("/templates/upload", files=[bad_file]) + resp = client.post(f"{API_PREFIX}/templates/upload", files=[bad_file]) assert resp.status_code == 400 assert "PDF" in resp.json()["detail"] def test_preview_missing_file(self, client): - resp = client.get("/templates/preview", params={"path": "src/inputs/nonexistent.pdf"}) + resp = client.get(f"{API_PREFIX}/templates/preview", params={"path": "src/inputs/nonexistent.pdf"}) assert resp.status_code == 404 def test_directory_traversal_blocked(self, client): import io pdf = ("file", ("evil.pdf", io.BytesIO(b"%PDF-1.4"), "application/pdf")) resp = client.post( - "/templates/upload", + f"{API_PREFIX}/templates/upload", files=[pdf], data={"directory": "/etc"}, ) @@ -169,7 +170,7 @@ class TestFormEndpoints: def _seed_template(self, client, mock_controller): """Helper: create a template and return its ID.""" - resp = client.post("/templates/create", json={ + resp = client.post(f"{API_PREFIX}/templates/create", json={ "name": "Employee Form", "pdf_path": "src/inputs/employee.pdf", "fields": { @@ -182,7 +183,7 @@ def _seed_template(self, client, mock_controller): def test_fill_form_success(self, client, mock_controller): tpl_id = self._seed_template(client, mock_controller) - resp = client.post("/forms/fill", json={ + resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": tpl_id, "input_text": "The employee is John Doe, email jdoe@ucsc.edu", }) @@ -195,7 +196,7 @@ def test_fill_form_success(self, client, mock_controller): mock_controller["form_ctrl"].fill_form.assert_called_once() def test_fill_form_missing_template(self, client, mock_controller): - resp = client.post("/forms/fill", json={ + resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": 9999, "input_text": "some text", }) @@ -205,7 +206,7 @@ def test_fill_form_template_file_not_found(self, client, mock_controller): tpl_id = self._seed_template(client, mock_controller) mock_controller["form_ctrl"].fill_form.side_effect = FileNotFoundError("PDF template not found") - resp = client.post("/forms/fill", json={ + resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": tpl_id, "input_text": "some text", }) @@ -215,7 +216,7 @@ def test_fill_form_template_file_not_found(self, client, mock_controller): def test_fill_form_validates_body(self, client): """Missing required fields → 422 with contract envelope.""" - resp = client.post("/forms/fill", json={}) + resp = client.post(f"{API_PREFIX}/forms/fill", json={}) assert resp.status_code == 422 body = resp.json() assert body["error_code"] == "VALIDATION_ERROR" @@ -243,7 +244,7 @@ def fake_post(url, params=None, files=None, timeout=None): monkeypatch.setattr("app.api.routes.forms.requests.post", fake_post) audio = ("audio", ("recording.wav", io.BytesIO(b"RIFFfake"), "audio/wav")) - resp = client.post("/forms/transcribe", files=[audio]) + resp = client.post(f"{API_PREFIX}/forms/transcribe", files=[audio]) assert resp.status_code == 200 assert resp.json()["text"] == "structure fire on main street" @@ -252,7 +253,7 @@ def fake_post(url, params=None, files=None, timeout=None): assert captured["params"]["output"] == "json" def test_list_models(self, client, monkeypatch): - """/forms/models lists Ollama models and always includes the default.""" + ""f"{API_PREFIX}/forms/models lists Ollama models and always includes the default.""" from unittest.mock import MagicMock fake_response = MagicMock() @@ -261,7 +262,7 @@ def test_list_models(self, client, monkeypatch): monkeypatch.setattr("app.api.routes.forms.requests.get", lambda *a, **k: fake_response) monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:1.5b") - resp = client.get("/forms/models") + resp = client.get(f"{API_PREFIX}/forms/models") assert resp.status_code == 200 body = resp.json() assert body["default"] == "qwen2.5:1.5b" @@ -278,14 +279,14 @@ def boom(*a, **k): monkeypatch.setattr("app.api.routes.forms.requests.get", boom) monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:1.5b") - resp = client.get("/forms/models") + resp = client.get(f"{API_PREFIX}/forms/models") assert resp.status_code == 200 assert resp.json()["models"] == ["qwen2.5:1.5b"] def test_fill_form_passes_model_override(self, client, mock_controller): """A `model` in the request reaches Controller.fill_form but isn't persisted.""" tpl_id = self._seed_template(client, mock_controller) - resp = client.post("/forms/fill", json={ + resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": tpl_id, "input_text": "John Doe", "model": "qwen2.5:3b", @@ -305,7 +306,7 @@ def fake_post(*args, **kwargs): monkeypatch.setattr("app.api.routes.forms.requests.post", fake_post) audio = ("audio", ("recording.wav", io.BytesIO(b"data"), "audio/wav")) - resp = client.post("/forms/transcribe", files=[audio]) + resp = client.post(f"{API_PREFIX}/forms/transcribe", files=[audio]) assert resp.status_code == 503 @@ -323,7 +324,7 @@ def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypa # -- Step 1: Upload a PDF -- monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path) upload_resp = client.post( - "/templates/upload", + f"{API_PREFIX}/templates/upload", files=[pdf_upload], data={"directory": str(tmp_path)}, ) @@ -332,7 +333,7 @@ def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypa assert uploaded_path.endswith(".pdf") # -- Step 2: Create a template from the uploaded PDF -- - create_resp = client.post("/templates/create", json={ + create_resp = client.post(f"{API_PREFIX}/templates/create", json={ "name": "Incident Report", "pdf_path": uploaded_path, "fields": { @@ -348,13 +349,13 @@ def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypa assert template_id is not None # -- Step 3: Verify template appears in list -- - list_resp = client.get("/templates") + list_resp = client.get(f"{API_PREFIX}/templates") assert list_resp.status_code == 200 templates = list_resp.json() assert any(t["id"] == template_id for t in templates) # -- Step 4: Fill the form -- - fill_resp = client.post("/forms/fill", json={ + fill_resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": template_id, "input_text": ( "Officer Jane Smith, badge 4521. On January 15 2025 at " diff --git a/tests/test_deletion.py b/tests/test_deletion.py new file mode 100644 index 00000000..5c39e431 --- /dev/null +++ b/tests/test_deletion.py @@ -0,0 +1,257 @@ +"""Tests for DELETE /api/v1/templates/{id}, DELETE /api/v1/forms/{id}, +POST /api/v1/forms/purge, and API-key access control. +""" + +import io +from datetime import datetime, timedelta, timezone +from pathlib import Path +from unittest.mock import patch + +import pytest + +from app.models import FormSubmission, Template +from app.core.config import API_PREFIX + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _seed_template(client, name="T1", pdf_path="src/inputs/t.pdf"): + resp = client.post(f"{API_PREFIX}/templates/create", json={ + "name": name, + "pdf_path": pdf_path, + "fields": {"name": "string"}, + }) + assert resp.status_code == 200, resp.json() + return resp.json()["id"] + + +def _seed_submission(db, template_id, output_pdf_path="src/outputs/out.pdf"): + sub = FormSubmission( + template_id=template_id, + input_text="John Doe, firefighter", + output_pdf_path=output_pdf_path, + ) + db.add(sub) + db.commit() + db.refresh(sub) + return sub.id + + +# =========================================================================== +# DELETE /api/v1/templates/{template_id} +# =========================================================================== + +class TestDeleteTemplate: + + def test_delete_template_no_key_required_when_unconfigured(self, client): + """No API key needed when FIREFORM_API_KEY is empty (default).""" + tpl_id = _seed_template(client) + resp = client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "success" + + def test_delete_template_removes_from_db(self, client, db): + tpl_id = _seed_template(client) + client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert db.get(Template, tpl_id) is None + + def test_delete_template_not_found(self, client): + resp = client.delete(f"{API_PREFIX}/templates/99999") + assert resp.status_code == 404 + + def test_delete_template_cascades_submissions(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + + client.delete(f"{API_PREFIX}/templates/{tpl_id}") + + assert db.get(FormSubmission, sub_id) is None + assert db.get(Template, tpl_id) is None + + def test_delete_template_deletes_pdf_file(self, client, tmp_path, monkeypatch): + """Verify the template PDF file is removed from disk on delete.""" + monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path) + pdf_file = tmp_path / "myform.pdf" + pdf_file.write_bytes(b"%PDF-1.4 fake") + + relative_path = "myform.pdf" + tpl_id = _seed_template(client, pdf_path=relative_path) + + client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert not pdf_file.exists() + + def test_delete_template_deletes_submission_output_pdfs(self, client, db, tmp_path, monkeypatch): + """Output PDFs of related submissions should be wiped on template deletion.""" + monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path) + + out_pdf = tmp_path / "filled.pdf" + out_pdf.write_bytes(b"%PDF-1.4 filled") + + tpl_id = _seed_template(client, pdf_path="tpl.pdf") + sub_id = _seed_submission(db, tpl_id, output_pdf_path="filled.pdf") + + client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert not out_pdf.exists() + + +# =========================================================================== +# DELETE /api/v1/forms/{submission_id} +# =========================================================================== + +class TestDeleteSubmission: + + def test_delete_submission_no_key_when_unconfigured(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + resp = client.delete(f"{API_PREFIX}/forms/{sub_id}") + assert resp.status_code == 200 + assert resp.json()["status"] == "success" + + def test_delete_submission_removes_from_db(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + client.delete(f"{API_PREFIX}/forms/{sub_id}") + assert db.get(FormSubmission, sub_id) is None + + def test_delete_submission_not_found(self, client): + resp = client.delete(f"{API_PREFIX}/forms/99999") + assert resp.status_code == 404 + + def test_delete_submission_removes_output_pdf(self, client, db, tmp_path, monkeypatch): + monkeypatch.setattr("app.api.routes.forms.PROJECT_ROOT", tmp_path) + out_pdf = tmp_path / "filled_out.pdf" + out_pdf.write_bytes(b"%PDF-1.4") + + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id, output_pdf_path="filled_out.pdf") + + client.delete(f"{API_PREFIX}/forms/{sub_id}") + assert not out_pdf.exists() + + +# =========================================================================== +# POST /api/v1/forms/purge +# =========================================================================== + +class TestPurgeSubmissions: + + def _seed_old_submission(self, db, tpl_id, days_old=40): + old_ts = datetime.now(timezone.utc) - timedelta(days=days_old) + sub = FormSubmission( + template_id=tpl_id, + input_text="old data", + output_pdf_path="src/outputs/old.pdf", + created_at=old_ts, + ) + db.add(sub) + db.commit() + db.refresh(sub) + return sub.id + + def test_purge_removes_old_submissions(self, client, db): + tpl_id = _seed_template(client) + old_id = self._seed_old_submission(db, tpl_id, days_old=40) + new_id = _seed_submission(db, tpl_id) # recent + + resp = client.post(f"{API_PREFIX}/forms/purge?days=30") + assert resp.status_code == 200 + assert resp.json()["purged_count"] == 1 + + assert db.get(FormSubmission, old_id) is None + assert db.get(FormSubmission, new_id) is not None + + def test_purge_nothing_to_remove(self, client, db): + tpl_id = _seed_template(client) + _seed_submission(db, tpl_id) # recent + resp = client.post(f"{API_PREFIX}/forms/purge?days=30") + assert resp.status_code == 200 + assert resp.json()["purged_count"] == 0 + + def test_purge_removes_output_pdf_file(self, client, db, tmp_path, monkeypatch): + monkeypatch.setattr("app.api.routes.forms.PROJECT_ROOT", tmp_path) + out_pdf = tmp_path / "old_filled.pdf" + out_pdf.write_bytes(b"%PDF-1.4") + + tpl_id = _seed_template(client) + old_ts = datetime.now(timezone.utc) - timedelta(days=50) + sub = FormSubmission( + template_id=tpl_id, + input_text="old", + output_pdf_path="old_filled.pdf", + created_at=old_ts, + ) + db.add(sub) + db.commit() + + resp = client.post(f"{API_PREFIX}/forms/purge?days=30") + assert resp.status_code == 200 + assert not out_pdf.exists() + + +# =========================================================================== +# API-Key Access Control +# =========================================================================== + +class TestApiKeyAccessControl: + + @pytest.fixture(autouse=True) + def _set_api_key(self, monkeypatch): + monkeypatch.setattr("app.api.deps.FIREFORM_API_KEY", "secret-test-key") + + def test_delete_template_requires_key_when_set(self, client): + tpl_id = _seed_template(client) + resp = client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert resp.status_code == 401 + + def test_delete_template_with_valid_x_api_key(self, client): + tpl_id = _seed_template(client) + resp = client.delete( + f"{API_PREFIX}/templates/{tpl_id}", + headers={"X-API-Key": "secret-test-key"}, + ) + assert resp.status_code == 200 + + def test_delete_template_with_bearer_token(self, client): + tpl_id = _seed_template(client) + resp = client.delete( + f"{API_PREFIX}/templates/{tpl_id}", + headers={"Authorization": "Bearer secret-test-key"}, + ) + assert resp.status_code == 200 + + def test_delete_template_wrong_key_rejected(self, client): + tpl_id = _seed_template(client) + resp = client.delete( + f"{API_PREFIX}/templates/{tpl_id}", + headers={"X-API-Key": "wrong-key"}, + ) + assert resp.status_code == 401 + + def test_delete_submission_requires_key_when_set(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + resp = client.delete(f"{API_PREFIX}/forms/{sub_id}") + assert resp.status_code == 401 + + def test_delete_submission_with_valid_key(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + resp = client.delete( + f"{API_PREFIX}/forms/{sub_id}", + headers={"X-API-Key": "secret-test-key"}, + ) + assert resp.status_code == 200 + + def test_purge_requires_key_when_set(self, client): + resp = client.post(f"{API_PREFIX}/forms/purge?days=30") + assert resp.status_code == 401 + + def test_purge_with_valid_key(self, client, db): + tpl_id = _seed_template(client) + resp = client.post( + f"{API_PREFIX}/forms/purge?days=30", + headers={"X-API-Key": "secret-test-key"}, + ) + assert resp.status_code == 200 diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 0355a7c9..c79e4a42 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -1,12 +1,13 @@ """Tests for async job submission and status endpoints.""" from unittest.mock import patch, MagicMock +from app.core.config import API_PREFIX class TestJobEndpoints: def _seed_template(self, client): - resp = client.post("/templates/create", json={ + resp = client.post(f"{API_PREFIX}/templates/create", json={ "name": "Test Template", "pdf_path": "test.pdf", "fields": {"name": "string"}, @@ -20,7 +21,7 @@ def test_submit_async_single(self, mock_task, client): mock_task.delay.return_value = mock_result tpl_id = self._seed_template(client) - resp = client.post("/forms/jobs", json={ + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ "template_ids": [tpl_id], "input_text": "John Doe firefighter", }) @@ -29,7 +30,7 @@ def test_submit_async_single(self, mock_task, client): assert len(data["jobs"]) == 1 assert data["jobs"][0]["status"] == "queued" assert "job_id" in data["jobs"][0] - assert data["jobs"][0]["poll_url"].startswith("/api/v1/jobs/") + assert data["jobs"][0]["poll_url"].startswith(f"{API_PREFIX}/jobs/") mock_task.delay.assert_called_once_with(tpl_id, "John Doe firefighter", None) @patch("app.api.routes.jobs.fill_form_task") @@ -41,7 +42,7 @@ def test_submit_async_batch(self, mock_task, client): t1 = self._seed_template(client) t2 = self._seed_template(client) - resp = client.post("/forms/jobs", json={ + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ "template_ids": [t1, t2], "input_text": "batch input", }) @@ -53,7 +54,7 @@ def test_submit_async_batch(self, mock_task, client): @patch("app.api.routes.jobs.fill_form_task") def test_submit_async_missing_template(self, mock_task, client): - resp = client.post("/forms/jobs", json={ + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ "template_ids": [9999], "input_text": "some text", }) @@ -65,13 +66,13 @@ def test_get_job_status(self, mock_task, client): mock_task.delay.return_value = MagicMock(id="celery-abc") tpl_id = self._seed_template(client) - submit_resp = client.post("/forms/jobs", json={ + submit_resp = client.post(f"{API_PREFIX}/forms/jobs", json={ "template_ids": [tpl_id], "input_text": "test input", }) job_id = submit_resp.json()["jobs"][0]["job_id"] - resp = client.get(f"/jobs/{job_id}") + resp = client.get(f"{API_PREFIX}/jobs/{job_id}") assert resp.status_code == 200 data = resp.json() assert data["job_id"] == job_id @@ -80,7 +81,7 @@ def test_get_job_status(self, mock_task, client): assert data["progress_percent"] == 0 def test_get_job_not_found(self, client): - resp = client.get("/jobs/00000000-0000-0000-0000-000000000000") + resp = client.get(f"{API_PREFIX}/jobs/00000000-0000-0000-0000-000000000000") assert resp.status_code == 404 @patch("app.api.routes.jobs.fill_form_task") @@ -88,7 +89,7 @@ def test_submit_with_model_override(self, mock_task, client): mock_task.delay.return_value = MagicMock(id="celery-xyz") tpl_id = self._seed_template(client) - resp = client.post("/forms/jobs", json={ + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ "template_ids": [tpl_id], "input_text": "test", "model": "mistral:latest", @@ -97,14 +98,14 @@ def test_submit_with_model_override(self, mock_task, client): mock_task.delay.assert_called_once_with(tpl_id, "test", "mistral:latest") def test_submit_empty_template_ids(self, client): - resp = client.post("/forms/jobs", json={ + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ "template_ids": [], "input_text": "test", }) assert resp.status_code == 422 def test_submit_empty_input_text(self, client): - resp = client.post("/forms/jobs", json={ + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ "template_ids": [1], "input_text": "", }) diff --git a/tests/test_v1_system.py b/tests/test_v1_system.py index a9903be3..b7b8dfcb 100644 --- a/tests/test_v1_system.py +++ b/tests/test_v1_system.py @@ -10,7 +10,7 @@ import pytest import requests as requests_lib -import app.api.v1.routes.system as system_mod +import app.api.routes.system as system_mod # --------------------------------------------------------------------------- @@ -120,7 +120,7 @@ class TestHealthEndpoint: def test_all_healthy(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") @@ -139,7 +139,7 @@ def test_all_healthy(self, client, monkeypatch): def test_ollama_down_returns_200_degraded(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_ollama_down) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_ollama_down) resp = client.get("/api/v1/health") @@ -153,7 +153,7 @@ def test_ollama_down_returns_200_degraded(self, client, monkeypatch): def test_whisper_down_returns_200_degraded(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_whisper_down) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_whisper_down) resp = client.get("/api/v1/health") @@ -166,7 +166,7 @@ def test_whisper_down_returns_200_degraded(self, client, monkeypatch): def test_db_down_returns_503_unhealthy(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_down()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") @@ -182,7 +182,7 @@ def test_db_execute_fails_returns_503_unhealthy(self, client, monkeypatch): """ monkeypatch.setattr(system_mod, "engine", _mock_engine_execute_fails()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") @@ -196,7 +196,7 @@ def test_current_load_not_fabricated(self, client, monkeypatch): """current_load must be absent or null — never a made-up value.""" monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") assert resp.status_code == 200 @@ -209,7 +209,7 @@ def test_ollama_slow_returns_degraded(self, client, monkeypatch): """ monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) monkeypatch.setattr(system_mod, "_SLOW_MS", -1) resp = client.get("/api/v1/health") @@ -222,7 +222,7 @@ def test_models_available_and_loaded_flag(self, client, monkeypatch): """models_available is built from /api/tags; loaded=True only for models in /api/ps.""" monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") assert resp.status_code == 200 @@ -238,7 +238,7 @@ def test_models_available_and_loaded_flag(self, client, monkeypatch): def test_model_loaded_reflects_running_model(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") assert resp.status_code == 200 @@ -248,7 +248,7 @@ def test_model_loaded_reflects_running_model(self, client, monkeypatch): def test_ollama_version_present(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") assert resp.status_code == 200 @@ -258,7 +258,7 @@ def test_ollama_version_present(self, client, monkeypatch): def test_storage_disk_free_present(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil(free_bytes=200 * 1024 ** 3)) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") assert resp.status_code == 200 @@ -269,7 +269,7 @@ def test_storage_disk_free_present(self, client, monkeypatch): def test_storage_unavailable_is_degraded(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil(raise_oserror=True)) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") assert resp.status_code == 200 @@ -280,15 +280,15 @@ def test_storage_unavailable_is_degraded(self, client, monkeypatch): def test_uptime_seconds_is_non_negative(self, client, monkeypatch): monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) - monkeypatch.setattr("app.api.v1.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) resp = client.get("/api/v1/health") assert resp.status_code == 200 assert resp.json()["uptime_seconds"] >= 0 def test_existing_routes_not_displaced(self, client): - """Sanity: adding v1_router must not break the existing /templates endpoint.""" - resp = client.get("/templates") + """Sanity: adding v1_router must not break the existing /api/v1/templates endpoint.""" + resp = client.get("/api/v1/templates") assert resp.status_code == 200 From b01e316a8cb0f44d45b8f8530ebbda6501a14233 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Wed, 24 Jun 2026 17:40:34 -0400 Subject: [PATCH 55/85] fix: :bug: missing import --- app/api/routes/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/routes/forms.py b/app/api/routes/forms.py index 3c1769b2..a0a10915 100644 --- a/app/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -14,7 +14,7 @@ from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, WHISPER_HOST, BASE_DIR, RETENTION_PERIOD_DAYS from app.core.errors.base import AppError from app.db.repositories import create_form, get_template, get_form_submission, delete_form_submission -from app.models import FormSubmission +from app.models import FormSubmission, Template from app.services.controller import Controller PROJECT_ROOT = BASE_DIR From d3d5da4278d7a4173b6d1581741a679a4ebfd4ae Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 25 Jun 2026 16:19:27 +0530 Subject: [PATCH 56/85] fix: CI CD test failure. fixes #592 --- .github/workflows/docker-build.yml | 2 +- .github/workflows/tests.yml | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 39bd9d86..25804171 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -15,4 +15,4 @@ jobs: - name: Build Docker image run: | - docker build -t fireform:test . \ No newline at end of file + docker build -t fireform:test -f docker/prod/Dockerfile . \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 932bdeba..9f0b366c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,12 +4,27 @@ on: push: branches: [main] pull_request: - branches: [main] + branches: [main,development] jobs: test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: fireform + POSTGRES_PASSWORD: fireform + POSTGRES_DB: fireform + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U fireform" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - uses: actions/checkout@v4 @@ -33,5 +48,7 @@ jobs: - name: Check for un-migrated model changes env: PYTHONPATH: . + DATABASE_URL: postgresql://fireform:fireform@localhost:5432/fireform run: | + python -m alembic upgrade head python -m alembic check \ No newline at end of file From 7e909a0e79bc4156c767e6fda8d996bc903f5239 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 26 Jun 2026 02:31:46 +0530 Subject: [PATCH 57/85] Add v1 contract DB models and migration (#544) --- alembic/env.py | 2 +- alembic/versions/002_v1_models.py | 144 +++++++++++ app/api/schemas/enums.py | 5 + app/models/__init__.py | 22 +- app/models/models.py | 132 +++++++++- tests/conftest.py | 2 +- tests/test_migrations.py | 181 +++++++++++++ tests/test_v1_models.py | 408 ++++++++++++++++++++++++++++++ 8 files changed, 891 insertions(+), 5 deletions(-) create mode 100644 alembic/versions/002_v1_models.py create mode 100644 tests/test_v1_models.py diff --git a/alembic/env.py b/alembic/env.py index 06639d87..dd2f3739 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -6,7 +6,7 @@ from sqlmodel import SQLModel from app.core.config import DATABASE_URL -from app.models import FormSubmission, Job, Template +from app.models import Extraction, Form, FormSubmission, Incident, Input, Job, Report, Template # noqa: F401 config = context.config diff --git a/alembic/versions/002_v1_models.py b/alembic/versions/002_v1_models.py new file mode 100644 index 00000000..624e5b40 --- /dev/null +++ b/alembic/versions/002_v1_models.py @@ -0,0 +1,144 @@ +"""v1 models — Input, Extraction, Incident, Form, Report tables. + +Revision ID: 002 +Revises: 001 +Create Date: 2026-06-26 + +JSON columns use sa.JSON (Postgres json type, not jsonb) for consistency with +migration 001 and SQLite test-harness compatibility. A follow-up can switch +high-query blobs (especially incident_contract) to jsonb via with_variant() if +containment operators or GIN indexing are needed. + +Form.job_id and Report.job_id are plain UUID columns with no FK constraint. +The contract Job model (UUID-PK, enum-typed) is a separate decision; the FK +will be added when that model is resolved. +""" + +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa +import sqlmodel + +revision: str = "002" +down_revision: str | None = "001" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.create_table( + "inputs", + sa.Column("input_id", sa.Uuid(), primary_key=True), + sa.Column("input_type", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("transcript", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("original_filename", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("audio_duration_seconds", sa.Float, nullable=True), + sa.Column("character_count", sa.Integer, nullable=True), + sa.Column("word_count", sa.Integer, nullable=True), + sa.Column("station_id", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("responder_badge", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("incident_date_hint", sa.Date, nullable=True), + sa.Column("error_detail", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + + op.create_table( + "extractions", + sa.Column("extract_id", sa.Uuid(), primary_key=True), + sa.Column( + "input_id", + sa.Uuid(), + sa.ForeignKey("inputs.input_id"), + nullable=False, + ), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("started_at", sa.DateTime, nullable=True), + sa.Column("completed_at", sa.DateTime, nullable=True), + sa.Column("model_used", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("processing_time_seconds", sa.Float, nullable=True), + sa.Column("incident_contract", sa.JSON, nullable=True), + sa.Column("corrections", sa.JSON, nullable=True), + sa.Column("error_type", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("error_detail", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + + op.create_table( + "incidents", + sa.Column("incident_id", sa.Uuid(), primary_key=True), + sa.Column( + "extract_id", + sa.Uuid(), + sa.ForeignKey("extractions.extract_id"), + nullable=False, + ), + sa.Column("incident_number", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("incident_name", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("incident_type", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("incident_date", sa.Date, nullable=True), + sa.Column("tags", sa.JSON, nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + sa.Column("deleted_at", sa.DateTime, nullable=True), + ) + + op.create_table( + "forms", + sa.Column("form_id", sa.Uuid(), primary_key=True), + sa.Column("form_type", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column( + "extract_id", + sa.Uuid(), + sa.ForeignKey("extractions.extract_id"), + nullable=False, + ), + sa.Column( + "incident_id", + sa.Uuid(), + sa.ForeignKey("incidents.incident_id"), + nullable=True, + ), + sa.Column("job_id", sa.Uuid(), nullable=True), # no FK — see module docstring + sa.Column("completed_at", sa.DateTime, nullable=True), + sa.Column("pdf_ready", sa.Boolean, nullable=False), + sa.Column("json_ready", sa.Boolean, nullable=False), + sa.Column("field_mapping_summary", sa.JSON, nullable=True), + sa.Column("pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("json_data", sa.JSON, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + + op.create_table( + "reports", + sa.Column("report_id", sa.Uuid(), primary_key=True), + sa.Column("period_type", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("period_label", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("year", sa.Integer, nullable=False), + sa.Column("month", sa.Integer, nullable=True), + sa.Column("quarter", sa.Integer, nullable=True), + sa.Column("output_format", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("generated_at", sa.DateTime, nullable=True), + sa.Column("summary", sa.JSON, nullable=True), + sa.Column("job_id", sa.Uuid(), nullable=True), # no FK — see module docstring + sa.Column("pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("json_data", sa.JSON, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + + +def downgrade() -> None: + op.drop_table("reports") + op.drop_table("forms") + op.drop_table("incidents") + op.drop_table("extractions") + op.drop_table("inputs") diff --git a/app/api/schemas/enums.py b/app/api/schemas/enums.py index 77c07c33..b6335389 100644 --- a/app/api/schemas/enums.py +++ b/app/api/schemas/enums.py @@ -1,6 +1,11 @@ from enum import Enum +class InputType(str, Enum): + voice = "voice" + text = "text" + + class InputStatus(str, Enum): queued = "queued" transcribing = "transcribing" diff --git a/app/models/__init__.py b/app/models/__init__.py index 9c5433b6..bba2eecb 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,5 +1,23 @@ """ORM models. Import from here: `from app.models import Template`.""" -from app.models.models import FormSubmission, Job, Template +from app.models.models import ( + Extraction, + Form, + FormSubmission, + Incident, + Input, + Job, + Report, + Template, +) -__all__ = ["Template", "FormSubmission", "Job"] +__all__ = [ + "Template", + "FormSubmission", + "Job", + "Input", + "Extraction", + "Incident", + "Form", + "Report", +] diff --git a/app/models/models.py b/app/models/models.py index fea977f1..cb9a5ff7 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,8 +1,22 @@ import uuid as uuid_mod +from uuid import UUID, uuid4 +from datetime import date, datetime, timezone from sqlalchemy import Column, JSON from sqlmodel import SQLModel, Field -from datetime import datetime, timezone +from sqlmodel.sql.sqltypes import AutoString + +from app.api.schemas.enums import ( + ExtractionStatus, + FormStatus, + FormType, + InputStatus, + InputType, + JobStatus, + OutputFormat, + PeriodType, + ReportStatus, +) class Template(SQLModel, table=True): @@ -34,4 +48,120 @@ class Job(SQLModel, table=True): error: dict | None = Field(default=None, sa_column=Column(JSON)) model: str | None = None created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +# --------------------------------------------------------------------------- +# v1 contract models +# --------------------------------------------------------------------------- + +class Input(SQLModel, table=True): + __tablename__ = "inputs" + + input_id: UUID = Field(default_factory=uuid4, primary_key=True) + # sa_column required on all str-Enum fields: without it SQLModel emits + # sa.Enum(native_enum=True) which creates a Postgres ENUM type — hard to + # migrate and inconsistent with the VARCHAR approach used in migration 001. + input_type: InputType = Field(sa_column=Column(AutoString, nullable=False)) + status: InputStatus = Field( + default=InputStatus.queued, sa_column=Column(AutoString, nullable=False) + ) + transcript: str | None = None + original_filename: str | None = None + audio_duration_seconds: float | None = None + character_count: int | None = None + word_count: int | None = None + station_id: str | None = None + responder_badge: str | None = None + incident_date_hint: date | None = None + error_detail: str | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Extraction(SQLModel, table=True): + __tablename__ = "extractions" + + extract_id: UUID = Field(default_factory=uuid4, primary_key=True) + input_id: UUID = Field(foreign_key="inputs.input_id") + status: ExtractionStatus = Field( + default=ExtractionStatus.processing, sa_column=Column(AutoString, nullable=False) + ) + started_at: datetime | None = None + completed_at: datetime | None = None + model_used: str | None = None + processing_time_seconds: float | None = None + # Full IncidentContract superset blob; stores partial result while processing, + # final canonical JSON when status=completed. + incident_contract: dict | None = Field(default=None, sa_column=Column(JSON)) + # Audit trail of manual corrections applied via PATCH /extract/{id}. + corrections: list | None = Field(default=None, sa_column=Column(JSON)) + error_type: str | None = None + error_detail: str | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Incident(SQLModel, table=True): + __tablename__ = "incidents" + + incident_id: UUID = Field(default_factory=uuid4, primary_key=True) + extract_id: UUID = Field(foreign_key="extractions.extract_id") + incident_number: str | None = None + status: ReportStatus = Field( + default=ReportStatus.draft, sa_column=Column(AutoString, nullable=False) + ) + incident_name: str | None = None + incident_type: str | None = None + incident_date: date | None = None + tags: list | None = Field(default=None, sa_column=Column(JSON)) + notes: str | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + deleted_at: datetime | None = None + + +class Form(SQLModel, table=True): + __tablename__ = "forms" + + form_id: UUID = Field(default_factory=uuid4, primary_key=True) + form_type: FormType = Field(sa_column=Column(AutoString, nullable=False)) + status: FormStatus = Field( + default=FormStatus.queued, sa_column=Column(AutoString, nullable=False) + ) + extract_id: UUID = Field(foreign_key="extractions.extract_id") + incident_id: UUID | None = Field(default=None, foreign_key="incidents.incident_id") + # Plain UUID, no FK constraint — pending contract Job model resolution (#544 decision A). + job_id: UUID | None = None + completed_at: datetime | None = None + pdf_ready: bool = Field(default=False) + json_ready: bool = Field(default=False) + field_mapping_summary: dict | None = Field(default=None, sa_column=Column(JSON)) + pdf_path: str | None = None + json_data: dict | None = Field(default=None, sa_column=Column(JSON)) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Report(SQLModel, table=True): + __tablename__ = "reports" + + report_id: UUID = Field(default_factory=uuid4, primary_key=True) + period_type: PeriodType = Field(sa_column=Column(AutoString, nullable=False)) + period_label: str | None = None + year: int + month: int | None = None + quarter: int | None = None + # Named output_format (not format) to avoid shadowing the Python builtin. + output_format: OutputFormat = Field(sa_column=Column(AutoString, nullable=False)) + status: JobStatus = Field( + default=JobStatus.queued, sa_column=Column(AutoString, nullable=False) + ) + generated_at: datetime | None = None + summary: dict | None = Field(default=None, sa_column=Column(JSON)) + # Plain UUID, no FK constraint — pending contract Job model resolution (#544 decision A). + job_id: UUID | None = None + pdf_path: str | None = None + json_data: dict | None = Field(default=None, sa_column=Column(JSON)) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 8c25516c..1f41d67e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ from app.main import app from app.api.deps import get_db from app.core.config import API_PREFIX # single source of truth for all tests -from app.models import Template, FormSubmission, Job # noqa: F401 — registers tables +from app.models import Template, FormSubmission, Job, Input, Extraction, Incident, Form, Report # noqa: F401 — registers tables # --------------------------------------------------------------------------- # In-memory database diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 87a6149c..6c404b80 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -36,6 +36,11 @@ def test_upgrade_head(alembic_cfg, alembic_engine): assert "template" in tables assert "formsubmission" in tables assert "job" in tables + assert "inputs" in tables + assert "extractions" in tables + assert "incidents" in tables + assert "forms" in tables + assert "reports" in tables assert "alembic_version" in tables @@ -126,3 +131,179 @@ def test_job_fk(alembic_cfg, alembic_engine): assert len(fks) == 1 assert fks[0]["referred_table"] == "template" assert fks[0]["referred_columns"] == ["id"] + + +# --------------------------------------------------------------------------- +# 002 — v1 model tables +# --------------------------------------------------------------------------- + +def test_inputs_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("inputs")} + assert columns == { + "input_id", + "input_type", + "status", + "transcript", + "original_filename", + "audio_duration_seconds", + "character_count", + "word_count", + "station_id", + "responder_badge", + "incident_date_hint", + "error_detail", + "created_at", + "updated_at", + } + + +def test_extractions_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("extractions")} + assert columns == { + "extract_id", + "input_id", + "status", + "started_at", + "completed_at", + "model_used", + "processing_time_seconds", + "incident_contract", + "corrections", + "error_type", + "error_detail", + "created_at", + "updated_at", + } + + +def test_extractions_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("extractions") + assert len(fks) == 1 + assert fks[0]["referred_table"] == "inputs" + assert fks[0]["referred_columns"] == ["input_id"] + + +def test_incidents_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("incidents")} + assert columns == { + "incident_id", + "extract_id", + "incident_number", + "status", + "incident_name", + "incident_type", + "incident_date", + "tags", + "notes", + "created_at", + "updated_at", + "deleted_at", + } + + +def test_incidents_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("incidents") + assert len(fks) == 1 + assert fks[0]["referred_table"] == "extractions" + assert fks[0]["referred_columns"] == ["extract_id"] + + +def test_forms_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("forms")} + assert columns == { + "form_id", + "form_type", + "status", + "extract_id", + "incident_id", + "job_id", + "completed_at", + "pdf_ready", + "json_ready", + "field_mapping_summary", + "pdf_path", + "json_data", + "created_at", + "updated_at", + } + + +def test_forms_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = {fk["referred_table"]: fk for fk in inspector.get_foreign_keys("forms")} + # extract_id → extractions and incident_id → incidents; job_id has NO FK constraint + assert "extractions" in fks + assert fks["extractions"]["referred_columns"] == ["extract_id"] + assert "incidents" in fks + assert fks["incidents"]["referred_columns"] == ["incident_id"] + assert len(fks) == 2 + + +def test_reports_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("reports")} + assert columns == { + "report_id", + "period_type", + "period_label", + "year", + "month", + "quarter", + "output_format", + "status", + "generated_at", + "summary", + "job_id", + "pdf_path", + "json_data", + "created_at", + "updated_at", + } + + +def test_reports_no_fk(alembic_cfg, alembic_engine): + """job_id on reports is a plain UUID column — no FK constraint until contract Job is resolved.""" + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("reports") + assert len(fks) == 0 + + +def test_downgrade_002(alembic_cfg, alembic_engine): + """Downgrade by one step removes only the 002 tables, leaving 001 tables intact.""" + command.upgrade(alembic_cfg, "head") + command.downgrade(alembic_cfg, "-1") + + inspector = inspect(alembic_engine) + tables = inspector.get_table_names() + assert "inputs" not in tables + assert "extractions" not in tables + assert "incidents" not in tables + assert "forms" not in tables + assert "reports" not in tables + assert "template" in tables + assert "formsubmission" in tables + assert "job" in tables diff --git a/tests/test_v1_models.py b/tests/test_v1_models.py new file mode 100644 index 00000000..6584ac36 --- /dev/null +++ b/tests/test_v1_models.py @@ -0,0 +1,408 @@ +"""Direct SQLModel instantiation tests for the five v1 models. + +Uses the same in-memory SQLite engine and _reset_tables fixture from conftest +(autouse=True) so every test starts with a clean schema. Tests stay at the ORM +layer — no routes, no Celery, no Ollama. +""" + +from datetime import date, datetime, timezone +from uuid import UUID, uuid4 + +import pytest +from sqlmodel import Session, select + +from app.api.schemas.enums import ( + ExtractionStatus, + FormStatus, + FormType, + InputStatus, + InputType, + JobStatus, + OutputFormat, + PeriodType, + ReportStatus, +) +from app.models import Extraction, Form, Incident, Input, Report + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _input(db: Session, **kwargs) -> Input: + defaults = dict(input_type=InputType.text) + row = Input(**{**defaults, **kwargs}) + db.add(row) + db.commit() + db.refresh(row) + return row + + +def _extraction(db: Session, input_id: UUID, **kwargs) -> "Extraction": + row = Extraction(input_id=input_id, **kwargs) + db.add(row) + db.commit() + db.refresh(row) + return row + + +# --------------------------------------------------------------------------- +# Input +# --------------------------------------------------------------------------- + +class TestInputModel: + + def test_defaults_on_create(self, db): + row = _input(db) + assert isinstance(row.input_id, UUID) + assert row.status == InputStatus.queued + assert row.transcript is None + assert row.audio_duration_seconds is None + assert row.character_count is None + assert isinstance(row.created_at, datetime) + assert isinstance(row.updated_at, datetime) + + def test_voice_fields_roundtrip(self, db): + row = _input( + db, + input_type=InputType.voice, + original_filename="call_recording.mp3", + audio_duration_seconds=47.3, + station_id="STA-12", + responder_badge="B-4421", + incident_date_hint=date(2026, 6, 1), + ) + fetched = db.get(Input, row.input_id) + assert fetched.input_type == InputType.voice + assert fetched.original_filename == "call_recording.mp3" + assert fetched.audio_duration_seconds == pytest.approx(47.3) + assert fetched.station_id == "STA-12" + assert fetched.incident_date_hint == date(2026, 6, 1) + + def test_status_transitions_persist(self, db): + row = _input(db) + row.status = InputStatus.transcribing + db.add(row) + db.commit() + fetched = db.get(Input, row.input_id) + assert fetched.status == InputStatus.transcribing + + def test_error_detail_on_failed(self, db): + row = _input(db, status=InputStatus.failed, error_detail="Whisper timeout") + fetched = db.get(Input, row.input_id) + assert fetched.status == InputStatus.failed + assert fetched.error_detail == "Whisper timeout" + + def test_word_and_char_counts(self, db): + row = _input( + db, + status=InputStatus.ready, + transcript="Structure fire at 4th and Main", + character_count=30, + word_count=6, + ) + fetched = db.get(Input, row.input_id) + assert fetched.character_count == 30 + assert fetched.word_count == 6 + + def test_unique_primary_keys(self, db): + a = _input(db) + b = _input(db) + assert a.input_id != b.input_id + + +# --------------------------------------------------------------------------- +# Extraction +# --------------------------------------------------------------------------- + +class TestExtractionModel: + + def test_defaults_on_create(self, db): + inp = _input(db) + ext = _extraction(db, inp.input_id) + assert isinstance(ext.extract_id, UUID) + assert ext.input_id == inp.input_id + assert ext.status == ExtractionStatus.processing + assert ext.incident_contract is None + assert ext.corrections is None + assert ext.started_at is None + + def test_incident_contract_json_roundtrip(self, db): + inp = _input(db) + contract = { + "schema_version": "1.1.0", + "incident": {"name": "Structure Fire Main St", "types": []}, + "location": {"address": "123 Main St", "state": "CA"}, + } + ext = _extraction(db, inp.input_id, incident_contract=contract) + fetched = db.get(Extraction, ext.extract_id) + assert fetched.incident_contract["schema_version"] == "1.1.0" + assert fetched.incident_contract["location"]["state"] == "CA" + + def test_corrections_json_roundtrip(self, db): + inp = _input(db) + corrections = [ + { + "field_path": "incident.name", + "original_value": "Wildland Fire", + "corrected_value": "Structure Fire", + "corrected_at": "2026-06-26T10:00:00Z", + "corrected_by": "dispatcher_01", + } + ] + ext = _extraction(db, inp.input_id, corrections=corrections) + fetched = db.get(Extraction, ext.extract_id) + assert len(fetched.corrections) == 1 + assert fetched.corrections[0]["field_path"] == "incident.name" + + def test_completed_status_fields(self, db): + inp = _input(db) + now = datetime.now(timezone.utc) + ext = _extraction( + db, + inp.input_id, + status=ExtractionStatus.completed, + started_at=now, + completed_at=now, + model_used="llama3:8b", + processing_time_seconds=38.4, + ) + fetched = db.get(Extraction, ext.extract_id) + assert fetched.status == ExtractionStatus.completed + assert fetched.model_used == "llama3:8b" + assert fetched.processing_time_seconds == pytest.approx(38.4) + + def test_failed_status_with_error_fields(self, db): + inp = _input(db) + ext = _extraction( + db, + inp.input_id, + status=ExtractionStatus.failed, + error_type="LLM_TIMEOUT", + error_detail="Ollama did not respond within 120s", + ) + fetched = db.get(Extraction, ext.extract_id) + assert fetched.status == ExtractionStatus.failed + assert fetched.error_type == "LLM_TIMEOUT" + + +# --------------------------------------------------------------------------- +# Incident +# --------------------------------------------------------------------------- + +class TestIncidentModel: + + def _make(self, db, **kwargs): + inp = _input(db) + ext = _extraction(db, inp.input_id) + defaults = dict(extract_id=ext.extract_id) + row = Incident(**{**defaults, **kwargs}) + db.add(row) + db.commit() + db.refresh(row) + return row + + def test_defaults_on_create(self, db): + row = self._make(db) + assert isinstance(row.incident_id, UUID) + assert row.status == ReportStatus.draft + assert row.deleted_at is None + assert row.tags is None + + def test_tags_json_roundtrip(self, db): + row = self._make(db, tags=["wildland", "high-priority", "multi-agency"]) + fetched = db.get(Incident, row.incident_id) + assert fetched.tags == ["wildland", "high-priority", "multi-agency"] + + def test_soft_delete(self, db): + row = self._make(db) + assert row.deleted_at is None + ts = datetime.now(timezone.utc) + row.deleted_at = ts + db.add(row) + db.commit() + fetched = db.get(Incident, row.incident_id) + assert fetched.deleted_at is not None + + def test_incident_date_field(self, db): + row = self._make(db, incident_date=date(2026, 5, 15)) + fetched = db.get(Incident, row.incident_id) + assert fetched.incident_date == date(2026, 5, 15) + + def test_all_nullable_fields_default_none(self, db): + row = self._make(db) + assert row.incident_number is None + assert row.incident_name is None + assert row.incident_type is None + assert row.incident_date is None + assert row.notes is None + + +# --------------------------------------------------------------------------- +# Form +# --------------------------------------------------------------------------- + +class TestFormModel: + + def _make(self, db, **kwargs): + inp = _input(db) + ext = _extraction(db, inp.input_id) + defaults = dict( + extract_id=ext.extract_id, + form_type=FormType.nfirs_basic, + ) + row = Form(**{**defaults, **kwargs}) + db.add(row) + db.commit() + db.refresh(row) + return row + + def test_defaults_on_create(self, db): + row = self._make(db) + assert isinstance(row.form_id, UUID) + assert row.status == FormStatus.queued + assert row.pdf_ready is False + assert row.json_ready is False + assert row.incident_id is None + assert row.job_id is None + assert row.completed_at is None + + def test_field_mapping_summary_roundtrip(self, db): + summary = { + "total_form_fields": 42, + "fields_filled": 38, + "fields_blank": 4, + "coverage_percent": 90.5, + } + row = self._make(db, field_mapping_summary=summary) + fetched = db.get(Form, row.form_id) + assert fetched.field_mapping_summary["total_form_fields"] == 42 + assert fetched.field_mapping_summary["coverage_percent"] == pytest.approx(90.5) + + def test_json_data_roundtrip(self, db): + agency_json = {"FDID": "CA99901", "INC_NO": "2026-0042", "ALARMS": 2} + row = self._make(db, json_data=agency_json, json_ready=True) + fetched = db.get(Form, row.form_id) + assert fetched.json_ready is True + assert fetched.json_data["FDID"] == "CA99901" + + def test_incident_fk_links_correctly(self, db): + inp = _input(db) + ext = _extraction(db, inp.input_id) + incident = Incident(extract_id=ext.extract_id) + db.add(incident) + db.commit() + db.refresh(incident) + + form = Form( + extract_id=ext.extract_id, + form_type=FormType.neris, + incident_id=incident.incident_id, + ) + db.add(form) + db.commit() + db.refresh(form) + assert form.incident_id == incident.incident_id + + def test_job_id_stored_without_fk(self, db): + """job_id is a plain UUID — can store any UUID without a FK constraint.""" + arbitrary_uuid = uuid4() + row = self._make(db, job_id=arbitrary_uuid) + fetched = db.get(Form, row.form_id) + assert fetched.job_id == arbitrary_uuid + + def test_all_form_types_accepted(self, db): + inp = _input(db) + ext = _extraction(db, inp.input_id) + for ft in FormType: + form = Form(extract_id=ext.extract_id, form_type=ft) + db.add(form) + db.commit() + results = db.exec(select(Form)).all() + stored_types = {f.form_type for f in results} + assert stored_types == set(FormType) + + +# --------------------------------------------------------------------------- +# Report +# --------------------------------------------------------------------------- + +class TestReportModel: + + def _make(self, db, **kwargs): + defaults = dict( + period_type=PeriodType.monthly, + year=2026, + month=6, + output_format=OutputFormat.pdf, + ) + row = Report(**{**defaults, **kwargs}) + db.add(row) + db.commit() + db.refresh(row) + return row + + def test_defaults_on_create(self, db): + row = self._make(db) + assert isinstance(row.report_id, UUID) + assert row.status == JobStatus.queued + assert row.generated_at is None + assert row.summary is None + assert row.pdf_path is None + assert row.json_data is None + assert row.job_id is None + + def test_summary_json_roundtrip(self, db): + summary = { + "total_incidents": 23, + "by_type": {"fire": 10, "ems": 13}, + "forms_generated": 71, + "avg_completeness_score": 88.3, + } + row = self._make(db, summary=summary) + fetched = db.get(Report, row.report_id) + assert fetched.summary["total_incidents"] == 23 + assert fetched.summary["avg_completeness_score"] == pytest.approx(88.3) + + def test_quarterly_report(self, db): + row = self._make( + db, + period_type=PeriodType.quarterly, + year=2026, + month=None, + quarter=2, + output_format=OutputFormat.json, + ) + fetched = db.get(Report, row.report_id) + assert fetched.period_type == PeriodType.quarterly + assert fetched.quarter == 2 + assert fetched.month is None + + def test_annual_report(self, db): + row = self._make( + db, + period_type=PeriodType.annual, + year=2025, + month=None, + quarter=None, + output_format=OutputFormat.both, + ) + fetched = db.get(Report, row.report_id) + assert fetched.period_type == PeriodType.annual + assert fetched.year == 2025 + assert fetched.output_format == OutputFormat.both + + def test_job_id_stored_without_fk(self, db): + arbitrary_uuid = uuid4() + row = self._make(db, job_id=arbitrary_uuid) + fetched = db.get(Report, row.report_id) + assert fetched.job_id == arbitrary_uuid + + def test_status_progression(self, db): + row = self._make(db) + assert row.status == JobStatus.queued + row.status = JobStatus.processing + db.add(row) + db.commit() + fetched = db.get(Report, row.report_id) + assert fetched.status == JobStatus.processing From dbcbe958cc37916f046eb62e511dd7c896da71f7 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 26 Jun 2026 12:23:42 +0530 Subject: [PATCH 58/85] Add text input layer: POST /input/text and GET /input/{input_id} (#546) --- app/api/router.py | 6 +- app/api/routes/input.py | 105 +++++++++++++++++++++++++ app/api/schemas/input.py | 45 +++++++++++ app/core/config.py | 4 + tests/test_v1_input.py | 165 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 app/api/routes/input.py create mode 100644 app/api/schemas/input.py create mode 100644 tests/test_v1_input.py diff --git a/app/api/router.py b/app/api/router.py index 962a26ca..b9dd62f9 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -1,6 +1,7 @@ from fastapi import APIRouter -from app.api.routes import forms, templates, weather, zipcode, jobs, system +from app.api.routes import forms, jobs, system, templates, weather, zipcode +from app.api.routes import input as input_routes from app.core.config import API_PREFIX api_router = APIRouter() @@ -9,4 +10,5 @@ api_router.include_router(system.router, prefix=API_PREFIX) api_router.include_router(jobs.router, prefix=API_PREFIX) api_router.include_router(weather.router, prefix=API_PREFIX) -api_router.include_router(zipcode.router, prefix=API_PREFIX) \ No newline at end of file +api_router.include_router(zipcode.router, prefix=API_PREFIX) +api_router.include_router(input_routes.router, prefix=API_PREFIX) \ No newline at end of file diff --git a/app/api/routes/input.py b/app/api/routes/input.py new file mode 100644 index 00000000..108121b8 --- /dev/null +++ b/app/api/routes/input.py @@ -0,0 +1,105 @@ +from datetime import datetime, timezone +from uuid import UUID + +from fastapi import APIRouter, Depends +from fastapi.responses import JSONResponse +from sqlmodel import Session, select + +from app.api.deps import get_db +from app.api.schemas.enums import InputStatus, InputType +from app.api.schemas.input import InputRecordResponse, TextInputRequest, TextInputResponse +from app.core.config import INPUT_POLL_INTERVAL_SECONDS +from app.core.errors.base import AppError +from app.models import Input + +router = APIRouter(prefix="/input", tags=["input"]) + + +@router.post("/text", response_model=TextInputResponse, status_code=201) +def submit_text_input(body: TextInputRequest, db: Session = Depends(get_db)): + narrative = body.narrative + + if len(narrative) > 50_000: + raise AppError( + "Narrative exceeds maximum length of 50,000 characters", + status_code=413, + error_code="NARRATIVE_TOO_LONG", + detail={"max_characters": 50_000, "received_characters": len(narrative)}, + ) + + words = narrative.split() + if len(words) < 10: + return JSONResponse( + status_code=422, + content={ + "error_code": "VALIDATION_ERROR", + "message": "Request validation failed", + "validation_errors": [ + { + "field": "narrative", + "issue": "Must contain at least 10 words", + "value": narrative, + } + ], + }, + ) + + now = datetime.now(timezone.utc) + record = Input( + input_type=InputType.text, + status=InputStatus.ready, + transcript=narrative, + character_count=len(narrative), + word_count=len(words), + station_id=body.station_id, + responder_badge=body.responder_badge, + incident_date_hint=body.incident_date_hint, + created_at=now, + updated_at=now, + ) + db.add(record) + db.commit() + db.refresh(record) + + return TextInputResponse( + input_id=record.input_id, + status=record.status, + input_type=record.input_type, + character_count=record.character_count, + word_count=record.word_count, + created_at=record.created_at, + ) + + +@router.get("/{input_id}", response_model=InputRecordResponse) +def get_input(input_id: UUID, db: Session = Depends(get_db)): + record = db.exec(select(Input).where(Input.input_id == input_id)).first() + if record is None: + raise AppError( + f"Input with ID {input_id} not found", + status_code=404, + error_code="INPUT_NOT_FOUND", + ) + + retry_after = ( + INPUT_POLL_INTERVAL_SECONDS + if record.status in (InputStatus.queued, InputStatus.transcribing) + else None + ) + return InputRecordResponse( + input_id=record.input_id, + input_type=record.input_type, + status=record.status, + transcript=record.transcript, + original_filename=record.original_filename, + audio_duration_seconds=record.audio_duration_seconds, + character_count=record.character_count, + word_count=record.word_count, + station_id=record.station_id, + responder_badge=record.responder_badge, + incident_date_hint=record.incident_date_hint, + error_detail=record.error_detail, + retry_after_seconds=retry_after, + created_at=record.created_at, + updated_at=record.updated_at, + ) diff --git a/app/api/schemas/input.py b/app/api/schemas/input.py new file mode 100644 index 00000000..ab705b55 --- /dev/null +++ b/app/api/schemas/input.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from datetime import date, datetime +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + +from app.api.schemas.enums import InputStatus, InputType + + +class TextInputRequest(BaseModel): + model_config = ConfigDict() + narrative: str = Field(min_length=20) + station_id: str | None = None + responder_badge: str | None = None + incident_date_hint: date | None = None + + +class TextInputResponse(BaseModel): + model_config = ConfigDict() + input_id: UUID + status: InputStatus + input_type: InputType + character_count: int | None = None + word_count: int | None = None + created_at: datetime | None = None + + +class InputRecordResponse(BaseModel): + model_config = ConfigDict() + input_id: UUID + input_type: InputType + status: InputStatus + transcript: str | None = None + original_filename: str | None = None + audio_duration_seconds: float | None = None + character_count: int | None = None + word_count: int | None = None + station_id: str | None = None + responder_badge: str | None = None + incident_date_hint: date | None = None + error_detail: str | None = None + retry_after_seconds: int | None = None + created_at: datetime | None = None + updated_at: datetime | None = None diff --git a/app/core/config.py b/app/core/config.py index 38db4813..e0f41f88 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -54,6 +54,10 @@ # hint, not a measured backpressure value — the app has no queue-depth signal. RETRY_AFTER_SECONDS = 30 +# Polling hint returned by GET /input/{id} when a voice input is still queued +# or transcribing. Value matches the contract example (contracts/path/input.yaml). +INPUT_POLL_INTERVAL_SECONDS = 5 + # --- API Versioning ------------------------------------------------------- API_PREFIX = "/api/v1" diff --git a/tests/test_v1_input.py b/tests/test_v1_input.py new file mode 100644 index 00000000..1a00b1c5 --- /dev/null +++ b/tests/test_v1_input.py @@ -0,0 +1,165 @@ +"""Tests for POST /api/v1/input/text and GET /api/v1/input/{input_id}. + +Uses the shared in-memory SQLite engine from conftest.py. No Ollama or +Whisper involvement — text input is fully synchronous. +""" + +import pytest + +TEXT_URL = "/api/v1/input/text" +INPUT_URL = "/api/v1/input" + +# Narrative that passes all validation: > 20 chars, >= 10 words, << 50,000 chars. +VALID_NARRATIVE = ( + "Responded to a wildfire at Bear Creek Trailhead around 1:45 PM on July 10th. " + "Lightning strike ignited timber litter in mixed conifer and chaparral." +) + + +# --------------------------------------------------------------------------- +# POST /api/v1/input/text +# --------------------------------------------------------------------------- + +class TestSubmitTextInput: + + def test_201_returns_required_fields(self, client): + resp = client.post(TEXT_URL, json={"narrative": VALID_NARRATIVE}) + assert resp.status_code == 201 + body = resp.json() + assert body["status"] == "ready" + assert body["input_type"] == "text" + assert "input_id" in body + assert "created_at" in body + + def test_201_character_and_word_counts_match_narrative(self, client): + resp = client.post(TEXT_URL, json={"narrative": VALID_NARRATIVE}) + assert resp.status_code == 201 + body = resp.json() + assert body["character_count"] == len(VALID_NARRATIVE) + assert body["word_count"] == len(VALID_NARRATIVE.split()) + + def test_201_optional_fields_accepted_and_visible_via_get(self, client): + resp = client.post(TEXT_URL, json={ + "narrative": VALID_NARRATIVE, + "station_id": "STA-045", + "responder_badge": "FD-7842", + "incident_date_hint": "2024-07-10", + }) + assert resp.status_code == 201 + input_id = resp.json()["input_id"] + + get_resp = client.get(f"{INPUT_URL}/{input_id}") + assert get_resp.status_code == 200 + body = get_resp.json() + assert body["station_id"] == "STA-045" + assert body["responder_badge"] == "FD-7842" + assert body["incident_date_hint"] == "2024-07-10" + + def test_narrative_stored_in_transcript_field(self, client): + resp = client.post(TEXT_URL, json={"narrative": VALID_NARRATIVE}) + input_id = resp.json()["input_id"] + get_resp = client.get(f"{INPUT_URL}/{input_id}") + assert get_resp.json()["transcript"] == VALID_NARRATIVE + + def test_413_narrative_over_50000_chars(self, client): + too_long = "x" * 50_001 + resp = client.post(TEXT_URL, json={"narrative": too_long}) + assert resp.status_code == 413 + body = resp.json() + assert body["error_code"] == "NARRATIVE_TOO_LONG" + assert body["detail"]["max_characters"] == 50_000 + assert body["detail"]["received_characters"] == 50_001 + + def test_413_boundary_exactly_50000_chars_is_accepted(self, client): + # Exactly at the limit: should succeed (> 50_000 is rejected, not >=). + # Build a narrative at exactly 50,000 chars that has >= 10 words. + phrase = "fire incident response " # 23 chars + narrative = (phrase * 2175)[:50_000] # 2175*23=50025, sliced to 50000 + resp = client.post(TEXT_URL, json={"narrative": narrative}) + assert resp.status_code == 201 + + def test_422_fewer_than_10_words_matches_request_validation_shape(self, client): + # >= 20 chars so Pydantic minLength passes, but only 6 words. + short = "Fire happened at the scene today here" # 7 words, > 20 chars + resp = client.post(TEXT_URL, json={"narrative": short}) + assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + assert body["message"] == "Request validation failed" + errs = body["validation_errors"] + assert len(errs) == 1 + assert errs[0]["field"] == "narrative" + assert errs[0]["issue"] == "Must contain at least 10 words" + assert errs[0]["value"] == short + + def test_422_narrative_under_20_chars_triggers_pydantic_validation(self, client): + resp = client.post(TEXT_URL, json={"narrative": "Too short"}) + assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + assert "validation_errors" in body + + def test_422_missing_narrative_field(self, client): + resp = client.post(TEXT_URL, json={}) + assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + + def test_422_empty_string_narrative(self, client): + resp = client.post(TEXT_URL, json={"narrative": ""}) + assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + + +# --------------------------------------------------------------------------- +# GET /api/v1/input/{input_id} +# --------------------------------------------------------------------------- + +class TestGetInput: + + def _create(self, client, **kwargs): + payload = {"narrative": VALID_NARRATIVE, **kwargs} + resp = client.post(TEXT_URL, json=payload) + assert resp.status_code == 201 + return resp.json()["input_id"] + + def test_200_returns_full_record(self, client): + input_id = self._create(client) + resp = client.get(f"{INPUT_URL}/{input_id}") + assert resp.status_code == 200 + body = resp.json() + assert body["input_id"] == input_id + assert body["input_type"] == "text" + assert body["status"] == "ready" + assert body["transcript"] == VALID_NARRATIVE + assert body["character_count"] == len(VALID_NARRATIVE) + assert body["word_count"] == len(VALID_NARRATIVE.split()) + assert body["created_at"] is not None + assert body["updated_at"] is not None + + def test_200_voice_only_fields_are_null_for_text_input(self, client): + input_id = self._create(client) + body = client.get(f"{INPUT_URL}/{input_id}").json() + assert body["original_filename"] is None + assert body["audio_duration_seconds"] is None + assert body["error_detail"] is None + + def test_200_retry_after_is_null_for_ready_input(self, client): + input_id = self._create(client) + body = client.get(f"{INPUT_URL}/{input_id}").json() + assert body.get("retry_after_seconds") is None + + def test_404_unknown_uuid_returns_app_error_envelope(self, client): + fake_id = "00000000-0000-0000-0000-000000000000" + resp = client.get(f"{INPUT_URL}/{fake_id}") + assert resp.status_code == 404 + body = resp.json() + assert body["error_code"] == "INPUT_NOT_FOUND" + assert fake_id in body["message"] + + def test_404_message_contains_the_requested_id(self, client): + target = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + resp = client.get(f"{INPUT_URL}/{target}") + assert resp.status_code == 404 + assert target in resp.json()["message"] From 3a79a0d6618ed9bafe91b5bd3b7a91561dfecd76 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sat, 27 Jun 2026 01:01:22 +0530 Subject: [PATCH 59/85] Refactor text input: service layer, repository, reuse validation handler --- app/api/router.py | 5 ++- app/api/routes/input.py | 68 ++++++++++++++++------------------------ app/api/schemas/input.py | 5 +-- app/db/repositories.py | 17 +++++++++- app/services/input.py | 30 ++++++++++++++++++ 5 files changed, 76 insertions(+), 49 deletions(-) create mode 100644 app/services/input.py diff --git a/app/api/router.py b/app/api/router.py index b9dd62f9..cc59847a 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -1,7 +1,6 @@ from fastapi import APIRouter -from app.api.routes import forms, jobs, system, templates, weather, zipcode -from app.api.routes import input as input_routes +from app.api.routes import forms, input, jobs, system, templates, weather, zipcode from app.core.config import API_PREFIX api_router = APIRouter() @@ -11,4 +10,4 @@ api_router.include_router(jobs.router, prefix=API_PREFIX) api_router.include_router(weather.router, prefix=API_PREFIX) api_router.include_router(zipcode.router, prefix=API_PREFIX) -api_router.include_router(input_routes.router, prefix=API_PREFIX) \ No newline at end of file +api_router.include_router(input.router, prefix=API_PREFIX) \ No newline at end of file diff --git a/app/api/routes/input.py b/app/api/routes/input.py index 108121b8..009bdf0e 100644 --- a/app/api/routes/input.py +++ b/app/api/routes/input.py @@ -1,65 +1,51 @@ -from datetime import datetime, timezone from uuid import UUID from fastapi import APIRouter, Depends -from fastapi.responses import JSONResponse -from sqlmodel import Session, select +from fastapi.exceptions import RequestValidationError +from sqlmodel import Session from app.api.deps import get_db -from app.api.schemas.enums import InputStatus, InputType +from app.api.schemas.enums import InputStatus from app.api.schemas.input import InputRecordResponse, TextInputRequest, TextInputResponse from app.core.config import INPUT_POLL_INTERVAL_SECONDS from app.core.errors.base import AppError -from app.models import Input +from app.db.repositories import create_input, get_input as repo_get_input +from app.services.input import InputService router = APIRouter(prefix="/input", tags=["input"]) @router.post("/text", response_model=TextInputResponse, status_code=201) def submit_text_input(body: TextInputRequest, db: Session = Depends(get_db)): - narrative = body.narrative - - if len(narrative) > 50_000: + if len(body.narrative) > 50_000: raise AppError( "Narrative exceeds maximum length of 50,000 characters", status_code=413, error_code="NARRATIVE_TOO_LONG", - detail={"max_characters": 50_000, "received_characters": len(narrative)}, + detail={"max_characters": 50_000, "received_characters": len(body.narrative)}, ) - words = narrative.split() - if len(words) < 10: - return JSONResponse( - status_code=422, - content={ - "error_code": "VALIDATION_ERROR", - "message": "Request validation failed", - "validation_errors": [ - { - "field": "narrative", - "issue": "Must contain at least 10 words", - "value": narrative, - } - ], - }, + svc = InputService() + try: + record = svc.build_text_input( + narrative=body.narrative, + station_id=body.station_id, + responder_badge=body.responder_badge, + incident_date_hint=body.incident_date_hint, + ) + except ValueError as exc: + raise RequestValidationError( + errors=[ + { + "loc": ("body", "narrative"), + "msg": str(exc), + "input": body.narrative, + "type": "value_error", + } + ] ) - now = datetime.now(timezone.utc) - record = Input( - input_type=InputType.text, - status=InputStatus.ready, - transcript=narrative, - character_count=len(narrative), - word_count=len(words), - station_id=body.station_id, - responder_badge=body.responder_badge, - incident_date_hint=body.incident_date_hint, - created_at=now, - updated_at=now, - ) - db.add(record) - db.commit() - db.refresh(record) + record = create_input(db, record) return TextInputResponse( input_id=record.input_id, @@ -73,7 +59,7 @@ def submit_text_input(body: TextInputRequest, db: Session = Depends(get_db)): @router.get("/{input_id}", response_model=InputRecordResponse) def get_input(input_id: UUID, db: Session = Depends(get_db)): - record = db.exec(select(Input).where(Input.input_id == input_id)).first() + record = repo_get_input(db, input_id) if record is None: raise AppError( f"Input with ID {input_id} not found", diff --git a/app/api/schemas/input.py b/app/api/schemas/input.py index ab705b55..cc37daf0 100644 --- a/app/api/schemas/input.py +++ b/app/api/schemas/input.py @@ -3,13 +3,12 @@ from datetime import date, datetime from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, Field from app.api.schemas.enums import InputStatus, InputType class TextInputRequest(BaseModel): - model_config = ConfigDict() narrative: str = Field(min_length=20) station_id: str | None = None responder_badge: str | None = None @@ -17,7 +16,6 @@ class TextInputRequest(BaseModel): class TextInputResponse(BaseModel): - model_config = ConfigDict() input_id: UUID status: InputStatus input_type: InputType @@ -27,7 +25,6 @@ class TextInputResponse(BaseModel): class InputRecordResponse(BaseModel): - model_config = ConfigDict() input_id: UUID input_type: InputType status: InputStatus diff --git a/app/db/repositories.py b/app/db/repositories.py index d7445ff6..9186d617 100644 --- a/app/db/repositories.py +++ b/app/db/repositories.py @@ -1,5 +1,8 @@ +from uuid import UUID + from sqlmodel import Session, select -from app.models import Template, FormSubmission, Job + +from app.models import Template, FormSubmission, Job, Input # Templates def create_template(session: Session, template: Template) -> Template: @@ -66,3 +69,15 @@ def delete_form_submission(session: Session, submission: FormSubmission) -> None session.delete(submission) session.commit() + +# Inputs +def create_input(session: Session, input_obj: Input) -> Input: + session.add(input_obj) + session.commit() + session.refresh(input_obj) + return input_obj + + +def get_input(session: Session, input_id: UUID) -> Input | None: + return session.get(Input, input_id) + diff --git a/app/services/input.py b/app/services/input.py new file mode 100644 index 00000000..b04868ca --- /dev/null +++ b/app/services/input.py @@ -0,0 +1,30 @@ +from datetime import date, datetime, timezone + +from app.api.schemas.enums import InputStatus, InputType +from app.models import Input + + +class InputService: + def build_text_input( + self, + narrative: str, + station_id: str | None = None, + responder_badge: str | None = None, + incident_date_hint: date | None = None, + ) -> Input: + words = narrative.split() + if len(words) < 10: + raise ValueError("Must contain at least 10 words") + now = datetime.now(timezone.utc) + return Input( + input_type=InputType.text, + status=InputStatus.ready, + transcript=narrative, + character_count=len(narrative), + word_count=len(words), + station_id=station_id, + responder_badge=responder_badge, + incident_date_hint=incident_date_hint, + created_at=now, + updated_at=now, + ) From 84f8ae8385405f48c729626de62a5f9c0107000d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sun, 28 Jun 2026 11:28:20 +0530 Subject: [PATCH 60/85] Add voice input + async transcription(#547) --- app/api/routes/forms.py | 35 ++--- app/api/routes/input.py | 93 ++++++++++++- app/api/schemas/input.py | 10 ++ app/core/celery.py | 2 +- app/core/config.py | 9 +- app/db/repositories.py | 7 + app/services/input.py | 19 +++ app/services/whisper.py | 39 ++++++ app/tasks/transcribe.py | 109 ++++++++++++++++ tests/conftest.py | 6 + tests/test_v1_voice.py | 275 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 572 insertions(+), 32 deletions(-) create mode 100644 app/services/whisper.py create mode 100644 app/tasks/transcribe.py create mode 100644 tests/test_v1_voice.py diff --git a/app/api/routes/forms.py b/app/api/routes/forms.py index 87e4a165..879fa393 100644 --- a/app/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -11,7 +11,8 @@ ModelsResponse, TranscriptionResponse, ) -from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, WHISPER_HOST, BASE_DIR, RETENTION_PERIOD_DAYS +from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, BASE_DIR, RETENTION_PERIOD_DAYS +from app.services.whisper import call_whisper_asr from app.core.errors.base import AppError from app.db.repositories import create_form, get_template, get_form_submission, delete_form_submission from app.models import FormSubmission @@ -94,34 +95,16 @@ def transcribe(audio: UploadFile = File(...)): audio is streamed straight through to the local STT service and never persisted — no PII leaves the machine. """ - whisper_url = f"{WHISPER_HOST}/asr" - - files = { - "audio_file": ( - audio.filename or "audio.wav", + try: + text = call_whisper_asr( audio.file.read(), + audio.filename or "audio.wav", audio.content_type or "audio/wav", ) - } - params = {"task": "transcribe", "output": "json", "encode": "true"} - - try: - response = requests.post(whisper_url, params=params, files=files, timeout=120) - response.raise_for_status() - except requests.exceptions.ConnectionError: - raise AppError( - f"Could not connect to the speech-to-text service at {whisper_url}. " - "Please ensure the whisper service is running.", - status_code=503, - error_code="STT_UNAVAILABLE", - ) - except requests.exceptions.RequestException as e: - raise AppError(f"Transcription failed: {e}", status_code=502, error_code="TRANSCRIPTION_FAILED") - - try: - text = (response.json().get("text") or "").strip() - except ValueError: - text = response.text.strip() + except ConnectionError as exc: + raise AppError(str(exc), status_code=503, error_code="STT_UNAVAILABLE") + except RuntimeError as exc: + raise AppError(str(exc), status_code=502, error_code="TRANSCRIPTION_FAILED") return TranscriptionResponse(text=text) diff --git a/app/api/routes/input.py b/app/api/routes/input.py index 009bdf0e..b677a34f 100644 --- a/app/api/routes/input.py +++ b/app/api/routes/input.py @@ -1,19 +1,104 @@ +from datetime import date +from pathlib import Path from uuid import UUID -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, File, Form, UploadFile from fastapi.exceptions import RequestValidationError from sqlmodel import Session from app.api.deps import get_db from app.api.schemas.enums import InputStatus -from app.api.schemas.input import InputRecordResponse, TextInputRequest, TextInputResponse -from app.core.config import INPUT_POLL_INTERVAL_SECONDS +from app.api.schemas.input import ( + InputRecordResponse, + TextInputRequest, + TextInputResponse, + VoiceInputResponse, +) +from app.core.config import AUDIO_DIR, ESTIMATED_TRANSCRIPTION_SECONDS, INPUT_POLL_INTERVAL_SECONDS from app.core.errors.base import AppError -from app.db.repositories import create_input, get_input as repo_get_input +from app.db.repositories import create_input, create_job, get_input as repo_get_input, update_job +from app.models import Job from app.services.input import InputService +from app.services.whisper import check_whisper_available +from app.tasks.transcribe import transcribe_audio_task router = APIRouter(prefix="/input", tags=["input"]) +_ALLOWED_AUDIO_EXTS = {"wav", "mp3", "m4a", "ogg", "webm"} +_MAX_AUDIO_BYTES = 500 * 1024 * 1024 # 500 MB + + +@router.post("/voice", response_model=VoiceInputResponse, status_code=201) +def submit_voice_input( + audio_file: UploadFile = File(...), + station_id: str | None = Form(default=None), + responder_badge: str | None = Form(default=None), + incident_date_hint: date | None = Form(default=None), + db: Session = Depends(get_db), +): + # validate format + filename = audio_file.filename or "" + ext = Path(filename).suffix.lstrip(".").lower() + if not ext or ext not in _ALLOWED_AUDIO_EXTS: + raise AppError( + "Audio format not supported. Accepted formats: wav, mp3, m4a, ogg, webm", + status_code=415, + error_code="UNSUPPORTED_FORMAT", + detail={"accepted_formats": ["wav", "mp3", "m4a", "ogg", "webm"]}, + ) + + # validate size + content = audio_file.file.read() + if len(content) > _MAX_AUDIO_BYTES: + raise AppError( + "Audio file exceeds maximum size of 500MB", + status_code=413, + error_code="FILE_TOO_LARGE", + detail={"max_size_bytes": _MAX_AUDIO_BYTES, "received_size_bytes": len(content)}, + ) + + # check Whisper availability before queuing + if not check_whisper_available(): + raise AppError( + "Whisper transcription service is not available", + status_code=503, + error_code="LLM_UNAVAILABLE", + ) + + # build and persist the Input record + svc = InputService() + record = svc.build_voice_input( + original_filename=filename, + station_id=station_id, + responder_badge=responder_badge, + incident_date_hint=incident_date_hint, + ) + record = create_input(db, record) + + # save audio to DATA_DIR/audio/{input_id}.{ext} + AUDIO_DIR.mkdir(parents=True, exist_ok=True) + audio_path = AUDIO_DIR / f"{record.input_id}.{ext}" + audio_path.write_bytes(content) + + # create Job before dispatch to avoid race + job = Job(celery_task_id="", job_type="transcription", status="queued") + job = create_job(db, job) + + # dispatch and backfill celery_task_id + result = transcribe_audio_task.delay(str(record.input_id), str(audio_path), job.job_id) + job.celery_task_id = result.id + job = update_job(db, job) + + return VoiceInputResponse( + input_id=record.input_id, + status=record.status, + input_type=record.input_type, + job_id=job.job_id, + poll_url=f"/api/v1/input/{record.input_id}", + estimated_processing_seconds=ESTIMATED_TRANSCRIPTION_SECONDS, + created_at=record.created_at, + ) + @router.post("/text", response_model=TextInputResponse, status_code=201) def submit_text_input(body: TextInputRequest, db: Session = Depends(get_db)): diff --git a/app/api/schemas/input.py b/app/api/schemas/input.py index cc37daf0..9d8b908c 100644 --- a/app/api/schemas/input.py +++ b/app/api/schemas/input.py @@ -8,6 +8,16 @@ from app.api.schemas.enums import InputStatus, InputType +class VoiceInputResponse(BaseModel): + input_id: UUID + status: InputStatus + input_type: InputType + estimated_processing_seconds: int | None = None + created_at: datetime | None = None + job_id: str | None = None + poll_url: str | None = None + + class TextInputRequest(BaseModel): narrative: str = Field(min_length=20) station_id: str | None = None diff --git a/app/core/celery.py b/app/core/celery.py index f7637025..a91146f7 100644 --- a/app/core/celery.py +++ b/app/core/celery.py @@ -16,7 +16,7 @@ result_expires=86400, ) -celery_app.conf.include = ["app.tasks.fill", "app.tasks.purge"] +celery_app.conf.include = ["app.tasks.fill", "app.tasks.purge", "app.tasks.transcribe"] # Optional Celery Beat schedule — runs purge_old_submissions once a day. # Enable by running: celery -A app.core.celery beat diff --git a/app/core/config.py b/app/core/config.py index e0f41f88..2a11b566 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -65,4 +65,11 @@ FIREFORM_API_KEY = os.getenv("FIREFORM_API_KEY", "") # --- Data Retention -------------------------------------------------------- -RETENTION_PERIOD_DAYS = int(os.getenv("RETENTION_PERIOD_DAYS", "30")) \ No newline at end of file +RETENTION_PERIOD_DAYS = int(os.getenv("RETENTION_PERIOD_DAYS", "30")) + +# --- Audio storage -------------------------------------------------------- +# Voice input audio files land here: {AUDIO_DIR}/{input_id}.{ext} +AUDIO_DIR = DATA_DIR / "audio" + +# Advisory estimate returned in VoiceInputResponse.estimated_processing_seconds. +ESTIMATED_TRANSCRIPTION_SECONDS = int(os.getenv("ESTIMATED_TRANSCRIPTION_SECONDS", "30")) \ No newline at end of file diff --git a/app/db/repositories.py b/app/db/repositories.py index 9186d617..0935c133 100644 --- a/app/db/repositories.py +++ b/app/db/repositories.py @@ -81,3 +81,10 @@ def create_input(session: Session, input_obj: Input) -> Input: def get_input(session: Session, input_id: UUID) -> Input | None: return session.get(Input, input_id) + +def update_input(session: Session, input_obj: Input) -> Input: + session.add(input_obj) + session.commit() + session.refresh(input_obj) + return input_obj + diff --git a/app/services/input.py b/app/services/input.py index b04868ca..a6fa229b 100644 --- a/app/services/input.py +++ b/app/services/input.py @@ -5,6 +5,25 @@ class InputService: + def build_voice_input( + self, + original_filename: str, + station_id: str | None = None, + responder_badge: str | None = None, + incident_date_hint: date | None = None, + ) -> Input: + now = datetime.now(timezone.utc) + return Input( + input_type=InputType.voice, + status=InputStatus.queued, + original_filename=original_filename, + station_id=station_id, + responder_badge=responder_badge, + incident_date_hint=incident_date_hint, + created_at=now, + updated_at=now, + ) + def build_text_input( self, narrative: str, diff --git a/app/services/whisper.py b/app/services/whisper.py new file mode 100644 index 00000000..bbc13fb4 --- /dev/null +++ b/app/services/whisper.py @@ -0,0 +1,39 @@ +import requests + +from app.core.config import WHISPER_HOST + + +def call_whisper_asr(audio_bytes: bytes, filename: str, content_type: str) -> str: + """Post audio to the local Whisper ASR sidecar and return the transcript. + + Raises ConnectionError if the service is unreachable, RuntimeError for any + other HTTP failure. Callers map these to their own error codes. + """ + whisper_url = f"{WHISPER_HOST}/asr" + files = {"audio_file": (filename, audio_bytes, content_type)} + params = {"task": "transcribe", "output": "json", "encode": "true"} + + try: + response = requests.post(whisper_url, params=params, files=files, timeout=120) + response.raise_for_status() + except requests.exceptions.ConnectionError as exc: + raise ConnectionError( + f"Could not connect to the speech-to-text service at {whisper_url}. " + "Please ensure the whisper service is running." + ) from exc + except requests.exceptions.RequestException as exc: + raise RuntimeError(f"Transcription failed: {exc}") from exc + + try: + return (response.json().get("text") or "").strip() + except ValueError: + return response.text.strip() + + +def check_whisper_available() -> bool: + """Return True if the Whisper sidecar responds on its base URL.""" + try: + requests.get(WHISPER_HOST, timeout=3) + return True + except requests.exceptions.RequestException: + return False diff --git a/app/tasks/transcribe.py b/app/tasks/transcribe.py new file mode 100644 index 00000000..76517ec2 --- /dev/null +++ b/app/tasks/transcribe.py @@ -0,0 +1,109 @@ +import logging +import wave +from datetime import datetime, timezone +from pathlib import Path +from uuid import UUID + +from app.api.schemas.enums import InputStatus +from app.core.celery import celery_app +from app.db.database import get_session +from app.db.repositories import get_input, get_job_by_uuid, update_input, update_job +from app.services.whisper import call_whisper_asr + +logger = logging.getLogger(__name__) + +_CONTENT_TYPES: dict[str, str] = { + "wav": "audio/wav", + "mp3": "audio/mpeg", + "m4a": "audio/m4a", + "ogg": "audio/ogg", + "webm": "audio/webm", +} + + +def _wav_duration(path: Path) -> float | None: + """Return duration in seconds for a WAV file; None for all other formats or on error.""" + if path.suffix.lower() != ".wav": + return None + try: + with wave.open(str(path)) as wf: + return wf.getnframes() / wf.getframerate() + except Exception: + return None + + +def _fail_records( + session, + input_id: UUID, + job_id_str: str, + error_code: str, + message: str, +) -> None: + now = datetime.now(timezone.utc) + record = get_input(session, input_id) + if record: + record.status = InputStatus.failed + record.error_detail = message + record.updated_at = now + update_input(session, record) + job = get_job_by_uuid(session, job_id_str) + if job: + job.status = "failed" + job.error = {"error_code": error_code, "message": message} + job.updated_at = now + update_job(session, job) + + +@celery_app.task(name="transcribe_audio") +def transcribe_audio_task(input_id_str: str, audio_path: str, job_id_str: str) -> dict: + session = next(get_session()) + input_id = UUID(input_id_str) + try: + input_record = get_input(session, input_id) + job = get_job_by_uuid(session, job_id_str) + + # start: mark both records in-flight + now = datetime.now(timezone.utc) + input_record.status = InputStatus.transcribing + input_record.updated_at = now + update_input(session, input_record) + + job.status = "processing" + job.updated_at = now + update_job(session, job) + + # transcribe + p = Path(audio_path) + ext = p.suffix.lstrip(".") + text = call_whisper_asr(p.read_bytes(), p.name, _CONTENT_TYPES.get(ext, "audio/wav")) + + # success + words = text.split() + now = datetime.now(timezone.utc) + input_record.status = InputStatus.ready + input_record.transcript = text + input_record.character_count = len(text) + input_record.word_count = len(words) + input_record.audio_duration_seconds = _wav_duration(p) + input_record.updated_at = now + update_input(session, input_record) + + job.status = "completed" + job.result_url = f"/api/v1/input/{input_id_str}" + job.updated_at = now + update_job(session, job) + + return {"input_id": input_id_str, "job_id": job_id_str} + + except ConnectionError as exc: + logger.exception("transcribe_audio_task: Whisper unavailable for input %s", input_id_str) + _fail_records(session, input_id, job_id_str, "LLM_UNAVAILABLE", str(exc)) + raise + + except RuntimeError as exc: + logger.exception("transcribe_audio_task: transcription failed for input %s", input_id_str) + _fail_records(session, input_id, job_id_str, "TRANSCRIPTION_FAILED", str(exc)) + raise + + finally: + session.close() diff --git a/tests/conftest.py b/tests/conftest.py index 1f41d67e..ab4370c3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,6 +55,12 @@ def db(): yield session +@pytest.fixture +def test_engine(): + """Expose the shared in-memory engine for tests that need to open extra sessions.""" + return _engine + + # --------------------------------------------------------------------------- # Minimal PDF bytes (valid 1-page blank PDF) # --------------------------------------------------------------------------- diff --git a/tests/test_v1_voice.py b/tests/test_v1_voice.py new file mode 100644 index 00000000..fd2e7f35 --- /dev/null +++ b/tests/test_v1_voice.py @@ -0,0 +1,275 @@ +"""Tests for POST /api/v1/input/voice (endpoint) and transcribe_audio_task (task unit). + +Endpoint tests: dispatch is mocked — no broker, no Whisper, no filesystem writes. +Task tests: called directly with an injected test session and mocked call_whisper_asr. +""" + +import io +import pytest +from pathlib import Path +from unittest.mock import MagicMock, patch + +from sqlmodel import Session + +from app.api.schemas.enums import InputStatus, InputType +from app.db.repositories import get_input, get_job_by_uuid +from app.models import Input, Job +from app.tasks.transcribe import transcribe_audio_task + +VOICE_URL = "/api/v1/input/voice" + +# Minimal valid WAV bytes (44-byte header + silence) — passes extension check. +_WAV_BYTES = b"RIFF$\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00\x80>\x00\x00\x00}\x00\x00\x02\x00\x10\x00data\x00\x00\x00\x00" + + +def _voice_post(client, filename="memo.wav", content=None, extra_fields=None): + """Helper: POST a voice upload, mocking dispatch and availability.""" + data = extra_fields or {} + files = {"audio_file": (filename, io.BytesIO(content or _WAV_BYTES), "audio/wav")} + return client, data, files + + +# --------------------------------------------------------------------------- +# POST /api/v1/input/voice — endpoint tests (dispatch mocked) +# --------------------------------------------------------------------------- + +class TestSubmitVoiceInput: + + def _post(self, client, filename="memo.wav", content=None, fields=None, whisper_up=True): + mock_result = MagicMock() + mock_result.id = "celery-task-uuid-001" + with patch("app.api.routes.input.check_whisper_available", return_value=whisper_up), \ + patch("app.api.routes.input.transcribe_audio_task") as mock_task, \ + patch("pathlib.Path.write_bytes"): + mock_task.delay.return_value = mock_result + resp = client.post( + VOICE_URL, + files={"audio_file": (filename, io.BytesIO(content or _WAV_BYTES), "audio/wav")}, + data=fields or {}, + ) + return resp, mock_task + + def test_201_returns_required_fields(self, client): + resp, _ = self._post(client) + assert resp.status_code == 201 + body = resp.json() + assert body["status"] == "queued" + assert body["input_type"] == "voice" + assert "input_id" in body + assert "job_id" in body + assert "poll_url" in body + assert "estimated_processing_seconds" in body + + def test_201_poll_url_points_to_input_record(self, client): + resp, _ = self._post(client) + body = resp.json() + assert body["poll_url"] == f"/api/v1/input/{body['input_id']}" + + def test_201_creates_queued_input_row(self, client, db): + resp, _ = self._post(client) + input_id = resp.json()["input_id"] + from uuid import UUID + record = get_input(db, UUID(input_id)) + assert record is not None + assert record.status == InputStatus.queued + assert record.input_type == InputType.voice + assert record.original_filename == "memo.wav" + + def test_201_creates_transcription_job_row(self, client, db): + resp, _ = self._post(client) + job_id = resp.json()["job_id"] + job = get_job_by_uuid(db, job_id) + assert job is not None + assert job.job_type == "transcription" + assert job.status == "queued" + assert job.celery_task_id == "celery-task-uuid-001" + + def test_201_dispatch_called_with_input_id_and_job_id(self, client, db): + resp, mock_task = self._post(client) + body = resp.json() + mock_task.delay.assert_called_once() + call_args = mock_task.delay.call_args[0] + assert call_args[0] == body["input_id"] # input_id_str + assert call_args[2] == body["job_id"] # job_id_str + # call_args[1] is the audio_path (filesystem path string) + assert body["input_id"] in call_args[1] + + def test_201_optional_metadata_stored_on_input(self, client, db): + resp, _ = self._post(client, fields={ + "station_id": "STA-045", + "responder_badge": "FD-7842", + "incident_date_hint": "2024-07-10", + }) + assert resp.status_code == 201 + from uuid import UUID + record = get_input(db, UUID(resp.json()["input_id"])) + assert record.station_id == "STA-045" + assert record.responder_badge == "FD-7842" + assert str(record.incident_date_hint) == "2024-07-10" + + def test_415_unsupported_format_rejected(self, client): + resp, _ = self._post(client, filename="memo.pdf") + assert resp.status_code == 415 + body = resp.json() + assert body["error_code"] == "UNSUPPORTED_FORMAT" + assert "accepted_formats" in body["detail"] + assert "wav" in body["detail"]["accepted_formats"] + + def test_415_no_extension_rejected(self, client): + resp, _ = self._post(client, filename="audiofile") + assert resp.status_code == 415 + assert resp.json()["error_code"] == "UNSUPPORTED_FORMAT" + + def test_413_oversize_file_rejected(self, client, monkeypatch): + monkeypatch.setattr("app.api.routes.input._MAX_AUDIO_BYTES", 5) + resp, _ = self._post(client, content=b"toolong") # 7 bytes > 5 + assert resp.status_code == 413 + body = resp.json() + assert body["error_code"] == "FILE_TOO_LARGE" + assert body["detail"]["max_size_bytes"] == 5 + assert body["detail"]["received_size_bytes"] == 7 + + def test_503_when_whisper_unavailable(self, client): + resp, _ = self._post(client, whisper_up=False) + assert resp.status_code == 503 + body = resp.json() + assert body["error_code"] == "LLM_UNAVAILABLE" + + def test_422_missing_audio_file(self, client): + with patch("app.api.routes.input.check_whisper_available", return_value=True): + resp = client.post(VOICE_URL, data={}) + assert resp.status_code == 422 + + +# --------------------------------------------------------------------------- +# transcribe_audio_task — unit tests (no broker, direct call, mocked Whisper) +# --------------------------------------------------------------------------- + +def _seed_input_and_job(session: Session): + """Insert a queued Input and Job, return both refreshed.""" + from datetime import datetime, timezone + now = datetime.now(timezone.utc) + inp = Input( + input_type=InputType.voice, + status=InputStatus.queued, + original_filename="test.wav", + created_at=now, + updated_at=now, + ) + session.add(inp) + job = Job(celery_task_id="celery-test-id", job_type="transcription", status="queued") + session.add(job) + session.commit() + session.refresh(inp) + session.refresh(job) + return inp, job + + +class TestTranscribeAudioTask: + + def _run_task(self, inp, job, test_engine, whisper_side_effect=None, whisper_return="Transcribed text here."): + """ + Call the task function directly against the in-memory DB. + Mocks get_session (so the task uses the test engine) and call_whisper_asr. + Also mocks Path.read_bytes so no filesystem access is needed. + """ + def _gen(): + with Session(test_engine) as s: + yield s + + with patch("app.tasks.transcribe.get_session", side_effect=_gen), \ + patch("app.tasks.transcribe.call_whisper_asr") as mock_whisper, \ + patch.object(Path, "read_bytes", return_value=b"fake_audio_bytes"): + if whisper_side_effect is not None: + mock_whisper.side_effect = whisper_side_effect + else: + mock_whisper.return_value = whisper_return + try: + transcribe_audio_task(str(inp.input_id), "/data/audio/test.wav", job.job_id) + except (ConnectionError, RuntimeError): + pass # expected on failure tests + + def test_success_input_becomes_ready_with_transcript(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_return="Incident at Main St, two casualties.") + + db.refresh(inp) + assert inp.status == InputStatus.ready + assert inp.transcript == "Incident at Main St, two casualties." + assert inp.character_count == len("Incident at Main St, two casualties.") + assert inp.word_count == len("Incident at Main St, two casualties.".split()) + + def test_success_job_becomes_completed_with_result_url(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_return="Incident at Main St.") + + db.refresh(job) + assert job.status == "completed" + assert job.result_url == f"/api/v1/input/{inp.input_id}" + + def test_connection_error_input_failed_error_detail(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_side_effect=ConnectionError("Cannot reach Whisper")) + + db.refresh(inp) + assert inp.status == InputStatus.failed + assert "Cannot reach Whisper" in inp.error_detail + + def test_connection_error_job_failed_with_llm_unavailable_code(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_side_effect=ConnectionError("Cannot reach Whisper")) + + db.refresh(job) + assert job.status == "failed" + assert job.error["error_code"] == "LLM_UNAVAILABLE" + assert "Cannot reach Whisper" in job.error["message"] + + def test_runtime_error_input_failed_error_detail(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_side_effect=RuntimeError("HTTP 500 from Whisper")) + + db.refresh(inp) + assert inp.status == InputStatus.failed + assert "HTTP 500 from Whisper" in inp.error_detail + + def test_runtime_error_job_failed_with_transcription_failed_code(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_side_effect=RuntimeError("HTTP 500 from Whisper")) + + db.refresh(job) + assert job.status == "failed" + assert job.error["error_code"] == "TRANSCRIPTION_FAILED" + assert "HTTP 500 from Whisper" in job.error["message"] + + def test_success_input_transitions_through_transcribing(self, db, test_engine): + """The task sets transcribing before calling Whisper and ready after.""" + inp, job = _seed_input_and_job(db) + observed_statuses: list[str] = [] + + original_update = __import__( + "app.db.repositories", fromlist=["update_input"] + ).update_input + + def tracking_update(session, input_obj): + observed_statuses.append(input_obj.status.value) + return original_update(session, input_obj) + + def _gen(): + with Session(test_engine) as s: + yield s + + with patch("app.tasks.transcribe.get_session", side_effect=_gen), \ + patch("app.tasks.transcribe.call_whisper_asr", return_value="Hello world text."), \ + patch("app.tasks.transcribe.update_input", side_effect=tracking_update), \ + patch.object(Path, "read_bytes", return_value=b"fake"): + transcribe_audio_task(str(inp.input_id), "/data/audio/test.wav", job.job_id) + + assert "transcribing" in observed_statuses + assert "ready" in observed_statuses + assert observed_statuses.index("transcribing") < observed_statuses.index("ready") From b623461dc8fe17c1fcd97d9fa927eb715ee757b2 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 30 Jun 2026 00:08:05 +0530 Subject: [PATCH 61/85] Address #547 review --- app/api/routes/input.py | 54 ++++++++++++++++++--------------------- app/core/config.py | 14 +++++++++- app/services/input.py | 44 ++++++++++++++++++++++++++++++- app/services/whisper.py | 5 ++-- app/tasks/transcribe.py | 13 +++------- contracts/path/input.yaml | 6 ++--- tests/test_v1_voice.py | 16 ++++-------- 7 files changed, 94 insertions(+), 58 deletions(-) diff --git a/app/api/routes/input.py b/app/api/routes/input.py index b677a34f..2cd2329c 100644 --- a/app/api/routes/input.py +++ b/app/api/routes/input.py @@ -14,17 +14,19 @@ TextInputResponse, VoiceInputResponse, ) -from app.core.config import AUDIO_DIR, ESTIMATED_TRANSCRIPTION_SECONDS, INPUT_POLL_INTERVAL_SECONDS +from app.core.config import ( + ALLOWED_AUDIO_EXTENSIONS, + AUDIO_CONTENT_TYPES, + ESTIMATED_TRANSCRIPTION_SECONDS, + INPUT_POLL_INTERVAL_SECONDS, +) from app.core.errors.base import AppError -from app.db.repositories import create_input, create_job, get_input as repo_get_input, update_job -from app.models import Job +from app.db.repositories import create_input, get_input as repo_get_input from app.services.input import InputService from app.services.whisper import check_whisper_available -from app.tasks.transcribe import transcribe_audio_task router = APIRouter(prefix="/input", tags=["input"]) -_ALLOWED_AUDIO_EXTS = {"wav", "mp3", "m4a", "ogg", "webm"} _MAX_AUDIO_BYTES = 500 * 1024 * 1024 # 500 MB @@ -36,18 +38,25 @@ def submit_voice_input( incident_date_hint: date | None = Form(default=None), db: Session = Depends(get_db), ): - # validate format + # 415 — format check (HTTP concern, before any I/O) filename = audio_file.filename or "" ext = Path(filename).suffix.lstrip(".").lower() - if not ext or ext not in _ALLOWED_AUDIO_EXTS: + if not ext or ext not in ALLOWED_AUDIO_EXTENSIONS: raise AppError( "Audio format not supported. Accepted formats: wav, mp3, m4a, ogg, webm", status_code=415, error_code="UNSUPPORTED_FORMAT", - detail={"accepted_formats": ["wav", "mp3", "m4a", "ogg", "webm"]}, + detail={"accepted_formats": list(AUDIO_CONTENT_TYPES)}, ) - # validate size + # 413 — size check: fast path via Content-Length (no read), fallback via len(bytes) + if audio_file.size is not None and audio_file.size > _MAX_AUDIO_BYTES: + raise AppError( + "Audio file exceeds maximum size of 500MB", + status_code=413, + error_code="FILE_TOO_LARGE", + detail={"max_size_bytes": _MAX_AUDIO_BYTES, "received_size_bytes": audio_file.size}, + ) content = audio_file.file.read() if len(content) > _MAX_AUDIO_BYTES: raise AppError( @@ -57,37 +66,24 @@ def submit_voice_input( detail={"max_size_bytes": _MAX_AUDIO_BYTES, "received_size_bytes": len(content)}, ) - # check Whisper availability before queuing + # 503 — Whisper availability (HTTP concern) if not check_whisper_available(): raise AppError( "Whisper transcription service is not available", status_code=503, - error_code="LLM_UNAVAILABLE", + error_code="STT_UNAVAILABLE", ) - # build and persist the Input record svc = InputService() - record = svc.build_voice_input( - original_filename=filename, + record, job = svc.process_voice_upload( + session=db, + audio_content=content, + ext=ext, + filename=filename, station_id=station_id, responder_badge=responder_badge, incident_date_hint=incident_date_hint, ) - record = create_input(db, record) - - # save audio to DATA_DIR/audio/{input_id}.{ext} - AUDIO_DIR.mkdir(parents=True, exist_ok=True) - audio_path = AUDIO_DIR / f"{record.input_id}.{ext}" - audio_path.write_bytes(content) - - # create Job before dispatch to avoid race - job = Job(celery_task_id="", job_type="transcription", status="queued") - job = create_job(db, job) - - # dispatch and backfill celery_task_id - result = transcribe_audio_task.delay(str(record.input_id), str(audio_path), job.job_id) - job.celery_task_id = result.id - job = update_job(db, job) return VoiceInputResponse( input_id=record.input_id, diff --git a/app/core/config.py b/app/core/config.py index 2a11b566..4a7ca897 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -72,4 +72,16 @@ AUDIO_DIR = DATA_DIR / "audio" # Advisory estimate returned in VoiceInputResponse.estimated_processing_seconds. -ESTIMATED_TRANSCRIPTION_SECONDS = int(os.getenv("ESTIMATED_TRANSCRIPTION_SECONDS", "30")) \ No newline at end of file +ESTIMATED_TRANSCRIPTION_SECONDS = int(os.getenv("ESTIMATED_TRANSCRIPTION_SECONDS", "30")) + +# Canonical audio format mapping — single source of truth for both the route +# (membership check, 415 detail list) and the task (content-type lookup). +# Dict insertion order gives the stable list shown in error responses. +AUDIO_CONTENT_TYPES: dict[str, str] = { + "wav": "audio/wav", + "mp3": "audio/mpeg", + "m4a": "audio/m4a", + "ogg": "audio/ogg", + "webm": "audio/webm", +} +ALLOWED_AUDIO_EXTENSIONS: frozenset[str] = frozenset(AUDIO_CONTENT_TYPES) \ No newline at end of file diff --git a/app/services/input.py b/app/services/input.py index a6fa229b..1aa28924 100644 --- a/app/services/input.py +++ b/app/services/input.py @@ -1,7 +1,12 @@ from datetime import date, datetime, timezone +from sqlmodel import Session + from app.api.schemas.enums import InputStatus, InputType -from app.models import Input +from app.core.config import AUDIO_CONTENT_TYPES, AUDIO_DIR +from app.db.repositories import create_input, create_job, update_job +from app.models import Input, Job +from app.tasks.transcribe import transcribe_audio_task class InputService: @@ -24,6 +29,43 @@ def build_voice_input( updated_at=now, ) + def process_voice_upload( + self, + session: Session, + audio_content: bytes, + ext: str, + filename: str, + station_id: str | None, + responder_badge: str | None, + incident_date_hint: date | None, + ) -> tuple[Input, Job]: + record = self.build_voice_input( + original_filename=filename, + station_id=station_id, + responder_badge=responder_badge, + incident_date_hint=incident_date_hint, + ) + record = create_input(session, record) + + AUDIO_DIR.mkdir(parents=True, exist_ok=True) + audio_path = AUDIO_DIR / f"{record.input_id}.{ext}" + audio_path.write_bytes(audio_content) + + # Create Job, dispatch, and backfill celery_task_id. + # On any failure after the file write, remove the orphaned audio file. + try: + job = Job(celery_task_id="", job_type="transcription", status="queued") + job = create_job(session, job) + result = transcribe_audio_task.delay(str(record.input_id), str(audio_path), job.job_id) + job.celery_task_id = result.id + job = update_job(session, job) + except Exception: + if audio_path.exists(): + audio_path.unlink() + raise + + return record, job + def build_text_input( self, narrative: str, diff --git a/app/services/whisper.py b/app/services/whisper.py index bbc13fb4..118af8ec 100644 --- a/app/services/whisper.py +++ b/app/services/whisper.py @@ -31,9 +31,8 @@ def call_whisper_asr(audio_bytes: bytes, filename: str, content_type: str) -> st def check_whisper_available() -> bool: - """Return True if the Whisper sidecar responds on its base URL.""" + """Return True if the Whisper sidecar responds with a successful status.""" try: - requests.get(WHISPER_HOST, timeout=3) - return True + return requests.get(WHISPER_HOST, timeout=3).ok except requests.exceptions.RequestException: return False diff --git a/app/tasks/transcribe.py b/app/tasks/transcribe.py index 76517ec2..91999c65 100644 --- a/app/tasks/transcribe.py +++ b/app/tasks/transcribe.py @@ -6,20 +6,13 @@ from app.api.schemas.enums import InputStatus from app.core.celery import celery_app +from app.core.config import AUDIO_CONTENT_TYPES from app.db.database import get_session from app.db.repositories import get_input, get_job_by_uuid, update_input, update_job from app.services.whisper import call_whisper_asr logger = logging.getLogger(__name__) -_CONTENT_TYPES: dict[str, str] = { - "wav": "audio/wav", - "mp3": "audio/mpeg", - "m4a": "audio/m4a", - "ogg": "audio/ogg", - "webm": "audio/webm", -} - def _wav_duration(path: Path) -> float | None: """Return duration in seconds for a WAV file; None for all other formats or on error.""" @@ -75,7 +68,7 @@ def transcribe_audio_task(input_id_str: str, audio_path: str, job_id_str: str) - # transcribe p = Path(audio_path) ext = p.suffix.lstrip(".") - text = call_whisper_asr(p.read_bytes(), p.name, _CONTENT_TYPES.get(ext, "audio/wav")) + text = call_whisper_asr(p.read_bytes(), p.name, AUDIO_CONTENT_TYPES.get(ext, "audio/wav")) # success words = text.split() @@ -97,7 +90,7 @@ def transcribe_audio_task(input_id_str: str, audio_path: str, job_id_str: str) - except ConnectionError as exc: logger.exception("transcribe_audio_task: Whisper unavailable for input %s", input_id_str) - _fail_records(session, input_id, job_id_str, "LLM_UNAVAILABLE", str(exc)) + _fail_records(session, input_id, job_id_str, "STT_UNAVAILABLE", str(exc)) raise except RuntimeError as exc: diff --git a/contracts/path/input.yaml b/contracts/path/input.yaml index a2d6a57a..def2f2ce 100644 --- a/contracts/path/input.yaml +++ b/contracts/path/input.yaml @@ -89,14 +89,14 @@ voice: - ogg - webm "503": - description: Ollama/Whisper service unavailable + description: Whisper transcription service unavailable content: application/json: schema: $ref: "../schemas/common.yaml#/ErrorResponse" example: - error_code: "LLM_UNAVAILABLE" - message: "Ollama transcription service is not available" + error_code: "STT_UNAVAILABLE" + message: "Whisper transcription service is not available" retry_after_seconds: 30 text: diff --git a/tests/test_v1_voice.py b/tests/test_v1_voice.py index fd2e7f35..1ae2ab09 100644 --- a/tests/test_v1_voice.py +++ b/tests/test_v1_voice.py @@ -22,13 +22,6 @@ _WAV_BYTES = b"RIFF$\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00\x80>\x00\x00\x00}\x00\x00\x02\x00\x10\x00data\x00\x00\x00\x00" -def _voice_post(client, filename="memo.wav", content=None, extra_fields=None): - """Helper: POST a voice upload, mocking dispatch and availability.""" - data = extra_fields or {} - files = {"audio_file": (filename, io.BytesIO(content or _WAV_BYTES), "audio/wav")} - return client, data, files - - # --------------------------------------------------------------------------- # POST /api/v1/input/voice — endpoint tests (dispatch mocked) # --------------------------------------------------------------------------- @@ -38,8 +31,9 @@ class TestSubmitVoiceInput: def _post(self, client, filename="memo.wav", content=None, fields=None, whisper_up=True): mock_result = MagicMock() mock_result.id = "celery-task-uuid-001" + # Dispatch is owned by InputService.process_voice_upload — patch there. with patch("app.api.routes.input.check_whisper_available", return_value=whisper_up), \ - patch("app.api.routes.input.transcribe_audio_task") as mock_task, \ + patch("app.services.input.transcribe_audio_task") as mock_task, \ patch("pathlib.Path.write_bytes"): mock_task.delay.return_value = mock_result resp = client.post( @@ -133,7 +127,7 @@ def test_503_when_whisper_unavailable(self, client): resp, _ = self._post(client, whisper_up=False) assert resp.status_code == 503 body = resp.json() - assert body["error_code"] == "LLM_UNAVAILABLE" + assert body["error_code"] == "STT_UNAVAILABLE" def test_422_missing_audio_file(self, client): with patch("app.api.routes.input.check_whisper_available", return_value=True): @@ -218,14 +212,14 @@ def test_connection_error_input_failed_error_detail(self, db, test_engine): assert inp.status == InputStatus.failed assert "Cannot reach Whisper" in inp.error_detail - def test_connection_error_job_failed_with_llm_unavailable_code(self, db, test_engine): + def test_connection_error_job_failed_with_stt_unavailable_code(self, db, test_engine): inp, job = _seed_input_and_job(db) self._run_task(inp, job, test_engine, whisper_side_effect=ConnectionError("Cannot reach Whisper")) db.refresh(job) assert job.status == "failed" - assert job.error["error_code"] == "LLM_UNAVAILABLE" + assert job.error["error_code"] == "STT_UNAVAILABLE" assert "Cannot reach Whisper" in job.error["message"] def test_runtime_error_input_failed_error_detail(self, db, test_engine): From 11547975db8117e1e25044b9b0e2557255aa54f2 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 30 Jun 2026 18:03:45 +0530 Subject: [PATCH 62/85] feat: added layout support and accept pdf reference parameter to match with PDF registry --- contracts/path/templates.yaml | 63 +++++++++++++++++++----- contracts/schemas/template.yaml | 87 +++++++++++++++++++++++++++------ 2 files changed, 124 insertions(+), 26 deletions(-) diff --git a/contracts/path/templates.yaml b/contracts/path/templates.yaml index dab9dca5..563ae465 100644 --- a/contracts/path/templates.yaml +++ b/contracts/path/templates.yaml @@ -13,7 +13,7 @@ templates: Returns all registered form templates including built-in standard templates (NERIS, NEMSIS, NIBRS, NFIRS modules, OSHA, etc.) and any custom templates added for specific jurisdictions. Each template defines the fields, validation - rules, and mapping from the canonical FireForm schema. + rules, and mapping from the FireForm incident schema. tags: - templates responses: @@ -61,7 +61,7 @@ templates: Registers a new form template for a jurisdiction or agency not yet supported. This is how FireForm extends to new states, countries, or custom agency forms without code changes. The template defines all fields, their types, validation - rules, and how each maps from the canonical FireForm schema. + rules, and how each maps from the FireForm incident schema. tags: - templates requestBody: @@ -75,13 +75,24 @@ templates: display_name: "Texas State Fire Marshal Incident Report" jurisdiction: "US-TX" agency_type: "fire_department" + pdf_template_ref: "templates/state_texas.pdf" fields: - field_name: "incident_number" field_type: "string" required: true max_length: 20 description: "State-assigned incident number" - canonical_mapping: "report_metadata.incident_number" + incident_mapping: "report_metadata.incident_number" + layout: + page: 0 + x: 188.33 + y: 621.33 + width: 127.33 + height: 28.67 + font: "Helvetica" + font_size: 10 + color: "#000000" + align: "left" - field_name: "fire_cause" field_type: "enum" required: true @@ -90,10 +101,26 @@ templates: - "natural" - "intentional" - "undetermined" - canonical_mapping: "fire.cause_category" - field_mappings_from_canonical: - "report_metadata.incident_number": "incident_number" - "fire.cause_category": "fire_cause" + incident_mapping: "fire.cause_category" + layout: + page: 0 + x: 188.33 + y: 560.0 + width: 200.0 + height: 18.0 + - field_name: "report_footer" + field_type: "string" + required: false + static_text: "Generated by FireForm" + incident_mapping: null + layout: + page: 0 + x: 72.0 + y: 40.0 + width: 300.0 + height: 12.0 + font_size: 8 + align: "center" responses: "201": description: Template created @@ -120,8 +147,8 @@ template_by_id: summary: Get full template schema description: | Returns the complete template definition including all fields, their types, - required/optional status, validation rules, and the mapping from canonical - FireForm schema fields. Includes the source standard reference where applicable. + required/optional status, validation rules, and the mapping from the FireForm + incident schema fields. Includes the source standard reference where applicable. tags: - templates parameters: @@ -201,7 +228,7 @@ template_fields: summary: Get template field definitions description: | Returns just the fields list for a template with type, required/optional - status, validation rules, and canonical schema mapping. Useful for building + status, validation rules, and incident-schema mapping. Useful for building validation checklists and for the validate endpoint to determine requirements. tags: - templates @@ -253,12 +280,24 @@ template_fields: field_type: "enum" required: true description: "Primary incident type code" - canonical_mapping: "incident.types[0].neris_code" + incident_mapping: "incident.types[0].neris_code" + layout: + page: 0 + x: 120.0 + y: 680.0 + width: 160.0 + height: 18.0 - field_name: "incident_date" field_type: "date" required: true description: "Date of incident" - canonical_mapping: "incident.start_datetime" + incident_mapping: "incident.start_datetime" + layout: + page: 0 + x: 120.0 + y: 655.0 + width: 120.0 + height: 18.0 "404": description: Template not found content: diff --git a/contracts/schemas/template.yaml b/contracts/schemas/template.yaml index 45c71f77..f9ce9214 100644 --- a/contracts/schemas/template.yaml +++ b/contracts/schemas/template.yaml @@ -7,14 +7,17 @@ TemplateSummary: type: string format: uuid form_type: - $ref: "../schemas/enums.yaml#/FormType" + type: string + description: Unique form type identifier (built-in or custom jurisdiction) display_name: type: string jurisdiction: type: string + nullable: true description: Jurisdiction code (e.g. "US-Federal", "US-CA", "US-GA") agency_type: type: string + nullable: true version: type: string last_updated: @@ -34,9 +37,7 @@ CreateTemplateRequest: required: - form_type - display_name - - jurisdiction - fields - - field_mappings_from_canonical properties: form_type: type: string @@ -45,19 +46,16 @@ CreateTemplateRequest: type: string jurisdiction: type: string + nullable: true + description: Jurisdiction code. Optional — the visual editor may register a + template before jurisdiction is assigned. agency_type: type: string + nullable: true fields: type: array items: $ref: "#/TemplateField" - field_mappings_from_canonical: - type: object - additionalProperties: - type: string - description: | - Mapping from canonical FireForm JSON paths to this template's field names. - Key = canonical path (e.g. "fire.cause_category"), value = template field name. source_standard: type: string nullable: true @@ -65,7 +63,7 @@ CreateTemplateRequest: pdf_template_ref: type: string nullable: true - description: Reference to the PDF template file + description: Reference to the PDF template file the layout coordinates apply to Template: allOf: @@ -136,9 +134,70 @@ TemplateField: type: string nullable: true description: Valid values for enum fields - canonical_mapping: + incident_mapping: type: string - description: JSON path in canonical FireForm schema this field maps from + nullable: true + description: | + JSON path in the FireForm incident schema this field pulls its value from + (e.g. "fire.cause_category"). Null for a static field. Exactly one of + incident_mapping or static_text is expected. + static_text: + type: string + nullable: true + description: | + Fixed text drawn into the field instead of a mapped value. Null for a + data-mapped field. Mutually exclusive with incident_mapping. default_value: nullable: true - description: Default value if canonical field is null + description: Default value if the mapped incident field is null + layout: + nullable: true + description: Visual placement of the field on the PDF. Null for fields with + no fixed position. + allOf: + - $ref: "#/TemplateFieldLayout" + +TemplateFieldLayout: + type: object + description: | + Visual placement of a field on the PDF page. Coordinates are in PDF points + with the origin at the bottom-left of the page, so the field box runs from + start = (x, y) to end = (x + width, y + height). The editor is responsible + for converting rendered-canvas pixels to PDF points before saving. + required: + - page + - x + - y + - width + - height + properties: + page: + type: integer + description: Zero-based page index + x: + type: number + description: Lower-left X of the box, in PDF points + y: + type: number + description: Lower-left Y of the box, in PDF points (origin bottom-left) + width: + type: number + height: + type: number + font: + type: string + default: Helvetica + font_size: + type: number + default: 10 + color: + type: string + default: "#000000" + description: Hex color, e.g. "#000000" + align: + type: string + enum: + - left + - center + - right + default: left From ae550fc91bcbaad219730587144b1b645e187e44 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 30 Jun 2026 18:04:40 +0530 Subject: [PATCH 63/85] added POST /api/v1/templates/pdf endpoint to upload the pdf first before template registry --- contracts/openapi.yaml | 2 + contracts/path/templates.yaml | 100 ++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml index 781c2217..c6c0c1f5 100644 --- a/contracts/openapi.yaml +++ b/contracts/openapi.yaml @@ -86,6 +86,8 @@ paths: $ref: "path/templates.yaml#/template_by_id" /api/v1/templates/{template_id}/fields: $ref: "path/templates.yaml#/template_fields" + /api/v1/templates/pdf: + $ref: "path/templates.yaml#/templates_pdf" # ── Layer 7: System ──────────────────────────────────────────── /api/v1/health: diff --git a/contracts/path/templates.yaml b/contracts/path/templates.yaml index 563ae465..c2621cac 100644 --- a/contracts/path/templates.yaml +++ b/contracts/path/templates.yaml @@ -4,6 +4,7 @@ # POST /api/v1/templates # PUT /api/v1/templates/{template_id} # GET /api/v1/templates/{template_id}/fields +# POST /api/v1/templates/pdf templates: get: @@ -304,3 +305,102 @@ template_fields: application/json: schema: $ref: "../schemas/common.yaml#/ErrorResponse" + +templates_pdf: + post: + operationId: uploadTemplatePdf + summary: Upload a blank PDF for a template before defining its fields + description: | + Uploads the blank agency PDF that a template's field coordinates will be + written onto, and returns a `pdf_template_ref` plus the page geometry. + + This is the first step of the template-authoring flow. The visual editor + renders the returned pages, the user draws a box per field, and the box + coordinates become each field's `layout`. The resulting `pdf_template_ref` + is then sent in the `POST /api/v1/templates` body. Upload precedes create + because the layout coordinates only have meaning relative to this PDF. + + Page dimensions are returned in PDF points (1/72 inch, origin bottom-left) + so the editor can map canvas positions to the same coordinate system the + `layout` fields use. + tags: + - templates + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - pdf_file + properties: + pdf_file: + type: string + format: binary + description: Blank fillable or flat PDF form. Max 50MB. + responses: + "201": + description: PDF stored and ready to be referenced by a template + content: + application/json: + schema: + type: object + required: + - pdf_template_ref + - original_filename + - page_count + - pages + properties: + pdf_template_ref: + type: string + description: Opaque reference to the stored PDF, passed back in + the template body as pdf_template_ref + original_filename: + type: string + page_count: + type: integer + pages: + type: array + description: Per-page geometry in PDF points, index 0 = first page + items: + type: object + required: + - page + - width + - height + properties: + page: + type: integer + width: + type: number + height: + type: number + example: + pdf_template_ref: "templates/uploads/550e8400-e29b-41d4-a716-446655440080.pdf" + original_filename: "texas_sfm_incident.pdf" + page_count: 2 + pages: + - page: 0 + width: 612.0 + height: 792.0 + - page: 1 + width: 612.0 + height: 792.0 + "400": + description: Missing file or malformed multipart request + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "413": + description: PDF exceeds the maximum allowed size + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "415": + description: Uploaded file is not a valid PDF + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" From 1cfddad3dbde972503dea2534bbe976b098de970 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 30 Jun 2026 23:57:41 +0530 Subject: [PATCH 64/85] fix(contract): align zipcode paths and api tags with backend /docs. Related to #599 --- contracts/openapi.yaml | 12 ++++++------ contracts/path/weather.yaml | 2 +- contracts/path/zipcode.yaml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml index 781c2217..47c5a1cd 100644 --- a/contracts/openapi.yaml +++ b/contracts/openapi.yaml @@ -31,8 +31,10 @@ tags: description: Health checks, schema introspection, and system status - name: jobs description: Cross-cutting async job status polling - - name: external-apis - description: Integrations with external APIs (Weather, Zipcode) + - name: weather + description: Weather forecast lookup via external API + - name: zipcode + description: Address and postal code geocoding via external API paths: # ── Layer 1: Input ────────────────────────────────────────────── @@ -102,7 +104,5 @@ paths: # ── Layer 9: External APIs ───────────────────────────────────── /api/v1/weather/forecast: $ref: "path/weather.yaml#/forecast" - /api/v1/zipcode/postal-code: - $ref: "path/zipcode.yaml#/postal_code" - /api/v1/zipcode/location: - $ref: "path/zipcode.yaml#/location" + /api/v1/zipcode/lookup-address: + $ref: "path/zipcode.yaml#/lookup_address" diff --git a/contracts/path/weather.yaml b/contracts/path/weather.yaml index ff56d935..601911bd 100644 --- a/contracts/path/weather.yaml +++ b/contracts/path/weather.yaml @@ -5,7 +5,7 @@ forecast: Fetch hourly weather data for the given location and date range. Coordinates must be provided as decimal degrees (e.g. `40.7128` for latitude, `-74.0060` for longitude). tags: - - external-apis + - weather parameters: - name: latitude in: query diff --git a/contracts/path/zipcode.yaml b/contracts/path/zipcode.yaml index dcc22c40..a3480171 100644 --- a/contracts/path/zipcode.yaml +++ b/contracts/path/zipcode.yaml @@ -7,7 +7,7 @@ lookup_address: place name, state, county, and country. The list is ordered by relevance (most relevant first, up to 10 results). tags: - - external-apis + - zipcode parameters: - name: address in: query From 30c71fd3bd18236b98b156d0f4aabded3ebdcb0d Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Tue, 30 Jun 2026 23:59:43 +0530 Subject: [PATCH 65/85] feat: add make docs to render OpenAPI contracts as Swagger --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a4730961..fd8a6947 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean status ready-banner sync +.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean status ready-banner sync docs COMPOSE = docker compose -f docker/dev/compose.yml --env-file docker/.env.dev ENV_DEV = docker/.env.dev @@ -31,6 +31,7 @@ help: @echo "make shell - Open shell in running app container" @echo "make pull-model - Pull Ollama model from .env.dev ($(OLLAMA_MODEL))" @echo "make test - Run test suite" + @echo "make docs - Serve interactive API docs from the OpenAPI contract" @echo "make migrate - Run pending Alembic migrations" @echo "make migration - Generate new migration (msg='description')" @echo "make clean - Stop containers (preserves volumes)" @@ -105,6 +106,10 @@ pull-model: test: @$(COMPOSE) exec -T app python3 -m pytest tests/ -v +docs: + @echo "Serving Swagger UI from contracts/openapi.yaml at http://localhost:8088" + @npx --yes swagger-ui-watcher contracts/openapi.yaml --port 8088 + migrate: @$(COMPOSE) exec -T app alembic upgrade head From c0fc959c55443dd4999aa69007d34e005504b9ac Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Wed, 1 Jul 2026 01:06:47 +0530 Subject: [PATCH 66/85] restricted path to python code and tests workflows. Fixes #602 --- .github/workflows/tests.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9f0b366c..281b6b16 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,19 @@ name: Tests on: push: - branches: [main] + branches: [main,development] + paths: + - '**.py' + - 'requirements.txt' + - 'alembic.ini' + - '.github/workflows/tests.yml' pull_request: branches: [main,development] + paths: + - '**.py' + - 'requirements.txt' + - 'alembic.ini' + - '.github/workflows/tests.yml' jobs: test: From b1ef2c75d597bd697b508dd6b85d36a0a254e40e Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 2 Jul 2026 19:51:32 +0530 Subject: [PATCH 67/85] fix: resolve lint error --- app/services/input.py | 2 +- tests/conftest.py | 1 - tests/test_deletion.py | 5 ----- tests/test_v1_input.py | 1 - tests/test_v1_voice.py | 1 - 5 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/services/input.py b/app/services/input.py index 1aa28924..bdd51c43 100644 --- a/app/services/input.py +++ b/app/services/input.py @@ -3,7 +3,7 @@ from sqlmodel import Session from app.api.schemas.enums import InputStatus, InputType -from app.core.config import AUDIO_CONTENT_TYPES, AUDIO_DIR +from app.core.config import AUDIO_DIR from app.db.repositories import create_input, create_job, update_job from app.models import Input, Job from app.tasks.transcribe import transcribe_audio_task diff --git a/tests/conftest.py b/tests/conftest.py index ab4370c3..62fae518 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,6 @@ from app.main import app from app.api.deps import get_db -from app.core.config import API_PREFIX # single source of truth for all tests from app.models import Template, FormSubmission, Job, Input, Extraction, Incident, Form, Report # noqa: F401 — registers tables # --------------------------------------------------------------------------- diff --git a/tests/test_deletion.py b/tests/test_deletion.py index 5c39e431..4c38ebb5 100644 --- a/tests/test_deletion.py +++ b/tests/test_deletion.py @@ -2,10 +2,7 @@ POST /api/v1/forms/purge, and API-key access control. """ -import io from datetime import datetime, timedelta, timezone -from pathlib import Path -from unittest.mock import patch import pytest @@ -90,7 +87,6 @@ def test_delete_template_deletes_submission_output_pdfs(self, client, db, tmp_pa out_pdf.write_bytes(b"%PDF-1.4 filled") tpl_id = _seed_template(client, pdf_path="tpl.pdf") - sub_id = _seed_submission(db, tpl_id, output_pdf_path="filled.pdf") client.delete(f"{API_PREFIX}/templates/{tpl_id}") assert not out_pdf.exists() @@ -249,7 +245,6 @@ def test_purge_requires_key_when_set(self, client): assert resp.status_code == 401 def test_purge_with_valid_key(self, client, db): - tpl_id = _seed_template(client) resp = client.post( f"{API_PREFIX}/forms/purge?days=30", headers={"X-API-Key": "secret-test-key"}, diff --git a/tests/test_v1_input.py b/tests/test_v1_input.py index 1a00b1c5..a73f0487 100644 --- a/tests/test_v1_input.py +++ b/tests/test_v1_input.py @@ -4,7 +4,6 @@ Whisper involvement — text input is fully synchronous. """ -import pytest TEXT_URL = "/api/v1/input/text" INPUT_URL = "/api/v1/input" diff --git a/tests/test_v1_voice.py b/tests/test_v1_voice.py index 1ae2ab09..3c512a52 100644 --- a/tests/test_v1_voice.py +++ b/tests/test_v1_voice.py @@ -5,7 +5,6 @@ """ import io -import pytest from pathlib import Path from unittest.mock import MagicMock, patch From de775077e0cf43ad1d57e832ce7f9355642d88e5 Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 2 Jul 2026 20:06:49 +0530 Subject: [PATCH 68/85] fix: include development branch in lint workflow --- .github/workflows/lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 97bd836b..22edb610 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,9 +2,9 @@ name: Lint on: push: - branches: [main] + branches: [main,development] pull_request: - branches: [main] + branches: [main,development] jobs: lint: From ed4dbda396fe096c6f6bd827a15233fde697166a Mon Sep 17 00:00:00 2001 From: chetanr25 Date: Thu, 2 Jul 2026 20:21:03 +0530 Subject: [PATCH 69/85] test: fix template deletion cleanup test --- tests/test_deletion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_deletion.py b/tests/test_deletion.py index 4c38ebb5..0d036dcb 100644 --- a/tests/test_deletion.py +++ b/tests/test_deletion.py @@ -87,6 +87,7 @@ def test_delete_template_deletes_submission_output_pdfs(self, client, db, tmp_pa out_pdf.write_bytes(b"%PDF-1.4 filled") tpl_id = _seed_template(client, pdf_path="tpl.pdf") + _seed_submission(db, tpl_id, output_pdf_path="filled.pdf") client.delete(f"{API_PREFIX}/templates/{tpl_id}") assert not out_pdf.exists() From fd575835ce8a308d83705a6e885ef193475ad36a Mon Sep 17 00:00:00 2001 From: marcvergees Date: Sat, 1 Aug 2026 12:18:36 +0200 Subject: [PATCH 70/85] feat: :sparkles: first implementation of the benchmark, including from little database to accuracy validators --- .github/workflows/benchmark.yml | 57 +++++ benchmark/README.md | 22 ++ benchmark/benchmark_report.json | 240 ++++++++++++++++++ benchmark/compare_benchmarks.py | 44 ++++ benchmark/datasets/ground_truth/ics201_1.json | 221 ++++++++++++++++ benchmark/datasets/ground_truth/ics202_1.json | 49 ++++ benchmark/datasets/narratives/ics201_1.txt | 13 + benchmark/datasets/narratives/ics202_1.txt | 9 + benchmark/datasets/templates/ics_201.json | 62 +++++ benchmark/datasets/templates/ics_202.json | 38 +++ benchmark/evaluators/accuracy.py | 75 ++++++ benchmark/pipelines/base.py | 17 ++ benchmark/pipelines/pipeline.py | 53 ++++ benchmark/runners/runner.py | 86 +++++++ benchmark/test_benchmark.py | 26 ++ 15 files changed, 1012 insertions(+) create mode 100644 .github/workflows/benchmark.yml create mode 100644 benchmark/README.md create mode 100644 benchmark/benchmark_report.json create mode 100644 benchmark/compare_benchmarks.py create mode 100644 benchmark/datasets/ground_truth/ics201_1.json create mode 100644 benchmark/datasets/ground_truth/ics202_1.json create mode 100644 benchmark/datasets/narratives/ics201_1.txt create mode 100644 benchmark/datasets/narratives/ics202_1.txt create mode 100644 benchmark/datasets/templates/ics_201.json create mode 100644 benchmark/datasets/templates/ics_202.json create mode 100644 benchmark/evaluators/accuracy.py create mode 100644 benchmark/pipelines/base.py create mode 100644 benchmark/pipelines/pipeline.py create mode 100644 benchmark/runners/runner.py create mode 100644 benchmark/test_benchmark.py diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..137976e7 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,57 @@ +name: Run Pipeline Benchmark + +on: + pull_request: + branches: [ main, development ] + +jobs: + benchmark: + runs-on: ubuntu-latest + + services: + ollama: + image: ollama/ollama:latest + ports: + - 11434:11434 + + steps: + - name: Checkout PR Branch + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install pytest + + - name: Run PR Branch Benchmark + run: | + pytest benchmark/test_benchmark.py -v + mv benchmark/benchmark_report.json branch_report.json + + - name: Checkout Target Branch + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + clean: false + + - name: Run Target Branch Benchmark + run: | + pytest benchmark/test_benchmark.py -v + mv benchmark/benchmark_report.json target_report.json + + - name: Compare Benchmarks + id: compare + run: | + python benchmark/compare_benchmarks.py branch_report.json target_report.json > comparison.md + cat comparison.md + + - name: Comment PR with results + uses: thollander/actions-comment-pull-request@v3 + with: + filePath: comparison.md diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 00000000..b1e52701 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,22 @@ +# FireForm Extraction Pipeline Benchmarking Suite + +This module contains the infrastructure to evaluate and compare different extraction pipelines across branches. + +## Structure +- `/datasets/`: Contains the evaluation narratives, ground truth JSON outputs, and form template schema descriptors. +- `/evaluators/`: Defines custom comparison metrics (fuzzy matching, exact matching). +- `/runners/`: Executable scripts to run pipelines against datasets. +- `test_benchmark.py`: Pytest suite to run the evaluation dynamically on whichever pipeline is checked out. +- `compare_benchmarks.py`: Tool to format a markdown diff report comparing two output JSON report files. + +## Running Locally +Install test requirements: +```bash +pip install pytest +``` + +Execute the local benchmark: +```bash +pytest benchmark/test_benchmark.py -v -s +``` +This produces `benchmark/benchmark_report.json` containing metrics (accuracy, latency) and full result logs. diff --git a/benchmark/benchmark_report.json b/benchmark/benchmark_report.json new file mode 100644 index 00000000..59940265 --- /dev/null +++ b/benchmark/benchmark_report.json @@ -0,0 +1,240 @@ +{ + "pipeline_name": "Pipeline_2026-07-08_13h_09m_20s", + "metrics": { + "total_latency_seconds": 2.1457672119140625e-06, + "average_accuracy": 0.0 + }, + "results": [ + { + "case_id": "ics201_1", + "latency_seconds": 9.5367431640625e-07, + "accuracy_score": 0.0, + "extracted_fields": {}, + "ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_incident_number": "US-EPA-R4-2026-0707", + "3_date_time_initiated": { + "date": "07/07/2026", + "time": "06:30" + }, + "4_map_sketch": { + "total_area_of_operations": "1.5-mile radius from the intersection of CSX Rail Milepost 142.5 and Highway 411", + "incident_site_area": "Rail Milepost 142.5; 7 cars derailed; Car #14 breached and leaking Benzene; Car #15 LPG threatened by fire", + "impacted_areas": "500 meters downstream of Whispering Pines Creek actively sheening; air monitoring shows elevated VOCs within 300 feet downwind East", + "threatened_areas": "Whispering Pines Municipal Water Intake (2 miles downstream); Whispering Pines Nature Reserve Wetlands (1 mile downstream)", + "overflight_results": "Drone mapping downstream sheen trajectory and pinpointing structural damage to Car #14", + "trajectories": "Plume traveling East-Southeast at 5 mph; water containment trajectory moving at 1.5 knots East", + "impacted_shorelines": "Whispering Pines Creek banks", + "graphics_and_symbology": "North at top, showing County Line Road, Creek Road, Highway 411, CSX Rail Line, Staging Area at County Fairgrounds, and Boom locations 1 and 2" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 05:45 hours, a CSX freight train suffered a mechanical failure leading to the derailment of 7 cars. Car #14 is breached, leaking Benzene into Whispering Pines Creek at an estimated rate of 20 gpm. A secondary brush fire (~0.5 acres) is burning adjacent to an intact LPG tank car. Local residential evacuation of a 0.5-mile downwind radius is underway.", + "health_and_safety_hazards": "Benzene exposure (carcinogen, vapor hazard, inhalation/dermal risk), flash fire hazard from pooling product, heavy machinery/rail movement, heat stress (88\u00b0F), and uneven terrain near the creek bed.", + "necessary_measures": "Isolate and deny entry with a 300-foot Exclusion Zone; continuous air monitoring for LEL, O2, and PID; Level B PPE (SCBA + chemical-resistant clothing) for Hot Zone, Level C for warm zone (<1 ppm VOCs); full structural turnout gear with SCBA for fire personnel; establish a formal wet-decon corridor at the Warm/Cold zone interface." + }, + "6_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief (Initial)", + "signature": "Marcus Vance", + "date_time": "07/07/2026 08:00" + }, + "7_current_and_planned_objectives": [ + "Ensure the life safety of all emergency responders and the public within the affected perimeter by 09:00 hours.", + "Complete evacuation of the 0.5-mile downwind hazard zone by 09:30 hours.", + "Suppress the brush fire adjacent to the LPG tank car to prevent thermal impingement by 10:00 hours.", + "Deploy containment and deflection booming across Whispering Pines Creek at designated points to prevent downstream migration to the municipal water intake.", + "Establish a Unified Command structure involving Federal, State, Local, and Responsible Party entities by 10:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "06:00", + "actions": "Initial dispatch of County Fire and Sheriff Dept. Exclusion zone established at 500 feet." + }, + { + "time": "06:15", + "actions": "Command established by Fire Chief Thomas. Notification sent to State DEQ, EPA, and CSX Railroad." + }, + { + "time": "06:45", + "actions": "Sheriff Dept begins door-to-door mandatory evacuations of 45 homes within the downwind path." + }, + { + "time": "07:15", + "actions": "Hazmat Team 1 arrives. Begins setting up air monitoring perimeter and evaluating tank car integrity." + }, + { + "time": "07:30", + "actions": "Engine 41 and Tanker 5 begin defensive fire suppression on the right-of-way brush fire using foam blankets." + }, + { + "time": "07:45", + "actions": "State DEQ and CSX advance response teams arrive on scene. Initiated creek booming strategy." + }, + { + "time": "08:15", + "actions": "Planned: Deploy Spill Response Team to install 500 feet of deflection boom at Boom Site 1." + }, + { + "time": "08:45", + "actions": "Planned: Initiate drone overflight to map downstream sheen trajectory and pinpoint structural damage to Car #14." + }, + { + "time": "09:30", + "actions": "Planned: Complete transition from Local Command to Unified Command at the designated Mobile Command Post." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief J. Thomas (County Fire)", + "S. Albright (State DEQ)", + "D. Miller (CSX Railroad RP)" + ], + "safety_officer": "Captain R. Mendez", + "public_information_officer": "A. Cho (County)", + "liaison_officer": "Inspector G. Sims (SO)", + "operations_section_chief": "B. Reynolds (State)", + "planning_section_chief": "Marcus Vance", + "logistics_section_chief": "K. Dunavan", + "finance_administration_section_chief": "", + "additional_positions": [ + { + "position": "Hazmat Group Supervisor", + "name": "Lt. T. Kincaid (Regional Team 1)" + }, + { + "position": "Fire Suppression Division Commander", + "name": "Batt. Chief E. Walters" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Hazmat Type 1", + "resource_identifier": "Regional Hazmat Team 1", + "date_time_ordered": "07/07/2026 06:00", + "eta": "07:00", + "arrived": true, + "notes": "Positioned at Cold Zone boundary. Conducting air monitoring and plugging entry." + }, + { + "resource": "Engine Co.", + "resource_identifier": "County Engine 41", + "date_time_ordered": "07/07/2026 05:48", + "eta": "06:00", + "arrived": true, + "notes": "Assigned to Fire Suppression Division. Suppressing brush fire near rail line." + }, + { + "resource": "Engine Co.", + "resource_identifier": "County Engine 45", + "date_time_ordered": "07/07/2026 05:48", + "eta": "06:05", + "arrived": true, + "notes": "Providing water supply support to Engine 41." + }, + { + "resource": "Water Tender", + "resource_identifier": "County Tanker 5", + "date_time_ordered": "07/07/2026 06:02", + "eta": "06:20", + "arrived": true, + "notes": "Supplying continuous water flow for foam application." + }, + { + "resource": "Law Enforcement", + "resource_identifier": "County Sheriff (4 Units)", + "date_time_ordered": "07/07/2026 05:48", + "eta": "05:55", + "arrived": true, + "notes": "Establishing traffic control points, roadblocks on Creek Rd, and evacuating residents." + }, + { + "resource": "Spill Contractor", + "resource_identifier": "HEPACO Spill Team A", + "date_time_ordered": "07/07/2026 06:45", + "eta": "08:15", + "arrived": false, + "notes": "En route with 1,000 ft containment boom and vacuum trucks. Assigned to Ops / Creek booming." + }, + { + "resource": "UAS (Drone)", + "resource_identifier": "State DEQ Drone Unit 1", + "date_time_ordered": "07/07/2026 07:10", + "eta": "08:30", + "arrived": false, + "notes": "En route. Will launch from Staging Area to provide real-time thermal imaging and plume visuals." + }, + { + "resource": "Ambulance", + "resource_identifier": "County EMS Unit 12", + "date_time_ordered": "07/07/2026 06:00", + "eta": "06:12", + "arrived": true, + "notes": "Standing by at Rehab/Staging for emergency medical support or responder heat issues." + }, + { + "resource": "Vacuum Truck", + "resource_identifier": "CSX Contractor TechRad", + "date_time_ordered": "07/07/2026 07:15", + "eta": "09:30", + "arrived": false, + "notes": "Ordered by Responsible Party for offloading breached car contents once stable." + } + ] + } + }, + { + "case_id": "ics202_1", + "latency_seconds": 1.1920928955078125e-06, + "accuracy_score": 0.0, + "extracted_fields": {}, + "ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the entirety of the night shift.", + "Deploy an additional five hundred feet of underflow and sorbent booming at designated downstream check-points on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen.", + "Complete the structural stabilization and grounding of the breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading.", + "Establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours to guarantee community safety.", + "Finalize the morning resource allocation and shift rotation plan by 04:00 hours to ensure seamless operational continuity." + ], + "4_operational_period_command_emphasis": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Command explicitly prioritizes the safety of the hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery.", + "general_situational_awareness": "The local weather forecast predicts clearing skies with a significant ambient temperature drop to 62\u00b0F by midnight, accompanied by winds shifting from the Northwest at three to five miles per hour. This wind shift introduces a critical safety warning regarding the potential accumulation of heavy Benzene vapors in low-lying drainage ditches and creek pockets east of the derailment site. Field crews are issued a universal safety message to remain highly vigilant for low-visibility hazards, uneven muddy terrain along the banks, and potential wildlife hazards native to the adjacent wetlands.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Mobile Command Post inside the Forward Operations Briefing Trailer", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "EPA Plume Trajectory Analysis Sheet", + "CSX Tank Car Cargo Manifest" + ] + }, + "7_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief J. Thomas", + "signature": "Chief J. Thomas", + "date_time": "07/07/2026 17:00" + }, + "iap_page_number": "1" + } + } + ] +} \ No newline at end of file diff --git a/benchmark/compare_benchmarks.py b/benchmark/compare_benchmarks.py new file mode 100644 index 00000000..256d28dd --- /dev/null +++ b/benchmark/compare_benchmarks.py @@ -0,0 +1,44 @@ +import json +import os +import sys + +def main(): + if len(sys.argv) < 3: + print("Usage: python compare_benchmarks.py ") + sys.exit(1) + + branch_file = sys.argv[1] + target_file = sys.argv[2] + + if not os.path.exists(branch_file) or not os.path.exists(target_file): + print(f"Error: One or both files do not exist: {branch_file}, {target_file}") + sys.exit(1) + + with open(branch_file, "r") as f: + branch_data = json.load(f) + + with open(target_file, "r") as f: + target_data = json.load(f) + + b_metrics = branch_data.get("metrics", {}) + t_metrics = target_data.get("metrics", {}) + + b_acc = b_metrics.get("average_accuracy", 0.0) + t_acc = t_metrics.get("average_accuracy", 0.0) + + b_lat = b_metrics.get("total_latency_seconds", 0.0) + t_lat = t_metrics.get("total_latency_seconds", 0.0) + + # Format Markdown comparison + markdown = f""" +## Pipeline Benchmark Comparison Report + +| Metric | Target Branch ({target_data.get('pipeline_name', 'Unknown')}) | PR Branch ({branch_data.get('pipeline_name', 'Unknown')}) | Difference | +|---|---|---|---| +| **Average Accuracy** | {t_acc:.2%} | {b_acc:.2%} | {b_acc - t_acc:+.2%} | +| **Total Latency** | {t_lat:.2f}s | {b_lat:.2f}s | {b_lat - t_lat:+.2f}s | +""" + print(markdown) + +if __name__ == "__main__": + main() diff --git a/benchmark/datasets/ground_truth/ics201_1.json b/benchmark/datasets/ground_truth/ics201_1.json new file mode 100644 index 00000000..ff15a1b3 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_1.json @@ -0,0 +1,221 @@ +{ + "ics_201_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_incident_number": "US-EPA-R4-2026-0707", + "3_date_time_initiated": { + "date": "07/07/2026", + "time": "06:30" + }, + "4_map_sketch": { + "total_area_of_operations": "1.5-mile radius from the intersection of CSX Rail Milepost 142.5 and Highway 411", + "incident_site_area": "Rail Milepost 142.5; 7 cars derailed; Car #14 breached and leaking Benzene; Car #15 LPG threatened by fire", + "impacted_areas": "500 meters downstream of Whispering Pines Creek actively sheening; air monitoring shows elevated VOCs within 300 feet downwind East", + "threatened_areas": "Whispering Pines Municipal Water Intake (2 miles downstream); Whispering Pines Nature Reserve Wetlands (1 mile downstream)", + "overflight_results": "Drone mapping downstream sheen trajectory and pinpointing structural damage to Car #14", + "trajectories": "Plume traveling East-Southeast at 5 mph; water containment trajectory moving at 1.5 knots East", + "impacted_shorelines": "Whispering Pines Creek banks", + "graphics_and_symbology": "North at top, showing County Line Road, Creek Road, Highway 411, CSX Rail Line, Staging Area at County Fairgrounds, and Boom locations 1 and 2" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 05:45 hours, a CSX freight train suffered a mechanical failure leading to the derailment of 7 cars. Car #14 is breached, leaking Benzene into Whispering Pines Creek at an estimated rate of 20 gpm. A secondary brush fire (~0.5 acres) is burning adjacent to an intact LPG tank car. Local residential evacuation of a 0.5-mile downwind radius is underway.", + "health_and_safety_hazards": "Benzene exposure (carcinogen, vapor hazard, inhalation/dermal risk), flash fire hazard from pooling product, heavy machinery/rail movement, heat stress (88°F), and uneven terrain near the creek bed.", + "necessary_measures": "Isolate and deny entry with a 300-foot Exclusion Zone; continuous air monitoring for LEL, O2, and PID; Level B PPE (SCBA + chemical-resistant clothing) for Hot Zone, Level C for warm zone (<1 ppm VOCs); full structural turnout gear with SCBA for fire personnel; establish a formal wet-decon corridor at the Warm/Cold zone interface." + }, + "6_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief (Initial)", + "signature": "Marcus Vance", + "date_time": "07/07/2026 08:00" + }, + "7_current_and_planned_objectives": [ + "Ensure the life safety of all emergency responders and the public within the affected perimeter by 09:00 hours.", + "Complete evacuation of the 0.5-mile downwind hazard zone by 09:30 hours.", + "Suppress the brush fire adjacent to the LPG tank car to prevent thermal impingement by 10:00 hours.", + "Deploy containment and deflection booming across Whispering Pines Creek at designated points to prevent downstream migration to the municipal water intake.", + "Establish a Unified Command structure involving Federal, State, Local, and Responsible Party entities by 10:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "06:00", + "actions": "Initial dispatch of County Fire and Sheriff Dept. Exclusion zone established at 500 feet." + }, + { + "time": "06:15", + "actions": "Command established by Fire Chief Thomas. Notification sent to State DEQ, EPA, and CSX Railroad." + }, + { + "time": "06:45", + "actions": "Sheriff Dept begins door-to-door mandatory evacuations of 45 homes within the downwind path." + }, + { + "time": "07:15", + "actions": "Hazmat Team 1 arrives. Begins setting up air monitoring perimeter and evaluating tank car integrity." + }, + { + "time": "07:30", + "actions": "Engine 41 and Tanker 5 begin defensive fire suppression on the right-of-way brush fire using foam blankets." + }, + { + "time": "07:45", + "actions": "State DEQ and CSX advance response teams arrive on scene. Initiated creek booming strategy." + }, + { + "time": "08:15", + "actions": "Planned: Deploy Spill Response Team to install 500 feet of deflection boom at Boom Site 1." + }, + { + "time": "08:45", + "actions": "Planned: Initiate drone overflight to map downstream sheen trajectory and pinpoint structural damage to Car #14." + }, + { + "time": "09:30", + "actions": "Planned: Complete transition from Local Command to Unified Command at the designated Mobile Command Post." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief J. Thomas (County Fire)", + "S. Albright (State DEQ)", + "D. Miller (CSX Railroad RP)" + ], + "safety_officer": "Captain R. Mendez", + "public_information_officer": "A. Cho (County)", + "liaison_officer": "Inspector G. Sims (SO)", + "operations_section_chief": "B. Reynolds (State)", + "planning_section_chief": "Marcus Vance", + "logistics_section_chief": "K. Dunavan", + "finance_administration_section_chief": "", + "additional_positions": [ + { + "position": "Hazmat Group Supervisor", + "name": "Lt. T. Kincaid (Regional Team 1)" + }, + { + "position": "Fire Suppression Division Commander", + "name": "Batt. Chief E. Walters" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Hazmat Type 1", + "resource_identifier": "Regional Hazmat Team 1", + "date_time_ordered": "07/07/2026 06:00", + "eta": "07:00", + "arrived": true, + "notes": "Positioned at Cold Zone boundary. Conducting air monitoring and plugging entry." + }, + { + "resource": "Engine Co.", + "resource_identifier": "County Engine 41", + "date_time_ordered": "07/07/2026 05:48", + "eta": "06:00", + "arrived": true, + "notes": "Assigned to Fire Suppression Division. Suppressing brush fire near rail line." + }, + { + "resource": "Engine Co.", + "resource_identifier": "County Engine 45", + "date_time_ordered": "07/07/2026 05:48", + "eta": "06:05", + "arrived": true, + "notes": "Providing water supply support to Engine 41." + }, + { + "resource": "Water Tender", + "resource_identifier": "County Tanker 5", + "date_time_ordered": "07/07/2026 06:02", + "eta": "06:20", + "arrived": true, + "notes": "Supplying continuous water flow for foam application." + }, + { + "resource": "Law Enforcement", + "resource_identifier": "County Sheriff (4 Units)", + "date_time_ordered": "07/07/2026 05:48", + "eta": "05:55", + "arrived": true, + "notes": "Establishing traffic control points, roadblocks on Creek Rd, and evacuating residents." + }, + { + "resource": "Spill Contractor", + "resource_identifier": "HEPACO Spill Team A", + "date_time_ordered": "07/07/2026 06:45", + "eta": "08:15", + "arrived": false, + "notes": "En route with 1,000 ft containment boom and vacuum trucks. Assigned to Ops / Creek booming." + }, + { + "resource": "UAS (Drone)", + "resource_identifier": "State DEQ Drone Unit 1", + "date_time_ordered": "07/07/2026 07:10", + "eta": "08:30", + "arrived": false, + "notes": "En route. Will launch from Staging Area to provide real-time thermal imaging and plume visuals." + }, + { + "resource": "Ambulance", + "resource_identifier": "County EMS Unit 12", + "date_time_ordered": "07/07/2026 06:00", + "eta": "06:12", + "arrived": true, + "notes": "Standing by at Rehab/Staging for emergency medical support or responder heat issues." + }, + { + "resource": "Vacuum Truck", + "resource_identifier": "CSX Contractor TechRad", + "date_time_ordered": "07/07/2026 07:15", + "eta": "09:30", + "arrived": false, + "notes": "Ordered by Responsible Party for offloading breached car contents once stable." + } + ] + }, + "ics_202_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the entirety of the night shift.", + "Deploy an additional five hundred feet of underflow and sorbent booming at designated downstream check-points on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen.", + "Complete the structural stabilization and grounding of the breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading.", + "Establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours to guarantee community safety.", + "Finalize the morning resource allocation and shift rotation plan by 04:00 hours to ensure seamless operational continuity." + ], + "4_operational_period_command_emphasis": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Command explicitly prioritizes the safety of the hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery.", + "general_situational_awareness": "The local weather forecast predicts clearing skies with a significant ambient temperature drop to 62°F by midnight, accompanied by winds shifting from the Northwest at three to five miles per hour. This wind shift introduces a critical safety warning regarding the potential accumulation of heavy Benzene vapors in low-lying drainage ditches and creek pockets east of the derailment site. Field crews must remain highly vigilant for low-visibility hazards, uneven muddy terrain along the banks, and potential wildlife hazards native to the adjacent wetlands.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Mobile Command Post inside the Forward Operations Briefing Trailer", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "EPA Plume Trajectory Analysis Sheet", + "CSX Tank Car Cargo Manifest" + ] + }, + "7_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief J. Thomas", + "signature": "Chief J. Thomas", + "date_time": "07/07/2026 17:00" + }, + "iap_page_number": "1" + } +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics202_1.json b/benchmark/datasets/ground_truth/ics202_1.json new file mode 100644 index 00000000..f6fc17d9 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_1.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the entirety of the night shift.", + "Deploy an additional five hundred feet of underflow and sorbent booming at designated downstream check-points on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen.", + "Complete the structural stabilization and grounding of the breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading.", + "Establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours to guarantee community safety.", + "Finalize the morning resource allocation and shift rotation plan by 04:00 hours to ensure seamless operational continuity." + ], + "4_operational_period_command_emphasis": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Command explicitly prioritizes the safety of the hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery.", + "general_situational_awareness": "The local weather forecast predicts clearing skies with a significant ambient temperature drop to 62°F by midnight, accompanied by winds shifting from the Northwest at three to five miles per hour. This wind shift introduces a critical safety warning regarding the potential accumulation of heavy Benzene vapors in low-lying drainage ditches and creek pockets east of the derailment site. Field crews are issued a universal safety message to remain highly vigilant for low-visibility hazards, uneven muddy terrain along the banks, and potential wildlife hazards native to the adjacent wetlands.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Mobile Command Post inside the Forward Operations Briefing Trailer", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "EPA Plume Trajectory Analysis Sheet", + "CSX Tank Car Cargo Manifest" + ] + }, + "7_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief J. Thomas", + "signature": "Chief J. Thomas", + "date_time": "07/07/2026 17:00" + }, + "iap_page_number": "1" + } +} \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics201_1.txt b/benchmark/datasets/narratives/ics201_1.txt new file mode 100644 index 00000000..a3d83038 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_1.txt @@ -0,0 +1,13 @@ +### Whispering Pines Derailment Mockup Incident Briefing (ICS 201) + +The Whispering Pines Derailment incident, designated under incident number US-EPA-R4-2026-0707, was formally initiated on July 07, 2026, at 06:30 hours. + +The operational landscape, mapped with North oriented at the top of the page, encompasses a 1.5-mile radius centered around the intersection of CSX Rail Milepost 142.5 and Highway 411. Within this sector, County Line Road is completely closed to public traffic to secure the Staging Area established at the County Fairgrounds. The primary incident site is located at Rail Milepost 142.5, where seven freight cars (#12 through #18) have jackknifed. The actively impacted area includes Car #14, a breached tank car releasing a Class 3 flammable and toxic liquid (Benzene) directly into Whispering Pines Creek at an estimated rate of 20 gallons per minute, causing visible surface sheening extending 500 meters downstream. Local air monitoring indicates elevated Volatile Organic Compounds within a 300-foot downwind plume tracking East-Southeast at 5 miles per hour. The critical threatened areas downstream include the Whispering Pines Nature Reserve Wetlands one mile away and the Municipal Water Intake located two miles downstream. Trajectory modeling shows waterborne containment issues moving East at 1.5 knots, which commands immediate defensive booming actions. + +The situation summary reveals that the derailment occurred at 05:45 hours due to a mechanical rail car failure, sparking a secondary 0.5-acre brush fire in the right-of-way that directly threatens an adjacent, intact liquefied petroleum gas tank car. In response, a mandatory 0.5-mile residential evacuation is being executed by local law enforcement. The comprehensive health and safety briefing identifies primary hazards as toxic Benzene inhalation, dermal exposure risks, flash fires from pooling product, heavy machinery movement around compromised rail lines, responder heat stress in the ambient 88°F weather, and slip-and-trip hazards along the steep creek banks. To mitigate these risks, an immediate 300-foot Exclusion Zone has been established around the rail cars. Responders entering the Hot Zone for containment are strictly required to wear Level B personal protective equipment, including self-contained breathing apparatus and chemical-resistant clothing. Level C protection is authorized for warm zone booming teams only if continuous photoionization detector monitoring confirms volatile organic compound levels remain below 1 part per million. Firefighters must wear full structural turnout gear with self-contained breathing apparatus, and all personnel must pass through a formal wet decontamination corridor set up at the Warm-to-Cold zone interface before leaving the operational area. This section was officially prepared by Marcus Vance, serving as the Initial Planning Section Chief, on July 07, 2026, at 08:00 hours. + +The current and planned objectives focus heavily on safety and containment, aiming to ensure the life safety of all emergency responders and the public within the perimeter by 09:00 hours, complete the 0.5-mile downwind residential evacuation by 09:30 hours, suppress the brush fire near the liquefied petroleum gas car to eliminate thermal risks by 10:00 hours, deploy effective deflection and underflow containment booming across the creek to protect the water intake, and formally establish a Unified Command structure by 10:30 hours. The chronological strategy and tactics began at 06:00 hours with the initial dispatch of County Fire and Sheriff units and the setup of a 500-foot isolation perimeter. At 06:15 hours, local Fire Chief Thomas established command and notified the State Department of Environmental Quality, the Environmental Protection Agency, and CSX Railroad. By 06:45 hours, the Sheriff’s Department initiated door-to-door evacuations of 45 homes. At 07:15 hours, Regional Hazmat Team 1 arrived to deploy an air-monitoring grid and inspect the damaged tanks, followed at 07:30 hours by Engine 41 and Tanker 5 initiating defensive foam applications on the brush fire. By 07:45 hours, advance teams from the state environmental agency and the railroad arrived to finalize the water-containment strategy. Planned actions include deploying a specialized spill contractor to drop 500 feet of deflection boom at Boom Site 1 at 08:15 hours, launching a drone overflight at 08:45 hours to map downstream tracking, and completing the transition to a fully unified command at the Mobile Command Post at 09:30 hours. This programmatic operational strategy was compiled and signed by Planning Section Chief Marcus Vance on July 07, 2026, at 08:00 hours. + +The current command and general staff organization is structured as a Unified Command consisting of Chief J. Thomas from County Fire, S. Albright from the State Department of Environmental Quality, and D. Miller representing CSX Railroad as the Responsible Party. This command core is directly supported by Safety Officer Captain R. Mendez, Public Information Officer A. Cho representing the county, and Liaison Officer Inspector G. Sims from the Sheriff's Office. The General Staff is led by Operations Section Chief B. Reynolds from the state agency, Planning Section Chief Marcus Vance, and Logistics Section Chief K. Dunavan. Under the Operations Section, field execution is split into the Hazardous Materials Group, supervised by Lieutenant T. Kincaid of Regional Hazmat Team 1, and the Fire Suppression Division, commanded by Battalion Chief E. Walters. This organizational layout was logged and validated by Planning Section Chief Marcus Vance on July 07, 2026, at 08:00 hours. + +The resource summary details the specific assets allocated to stabilize the incident. Regional Hazmat Team 1, a Type 1 Hazardous Materials unit, was ordered at 06:00 hours, arrived on scene at 07:00 hours, and is currently positioned at the Cold Zone boundary conducting perimeter monitoring and plugging operations. County Engine 41 and County Engine 45 were both ordered at 05:48 hours, arriving at 06:00 hours and 06:05 hours respectively; Engine 41 is actively suppressing the brush fire while Engine 45 manages critical water supply lines. County Tanker 5, a water tender ordered at 06:02 hours, arrived at 06:20 hours and is providing continuous flow for foam application. Four law enforcement units from the County Sheriff arrived at 05:55 hours following a 05:48 hours dispatch and are running traffic checkpoints and roadblocks on Creek Road alongside evacuation duties. County EMS Unit 12 arrived at 06:12 hours after being ordered at 06:00 hours, standing by at the Staging Area for responder rehabilitation and medical backup. Several critical resources are currently en route: HEPACO Spill Team A was ordered at 06:45 hours with an estimated arrival of 08:15 hours to deploy 1,000 feet of containment boom and vacuum trucks; State DEQ Drone Unit 1 was ordered at 07:10 hours with an estimated arrival of 08:30 hours to provide aerial thermal imagery; and the CSX contractor vacuum truck TechRad was ordered at 07:15 hours with an estimated arrival of 09:30 hours to begin product offloading from the breached tank car once the scene stabilizes. This complete resource ledger was verified and signed by Planning Section Chief Marcus Vance on July 07, 2026, at 08:00 hours. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics202_1.txt b/benchmark/datasets/narratives/ics202_1.txt new file mode 100644 index 00000000..a0af7cea --- /dev/null +++ b/benchmark/datasets/narratives/ics202_1.txt @@ -0,0 +1,9 @@ +### Whispering Pines Derailment Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from July 07, 2026, at 18:00 hours through July 08, 2026, at 06:00 hours, marking the first critical night-shift rotation of the response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the entirety of the night shift. The second objective dictates that containment teams must deploy an additional five hundred feet of underflow and sorbent booming at designated downstream check-points on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen. The third objective requires hazardous materials entry teams to complete the structural stabilization and grounding of the breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading. The fourth objective mandates that the Air Monitoring Group establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours to guarantee community safety. The fifth and final objective requires the Planning and Logistics sections to finalize the morning resource allocation and shift rotation plan by 04:00 hours to ensure seamless operational continuity. + +The operational period command emphasis directs all supervisors to place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Command explicitly prioritizes the safety of the hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery. General situational awareness notes indicate that the local weather forecast predicts clearing skies with a significant ambient temperature drop to 62°F by midnight, accompanied by winds shifting from the Northwest at three to five miles per hour. This wind shift introduces a critical safety warning regarding the potential accumulation of heavy Benzene vapors in low-lying drainage ditches and creek pockets east of the derailment site. Field crews are issued a universal safety message to remain highly vigilant for low-visibility hazards, uneven muddy terrain along the banks, and potential wildlife hazards native to the adjacent wetlands. Due to the high-consequence nature of the chemical release, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the primary Mobile Command Post inside the Forward Operations Briefing Trailer. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for the active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the hot zone, and the localized National Weather Service Forecast. Additional attachments compiled into the final briefing packet include the specialized EPA Plume Trajectory Analysis Sheet and the CSX Tank Car Cargo Manifest. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Marcus Vance, who reviewed and signed the form on July 07, 2026, at 16:30 hours. Final executive approval for the implementation of these objectives was granted by Unified Incident Commander Chief J. Thomas of County Fire, who signed off on the complete package on July 07, 2026, at 17:00 hours. \ No newline at end of file diff --git a/benchmark/datasets/templates/ics_201.json b/benchmark/datasets/templates/ics_201.json new file mode 100644 index 00000000..f308358c --- /dev/null +++ b/benchmark/datasets/templates/ics_201.json @@ -0,0 +1,62 @@ +{ + "1_incident_name": "string", + "2_incident_number": "string", + "3_date_time_initiated": { + "date": "string", + "time": "string" + }, + "4_map_sketch": { + "total_area_of_operations": "string", + "incident_site_area": "string", + "impacted_areas": "string", + "threatened_areas": "string", + "overflight_results": "string", + "trajectories": "string", + "impacted_shorelines": "string", + "graphics_and_symbology": "string" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "string", + "health_and_safety_hazards": "string", + "necessary_measures": "string" + }, + "6_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "7_current_and_planned_objectives": ["string"], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "string", + "actions": "string" + } + ], + "9_current_organization": { + "incident_commanders": ["string"], + "safety_officer": "string", + "public_information_officer": "string", + "liaison_officer": "string", + "operations_section_chief": "string", + "planning_section_chief": "string", + "logistics_section_chief": "string", + "finance_administration_section_chief": "string", + "additional_positions": [ + { + "position": "string", + "name": "string" + } + ] + }, + "10_resource_summary": [ + { + "resource": "string", + "resource_identifier": "string", + "date_time_ordered": "string", + "eta": "string", + "arrived": "boolean", + "notes": "string" + } + ] +} diff --git a/benchmark/datasets/templates/ics_202.json b/benchmark/datasets/templates/ics_202.json new file mode 100644 index 00000000..2adf6ebe --- /dev/null +++ b/benchmark/datasets/templates/ics_202.json @@ -0,0 +1,38 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_objectives": ["string"], + "4_operational_period_command_emphasis": "string", + "general_situational_awareness": "string", + "5_site_safety_plan_required": "boolean", + "approved_site_safety_plans_located_at": "string", + "6_incident_action_plan_attachments": { + "ics_203": "boolean", + "ics_204": "boolean", + "ics_205": "boolean", + "ics_205a": "boolean", + "ics_206": "boolean", + "ics_207": "boolean", + "ics_208": "boolean", + "map_chart": "boolean", + "weather_forecast_tides_currents": "boolean", + "other_attachments": ["string"] + }, + "7_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "8_approved_by_incident_commander": { + "name": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/evaluators/accuracy.py b/benchmark/evaluators/accuracy.py new file mode 100644 index 00000000..c4bd30d7 --- /dev/null +++ b/benchmark/evaluators/accuracy.py @@ -0,0 +1,75 @@ +import re + +def calculate_accuracy(extracted: any, ground_truth: any) -> float: + """ + Computes a score from 0.0 to 1.0 representing accuracy. + Handles nested dicts, lists, booleans, and string fuzzy matching. + """ + if type(extracted) != type(ground_truth): + # Allow string representations of booleans/numbers + if isinstance(ground_truth, bool) and isinstance(extracted, str): + extracted = extracted.lower() in ("true", "1", "yes") + elif isinstance(ground_truth, (int, float)) and isinstance(extracted, str): + try: + extracted = float(extracted) if isinstance(ground_truth, float) else int(extracted) + except ValueError: + return 0.0 + else: + return 0.0 + + if isinstance(ground_truth, bool): + return 1.0 if extracted == ground_truth else 0.0 + + if isinstance(ground_truth, (int, float)): + return 1.0 if extracted == ground_truth else 0.0 + + if isinstance(ground_truth, str): + return fuzzy_string_similarity(extracted, ground_truth) + + if isinstance(ground_truth, dict): + if not ground_truth: + return 1.0 + total_score = 0.0 + keys = ground_truth.keys() + for k in keys: + if k in extracted: + total_score += calculate_accuracy(extracted[k], ground_truth[k]) + return total_score / len(keys) + + if isinstance(ground_truth, list): + if not ground_truth: + return 1.0 if not extracted else 0.0 + # Compute match score for list items + matched_scores = [] + temp_extracted = list(extracted) + for gt_item in ground_truth: + best_match = 0.0 + best_idx = -1 + for idx, ext_item in enumerate(temp_extracted): + score = calculate_accuracy(ext_item, gt_item) + if score > best_match: + best_match = score + best_idx = idx + matched_scores.append(best_match) + if best_idx != -1: + temp_extracted.pop(best_idx) + return sum(matched_scores) / len(ground_truth) + + return 0.0 + + +def fuzzy_string_similarity(s1: str, s2: str) -> float: + """ + Computes a simple token-based overlap similarity score between 0.0 and 1.0. + """ + s1_clean = set(re.findall(r'\w+', s1.lower())) + s2_clean = set(re.findall(r'\w+', s2.lower())) + + if not s1_clean or not s2_clean: + return 1.0 if s1_clean == s2_clean else 0.0 + + intersection = s1_clean.intersection(s2_clean) + union = s1_clean.union(s2_clean) + + # Jaccard index + return len(intersection) / len(union) diff --git a/benchmark/pipelines/base.py b/benchmark/pipelines/base.py new file mode 100644 index 00000000..80367534 --- /dev/null +++ b/benchmark/pipelines/base.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +from abc import ABC, abstractmethod + +@dataclass +class PipelineExtractionOutput: + """Output structure for extraction pipelines.""" + extracted_fields: dict[str, any] + field_confidence: dict[str, float] + latency_seconds: float + + +class BasePipeline(ABC): + """Base class for all extraction pipelines.""" + + @abstractmethod + def run(self, narrative: str, template_schema: dict) -> PipelineExtractionOutput: + pass \ No newline at end of file diff --git a/benchmark/pipelines/pipeline.py b/benchmark/pipelines/pipeline.py new file mode 100644 index 00000000..7abfed4c --- /dev/null +++ b/benchmark/pipelines/pipeline.py @@ -0,0 +1,53 @@ +import time +from benchmark.pipelines.base import BasePipeline, PipelineExtractionOutput + +from app.api.routes import create_template, wait_for_job_completion, fill_form +from app.core.config import OLLAMA_MODEL + +class Pipeline(BasePipeline): + def run(self, narrative: str, template_schema: dict) -> PipelineExtractionOutput: + ''' + # Create a unique template ID + template_id = f"benchmark_template_{int(time.time())}" + + # Create the template (this is synchronous) + template_response = create_template( + name=template_id, + description="Benchmark template", + pdf_path="/tmp/empty.pdf", + fields=template_schema + ) + + if not template_response: + raise RuntimeError("Failed to create template") + + # Submit the form fill (this is asynchronous) + form_response = fill_form( + template_id=template_response.id, + input_text=narrative, + model=OLLAMA_MODEL, + api_key="benchmark-key" + ) + + if not form_response: + raise RuntimeError("Failed to submit form fill") + + # Wait for the job to complete (synchronous waiting) + job_id = form_response.job_id + result = wait_for_job_completion(job_id) + + if not result: + raise RuntimeError("Job did not complete") + + # Extract the output + extracted_fields = result.extracted_fields + field_confidence = result.field_confidence + latency_seconds = result.latency_seconds + + return PipelineExtractionOutput( + extracted_fields=extracted_fields, + field_confidence=field_confidence, + latency_seconds=latency_seconds + ) + ''' + pass diff --git a/benchmark/runners/runner.py b/benchmark/runners/runner.py new file mode 100644 index 00000000..d6da6c35 --- /dev/null +++ b/benchmark/runners/runner.py @@ -0,0 +1,86 @@ +import time +import json +import os +from benchmark.evaluators.accuracy import calculate_accuracy + +class Runner: + def __init__(self, pipeline_class, pipeline_name: str): + self.pipeline = pipeline_class() + self.pipeline_name = pipeline_name + + def run_benchmark(self) -> dict[str, any]: + datasets_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "datasets") + narratives_dir = os.path.join(datasets_dir, "narratives") + ground_truth_dir = os.path.join(datasets_dir, "ground_truth") + templates_dir = os.path.join(datasets_dir, "templates") + + results = [] + total_latency = 0.0 + + # Scan narratives directory to find matching ground_truth and template files + narrative_files = [f for f in os.listdir(narratives_dir) if f.endswith(".txt")] + + for narrative_file in sorted(narrative_files): + case_name = os.path.splitext(narrative_file)[0] + # Match ics201_1.txt -> ics201_1.json or ics201.json + # Find matching ground truth file + gt_name = case_name.split("_")[0] + "_1.json" # default fallback + possible_gts = [case_name + ".json", case_name.split("_")[0] + "_1.json"] + gt_file = None + for p in possible_gts: + if os.path.exists(os.path.join(ground_truth_dir, p)): + gt_file = p + break + + # Find matching template file + # e.g., ics201_1 -> ics_201.json + base_form_name = case_name.split("_")[0] + # convert ics201 to ics_201 if needed + if "ics" in base_form_name and "_" not in base_form_name: + base_form_name = "ics_" + base_form_name[3:] + template_file = f"{base_form_name}.json" + + narrative_path = os.path.join(narratives_dir, narrative_file) + gt_path = os.path.join(ground_truth_dir, gt_file) if gt_file else "" + template_path = os.path.join(templates_dir, template_file) + + if not os.path.exists(gt_path) or not os.path.exists(template_path): + continue + + with open(narrative_path, "r") as f: + narrative_text = f.read() + + with open(gt_path, "r") as f: + gt_data = json.load(f) + # Unwrap the outer wrapper key if present + gt_key = list(gt_data.keys())[0] + gt_content = gt_data[gt_key] + + with open(template_path, "r") as f: + template_schema = json.load(f) + + start_time = time.time() + output = self.pipeline.run(narrative_text, template_schema) + latency = time.time() - start_time + total_latency += latency + + accuracy = calculate_accuracy(output.extracted_fields, gt_content) + + results.append({ + "case_id": case_name, + "latency_seconds": latency, + "accuracy_score": accuracy, + "extracted_fields": output.extracted_fields, + "ground_truth": gt_content + }) + + avg_accuracy = sum(r["accuracy_score"] for r in results) / len(results) if results else 0.0 + + return { + "pipeline_name": self.pipeline_name, + "metrics": { + "total_latency_seconds": total_latency, + "average_accuracy": avg_accuracy + }, + "results": results + } diff --git a/benchmark/test_benchmark.py b/benchmark/test_benchmark.py new file mode 100644 index 00000000..44262287 --- /dev/null +++ b/benchmark/test_benchmark.py @@ -0,0 +1,26 @@ +import os +import json +from datetime import datetime +from benchmark.pipelines.pipeline import Pipeline +from benchmark.runners.runner import Runner + +def test_pipeline_execution(): + """ + Standard test executor that finds the available Pipeline class, + runs the benchmark dataset, writes execution results to a file, and asserts accuracy. + """ + # Pipeline name contains current date and hour, minute and second (e.g. Pipeline_2026-07-08_12h_12m_12s) + timestamp = datetime.now().strftime("%Y-%m-%d_%Hh_%Mm_%Ss") + pipeline_name = f"Pipeline_{timestamp}" + + runner = Runner(Pipeline, pipeline_name) + report = runner.run_benchmark() + + # Save results to a report file to be compared in CI/CD pipeline + report_path = os.path.join(os.path.dirname(__file__), "benchmark_report.json") + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + + # Assert basic quality sanity check + assert report["metrics"]["average_accuracy"] >= 0.0 + print(f"\n{pipeline_name} evaluation complete. Average Accuracy: {report['metrics']['average_accuracy']:.2f}") From 91fd49640d2508d15f2bbb965f3435c52f8e557a Mon Sep 17 00:00:00 2001 From: marcvergees Date: Mon, 3 Aug 2026 18:17:21 +0200 Subject: [PATCH 71/85] fix: :bug: fixing test passing with empty structure of everything --- benchmark/__init__.py | 0 benchmark/benchmark_report.json | 8 ++++---- benchmark/datasets/__init__.py | 0 benchmark/evaluators/__init__.py | 0 benchmark/pipelines/__init__.py | 0 benchmark/pipelines/pipeline.py | 8 +++++--- benchmark/runners/__init__.py | 0 7 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 benchmark/__init__.py create mode 100644 benchmark/datasets/__init__.py create mode 100644 benchmark/evaluators/__init__.py create mode 100644 benchmark/pipelines/__init__.py create mode 100644 benchmark/runners/__init__.py diff --git a/benchmark/__init__.py b/benchmark/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/benchmark_report.json b/benchmark/benchmark_report.json index 59940265..a93e32aa 100644 --- a/benchmark/benchmark_report.json +++ b/benchmark/benchmark_report.json @@ -1,13 +1,13 @@ { - "pipeline_name": "Pipeline_2026-07-08_13h_09m_20s", + "pipeline_name": "Pipeline_2026-08-03_18h_16m_49s", "metrics": { - "total_latency_seconds": 2.1457672119140625e-06, + "total_latency_seconds": 1.1920928955078125e-06, "average_accuracy": 0.0 }, "results": [ { "case_id": "ics201_1", - "latency_seconds": 9.5367431640625e-07, + "latency_seconds": 1.1920928955078125e-06, "accuracy_score": 0.0, "extracted_fields": {}, "ground_truth": { @@ -185,7 +185,7 @@ }, { "case_id": "ics202_1", - "latency_seconds": 1.1920928955078125e-06, + "latency_seconds": 0.0, "accuracy_score": 0.0, "extracted_fields": {}, "ground_truth": { diff --git a/benchmark/datasets/__init__.py b/benchmark/datasets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/evaluators/__init__.py b/benchmark/evaluators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/pipelines/__init__.py b/benchmark/pipelines/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/pipelines/pipeline.py b/benchmark/pipelines/pipeline.py index 7abfed4c..3387f7ef 100644 --- a/benchmark/pipelines/pipeline.py +++ b/benchmark/pipelines/pipeline.py @@ -1,8 +1,6 @@ import time from benchmark.pipelines.base import BasePipeline, PipelineExtractionOutput -from app.api.routes import create_template, wait_for_job_completion, fill_form -from app.core.config import OLLAMA_MODEL class Pipeline(BasePipeline): def run(self, narrative: str, template_schema: dict) -> PipelineExtractionOutput: @@ -50,4 +48,8 @@ def run(self, narrative: str, template_schema: dict) -> PipelineExtractionOutput latency_seconds=latency_seconds ) ''' - pass + return PipelineExtractionOutput( + extracted_fields={}, + field_confidence={}, + latency_seconds=0.0 + ) diff --git a/benchmark/runners/__init__.py b/benchmark/runners/__init__.py new file mode 100644 index 00000000..e69de29b From 4927d9a9cfee7a5f2cd0a4d3a73dc94211568df4 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Mon, 3 Aug 2026 18:22:49 +0200 Subject: [PATCH 72/85] style: :lipstick: adding notes for guys --- benchmark/pipelines/pipeline.py | 55 ++++++++------------------------- 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/benchmark/pipelines/pipeline.py b/benchmark/pipelines/pipeline.py index 3387f7ef..f6883d5e 100644 --- a/benchmark/pipelines/pipeline.py +++ b/benchmark/pipelines/pipeline.py @@ -5,48 +5,19 @@ class Pipeline(BasePipeline): def run(self, narrative: str, template_schema: dict) -> PipelineExtractionOutput: ''' - # Create a unique template ID - template_id = f"benchmark_template_{int(time.time())}" - - # Create the template (this is synchronous) - template_response = create_template( - name=template_id, - description="Benchmark template", - pdf_path="/tmp/empty.pdf", - fields=template_schema - ) - - if not template_response: - raise RuntimeError("Failed to create template") - - # Submit the form fill (this is asynchronous) - form_response = fill_form( - template_id=template_response.id, - input_text=narrative, - model=OLLAMA_MODEL, - api_key="benchmark-key" - ) - - if not form_response: - raise RuntimeError("Failed to submit form fill") - - # Wait for the job to complete (synchronous waiting) - job_id = form_response.job_id - result = wait_for_job_completion(job_id) - - if not result: - raise RuntimeError("Job did not complete") - - # Extract the output - extracted_fields = result.extracted_fields - field_confidence = result.field_confidence - latency_seconds = result.latency_seconds - - return PipelineExtractionOutput( - extracted_fields=extracted_fields, - field_confidence=field_confidence, - latency_seconds=latency_seconds - ) + You guys should here implement the whole pipeline implementation depending on approach B or C. + + Expected behavior: + - Create and fill templates, then wait for jobs to complete. + - Return extracted fields, confidence scores, and latency. + + Example of a single step: + >>> template_id = create_template(...) + >>> form_response = fill_form(...) + >>> result = wait_for_job_completion(form_response.job_id) + >>> extracted = result.extracted_fields + >>> confidence = result.field_confidence + >>> latency = result.latency_seconds ''' return PipelineExtractionOutput( extracted_fields={}, From 73a382cfdc67a2b71ac381690d17059cb3384fb2 Mon Sep 17 00:00:00 2001 From: vharkins <105030530+vharkins1@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:38:10 -0700 Subject: [PATCH 73/85] feat: add large-model reference evaluator --- .../reference_candidates/ics201_1.json | 172 ++++++++++++++++++ .../reference_candidates/ics202_1.json | 47 +++++ benchmark/evaluators/reference_accuracy.py | 78 ++++++++ 3 files changed, 297 insertions(+) create mode 100644 benchmark/datasets/reference_candidates/ics201_1.json create mode 100644 benchmark/datasets/reference_candidates/ics202_1.json create mode 100644 benchmark/evaluators/reference_accuracy.py diff --git a/benchmark/datasets/reference_candidates/ics201_1.json b/benchmark/datasets/reference_candidates/ics201_1.json new file mode 100644 index 00000000..ca27b752 --- /dev/null +++ b/benchmark/datasets/reference_candidates/ics201_1.json @@ -0,0 +1,172 @@ +{ + "1_incident_name": "Whispering Pines Derailment", + "2_incident_number": "US-EPA-R4-2026-0707", + "3_date_time_initiated": { + "date": "July 07, 2026", + "time": "06:30" + }, + "4_map_sketch": { + "total_area_of_operations": "1.5-mile radius centered around the intersection of CSX Rail Milepost 142.5 and Highway 411", + "incident_site_area": "CSX Rail Milepost 142.5, where seven freight cars (#12 through #18) have jackknifed", + "impacted_areas": "Car #14 is releasing Benzene into Whispering Pines Creek at approximately 20 gallons per minute, with visible surface sheening extending 500 meters downstream. Elevated volatile organic compounds are present within a 300-foot downwind plume tracking East-Southeast at 5 miles per hour.", + "threatened_areas": "Whispering Pines Nature Reserve Wetlands one mile downstream, the Municipal Water Intake two miles downstream, and an intact liquefied petroleum gas tank car threatened by the adjacent brush fire", + "overflight_results": "", + "trajectories": "The downwind plume is tracking East-Southeast at 5 miles per hour. Waterborne contamination is moving East at 1.5 knots.", + "impacted_shorelines": "Whispering Pines Creek, with visible surface sheening extending 500 meters downstream", + "graphics_and_symbology": "North is oriented at the top of the page. County Line Road is closed to public traffic, and the Staging Area is at the County Fairgrounds." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "The derailment occurred at 05:45 hours due to a mechanical rail car failure. Seven freight cars jackknifed, and breached tank Car #14 is releasing Benzene into Whispering Pines Creek at an estimated 20 gallons per minute. A secondary 0.5-acre brush fire threatens an adjacent intact liquefied petroleum gas tank car, and law enforcement is executing a mandatory 0.5-mile residential evacuation.", + "health_and_safety_hazards": "Toxic Benzene inhalation, dermal exposure, flash fires from pooling product, heavy machinery movement around compromised rail lines, responder heat stress in 88°F weather, and slip-and-trip hazards along steep creek banks.", + "necessary_measures": "Maintain a 300-foot Exclusion Zone around the rail cars. Hot Zone containment responders must wear Level B personal protective equipment, including self-contained breathing apparatus and chemical-resistant clothing. Level C protection is authorized for warm zone booming teams only when continuous photoionization detector monitoring confirms volatile organic compound levels below 1 part per million. Firefighters must wear full structural turnout gear with self-contained breathing apparatus. All personnel must pass through the formal wet decontamination corridor at the Warm-to-Cold zone interface before leaving the operational area." + }, + "6_prepared_by": { + "name": "Marcus Vance", + "position_title": "Initial Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "July 07, 2026, at 08:00 hours" + }, + "7_current_and_planned_objectives": [ + "Ensure the life safety of all emergency responders and the public within the perimeter by 09:00 hours.", + "Complete the 0.5-mile downwind residential evacuation by 09:30 hours.", + "Suppress the brush fire near the liquefied petroleum gas car to eliminate thermal risks by 10:00 hours.", + "Deploy effective deflection and underflow containment booming across the creek to protect the water intake.", + "Establish a Unified Command structure by 10:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "06:00", + "actions": "County Fire and Sheriff units were dispatched, and a 500-foot isolation perimeter was established." + }, + { + "time": "06:15", + "actions": "Local Fire Chief Thomas established command and notified the State Department of Environmental Quality, the Environmental Protection Agency, and CSX Railroad." + }, + { + "time": "06:45", + "actions": "The Sheriff's Department initiated door-to-door evacuations of 45 homes." + }, + { + "time": "07:15", + "actions": "Regional Hazmat Team 1 arrived to deploy an air-monitoring grid and inspect the damaged tanks." + }, + { + "time": "07:30", + "actions": "Engine 41 and Tanker 5 initiated defensive foam applications on the brush fire." + }, + { + "time": "07:45", + "actions": "Advance teams from the state environmental agency and the railroad arrived to finalize the water-containment strategy." + }, + { + "time": "08:15", + "actions": "Planned: deploy a specialized spill contractor to place 500 feet of deflection boom at Boom Site 1." + }, + { + "time": "08:45", + "actions": "Planned: launch a drone overflight to map downstream tracking." + }, + { + "time": "09:30", + "actions": "Planned: complete the transition to a fully unified command at the Mobile Command Post." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief J. Thomas, County Fire", + "S. Albright, State Department of Environmental Quality", + "D. Miller, CSX Railroad Responsible Party" + ], + "safety_officer": "Captain R. Mendez", + "public_information_officer": "A. Cho, County", + "liaison_officer": "Inspector G. Sims, Sheriff's Office", + "operations_section_chief": "B. Reynolds, State Department of Environmental Quality", + "planning_section_chief": "Marcus Vance", + "logistics_section_chief": "K. Dunavan", + "finance_administration_section_chief": "", + "additional_positions": [ + { + "position": "Hazardous Materials Group Supervisor", + "name": "Lieutenant T. Kincaid, Regional Hazmat Team 1" + }, + { + "position": "Fire Suppression Division Commander", + "name": "Battalion Chief E. Walters" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Type 1 Hazardous Materials Unit", + "resource_identifier": "Regional Hazmat Team 1", + "date_time_ordered": "July 07, 2026, at 06:00 hours", + "eta": "", + "arrived": true, + "notes": "Arrived at 07:00 hours and is positioned at the Cold Zone boundary conducting perimeter monitoring and plugging operations." + }, + { + "resource": "Fire Engine", + "resource_identifier": "County Engine 41", + "date_time_ordered": "July 07, 2026, at 05:48 hours", + "eta": "", + "arrived": true, + "notes": "Arrived at 06:00 hours and is actively suppressing the brush fire." + }, + { + "resource": "Fire Engine", + "resource_identifier": "County Engine 45", + "date_time_ordered": "July 07, 2026, at 05:48 hours", + "eta": "", + "arrived": true, + "notes": "Arrived at 06:05 hours and manages critical water supply lines." + }, + { + "resource": "Water Tender", + "resource_identifier": "County Tanker 5", + "date_time_ordered": "July 07, 2026, at 06:02 hours", + "eta": "", + "arrived": true, + "notes": "Arrived at 06:20 hours and provides continuous flow for foam application." + }, + { + "resource": "Law Enforcement Units", + "resource_identifier": "County Sheriff (4 units)", + "date_time_ordered": "July 07, 2026, at 05:48 hours", + "eta": "", + "arrived": true, + "notes": "Arrived at 05:55 hours and are operating traffic checkpoints and roadblocks on Creek Road while supporting evacuations." + }, + { + "resource": "Emergency Medical Services Unit", + "resource_identifier": "County EMS Unit 12", + "date_time_ordered": "July 07, 2026, at 06:00 hours", + "eta": "", + "arrived": true, + "notes": "Arrived at 06:12 hours and is standing by at the Staging Area for responder rehabilitation and medical backup." + }, + { + "resource": "Spill Response Team", + "resource_identifier": "HEPACO Spill Team A", + "date_time_ordered": "July 07, 2026, at 06:45 hours", + "eta": "July 07, 2026, at 08:15 hours", + "arrived": false, + "notes": "En route to deploy 1,000 feet of containment boom and vacuum trucks." + }, + { + "resource": "Drone Unit", + "resource_identifier": "State DEQ Drone Unit 1", + "date_time_ordered": "July 07, 2026, at 07:10 hours", + "eta": "July 07, 2026, at 08:30 hours", + "arrived": false, + "notes": "En route to provide aerial thermal imagery." + }, + { + "resource": "Vacuum Truck", + "resource_identifier": "CSX contractor TechRad", + "date_time_ordered": "July 07, 2026, at 07:15 hours", + "eta": "July 07, 2026, at 09:30 hours", + "arrived": false, + "notes": "En route to begin product offloading from the breached tank car once the scene stabilizes." + } + ] +} diff --git a/benchmark/datasets/reference_candidates/ics202_1.json b/benchmark/datasets/reference_candidates/ics202_1.json new file mode 100644 index 00000000..32edc668 --- /dev/null +++ b/benchmark/datasets/reference_candidates/ics202_1.json @@ -0,0 +1,47 @@ +{ + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "July 07, 2026", + "date_to": "July 08, 2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the night shift.", + "Deploy an additional 500 feet of underflow and sorbent booming at designated downstream checkpoints on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen.", + "Complete the structural stabilization and grounding of breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading.", + "Establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours.", + "Finalize the morning resource allocation and shift rotation plan by 04:00 hours." + ], + "4_operational_period_command_emphasis": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Prioritize the safety of hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery.", + "general_situational_awareness": "Clearing skies are forecast, with the ambient temperature dropping to 62°F by midnight and winds shifting from the Northwest at 3 to 5 miles per hour. Heavy Benzene vapors may accumulate in low-lying drainage ditches and creek pockets east of the derailment site. Remain vigilant for low-visibility hazards, uneven muddy terrain along the banks, and wildlife hazards in the adjacent wetlands.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Primary Mobile Command Post inside the Forward Operations Briefing Trailer", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "EPA Plume Trajectory Analysis Sheet", + "CSX Tank Car Cargo Manifest" + ] + }, + "7_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "July 07, 2026, at 16:30 hours" + }, + "8_approved_by_incident_commander": { + "name": "Chief J. Thomas of County Fire", + "signature": "Chief J. Thomas", + "date_time": "July 07, 2026, at 17:00 hours" + }, + "iap_page_number": "1" +} diff --git a/benchmark/evaluators/reference_accuracy.py b/benchmark/evaluators/reference_accuracy.py new file mode 100644 index 00000000..a8a608c5 --- /dev/null +++ b/benchmark/evaluators/reference_accuracy.py @@ -0,0 +1,78 @@ +import hashlib +import json +import os + +import requests + +from benchmark.evaluators.accuracy import calculate_accuracy + + +PROMPT_VERSION = "1" +SYSTEM_PROMPT = """You create reference answers for extraction benchmarks. +Use only facts supported by the narrative, preserve source wording when practical, +and return only JSON matching the supplied template. Do not add fields.""" + + +def calculate_reference_accuracy( + extracted: dict, narrative: str, template: dict, cache_path: str +) -> tuple[float, dict]: + """Score an on-device extraction against a cached large-model reference.""" + model = os.environ["BENCHMARK_REFERENCE_MODEL"] + input_hash = hashlib.sha256( + ( + narrative + + json.dumps(template, sort_keys=True) + + model + + PROMPT_VERSION + ).encode() + ).hexdigest() + + if os.path.exists(cache_path): + with open(cache_path) as file: + cached = json.load(file) + if cached["metadata"]["input_hash"] == input_hash: + reference = cached["reference"] + return calculate_accuracy(extracted, reference), reference + + headers = {"Content-Type": "application/json"} + if api_key := os.getenv("BENCHMARK_REFERENCE_API_KEY"): + headers["Authorization"] = f"Bearer {api_key}" + + response = requests.post( + os.environ["BENCHMARK_REFERENCE_URL"], + headers=headers, + json={ + "model": model, + "temperature": 0, + "response_format": {"type": "json_object"}, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + { + "role": "user", + "content": json.dumps( + {"template": template, "narrative": narrative} + ), + }, + ], + }, + timeout=int(os.getenv("BENCHMARK_REFERENCE_TIMEOUT", "120")), + ) + response.raise_for_status() + reference = json.loads(response.json()["choices"][0]["message"]["content"]) + + os.makedirs(os.path.dirname(cache_path), exist_ok=True) + with open(cache_path, "w") as file: + json.dump( + { + "metadata": { + "model": model, + "prompt_version": PROMPT_VERSION, + "input_hash": input_hash, + }, + "reference": reference, + }, + file, + indent=2, + ) + + return calculate_accuracy(extracted, reference), reference From 684ebfa8075633b8f6edda819391534d51f2eadc Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 16:46:32 +0200 Subject: [PATCH 74/85] ics201 & 202 --- benchmark/datasets/ground_truth/ics201_2.json | 139 +++++++++++++++++ benchmark/datasets/ground_truth/ics201_3.json | 139 +++++++++++++++++ benchmark/datasets/ground_truth/ics201_4.json | 145 ++++++++++++++++++ benchmark/datasets/ground_truth/ics201_5.json | 145 ++++++++++++++++++ benchmark/datasets/ground_truth/ics201_6.json | 144 +++++++++++++++++ benchmark/datasets/ground_truth/ics201_7.json | 145 ++++++++++++++++++ benchmark/datasets/ground_truth/ics201_8.json | 145 ++++++++++++++++++ benchmark/datasets/ground_truth/ics201_9.json | 145 ++++++++++++++++++ benchmark/datasets/ground_truth/ics202_2.json | 49 ++++++ benchmark/datasets/ground_truth/ics202_3.json | 49 ++++++ benchmark/datasets/ground_truth/ics202_4.json | 49 ++++++ benchmark/datasets/ground_truth/ics202_5.json | 49 ++++++ benchmark/datasets/ground_truth/ics202_6.json | 49 ++++++ benchmark/datasets/narratives/ics201_2.txt | 24 +++ benchmark/datasets/narratives/ics201_3.txt | 29 ++++ benchmark/datasets/narratives/ics201_4.txt | 11 ++ benchmark/datasets/narratives/ics201_5.txt | 11 ++ benchmark/datasets/narratives/ics201_6.txt | 11 ++ benchmark/datasets/narratives/ics201_7.txt | 11 ++ benchmark/datasets/narratives/ics201_8.txt | 11 ++ benchmark/datasets/narratives/ics201_9.txt | 11 ++ benchmark/datasets/narratives/ics202_2.txt | 9 ++ benchmark/datasets/narratives/ics202_3.txt | 9 ++ benchmark/datasets/narratives/ics202_4.txt | 9 ++ benchmark/datasets/narratives/ics202_5.txt | 9 ++ benchmark/datasets/narratives/ics202_6.txt | 9 ++ 26 files changed, 1556 insertions(+) create mode 100644 benchmark/datasets/ground_truth/ics201_2.json create mode 100644 benchmark/datasets/ground_truth/ics201_3.json create mode 100644 benchmark/datasets/ground_truth/ics201_4.json create mode 100644 benchmark/datasets/ground_truth/ics201_5.json create mode 100644 benchmark/datasets/ground_truth/ics201_6.json create mode 100644 benchmark/datasets/ground_truth/ics201_7.json create mode 100644 benchmark/datasets/ground_truth/ics201_8.json create mode 100644 benchmark/datasets/ground_truth/ics201_9.json create mode 100644 benchmark/datasets/ground_truth/ics202_2.json create mode 100644 benchmark/datasets/ground_truth/ics202_3.json create mode 100644 benchmark/datasets/ground_truth/ics202_4.json create mode 100644 benchmark/datasets/ground_truth/ics202_5.json create mode 100644 benchmark/datasets/ground_truth/ics202_6.json create mode 100644 benchmark/datasets/narratives/ics201_2.txt create mode 100644 benchmark/datasets/narratives/ics201_3.txt create mode 100644 benchmark/datasets/narratives/ics201_4.txt create mode 100644 benchmark/datasets/narratives/ics201_5.txt create mode 100644 benchmark/datasets/narratives/ics201_6.txt create mode 100644 benchmark/datasets/narratives/ics201_7.txt create mode 100644 benchmark/datasets/narratives/ics201_8.txt create mode 100644 benchmark/datasets/narratives/ics201_9.txt create mode 100644 benchmark/datasets/narratives/ics202_2.txt create mode 100644 benchmark/datasets/narratives/ics202_3.txt create mode 100644 benchmark/datasets/narratives/ics202_4.txt create mode 100644 benchmark/datasets/narratives/ics202_5.txt create mode 100644 benchmark/datasets/narratives/ics202_6.txt diff --git a/benchmark/datasets/ground_truth/ics201_2.json b/benchmark/datasets/ground_truth/ics201_2.json new file mode 100644 index 00000000..403dcadc --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_2.json @@ -0,0 +1,139 @@ +{ + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_incident_number": "US-EPA-R5-2026-0813", + "3_date_time_initiated": { + "date": "August 13, 2026", + "time": "07:15 hours" + }, + "4_map_sketch": { + "total_area_of_operations": "3.0-mile radius around the Blackwood Industrial Corridor", + "incident_site_area": "Pipeline Valve Station 14-B along Blackwood River Road", + "impacted_areas": "150-foot radius surrounding the breached 8-inch pressurized line releasing anhydrous ammonia gas", + "threatened_areas": "Oakridge Residential Subdivision 0.75 miles downwind to the East; Valley Water Authority Municipal Intake Facility 1.5 miles downstream", + "overflight_results": "Dense, low-hanging vapor cloud hugging the ground and moving steadily downwind, along with a localized surface sheen on the riverbank", + "trajectories": "Toxic vapor plume moving East-Northeast at 6 mph; waterborne chemical runoff traveling downstream at approximately 2.0 knots", + "impacted_shorelines": "1,000 yards down the adjacent Blackwood River", + "graphics_and_symbology": "Standard graphics and GIS symbology, oriented with North at top" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "Third-party excavation crew accidentally punctured the main 8-inch transfer pipeline at 06:50 hours, causing an uncontrolled high-pressure release of hazardous anhydrous ammonia gas.", + "health_and_safety_hazards": "Severe atmospheric exposure to anhydrous ammonia vapor, cryogenic freeze burns upon direct contact with liquid leakage, respiratory injury, structural eye damage, responder heat stress in 85°F temperatures, and slip-and-fall risks near steep river embankment slopes.", + "necessary_measures": "Immediate establishment of a 500-foot Exclusion Zone, mandatory Level A vapor-tight PPE with SCBA for Hot Zone entry, continuous PID air monitoring at control perimeters, water curtain deployment to knock down vapor clouds, and compulsory full-body chemical decontamination at the Warm-to-Cold zone boundary prior to site exit." + }, + "6_prepared_by": { + "name": "Sarah Jenkins", + "position_title": "Initial Planning Section Chief", + "signature": "Sarah Jenkins", + "date_time": "August 13, 2026, at 08:30 hours" + }, + "7_current_and_planned_objectives": [ + "Achieve complete isolation of the pipeline segment by closing upstream valves by 09:30 hours.", + "Finish the sheltering-in-place order and partial evacuation of 120 homes in the downwind Oakridge subdivision by 10:00 hours.", + "Deploy absorbent and containment booming across the Blackwood River above the municipal water intake by 10:30 hours.", + "Fully establish a Multi-Agency Unified Command at the Regional Emergency Operations Center by 11:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "07:00 hours", + "actions": "Initial dispatch sent Blackwood Fire Department and Local Sheriff units to establish an outer safety perimeter." + }, + { + "time": "07:20 hours", + "actions": "Fire Chief R. Vance established Incident Command and ordered an immediate downwind shelter-in-place alert." + }, + { + "time": "07:45 hours", + "actions": "Hazmat Team 5 arrived on scene to conduct initial perimeter air monitoring and assist in establishing control zones." + }, + { + "time": "08:15 hours", + "actions": "Pipeline technicians confirmed valve isolation procedures were initiated, while fire crews set up unmanned monitor nozzles to disperse airborne vapors." + }, + { + "time": "09:00 hours", + "actions": "Deploy the Regional Spill Response Team to launch containment boom at River Boom Site Alpha." + }, + { + "time": "09:30 hours", + "actions": "Conduct a secondary drone air sampling flight." + }, + { + "time": "10:00 hours", + "actions": "Finalize the shift to a Unified Command structure at the Regional Emergency Operations Center." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief R. Vance (Blackwood Fire Department)", + "Officer M. Ross (State Environmental Protection Agency)", + "J. Vance (Blackwood Pipeline Co., Responsible Party)" + ], + "safety_officer": "Captain L. Hayes", + "public_information_officer": "E. Wright", + "liaison_officer": "Deputy K. Miller", + "operations_section_chief": "D. Kowalski", + "planning_section_chief": "Sarah Jenkins", + "logistics_section_chief": "T. Bradley", + "finance_administration_section_chief": "A. Patel", + "additional_positions": [ + { + "position": "Hazmat Group Supervisor", + "name": "Lieutenant C. Webb" + }, + { + "position": "Air Monitoring Specialist", + "name": "Dr. H. Thorne" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Hazmat Team 5", + "resource_identifier": "HZM-05", + "date_time_ordered": "August 13, 2026, 07:00 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Executing air monitoring and entry ops at the Hot Zone perimeter." + }, + { + "resource": "Blackwood Engine 12", + "resource_identifier": "ENG-12", + "date_time_ordered": "August 13, 2026, 06:55 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Actively operating water curtains for vapor suppression." + }, + { + "resource": "County Water Tender 3", + "resource_identifier": "WT-03", + "date_time_ordered": "August 13, 2026, 07:10 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Supplying continuous water flow to Engine 12." + }, + { + "resource": "Sheriff Patrol Unit Group Alpha", + "resource_identifier": "SO-ALPHA", + "date_time_ordered": "August 13, 2026, 06:55 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Maintaining road closures and executing downwind evacuation notices." + }, + { + "resource": "CleanHarbor Spill Response Team", + "resource_identifier": "CH-SRT-1", + "date_time_ordered": "August 13, 2026, 07:30 hours", + "eta": "09:00 hours", + "arrived": false, + "notes": "En route with 1,500 feet of river containment boom and vacuum recovery equipment." + }, + { + "resource": "State EPA Air Monitoring Drone Unit", + "resource_identifier": "EPA-DRONE-2", + "date_time_ordered": "August 13, 2026, 07:40 hours", + "eta": "08:45 hours", + "arrived": false, + "notes": "En route to provide real-time thermal and chemical plume mapping." + } + ] +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics201_3.json b/benchmark/datasets/ground_truth/ics201_3.json new file mode 100644 index 00000000..e80a5e49 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_3.json @@ -0,0 +1,139 @@ +{ + "1_incident_name": "Cypress Bend Tanker Collision", + "2_incident_number": "US-CG-D8-2026-0921", + "3_date_time_initiated": { + "date": "September 21, 2026", + "time": "14:15 hours" + }, + "4_map_sketch": { + "total_area_of_operations": "4.0-mile radius centered at Intracoastal Waterway Mile Marker 245", + "incident_site_area": "Confluence of the Intracoastal Waterway and Cypress Bayou", + "impacted_areas": "300-yard containment zone surrounding a punctured double-hulled barge discharging Heavy Fuel Oil (HFO 380)", + "threatened_areas": "Grand Cypress Mangrove Sanctuary 2.0 miles south-southeast; Delta Commercial Oyster Leases 3.5 miles down-river", + "overflight_results": "Surface slick measuring 2.0 miles long by 100 yards wide moving with the ebb tide, with visible heavy sheen accumulating along the shoreline", + "trajectories": "Waterborne oil plume moving South-Southeast at 1.8 knots; local coastal winds blowing from Northwest at 12 knots", + "impacted_shorelines": "1.5 miles south along the eastern bank of Cypress Bayou", + "graphics_and_symbology": "Standard navigational charts and GIS symbology, oriented with North at top" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "Tugboat towing two loaded asphalt barges collided with an anchored commercial tanker at 13:50 hours, rupturing Cargo Tank #2 starboard and releasing heavy fuel oil.", + "health_and_safety_hazards": "Hydrogen sulfide gas accumulation, hydrocarbon inhalation, direct dermal exposure, slip-and-fall hazards on oily/muddy surfaces, responder heat exhaustion in 91°F heat, and water drowning risks.", + "necessary_measures": "Establishment of a 1,000-foot Maritime Safety Zone, mandatory Level C PPE with half-mask organic vapor respirators and PFDs for over-water personnel, continuous H2S/PID air monitoring, mandatory work-rest hydration cycles, and vessel-based decontamination station." + }, + "6_prepared_by": { + "name": "Commander Elena Rostova", + "position_title": "Initial Planning Section Chief", + "signature": "Commander Elena Rostova", + "date_time": "September 21, 2026, at 15:30 hours" + }, + "7_current_and_planned_objectives": [ + "Complete primary deflection boom deployment across the mouth of Cypress Bayou by 16:30 hours.", + "Secure mechanical patching or product transfer (lightering) of Cargo Tank #2 by 18:00 hours.", + "Deploy skimming vessels to recover free-floating surface product before nightfall at 19:30 hours.", + "Establish a full Joint Information Center to handle media inquiries by 20:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "14:00 hours", + "actions": "Coast Guard Station Cypress Bend dispatched Sector Patrol Craft and issued a Notice to Mariners closing the waterway." + }, + { + "time": "14:30 hours", + "actions": "Captain M. Thorne established Incident Command and deployed first-responder skimmer boats." + }, + { + "time": "15:00 hours", + "actions": "Port Authority Hazmat Teams completed initial safety sweeps and confirmed zero structural fire threats." + }, + { + "time": "15:30 hours", + "actions": "Marine salvage engineers boarded the barge to inspect damage and prep transfer pumps." + }, + { + "time": "16:00 hours", + "actions": "Drop 2,000 feet of hard boom across the mangrove inlet." + }, + { + "time": "17:00 hours", + "actions": "Initiate lightering operations to pump fuel out of the damaged tank." + }, + { + "time": "18:30 hours", + "actions": "Deploy an evening drone overflight to track thermal slick drift patterns." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain M. Thorne (US Coast Guard)", + "Director D. Sterling (State Department of Environmental Quality)", + "H. Vance (Cypress Towing Co., Responsible Party)" + ], + "safety_officer": "Lieutenant Commander J. Ruiz", + "public_information_officer": "C. Alvarez", + "liaison_officer": "Agent P. Brooks", + "operations_section_chief": "G. Morales", + "planning_section_chief": "Commander Elena Rostova", + "logistics_section_chief": "R. O'Connor", + "finance_administration_section_chief": "M. Lin", + "additional_positions": [ + { + "position": "Salvage & Engineering Group Supervisor", + "name": "Chief Specialist K. Vance" + }, + { + "position": "Wildlife Rescue Leader", + "name": "Dr. A. Mercer" + } + ] + }, + "10_resource_summary": [ + { + "resource": "USCG Marine Safety Detachment 1", + "resource_identifier": "USCG-MSD-01", + "date_time_ordered": "September 21, 2026, 14:00 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Enforcing maritime safety zone and conducting gas monitoring." + }, + { + "resource": "Cypress Port Skimmer Vessel 4", + "resource_identifier": "SKIM-04", + "date_time_ordered": "September 21, 2026, 14:10 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Actively skimming free surface product around the barge stern." + }, + { + "resource": "Delta Salvage Tug 'Warrior'", + "resource_identifier": "TUG-WARRIOR", + "date_time_ordered": "September 21, 2026, 14:30 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Providing stabilizing push-support and powering transfer pumps." + }, + { + "resource": "State DEQ Water Sampling Craft", + "resource_identifier": "DEQ-BOAT-2", + "date_time_ordered": "September 21, 2026, 14:20 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Collecting downstream water samples and turbidity readings." + }, + { + "resource": "National Spill Response Team Barge 8", + "resource_identifier": "NSRT-B-08", + "date_time_ordered": "September 21, 2026, 14:45 hours", + "eta": "16:30 hours", + "arrived": false, + "notes": "En route with 3,000 feet of ocean containment boom and heavy skimmers." + }, + { + "resource": "Gulf Coast Wildlife Rescue Unit", + "resource_identifier": "GC-WILD-1", + "date_time_ordered": "September 21, 2026, 15:10 hours", + "eta": "17:15 hours", + "arrived": false, + "notes": "En route to set up an oily bird stabilization and staging facility." + } + ] +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics201_4.json b/benchmark/datasets/ground_truth/ics201_4.json new file mode 100644 index 00000000..60d81c6e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_4.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Blackwood River Chemical Spill", + "2_incident_number": "USCG-EPA-R4-2026-0813", + "3_date_time_initiated": { + "date": "08/13/2026", + "time": "07:15" + }, + "4_map_sketch": { + "total_area_of_operations": "3.5-mile radius encompassing Blackwood Industrial Park, Mile Marker 42 of the Blackwood River, and State Route 104 corridor.", + "incident_site_area": "Storage Tank #4 at Apex Chemical Tank Farm (MM 42.1, East Bank), involving a structural fracture leaking concentrated Styrene Monomer.", + "impacted_areas": "1,200 meters of Blackwood River surface exhibiting heavy product sheening and a continuous vapor plume extending 800 yards downwind across State Route 104.", + "threatened_areas": "Blackwood Municipal Drinking Water Intake (3 miles downstream at MM 39.1) and Blackwood Marsh Ecological Reserve (1.5 miles downstream).", + "overflight_results": "USCG Aux Drone Flight #1 confirmed a 200-yard wide slick migrating south-southwest at 1.8 knots with moderate shore oiling along the east riverbank.", + "trajectories": "Airborne volatile organic plume tracking East-Northeast at 6 mph; aquatic slick migrating South-Southwest at 1.8 knots downstream.", + "impacted_shorelines": "East bank riprap shoreline and adjacent low-lying mudflats from MM 42.1 to MM 41.3.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area A at Westside High School, Boom Site 1 (Deflection), Boom Site 2 (Containment), Command Post at County Fire Station 12, Exclusion Zone boundary (500 ft radius), and water intake protection zones." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 06:45 EDT, a catastrophic seal failure occurred on Tank #4 at Apex Chemical Facility, releasing approximately 8,500 gallons of liquid Styrene Monomer into secondary containment, with overflow entering Blackwood River via storm drain #3 at an estimated 30 gpm. Secondary vapor cloud formed over Route 104, triggering immediate road closures and evacuation of 60 surrounding industrial properties.", + "health_and_safety_hazards": "Inhalation of toxic styrene vapors (narcotic, central nervous system depressant, respiratory irritant), flammability risk (flash point 88°F), dermal contact hazards, slip/trip/fall hazards along wet river banks, and severe heat stress (ambient temp 91°F).", + "necessary_measures": "Establish 500-foot Exclusion Zone; mandate Level B PPE (SCBA with continuous air monitor PID/LEL/O2) within Hot Zone; Level C PPE (full-face APR with organic vapor cartridges) within Warm Zone; continuous air monitoring required at CP and downwind perimeters; establish wet-decontamination corridor at Gate 2." + }, + "6_prepared_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 09:30" + }, + "7_current_and_planned_objectives": [ + "Maintain life safety and enforce 500-foot exclusion perimeter by 08:00 hours.", + "Complete containment booming at Boom Site 1 (MM 41.5) by 10:30 hours to protect downstream drinking water intake.", + "Secure leak source on Apex Tank #4 and complete product transfer by 12:00 hours.", + "Perform continuous air monitoring downwind along Route 104 and publish safety advisories by 11:00 hours.", + "Transition to full Unified Command (USCG, EPA, State DEQ, County Hazmat) by 10:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "07:15", + "actions": "Initial alarm dispatch; County Fire Engine 12 and Hazmat 1 respond. Initial 500-foot perimeter established." + }, + { + "time": "07:30", + "actions": "Local Command established by Chief Henderson; Apex facility emergency shutdown initiated. Route 104 closed to traffic." + }, + { + "time": "07:55", + "actions": "Regional Hazmat Team 4 arrives; initiates perimeter PID air monitoring and establishes decontamination corridor at Gate 2." + }, + { + "time": "08:20", + "actions": "USCG Sector Strike Team and EPA On-Scene Coordinator arrive on scene; initiate Unified Command setup at Fire Station 12." + }, + { + "time": "08:45", + "actions": "Marine Spill Response Vessel River Guardian deploys 1,000 feet of hard containment boom at MM 41.5 (Boom Site 1)." + }, + { + "time": "09:15", + "actions": "Planned: Entry Team 1 to enter Hot Zone under Level B PPE to secure Tank #4 bottom manifold valve." + }, + { + "time": "10:00", + "actions": "Planned: Deploy secondary sorbent deflection boom array at Boom Site 2 (MM 40.2) upstream of drinking water intake." + }, + { + "time": "11:30", + "actions": "Planned: Vacuum truck operations commence skimmer recovery of pooled styrene at storm drain outfall." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain M. Ross (USCG Unified Command IC)", + "OSC E. Vance (EPA Co-IC)", + "Chief R. Henderson (County Fire IC)", + "J. Mercer (Apex Facility RP IC)" + ], + "safety_officer": "Lt. Commander David Miller", + "public_information_officer": "Elena Gomez (County OEM)", + "liaison_officer": "Captain Arthur Pendelton (State Police)", + "operations_section_chief": "Battalion Chief Marcus Brody", + "planning_section_chief": "Sarah L. Jenkins", + "logistics_section_chief": "Karen Albright", + "finance_administration_section_chief": "Robert Sterling", + "additional_positions": [ + { + "position": "Hazmat Group Supervisor", + "name": "Captain Donald Kross (Hazmat 1)" + }, + { + "position": "Waterborne Ops Branch Director", + "name": "Lt. Timothy Vance (USCG)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Hazmat Unit", + "resource_identifier": "HM-01", + "date_time_ordered": "08/13/2026 07:20", + "eta": "N/A", + "arrived": true, + "notes": "Conducted initial air monitoring; operating Hot Zone entry." + }, + { + "resource": "Fire Engine", + "resource_identifier": "ENG-12", + "date_time_ordered": "08/13/2026 07:15", + "eta": "N/A", + "arrived": true, + "notes": "Securing perimeter and providing fire standby protection." + }, + { + "resource": "USCG Strike Team", + "resource_identifier": "USCG-ST-04", + "date_time_ordered": "08/13/2026 07:40", + "eta": "N/A", + "arrived": true, + "notes": "Assisting with Unified Command and waterborne ops supervision." + }, + { + "resource": "Spill Response Vessel", + "resource_identifier": "RV-RG-02", + "date_time_ordered": "08/13/2026 08:00", + "eta": "N/A", + "arrived": true, + "notes": "Deployed 1,000 ft containment boom at Boom Site 1." + }, + { + "resource": "Vacuum Truck Unit", + "resource_identifier": "VAC-99", + "date_time_ordered": "08/13/2026 08:30", + "eta": "08/13/2026 10:45", + "arrived": false, + "notes": "En route to perform liquid product recovery at outfall." + }, + { + "resource": "Air Monitoring Unit", + "resource_identifier": "AMR-03", + "date_time_ordered": "08/13/2026 08:15", + "eta": "08/13/2026 09:45", + "arrived": false, + "notes": "Transporting high-sensitivity PID and Jerome mercury/VOC analyzers." + } + ] +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics201_5.json b/benchmark/datasets/ground_truth/ics201_5.json new file mode 100644 index 00000000..b99d02e0 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_5.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_incident_number": "USCG-EPA-R6-2026-1014", + "3_date_time_initiated": { + "date": "10/14/2026", + "time": "11:20" + }, + "4_map_sketch": { + "total_area_of_operations": "2.5-mile radius centered around Berth 42 of the Houston Ship Channel Tank Terminal.", + "incident_site_area": "Tanker Berth 42 (MM 51.2, West Bank), where a manifold failure on chemical parcel tanker M/T Stolt Synergy released concentrated 98% sulfuric acid.", + "impacted_areas": "500-yard perimeter along Berth 42 exhibiting localized water discoloration and acidic vapor mist, extending 400 yards downwind across adjacent industrial docks.", + "threatened_areas": "San Jacinto Battleground Historic Site 1.2 miles down-river and Channelview Residential Neighborhood 2.0 miles downwind to the North-Northwest.", + "overflight_results": "USCG Air Station Houston Helicopter 6512 confirmed a localized 150-yard plume dissipating in the ship channel with continuous water sampling underway.", + "trajectories": "Acidic vapor mist tracking North-Northwest at 8 mph; waterborne runoff moving with ebb tide East-Southeast at 1.2 knots.", + "impacted_shorelines": "400 yards of concrete bulkhead and wooden fender piling at Berth 42.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Bravo at Jacintoport Terminal, Spill Boom Site 1, Command Post at USCG Sector Houston-Galveston, Exclusion Zone boundaries (1,000 ft radius), and ship channel navigation safety zones." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 10:50 CDT on 10/14/2026, a high-pressure discharge hose burst during offloading operations at Berth 42, spilling approximately 4,200 gallons of 98% sulfuric acid into secondary containment and the ship channel, generating a dense corrosive acid mist cloud.", + "health_and_safety_hazards": "Severe skin necrosis upon contact, permanent ocular damage, pulmonary edema from acid mist inhalation, extreme heat reaction when mixed with water, and slip hazards on degraded berth decking.", + "necessary_measures": "Enforce 1,000-foot Exclusion Zone; Level A chemical suit protection with SCBA for Hot Zone entries; Level B protective suits with SCBA for Warm Zone support; continuous pH water sampling and real-time acid mist sensor monitoring; mandatory dual-stage lime neutralization decontamination wash at Gate 4." + }, + "6_prepared_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 13:00" + }, + "7_current_and_planned_objectives": [ + "Enforce a 1,000-foot Exclusion Zone around Berth 42 and secure vessel offloading by 12:00 hours.", + "Complete neutralization of deck spill using sodium bicarbonate slurry by 14:00 hours.", + "Deploy chemical neutralization boom array at Berth 42 slip by 13:30 hours.", + "Monitor atmospheric acid mist levels along Channelview perimeter to ensure public safety by 15:00 hours.", + "Transition to Unified Command (USCG, EPA, TCEQ, RP) by 12:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "11:20", + "actions": "Initial emergency response initiated; Port Authority Fire Engines 81 and 82 respond; 1,000-foot safety perimeter set up." + }, + { + "time": "11:45", + "actions": "Sector Command established by Captain H. Vance; Houston Ship Channel traffic restricted to one-way slow speed." + }, + { + "time": "12:15", + "actions": "Port Hazmat Team 1 arrives on scene to establish pH monitoring grid and set up neutralization decon corridor." + }, + { + "time": "12:50", + "actions": "USCG Strike Team arrives; initiates shipboard inspection and containment audit." + }, + { + "time": "13:15", + "actions": "Stolt Tankers RP response vessel deploys chemical sorbent boom around M/T Stolt Synergy." + }, + { + "time": "14:00", + "actions": "Planned: Entry Team Alpha under Level A PPE executes emergency valve shutdown on shipboard manifold." + }, + { + "time": "14:30", + "actions": "Planned: Begin high-volume sodium bicarbonate dry-powder application on vessel deck." + }, + { + "time": "16:00", + "actions": "Planned: Perform secondary overflight to confirm complete plume dissipation." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain H. Vance (USCG Unified Command IC)", + "OSC M. Reynolds (EPA Co-IC)", + "Chief D. Garcia (Port Authority Fire IC)", + "K. Lindqvist (Stolt Tankers RP IC)" + ], + "safety_officer": "Commander Thomas Blake", + "public_information_officer": "Laura Martinez (Port Authority)", + "liaison_officer": "Inspector R. Sterling (TCEQ)", + "operations_section_chief": "Battalion Chief A. Ross", + "planning_section_chief": "Commander Richard Croft", + "logistics_section_chief": "Sandra Keller", + "finance_administration_section_chief": "Wayne Miller", + "additional_positions": [ + { + "position": "Chemical Hazards Specialist", + "name": "Dr. V. Patel" + }, + { + "position": "Vessel Boarding Group Supervisor", + "name": "Lt. J. Thorne" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Port Authority Hazmat Team", + "resource_identifier": "HZM-PORT-1", + "date_time_ordered": "10/14/2026 11:25", + "eta": "N/A", + "arrived": true, + "notes": "Executing pH sampling grid and Hot Zone entry prep." + }, + { + "resource": "Port Fire Engine", + "resource_identifier": "ENG-81", + "date_time_ordered": "10/14/2026 11:20", + "eta": "N/A", + "arrived": true, + "notes": "Securing 1,000 ft landward perimeter and foam standby." + }, + { + "resource": "USCG Gulf Strike Team", + "resource_identifier": "USCG-GST-02", + "date_time_ordered": "10/14/2026 11:50", + "eta": "N/A", + "arrived": true, + "notes": "Supervising vessel entry protocol and safety verification." + }, + { + "resource": "Chemical Response Vessel", + "resource_identifier": "RV-AC-01", + "date_time_ordered": "10/14/2026 12:00", + "eta": "N/A", + "arrived": true, + "notes": "Deployed chemical sorbent boom around vessel berth." + }, + { + "resource": "Lime Neutralization Truck Unit", + "resource_identifier": "NEUT-TRK-05", + "date_time_ordered": "10/14/2026 12:30", + "eta": "10/14/2026 14:15", + "arrived": false, + "notes": "Delivering 10 tons of dry sodium bicarbonate slurry." + }, + { + "resource": "Mobile Air Sampling Van", + "resource_identifier": "AIR-SAM-02", + "date_time_ordered": "10/14/2026 12:10", + "eta": "10/14/2026 13:45", + "arrived": false, + "notes": "Transporting real-time SO3/acid mist photoionization sensors." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics201_6.json b/benchmark/datasets/ground_truth/ics201_6.json new file mode 100644 index 00000000..d22b858a --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_6.json @@ -0,0 +1,144 @@ +{ + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_incident_number": "NTSB-EPA-R10-2026-1102", + "3_date_time_initiated": { + "date": "11/02/2026", + "time": "04:45" + }, + "4_map_sketch": { + "total_area_of_operations": "5.0-mile radius centered around BNSF Railway Milepost 218.4 near Skykomish, WA.", + "incident_site_area": "BNSF Rail Milepost 218.4; 5 freight cars derailed including pressurized tank car DOT-105J500W (BNSF-77402) containing liquefied chlorine gas with damaged protective valve housing.", + "impacted_areas": "1,500-foot vapor dispersion zone along the rail right-of-way and adjacent State Route 2.", + "threatened_areas": "Town of Skykomish (1.8 miles West downwind) and South Fork Skykomish River salmon spawning habitat (300 feet South).", + "overflight_results": "WSP FLIR Helicopter WSP-AIR-2 confirmed a localized green-yellow chlorine cloud hugging the ravine floor and drifting West-Northwest at 4 mph.", + "trajectories": "Airborne toxic plume tracking West-Northwest along State Route 2 corridor at 4 mph; zero liquid chemical runoff entering waterways.", + "impacted_shorelines": "None; ground contamination restricted to northern ravine embankment slopes.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Charlie at Stevens Pass Maintenance Yard, Mobile Command Post at Skykomish School District Gym, 1.5-mile Exclusion Zone boundary, and State Route 2 traffic closure checkpoints." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 04:15 PST on 11/02/2026, a mountain derailment caused 5 freight cars to leave the tracks. Tank car BNSF-77402 sustained severe dome housing damage, resulting in a low-rate pressurized vapor release of toxic chlorine at approximately 15 lbs/min. State Route 2 was closed and mandatory evacuation issued for 250 residents of Skykomish.", + "health_and_safety_hazards": "Severe pulmonary edema risk from chlorine gas inhalation, ocular and skin chemical burns, acute respiratory collapse, mountain hypothermia (ambient temp 34°F), and steep terrain slip hazards.", + "necessary_measures": "Enforce 1.5-mile Exclusion Zone; mandatory Level A vapor-tight encapsulated suits with SCBA for Hot Zone entry personnel; Level B suits for Warm Zone support; continuous electrochemical chlorine sensor monitoring at CP baselines; mandatory indoor shelter-in-place for outer perimeters; heated wet-decontamination trailer wash at Staging Area Charlie." + }, + "6_prepared_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 07:15" + }, + "7_current_and_planned_objectives": [ + "Complete mandatory evacuation of Skykomish township within 1.5-mile radius by 07:30 hours.", + "Perform Hot Zone entry to apply Emergency B-Kit capping assembly on chlorine railcar dome by 09:30 hours.", + "Establish continuous multi-point perimeter air monitoring along State Route 2 by 08:00 hours.", + "Secure rail right-of-way and establish Unified Command by 07:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "04:45", + "actions": "Initial alarm dispatch; Skykomish Fire and King County Sheriff units respond, closing State Route 2 at Milepost 215." + }, + { + "time": "05:15", + "actions": "Local Command established by Chief D. Olson; mandatory evacuation sirens activated across Skykomish." + }, + { + "time": "05:50", + "actions": "Regional Hazmat Team 3 and BNSF Response Team arrived on scene to initiate perimeter air testing." + }, + { + "time": "06:30", + "actions": "WSP FLIR helicopter completed thermal overflight mapping plume dispersion." + }, + { + "time": "07:00", + "actions": "Unified Command established at Skykomish School Gym with EPA Region 10 and BNSF RP." + }, + { + "time": "08:15", + "actions": "Planned: Entry Team 1 under Level A PPE initiates B-Kit capping tool installation on railcar valve dome." + }, + { + "time": "09:30", + "actions": "Planned: Perform pressure test and seal verification of capping assembly." + }, + { + "time": "11:00", + "actions": "Planned: Complete environmental health review to evaluate lifting outer zone evacuation orders." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain E. Miller (WSP Unified Command IC)", + "OSC R. Brooks (EPA R10 Co-IC)", + "Chief D. Olson (Skykomish Fire IC)", + "M. Campbell (BNSF Railway RP IC)" + ], + "safety_officer": "Captain Marcus Vance", + "public_information_officer": "Jennifer Hayes (WSDOT)", + "liaison_officer": "Deputy S. Kowalski (KCSO)", + "operations_section_chief": "Battalion Chief T. Higgins", + "planning_section_chief": "Lt. Colonel Alan Vance", + "logistics_section_chief": "David Sterling", + "finance_administration_section_chief": "Patricia Ross", + "additional_positions": [ + { + "position": "Rail Hazmat Specialist", + "name": "G. Peterson (BNSF)" + }, + { + "position": "Evacuation Group Supervisor", + "name": "Sgt. H. Lin (KCSO)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "King County Hazmat 3", + "resource_identifier": "HZM-33", + "date_time_ordered": "11/02/2026 05:00", + "eta": "N/A", + "arrived": true, + "notes": "Preparing Level A Entry Team 1 and Emergency B-Kit capping assembly." + }, + { + "resource": "Skykomish Engine 11", + "resource_identifier": "ENG-11", + "date_time_ordered": "11/02/2026 04:45", + "eta": "N/A", + "arrived": true, + "notes": "Securing State Route 2 East closure checkpoint." + }, + { + "resource": "BNSF Emergency Response Unit", + "resource_identifier": "BNSF-ER-01", + "date_time_ordered": "11/02/2026 05:15", + "eta": "N/A", + "arrived": true, + "notes": "On scene with railcar specialized capping kits." + }, + { + "resource": "WSP Aviation FLIR Helicopter", + "resource_identifier": "WSP-AIR-2", + "date_time_ordered": "11/02/2026 05:30", + "eta": "N/A", + "arrived": true, + "notes": "Completed thermal imagery mapping of plume drift." + }, + { + "resource": "Mobile Decontamination Trailer", + "resource_identifier": "DECON-04", + "date_time_ordered": "11/02/2026 05:45", + "eta": "11/02/2026 08:30", + "arrived": false, + "notes": "En route to establish warm water decon at Staging Area Charlie." + }, + { + "resource": "Hazmat Air Monitoring Recon", + "resource_identifier": "AIR-MON-10", + "date_time_ordered": "11/02/2026 06:00", + "eta": "11/02/2026 07:45", + "arrived": false, + "notes": "En route with multi-gas chlorine sensors." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics201_7.json b/benchmark/datasets/ground_truth/ics201_7.json new file mode 100644 index 00000000..99dcd09e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_7.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_incident_number": "CalOES-EPA-R9-2026-0518", + "3_date_time_initiated": { + "date": "05/18/2026", + "time": "13:10" + }, + "4_map_sketch": { + "total_area_of_operations": "4.0-mile radius including Trans-California Pipeline MM 87.3, Sawpit Canyon, and Silverwood Reservoir basin.", + "incident_site_area": "Valve Station 12 along Trans-California Pipeline MM 87.3 in Sawpit Canyon, where a 12-inch pressurized crude line ruptured.", + "impacted_areas": "800 yards of Sawpit Canyon creek bed carrying crude oil towards Silverwood Lake, with a 30-acre surface oil sheen in reservoir South arm.", + "threatened_areas": "Mojave Water Agency Municipal Pumping Plant (1.5 miles North) and Silverwood Lake State Recreation Area campgrounds (0.8 miles West).", + "overflight_results": "Cal FIRE Recon Aircraft 240 confirmed heavy black crude pooling in Sawpit Canyon and a 400-yard wide oil ribbon entering Silverwood Lake.", + "trajectories": "Waterborne crude oil slick moving North at 1.0 knot towards main reservoir basin; airborne volatile organic cloud tracking Southeast at 7 mph into canyon slopes.", + "impacted_shorelines": "1.2 miles of rocky reservoir shoreline and marsh vegetation along Sawpit Canyon inlet.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Delta at Silverwood Lake Marina, Incident Command Post at Hesperia Fire Station 30, 1,000-foot Exclusion Zone perimeter, Deflection Boom Sites A and B, and municipal water intake protection zones." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 12:40 PDT on 05/18/2026, a hillside land movement ruptured a 12-inch crude oil pipeline at MM 87.3, discharging approximately 12,000 gallons of heavy crude oil into Sawpit Canyon creek before automated valves shut off flow. The oil flowed downstream into Silverwood Reservoir, threatening southern California drinking water supplies.", + "health_and_safety_hazards": "Toxic benzene vapor inhalation, volatile organic compound exposure, acute flammability (flash point 65°F), wildfire ignition risk in dry brush, slip-and-fall hazards on oily terrain, and severe heat exhaustion (ambient temp 94°F).", + "necessary_measures": "Mandatory 1,000-foot Exclusion Zone; Level B PPE with SCBA for initial Hot Zone creek entry; Level C PPE with organic vapor respirators for shoreline booming teams; continuous PID air monitoring downwind; mandatory wildland fire standby team with AFFF foam; establishment of dual-basin wash decontamination at Marina Ramp 2." + }, + "6_prepared_by": { + "name": "Captain Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 15:30" + }, + "7_current_and_planned_objectives": [ + "Enforce 1,000-foot Exclusion Zone and secure pipeline isolation by 14:00 hours.", + "Deploy 2,000 feet of containment boom across Sawpit Canyon inlet by 16:30 hours to prevent oil migration to water intake.", + "Evacuate Silverwood Lake State Recreation Area campgrounds by 15:00 hours.", + "Initiate skimmer boat oil recovery in South arm of reservoir by 17:00 hours.", + "Establish Unified Command (Cal OES, EPA R9, USFS, San Bernardino County Fire, Pipeline RP) by 14:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "13:10", + "actions": "Alarm dispatch; San Bernardino County Fire Engine 30 and USFS Crew 4 respond to set up initial safety perimeter." + }, + { + "time": "13:40", + "actions": "Local Command established by Chief M. Thorne; Trans-California Pipeline operators confirm remote valve isolation at MM 85 and MM 90." + }, + { + "time": "14:15", + "actions": "State Parks Rangers complete full evacuation of Silverwood Lake campgrounds (150 visitors evacuated)." + }, + { + "time": "14:45", + "actions": "Cal OES and EPA OSC arrive on scene; Unified Command established at Station 30." + }, + { + "time": "15:15", + "actions": "Clean Harbors Spill Response Vessel deploys 1,200 feet of hard boom at Sawpit Inlet (Boom Site A)." + }, + { + "time": "16:00", + "actions": "Planned: Deploy secondary sorbent deflection boom array at Boom Site B upstream of Mojave Water Intake." + }, + { + "time": "17:00", + "actions": "Planned: Vacuum skimmer vessel Lake Clean 1 initiates surface crude extraction in South arm." + }, + { + "time": "18:30", + "actions": "Planned: Complete initial shoreline oiling assessment along Sawpit Canyon inlet." + } + ], + "9_current_organization": { + "incident_commanders": [ + "OSC C. Martinez (EPA R9 Co-IC)", + "Chief M. Thorne (SBCo Fire IC)", + "Chief Inspector A. Kim (Cal OES)", + "W. Vance (Pipeline RP IC)" + ], + "safety_officer": "Captain Gregory Hall", + "public_information_officer": "Samantha Norris (Cal OES)", + "liaison_officer": "Ranger D. Stevens (State Parks)", + "operations_section_chief": "Battalion Chief Kevin Ross", + "planning_section_chief": "Captain Rachel Brooks", + "logistics_section_chief": "Megan Taylor", + "finance_administration_section_chief": "Jason Wu", + "additional_positions": [ + { + "position": "Environmental Unit Leader", + "name": "Dr. L. Arispe (USFS)" + }, + { + "position": "Waterborne Recovery Supervisor", + "name": "Captain B. Walsh (Clean Harbors)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "San Bernardino Hazmat Engine", + "resource_identifier": "ENG-30", + "date_time_ordered": "05/18/2026 13:10", + "eta": "N/A", + "arrived": true, + "notes": "Operating perimeter vapor monitoring and fire standby." + }, + { + "resource": "USFS Wildland Handcrew", + "resource_identifier": "CREW-04", + "date_time_ordered": "05/18/2026 13:15", + "eta": "N/A", + "arrived": true, + "notes": "Clearing brush and building containment dikes in Sawpit Canyon." + }, + { + "resource": "State Parks Ranger Unit", + "resource_identifier": "PARK-12", + "date_time_ordered": "05/18/2026 13:20", + "eta": "N/A", + "arrived": true, + "notes": "Completed campground evacuations and road closures." + }, + { + "resource": "Clean Harbors Spill Vessel", + "resource_identifier": "RV-LC-01", + "date_time_ordered": "05/18/2026 14:00", + "eta": "N/A", + "arrived": true, + "notes": "Deployed 1,200 ft hard boom at Sawpit Canyon inlet." + }, + { + "resource": "Heavy Vacuum Skimmer Boat", + "resource_identifier": "SKIM-02", + "date_time_ordered": "05/18/2026 14:30", + "eta": "05/18/2026 16:45", + "arrived": false, + "notes": "En route from San Pedro for reservoir skimmer recovery." + }, + { + "resource": "Air Monitoring Recon Unit", + "resource_identifier": "AIR-MON-08", + "date_time_ordered": "05/18/2026 14:10", + "eta": "05/18/2026 15:45", + "arrived": false, + "notes": "En route with photoionization detectors and benzene gas sensors." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics201_8.json b/benchmark/datasets/ground_truth/ics201_8.json new file mode 100644 index 00000000..50fed48e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_8.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Delaware Bay Container Vessel Collision & Fuel Oil Spill", + "2_incident_number": "USCG-D5-2026-0629", + "3_date_time_initiated": { + "date": "06/29/2026", + "time": "22:40" + }, + "4_map_sketch": { + "total_area_of_operations": "6.0-mile radius centered at Delaware Bay Shipping Channel Buoy 14 near Lewes, DE.", + "incident_site_area": "Delaware Bay Channel Buoy 14 (38.85°N, 75.12°W), where container vessel M/V Atlantic Voyager collided with a bulk carrier, breaching fuel tank #3 port side.", + "impacted_areas": "3.5-mile long heavy oil sheen extending East-Southeast across the bay channel towards Cape Henlopen State Park.", + "threatened_areas": "Prime Hook National Wildlife Refuge (4.0 miles Northwest) and Cape Henlopen Tidal Salt Marshes (2.5 miles South).", + "overflight_results": "USCG HC-144 Ocean Sentry aircraft confirmed a 300-yard wide slick of Intermediate Fuel Oil (IFO 380) drifting with the flood tide.", + "trajectories": "Waterborne oil slick migrating East-Southeast at 2.2 knots toward Cape Henlopen under coastal winds of 14 knots from West-Northwest.", + "impacted_shorelines": "2.0 miles of outer sandy beach and dune lines along Cape Henlopen State Park.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Echo at Lewes Ferry Terminal, Incident Command Post at USCG Sector Delaware Bay Headquarters, 1,000-yard Maritime Safety Zone, Boom Sites 1, 2, and 3, and wildlife protection zones." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 22:15 EDT on 06/29/2026, container vessel M/V Atlantic Voyager collided with anchored bulk carrier M/V Pacific Trader near Buoy 14. Port fuel tank #3 breached, discharging approximately 25,000 gallons of heavy Intermediate Fuel Oil (IFO 380) before internal fuel transfer halted the release.", + "health_and_safety_hazards": "Hydrocarbon vapor exposure (benzene/H2S), direct dermal toxicity, severe slip hazards on oily vessel hulls and shorelines, nighttime over-water operations, water drowning risk, and wildlife exposure risks.", + "necessary_measures": "Establish 1,000-yard Maritime Safety Zone; mandatory Level C PPE with PFDs and half-mask organic vapor respirators for maritime responders; continuous H2S and PID air monitoring on response vessels; compulsory work-rest hydration cycles; mobile vessel decontamination station established at Lewes Pier." + }, + "6_prepared_by": { + "name": "Commander James Vance", + "position_title": "Planning Section Chief", + "signature": "James Vance", + "date_time": "06/30/2026 01:30" + }, + "7_current_and_planned_objectives": [ + "Secure maritime safety perimeter and stabilize damaged vessel by 01:00 hours.", + "Deploy protective booming across Cape Henlopen salt marsh inlets by 04:00 hours.", + "Initiate offshore skimming operations using MSRC oil recovery vessels by 05:30 hours.", + "Activate wildlife rescue and rehab operations with Tri-State Bird Rescue by 06:00 hours.", + "Establish Unified Command (USCG, EPA, Delaware DNREC, M/V Atlantic Voyager RP) by 02:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "22:40", + "actions": "Initial alarm dispatch; USCG Station Lewes response boats 45601 and 45602 respond to establish maritime safety zone." + }, + { + "time": "23:15", + "actions": "Local Command established by Captain P. Hayes; Sector Delaware Bay requests MSRC oil spill response vessel mobilization." + }, + { + "time": "23:50", + "actions": "M/V Atlantic Voyager crew completes internal fuel transfer, halting active discharge from fuel tank #3." + }, + { + "time": "00:30", + "actions": "USCG HC-144 overflight completes IR sensor oil slick mapping." + }, + { + "time": "01:15", + "actions": "Delaware DNREC response team arrives at Lewes Command Post; Unified Command established." + }, + { + "time": "03:00", + "actions": "Planned: MSRC Response Vessel Delaware Responder deploys 2,500 feet of ocean containment boom at Buoy 14." + }, + { + "time": "04:30", + "actions": "Planned: Deploy sorbent diversion boom at Cape Henlopen Inlet (Boom Site 2)." + }, + { + "time": "06:00", + "actions": "Planned: Tri-State Bird Rescue team commences shoreline wildlife search along Cape Henlopen beaches." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain P. Hayes (USCG Unified Command IC)", + "OSC T. Gallagher (EPA R3)", + "Secretary A. Lawson (Delaware DNREC)", + "H. Lindemann (Atlantic Shipping RP IC)" + ], + "safety_officer": "Lt. Commander Mark Reynolds", + "public_information_officer": "Chief Petty Officer Kelly Adams", + "liaison_officer": "Inspector R. Thorne (DNREC)", + "operations_section_chief": "Commander Frank Miller", + "planning_section_chief": "Commander James Vance", + "logistics_section_chief": "Laura Bennett", + "finance_administration_section_chief": "Charles Foster", + "additional_positions": [ + { + "position": "Scientific Support Coordinator", + "name": "Dr. E. Sullivan (NOAA)" + }, + { + "position": "Wildlife Branch Director", + "name": "Dr. C. Jenkins (Tri-State)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "USCG Response Boat Medium", + "resource_identifier": "RBM-45601", + "date_time_ordered": "06/29/2026 22:40", + "eta": "N/A", + "arrived": true, + "notes": "Maintaining 1,000-yard safety zone around damaged vessel." + }, + { + "resource": "USCG Station Lewes RBM", + "resource_identifier": "RBM-45602", + "date_time_ordered": "06/29/2026 22:40", + "eta": "N/A", + "arrived": true, + "notes": "Conducting water sampling and perimeter safety watch." + }, + { + "resource": "MSRC Spill Vessel Delaware Responder", + "resource_identifier": "RV-MSRC-01", + "date_time_ordered": "06/29/2026 23:00", + "eta": "N/A", + "arrived": true, + "notes": "Deployed 2,500 ft ocean boom and preparing offshore skimmer." + }, + { + "resource": "USCG Ocean Sentry Aircraft", + "resource_identifier": "USCG-HC144", + "date_time_ordered": "06/29/2026 23:10", + "eta": "N/A", + "arrived": true, + "notes": "Completed IR slick mapping and overflight tracking." + }, + { + "resource": "Mobile Wildlife Rehabilitation Trailer", + "resource_identifier": "WILD-REHAB-1", + "date_time_ordered": "06/30/2026 00:15", + "eta": "06/30/2026 05:45", + "arrived": false, + "notes": "En route to establish bird cleaning station at Lewes Pier." + }, + { + "resource": "Shoreline Cleanup Strike Team", + "resource_identifier": "SCAT-TEAM-1", + "date_time_ordered": "06/30/2026 00:45", + "eta": "06/30/2026 06:30", + "arrived": false, + "notes": "Mobilizing for Cape Henlopen beach oil assessment." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics201_9.json b/benchmark/datasets/ground_truth/ics201_9.json new file mode 100644 index 00000000..c182deff --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_9.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Oakridge Industrial Park Anhydrous Ammonia Release", + "2_incident_number": "EPA-R2-2026-0905", + "3_date_time_initiated": { + "date": "09/05/2026", + "time": "08:15" + }, + "4_map_sketch": { + "total_area_of_operations": "2.0-mile radius centered at Cold Storage Logistics Building 7, Edison, NJ.", + "incident_site_area": "Mechanical Room 3 on roof deck of Cold Storage Logistics Building 7 (100 Industrial Parkway), where an 8-inch high-pressure ammonia chiller line fractured.", + "impacted_areas": "400-yard vapor dispersion plume encompassing Building 7 loading dock and northern section of Industrial Parkway.", + "threatened_areas": "Meadowlands Residential Community (0.9 miles East downwind) and Edison Transit Center (1.2 miles Southeast).", + "overflight_results": "Edison Police Drone Recon Unit DRONE-PD-1 confirmed a dense white fog cloud hovering over Roof Mechanical Room 3 and drifting East-Northeast at 5 mph.", + "trajectories": "Airborne toxic anhydrous ammonia cloud migrating East-Northeast at 5 mph; zero liquid chemical product entering storm drainage systems.", + "impacted_shorelines": "None; contamination restricted to localized industrial ground surfaces and rooftop structures.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Foxtrot at Edison High School parking lot, Command Post at Edison Fire Station 4, 1,000-foot Exclusion Zone boundary, and Industrial Parkway traffic control points." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 07:45 EDT on 09/05/2026, a mechanical vibration failure severed an 8-inch liquid refrigeration line inside Mechanical Room 3 at Cold Storage Logistics, releasing approximately 3,000 lbs of anhydrous ammonia gas. Building 7 was evacuated immediately (85 employees), and a 1,000-foot perimeter isolation zone was established.", + "health_and_safety_hazards": "Severe chemical asphyxiation, acute pulmonary edema, permanent ocular injury, cryogenic chemical freeze burns, flammability risk in enclosed spaces (LEL 15-28%), and physical fall hazards from roof decking.", + "necessary_measures": "Mandatory 1,000-foot Exclusion Zone; Level A encapsulated vapor suits with SCBA for all roof entry personnel; Level B suits for ground backup teams; continuous electrochemical ammonia air monitoring at perimeter boundaries; high-volume water curtain deployment for vapor knockdown; mandatory dual-basin chemical wash decontamination corridor at Building 7 main entrance." + }, + "6_prepared_by": { + "name": "Captain Michael Vance", + "position_title": "Planning Section Chief", + "signature": "Michael Vance", + "date_time": "09/05/2026 10:30" + }, + "7_current_and_planned_objectives": [ + "Enforce 1,000-foot Exclusion Zone and isolate Building 7 HVAC intake by 08:30 hours.", + "Perform roof entry under Level A PPE to manually isolate main receiver valve by 11:00 hours.", + "Deploy high-volume water curtain monitoring nozzles to suppress cloud drift by 09:30 hours.", + "Perform continuous downwind air monitoring along Meadowlands border by 09:00 hours.", + "Establish Unified Command (Edison Fire, NJ DEP, EPA R2, Facility RP) by 08:45 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "08:15", + "actions": "Initial alarm dispatch; Edison Fire Engines 4 and 6 respond and assist Building 7 evacuation." + }, + { + "time": "08:35", + "actions": "Local Command established by Chief E. Sullivan; Industrial Parkway closed at Kilmer Road." + }, + { + "time": "09:00", + "actions": "Middlesex County Hazmat Team 2 arrived to set up perimeter air monitoring grid and water fog curtain monitors." + }, + { + "time": "09:40", + "actions": "NJ DEP and EPA Region 2 OSC arrived on scene; Unified Command established at Station 4." + }, + { + "time": "10:15", + "actions": "Drone recon flight confirmed water curtains reduced downwind cloud density by 60%." + }, + { + "time": "11:00", + "actions": "Planned: Entry Team 1 under Level A PPE executes emergency roof entry to isolate main refrigeration manifold." + }, + { + "time": "12:00", + "actions": "Planned: Initiate mechanical ventilation of Building 7 interior through charcoal scrubber array." + }, + { + "time": "13:30", + "actions": "Planned: Conduct clearance air sampling inside Building 7 prior to facility re-entry." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief E. Sullivan (Edison Fire IC)", + "OSC H. Miller (EPA R2)", + "Inspector G. Ross (NJ DEP)", + "T. Jenkins (Cold Storage RP IC)" + ], + "safety_officer": "Captain Robert Hayes", + "public_information_officer": "Amanda Walsh (Edison OEM)", + "liaison_officer": "Lt. D. Kincaid (Middlesex PD)", + "operations_section_chief": "Battalion Chief Brian O'Connor", + "planning_section_chief": "Captain Michael Vance", + "logistics_section_chief": "Karen Miller", + "finance_administration_section_chief": "Steven Zhang", + "additional_positions": [ + { + "position": "Refrigeration Systems Specialist", + "name": "C. Bauer (Cold Storage RP)" + }, + { + "position": "Air Monitoring Leader", + "name": "Lt. V. Patel (County Hazmat)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Middlesex County Hazmat 2", + "resource_identifier": "HZM-MC-02", + "date_time_ordered": "09/05/2026 08:20", + "eta": "N/A", + "arrived": true, + "notes": "Operating water curtains and preparing Level A roof entry." + }, + { + "resource": "Edison Fire Engine 4", + "resource_identifier": "ENG-04", + "date_time_ordered": "09/05/2026 08:15", + "eta": "N/A", + "arrived": true, + "notes": "Supplying high-pressure water to fog monitors for vapor suppression." + }, + { + "resource": "Edison Ladder Truck 2", + "resource_identifier": "LADDER-02", + "date_time_ordered": "09/05/2026 08:15", + "eta": "N/A", + "arrived": true, + "notes": "Positioned for roof access and aerial water stream deployment." + }, + { + "resource": "Police Drone Recon Unit", + "resource_identifier": "DRONE-PD-1", + "date_time_ordered": "09/05/2026 08:30", + "eta": "N/A", + "arrived": true, + "notes": "Conducting continuous thermal and aerial monitoring of plume drift." + }, + { + "resource": "Mobile Mechanical Scrubber Unit", + "resource_identifier": "VENT-SCRUB-3", + "date_time_ordered": "09/05/2026 09:15", + "eta": "09/05/2026 11:45", + "arrived": false, + "notes": "En route to perform positive pressure building scrubbing." + }, + { + "resource": "High-Sensitivity Air Monitoring Truck", + "resource_identifier": "AIR-TRK-07", + "date_time_ordered": "09/05/2026 09:00", + "eta": "09/05/2026 10:15", + "arrived": false, + "notes": "En route to monitor downwind Meadowlands residential perimeter." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics202_2.json b/benchmark/datasets/ground_truth/ics202_2.json new file mode 100644 index 00000000..c0af4580 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_2.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain 100% responder compliance with SCBA Level B PPE within the 500-foot Exclusion Zone throughout the night shift.", + "Complete installation of 1,000 feet of sorbent containment boom array across Blackwood River at Boom Site 2 by 22:00 hours to protect downstream municipal water intake.", + "Complete hot-tap product evacuation of damaged pipeline section by 02:00 hours to stop active anhydrous ammonia leakage.", + "Maintain continuous automated PID perimeter air monitoring downwind along Route 104 with telemetry reporting to CP every 30 minutes.", + "Finalize Operational Period 2 IAP shift plan and resource request documentation by 04:30 hours." + ], + "4_operational_period_command_emphasis": "Focus tactical operations on maintaining river boom integrity under rising night tides and ensuring strict atmospheric air monitoring during the overnight temperature drop when ammonia vapors hug low ground.", + "general_situational_awareness": "Clear night skies, temperatures dropping to 58°F, and wind shifting from East-Northeast to North at 4 mph. Low-lying fog expected near riverbanks between 02:00 and 06:00 hours. High vigilance required for slick riverbank terrain and reduced nighttime visibility.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Mobile Command Post Briefing Trailer at Station 12", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "Ammonia Air Dispersion Model Map", + "Pipeline Shutoff Schematic" + ] + }, + "7_prepared_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief R. Henderson", + "signature": "Chief R. Henderson", + "date_time": "08/13/2026 17:15" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics202_3.json b/benchmark/datasets/ground_truth/ics202_3.json new file mode 100644 index 00000000..786d9948 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_3.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_objectives": [ + "Ensure zero safety incidents by enforcing mandatory Level A chemical suit entry procedures for all vessel deck operations.", + "Apply 10 tons of dry sodium bicarbonate slurry to neutralize pooled acid on Berth 42 deck by 12:00 hours.", + "Maintain 2,000 feet of chemical sorbent boom around M/T Stolt Synergy and conduct hourly river pH sampling to ensure zero downstream acid migration.", + "Complete vessel hull structural integrity audit and offloading manifold pressure test by 15:00 hours.", + "Conduct continuous public air monitoring along Channelview community boundary and issue real-time safety updates by 17:00 hours." + ], + "4_operational_period_command_emphasis": "Primary tactical focus is placed on safe chemical neutralization and pH stabilization in the ship channel water column. Personnel safety during high-temperature Level A suit operations takes absolute priority over operational speed.", + "general_situational_awareness": "Daytime temperatures reaching 88°F with high humidity (78%), creating severe heat stress conditions for suit technicians. Winds from Southeast at 9 mph. Work-rest cycles of 20 minutes active entry followed by 40 minutes hydration/cooling are strictly mandated.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "USCG Sector Houston Command Center Safety Office", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": true, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "TCEQ Water Quality Monitoring Plan", + "Vessel Cargo Stowage Plan" + ] + }, + "7_prepared_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 06:00" + }, + "8_approved_by_incident_commander": { + "name": "Captain H. Vance", + "signature": "Captain H. Vance", + "date_time": "10/14/2026 06:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics202_4.json b/benchmark/datasets/ground_truth/ics202_4.json new file mode 100644 index 00000000..3848dd01 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_4.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Enforce strict Level A PPE compliance and buddy system protocols during all night operations around derailment site.", + "Complete installation and torque testing of Emergency B-Kit capping assembly on chlorine tank car BNSF-77402 by 22:00 hours.", + "Maintain perimeter chlorine air monitoring at 15-minute intervals along State Route 2 evacuation boundary.", + "Operate heated decontamination trailer and warm hydration station continuously at Staging Area Charlie.", + "Formulate environmental sampling plan and morning shift briefing package by 04:00 hours." + ], + "4_operational_period_command_emphasis": "Command emphasizes absolute safety of capping entry teams working under nighttime freezing conditions. Ensure continuous warm water decon availability to prevent suit icing.", + "general_situational_awareness": "Overcast skies with mountain temperatures dropping to 28°F overnight; snow flurries expected after 01:00 hours. Light winds from West at 3 mph drifting toward ravine floor. Icing hazards on rail ballast and steep access slopes. High hypothermia hazard for standing security personnel.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Skykomish School Gym Operations Briefing Room", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": true, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "Railcar Capping Procedure Manual", + "Chlorine Gas Toxicity Chart" + ] + }, + "7_prepared_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Captain E. Miller", + "signature": "Captain E. Miller", + "date_time": "11/02/2026 17:15" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics202_5.json b/benchmark/datasets/ground_truth/ics202_5.json new file mode 100644 index 00000000..92a48fcc --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_5.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain 100% safety record with zero heat injuries or toxic vapor exposures during night shift operations.", + "Secure 2,000 feet of containment boom across Sawpit Canyon inlet and maintain skimmer vessel recovery in South arm until 23:00 hours.", + "Complete pipeline mechanical clamp repair on 12-inch main line at MM 87.3 by 02:00 hours.", + "Perform hourly water sampling upstream of Mojave Water Intake facility to ensure non-detectable hydrocarbon levels.", + "Prepare Day 2 Operational Period Incident Action Plan and resource requests by 04:30 hours." + ], + "4_operational_period_command_emphasis": "Focus tactical priority on preventing any oil migration toward the municipal water intake. Night skimming operations must maintain illuminated safety perimeters and life-vest compliance at all times.", + "general_situational_awareness": "Evening temperature cooling to 70°F with calm winds under 5 mph. Mountain lions and nocturnal wildlife reported near Sawpit Canyon inlet. Flashlight illumination required along all shoreline walking paths due to steep, oily riprap.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Incident Command Post at Hesperia Fire Station 30", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "Silverwood Lake Water Sampling Map", + "Pipeline Repair Safety Plan" + ] + }, + "7_prepared_by": { + "name": "Captain Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief M. Thorne", + "signature": "Chief M. Thorne", + "date_time": "05/18/2026 17:00" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics202_6.json b/benchmark/datasets/ground_truth/ics202_6.json new file mode 100644 index 00000000..3ba25549 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_6.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Oakridge Industrial Park Anhydrous Ammonia Release", + "2_operational_period": { + "date_from": "09/05/2026", + "date_to": "09/06/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_objectives": [ + "Enforce strict Level A/B PPE protocols and 1,000-foot Exclusion Zone control throughout daytime mechanical ventilation operations.", + "Complete charcoal scrubber building ventilation of Building 7 interior to reduce interior ammonia concentrations below 15 ppm by 13:00 hours.", + "Conduct full structural and piping stress inspection of Roof Mechanical Room 3 by 15:00 hours.", + "Perform interior clearance air sampling across all 4 building quadrants by 17:00 hours.", + "Submit final environmental decontamination report to NJ DEP and lift perimeter road closures by 18:30 hours." + ], + "4_operational_period_command_emphasis": "Focus operational efforts on safe indoor ventilation and structural validation. Ensure air monitoring teams continuously verify downwind residential air quality before discharging building exhaust.", + "general_situational_awareness": "Mostly sunny with daytime high of 82°F. Winds from West-Southwest at 7 mph. Thermal updrafts on Building 7 roof deck require secure tie-offs for all entry personnel.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Edison Fire Station 4 Command Post Conference Room", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": true, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "Building 7 Ventilation Engineering Plan", + "NJ DEP Air Quality Standard Sheet" + ] + }, + "7_prepared_by": { + "name": "Captain Michael Vance", + "position_title": "Planning Section Chief", + "signature": "Michael Vance", + "date_time": "09/05/2026 06:00" + }, + "8_approved_by_incident_commander": { + "name": "Chief E. Sullivan", + "signature": "Chief E. Sullivan", + "date_time": "09/05/2026 06:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/narratives/ics201_2.txt b/benchmark/datasets/narratives/ics201_2.txt new file mode 100644 index 00000000..1ff0ef5c --- /dev/null +++ b/benchmark/datasets/narratives/ics201_2.txt @@ -0,0 +1,24 @@ +Blackwood Chemical Pipeline Breach Mockup Incident Briefing (ICS 201) +The Blackwood Chemical Pipeline Breach incident, designated under incident number US-EPA-R5-2026-0813, was formally initiated on August 13, 2026, at 07:15 hours. + +The operational landscape, mapped with standard graphics and GIS symbology, encompasses a total area of operations spanning a 3.0-mile radius around the Blackwood Industrial Corridor. Within this zone, the primary incident site area is centered at Pipeline Valve Station 14-B along Blackwood River Road. The actively impacted areas include a 150-foot radius surrounding the breached 8-inch pressurized line releasing anhydrous ammonia gas, along with an impacted shoreline extending 1,000 yards down the adjacent Blackwood River. Threatened areas downstream include the Oakridge Residential Subdivision 0.75 miles downwind to the East, as well as the Valley Water Authority Municipal Intake Facility located 1.5 miles downstream. Overflight results from state police aviation confirm a dense, low-hanging vapor cloud hugging the ground and moving steadily downwind, alongside a localized surface sheen on the riverbank. Dispersion trajectories model a toxic vapor plume moving East-Northeast at 6 miles per hour, with waterborne chemical runoff traveling downstream at approximately 2.0 knots. + +The situation summary indicates that a third-party excavation crew accidentally punctured the main transfer pipeline at 06:50 hours, causing an uncontrolled high-pressure release of hazardous material. The comprehensive health and safety briefing identifies primary hazards as severe atmospheric exposure to anhydrous ammonia vapor, cryogenic freeze burns upon direct contact with liquid leakage, respiratory injury, structural eye damage, heat stress among response personnel wearing heavy gear in 85°F temperatures, and slip-and-fall risks near steep river embankment slopes. Necessary measures to protect responders and the public include the immediate establishment of a 500-foot Exclusion Zone, mandatory Level A vapor-tight personal protective equipment with self-contained breathing apparatus for all entries into the Hot Zone, continuous photoionization detector air monitoring at all control perimeter boundaries, water curtain deployment to knock down vapor clouds, and a compulsory full-body chemical decontamination procedure at the Warm-to-Cold zone boundary prior to exiting the site. This section was formally prepared by Sarah Jenkins, serving as the Initial Planning Section Chief, who applied her signature on August 13, 2026, at 08:30 hours. + +Current and planned objectives center on life safety, hazard isolation, and environmental mitigation: achieve complete isolation of the pipeline segment by closing upstream valves by 09:30 hours; finish the sheltering-in-place order and partial evacuation of 120 homes in the downwind Oakridge subdivision by 10:00 hours; deploy absorbent and containment booming across the Blackwood River above the municipal water intake by 10:30 hours; and fully establish a Multi-Agency Unified Command at the Regional Emergency Operations Center by 11:00 hours. The chronological sequence of current and planned actions, strategies, and tactics began at 07:00 hours, when initial dispatch sent Blackwood Fire Department and Local Sheriff units to establish an outer safety perimeter. At 07:20 hours, Fire Chief R. Vance established Incident Command and ordered an immediate downwind shelter-in-place alert. At 07:45 hours, Hazmat Team 5 arrived on scene to conduct initial perimeter air monitoring and assist in establishing control zones. At 08:15 hours, pipeline technicians confirmed valve isolation procedures were initiated, while fire crews set up unmanned monitor nozzles to disperse airborne vapors. Planned actions include deploying the Regional Spill Response Team at 09:00 hours to launch containment boom at River Boom Site Alpha, conducting a secondary drone air sampling flight at 09:30 hours, and finalizing the shift to a Unified Command structure at 10:00 hours. + +The current command and general staff organization operates under a Unified Command structure led by Incident Commanders Chief R. Vance (Blackwood Fire Department), Officer M. Ross (State Environmental Protection Agency), and J. Vance (Blackwood Pipeline Co., Responsible Party). The Command Staff features Safety Officer Captain L. Hayes, Public Information Officer E. Wright, and Liaison Officer Deputy K. Miller. The General Staff comprises Operations Section Chief D. Kowalski, Planning Section Chief Sarah Jenkins, Logistics Section Chief T. Bradley, and Finance/Administration Section Chief A. Patel. Additional tactical positions established within the operational structure include Hazmat Group Supervisor Lieutenant C. Webb and Air Monitoring Specialist Dr. H. Thorne. + +The resource summary details the operational status and tracking of critical response assets: + +Hazmat Team 5 (Resource ID: HZM-05), ordered on August 13 at 07:00 hours, arrived on scene at 07:40 hours (Arrived: True) and is currently executing air monitoring and entry ops at the Hot Zone perimeter. + +Blackwood Engine 12 (Resource ID: ENG-12), ordered at 06:55 hours, arrived at 07:05 hours (Arrived: True) and is actively operating water curtains for vapor suppression. + +County Water Tender 3 (Resource ID: WT-03), ordered at 07:10 hours, arrived at 07:25 hours (Arrived: True) and is supplying continuous water flow to Engine 12. + +Sheriff Patrol Unit Group Alpha (Resource ID: SO-ALPHA), ordered at 06:55 hours, arrived at 07:10 hours (Arrived: True) and is maintaining road closures and executing downwind evacuation notices. + +CleanHarbor Spill Response Team (Resource ID: CH-SRT-1), ordered at 07:30 hours with an estimated time of arrival at 09:00 hours (Arrived: False), is currently en route with 1,500 feet of river containment boom and vacuum recovery equipment. + +State EPA Air Monitoring Drone Unit (Resource ID: EPA-DRONE-2), ordered at 07:40 hours with an estimated time of arrival at 08:45 hours (Arrived: False), is en route to provide real-time thermal and chemical plume mapping. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics201_3.txt b/benchmark/datasets/narratives/ics201_3.txt new file mode 100644 index 00000000..a1c04a37 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_3.txt @@ -0,0 +1,29 @@ +Cypress Bend Tanker Collision Mockup Incident Briefing (ICS 201) +The Cypress Bend Tanker Collision incident, designated under incident number US-CG-D8-2026-0921, was formally initiated on September 21, +2026, at 14: 15 hours. + +The operational landscape, mapped with standard navigational charts and GIS symbology, encompasses a total area of operations spanning a 4.0-mile radius centered at Intracoastal Waterway Mile Marker 245. Within this zone, the primary incident site area is located at the confluence of the Intracoastal Waterway and Cypress Bayou. The actively impacted areas include a 300-yard containment zone surrounding a punctured double-hulled barge discharging Heavy Fuel Oil (HFO 380) at approximately 30 gallons per minute, alongside an impacted shoreline extending 1.5 miles south along the eastern bank of Cypress Bayou. Threatened areas downstream include the Grand Cypress Mangrove Sanctuary located 2.0 miles south-southeast and the Delta Commercial Oyster Leases 3.5 miles down-river. Overflight results from US Coast Guard Aviation Unit 65 confirm a surface slick measuring 2.0 miles long by 100 yards wide moving with the ebb tide, with visible heavy sheen accumulating along the shoreline. Dispersion trajectories model the waterborne oil plume moving South-Southeast at 1.8 knots with local coastal wind vectors blowing from the Northwest at 12 knots. + +The situation summary indicates that a tugboat towing two loaded asphalt barges collided with an anchored commercial tanker at 13: 50 hours, rupturing Cargo Tank #2 starboard. The comprehensive health and safety briefing identifies primary hazards as hydrogen sulfide gas accumulation from the fuel oil cargo, inhalation of petroleum hydrocarbons, direct dermal irritation, slip-and-fall hazards on oily vessel decks and muddy riverbanks, responder heat exhaustion in humid 91°F conditions, and water safety/drowning risks during vessel-to-vessel transfers. Necessary measures to protect responders include establishing a 1, +000-foot Maritime Safety Zone, requiring Level C personal protective equipment with half-mask organic vapor respirators and personal flotation devices for all over-water personnel, continuous multi-gas air monitoring near the breached tank, mandatory work-rest cycles with hydration stations in the staging area, and operating a vessel-based decontamination station anchored at the cold zone boundary. This section was formally prepared by Commander Elena Rostova, serving as the Initial Planning Section Chief, who applied her signature on September 21, +2026, at 15: 30 hours. + +Current and planned objectives prioritize environmental protection and spill isolation: complete primary deflection boom deployment across the mouth of Cypress Bayou by 16: 30 hours; secure mechanical patching or product transfer (lightering) of Cargo Tank #2 by 18: 00 hours; deploy skimming vessels to recover free-floating surface product before nightfall at 19: 30 hours; and establish a full Joint Information Center to handle media inquiries by 20: 00 hours. The chronological sequence of current and planned actions, strategies, and tactics began at 14: 00 hours, when Coast Guard Station Cypress Bend dispatched Sector Patrol Craft and issued a Notice to Mariners closing the waterway. At 14: 30 hours, Captain M. Thorne established Incident Command and deployed first-responder skimmer boats. At 15: 00 hours, Port Authority Hazmat Teams completed initial safety sweeps and confirmed zero structural fire threats. At 15: 30 hours, marine salvage engineers boarded the barge to inspect damage and prep transfer pumps. Planned actions include dropping 2, +000 feet of hard boom across the mangrove inlet at 16: 00 hours, initiating lightering operations to pump fuel out of the damaged tank at 17: 00 hours, and deploying an evening drone overflight at 18: 30 hours to track thermal slick drift patterns. + +The current command and general staff organization operates under a Unified Command structure led by Incident Commanders Captain M. Thorne (US Coast Guard), Director D. Sterling (State Department of Environmental Quality), and H. Vance (Cypress Towing Co., Responsible Party). The Command Staff features Safety Officer Lieutenant Commander J. Ruiz, Public Information Officer C. Alvarez, and Liaison Officer Agent P. Brooks. The General Staff comprises Operations Section Chief G. Morales, Planning Section Chief Commander Elena Rostova, Logistics Section Chief R. O'Connor, and Finance/Administration Section Chief M. Lin. Additional tactical positions established within the operational structure include Salvage & Engineering Group Supervisor Chief Specialist K. Vance and Wildlife Rescue Leader Dr. A. Mercer. + +The resource summary details the operational status and tracking of critical response assets: + +USCG Marine Safety Detachment 1 (Resource ID: USCG-MSD-01), ordered on September 21 at 14: 00 hours, arrived on scene at 14: 25 hours (Arrived: True) and is currently enforcing the maritime safety zone and conducting gas monitoring. + +Cypress Port Skimmer Vessel 4 (Resource ID: SKIM-04), ordered at 14: 10 hours, arrived at 14: 35 hours (Arrived: True) and is actively skimming free surface product around the barge stern. + +Delta Salvage Tug 'Warrior' (Resource ID: TUG-WARRIOR), ordered at 14: 30 hours, arrived at 15: 15 hours (Arrived: True) and is providing stabilizing push-support and powering transfer pumps. + +State DEQ Water Sampling Craft (Resource ID: DEQ-BOAT-2), ordered at 14: 20 hours, arrived at 15: 00 hours (Arrived: True) and is collecting downstream water samples and turbidity readings. + +National Spill Response Team Barge 8 (Resource ID: NSRT-B-08), ordered at 14: 45 hours with an estimated time of arrival at 16: 30 hours (Arrived: False), is currently en route with 3, +000 feet of ocean containment boom and heavy skimmers. + +Gulf Coast Wildlife Rescue Unit (Resource ID: GC-WILD-1), ordered at 15: 10 hours with an estimated time of arrival at 17: 15 hours (Arrived: False), is en route to set up a oily bird stabilization and staging facility. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics201_4.txt b/benchmark/datasets/narratives/ics201_4.txt new file mode 100644 index 00000000..d3f129a4 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_4.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Blackwood River Chemical Spill + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Blackwood River Chemical Spill (Incident Number: USCG-EPA-R4-2026-0813) was formally initiated on 08/13/2026 at 07:15 EDT. The designated total area of operations encompasses a 3.5-mile radius including the Blackwood Industrial Park, Mile Marker 42 of the Blackwood River, and the adjacent State Route 104 transportation corridor. The primary incident site is localized at Storage Tank #4 within the Apex Chemical Tank Farm (MM 42.1, East Bank), where a structural shell fracture is leaking concentrated Styrene Monomer. Currently impacted areas include 1,200 meters of the river surface displaying heavy chemical sheening and a volatile organic compound (VOC) vapor plume extending 800 yards downwind (East-Northeast) across State Route 104. Primary threatened locations include the Blackwood Municipal Drinking Water Intake situated 3.0 miles downstream at MM 39.1 and the Blackwood Marsh Ecological Reserve located 1.5 miles downstream. Overflight reconnaissance conducted via USCG Aux Drone Flight #1 confirmed a 200-yard wide chemical slick migrating South-Southwest at 1.8 knots with moderate shore oiling along the east bank. Dispersion trajectories indicate the airborne VOC plume is tracking East-Northeast at 6 mph towards unpopulated forestry, while the aquatic slick moves downstream toward the municipal water intake. Impacted shorelines are currently restricted to the east bank riprap and adjacent low-lying mudflats from MM 42.1 to MM 41.3. Tactical graphics and maps oriented North vertically depict Staging Area A at Westside High School, Boom Site 1 (Deflection), Boom Site 2 (Containment), the Mobile Command Post at County Fire Station 12, a 500-foot Exclusion Zone perimeter, and downstream drinking water intake protection zones. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 06:45 EDT on 08/13/2026, a catastrophic seal failure occurred on Tank #4 at the Apex Chemical Facility, releasing approximately 8,500 gallons of liquid Styrene Monomer into secondary containment, with overflow entering the Blackwood River via storm drain outfall #3 at an estimated 30 gpm. A secondary vapor cloud formed across State Route 104, triggering immediate road closures and precautionary evacuations of 60 surrounding industrial properties. Primary health and safety hazards include acute inhalation toxicity from styrene vapors (causing narcotic effects, central nervous system depression, and respiratory irritation), flammability risk (flash point 88°F), dermal chemical burns, slip/trip/fall hazards along steep river banks, and ambient heat stress (current temperature 91°F). Necessary protective measures require establishing a strict 500-foot Exclusion Zone; mandating Level B PPE (supplied air SCBA with continuous PID/LEL/O2 monitoring) for all Hot Zone operations; Level C PPE (full-face APR with organic vapor cartridges) within the Warm Zone; continuous perimeter air monitoring at CP and downwind baselines; and operating a formal wet-decontamination corridor at Facility Gate 2. This briefing was formally prepared by Sarah L. Jenkins, Planning Section Chief, signed Sarah L. Jenkins, on 08/13/2026 at 09:30 EDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 (08/13/2026) are: (1) Maintain life safety and enforce the 500-foot exclusion perimeter by 08:00 hours; (2) Complete containment booming at Boom Site 1 (MM 41.5) by 10:30 hours to protect downstream drinking water intake; (3) Secure the leak source on Apex Tank #4 and complete product transfer by 12:00 hours; (4) Perform continuous air monitoring downwind along Route 104 and publish public safety advisories by 11:00 hours; and (5) Transition to full Unified Command (USCG, EPA, State DEQ, County Hazmat) by 10:00 hours. The chronological timeline of tactical actions proceeds as follows: At 07:15, initial alarm dispatch occurred, sending County Fire Engine 12 and Hazmat 1 to establish the 500-foot perimeter. At 07:30, Local Command was established by Chief Henderson, initiating facility emergency shutdown and closing Route 104. At 07:55, Regional Hazmat Team 4 arrived, initiating perimeter PID air monitoring and setting up the decon corridor at Gate 2. At 08:20, USCG Sector Strike Team and EPA On-Scene Coordinator arrived on scene to establish Unified Command at Fire Station 12. At 08:45, Spill Response Vessel River Guardian deployed 1,000 feet of hard containment boom at MM 41.5 (Boom Site 1). Planned tactical actions include: At 09:15, Entry Team 1 enters the Hot Zone under Level B PPE to secure Tank #4 bottom manifold valve; at 10:00, deploy secondary sorbent deflection boom array at Boom Site 2 (MM 40.2) upstream of the drinking water intake; and at 11:30, commence vacuum truck skimmer recovery of pooled styrene at storm drain outfall #3. + +Paragraph 4: Command & General Staff Organization Structure The Incident Organization operates under a Unified Command structure comprising Unified Incident Commanders Captain M. Ross (USCG Unified Command IC), OSC E. Vance (EPA Co-IC), Chief R. Henderson (County Fire IC), and J. Mercer (Apex Facility RP IC). Command Staff includes Lt. Commander David Miller as Safety Officer, Elena Gomez (County OEM) as Public Information Officer, and Captain Arthur Pendelton (State Police) as Liaison Officer. General Staff operations are directed by Battalion Chief Marcus Brody as Operations Section Chief, Sarah L. Jenkins as Planning Section Chief, Karen Albright as Logistics Section Chief, and Robert Sterling as Finance/Administration Section Chief. Specialized operational units are led by Captain Donald Kross (Hazmat 1) serving as Hazmat Group Supervisor and Lt. Timothy Vance (USCG) serving as Waterborne Operations Branch Director. + +Paragraph 5: Resource Summary & Tactical Assignments Assigned operational resources are tracked as follows: (1) Hazmat Unit (Identifier: HM-01), ordered 08/13/2026 07:20, Status: Arrived (ETA N/A), assigned to conduct initial air monitoring and execute Hot Zone valve isolation; (2) Fire Engine (Identifier: ENG-12), ordered 08/13/2026 07:15, Status: Arrived (ETA N/A), assigned to secure outer perimeter and maintain foam fire standby; (3) USCG Strike Team (Identifier: USCG-ST-04), ordered 08/13/2026 07:40, Status: Arrived (ETA N/A), assigned to assist Unified Command and oversee waterborne operations; (4) Spill Response Vessel (Identifier: RV-RG-02), ordered 08/13/2026 08:00, Status: Arrived (ETA N/A), assigned to deploy 1,000 ft containment boom at Boom Site 1; (5) Vacuum Truck Unit (Identifier: VAC-99), ordered 08/13/2026 08:30, Status: En Route (ETA 08/13/2026 10:45), assigned to perform liquid product recovery at outfall #3 upon arrival; and (6) Air Monitoring Unit (Identifier: AMR-03), ordered 08/13/2026 08:15, Status: En Route (ETA 08/13/2026 09:45), assigned to transport high-sensitivity PID and Jerome mercury/VOC analyzers to establish downwind grid monitoring. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics201_5.txt b/benchmark/datasets/narratives/ics201_5.txt new file mode 100644 index 00000000..22ec49c2 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_5.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Port of Houston Sulfuric Acid Tanker Leak + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Port of Houston Sulfuric Acid Tanker Leak (Incident Number: USCG-EPA-R6-2026-1014) was formally initiated on 10/14/2026 at 11:20 CDT. The designated total area of operations covers a 2.5-mile radius centered around Berth 42 of the Houston Ship Channel Tank Terminal. The primary incident site is localized at Tanker Berth 42 (MM 51.2, West Bank), where a manifold failure on the chemical parcel tanker M/T Stolt Synergy released concentrated 98% sulfuric acid onto the vessel deck and into secondary containment, with runoff entering the waterway. Currently impacted areas include a 500-yard perimeter along Berth 42 exhibiting localized water discoloration and acidic vapor mist, extending 400 yards downwind across adjacent industrial docks. Primary threatened locations include the San Jacinto Battleground Historic Site 1.2 miles down-river and the Channelview Residential Neighborhood 2.0 miles downwind to the North-Northwest. Overflight reconnaissance conducted via USCG Air Station Houston Helicopter 6512 confirmed a localized 150-yard plume dissipating in the ship channel with continuous water sampling underway. Dispersion trajectories model the acidic vapor mist tracking North-Northwest at 8 mph, while waterborne runoff moves with the ebb tide East-Southeast at 1.2 knots. Impacted shorelines are confined to 400 yards of concrete bulkhead and wooden fender piling at Berth 42. Tactical graphics and maps oriented North vertically depict Staging Area Bravo at Jacintoport Terminal, Spill Boom Site 1, Command Post at USCG Sector Houston-Galveston, Exclusion Zone boundaries (1,000 ft radius), and ship channel navigation safety zones. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 10:50 CDT on 10/14/2026, a high-pressure discharge hose burst during offloading operations at Berth 42, spilling approximately 4,200 gallons of 98% sulfuric acid. Heat generated by exothermic reaction with bilge water created a dense corrosive acid mist cloud over the berth. Primary health and safety hazards include severe skin necrosis upon contact, permanent ocular damage, pulmonary edema from acid mist inhalation, extreme heat reaction when mixed with water, and slip hazards on degraded berth decking. Protective measures enforce a 1,000-foot Exclusion Zone; Level A chemical suit protection with SCBA for Hot Zone entries; Level B protective suits with SCBA for Warm Zone support; continuous pH water sampling and real-time acid mist sensor monitoring; and a mandatory dual-stage lime neutralization decontamination wash at Gate 4. This briefing was formally prepared by Commander Richard Croft, Planning Section Chief, signed Richard Croft, on 10/14/2026 at 13:00 CDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Enforce a 1,000-foot Exclusion Zone around Berth 42 and secure vessel offloading by 12:00 hours; (2) Complete neutralization of deck spill using sodium bicarbonate slurry by 14:00 hours; (3) Deploy chemical neutralization boom array at Berth 42 slip by 13:30 hours; (4) Monitor atmospheric acid mist levels along Channelview perimeter to ensure public safety by 15:00 hours; and (5) Transition to Unified Command (USCG, EPA, TCEQ, RP) by 12:30 hours. Chronological tactical timeline: At 11:20, initial emergency response initiated; Port Authority Fire Engines 81 and 82 respond; 1,000-foot safety perimeter set up. At 11:45, Sector Command established by Captain H. Vance; Houston Ship Channel traffic restricted to one-way slow speed. At 12:15, Port Hazmat Team 1 arrives on scene to establish pH monitoring grid and set up neutralization decon corridor. At 12:50, USCG Strike Team arrives; initiates shipboard inspection and containment audit. At 13:15, Stolt Tankers RP response vessel deploys chemical sorbent boom around M/T Stolt Synergy. Planned actions: At 14:00, Entry Team Alpha under Level A PPE executes emergency valve shutdown on shipboard manifold; at 14:30, begin high-volume sodium bicarbonate dry-powder application on vessel deck; at 16:00, perform secondary overflight to confirm complete plume dissipation. + +Paragraph 4: Command & General Staff Organization Structure Operational command functions under a Unified Command structure comprising Unified Incident Commanders Captain H. Vance (USCG Unified Command IC), OSC M. Reynolds (EPA Co-IC), Chief D. Garcia (Port Authority Fire IC), and K. Lindqvist (Stolt Tankers RP IC). Command Staff includes Commander Thomas Blake as Safety Officer, Laura Martinez (Port Authority) as Public Information Officer, and Inspector R. Sterling (TCEQ) as Liaison Officer. General Staff consists of Battalion Chief A. Ross as Operations Section Chief, Commander Richard Croft as Planning Section Chief, Sandra Keller as Logistics Section Chief, and Wayne Miller as Finance/Admin Section Chief. Specialized positions include Chemical Hazards Specialist Dr. V. Patel and Vessel Boarding Group Supervisor Lt. J. Thorne. + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational units: (1) Port Authority Hazmat Team (Identifier: HZM-PORT-1), ordered 10/14/2026 11:25, Status: Arrived (ETA N/A), executing pH sampling grid and Hot Zone entry prep; (2) Port Fire Engine 81 (Identifier: ENG-81), ordered 10/14/2026 11:20, Status: Arrived (ETA N/A), securing 1,000 ft landward perimeter and foam standby; (3) USCG Gulf Strike Team (Identifier: USCG-GST-02), ordered 10/14/2026 11:50, Status: Arrived (ETA N/A), supervising vessel entry protocol and safety verification; (4) Chemical Response Vessel Acid Contain 1 (Identifier: RV-AC-01), ordered 10/14/2026 12:00, Status: Arrived (ETA N/A), deployed chemical sorbent boom around vessel berth; (5) Lime Neutralization Truck Unit (Identifier: NEUT-TRK-05), ordered 10/14/2026 12:30, Status: En Route (ETA 10/14/2026 14:15), delivering 10 tons of dry sodium bicarbonate slurry; and (6) Mobile Air Sampling Van (Identifier: AIR-SAM-02), ordered 10/14/2026 12:10, Status: En Route (ETA 10/14/2026 13:45), transporting real-time SO3/acid mist photoionization sensors. diff --git a/benchmark/datasets/narratives/ics201_6.txt b/benchmark/datasets/narratives/ics201_6.txt new file mode 100644 index 00000000..d36e9cc2 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_6.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Cascade Pass Chlorine Railcar Derailment + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Cascade Pass Chlorine Railcar Derailment (Incident Number: NTSB-EPA-R10-2026-1102) was formally initiated on 11/02/2026 at 04:45 PST. The total area of operations encompasses a 5.0-mile radius centered around BNSF Railway Milepost 218.4 near Skykomish, WA. The primary incident site is located at Rail Milepost 218.4, where five freight cars derailed, including pressurized tank car DOT-105J500W (BNSF-77402) containing liquefied chlorine gas with damaged protective valve housing. Currently impacted areas include a 1,500-foot vapor dispersion zone along the rail right-of-way and adjacent State Route 2. Threatened areas include the Town of Skykomish situated 1.8 miles West downwind and the South Fork Skykomish River salmon spawning habitat located 300 feet South. Overflight reconnaissance conducted via Washington State Patrol FLIR Helicopter WSP-AIR-2 confirmed a localized green-yellow chlorine cloud hugging the ravine floor and drifting West-Northwest at 4 mph. Dispersion trajectories model the airborne toxic plume tracking West-Northwest along the State Route 2 corridor at 4 mph, with zero liquid chemical runoff entering waterways. Impacted shorelines are unimpacted, with ground contamination restricted to northern ravine embankment slopes. Tactical graphics and maps oriented North vertically depict Staging Area Charlie at Stevens Pass Maintenance Yard, Mobile Command Post at Skykomish School District Gym, a 1.5-mile Exclusion Zone boundary, and State Route 2 traffic closure checkpoints. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 04:15 PST on 11/02/2026, a mountain derailment caused five freight cars to leave the tracks. Tank car BNSF-77402 sustained severe dome housing damage, resulting in a low-rate pressurized vapor release of toxic chlorine at approximately 15 lbs/min. State Route 2 was closed immediately and a mandatory evacuation was ordered for 250 residents of Skykomish. Primary health and safety hazards include severe pulmonary edema risk from chlorine gas inhalation, ocular and skin chemical burns, acute respiratory collapse, mountain hypothermia (ambient temp 34°F), and steep terrain slip hazards. Protective measures enforce a 1.5-mile Exclusion Zone; mandatory Level A vapor-tight encapsulated suits with SCBA for all Hot Zone entry personnel; Level B suits for Warm Zone support; continuous electrochemical chlorine sensor monitoring at CP baselines; mandatory indoor shelter-in-place for outer perimeters; and a heated wet-decontamination trailer wash at Staging Area Charlie. This briefing was formally prepared by Lt. Colonel Alan Vance, Planning Section Chief, signed Alan Vance, on 11/02/2026 at 07:15 PST. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Complete mandatory evacuation of Skykomish township within 1.5-mile radius by 07:30 hours; (2) Perform Hot Zone entry to apply Emergency B-Kit capping assembly on chlorine railcar dome by 09:30 hours; (3) Establish continuous multi-point perimeter air monitoring along State Route 2 by 08:00 hours; and (4) Secure rail right-of-way and establish Unified Command by 07:00 hours. Chronological tactical timeline: At 04:45, initial alarm dispatch occurred; Skykomish Fire and King County Sheriff units respond, closing State Route 2 at Milepost 215. At 05:15, Local Command was established by Chief D. Olson; mandatory evacuation sirens activated across Skykomish. At 05:50, Regional Hazmat Team 3 and BNSF Response Team arrived on scene to initiate perimeter air testing. At 06:30, WSP FLIR helicopter completed thermal overflight mapping plume dispersion. At 07:00, Unified Command was established at Skykomish School Gym with EPA Region 10 and BNSF RP. Planned actions: At 08:15, Entry Team 1 under Level A PPE initiates B-Kit capping tool installation on railcar valve dome; at 09:30, perform pressure test and seal verification of capping assembly; at 11:00, complete environmental health review to evaluate lifting outer zone evacuation orders. + +Paragraph 4: Command & General Staff Organization Structure Operational command functions under a Unified Command structure comprising Unified Incident Commanders Captain E. Miller (WSP Unified Command IC), OSC R. Brooks (EPA R10 Co-IC), Chief D. Olson (Skykomish Fire IC), and M. Campbell (BNSF Railway RP IC). Command Staff includes Captain Marcus Vance as Safety Officer, Jennifer Hayes (WSDOT) as Public Information Officer, and Deputy S. Kowalski (KCSO) as Liaison Officer. General Staff operations are managed by Battalion Chief T. Higgins as Operations Section Chief, Lt. Colonel Alan Vance as Planning Section Chief, David Sterling as Logistics Section Chief, and Patricia Ross as Finance/Admin Section Chief. Specialized tactical roles include Rail Hazmat Specialist G. Peterson (BNSF) and Evacuation Group Supervisor Sgt. H. Lin (KCSO). + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational assets: (1) King County Hazmat 3 (Identifier: HZM-33), ordered 11/02/2026 05:00, Status: Arrived (ETA N/A), preparing Level A Entry Team 1 and Emergency B-Kit capping assembly; (2) Skykomish Engine 11 (Identifier: ENG-11), ordered 11/02/2026 04:45, Status: Arrived (ETA N/A), securing State Route 2 East closure checkpoint; (3) BNSF Emergency Response Unit (Identifier: BNSF-ER-01), ordered 11/02/2026 05:15, Status: Arrived (ETA N/A), on scene with railcar specialized capping kits; (4) WSP Aviation FLIR Helicopter (Identifier: WSP-AIR-2), ordered 11/02/2026 05:30, Status: Arrived (ETA N/A), completed thermal imagery mapping of plume drift; (5) Mobile Decontamination Trailer (Identifier: DECON-04), ordered 11/02/2026 05:45, Status: En Route (ETA 11/02/2026 08:30), en route to establish warm water decon at Staging Area Charlie; and (6) Hazmat Air Monitoring Recon (Identifier: AIR-MON-10), ordered 11/02/2026 06:00, Status: En Route (ETA 11/02/2026 07:45), en route with multi-gas chlorine sensors. diff --git a/benchmark/datasets/narratives/ics201_7.txt b/benchmark/datasets/narratives/ics201_7.txt new file mode 100644 index 00000000..b80073b6 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_7.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Silverwood Reservoir Crude Oil Pipeline Rupture + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Silverwood Reservoir Crude Oil Pipeline Rupture (Incident Number: CalOES-EPA-R9-2026-0518) was formally initiated on 05/18/2026 at 13:10 PDT. The total area of operations encompasses a 4.0-mile radius including Trans-California Pipeline MM 87.3, Sawpit Canyon, and the Silverwood Reservoir basin. The primary incident site is located at Valve Station 12 along Trans-California Pipeline MM 87.3 in Sawpit Canyon, where a 12-inch pressurized crude line ruptured. Currently impacted areas include 800 yards of Sawpit Canyon creek bed carrying crude oil towards Silverwood Lake, with a 30-acre surface oil sheen in the reservoir South arm. Primary threatened locations include the Mojave Water Agency Municipal Pumping Plant situated 1.5 miles North and Silverwood Lake State Recreation Area campgrounds located 0.8 miles West. Overflight reconnaissance conducted via Cal FIRE Recon Aircraft 240 confirmed heavy black crude pooling in Sawpit Canyon and a 400-yard wide oil ribbon entering Silverwood Lake. Dispersion trajectories model the waterborne crude oil slick moving North at 1.0 knot towards the main reservoir basin, while the airborne volatile organic cloud tracks Southeast at 7 mph into canyon slopes. Impacted shorelines encompass 1.2 miles of rocky reservoir shoreline and marsh vegetation along the Sawpit Canyon inlet. Tactical graphics and maps oriented North vertically depict Staging Area Delta at Silverwood Lake Marina, Incident Command Post at Hesperia Fire Station 30, a 1,000-foot Exclusion Zone perimeter, Deflection Boom Sites A and B, and municipal water intake protection zones. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 12:40 PDT on 05/18/2026, a hillside land movement ruptured a 12-inch crude oil pipeline at MM 87.3, discharging approximately 12,000 gallons of heavy crude oil into Sawpit Canyon creek before automated valves shut off flow. The oil flowed downstream into Silverwood Reservoir, threatening southern California drinking water supplies. Primary health and safety hazards include toxic benzene vapor inhalation, volatile organic compound exposure, acute flammability (flash point 65°F), wildfire ignition risk in dry brush, slip-and-fall hazards on oily terrain, and severe heat exhaustion (ambient temp 94°F). Protective measures enforce a mandatory 1,000-foot Exclusion Zone; Level B PPE with SCBA for initial Hot Zone creek entry; Level C PPE with organic vapor respirators for shoreline booming teams; continuous PID air monitoring downwind; mandatory wildland fire standby team with AFFF foam; and establishment of dual-basin wash decontamination at Marina Ramp 2. This briefing was formally prepared by Captain Rachel Brooks, Planning Section Chief, signed Rachel Brooks, on 05/18/2026 at 15:30 PDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Enforce 1,000-foot Exclusion Zone and secure pipeline isolation by 14:00 hours; (2) Deploy 2,000 feet of containment boom across Sawpit Canyon inlet by 16:30 hours to prevent oil migration to water intake; (3) Evacuate Silverwood Lake State Recreation Area campgrounds by 15:00 hours; (4) Initiate skimmer boat oil recovery in South arm of reservoir by 17:00 hours; and (5) Establish Unified Command (Cal OES, EPA R9, USFS, San Bernardino County Fire, Pipeline RP) by 14:30 hours. Chronological tactical timeline: At 13:10, alarm dispatch occurred; San Bernardino County Fire Engine 30 and USFS Crew 4 respond to set up initial safety perimeter. At 13:40, Local Command was established by Chief M. Thorne; Trans-California Pipeline operators confirm remote valve isolation at MM 85 and MM 90. At 14:15, State Parks Rangers complete full evacuation of Silverwood Lake campgrounds (150 visitors evacuated). At 14:45, Cal OES and EPA OSC arrive on scene; Unified Command established at Station 30. At 15:15, Clean Harbors Spill Response Vessel deploys 1,200 feet of hard boom at Sawpit Inlet (Boom Site A). Planned actions: At 16:00, deploy secondary sorbent deflection boom array at Boom Site B upstream of Mojave Water Intake; at 17:00, vacuum skimmer vessel Lake Clean 1 initiates surface crude extraction in South arm; at 18:30, complete initial shoreline oiling assessment along Sawpit Canyon inlet. + +Paragraph 4: Command & General Staff Organization Structure Operational command operates under a Unified Command structure comprising Unified Incident Commanders OSC C. Martinez (EPA R9 Co-IC), Chief M. Thorne (SBCo Fire IC), Chief Inspector A. Kim (Cal OES), and W. Vance (Pipeline RP IC). Command Staff includes Captain Gregory Hall as Safety Officer, Samantha Norris (Cal OES) as Public Information Officer, and Ranger D. Stevens (State Parks) as Liaison Officer. General Staff operations are directed by Battalion Chief Kevin Ross as Operations Section Chief, Captain Rachel Brooks as Planning Section Chief, Megan Taylor as Logistics Section Chief, and Jason Wu as Finance/Admin Section Chief. Specialized positions include Environmental Unit Leader Dr. L. Arispe (USFS) and Waterborne Recovery Supervisor Captain B. Walsh (Clean Harbors). + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational assets: (1) San Bernardino Hazmat Engine (Identifier: ENG-30), ordered 05/18/2026 13:10, Status: Arrived (ETA N/A), operating perimeter vapor monitoring and fire standby; (2) USFS Wildland Handcrew (Identifier: CREW-04), ordered 05/18/2026 13:15, Status: Arrived (ETA N/A), clearing brush and building containment dikes in Sawpit Canyon; (3) State Parks Ranger Unit (Identifier: PARK-12), ordered 05/18/2026 13:20, Status: Arrived (ETA N/A), completed campground evacuations and road closures; (4) Clean Harbors Spill Vessel (Identifier: RV-LC-01), ordered 05/18/2026 14:00, Status: Arrived (ETA N/A), deployed 1,200 ft hard boom at Sawpit Canyon inlet; (5) Heavy Vacuum Skimmer Boat (Identifier: SKIM-02), ordered 05/18/2026 14:30, Status: En Route (ETA 05/18/2026 16:45), en route from San Pedro for reservoir skimmer recovery; and (6) Air Monitoring Recon Unit (Identifier: AIR-MON-08), ordered 05/18/2026 14:10, Status: En Route (ETA 05/18/2026 15:45), en route with photoionization detectors and benzene gas sensors. diff --git a/benchmark/datasets/narratives/ics201_8.txt b/benchmark/datasets/narratives/ics201_8.txt new file mode 100644 index 00000000..89fb93e7 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_8.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Delaware Bay Container Vessel Collision & Fuel Oil Spill + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Delaware Bay Container Vessel Collision & Fuel Oil Spill (Incident Number: USCG-D5-2026-0629) was formally initiated on 06/29/2026 at 22:40 EDT. The designated total area of operations covers a 6.0-mile radius centered at Delaware Bay Shipping Channel Buoy 14 near Lewes, DE. The primary incident site is located at Delaware Bay Channel Buoy 14 (38.85°N, 75.12°W), where container vessel M/V Atlantic Voyager collided with a bulk carrier, breaching fuel tank #3 port side. Currently impacted areas include a 3.5-mile long heavy oil sheen extending East-Southeast across the bay channel towards Cape Henlopen State Park. Primary threatened locations include Prime Hook National Wildlife Refuge situated 4.0 miles Northwest and Cape Henlopen Tidal Salt Marshes located 2.5 miles South. Overflight reconnaissance conducted via USCG HC-144 Ocean Sentry aircraft confirmed a 300-yard wide slick of Intermediate Fuel Oil (IFO 380) drifting with the flood tide. Dispersion trajectories model the waterborne oil slick migrating East-Southeast at 2.2 knots toward Cape Henlopen under coastal winds of 14 knots from the West-Northwest. Impacted shorelines encompass 2.0 miles of outer sandy beach and dune lines along Cape Henlopen State Park. Tactical graphics and maps oriented North vertically depict Staging Area Echo at Lewes Ferry Terminal, Incident Command Post at USCG Sector Delaware Bay Headquarters, a 1,000-yard Maritime Safety Zone, Boom Sites 1, 2, and 3, and wildlife protection zones. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 22:15 EDT on 06/29/2026, container vessel M/V Atlantic Voyager collided with anchored bulk carrier M/V Pacific Trader near Buoy 14. Port fuel tank #3 breached, discharging approximately 25,000 gallons of heavy Intermediate Fuel Oil (IFO 380) before vessel operators completed internal fuel transfer to starboard tanks. Primary health and safety hazards include hydrocarbon vapor exposure (benzene/H2S), direct dermal toxicity, severe slip hazards on oily vessel hulls and shorelines, nighttime over-water operations, water drowning risk, and wildlife exposure risks. Protective measures enforce a 1,000-yard Maritime Safety Zone; mandatory Level C PPE with PFDs and half-mask organic vapor respirators for maritime responders; continuous H2S and PID air monitoring on response vessels; compulsory work-rest hydration cycles; and a mobile vessel decontamination station established at Lewes Pier. This briefing was formally prepared by Commander James Vance, Planning Section Chief, signed James Vance, on 06/30/2026 at 01:30 EDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Secure maritime safety perimeter and stabilize damaged vessel by 01:00 hours; (2) Deploy protective booming across Cape Henlopen salt marsh inlets by 04:00 hours; (3) Initiate offshore skimming operations using MSRC oil recovery vessels by 05:30 hours; (4) Activate wildlife rescue and rehab operations with Tri-State Bird Rescue by 06:00 hours; and (5) Establish Unified Command (USCG, EPA, Delaware DNREC, M/V Atlantic Voyager RP) by 02:00 hours. Chronological tactical timeline: At 22:40, alarm dispatch occurred; USCG Station Lewes response boats 45601 and 45602 respond to establish maritime safety zone. At 23:15, Local Command was established by Captain P. Hayes; Sector Delaware Bay requests MSRC oil spill response vessel mobilization. At 23:50, M/V Atlantic Voyager crew completes internal fuel transfer, halting active discharge from fuel tank #3. At 00:30, USCG HC-144 overflight completes IR sensor oil slick mapping. At 01:15, Delaware DNREC response team arrives at Lewes Command Post; Unified Command established. Planned actions: At 03:00, MSRC Response Vessel Delaware Responder deploys 2,500 feet of ocean containment boom at Buoy 14; at 04:30, deploy sorbent diversion boom at Cape Henlopen Inlet (Boom Site 2); at 06:00, Tri-State Bird Rescue team commences shoreline wildlife search along Cape Henlopen beaches. + +Paragraph 4: Command & General Staff Organization Structure Operational command operates under a Unified Command structure comprising Unified Incident Commanders Captain P. Hayes (USCG Unified Command IC), OSC T. Gallagher (EPA R3), Secretary A. Lawson (Delaware DNREC), and H. Lindemann (Atlantic Shipping RP IC). Command Staff includes Lt. Commander Mark Reynolds as Safety Officer, Chief Petty Officer Kelly Adams as Public Information Officer, and Inspector R. Thorne (DNREC) as Liaison Officer. General Staff operations are directed by Commander Frank Miller as Operations Section Chief, Commander James Vance as Planning Section Chief, Laura Bennett as Logistics Section Chief, and Charles Foster as Finance/Admin Section Chief. Specialized tactical positions include Scientific Support Coordinator Dr. E. Sullivan (NOAA) and Wildlife Branch Director Dr. C. Jenkins (Tri-State). + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational assets: (1) USCG Response Boat Medium (Identifier: RBM-45601), ordered 06/29/2026 22:40, Status: Arrived (ETA N/A), maintaining 1,000-yard safety zone around damaged vessel; (2) USCG Station Lewes RBM (Identifier: RBM-45602), ordered 06/29/2026 22:40, Status: Arrived (ETA N/A), conducting water sampling and perimeter safety watch; (3) MSRC Spill Vessel Delaware Responder (Identifier: RV-MSRC-01), ordered 06/29/2026 23:00, Status: Arrived (ETA N/A), deployed 2,500 ft ocean boom and preparing offshore skimmer; (4) USCG Ocean Sentry Aircraft (Identifier: USCG-HC144), ordered 06/29/2026 23:10, Status: Arrived (ETA N/A), completed IR slick mapping and overflight tracking; (5) Mobile Wildlife Rehabilitation Trailer (Identifier: WILD-REHAB-1), ordered 06/30/2026 00:15, Status: En Route (ETA 06/30/2026 05:45), en route to establish bird cleaning station at Lewes Pier; and (6) Shoreline Cleanup Strike Team (Identifier: SCAT-TEAM-1), ordered 06/30/2026 00:45, Status: En Route (ETA 06/30/2026 06:30), mobilizing for Cape Henlopen beach oil assessment. diff --git a/benchmark/datasets/narratives/ics201_9.txt b/benchmark/datasets/narratives/ics201_9.txt new file mode 100644 index 00000000..4e580cb9 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_9.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Oakridge Industrial Park Anhydrous Ammonia Release + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Oakridge Industrial Park Anhydrous Ammonia Release (Incident Number: EPA-R2-2026-0905) was formally initiated on 09/05/2026 at 08:15 EDT. The designated total area of operations covers a 2.0-mile radius centered at Cold Storage Logistics Building 7, Edison, NJ. The primary incident site is located at Mechanical Room 3 on the roof deck of Cold Storage Logistics Building 7 (100 Industrial Parkway), where an 8-inch high-pressure ammonia chiller line fractured. Currently impacted areas include a 400-yard vapor dispersion plume encompassing the Building 7 loading dock and northern section of Industrial Parkway. Primary threatened locations include the Meadowlands Residential Community situated 0.9 miles East downwind and the Edison Transit Center located 1.2 miles Southeast. Overflight reconnaissance conducted via Edison Police Drone Recon Unit DRONE-PD-1 confirmed a dense white fog cloud hovering over Roof Mechanical Room 3 and drifting East-Northeast at 5 mph. Dispersion trajectories model the airborne toxic anhydrous ammonia cloud migrating East-Northeast at 5 mph, with zero liquid chemical product entering storm drainage systems. Impacted shorelines are unimpacted, with contamination restricted to localized industrial ground surfaces and rooftop structures. Tactical graphics and maps oriented North vertically depict Staging Area Foxtrot at Edison High School parking lot, Command Post at Edison Fire Station 4, a 1,000-foot Exclusion Zone boundary, and Industrial Parkway traffic control points. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 07:45 EDT on 09/05/2026, a mechanical vibration failure severed an 8-inch liquid refrigeration line inside Mechanical Room 3 at Cold Storage Logistics, releasing approximately 3,000 lbs of anhydrous ammonia gas. Building 7 was evacuated immediately (85 employees), and a 1,000-foot perimeter isolation zone was established. Primary health and safety hazards include severe chemical asphyxiation, acute pulmonary edema, permanent ocular injury, cryogenic chemical freeze burns, flammability risk in enclosed spaces (LEL 15-28%), and physical fall hazards from roof decking. Protective measures enforce a mandatory 1,000-foot Exclusion Zone; Level A encapsulated vapor suits with SCBA for all roof entry personnel; Level B suits for ground backup teams; continuous electrochemical ammonia air monitoring at perimeter boundaries; high-volume water curtain deployment for vapor knockdown; and a mandatory dual-basin chemical wash decontamination corridor at Building 7 main entrance. This briefing was formally prepared by Captain Michael Vance, Planning Section Chief, signed Michael Vance, on 09/05/2026 at 10:30 EDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Enforce 1,000-foot Exclusion Zone and isolate Building 7 HVAC intake by 08:30 hours; (2) Perform roof entry under Level A PPE to manually isolate main receiver valve by 11:00 hours; (3) Deploy high-volume water curtain monitoring nozzles to suppress cloud drift by 09:30 hours; (4) Perform continuous downwind air monitoring along Meadowlands border by 09:00 hours; and (5) Establish Unified Command (Edison Fire, NJ DEP, EPA R2, Facility RP) by 08:45 hours. Chronological tactical timeline: At 08:15, initial alarm dispatch occurred; Edison Fire Engines 4 and 6 respond and assist Building 7 evacuation. At 08:35, Local Command was established by Chief E. Sullivan; Industrial Parkway closed at Kilmer Road. At 09:00, Middlesex County Hazmat Team 2 arrived to set up perimeter air monitoring grid and water fog curtain monitors. At 09:40, NJ DEP and EPA Region 2 OSC arrived on scene; Unified Command established at Station 4. At 10:15, drone recon flight confirmed water curtains reduced downwind cloud density by 60%. Planned actions: At 11:00, Entry Team 1 under Level A PPE executes emergency roof entry to isolate main refrigeration manifold; at 12:00, initiate mechanical ventilation of Building 7 interior through charcoal scrubber array; at 13:30, conduct clearance air sampling inside Building 7 prior to facility re-entry. + +Paragraph 4: Command & General Staff Organization Structure Operational command functions under a Unified Command structure comprising Unified Incident Commanders Chief E. Sullivan (Edison Fire IC), OSC H. Miller (EPA R2), Inspector G. Ross (NJ DEP), and T. Jenkins (Cold Storage RP IC). Command Staff includes Captain Robert Hayes as Safety Officer, Amanda Walsh (Edison OEM) as Public Information Officer, and Lt. D. Kincaid (Middlesex PD) as Liaison Officer. General Staff operations are directed by Battalion Chief Brian O'Connor as Operations Section Chief, Captain Michael Vance as Planning Section Chief, Karen Miller as Logistics Section Chief, and Steven Zhang as Finance/Admin Section Chief. Specialized positions include Refrigeration Systems Specialist C. Bauer (Cold Storage RP) and Air Monitoring Leader Lt. V. Patel (County Hazmat). + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational assets: (1) Middlesex County Hazmat 2 (Identifier: HZM-MC-02), ordered 09/05/2026 08:20, Status: Arrived (ETA N/A), operating water curtains and preparing Level A roof entry; (2) Edison Fire Engine 4 (Identifier: ENG-04), ordered 09/05/2026 08:15, Status: Arrived (ETA N/A), supplying high-pressure water to fog monitors for vapor suppression; (3) Edison Ladder Truck 2 (Identifier: LADDER-02), ordered 09/05/2026 08:15, Status: Arrived (ETA N/A), positioned for roof access and aerial water stream deployment; (4) Police Drone Recon Unit (Identifier: DRONE-PD-1), ordered 09/05/2026 08:30, Status: Arrived (ETA N/A), conducting continuous thermal and aerial monitoring of plume drift; (5) Mobile Mechanical Scrubber Unit (Identifier: VENT-SCRUB-3), ordered 09/05/2026 09:15, Status: En Route (ETA 09/05/2026 11:45), en route to perform positive pressure building scrubbing; and (6) High-Sensitivity Air Monitoring Truck (Identifier: AIR-TRK-07), ordered 09/05/2026 09:00, Status: En Route (ETA 09/05/2026 10:15), en route to monitor downwind Meadowlands residential perimeter. diff --git a/benchmark/datasets/narratives/ics202_2.txt b/benchmark/datasets/narratives/ics202_2.txt new file mode 100644 index 00000000..051994e4 --- /dev/null +++ b/benchmark/datasets/narratives/ics202_2.txt @@ -0,0 +1,9 @@ +### Blackwood Chemical Pipeline Breach Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from August 13, 2026, at 18:00 hours through August 14, 2026, at 06:00 hours, marking the critical night-shift rotation of the emergency response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to maintain 100% responder compliance with SCBA Level B PPE within the 500-foot Exclusion Zone throughout the night shift. The second objective dictates that containment teams must complete installation of 1,000 feet of sorbent containment boom array across Blackwood River at Boom Site 2 by 22:00 hours to protect downstream municipal water intake. The third objective requires hazardous materials entry teams to complete hot-tap product evacuation of damaged pipeline section by 02:00 hours to stop active anhydrous ammonia leakage. The fourth objective mandates that the Air Monitoring Group maintain continuous automated PID perimeter air monitoring downwind along Route 104 with telemetry reporting to CP every 30 minutes. The fifth and final objective requires the Planning and Logistics sections to finalize Operational Period 2 IAP shift plan and resource request documentation by 04:30 hours. + +The operational period command emphasis directs all supervisors to focus tactical operations on maintaining river boom integrity under rising night tides and ensuring strict atmospheric air monitoring during the overnight temperature drop when ammonia vapors hug low ground. Command explicitly prioritizes responder safety in low-visibility nighttime conditions over recovery speed. General situational awareness notes indicate that the local weather forecast predicts clear night skies, temperatures dropping to 58°F, and wind shifting from East-Northeast to North at 4 mph. Low-lying fog is expected near riverbanks between 02:00 and 06:00 hours. High vigilance is required for slick riverbank terrain and reduced nighttime visibility. Due to the hazardous chemical nature of the release, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the Mobile Command Post Briefing Trailer at Station 12. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the hot zone, and the localized Weather Forecast. Additional attachments compiled into the final briefing packet include the Ammonia Air Dispersion Model Map and the Pipeline Shutoff Schematic. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Sarah L. Jenkins, who reviewed and signed the form on August 13, 2026, at 16:30 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Chief R. Henderson, who signed off on the complete package on August 13, 2026, at 17:15 hours. diff --git a/benchmark/datasets/narratives/ics202_3.txt b/benchmark/datasets/narratives/ics202_3.txt new file mode 100644 index 00000000..d3f12108 --- /dev/null +++ b/benchmark/datasets/narratives/ics202_3.txt @@ -0,0 +1,9 @@ +### Port of Houston Sulfuric Acid Tanker Leak Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from October 14, 2026, at 07:00 hours through October 15, 2026, at 19:00 hours, marking the primary daytime operational shift of the chemical spill response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to ensure zero safety incidents by enforcing mandatory Level A chemical suit entry procedures for all vessel deck operations. The second objective dictates that neutralization teams must apply 10 tons of dry sodium bicarbonate slurry to neutralize pooled acid on Berth 42 deck by 12:00 hours. The third objective requires waterborne response teams to maintain 2,000 feet of chemical sorbent boom around M/T Stolt Synergy and conduct hourly river pH sampling to ensure zero downstream acid migration. The fourth objective mandates that vessel salvage specialists complete vessel hull structural integrity audit and offloading manifold pressure test by 15:00 hours. The fifth and final objective requires the Environmental Unit to conduct continuous public air monitoring along Channelview community boundary and issue real-time safety updates by 17:00 hours. + +The operational period command emphasis directs all supervisors to place primary tactical focus on safe chemical neutralization and pH stabilization in the ship channel water column. Personnel safety during high-temperature Level A suit operations takes absolute priority over operational speed. General situational awareness notes indicate daytime temperatures reaching 88°F with high humidity (78%), creating severe heat stress conditions for suit technicians. Winds are blowing from the Southeast at 9 mph. Work-rest cycles of 20 minutes active entry followed by 40 minutes hydration/cooling are strictly mandated. Due to the high hazard profile of concentrated sulfuric acid, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the USCG Sector Houston Command Center Safety Office. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 205a Communications List, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the port terminal, and localized Weather Forecast and Tides tables. Additional attachments compiled into the final briefing packet include the TCEQ Water Quality Monitoring Plan and the Vessel Cargo Stowage Plan. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Commander Richard Croft, who reviewed and signed the form on October 14, 2026, at 06:00 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Captain H. Vance, who signed off on the complete package on October 14, 2026, at 06:30 hours. diff --git a/benchmark/datasets/narratives/ics202_4.txt b/benchmark/datasets/narratives/ics202_4.txt new file mode 100644 index 00000000..0deec926 --- /dev/null +++ b/benchmark/datasets/narratives/ics202_4.txt @@ -0,0 +1,9 @@ +### Cascade Pass Chlorine Railcar Derailment Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from November 02, 2026, at 18:00 hours through November 03, 2026, at 06:00 hours, marking the critical overnight shift of the hazardous materials response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to enforce strict Level A PPE compliance and buddy system protocols during all night operations around the derailment site. The second objective dictates that hazardous materials capping teams must complete installation and torque testing of Emergency B-Kit capping assembly on chlorine tank car BNSF-77402 by 22:00 hours. The third objective requires air monitoring units to maintain perimeter chlorine air monitoring at 15-minute intervals along State Route 2 evacuation boundary. The fourth objective mandates that logistics staff operate heated decontamination trailer and warm hydration station continuously at Staging Area Charlie. The fifth and final objective requires the Environmental Unit to formulate environmental sampling plan and morning shift briefing package by 04:00 hours. + +The operational period command emphasis directs all supervisors to emphasize absolute safety of capping entry teams working under nighttime freezing conditions. Ensure continuous warm water decon availability to prevent suit icing. General situational awareness notes indicate overcast skies with mountain temperatures dropping to 28°F overnight, with snow flurries expected after 01:00 hours. Light winds are blowing from the West at 3 mph drifting toward the ravine floor. Icing hazards exist on rail ballast and steep access slopes, alongside high hypothermia hazards for standing security personnel. Due to toxic chlorine gas hazards, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the Skykomish School Gym Operations Briefing Room. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 206 Medical Plan, the ICS 207 Organization Chart, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the rail right-of-way, and the Mountain Weather Forecast package. Additional attachments compiled into the final briefing packet include the Railcar Capping Procedure Manual and the Chlorine Gas Toxicity Chart. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Lt. Colonel Alan Vance, who reviewed and signed the form on November 02, 2026, at 16:30 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Captain E. Miller, who signed off on the complete package on November 02, 2026, at 17:15 hours. diff --git a/benchmark/datasets/narratives/ics202_5.txt b/benchmark/datasets/narratives/ics202_5.txt new file mode 100644 index 00000000..52060a94 --- /dev/null +++ b/benchmark/datasets/narratives/ics202_5.txt @@ -0,0 +1,9 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from May 18, 2026, at 18:00 hours through May 19, 2026, at 06:00 hours, marking the overnight containment shift of the inland oil spill response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to maintain 100% safety record with zero heat injuries or toxic vapor exposures during night shift operations. The second objective dictates that marine spill response crews must secure 2,000 feet of containment boom across Sawpit Canyon inlet and maintain skimmer vessel recovery in South arm until 23:00 hours. The third objective requires pipeline repair crews to complete pipeline mechanical clamp repair on 12-inch main line at MM 87.3 by 02:00 hours. The fourth objective mandates that water sampling teams perform hourly water sampling upstream of Mojave Water Intake facility to ensure non-detectable hydrocarbon levels. The fifth and final objective requires the Planning Section to prepare Day 2 Operational Period Incident Action Plan and resource requests by 04:30 hours. + +The operational period command emphasis directs all supervisors to focus tactical priority on preventing any oil migration toward the municipal water intake. Night skimming operations must maintain illuminated safety perimeters and life-vest compliance at all times. General situational awareness notes indicate evening temperature cooling to 70°F with calm winds under 5 mph. Mountain lions and nocturnal wildlife have been reported near Sawpit Canyon inlet. Flashlight illumination is required along all shoreline walking paths due to steep, oily riprap. Due to potential drinking water contamination risks, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the Incident Command Post at Hesperia Fire Station 30. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of Silverwood Lake, and localized Weather and Reservoir Current Forecasts. Additional attachments compiled into the final briefing packet include the Silverwood Lake Water Sampling Map and the Pipeline Repair Safety Plan. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Captain Rachel Brooks, who reviewed and signed the form on May 18, 2026, at 16:30 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Chief M. Thorne, who signed off on the complete package on May 18, 2026, at 17:00 hours. diff --git a/benchmark/datasets/narratives/ics202_6.txt b/benchmark/datasets/narratives/ics202_6.txt new file mode 100644 index 00000000..c44f517a --- /dev/null +++ b/benchmark/datasets/narratives/ics202_6.txt @@ -0,0 +1,9 @@ +### Oakridge Industrial Park Anhydrous Ammonia Release Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Oakridge Industrial Park Anhydrous Ammonia Release incident under tracking number EPA-R2-2026-0905, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from September 05, 2026, at 07:00 hours through September 06, 2026, at 19:00 hours, marking the recovery and clearance operational shift of the industrial hazmat incident. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to enforce strict Level A/B PPE protocols and 1,000-foot Exclusion Zone control throughout daytime mechanical ventilation operations. The second objective dictates that mechanical ventilation teams must complete charcoal scrubber building ventilation of Building 7 interior to reduce interior ammonia concentrations below 15 ppm by 13:00 hours. The third objective requires structural engineers to conduct full structural and piping stress inspection of Roof Mechanical Room 3 by 15:00 hours. The fourth objective mandates that environmental sampling teams perform interior clearance air sampling across all 4 building quadrants by 17:00 hours. The fifth and final objective requires Liaison and Command Staff to submit final environmental decontamination report to NJ DEP and lift perimeter road closures by 18:30 hours. + +The operational period command emphasis directs all supervisors to focus operational efforts on safe indoor ventilation and structural validation. Ensure air monitoring teams continuously verify downwind residential air quality before discharging building exhaust. General situational awareness notes indicate mostly sunny conditions with a daytime high of 82°F. Winds are blowing from the West-Southwest at 7 mph. Thermal updrafts on Building 7 roof deck require secure safety tie-offs for all entry personnel. Due to high vapor pressure chemical hazards, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the Edison Fire Station 4 Command Post Conference Room. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 205a Communications List, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the industrial park, and localized Weather Forecast updates. Additional attachments compiled into the final briefing packet include the Building 7 Ventilation Engineering Plan and the NJ DEP Air Quality Standard Sheet. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Captain Michael Vance, who reviewed and signed the form on September 05, 2026, at 06:00 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Chief E. Sullivan, who signed off on the complete package on September 05, 2026, at 06:30 hours. From fe000b19dfeb7003a5feefa333987711a2c15206 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 16:51:39 +0200 Subject: [PATCH 75/85] ics203 --- benchmark/datasets/ground_truth/ics203_1.json | 125 +++++++++++++++++ benchmark/datasets/ground_truth/ics203_2.json | 125 +++++++++++++++++ benchmark/datasets/ground_truth/ics203_3.json | 126 ++++++++++++++++++ benchmark/datasets/ground_truth/ics203_4.json | 126 ++++++++++++++++++ benchmark/datasets/ground_truth/ics203_5.json | 126 ++++++++++++++++++ benchmark/datasets/narratives/ics203_1.txt | 13 ++ benchmark/datasets/narratives/ics203_2.txt | 13 ++ benchmark/datasets/narratives/ics203_3.txt | 13 ++ benchmark/datasets/narratives/ics203_4.txt | 13 ++ benchmark/datasets/narratives/ics203_5.txt | 13 ++ benchmark/datasets/templates/ics_203.json | 88 ++++++++++++ 11 files changed, 781 insertions(+) create mode 100644 benchmark/datasets/ground_truth/ics203_1.json create mode 100644 benchmark/datasets/ground_truth/ics203_2.json create mode 100644 benchmark/datasets/ground_truth/ics203_3.json create mode 100644 benchmark/datasets/ground_truth/ics203_4.json create mode 100644 benchmark/datasets/ground_truth/ics203_5.json create mode 100644 benchmark/datasets/narratives/ics203_1.txt create mode 100644 benchmark/datasets/narratives/ics203_2.txt create mode 100644 benchmark/datasets/narratives/ics203_3.txt create mode 100644 benchmark/datasets/narratives/ics203_4.txt create mode 100644 benchmark/datasets/narratives/ics203_5.txt create mode 100644 benchmark/datasets/templates/ics_203.json diff --git a/benchmark/datasets/ground_truth/ics203_1.json b/benchmark/datasets/ground_truth/ics203_1.json new file mode 100644 index 00000000..f049792d --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_1.json @@ -0,0 +1,125 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "Chief J. Thomas (County Fire)", + "S. Albright (State DEQ)", + "D. Miller (CSX Railroad RP)" + ], + "deputy": "Assistant Chief M. Reynolds", + "safety_officer": "Captain R. Mendez", + "public_info_officer": "A. Cho (County OEM)", + "liaison_officer": "Inspector G. Sims (SO)" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "US EPA Region 4", + "name": "OSC K. Lawson" + }, + { + "agency_organization": "CSX Railroad", + "name": "V. Patel" + }, + { + "agency_organization": "Whispering Pines Police Dept", + "name": "Commander B. Davis" + } + ], + "5_planning_section": { + "chief": "Marcus Vance", + "deputy": "L. Sullivan", + "resources_unit": "T. Jenkins", + "situation_unit": "E. Brooks", + "documentation_unit": "H. Martinez", + "demobilization_unit": "R. Sterling", + "technical_specialists": [ + { + "specialty": "Rail Tank Car Specialist", + "name": "D. Kowalski" + }, + { + "specialty": "Air Dispersion Modeler", + "name": "Dr. A. Chen" + } + ] + }, + "6_logistics_section": { + "chief": "K. Dunavan", + "deputy": "P. Wright", + "support_branch": { + "director": "J. Myers", + "supply_unit": "S. Taylor", + "facilities_unit": "C. Adams", + "ground_support_unit": "M. Evans" + }, + "service_branch": { + "director": "B. Foster", + "communications_unit": "R. Lee", + "medical_unit": "Dr. N. Howard", + "food_unit": "L. King" + } + }, + "7_operations_section": { + "chief": "B. Reynolds", + "deputy": "Lt. Commander D. Miller", + "staging_area": "Staging Area Alpha (County Fairgrounds)", + "branches": [ + { + "branch_name": "Hazardous Materials Branch", + "branch_director": "Lt. T. Kincaid", + "deputy": "Sgt. R. Hall", + "divisions_groups": [ + { + "identifier": "Hazmat Entry Group", + "supervisor": "Capt. P. Gomez" + }, + { + "identifier": "Decontamination Group", + "supervisor": "Lt. S. Baker" + } + ] + }, + { + "branch_name": "Fire Suppression Branch", + "branch_director": "Batt. Chief E. Walters", + "deputy": "Capt. J. Miller", + "divisions_groups": [ + { + "identifier": "Division A (Rail Right-of-Way)", + "supervisor": "Capt. D. Ross" + }, + { + "identifier": "Water Supply Group", + "supervisor": "Lt. M. Turner" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Capt. H. Nelson" + } + }, + "8_finance_administration_section": { + "chief": "Robert Sterling", + "deputy": "A. Patel", + "time_unit": "C. White", + "procurement_unit": "G. Scott", + "comp_claims_unit": "E. Green", + "cost_unit": "W. Harris" + }, + "9_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_2.json b/benchmark/datasets/ground_truth/ics203_2.json new file mode 100644 index 00000000..238a218f --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_2.json @@ -0,0 +1,125 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "Chief R. Vance (Blackwood Fire Dept)", + "Officer M. Ross (State EPA)", + "J. Vance (Blackwood Pipeline Co. RP)" + ], + "deputy": "Deputy Chief L. Morgan", + "safety_officer": "Captain L. Hayes", + "public_info_officer": "E. Wright", + "liaison_officer": "Deputy K. Miller" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "US EPA Region 5", + "name": "OSC R. Brooks" + }, + { + "agency_organization": "Blackwood County Sheriff", + "name": "Sheriff T. Higgins" + }, + { + "agency_organization": "Valley Water Authority", + "name": "Director P. Simmons" + } + ], + "5_planning_section": { + "chief": "Sarah L. Jenkins", + "deputy": "T. Bradley", + "resources_unit": "A. Patel", + "situation_unit": "C. Webb", + "documentation_unit": "Dr. H. Thorne", + "demobilization_unit": "M. Brody", + "technical_specialists": [ + { + "specialty": "Pipeline Integrity Specialist", + "name": "E. Vance" + }, + { + "specialty": "Chemical Toxicology Specialist", + "name": "Dr. S. Ray" + } + ] + }, + "6_logistics_section": { + "chief": "T. Bradley", + "deputy": "W. Miller", + "support_branch": { + "director": "G. Kross", + "supply_unit": "H. Lin", + "facilities_unit": "R. Sterling", + "ground_support_unit": "D. Miller" + }, + "service_branch": { + "director": "S. Norris", + "communications_unit": "K. Albright", + "medical_unit": "Dr. T. Vance", + "food_unit": "A. Cho" + } + }, + "7_operations_section": { + "chief": "D. Kowalski", + "deputy": "Lt. Timothy Vance", + "staging_area": "Staging Area Bravo (Westside High School)", + "branches": [ + { + "branch_name": "Pipeline Isolation Branch", + "branch_director": "Capt. Donald Kross", + "deputy": "Lt. C. Webb", + "divisions_groups": [ + { + "identifier": "Hot Zone Entry Group", + "supervisor": "Lt. R. Mendez" + }, + { + "identifier": "Vapor Suppression Group", + "supervisor": "Capt. A. Ross" + } + ] + }, + { + "branch_name": "River Spill Containment Branch", + "branch_director": "Commander Thomas Blake", + "deputy": "Inspector G. Sims", + "divisions_groups": [ + { + "identifier": "Booming Division 1", + "supervisor": "Lt. J. Thorne" + }, + { + "identifier": "Water Intake Protection Group", + "supervisor": "Capt. B. Walsh" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Capt. E. Walters" + } + }, + "8_finance_administration_section": { + "chief": "A. Patel", + "deputy": "Robert Sterling", + "time_unit": "J. Mercer", + "procurement_unit": "Laura Martinez", + "comp_claims_unit": "Inspector R. Sterling", + "cost_unit": "Sandra Keller" + }, + "9_prepared_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_3.json b/benchmark/datasets/ground_truth/ics203_3.json new file mode 100644 index 00000000..e23d680f --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_3.json @@ -0,0 +1,126 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "Captain H. Vance (USCG Unified Command IC)", + "OSC M. Reynolds (EPA Co-IC)", + "Chief D. Garcia (Port Authority Fire IC)", + "K. Lindqvist (Stolt Tankers RP IC)" + ], + "deputy": "Commander T. Blake", + "safety_officer": "Commander Thomas Blake", + "public_info_officer": "Laura Martinez (Port Authority)", + "liaison_officer": "Inspector R. Sterling (TCEQ)" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "Texas Commission on Environmental Quality", + "name": "Inspector R. Sterling" + }, + { + "agency_organization": "Stolt Tankers Shipping", + "name": "K. Lindqvist" + }, + { + "agency_organization": "Port of Houston Pilots", + "name": "Captain M. Brody" + } + ], + "5_planning_section": { + "chief": "Commander Richard Croft", + "deputy": "Lt. J. Thorne", + "resources_unit": "Dr. V. Patel", + "situation_unit": "Sandra Keller", + "documentation_unit": "Wayne Miller", + "demobilization_unit": "Battalion Chief A. Ross", + "technical_specialists": [ + { + "specialty": "Chemical Hazard Specialist", + "name": "Dr. V. Patel" + }, + { + "specialty": "Marine Salvage Engineer", + "name": "E. Miller" + } + ] + }, + "6_logistics_section": { + "chief": "Sandra Keller", + "deputy": "Wayne Miller", + "support_branch": { + "director": "Capt. H. Nelson", + "supply_unit": "C. White", + "facilities_unit": "G. Scott", + "ground_support_unit": "E. Green" + }, + "service_branch": { + "director": "W. Harris", + "communications_unit": "J. Myers", + "medical_unit": "Dr. A. Chen", + "food_unit": "S. Taylor" + } + }, + "7_operations_section": { + "chief": "Battalion Chief A. Ross", + "deputy": "Lt. J. Thorne", + "staging_area": "Staging Area Bravo (Jacintoport Terminal)", + "branches": [ + { + "branch_name": "Vessel Salvage & Entry Branch", + "branch_director": "Lt. J. Thorne", + "deputy": "Capt. P. Gomez", + "divisions_groups": [ + { + "identifier": "Deck Neutralization Group", + "supervisor": "Lt. S. Baker" + }, + { + "identifier": "Manifold Isolation Team", + "supervisor": "Capt. D. Ross" + } + ] + }, + { + "branch_name": "Ship Channel Protection Branch", + "branch_director": "Capt. Donald Kross", + "deputy": "Lt. M. Turner", + "divisions_groups": [ + { + "identifier": "Water Quality Sampling Division", + "supervisor": "Dr. S. Ray" + }, + { + "identifier": "Booming Operations Group", + "supervisor": "Capt. B. Walsh" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Commander T. Blake" + } + }, + "8_finance_administration_section": { + "chief": "Wayne Miller", + "deputy": "Sandra Keller", + "time_unit": "C. Adams", + "procurement_unit": "M. Evans", + "comp_claims_unit": "B. Foster", + "cost_unit": "R. Lee" + }, + "9_prepared_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 06:00" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_4.json b/benchmark/datasets/ground_truth/ics203_4.json new file mode 100644 index 00000000..ec6d638e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_4.json @@ -0,0 +1,126 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "Captain E. Miller (WSP Unified Command IC)", + "OSC R. Brooks (EPA R10 Co-IC)", + "Chief D. Olson (Skykomish Fire IC)", + "M. Campbell (BNSF Railway RP IC)" + ], + "deputy": "Inspector G. Peterson", + "safety_officer": "Captain Marcus Vance", + "public_info_officer": "Jennifer Hayes (WSDOT)", + "liaison_officer": "Deputy S. Kowalski (KCSO)" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "Washington State Department of Transportation", + "name": "Jennifer Hayes" + }, + { + "agency_organization": "BNSF Railway", + "name": "G. Peterson" + }, + { + "agency_organization": "King County Sheriff's Office", + "name": "Sgt. H. Lin" + } + ], + "5_planning_section": { + "chief": "Lt. Colonel Alan Vance", + "deputy": "David Sterling", + "resources_unit": "Patricia Ross", + "situation_unit": "Battalion Chief T. Higgins", + "documentation_unit": "Sgt. H. Lin", + "demobilization_unit": "G. Peterson", + "technical_specialists": [ + { + "specialty": "Rail Hazmat Specialist", + "name": "G. Peterson" + }, + { + "specialty": "Toxic Gas Modeler", + "name": "Dr. H. Thorne" + } + ] + }, + "6_logistics_section": { + "chief": "David Sterling", + "deputy": "Patricia Ross", + "support_branch": { + "director": "L. King", + "supply_unit": "J. Myers", + "facilities_unit": "S. Taylor", + "ground_support_unit": "C. Adams" + }, + "service_branch": { + "director": "M. Evans", + "communications_unit": "B. Foster", + "medical_unit": "Dr. N. Howard", + "food_unit": "R. Lee" + } + }, + "7_operations_section": { + "chief": "Battalion Chief T. Higgins", + "deputy": "Sgt. H. Lin", + "staging_area": "Staging Area Charlie (Stevens Pass Yard)", + "branches": [ + { + "branch_name": "Hazmat Capping Branch", + "branch_director": "Capt. P. Gomez", + "deputy": "Lt. S. Baker", + "divisions_groups": [ + { + "identifier": "Railcar Entry Group 1", + "supervisor": "Capt. D. Ross" + }, + { + "identifier": "Decontamination Group", + "supervisor": "Lt. M. Turner" + } + ] + }, + { + "branch_name": "Evacuation & Security Branch", + "branch_director": "Sgt. H. Lin", + "deputy": "Deputy S. Kowalski", + "divisions_groups": [ + { + "identifier": "Highway 2 Control Division", + "supervisor": "Capt. H. Nelson" + }, + { + "identifier": "Skykomish Evacuation Group", + "supervisor": "Sgt. R. Hall" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Capt. E. Walters" + } + }, + "8_finance_administration_section": { + "chief": "Patricia Ross", + "deputy": "David Sterling", + "time_unit": "W. Harris", + "procurement_unit": "C. White", + "comp_claims_unit": "G. Scott", + "cost_unit": "E. Green" + }, + "9_prepared_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_5.json b/benchmark/datasets/ground_truth/ics203_5.json new file mode 100644 index 00000000..b807bacd --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_5.json @@ -0,0 +1,126 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "OSC C. Martinez (EPA R9 Co-IC)", + "Chief M. Thorne (SBCo Fire IC)", + "Chief Inspector A. Kim (Cal OES)", + "W. Vance (Pipeline RP IC)" + ], + "deputy": "Commander F. Miller", + "safety_officer": "Captain Gregory Hall", + "public_info_officer": "Samantha Norris (Cal OES)", + "liaison_officer": "Ranger D. Stevens (State Parks)" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "US Forest Service", + "name": "Dr. L. Arispe" + }, + { + "agency_organization": "Mojave Water Agency", + "name": "Engineer K. Ross" + }, + { + "agency_organization": "Clean Harbors Environmental", + "name": "Captain B. Walsh" + } + ], + "5_planning_section": { + "chief": "Captain Rachel Brooks", + "deputy": "Megan Taylor", + "resources_unit": "Jason Wu", + "situation_unit": "Battalion Chief Kevin Ross", + "documentation_unit": "Dr. L. Arispe", + "demobilization_unit": "Captain B. Walsh", + "technical_specialists": [ + { + "specialty": "Environmental Unit Leader", + "name": "Dr. L. Arispe" + }, + { + "specialty": "Hydrological Flow Specialist", + "name": "Dr. S. Ray" + } + ] + }, + "6_logistics_section": { + "chief": "Megan Taylor", + "deputy": "Jason Wu", + "support_branch": { + "director": "Capt. P. Gomez", + "supply_unit": "Lt. S. Baker", + "facilities_unit": "Capt. D. Ross", + "ground_support_unit": "Lt. M. Turner" + }, + "service_branch": { + "director": "Capt. H. Nelson", + "communications_unit": "Sgt. R. Hall", + "medical_unit": "Dr. N. Howard", + "food_unit": "L. King" + } + }, + "7_operations_section": { + "chief": "Battalion Chief Kevin Ross", + "deputy": "Captain B. Walsh", + "staging_area": "Staging Area Delta (Silverwood Lake Marina)", + "branches": [ + { + "branch_name": "Canyon Pipeline Repair Branch", + "branch_director": "Capt. Gregory Hall", + "deputy": "Lt. R. Mendez", + "divisions_groups": [ + { + "identifier": "Clamp Repair Group", + "supervisor": "Capt. A. Ross" + }, + { + "identifier": "Sawpit Containment Group", + "supervisor": "Lt. C. Webb" + } + ] + }, + { + "branch_name": "Reservoir Skimming & Booming Branch", + "branch_director": "Captain B. Walsh", + "deputy": "Lt. J. Thorne", + "divisions_groups": [ + { + "identifier": "South Arm Skimmer Division", + "supervisor": "Capt. B. Walsh" + }, + { + "identifier": "Water Intake Protection Group", + "supervisor": "Dr. L. Arispe" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Capt. E. Walters" + } + }, + "8_finance_administration_section": { + "chief": "Jason Wu", + "deputy": "Megan Taylor", + "time_unit": "J. Myers", + "procurement_unit": "S. Taylor", + "comp_claims_unit": "C. Adams", + "cost_unit": "M. Evans" + }, + "9_prepared_by": { + "name": "Captain Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/narratives/ics203_1.txt b/benchmark/datasets/narratives/ics203_1.txt new file mode 100644 index 00000000..14d7b853 --- /dev/null +++ b/benchmark/datasets/narratives/ics203_1.txt @@ -0,0 +1,13 @@ +### Whispering Pines Derailment Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, officially documents the active organizational structure and personnel assignments for Operational Period 1 (July 07, 2026, 18:00 hours to July 08, 2026, 06:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders Chief J. Thomas representing County Fire, S. Albright representing State DEQ, and D. Miller representing CSX Railroad. Executive operational support is provided by Deputy Incident Commander Assistant Chief M. Reynolds. Safety oversight is directed by Safety Officer Captain R. Mendez, public communications are managed by Public Information Officer A. Cho of County OEM, and inter-agency coordination is handled by Liaison Officer Inspector G. Sims of the Sheriff's Office. External agency representatives active at the command post include OSC K. Lawson representing US EPA Region 4, V. Patel representing CSX Railroad, and Commander B. Davis representing the Whispering Pines Police Department. + +The Planning Section is led by Planning Section Chief Marcus Vance, supported by Deputy L. Sullivan. Unit Leaders under Planning include T. Jenkins managing the Resources Unit, E. Brooks managing the Situation Unit, H. Martinez managing the Documentation Unit, and R. Sterling managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists D. Kowalski (Rail Tank Car Specialist) and Dr. A. Chen (Air Dispersion Modeler). + +The Logistics Section is commanded by Logistics Section Chief K. Dunavan, supported by Deputy P. Wright. Logistics is organized into two primary branches: the Support Branch directed by J. Myers, overseeing Supply Unit Leader S. Taylor, Facilities Unit Leader C. Adams, and Ground Support Unit Leader M. Evans; and the Service Branch directed by B. Foster, overseeing Communications Unit Leader R. Lee, Medical Unit Leader Dr. N. Howard, and Food Unit Leader L. King. + +The Operations Section is directed by Operations Section Chief B. Reynolds, supported by Deputy Lt. Commander D. Miller. Tactical field units operate from Staging Area Alpha located at the County Fairgrounds. Operational branches include the Hazardous Materials Branch led by Branch Director Lt. T. Kincaid and Deputy Sgt. R. Hall, supervising the Hazmat Entry Group under Capt. P. Gomez and the Decontamination Group under Lt. S. Baker; and the Fire Suppression Branch led by Branch Director Batt. Chief E. Walters and Deputy Capt. J. Miller, supervising Division A (Rail Right-of-Way) under Capt. D. Ross and the Water Supply Group under Lt. M. Turner. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Capt. H. Nelson. + +The Finance/Administration Section is managed by Finance/Admin Section Chief Robert Sterling, supported by Deputy A. Patel. Financial operations are staffed by Time Unit Leader C. White, Procurement Unit Leader G. Scott, Compensation/Claims Unit Leader E. Green, and Cost Unit Leader W. Harris. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Marcus Vance on July 07, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics203_2.txt b/benchmark/datasets/narratives/ics203_2.txt new file mode 100644 index 00000000..7cbb7d3a --- /dev/null +++ b/benchmark/datasets/narratives/ics203_2.txt @@ -0,0 +1,13 @@ +### Blackwood Chemical Pipeline Breach Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, officially documents the active organizational structure and personnel assignments for Operational Period 1 (August 13, 2026, 18:00 hours to August 14, 2026, 06:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders Chief R. Vance representing Blackwood Fire Department, Officer M. Ross representing State EPA, and J. Vance representing Blackwood Pipeline Co. Executive operational support is provided by Deputy Incident Commander Deputy Chief L. Morgan. Safety oversight is directed by Safety Officer Captain L. Hayes, public communications are managed by Public Information Officer E. Wright, and inter-agency coordination is handled by Liaison Officer Deputy K. Miller. External agency representatives active at the command post include OSC R. Brooks representing US EPA Region 5, Sheriff T. Higgins representing Blackwood County Sheriff, and Director P. Simmons representing Valley Water Authority. + +The Planning Section is led by Planning Section Chief Sarah L. Jenkins, supported by Deputy T. Bradley. Unit Leaders under Planning include A. Patel managing the Resources Unit, C. Webb managing the Situation Unit, Dr. H. Thorne managing the Documentation Unit, and M. Brody managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists E. Vance (Pipeline Integrity Specialist) and Dr. S. Ray (Chemical Toxicology Specialist). + +The Logistics Section is commanded by Logistics Section Chief T. Bradley, supported by Deputy W. Miller. Logistics is organized into two primary branches: the Support Branch directed by G. Kross, overseeing Supply Unit Leader H. Lin, Facilities Unit Leader R. Sterling, and Ground Support Unit Leader D. Miller; and the Service Branch directed by S. Norris, overseeing Communications Unit Leader K. Albright, Medical Unit Leader Dr. T. Vance, and Food Unit Leader A. Cho. + +The Operations Section is directed by Operations Section Chief D. Kowalski, supported by Deputy Lt. Timothy Vance. Tactical field units operate from Staging Area Bravo located at Westside High School. Operational branches include the Pipeline Isolation Branch led by Branch Director Capt. Donald Kross and Deputy Lt. C. Webb, supervising the Hot Zone Entry Group under Lt. R. Mendez and the Vapor Suppression Group under Capt. A. Ross; and the River Spill Containment Branch led by Branch Director Commander Thomas Blake and Deputy Inspector G. Sims, supervising Booming Division 1 under Lt. J. Thorne and the Water Intake Protection Group under Capt. B. Walsh. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Capt. E. Walters. + +The Finance/Administration Section is managed by Finance/Admin Section Chief A. Patel, supported by Deputy Robert Sterling. Financial operations are staffed by Time Unit Leader J. Mercer, Procurement Unit Leader Laura Martinez, Compensation/Claims Unit Leader Inspector R. Sterling, and Cost Unit Leader Sandra Keller. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Sarah L. Jenkins on August 13, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics203_3.txt b/benchmark/datasets/narratives/ics203_3.txt new file mode 100644 index 00000000..5490d3e1 --- /dev/null +++ b/benchmark/datasets/narratives/ics203_3.txt @@ -0,0 +1,13 @@ +### Port of Houston Sulfuric Acid Tanker Leak Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, officially documents the active organizational structure and personnel assignments for Operational Period 1 (October 14, 2026, 07:00 hours to October 15, 2026, 19:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders Captain H. Vance representing USCG, OSC M. Reynolds representing EPA, Chief D. Garcia representing Port Authority Fire, and K. Lindqvist representing Stolt Tankers. Executive operational support is provided by Deputy Incident Commander Commander T. Blake. Safety oversight is directed by Safety Officer Commander Thomas Blake, public communications are managed by Public Information Officer Laura Martinez of Port Authority, and inter-agency coordination is handled by Liaison Officer Inspector R. Sterling of TCEQ. External agency representatives active at the command post include Inspector R. Sterling representing Texas Commission on Environmental Quality, K. Lindqvist representing Stolt Tankers Shipping, and Captain M. Brody representing Port of Houston Pilots. + +The Planning Section is led by Planning Section Chief Commander Richard Croft, supported by Deputy Lt. J. Thorne. Unit Leaders under Planning include Dr. V. Patel managing the Resources Unit, Sandra Keller managing the Situation Unit, Wayne Miller managing the Documentation Unit, and Battalion Chief A. Ross managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists Dr. V. Patel (Chemical Hazard Specialist) and E. Miller (Marine Salvage Engineer). + +The Logistics Section is commanded by Logistics Section Chief Sandra Keller, supported by Deputy Wayne Miller. Logistics is organized into two primary branches: the Support Branch directed by Capt. H. Nelson, overseeing Supply Unit Leader C. White, Facilities Unit Leader G. Scott, and Ground Support Unit Leader E. Green; and the Service Branch directed by W. Harris, overseeing Communications Unit Leader J. Myers, Medical Unit Leader Dr. A. Chen, and Food Unit Leader S. Taylor. + +The Operations Section is directed by Operations Section Chief Battalion Chief A. Ross, supported by Deputy Lt. J. Thorne. Tactical field units operate from Staging Area Bravo located at Jacintoport Terminal. Operational branches include the Vessel Salvage & Entry Branch led by Branch Director Lt. J. Thorne and Deputy Capt. P. Gomez, supervising the Deck Neutralization Group under Lt. S. Baker and the Manifold Isolation Team under Capt. D. Ross; and the Ship Channel Protection Branch led by Branch Director Capt. Donald Kross and Deputy Lt. M. Turner, supervising the Water Quality Sampling Division under Dr. S. Ray and the Booming Operations Group under Capt. B. Walsh. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Commander T. Blake. + +The Finance/Administration Section is managed by Finance/Admin Section Chief Wayne Miller, supported by Deputy Sandra Keller. Financial operations are staffed by Time Unit Leader C. Adams, Procurement Unit Leader M. Evans, Compensation/Claims Unit Leader B. Foster, and Cost Unit Leader R. Lee. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Commander Richard Croft on October 14, 2026, at 06:00 hours. diff --git a/benchmark/datasets/narratives/ics203_4.txt b/benchmark/datasets/narratives/ics203_4.txt new file mode 100644 index 00000000..9d998f6c --- /dev/null +++ b/benchmark/datasets/narratives/ics203_4.txt @@ -0,0 +1,13 @@ +### Cascade Pass Chlorine Railcar Derailment Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, officially documents the active organizational structure and personnel assignments for Operational Period 1 (November 02, 2026, 18:00 hours to November 03, 2026, 06:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders Captain E. Miller representing WSP, OSC R. Brooks representing EPA Region 10, Chief D. Olson representing Skykomish Fire, and M. Campbell representing BNSF Railway. Executive operational support is provided by Deputy Incident Commander Inspector G. Peterson. Safety oversight is directed by Safety Officer Captain Marcus Vance, public communications are managed by Public Information Officer Jennifer Hayes of WSDOT, and inter-agency coordination is handled by Liaison Officer Deputy S. Kowalski of King County Sheriff's Office. External agency representatives active at the command post include Jennifer Hayes representing Washington State Department of Transportation, G. Peterson representing BNSF Railway, and Sgt. H. Lin representing King County Sheriff's Office. + +The Planning Section is led by Planning Section Chief Lt. Colonel Alan Vance, supported by Deputy David Sterling. Unit Leaders under Planning include Patricia Ross managing the Resources Unit, Battalion Chief T. Higgins managing the Situation Unit, Sgt. H. Lin managing the Documentation Unit, and G. Peterson managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists G. Peterson (Rail Hazmat Specialist) and Dr. H. Thorne (Toxic Gas Modeler). + +The Logistics Section is commanded by Logistics Section Chief David Sterling, supported by Deputy Patricia Ross. Logistics is organized into two primary branches: the Support Branch directed by L. King, overseeing Supply Unit Leader J. Myers, Facilities Unit Leader S. Taylor, and Ground Support Unit Leader C. Adams; and the Service Branch directed by M. Evans, overseeing Communications Unit Leader B. Foster, Medical Unit Leader Dr. N. Howard, and Food Unit Leader R. Lee. + +The Operations Section is directed by Operations Section Chief Battalion Chief T. Higgins, supported by Deputy Sgt. H. Lin. Tactical field units operate from Staging Area Charlie located at the Stevens Pass Maintenance Yard. Operational branches include the Hazmat Capping Branch led by Branch Director Capt. P. Gomez and Deputy Lt. S. Baker, supervising Railcar Entry Group 1 under Capt. D. Ross and the Decontamination Group under Lt. M. Turner; and the Evacuation & Security Branch led by Branch Director Sgt. H. Lin and Deputy Deputy S. Kowalski, supervising the Highway 2 Control Division under Capt. H. Nelson and the Skykomish Evacuation Group under Sgt. R. Hall. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Capt. E. Walters. + +The Finance/Administration Section is managed by Finance/Admin Section Chief Patricia Ross, supported by Deputy David Sterling. Financial operations are staffed by Time Unit Leader W. Harris, Procurement Unit Leader C. White, Compensation/Claims Unit Leader G. Scott, and Cost Unit Leader E. Green. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Lt. Colonel Alan Vance on November 02, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics203_5.txt b/benchmark/datasets/narratives/ics203_5.txt new file mode 100644 index 00000000..8d961ebd --- /dev/null +++ b/benchmark/datasets/narratives/ics203_5.txt @@ -0,0 +1,13 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, officially documents the active organizational structure and personnel assignments for Operational Period 1 (May 18, 2026, 18:00 hours to May 19, 2026, 06:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders OSC C. Martinez representing EPA Region 9, Chief M. Thorne representing San Bernardino County Fire, Chief Inspector A. Kim representing Cal OES, and W. Vance representing Pipeline RP. Executive operational support is provided by Deputy Incident Commander Commander F. Miller. Safety oversight is directed by Safety Officer Captain Gregory Hall, public communications are managed by Public Information Officer Samantha Norris of Cal OES, and inter-agency coordination is handled by Liaison Officer Ranger D. Stevens of State Parks. External agency representatives active at the command post include Dr. L. Arispe representing US Forest Service, Engineer K. Ross representing Mojave Water Agency, and Captain B. Walsh representing Clean Harbors Environmental. + +The Planning Section is led by Planning Section Chief Captain Rachel Brooks, supported by Deputy Megan Taylor. Unit Leaders under Planning include Jason Wu managing the Resources Unit, Battalion Chief Kevin Ross managing the Situation Unit, Dr. L. Arispe managing the Documentation Unit, and Captain B. Walsh managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists Dr. L. Arispe (Environmental Unit Leader) and Dr. S. Ray (Hydrological Flow Specialist). + +The Logistics Section is commanded by Logistics Section Chief Megan Taylor, supported by Deputy Jason Wu. Logistics is organized into two primary branches: the Support Branch directed by Capt. P. Gomez, overseeing Supply Unit Leader Lt. S. Baker, Facilities Unit Leader Capt. D. Ross, and Ground Support Unit Leader Lt. M. Turner; and the Service Branch directed by Capt. H. Nelson, overseeing Communications Unit Leader Sgt. R. Hall, Medical Unit Leader Dr. N. Howard, and Food Unit Leader L. King. + +The Operations Section is directed by Operations Section Chief Battalion Chief Kevin Ross, supported by Deputy Captain B. Walsh. Tactical field units operate from Staging Area Delta located at the Silverwood Lake Marina. Operational branches include the Canyon Pipeline Repair Branch led by Branch Director Capt. Gregory Hall and Deputy Lt. R. Mendez, supervising the Clamp Repair Group under Capt. A. Ross and the Sawpit Containment Group under Lt. C. Webb; and the Reservoir Skimming & Booming Branch led by Branch Director Captain B. Walsh and Deputy Lt. J. Thorne, supervising the South Arm Skimmer Division under Capt. B. Walsh and the Water Intake Protection Group under Dr. L. Arispe. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Capt. E. Walters. + +The Finance/Administration Section is managed by Finance/Admin Section Chief Jason Wu, supported by Deputy Megan Taylor. Financial operations are staffed by Time Unit Leader J. Myers, Procurement Unit Leader S. Taylor, Compensation/Claims Unit Leader C. Adams, and Cost Unit Leader M. Evans. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Captain Rachel Brooks on May 18, 2026, at 16:30 hours. diff --git a/benchmark/datasets/templates/ics_203.json b/benchmark/datasets/templates/ics_203.json new file mode 100644 index 00000000..380f9115 --- /dev/null +++ b/benchmark/datasets/templates/ics_203.json @@ -0,0 +1,88 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": ["string"], + "deputy": "string", + "safety_officer": "string", + "public_info_officer": "string", + "liaison_officer": "string" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "string", + "name": "string" + } + ], + "5_planning_section": { + "chief": "string", + "deputy": "string", + "resources_unit": "string", + "situation_unit": "string", + "documentation_unit": "string", + "demobilization_unit": "string", + "technical_specialists": [ + { + "specialty": "string", + "name": "string" + } + ] + }, + "6_logistics_section": { + "chief": "string", + "deputy": "string", + "support_branch": { + "director": "string", + "supply_unit": "string", + "facilities_unit": "string", + "ground_support_unit": "string" + }, + "service_branch": { + "director": "string", + "communications_unit": "string", + "medical_unit": "string", + "food_unit": "string" + } + }, + "7_operations_section": { + "chief": "string", + "deputy": "string", + "staging_area": "string", + "branches": [ + { + "branch_name": "string", + "branch_director": "string", + "deputy": "string", + "divisions_groups": [ + { + "identifier": "string", + "supervisor": "string" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "string" + } + }, + "8_finance_administration_section": { + "chief": "string", + "deputy": "string", + "time_unit": "string", + "procurement_unit": "string", + "comp_claims_unit": "string", + "cost_unit": "string" + }, + "9_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} From d9f0499c82cfcaaa084e5634d613bc2057f4b3b6 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 16:54:34 +0200 Subject: [PATCH 76/85] ics204 --- benchmark/datasets/ground_truth/ics204_1.json | 81 +++++++++++++++++++ benchmark/datasets/ground_truth/ics204_2.json | 81 +++++++++++++++++++ benchmark/datasets/ground_truth/ics204_3.json | 81 +++++++++++++++++++ benchmark/datasets/ground_truth/ics204_4.json | 81 +++++++++++++++++++ benchmark/datasets/ground_truth/ics204_5.json | 81 +++++++++++++++++++ benchmark/datasets/narratives/ics204_1.txt | 14 ++++ benchmark/datasets/narratives/ics204_2.txt | 14 ++++ benchmark/datasets/narratives/ics204_3.txt | 14 ++++ benchmark/datasets/narratives/ics204_4.txt | 14 ++++ benchmark/datasets/narratives/ics204_5.txt | 14 ++++ benchmark/datasets/templates/ics_204.json | 53 ++++++++++++ 11 files changed, 528 insertions(+) create mode 100644 benchmark/datasets/ground_truth/ics204_1.json create mode 100644 benchmark/datasets/ground_truth/ics204_2.json create mode 100644 benchmark/datasets/ground_truth/ics204_3.json create mode 100644 benchmark/datasets/ground_truth/ics204_4.json create mode 100644 benchmark/datasets/ground_truth/ics204_5.json create mode 100644 benchmark/datasets/narratives/ics204_1.txt create mode 100644 benchmark/datasets/narratives/ics204_2.txt create mode 100644 benchmark/datasets/narratives/ics204_3.txt create mode 100644 benchmark/datasets/narratives/ics204_4.txt create mode 100644 benchmark/datasets/narratives/ics204_5.txt create mode 100644 benchmark/datasets/templates/ics_204.json diff --git a/benchmark/datasets/ground_truth/ics204_1.json b/benchmark/datasets/ground_truth/ics204_1.json new file mode 100644 index 00000000..6fb91f7d --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_1.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Hazardous Materials Branch", + "division": "Division A", + "group": "Hazmat Entry Group", + "staging_area": "Staging Area Alpha (County Fairgrounds)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "B. Reynolds", + "contact_number": "555-0192 / Ch 1" + }, + "branch_director": { + "name": "Lt. T. Kincaid", + "contact_number": "555-0144 / Ch 3" + }, + "division_group_supervisor": { + "name": "Capt. P. Gomez", + "contact_number": "555-0188 / Ch 4" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "HZM-01", + "leader": "Lt. T. Kincaid", + "number_of_persons": "6", + "contact": "Radio Ch 4 (462.550 MHz)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Hot Zone Gate 1 by 18:30. Level B SCBA suits, PID air monitors, non-sparking aluminum tools." + }, + { + "resource_identifier": "DECON-02", + "leader": "Lt. S. Baker", + "number_of_persons": "4", + "contact": "Radio Ch 4", + "reporting_location_special_equipment_remarks_notes_information": "Establish wet-decon station at Warm Zone boundary Gate 2 by 18:45. Decon shower trailer with lime wash solution." + }, + { + "resource_identifier": "ENG-41", + "leader": "Capt. D. Ross", + "number_of_persons": "3", + "contact": "Radio Ch 2", + "reporting_location_special_equipment_remarks_notes_information": "Position at Warm Zone boundary for continuous AFFF foam blanket standby during tank car grounding." + } + ], + "6_work_assignments": "Execute Hot Zone entry into rail right-of-way to secure structural stabilization straps on derailed Car #14. Attach grounding cables to prevent static spark ignition. Perform real-time PID air monitoring around breached Benzene manifold and report VOC concentrations to Branch Director every 15 minutes.", + "7_special_instructions": "All entry personnel must wear Level B PPE with SCBA. Continuous air monitoring required; evacuate Hot Zone immediately if PID readings exceed 5 ppm Benzene or 10% LEL. Ensure formal decontamination shower prior to exiting Warm Zone.", + "8_communications": [ + { + "name_function": "Command Channel / Ops Chief", + "primary_contact": "Radio Ch 1 (154.280 MHz)" + }, + { + "name_function": "Hazmat Tactical Channel", + "primary_contact": "Radio Ch 4 (462.550 MHz)" + }, + { + "name_function": "Safety Officer Direct Line", + "primary_contact": "Cell: 555-0177" + }, + { + "name_function": "Medical Emergency Call", + "primary_contact": "Radio Ch 5 / Cell: 555-0199" + } + ], + "9_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_2.json b/benchmark/datasets/ground_truth/ics204_2.json new file mode 100644 index 00000000..e1cc69c0 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_2.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Pipeline Isolation Branch", + "division": "Division B", + "group": "Hot Zone Entry Group", + "staging_area": "Staging Area Bravo (Westside High School)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "D. Kowalski", + "contact_number": "555-0210 / Ch 1" + }, + "branch_director": { + "name": "Capt. Donald Kross", + "contact_number": "555-0233 / Ch 2" + }, + "division_group_supervisor": { + "name": "Lt. R. Mendez", + "contact_number": "555-0255 / Ch 3" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "HZM-05", + "leader": "Lt. C. Webb", + "number_of_persons": "5", + "contact": "Radio Ch 3 (467.775 MHz)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Gate 2 by 18:15. Level A encapsulated SCBA suits, pneumatic pipe clamp, thermal imaging camera." + }, + { + "resource_identifier": "ENG-12", + "leader": "Capt. A. Ross", + "number_of_persons": "4", + "contact": "Radio Ch 2", + "reporting_location_special_equipment_remarks_notes_information": "Deploy unmanned fog nozzles at 500 ft perimeter to maintain water curtains over airborne ammonia plume." + }, + { + "resource_identifier": "WT-03", + "leader": "Lt. M. Turner", + "number_of_persons": "2", + "contact": "Radio Ch 2", + "reporting_location_special_equipment_remarks_notes_information": "Supply continuous water feed to Engine 12 monitors." + } + ], + "6_work_assignments": "Enter Hot Zone at Valve Station 14-B under Level A protection to execute manual hot-tap isolation on the 8-inch pressurized line. Secure bypass manifold to stop active liquid ammonia discharge. Maintain water curtains downwind during valve mechanical manipulation.", + "7_special_instructions": "Level A PPE mandatory for entry team. Maintain 2-person backup entry team in Level A at Warm Zone line. If ammonia concentration exceeds 300 ppm IDLH at Warm Zone baseline, sound emergency withdrawal horn (3 long blasts).", + "8_communications": [ + { + "name_function": "Command / Operations", + "primary_contact": "Radio Ch 1 (153.830 MHz)" + }, + { + "name_function": "Pipeline Tactical", + "primary_contact": "Radio Ch 3 (467.775 MHz)" + }, + { + "name_function": "Safety Officer Line", + "primary_contact": "Cell: 555-0288" + }, + { + "name_function": "Decon Line", + "primary_contact": "Radio Ch 6 (462.625 MHz)" + } + ], + "9_prepared_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_3.json b/benchmark/datasets/ground_truth/ics204_3.json new file mode 100644 index 00000000..9d4cb76e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_3.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Vessel Salvage & Entry Branch", + "division": "Division C (Berth 42)", + "group": "Deck Neutralization Group", + "staging_area": "Staging Area Bravo (Jacintoport Terminal)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "Battalion Chief A. Ross", + "contact_number": "555-0312 / Ch 1" + }, + "branch_director": { + "name": "Lt. J. Thorne", + "contact_number": "555-0344 / Ch 2" + }, + "division_group_supervisor": { + "name": "Lt. S. Baker", + "contact_number": "555-0366 / Ch 4" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "HZM-PORT-1", + "leader": "Capt. P. Gomez", + "number_of_persons": "6", + "contact": "Radio Ch 4 (453.225 MHz)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Berth 42 Gangway by 07:30. Level A chemical suits, dry lime blower, pH test strips." + }, + { + "resource_identifier": "NEUT-TRK-05", + "leader": "Driver E. Green", + "number_of_persons": "2", + "contact": "Radio Ch 4", + "reporting_location_special_equipment_remarks_notes_information": "Position at Berth 42 apron. Supply dry sodium bicarbonate powder to deck entry team." + }, + { + "resource_identifier": "RV-AC-01", + "leader": "Capt. B. Walsh", + "number_of_persons": "4", + "contact": "Radio Ch 3", + "reporting_location_special_equipment_remarks_notes_information": "Conduct continuous water sampling and maintain acid sorbent boom around vessel hull." + } + ], + "6_work_assignments": "Board M/T Stolt Synergy under Level A protection. Apply dry sodium bicarbonate slurry over 4,200 gallons of pooled sulfuric acid on vessel deck until deck runoff pH stabilizes between 6.5 and 8.5. Inspect offloading manifold for structural stress.", + "7_special_instructions": "Strict 20-minute entry limit per technician due to heat stress (88°F/78% RH). Mandatory lime wash decon before unsuiting. Do not apply raw water directly to concentrated acid pool to prevent violent exothermic splattering.", + "8_communications": [ + { + "name_function": "Operations Command", + "primary_contact": "Radio Ch 1 (156.800 MHz / VHF 16)" + }, + { + "name_function": "Vessel Tactical", + "primary_contact": "Radio Ch 4 (453.225 MHz)" + }, + { + "name_function": "Port Safety Line", + "primary_contact": "Cell: 555-0399" + }, + { + "name_function": "Medical Standby", + "primary_contact": "Radio Ch 5 (462.600 MHz)" + } + ], + "9_prepared_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 06:00" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_4.json b/benchmark/datasets/ground_truth/ics204_4.json new file mode 100644 index 00000000..875ee3bc --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_4.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Hazmat Capping Branch", + "division": "Division Ravine", + "group": "Railcar Entry Group 1", + "staging_area": "Staging Area Charlie (Stevens Pass Yard)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "Battalion Chief T. Higgins", + "contact_number": "555-0411 / Ch 1" + }, + "branch_director": { + "name": "Capt. P. Gomez", + "contact_number": "555-0433 / Ch 2" + }, + "division_group_supervisor": { + "name": "Capt. D. Ross", + "contact_number": "555-0455 / Ch 3" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "HZM-33", + "leader": "Lt. S. Baker", + "number_of_persons": "5", + "contact": "Radio Ch 3 (462.700 MHz)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Ravine Checkpoint by 18:30. Level A encapsulated suits, Emergency B-Kit capping assembly, pneumatic torque wrenches." + }, + { + "resource_identifier": "BNSF-ER-01", + "leader": "Spec. G. Peterson", + "number_of_persons": "3", + "contact": "Radio Ch 3", + "reporting_location_special_equipment_remarks_notes_information": "Provide railcar dome technical oversight and heavy rigging hardware." + }, + { + "resource_identifier": "DECON-04", + "leader": "Tech M. Turner", + "number_of_persons": "4", + "contact": "Radio Ch 2", + "reporting_location_special_equipment_remarks_notes_information": "Operate heated water decontamination trailer at Staging Area Charlie." + } + ], + "6_work_assignments": "Descend ravine to derailed railcar BNSF-77402 under Level A protection. Mount and torque Emergency B-Kit hood over damaged chlorine angle valve dome. Verify seal integrity using ammonia vapor swab test until zero leakage is detected.", + "7_special_instructions": "Freezing temperature hazard (28°F) and icy slopes. Suit entry teams limited to 30-minute rotations. Heated decon wash mandatory to prevent suit freeze-up. Continuous electrochemical chlorine monitoring required.", + "8_communications": [ + { + "name_function": "Command Channel", + "primary_contact": "Radio Ch 1 (154.400 MHz)" + }, + { + "name_function": "Capping Tactical", + "primary_contact": "Radio Ch 3 (462.700 MHz)" + }, + { + "name_function": "Safety Officer Line", + "primary_contact": "Cell: 555-0488" + }, + { + "name_function": "Evac Control Line", + "primary_contact": "Radio Ch 4 (467.550 MHz)" + } + ], + "9_prepared_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_5.json b/benchmark/datasets/ground_truth/ics204_5.json new file mode 100644 index 00000000..ccea5412 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_5.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Reservoir Skimming & Booming Branch", + "division": "Division Reservoir South", + "group": "South Arm Skimmer Division", + "staging_area": "Staging Area Delta (Silverwood Lake Marina)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "Battalion Chief Kevin Ross", + "contact_number": "555-0515 / Ch 1" + }, + "branch_director": { + "name": "Captain B. Walsh", + "contact_number": "555-0535 / Ch 2" + }, + "division_group_supervisor": { + "name": "Lt. J. Thorne", + "contact_number": "555-0560 / Ch 3" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "RV-LC-01", + "leader": "Capt. B. Walsh", + "number_of_persons": "4", + "contact": "Radio Ch 3 (156.550 MHz / VHF 11)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Marina Slip 4 by 18:15. 1,200 ft hard boom, drum skimmer, high-intensity LED deck searchlights." + }, + { + "resource_identifier": "SKIM-02", + "leader": "Operator R. Mendez", + "number_of_persons": "3", + "contact": "Radio Ch 3", + "reporting_location_special_equipment_remarks_notes_information": "Operate heavy weir skimmer vessel in South arm pool. Pump oil to 5,000 gal floating bladder." + }, + { + "resource_identifier": "AIR-MON-08", + "leader": "Tech A. Ross", + "number_of_persons": "2", + "contact": "Radio Ch 4", + "reporting_location_special_equipment_remarks_notes_information": "Conduct continuous PID benzene air monitoring on boat deck and shoreline baseline." + } + ], + "6_work_assignments": "Deploy and anchor 1,200 feet of hard containment boom across Sawpit Canyon inlet to isolate reservoir. Operate drum skimmers and weir skimmer vessel SKIM-02 continuously throughout night shift to recover floating crude oil slick in South arm.", + "7_special_instructions": "Mandatory USCG-approved PFDs for all over-water personnel. Maintain vessel deck lighting during night operations. If PID readings exceed 1 ppm benzene, mandate half-mask APR organic vapor respirators.", + "8_communications": [ + { + "name_function": "Command Channel", + "primary_contact": "Radio Ch 1 (154.150 MHz)" + }, + { + "name_function": "Marine Tactical Channel", + "primary_contact": "Radio Ch 3 (156.550 MHz / VHF 11)" + }, + { + "name_function": "Safety Officer Direct", + "primary_contact": "Cell: 555-0588" + }, + { + "name_function": "Water Intake Guard", + "primary_contact": "Radio Ch 5 (462.575 MHz)" + } + ], + "9_prepared_by": { + "name": "Captain Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/narratives/ics204_1.txt b/benchmark/datasets/narratives/ics204_1.txt new file mode 100644 index 00000000..b803e274 --- /dev/null +++ b/benchmark/datasets/narratives/ics204_1.txt @@ -0,0 +1,14 @@ +### Whispering Pines Derailment Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, details the tactical assignments for the Hazmat Entry Group operating within Division A under the Hazardous Materials Branch during Operational Period 1 (July 07, 2026, 18:00 hours to July 08, 2026, 06:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief B. Reynolds (Contact: 555-0192 / Radio Ch 1), Branch Director Lt. T. Kincaid (Contact: 555-0144 / Radio Ch 3), and Division/Group Supervisor Capt. P. Gomez (Contact: 555-0188 / Radio Ch 4). Tactical resources staging and deployment are managed out of Staging Area Alpha located at the County Fairgrounds. + +Resources assigned to the Hazmat Entry Group for this shift include: +1. Hazmat Unit HZM-01, led by Lt. T. Kincaid, staffing 6 personnel, contactable via Radio Ch 4 (462.550 MHz). Special instructions require reporting to Hot Zone Gate 1 by 18:30 equipped with Level B SCBA chemical suits, PID air monitors, and non-sparking aluminum tools. +2. Decontamination Trailer DECON-02, led by Lt. S. Baker, staffing 4 personnel, contactable via Radio Ch 4. Special instructions mandate establishing a wet-decontamination station at the Warm Zone boundary Gate 2 by 18:45 utilizing a decon shower trailer with lime wash solution. +3. Fire Engine ENG-41, led by Capt. D. Ross, staffing 3 personnel, contactable via Radio Ch 2. Special instructions require positioning at the Warm Zone boundary for continuous AFFF foam blanket standby during tank car grounding operations. + +The specific work assignments for the Hazmat Entry Group direct personnel to execute Hot Zone entry into the rail right-of-way to secure structural stabilization straps on derailed Car #14, attach grounding cables to prevent static spark ignition, and perform real-time PID air monitoring around the breached Benzene manifold, reporting VOC concentrations to the Branch Director every 15 minutes. + +Special safety instructions mandate that all entry personnel wear Level B PPE with SCBA. Continuous air monitoring is strictly required; personnel must evacuate the Hot Zone immediately if PID readings exceed 5 ppm Benzene or 10% LEL. Formal decontamination shower procedures must be completed prior to exiting the Warm Zone. Communications protocols specify Radio Ch 1 (154.280 MHz) for Command, Radio Ch 4 (462.550 MHz) for Hazmat Tactical, Cell 555-0177 for Safety Officer Direct, and Radio Ch 5 / Cell 555-0199 for Medical Emergency Call. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Marcus Vance on July 07, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics204_2.txt b/benchmark/datasets/narratives/ics204_2.txt new file mode 100644 index 00000000..1ef56c3c --- /dev/null +++ b/benchmark/datasets/narratives/ics204_2.txt @@ -0,0 +1,14 @@ +### Blackwood Chemical Pipeline Breach Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, details the tactical assignments for the Hot Zone Entry Group operating within Division B under the Pipeline Isolation Branch during Operational Period 1 (August 13, 2026, 18:00 hours to August 14, 2026, 06:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief D. Kowalski (Contact: 555-0210 / Radio Ch 1), Branch Director Capt. Donald Kross (Contact: 555-0233 / Radio Ch 2), and Division/Group Supervisor Lt. R. Mendez (Contact: 555-0255 / Radio Ch 3). Tactical resources staging and deployment are managed out of Staging Area Bravo located at Westside High School. + +Resources assigned to the Hot Zone Entry Group for this shift include: +1. Hazmat Unit HZM-05, led by Lt. C. Webb, staffing 5 personnel, contactable via Radio Ch 3 (467.775 MHz). Special instructions require reporting to Gate 2 by 18:15 equipped with Level A encapsulated SCBA suits, pneumatic pipe clamp, and thermal imaging camera. +2. Fire Engine ENG-12, led by Capt. A. Ross, staffing 4 personnel, contactable via Radio Ch 2. Special instructions mandate deploying unmanned fog nozzles at 500 ft perimeter to maintain water curtains over airborne ammonia plume. +3. Water Tender WT-03, led by Lt. M. Turner, staffing 2 personnel, contactable via Radio Ch 2. Special instructions require supplying continuous water feed to Engine 12 monitors. + +The specific work assignments for the Hot Zone Entry Group direct personnel to enter Hot Zone at Valve Station 14-B under Level A protection to execute manual hot-tap isolation on the 8-inch pressurized line, secure bypass manifold to stop active liquid ammonia discharge, and maintain water curtains downwind during valve mechanical manipulation. + +Special safety instructions mandate Level A PPE for entry team, maintaining a 2-person backup entry team in Level A at Warm Zone line, and sounding emergency withdrawal horn (3 long blasts) if ammonia concentration exceeds 300 ppm IDLH at Warm Zone baseline. Communications protocols specify Radio Ch 1 (153.830 MHz) for Command / Operations, Radio Ch 3 (467.775 MHz) for Pipeline Tactical, Cell 555-0288 for Safety Officer Line, and Radio Ch 6 (462.625 MHz) for Decon Line. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Sarah L. Jenkins on August 13, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics204_3.txt b/benchmark/datasets/narratives/ics204_3.txt new file mode 100644 index 00000000..d8d3fa1c --- /dev/null +++ b/benchmark/datasets/narratives/ics204_3.txt @@ -0,0 +1,14 @@ +### Port of Houston Sulfuric Acid Tanker Leak Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, details the tactical assignments for the Deck Neutralization Group operating within Division C (Berth 42) under the Vessel Salvage & Entry Branch during Operational Period 1 (October 14, 2026, 07:00 hours to October 15, 2026, 19:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief Battalion Chief A. Ross (Contact: 555-0312 / Radio Ch 1), Branch Director Lt. J. Thorne (Contact: 555-0344 / Radio Ch 2), and Division/Group Supervisor Lt. S. Baker (Contact: 555-0366 / Radio Ch 4). Tactical resources staging and deployment are managed out of Staging Area Bravo located at Jacintoport Terminal. + +Resources assigned to the Deck Neutralization Group for this shift include: +1. Port Authority Hazmat Team HZM-PORT-1, led by Capt. P. Gomez, staffing 6 personnel, contactable via Radio Ch 4 (453.225 MHz). Special instructions require reporting to Berth 42 Gangway by 07:30 equipped with Level A chemical suits, dry lime blower, and pH test strips. +2. Neutralization Truck Unit NEUT-TRK-05, led by Driver E. Green, staffing 2 personnel, contactable via Radio Ch 4. Special instructions mandate positioning at Berth 42 apron to supply dry sodium bicarbonate powder to deck entry team. +3. Chemical Response Vessel RV-AC-01, led by Capt. B. Walsh, staffing 4 personnel, contactable via Radio Ch 3. Special instructions require conducting continuous water sampling and maintaining acid sorbent boom around vessel hull. + +The specific work assignments for the Deck Neutralization Group direct personnel to board M/T Stolt Synergy under Level A protection, apply dry sodium bicarbonate slurry over 4,200 gallons of pooled sulfuric acid on vessel deck until deck runoff pH stabilizes between 6.5 and 8.5, and inspect offloading manifold for structural stress. + +Special safety instructions mandate a strict 20-minute entry limit per technician due to heat stress (88°F/78% RH), compulsory lime wash decon before unsuiting, and prohibiting direct application of raw water to concentrated acid pool to prevent violent exothermic splattering. Communications protocols specify Radio Ch 1 (156.800 MHz / VHF 16) for Operations Command, Radio Ch 4 (453.225 MHz) for Vessel Tactical, Cell 555-0399 for Port Safety Line, and Radio Ch 5 (462.600 MHz) for Medical Standby. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Commander Richard Croft on October 14, 2026, at 06:00 hours. diff --git a/benchmark/datasets/narratives/ics204_4.txt b/benchmark/datasets/narratives/ics204_4.txt new file mode 100644 index 00000000..568b4f3e --- /dev/null +++ b/benchmark/datasets/narratives/ics204_4.txt @@ -0,0 +1,14 @@ +### Cascade Pass Chlorine Railcar Derailment Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, details the tactical assignments for the Railcar Entry Group 1 operating within Division Ravine under the Hazmat Capping Branch during Operational Period 1 (November 02, 2026, 18:00 hours to November 03, 2026, 06:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief Battalion Chief T. Higgins (Contact: 555-0411 / Radio Ch 1), Branch Director Capt. P. Gomez (Contact: 555-0433 / Radio Ch 2), and Division/Group Supervisor Capt. D. Ross (Contact: 555-0455 / Radio Ch 3). Tactical resources staging and deployment are managed out of Staging Area Charlie located at the Stevens Pass Maintenance Yard. + +Resources assigned to Railcar Entry Group 1 for this shift include: +1. King County Hazmat 3 HZM-33, led by Lt. S. Baker, staffing 5 personnel, contactable via Radio Ch 3 (462.700 MHz). Special instructions require reporting to Ravine Checkpoint by 18:30 equipped with Level A encapsulated suits, Emergency B-Kit capping assembly, and pneumatic torque wrenches. +2. BNSF Emergency Response Unit BNSF-ER-01, led by Spec. G. Peterson, staffing 3 personnel, contactable via Radio Ch 3. Special instructions mandate providing railcar dome technical oversight and heavy rigging hardware. +3. Mobile Decontamination Trailer DECON-04, led by Tech M. Turner, staffing 4 personnel, contactable via Radio Ch 2. Special instructions require operating heated water decontamination trailer at Staging Area Charlie. + +The specific work assignments for Railcar Entry Group 1 direct personnel to descend ravine to derailed railcar BNSF-77402 under Level A protection, mount and torque Emergency B-Kit hood over damaged chlorine angle valve dome, and verify seal integrity using ammonia vapor swab test until zero leakage is detected. + +Special safety instructions mandate precautions for freezing temperature hazards (28°F) and icy slopes. Suit entry teams are limited to 30-minute rotations. Heated decon wash is mandatory to prevent suit freeze-up, and continuous electrochemical chlorine monitoring is required throughout the entry. Communications protocols specify Radio Ch 1 (154.400 MHz) for Command Channel, Radio Ch 3 (462.700 MHz) for Capping Tactical, Cell 555-0488 for Safety Officer Line, and Radio Ch 4 (467.550 MHz) for Evac Control Line. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Lt. Colonel Alan Vance on November 02, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics204_5.txt b/benchmark/datasets/narratives/ics204_5.txt new file mode 100644 index 00000000..8baf3210 --- /dev/null +++ b/benchmark/datasets/narratives/ics204_5.txt @@ -0,0 +1,14 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, details the tactical assignments for the South Arm Skimmer Division operating within Division Reservoir South under the Reservoir Skimming & Booming Branch during Operational Period 1 (May 18, 2026, 18:00 hours to May 19, 2026, 06:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief Battalion Chief Kevin Ross (Contact: 555-0515 / Radio Ch 1), Branch Director Captain B. Walsh (Contact: 555-0535 / Radio Ch 2), and Division/Group Supervisor Lt. J. Thorne (Contact: 555-0560 / Radio Ch 3). Tactical resources staging and deployment are managed out of Staging Area Delta located at the Silverwood Lake Marina. + +Resources assigned to the South Arm Skimmer Division for this shift include: +1. Clean Harbors Spill Vessel RV-LC-01, led by Capt. B. Walsh, staffing 4 personnel, contactable via Radio Ch 3 (156.550 MHz / VHF 11). Special instructions require reporting to Marina Slip 4 by 18:15 equipped with 1,200 ft hard boom, drum skimmer, and high-intensity LED deck searchlights. +2. Heavy Vacuum Skimmer Boat SKIM-02, led by Operator R. Mendez, staffing 3 personnel, contactable via Radio Ch 3. Special instructions mandate operating heavy weir skimmer vessel in South arm pool, pumping oil into a 5,000 gal floating bladder. +3. Air Monitoring Recon Unit AIR-MON-08, led by Tech A. Ross, staffing 2 personnel, contactable via Radio Ch 4. Special instructions require conducting continuous PID benzene air monitoring on boat deck and shoreline baseline. + +The specific work assignments for the South Arm Skimmer Division direct personnel to deploy and anchor 1,200 feet of hard containment boom across Sawpit Canyon inlet to isolate the reservoir, and operate drum skimmers and weir skimmer vessel SKIM-02 continuously throughout the night shift to recover floating crude oil slick in the South arm. + +Special safety instructions mandate USCG-approved PFDs for all over-water personnel. Maintain vessel deck lighting during night operations. If PID readings exceed 1 ppm benzene, mandate half-mask APR organic vapor respirators. Communications protocols specify Radio Ch 1 (154.150 MHz) for Command Channel, Radio Ch 3 (156.550 MHz / VHF 11) for Marine Tactical Channel, Cell 555-0588 for Safety Officer Direct, and Radio Ch 5 (462.575 MHz) for Water Intake Guard. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Captain Rachel Brooks on May 18, 2026, at 16:30 hours. diff --git a/benchmark/datasets/templates/ics_204.json b/benchmark/datasets/templates/ics_204.json new file mode 100644 index 00000000..615d3ddb --- /dev/null +++ b/benchmark/datasets/templates/ics_204.json @@ -0,0 +1,53 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_branch_division_group_staging_area": { + "branch": "string", + "division": "string", + "group": "string", + "staging_area": "string" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "string", + "contact_number": "string" + }, + "branch_director": { + "name": "string", + "contact_number": "string" + }, + "division_group_supervisor": { + "name": "string", + "contact_number": "string" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "string", + "leader": "string", + "number_of_persons": "string", + "contact": "string", + "reporting_location_special_equipment_remarks_notes_information": "string" + } + ], + "6_work_assignments": "string", + "7_special_instructions": "string", + "8_communications": [ + { + "name_function": "string", + "primary_contact": "string" + } + ], + "9_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} From a3ab05abf63c00d09698f71120249a878d481872 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 16:59:19 +0200 Subject: [PATCH 77/85] ics205 & ics205a --- benchmark/datasets/ground_truth/ics205_1.json | 76 +++++++++++++++++++ benchmark/datasets/ground_truth/ics205_2.json | 76 +++++++++++++++++++ benchmark/datasets/ground_truth/ics205_3.json | 76 +++++++++++++++++++ benchmark/datasets/ground_truth/ics205_4.json | 76 +++++++++++++++++++ benchmark/datasets/ground_truth/ics205_5.json | 76 +++++++++++++++++++ .../datasets/ground_truth/ics205a_1.json | 55 ++++++++++++++ .../datasets/ground_truth/ics205a_2.json | 55 ++++++++++++++ .../datasets/ground_truth/ics205a_3.json | 55 ++++++++++++++ .../datasets/ground_truth/ics205a_4.json | 55 ++++++++++++++ .../datasets/ground_truth/ics205a_5.json | 55 ++++++++++++++ benchmark/datasets/narratives/ics205_1.txt | 11 +++ benchmark/datasets/narratives/ics205_2.txt | 11 +++ benchmark/datasets/narratives/ics205_3.txt | 11 +++ benchmark/datasets/narratives/ics205_4.txt | 11 +++ benchmark/datasets/narratives/ics205_5.txt | 11 +++ benchmark/datasets/narratives/ics205a_1.txt | 14 ++++ benchmark/datasets/narratives/ics205a_2.txt | 14 ++++ benchmark/datasets/narratives/ics205a_3.txt | 14 ++++ benchmark/datasets/narratives/ics205a_4.txt | 14 ++++ benchmark/datasets/narratives/ics205a_5.txt | 14 ++++ benchmark/datasets/templates/ics_205.json | 35 +++++++++ benchmark/datasets/templates/ics_205a.json | 23 ++++++ 22 files changed, 838 insertions(+) create mode 100644 benchmark/datasets/ground_truth/ics205_1.json create mode 100644 benchmark/datasets/ground_truth/ics205_2.json create mode 100644 benchmark/datasets/ground_truth/ics205_3.json create mode 100644 benchmark/datasets/ground_truth/ics205_4.json create mode 100644 benchmark/datasets/ground_truth/ics205_5.json create mode 100644 benchmark/datasets/ground_truth/ics205a_1.json create mode 100644 benchmark/datasets/ground_truth/ics205a_2.json create mode 100644 benchmark/datasets/ground_truth/ics205a_3.json create mode 100644 benchmark/datasets/ground_truth/ics205a_4.json create mode 100644 benchmark/datasets/ground_truth/ics205a_5.json create mode 100644 benchmark/datasets/narratives/ics205_1.txt create mode 100644 benchmark/datasets/narratives/ics205_2.txt create mode 100644 benchmark/datasets/narratives/ics205_3.txt create mode 100644 benchmark/datasets/narratives/ics205_4.txt create mode 100644 benchmark/datasets/narratives/ics205_5.txt create mode 100644 benchmark/datasets/narratives/ics205a_1.txt create mode 100644 benchmark/datasets/narratives/ics205a_2.txt create mode 100644 benchmark/datasets/narratives/ics205a_3.txt create mode 100644 benchmark/datasets/narratives/ics205a_4.txt create mode 100644 benchmark/datasets/narratives/ics205a_5.txt create mode 100644 benchmark/datasets/templates/ics_205.json create mode 100644 benchmark/datasets/templates/ics_205a.json diff --git a/benchmark/datasets/ground_truth/ics205_1.json b/benchmark/datasets/ground_truth/ics205_1.json new file mode 100644 index 00000000..1053aaef --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_1.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_date_time_prepared": { + "date": "07/07/2026", + "time": "16:00" + }, + "3_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone 1", + "channel_number": "1", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "TAC-1", + "assignment": "Unified Command / Section Chiefs", + "rx_frequency_n_or_w": "154.2800 N", + "rx_tone_nac": "156.7", + "tx_frequency_n_or_w": "159.4800 N", + "tx_tone_nac": "156.7", + "mode_a_d_or_m": "A", + "remarks": "Repeater 1 on Ridge Top" + }, + { + "zone_grp": "Zone 1", + "channel_number": "2", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "TAC-2", + "assignment": "Hazmat Entry Group", + "rx_frequency_n_or_w": "462.5500 N", + "rx_tone_nac": "114.8", + "tx_frequency_n_or_w": "467.5500 N", + "tx_tone_nac": "114.8", + "mode_a_d_or_m": "D", + "remarks": "Encrypted digital talkgroup" + }, + { + "zone_grp": "Zone 1", + "channel_number": "3", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "TAC-3", + "assignment": "Fire Suppression Branch", + "rx_frequency_n_or_w": "153.8300 N", + "rx_tone_nac": "156.7", + "tx_frequency_n_or_w": "153.8300 N", + "tx_tone_nac": "156.7", + "mode_a_d_or_m": "A", + "remarks": "Direct tactical simplex" + }, + { + "zone_grp": "Zone 1", + "channel_number": "4", + "function": "Support", + "channel_name_trunked_radio_system_talkgroup": "SUPP-1", + "assignment": "Logistics & Decon", + "rx_frequency_n_or_w": "462.6250 N", + "rx_tone_nac": "123.0", + "tx_frequency_n_or_w": "462.6250 N", + "tx_tone_nac": "123.0", + "mode_a_d_or_m": "A", + "remarks": "Staging & decon trailer" + } + ], + "5_special_instructions": "All emergency traffic must use 'MAYDAY' call sign on Channel 1. Channel 2 encryption key loaded at Staging Area Alpha.", + "6_prepared_by": { + "name": "R. Lee", + "signature": "R. Lee", + "date_time": "07/07/2026 16:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_2.json b/benchmark/datasets/ground_truth/ics205_2.json new file mode 100644 index 00000000..59c9b066 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_2.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_date_time_prepared": { + "date": "08/13/2026", + "time": "16:00" + }, + "3_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone A", + "channel_number": "1", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "STATE-CMD", + "assignment": "Unified Command / Operations", + "rx_frequency_n_or_w": "153.8300 N", + "rx_tone_nac": "131.8", + "tx_frequency_n_or_w": "158.9700 N", + "tx_tone_nac": "131.8", + "mode_a_d_or_m": "A", + "remarks": "County Repeater 4" + }, + { + "zone_grp": "Zone A", + "channel_number": "2", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "HAZ-TAC-1", + "assignment": "Hot Zone Entry Group", + "rx_frequency_n_or_w": "467.7750 N", + "rx_tone_nac": "D023", + "tx_frequency_n_or_w": "467.7750 N", + "tx_tone_nac": "D023", + "mode_a_d_or_m": "D", + "remarks": "Intrinsically safe radios only" + }, + { + "zone_grp": "Zone A", + "channel_number": "3", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "BOOM-TAC", + "assignment": "River Booming Division", + "rx_frequency_n_or_w": "154.2800 N", + "rx_tone_nac": "131.8", + "tx_frequency_n_or_w": "154.2800 N", + "tx_tone_nac": "131.8", + "mode_a_d_or_m": "A", + "remarks": "Simplex marine/land link" + }, + { + "zone_grp": "Zone A", + "channel_number": "4", + "function": "Dispatch", + "channel_name_trunked_radio_system_talkgroup": "LAW-DISP", + "assignment": "Perimeter Evacuation / SO", + "rx_frequency_n_or_w": "460.1250 N", + "rx_tone_nac": "156.7", + "tx_frequency_n_or_w": "465.1250 N", + "tx_tone_nac": "156.7", + "mode_a_d_or_m": "D", + "remarks": "County Sheriff Dispatch" + } + ], + "5_special_instructions": "Only intrinsically safe portable radios (Class I, Div 1) permitted inside the 500-foot Exclusion Zone. Maintain 30-minute radio check-ins.", + "6_prepared_by": { + "name": "K. Albright", + "signature": "K. Albright", + "date_time": "08/13/2026 16:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_3.json b/benchmark/datasets/ground_truth/ics205_3.json new file mode 100644 index 00000000..4844bd5f --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_3.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_date_time_prepared": { + "date": "10/14/2026", + "time": "05:30" + }, + "3_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone Port", + "channel_number": "16", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "VHF 16 / CMD", + "assignment": "USCG / Unified Command", + "rx_frequency_n_or_w": "156.8000 W", + "rx_tone_nac": "None", + "tx_frequency_n_or_w": "156.8000 W", + "tx_tone_nac": "None", + "mode_a_d_or_m": "A", + "remarks": "International Maritime Distress & Command" + }, + { + "zone_grp": "Zone Port", + "channel_number": "11", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "VHF 11 / OPS", + "assignment": "Vessel Deck Entry Group", + "rx_frequency_n_or_w": "156.5500 W", + "rx_tone_nac": "None", + "tx_frequency_n_or_w": "156.5500 W", + "tx_tone_nac": "None", + "mode_a_d_or_m": "A", + "remarks": "Ship-to-shore deck entry" + }, + { + "zone_grp": "Zone Port", + "channel_number": "4", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "PORT-HAZ", + "assignment": "Chemical Neutralization Team", + "rx_frequency_n_or_w": "453.2250 N", + "rx_tone_nac": "141.3", + "tx_frequency_n_or_w": "458.2250 N", + "tx_tone_nac": "141.3", + "mode_a_d_or_m": "D", + "remarks": "Port Fire Hazmat digital" + }, + { + "zone_grp": "Zone Port", + "channel_number": "5", + "function": "Support", + "channel_name_trunked_radio_system_talkgroup": "MED-NET", + "assignment": "Medical & Decon Operations", + "rx_frequency_n_or_w": "462.6000 N", + "rx_tone_nac": "100.0", + "tx_frequency_n_or_w": "462.6000 N", + "tx_tone_nac": "100.0", + "mode_a_d_or_m": "A", + "remarks": "Decon trailer baseline" + } + ], + "5_special_instructions": "VHF Channel 16 reserved for Command & emergency traffic only. Deck entry team must maintain dual-watch on VHF 11 and Port-Haz Ch 4.", + "6_prepared_by": { + "name": "J. Myers", + "signature": "J. Myers", + "date_time": "10/14/2026 05:30" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_4.json b/benchmark/datasets/ground_truth/ics205_4.json new file mode 100644 index 00000000..5efc9bae --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_4.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_date_time_prepared": { + "date": "11/02/2026", + "time": "16:00" + }, + "3_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone Pass", + "channel_number": "1", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "WSP-CMD", + "assignment": "Unified Command / Operations", + "rx_frequency_n_or_w": "154.4000 N", + "rx_tone_nac": "103.5", + "tx_frequency_n_or_w": "159.0300 N", + "tx_tone_nac": "103.5", + "mode_a_d_or_m": "A", + "remarks": "Stevens Pass Mountain Repeater" + }, + { + "zone_grp": "Zone Pass", + "channel_number": "3", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "CAPP-TAC", + "assignment": "Railcar Entry & Capping Team", + "rx_frequency_n_or_w": "462.7000 N", + "rx_tone_nac": "D074", + "tx_frequency_n_or_w": "467.7000 N", + "tx_tone_nac": "D074", + "mode_a_d_or_m": "D", + "remarks": "Digital cross-band repeater in ravine" + }, + { + "zone_grp": "Zone Pass", + "channel_number": "4", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "EVAC-TAC", + "assignment": "Highway 2 & Evacuation Security", + "rx_frequency_n_or_w": "467.5500 N", + "rx_tone_nac": "114.8", + "tx_frequency_n_or_w": "467.5500 N", + "tx_tone_nac": "114.8", + "mode_a_d_or_m": "A", + "remarks": "Sheriff patrol direct" + }, + { + "zone_grp": "Zone Pass", + "channel_number": "6", + "function": "Support", + "channel_name_trunked_radio_system_talkgroup": "LOG-SUPP", + "assignment": "Decon & Staging Charlie", + "rx_frequency_n_or_w": "151.6250 N", + "rx_tone_nac": "67.0", + "tx_frequency_n_or_w": "151.6250 N", + "tx_tone_nac": "67.0", + "mode_a_d_or_m": "A", + "remarks": "Staging Area Charlie link" + } + ], + "5_special_instructions": "Portable cross-band repeater deployed at ravine rim to ensure coverage inside mountain shadow. Cold-weather battery packs required for all handheld portables.", + "6_prepared_by": { + "name": "B. Foster", + "signature": "B. Foster", + "date_time": "11/02/2026 16:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_5.json b/benchmark/datasets/ground_truth/ics205_5.json new file mode 100644 index 00000000..2429db4f --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_5.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_date_time_prepared": { + "date": "05/18/2026", + "time": "16:00" + }, + "3_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone Lake", + "channel_number": "1", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "FIRE-CMD", + "assignment": "Unified Command / Station 30", + "rx_frequency_n_or_w": "154.1500 N", + "rx_tone_nac": "146.2", + "tx_frequency_n_or_w": "158.8500 N", + "tx_tone_nac": "146.2", + "mode_a_d_or_m": "A", + "remarks": "County Station 30 Repeater" + }, + { + "zone_grp": "Zone Lake", + "channel_number": "3", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "MARINE-11", + "assignment": "Reservoir Skimmer Fleet", + "rx_frequency_n_or_w": "156.5500 W", + "rx_tone_nac": "None", + "tx_frequency_n_or_w": "156.5500 W", + "tx_tone_nac": "None", + "mode_a_d_or_m": "A", + "remarks": "VHF Channel 11 Marine Simplex" + }, + { + "zone_grp": "Zone Lake", + "channel_number": "4", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "PIPE-TAC", + "assignment": "Canyon Pipeline Repair Group", + "rx_frequency_n_or_w": "462.5750 N", + "rx_tone_nac": "D115", + "tx_frequency_n_or_w": "467.5750 N", + "tx_tone_nac": "D115", + "mode_a_d_or_m": "D", + "remarks": "Sawpit Canyon tactical link" + }, + { + "zone_grp": "Zone Lake", + "channel_number": "5", + "function": "Support", + "channel_name_trunked_radio_system_talkgroup": "PARK-SUPP", + "assignment": "State Parks & Intake Guard", + "rx_frequency_n_or_w": "151.4000 N", + "rx_tone_nac": "146.2", + "tx_frequency_n_or_w": "151.4000 N", + "tx_tone_nac": "146.2", + "mode_a_d_or_m": "A", + "remarks": "Water Agency & Park Rangers" + } + ], + "5_special_instructions": "All marine vessels must maintain continuous watch on VHF Channel 11. Emergency Mayday protocol monitored by Command on Ch 1.", + "6_prepared_by": { + "name": "Sgt. R. Hall", + "signature": "R. Hall", + "date_time": "05/18/2026 16:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_1.json b/benchmark/datasets/ground_truth/ics205a_1.json new file mode 100644 index 00000000..0fb968ff --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_1.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "Incident Commander (County Fire)", + "name": "Chief J. Thomas", + "methods_of_contact": "Cell: 555-0101 / Radio Ch 1 / Vehicle: FIRE-CMD-1" + }, + { + "incident_assigned_position": "Co-Incident Commander (State DEQ)", + "name": "S. Albright", + "methods_of_contact": "Cell: 555-0102 / Radio Ch 1" + }, + { + "incident_assigned_position": "Co-Incident Commander (CSX RP)", + "name": "D. Miller", + "methods_of_contact": "Cell: 555-0103 / Radio Ch 1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Captain R. Mendez", + "methods_of_contact": "Cell: 555-0177 / Radio Ch 1 & 4" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "B. Reynolds", + "methods_of_contact": "Cell: 555-0192 / Radio Ch 1" + }, + { + "incident_assigned_position": "Hazmat Branch Director", + "name": "Lt. T. Kincaid", + "methods_of_contact": "Cell: 555-0144 / Radio Ch 3 & 4 / Vehicle: HZM-01" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Marcus Vance", + "methods_of_contact": "Cell: 555-0150 / CP Desk Ext: 104" + } + ], + "4_prepared_by": { + "name": "R. Lee", + "position_title": "Communications Unit Leader", + "signature": "R. Lee", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_2.json b/benchmark/datasets/ground_truth/ics205a_2.json new file mode 100644 index 00000000..0a876ffc --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_2.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "Incident Commander (Fire)", + "name": "Chief R. Vance", + "methods_of_contact": "Cell: 555-0201 / Radio Ch 1" + }, + { + "incident_assigned_position": "Incident Commander (State EPA)", + "name": "Officer M. Ross", + "methods_of_contact": "Cell: 555-0202 / Radio Ch 1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Captain L. Hayes", + "methods_of_contact": "Cell: 555-0288 / Radio Ch 1" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "D. Kowalski", + "methods_of_contact": "Cell: 555-0210 / Radio Ch 1" + }, + { + "incident_assigned_position": "Pipeline Branch Director", + "name": "Capt. Donald Kross", + "methods_of_contact": "Cell: 555-0233 / Radio Ch 2 / Vehicle: HAZ-1" + }, + { + "incident_assigned_position": "Entry Group Supervisor", + "name": "Lt. R. Mendez", + "methods_of_contact": "Cell: 555-0255 / Radio Ch 3 / Vehicle: HZM-05" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Sarah L. Jenkins", + "methods_of_contact": "Cell: 555-0250 / CP Ext: 201" + } + ], + "4_prepared_by": { + "name": "K. Albright", + "position_title": "Communications Unit Leader", + "signature": "K. Albright", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_3.json b/benchmark/datasets/ground_truth/ics205a_3.json new file mode 100644 index 00000000..1be12fe0 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_3.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "USCG Unified Commander", + "name": "Captain H. Vance", + "methods_of_contact": "Cell: 555-0301 / VHF Ch 16 / Vessel: CG-45601" + }, + { + "incident_assigned_position": "EPA Co-Incident Commander", + "name": "OSC M. Reynolds", + "methods_of_contact": "Cell: 555-0302 / Radio Ch 1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Commander Thomas Blake", + "methods_of_contact": "Cell: 555-0399 / VHF Ch 16" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "Battalion Chief A. Ross", + "methods_of_contact": "Cell: 555-0312 / VHF Ch 16" + }, + { + "incident_assigned_position": "Vessel Salvage Branch Director", + "name": "Lt. J. Thorne", + "methods_of_contact": "Cell: 555-0344 / VHF Ch 11" + }, + { + "incident_assigned_position": "Deck Entry Supervisor", + "name": "Lt. S. Baker", + "methods_of_contact": "Cell: 555-0366 / Radio Ch 4 / Vehicle: HZM-PORT-1" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Commander Richard Croft", + "methods_of_contact": "Cell: 555-0350 / Sector Ext: 305" + } + ], + "4_prepared_by": { + "name": "J. Myers", + "position_title": "Communications Unit Leader", + "signature": "J. Myers", + "date_time": "10/14/2026 06:15" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_4.json b/benchmark/datasets/ground_truth/ics205a_4.json new file mode 100644 index 00000000..b1107d23 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_4.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "WSP Incident Commander", + "name": "Captain E. Miller", + "methods_of_contact": "Cell: 555-0401 / Radio Ch 1 / Unit: WSP-100" + }, + { + "incident_assigned_position": "EPA R10 Co-Commander", + "name": "OSC R. Brooks", + "methods_of_contact": "Cell: 555-0402 / Radio Ch 1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Captain Marcus Vance", + "methods_of_contact": "Cell: 555-0488 / Radio Ch 1" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "Battalion Chief T. Higgins", + "methods_of_contact": "Cell: 555-0411 / Radio Ch 1" + }, + { + "incident_assigned_position": "Hazmat Capping Branch Director", + "name": "Capt. P. Gomez", + "methods_of_contact": "Cell: 555-0433 / Radio Ch 2 & 3" + }, + { + "incident_assigned_position": "Railcar Entry Supervisor", + "name": "Capt. D. Ross", + "methods_of_contact": "Cell: 555-0455 / Radio Ch 3 / Vehicle: HZM-33" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Lt. Colonel Alan Vance", + "methods_of_contact": "Cell: 555-0450 / CP Desk: Gym-1" + } + ], + "4_prepared_by": { + "name": "B. Foster", + "position_title": "Communications Unit Leader", + "signature": "B. Foster", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_5.json b/benchmark/datasets/ground_truth/ics205a_5.json new file mode 100644 index 00000000..afaec8e3 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_5.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "EPA R9 Incident Commander", + "name": "OSC C. Martinez", + "methods_of_contact": "Cell: 555-0501 / Radio Ch 1" + }, + { + "incident_assigned_position": "SBCo Fire Incident Commander", + "name": "Chief M. Thorne", + "methods_of_contact": "Cell: 555-0502 / Radio Ch 1 / Vehicle: FIRE-SB-1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Captain Gregory Hall", + "methods_of_contact": "Cell: 555-0588 / Radio Ch 1" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "Battalion Chief Kevin Ross", + "methods_of_contact": "Cell: 555-0515 / Radio Ch 1" + }, + { + "incident_assigned_position": "Marine Skimming Branch Director", + "name": "Captain B. Walsh", + "methods_of_contact": "Cell: 555-0535 / Radio Ch 3 (VHF 11) / Vessel: RV-LC-01" + }, + { + "incident_assigned_position": "Canyon Repair Group Supervisor", + "name": "Lt. J. Thorne", + "methods_of_contact": "Cell: 555-0560 / Radio Ch 4 / Vehicle: CREW-04" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Captain Rachel Brooks", + "methods_of_contact": "Cell: 555-0550 / CP Desk Ext: 403" + } + ], + "4_prepared_by": { + "name": "Sgt. R. Hall", + "position_title": "Communications Unit Leader", + "signature": "R. Hall", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/narratives/ics205_1.txt b/benchmark/datasets/narratives/ics205_1.txt new file mode 100644 index 00000000..b32bd728 --- /dev/null +++ b/benchmark/datasets/narratives/ics205_1.txt @@ -0,0 +1,11 @@ +### Whispering Pines Derailment Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, was prepared on July 07, 2026, at 16:00 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (July 07, 2026, 18:00 hours through July 08, 2026, 06:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 1 (Zone 1): Functioned for Command, channel name TAC-1, assigned to Unified Command and Section Chiefs. Operates on RX frequency 154.2800 N (Tone 156.7) and TX frequency 159.4800 N (Tone 156.7) in Analog mode (A), utilizing Repeater 1 located on Ridge Top. +2. Channel 2 (Zone 1): Functioned for Tactical operations, channel name TAC-2, assigned to Hazmat Entry Group. Operates on RX frequency 462.5500 N (Tone 114.8) and TX frequency 467.5500 N (Tone 114.8) in Digital mode (D), utilizing an encrypted digital talkgroup. +3. Channel 3 (Zone 1): Functioned for Tactical operations, channel name TAC-3, assigned to Fire Suppression Branch. Operates on RX frequency 153.8300 N (Tone 156.7) and TX frequency 153.8300 N (Tone 156.7) in Analog mode (A), utilizing direct tactical simplex communications. +4. Channel 4 (Zone 1): Functioned for Support operations, channel name SUPP-1, assigned to Logistics and Decon. Operates on RX frequency 462.6250 N (Tone 123.0) and TX frequency 462.6250 N (Tone 123.0) in Analog mode (A), serving Staging Area Alpha and the decon trailer. + +Special instructions dictate that all emergency radio traffic must use the 'MAYDAY' call sign prefix on Channel 1. Channel 2 encryption keys must be loaded into handheld radios at Staging Area Alpha prior to Hot Zone deployment. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader R. Lee on July 07, 2026, at 16:00 hours. diff --git a/benchmark/datasets/narratives/ics205_2.txt b/benchmark/datasets/narratives/ics205_2.txt new file mode 100644 index 00000000..e09c47fd --- /dev/null +++ b/benchmark/datasets/narratives/ics205_2.txt @@ -0,0 +1,11 @@ +### Blackwood Chemical Pipeline Breach Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, was prepared on August 13, 2026, at 16:00 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (August 13, 2026, 18:00 hours through August 14, 2026, 06:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 1 (Zone A): Functioned for Command, channel name STATE-CMD, assigned to Unified Command and Operations. Operates on RX frequency 153.8300 N (Tone 131.8) and TX frequency 158.9700 N (Tone 131.8) in Analog mode (A), utilizing County Repeater 4. +2. Channel 2 (Zone A): Functioned for Tactical operations, channel name HAZ-TAC-1, assigned to Hot Zone Entry Group. Operates on RX frequency 467.7750 N (Tone D023) and TX frequency 467.7750 N (Tone D023) in Digital mode (D), restricting use strictly to intrinsically safe radios. +3. Channel 3 (Zone A): Functioned for Tactical operations, channel name BOOM-TAC, assigned to River Booming Division. Operates on RX frequency 154.2800 N (Tone 131.8) and TX frequency 154.2800 N (Tone 131.8) in Analog mode (A), serving as a simplex marine and land communications link. +4. Channel 4 (Zone A): Functioned for Dispatch, channel name LAW-DISP, assigned to Perimeter Evacuation and Sheriff's Office. Operates on RX frequency 460.1250 N (Tone 156.7) and TX frequency 465.1250 N (Tone 156.7) in Digital mode (D), linking to County Sheriff Dispatch. + +Special instructions mandate that only intrinsically safe portable radios (Class I, Div 1) are permitted inside the 500-foot Exclusion Zone. Personnel must maintain 30-minute mandatory radio check-ins. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader K. Albright on August 13, 2026, at 16:00 hours. diff --git a/benchmark/datasets/narratives/ics205_3.txt b/benchmark/datasets/narratives/ics205_3.txt new file mode 100644 index 00000000..7e187842 --- /dev/null +++ b/benchmark/datasets/narratives/ics205_3.txt @@ -0,0 +1,11 @@ +### Port of Houston Sulfuric Acid Tanker Leak Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, was prepared on October 14, 2026, at 05:30 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (October 14, 2026, 07:00 hours through October 15, 2026, 19:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 16 (Zone Port): Functioned for Command, channel name VHF 16 / CMD, assigned to USCG and Unified Command. Operates on RX frequency 156.8000 W (Tone None) and TX frequency 156.8000 W (Tone None) in Analog mode (A), serving as International Maritime Distress and Command channel. +2. Channel 11 (Zone Port): Functioned for Tactical operations, channel name VHF 11 / OPS, assigned to Vessel Deck Entry Group. Operates on RX frequency 156.5500 W (Tone None) and TX frequency 156.5500 W (Tone None) in Analog mode (A), serving ship-to-shore deck entry operations. +3. Channel 4 (Zone Port): Functioned for Tactical operations, channel name PORT-HAZ, assigned to Chemical Neutralization Team. Operates on RX frequency 453.2250 N (Tone 141.3) and TX frequency 458.2250 N (Tone 141.3) in Digital mode (D), utilizing Port Fire Hazmat digital system. +4. Channel 5 (Zone Port): Functioned for Support operations, channel name MED-NET, assigned to Medical & Decon Operations. Operates on RX frequency 462.6000 N (Tone 100.0) and TX frequency 462.6000 N (Tone 100.0) in Analog mode (A), maintaining communication with decon trailer baseline. + +Special instructions dictate that VHF Channel 16 is reserved strictly for Command and emergency traffic. The deck entry team must maintain dual-watch monitoring on VHF Channel 11 and Port-Haz Channel 4. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader J. Myers on October 14, 2026, at 05:30 hours. diff --git a/benchmark/datasets/narratives/ics205_4.txt b/benchmark/datasets/narratives/ics205_4.txt new file mode 100644 index 00000000..1966788d --- /dev/null +++ b/benchmark/datasets/narratives/ics205_4.txt @@ -0,0 +1,11 @@ +### Cascade Pass Chlorine Railcar Derailment Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, was prepared on November 02, 2026, at 16:00 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (November 02, 2026, 18:00 hours through November 03, 2026, 06:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 1 (Zone Pass): Functioned for Command, channel name WSP-CMD, assigned to Unified Command and Operations. Operates on RX frequency 154.4000 N (Tone 103.5) and TX frequency 159.0300 N (Tone 103.5) in Analog mode (A), utilizing Stevens Pass Mountain Repeater. +2. Channel 3 (Zone Pass): Functioned for Tactical operations, channel name CAPP-TAC, assigned to Railcar Entry & Capping Team. Operates on RX frequency 462.7000 N (Tone D074) and TX frequency 467.7000 N (Tone D074) in Digital mode (D), utilizing a digital cross-band repeater deployed in the ravine. +3. Channel 4 (Zone Pass): Functioned for Tactical operations, channel name EVAC-TAC, assigned to Highway 2 & Evacuation Security. Operates on RX frequency 467.5500 N (Tone 114.8) and TX frequency 467.5500 N (Tone 114.8) in Analog mode (A), serving Sheriff patrol direct communications. +4. Channel 6 (Zone Pass): Functioned for Support operations, channel name LOG-SUPP, assigned to Decon & Staging Charlie. Operates on RX frequency 151.6250 N (Tone 67.0) and TX frequency 151.6250 N (Tone 67.0) in Analog mode (A), maintaining Staging Area Charlie link. + +Special instructions indicate that a portable cross-band repeater is deployed at the ravine rim to ensure coverage inside mountain signal shadows. Cold-weather battery packs are strictly required for all handheld portable radios. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader B. Foster on November 02, 2026, at 16:00 hours. diff --git a/benchmark/datasets/narratives/ics205_5.txt b/benchmark/datasets/narratives/ics205_5.txt new file mode 100644 index 00000000..de01cf3f --- /dev/null +++ b/benchmark/datasets/narratives/ics205_5.txt @@ -0,0 +1,11 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, was prepared on May 18, 2026, at 16:00 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (May 18, 2026, 18:00 hours through May 19, 2026, 06:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 1 (Zone Lake): Functioned for Command, channel name FIRE-CMD, assigned to Unified Command and Station 30. Operates on RX frequency 154.1500 N (Tone 146.2) and TX frequency 158.8500 N (Tone 146.2) in Analog mode (A), utilizing County Station 30 Repeater. +2. Channel 3 (Zone Lake): Functioned for Tactical operations, channel name MARINE-11, assigned to Reservoir Skimmer Fleet. Operates on RX frequency 156.5500 W (Tone None) and TX frequency 156.5500 W (Tone None) in Analog mode (A), using VHF Channel 11 Marine Simplex. +3. Channel 4 (Zone Lake): Functioned for Tactical operations, channel name PIPE-TAC, assigned to Canyon Pipeline Repair Group. Operates on RX frequency 462.5750 N (Tone D115) and TX frequency 467.5750 N (Tone D115) in Digital mode (D), linking Sawpit Canyon tactical operations. +4. Channel 5 (Zone Lake): Functioned for Support operations, channel name PARK-SUPP, assigned to State Parks & Water Intake Guard. Operates on RX frequency 151.4000 N (Tone 146.2) and TX frequency 151.4000 N (Tone 146.2) in Analog mode (A), maintaining communication with Mojave Water Agency and Park Rangers. + +Special instructions dictate that all marine vessels must maintain continuous watch on VHF Channel 11. Emergency Mayday calls will be monitored by Command on Channel 1. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader Sgt. R. Hall on May 18, 2026, at 16:00 hours. diff --git a/benchmark/datasets/narratives/ics205a_1.txt b/benchmark/datasets/narratives/ics205a_1.txt new file mode 100644 index 00000000..84517d38 --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_1.txt @@ -0,0 +1,14 @@ +### Whispering Pines Derailment Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, functions as the primary incident contact directory for Operational Period 1 (July 07, 2026, 18:00 hours through July 08, 2026, 06:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: Incident Commander (County Fire), Name: Chief J. Thomas, Contact Methods: Cell: 555-0101 / Radio Ch 1 / Vehicle: FIRE-CMD-1. +2. Position: Co-Incident Commander (State DEQ), Name: S. Albright, Contact Methods: Cell: 555-0102 / Radio Ch 1. +3. Position: Co-Incident Commander (CSX RP), Name: D. Miller, Contact Methods: Cell: 555-0103 / Radio Ch 1. +4. Position: Safety Officer, Name: Captain R. Mendez, Contact Methods: Cell: 555-0177 / Radio Ch 1 & 4. +5. Position: Operations Section Chief, Name: B. Reynolds, Contact Methods: Cell: 555-0192 / Radio Ch 1. +6. Position: Hazmat Branch Director, Name: Lt. T. Kincaid, Contact Methods: Cell: 555-0144 / Radio Ch 3 & 4 / Vehicle: HZM-01. +7. Position: Planning Section Chief, Name: Marcus Vance, Contact Methods: Cell: 555-0150 / CP Desk Ext: 104. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader R. Lee on July 07, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics205a_2.txt b/benchmark/datasets/narratives/ics205a_2.txt new file mode 100644 index 00000000..e7e74cca --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_2.txt @@ -0,0 +1,14 @@ +### Blackwood Chemical Pipeline Breach Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, functions as the primary incident contact directory for Operational Period 1 (August 13, 2026, 18:00 hours through August 14, 2026, 06:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: Incident Commander (Fire), Name: Chief R. Vance, Contact Methods: Cell: 555-0201 / Radio Ch 1. +2. Position: Incident Commander (State EPA), Name: Officer M. Ross, Contact Methods: Cell: 555-0202 / Radio Ch 1. +3. Position: Safety Officer, Name: Captain L. Hayes, Contact Methods: Cell: 555-0288 / Radio Ch 1. +4. Position: Operations Section Chief, Name: D. Kowalski, Contact Methods: Cell: 555-0210 / Radio Ch 1. +5. Position: Pipeline Branch Director, Name: Capt. Donald Kross, Contact Methods: Cell: 555-0233 / Radio Ch 2 / Vehicle: HAZ-1. +6. Position: Entry Group Supervisor, Name: Lt. R. Mendez, Contact Methods: Cell: 555-0255 / Radio Ch 3 / Vehicle: HZM-05. +7. Position: Planning Section Chief, Name: Sarah L. Jenkins, Contact Methods: Cell: 555-0250 / CP Ext: 201. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader K. Albright on August 13, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics205a_3.txt b/benchmark/datasets/narratives/ics205a_3.txt new file mode 100644 index 00000000..e77a1467 --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_3.txt @@ -0,0 +1,14 @@ +### Port of Houston Sulfuric Acid Tanker Leak Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, functions as the primary incident contact directory for Operational Period 1 (October 14, 2026, 07:00 hours through October 15, 2026, 19:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: USCG Unified Commander, Name: Captain H. Vance, Contact Methods: Cell: 555-0301 / VHF Ch 16 / Vessel: CG-45601. +2. Position: EPA Co-Incident Commander, Name: OSC M. Reynolds, Contact Methods: Cell: 555-0302 / Radio Ch 1. +3. Position: Safety Officer, Name: Commander Thomas Blake, Contact Methods: Cell: 555-0399 / VHF Ch 16. +4. Position: Operations Section Chief, Name: Battalion Chief A. Ross, Contact Methods: Cell: 555-0312 / VHF Ch 16. +5. Position: Vessel Salvage Branch Director, Name: Lt. J. Thorne, Contact Methods: Cell: 555-0344 / VHF Ch 11. +6. Position: Deck Entry Supervisor, Name: Lt. S. Baker, Contact Methods: Cell: 555-0366 / Radio Ch 4 / Vehicle: HZM-PORT-1. +7. Position: Planning Section Chief, Name: Commander Richard Croft, Contact Methods: Cell: 555-0350 / Sector Ext: 305. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader J. Myers on October 14, 2026, at 06:15 hours. diff --git a/benchmark/datasets/narratives/ics205a_4.txt b/benchmark/datasets/narratives/ics205a_4.txt new file mode 100644 index 00000000..fd6ef8ee --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_4.txt @@ -0,0 +1,14 @@ +### Cascade Pass Chlorine Railcar Derailment Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, functions as the primary incident contact directory for Operational Period 1 (November 02, 2026, 18:00 hours through November 03, 2026, 06:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: WSP Incident Commander, Name: Captain E. Miller, Contact Methods: Cell: 555-0401 / Radio Ch 1 / Unit: WSP-100. +2. Position: EPA R10 Co-Commander, Name: OSC R. Brooks, Contact Methods: Cell: 555-0402 / Radio Ch 1. +3. Position: Safety Officer, Name: Captain Marcus Vance, Contact Methods: Cell: 555-0488 / Radio Ch 1. +4. Position: Operations Section Chief, Name: Battalion Chief T. Higgins, Contact Methods: Cell: 555-0411 / Radio Ch 1. +5. Position: Hazmat Capping Branch Director, Name: Capt. P. Gomez, Contact Methods: Cell: 555-0433 / Radio Ch 2 & 3. +6. Position: Railcar Entry Supervisor, Name: Capt. D. Ross, Contact Methods: Cell: 555-0455 / Radio Ch 3 / Vehicle: HZM-33. +7. Position: Planning Section Chief, Name: Lt. Colonel Alan Vance, Contact Methods: Cell: 555-0450 / CP Desk: Gym-1. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader B. Foster on November 02, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics205a_5.txt b/benchmark/datasets/narratives/ics205a_5.txt new file mode 100644 index 00000000..bb34ddac --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_5.txt @@ -0,0 +1,14 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, functions as the primary incident contact directory for Operational Period 1 (May 18, 2026, 18:00 hours through May 19, 2026, 06:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: EPA R9 Incident Commander, Name: OSC C. Martinez, Contact Methods: Cell: 555-0501 / Radio Ch 1. +2. Position: SBCo Fire Incident Commander, Name: Chief M. Thorne, Contact Methods: Cell: 555-0502 / Radio Ch 1 / Vehicle: FIRE-SB-1. +3. Position: Safety Officer, Name: Captain Gregory Hall, Contact Methods: Cell: 555-0588 / Radio Ch 1. +4. Position: Operations Section Chief, Name: Battalion Chief Kevin Ross, Contact Methods: Cell: 555-0515 / Radio Ch 1. +5. Position: Marine Skimming Branch Director, Name: Captain B. Walsh, Contact Methods: Cell: 555-0535 / Radio Ch 3 (VHF 11) / Vessel: RV-LC-01. +6. Position: Canyon Repair Group Supervisor, Name: Lt. J. Thorne, Contact Methods: Cell: 555-0560 / Radio Ch 4 / Vehicle: CREW-04. +7. Position: Planning Section Chief, Name: Captain Rachel Brooks, Contact Methods: Cell: 555-0550 / CP Desk Ext: 403. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader Sgt. R. Hall on May 18, 2026, at 16:30 hours. diff --git a/benchmark/datasets/templates/ics_205.json b/benchmark/datasets/templates/ics_205.json new file mode 100644 index 00000000..0e1af7a7 --- /dev/null +++ b/benchmark/datasets/templates/ics_205.json @@ -0,0 +1,35 @@ +{ + "1_incident_name": "string", + "2_date_time_prepared": { + "date": "string", + "time": "string" + }, + "3_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "string", + "channel_number": "string", + "function": "string", + "channel_name_trunked_radio_system_talkgroup": "string", + "assignment": "string", + "rx_frequency_n_or_w": "string", + "rx_tone_nac": "string", + "tx_frequency_n_or_w": "string", + "tx_tone_nac": "string", + "mode_a_d_or_m": "string", + "remarks": "string" + } + ], + "5_special_instructions": "string", + "6_prepared_by": { + "name": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_205a.json b/benchmark/datasets/templates/ics_205a.json new file mode 100644 index 00000000..92883977 --- /dev/null +++ b/benchmark/datasets/templates/ics_205a.json @@ -0,0 +1,23 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "string", + "name": "string", + "methods_of_contact": "string" + } + ], + "4_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} From 2f24ff829f5be9f4967a8e5af59c039cd99e2167 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 17:08:49 +0200 Subject: [PATCH 78/85] ics 206 & ics 213 --- benchmark/datasets/ground_truth/ics206_1.json | 80 +++++++++++++++++++ benchmark/datasets/ground_truth/ics206_2.json | 80 +++++++++++++++++++ benchmark/datasets/ground_truth/ics206_3.json | 80 +++++++++++++++++++ benchmark/datasets/ground_truth/ics206_4.json | 80 +++++++++++++++++++ benchmark/datasets/ground_truth/ics206_5.json | 80 +++++++++++++++++++ benchmark/datasets/ground_truth/ics213_1.json | 29 +++++++ benchmark/datasets/ground_truth/ics213_2.json | 29 +++++++ benchmark/datasets/ground_truth/ics213_3.json | 29 +++++++ benchmark/datasets/ground_truth/ics213_4.json | 29 +++++++ benchmark/datasets/ground_truth/ics213_5.json | 29 +++++++ benchmark/datasets/narratives/ics206_1.txt | 9 +++ benchmark/datasets/narratives/ics206_2.txt | 9 +++ benchmark/datasets/narratives/ics206_3.txt | 9 +++ benchmark/datasets/narratives/ics206_4.txt | 9 +++ benchmark/datasets/narratives/ics206_5.txt | 9 +++ benchmark/datasets/narratives/ics213_1.txt | 5 ++ benchmark/datasets/narratives/ics213_2.txt | 5 ++ benchmark/datasets/narratives/ics213_3.txt | 5 ++ benchmark/datasets/narratives/ics213_4.txt | 5 ++ benchmark/datasets/narratives/ics213_5.txt | 5 ++ benchmark/datasets/templates/ics_206.json | 58 ++++++++++++++ benchmark/datasets/templates/ics_213.json | 27 +++++++ 22 files changed, 700 insertions(+) create mode 100644 benchmark/datasets/ground_truth/ics206_1.json create mode 100644 benchmark/datasets/ground_truth/ics206_2.json create mode 100644 benchmark/datasets/ground_truth/ics206_3.json create mode 100644 benchmark/datasets/ground_truth/ics206_4.json create mode 100644 benchmark/datasets/ground_truth/ics206_5.json create mode 100644 benchmark/datasets/ground_truth/ics213_1.json create mode 100644 benchmark/datasets/ground_truth/ics213_2.json create mode 100644 benchmark/datasets/ground_truth/ics213_3.json create mode 100644 benchmark/datasets/ground_truth/ics213_4.json create mode 100644 benchmark/datasets/ground_truth/ics213_5.json create mode 100644 benchmark/datasets/narratives/ics206_1.txt create mode 100644 benchmark/datasets/narratives/ics206_2.txt create mode 100644 benchmark/datasets/narratives/ics206_3.txt create mode 100644 benchmark/datasets/narratives/ics206_4.txt create mode 100644 benchmark/datasets/narratives/ics206_5.txt create mode 100644 benchmark/datasets/narratives/ics213_1.txt create mode 100644 benchmark/datasets/narratives/ics213_2.txt create mode 100644 benchmark/datasets/narratives/ics213_3.txt create mode 100644 benchmark/datasets/narratives/ics213_4.txt create mode 100644 benchmark/datasets/narratives/ics213_5.txt create mode 100644 benchmark/datasets/templates/ics_206.json create mode 100644 benchmark/datasets/templates/ics_213.json diff --git a/benchmark/datasets/ground_truth/ics206_1.json b/benchmark/datasets/ground_truth/ics206_1.json new file mode 100644 index 00000000..9b8566b6 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_1.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_medical_aid_stations": [ + { + "name": "Staging Area Alpha First Aid Station", + "location": "Whispering Pines Creek Staging Area", + "contact_number_frequency": "462.5500 MHz / Ch 1", + "paramedic_service": true + }, + { + "name": "Forward Decon Medical Post", + "location": "Derailment Site Perimeter West", + "contact_number_frequency": "467.7750 MHz / Ch 2", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "County Emergency Medical Services Unit 41", + "location": "Whispering Pines Staging Area", + "contact_number_frequency": "(555) 234-8901 / Ch 1", + "paramedic_service": true + }, + { + "name": "Pine Valley Volunteer Fire & Rescue Ambulance 12", + "location": "Station 12 Base", + "contact_number_frequency": "(555) 234-8902 / Ch 1", + "paramedic_service": false + } + ], + "air_ambulance_services": [ + { + "name": "LifeFlight Air Medical Helicopter", + "location": "Regional Trauma Center Helipad", + "contact_number_frequency": "(555) 999-4321 / VHF 155.340 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Whispering Pines Regional Medical Center", + "address_latitude_longitude": "100 Medical Center Drive, Whispering Pines, 42.1234 N, 73.5678 W", + "contact_number_frequency": "(555) 789-0100 / Med Channel 3", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "State University Burn & Trauma Center", + "address_latitude_longitude": "500 University Ave, Capital City, 42.3500 N, 73.8000 W", + "contact_number_frequency": "(555) 789-0900 / Med Channel 5", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "In the event of a medical emergency or chemical exposure during night operations, responders must immediately notify the Incident Commander and Medical Unit Leader over Command Channel 1. All entry personnel exposed to Benzene must undergo full gross and technical decontamination at the Forward Decon Medical Post before transport. For critical life safety, LifeFlight Air Medical is on standby for immediate evacuation from Landing Zone Alpha located adjacent to Staging Area Alpha.", + "7_prepared_by": { + "name": "Dr. Evelyn Reed", + "position_title": "Medical Unit Leader", + "signature": "Evelyn Reed", + "date_time": "07/07/2026 16:30" + }, + "8_approved_by_safety_officer": { + "name": "Captain Thomas Wright", + "signature": "Thomas Wright", + "date_time": "07/07/2026 17:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_2.json b/benchmark/datasets/ground_truth/ics206_2.json new file mode 100644 index 00000000..b216b947 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_2.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_medical_aid_stations": [ + { + "name": "Blackwood Command Post Medical Station", + "location": "Station 12 Briefing Trailer", + "contact_number_frequency": "453.2125 MHz / Ch 1", + "paramedic_service": true + }, + { + "name": "Hot Zone Emergency Decon Aid Station", + "location": "Pipeline Gate 4 Access Point", + "contact_number_frequency": "458.2125 MHz / Ch 2", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "Blackwood County EMS Unit 10", + "location": "Station 12 Staging Area", + "contact_number_frequency": "(555) 345-6789 / Ch 1", + "paramedic_service": true + }, + { + "name": "Metro West Medic 5", + "location": "Route 104 Checkpoint", + "contact_number_frequency": "(555) 345-6790 / Ch 1", + "paramedic_service": true + } + ], + "air_ambulance_services": [ + { + "name": "AirEvac Response Helicopter 3", + "location": "Blackwood Airport Helipad", + "contact_number_frequency": "(555) 888-1234 / VHF 155.400 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Blackwood Community Hospital", + "address_latitude_longitude": "45 River Road, Blackwood, 39.8765 N, 84.1234 W", + "contact_number_frequency": "(555) 678-1100 / Med Net 1", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "Valley Regional Toxicology & Trauma Center", + "address_latitude_longitude": "1200 Health Park Way, Dayton, 39.7500 N, 84.2000 W", + "contact_number_frequency": "(555) 678-9900 / Med Net 4", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "In the event of an acute respiratory exposure to Anhydrous Ammonia, responders must immediately evacuate the patient upwind to the Hot Zone Emergency Decon Aid Station at Gate 4. Continuous high-flow oxygen administration and eye/skin water irrigation must commence immediately during technical decon. AirEvac Response Helicopter 3 is available at LZ Bravo near Gate 4 for rapid transfer to Valley Regional Toxicology Center.", + "7_prepared_by": { + "name": "Dr. Aris Thorne", + "position_title": "Medical Unit Leader", + "signature": "Aris Thorne", + "date_time": "08/13/2026 16:30" + }, + "8_approved_by_safety_officer": { + "name": "Safety Officer Mark Davis", + "signature": "Mark Davis", + "date_time": "08/13/2026 17:15" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_3.json b/benchmark/datasets/ground_truth/ics206_3.json new file mode 100644 index 00000000..37f29f5d --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_3.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_medical_aid_stations": [ + { + "name": "Berth 42 Decon & Medical Station", + "location": "Berth 42 Dockside Command Post", + "contact_number_frequency": "156.800 MHz / Ch 16", + "paramedic_service": true + }, + { + "name": "M/T Stolt Synergy Deck Medical Post", + "location": "Vessel Starboard Main Deck", + "contact_number_frequency": "467.525 MHz / Ch 3", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "Houston Fire Department Medic 42", + "location": "Port Gate 5 Security Staging", + "contact_number_frequency": "(555) 456-7890 / Ch 1", + "paramedic_service": true + }, + { + "name": "Harris County Emergency Corps Unit 18", + "location": "Channelview Staging Area", + "contact_number_frequency": "(555) 456-7891 / Ch 1", + "paramedic_service": true + } + ], + "air_ambulance_services": [ + { + "name": "Memorial Hermann Life Flight", + "location": "Houston Medical Center Helipad", + "contact_number_frequency": "(555) 777-9111 / VHF 155.280 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Houston Methodist Hospital Baytown", + "address_latitude_longitude": "4401 Garth Road, Baytown, 29.7355 N, 94.9774 W", + "contact_number_frequency": "(555) 890-2200 / Marine Channel 22", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "Memorial Hermann Texas Medical Center", + "address_latitude_longitude": "6411 Fannin St, Houston, 29.7108 N, 95.3986 W", + "contact_number_frequency": "(555) 890-9900 / Marine Channel 24", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "Given high ambient heat and humidity during Level A chemical suit entry, responder entry times are strictly capped at 20 minutes followed by 40 minutes active cooling and hydration. In the event of chemical acid splashes or suit breaches, perform instant water deluge at the Berth 42 Decon Station for a minimum of 15 minutes. Memorial Hermann Life Flight is designated for critical chemical burn transport from Berth 42 Pier Helipad.", + "7_prepared_by": { + "name": "LT Commander James Miller", + "position_title": "Medical Unit Leader", + "signature": "James Miller", + "date_time": "10/14/2026 06:00" + }, + "8_approved_by_safety_officer": { + "name": "Safety Officer Sarah Jenkins", + "signature": "Sarah Jenkins", + "date_time": "10/14/2026 06:30" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_4.json b/benchmark/datasets/ground_truth/ics206_4.json new file mode 100644 index 00000000..06b4103c --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_4.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_medical_aid_stations": [ + { + "name": "Staging Area Charlie Medical Station", + "location": "Skykomish School Gym Briefing Room", + "contact_number_frequency": "461.2250 MHz / Ch 1", + "paramedic_service": true + }, + { + "name": "Warm Decon Hydration & Medical Aid Station", + "location": "State Route 2 Derailment Overlook", + "contact_number_frequency": "466.2250 MHz / Ch 2", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "Cascade Regional EMS Medic 3", + "location": "Staging Area Charlie", + "contact_number_frequency": "(555) 567-8901 / Ch 1", + "paramedic_service": true + }, + { + "name": "Skykomish Volunteer Fire Department Ambulance 8", + "location": "Skykomish Fire Station", + "contact_number_frequency": "(555) 567-8902 / Ch 1", + "paramedic_service": false + } + ], + "air_ambulance_services": [ + { + "name": "Airlift Northwest Helicopter 1", + "location": "Arlington Regional Helipad", + "contact_number_frequency": "(555) 666-5432 / VHF 155.355 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Everett General Hospital", + "address_latitude_longitude": "1330 Colby Ave, Everett, 47.9789 N, 122.2012 W", + "contact_number_frequency": "(555) 901-3300 / State EMS Ch 4", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "Harborview Medical Center", + "address_latitude_longitude": "325 9th Ave, Seattle, 47.6042 N, 122.3242 W", + "contact_number_frequency": "(555) 901-9900 / State EMS Ch 8", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "Under freezing night conditions (28°F overnight) and toxic Chlorine threat, heated water decontamination trailers and active warming blankets must be operated continuously at the Warm Decon Station to prevent suit icing and hypothermia. Any exposure to Chlorine vapor requires immediate inhalation treatment with humidified oxygen and urgent transport to Harborview Medical Center. Airlift Northwest Helicopter 1 is positioned at Skykomish High School Football Field LZ.", + "7_prepared_by": { + "name": "Captain David Miller", + "position_title": "Medical Unit Leader", + "signature": "David Miller", + "date_time": "11/02/2026 16:30" + }, + "8_approved_by_safety_officer": { + "name": "Safety Officer Carl Stevens", + "signature": "Carl Stevens", + "date_time": "11/02/2026 17:15" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_5.json b/benchmark/datasets/ground_truth/ics206_5.json new file mode 100644 index 00000000..94c6c702 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_5.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_medical_aid_stations": [ + { + "name": "ICP Main Medical Station", + "location": "Hesperia Fire Station 30", + "contact_number_frequency": "460.1250 MHz / Ch 1", + "paramedic_service": true + }, + { + "name": "Sawpit Canyon Water Rescue & Medical Aid Station", + "location": "Sawpit Canyon Boat Launch", + "contact_number_frequency": "465.1250 MHz / Ch 2", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "San Bernardino County Fire Medic 30", + "location": "Hesperia Fire Station 30", + "contact_number_frequency": "(555) 678-9012 / Ch 1", + "paramedic_service": true + }, + { + "name": "Desert Ambulance Unit 14", + "location": "Silverwood Lake Main Entrance", + "contact_number_frequency": "(555) 678-9013 / Ch 1", + "paramedic_service": false + } + ], + "air_ambulance_services": [ + { + "name": "Mercy Air Helicopter 2", + "location": "Victorville Regional Airport Helipad", + "contact_number_frequency": "(555) 444-8765 / VHF 155.340 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Desert Valley Hospital", + "address_latitude_longitude": "16850 Bear Valley Rd, Victorville, 34.4712 N, 117.2954 W", + "contact_number_frequency": "(555) 012-4400 / County Med Net 2", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "Loma Linda University Medical Center", + "address_latitude_longitude": "11234 Anderson St, Loma Linda, 34.0489 N, 117.2641 W", + "contact_number_frequency": "(555) 012-9900 / County Med Net 9", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "Night operations on water present dual risks of hydrocarbon skin absorption/ingestion and water submersion. All personnel operating on skimmer vessels or boom lines must wear USCG-approved PFDs and safety tether lines. In case of accidental water entry or oil ingestion, immediately transport the patient to Sawpit Canyon Water Rescue Medical Aid Station for emergency decon and airway management. Mercy Air Helicopter 2 is on standby at Hesperia Fire Station 30 Helipad.", + "7_prepared_by": { + "name": "Dr. Lisa Martinez", + "position_title": "Medical Unit Leader", + "signature": "Lisa Martinez", + "date_time": "05/18/2026 16:30" + }, + "8_approved_by_safety_officer": { + "name": "Safety Officer Frank Owens", + "signature": "Frank Owens", + "date_time": "05/18/2026 17:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics213_1.json b/benchmark/datasets/ground_truth/ics213_1.json new file mode 100644 index 00000000..0a0836f1 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_1.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_to": { + "name": "Marcus Vance", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Commander Eric Sterling", + "position": "Operations Section Chief" + }, + "4_subject": "Request for Additional Vapor Suppression Foam and Boom Resources", + "5_date": "07/07/2026", + "6_time": "19:15", + "7_message": "Due to increased ambient vapor concentrations of Benzene detected downwind along Whispering Pines Creek, Operations urgently requests five additional totes (1,250 gallons) of fluoroprotein vapor suppression foam and 500 feet of sorbent boom to reinforce Checkpoint Bravo before 22:00 hours.", + "8_approved_by": { + "name": "Chief J. Thomas", + "position_title": "Incident Commander", + "signature": "Chief J. Thomas" + }, + "9_reply": "Supply Unit has authorized immediate dispatch of five totes of foam and 500 feet of sorbent boom from Regional Logistics Depot. ETA to Staging Area Alpha is 21:00 hours.", + "10_replied_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 19:45" + } + } +} diff --git a/benchmark/datasets/ground_truth/ics213_2.json b/benchmark/datasets/ground_truth/ics213_2.json new file mode 100644 index 00000000..f8d59ec3 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_2.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_to": { + "name": "Sarah L. Jenkins", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Division Supervisor Alan Cole", + "position": "Division A Supervisor" + }, + "4_subject": "Downstream Municipal Water Intake Precautionary Shutoff Notice", + "5_date": "08/13/2026", + "6_time": "19:30", + "7_message": "Field water monitoring team at Boom Site 2 reports low-level dissolved ammonia readings reaching 0.05 ppm near Blackwood River mile 14. Recommend immediately notifying Blackwood Water Authority to initiate precautionary intake gate closure.", + "8_approved_by": { + "name": "Chief R. Henderson", + "position_title": "Incident Commander", + "signature": "Chief R. Henderson" + }, + "9_reply": "Liaison Officer contacted Blackwood Water Authority at 19:50 hours. Water intake gates closed at 20:00 hours. Alternate reservoir supply activated.", + "10_replied_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 20:10" + } + } +} diff --git a/benchmark/datasets/ground_truth/ics213_3.json b/benchmark/datasets/ground_truth/ics213_3.json new file mode 100644 index 00000000..27df43b8 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_3.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_to": { + "name": "Commander Richard Croft", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Hazmat Group Supervisor Mark Ross", + "position": "Hazmat Group Supervisor" + }, + "4_subject": "Authorization for Sodium Bicarbonate Neutralization Slurry Application", + "5_date": "10/14/2026", + "6_time": "08:45", + "7_message": "Hazmat entry team has contained deck pooling at Berth 42. Request formal authorization from UC to apply 10 tons of dry sodium bicarbonate slurry to deck pooling to neutralize concentrated sulfuric acid prior to washdown.", + "8_approved_by": { + "name": "Captain H. Vance", + "position_title": "Incident Commander", + "signature": "Captain H. Vance" + }, + "9_reply": "Unified Command approves application of 10 tons sodium bicarbonate slurry. Technical Specialists from TCEQ are monitoring runoff pH at Berth 42 outfall.", + "10_replied_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 09:15" + } + } +} diff --git a/benchmark/datasets/ground_truth/ics213_4.json b/benchmark/datasets/ground_truth/ics213_4.json new file mode 100644 index 00000000..829659e5 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_4.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_to": { + "name": "Lt. Colonel Alan Vance", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Rail Tactical Specialist George Miller", + "position": "Technical Specialist" + }, + "4_subject": "Urgent Transport Request for Emergency B-Kit Capping Assembly", + "5_date": "11/02/2026", + "6_time": "19:00", + "7_message": "Capping team at car BNSF-77402 requires specialized torque wrench set and secondary seal gaskets for Emergency B-Kit capping assembly. Request immediate dispatch from Staging Area Charlie via all-terrain vehicle.", + "8_approved_by": { + "name": "Captain E. Miller", + "position_title": "Incident Commander", + "signature": "Captain E. Miller" + }, + "9_reply": "Ground Support Unit dispatched ATV-02 with requested torque wrench set and B-Kit gaskets at 19:20 hours. ETA to railcar site is 19:45 hours.", + "10_replied_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 19:30" + } + } +} diff --git a/benchmark/datasets/ground_truth/ics213_5.json b/benchmark/datasets/ground_truth/ics213_5.json new file mode 100644 index 00000000..fe5be9a3 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_5.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_to": { + "name": "Rachel Brooks", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Skimmer Operations Leader Frank Gomez", + "position": "Operations Group Supervisor" + }, + "4_subject": "Skimmer Vessel Relocation and Sorbent Boom Realignment", + "5_date": "05/18/2026", + "6_time": "20:00", + "7_message": "Due to shifts in surface currents toward Sawpit Canyon inlet, requesting authorization to reposition Skimmer Vessel RV-LC-01 to South Arm and deploy an additional 500 feet of hard containment boom by 22:00 hours.", + "8_approved_by": { + "name": "Chief M. Thorne", + "position_title": "Incident Commander", + "signature": "Chief M. Thorne" + }, + "9_reply": "Operations Chief approves relocation of RV-LC-01 and deployment of 500 feet hard boom. Work Boat 3 assigned to assist boom rigging.", + "10_replied_by": { + "name": "Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 20:30" + } + } +} diff --git a/benchmark/datasets/narratives/ics206_1.txt b/benchmark/datasets/narratives/ics206_1.txt new file mode 100644 index 00000000..7973f942 --- /dev/null +++ b/benchmark/datasets/narratives/ics206_1.txt @@ -0,0 +1,9 @@ +This Medical Plan applies to the Whispering Pines Derailment operational period starting 07/07/2026 at 18:00 and concluding 07/08/2026 at 06:00. Medical care and responder health monitoring are established across two primary medical aid stations. The Staging Area Alpha First Aid Station is located at Whispering Pines Creek Staging Area, maintaining contact via 462.5500 MHz / Ch 1, staffed with full paramedic service. The Forward Decon Medical Post is stationed at Derailment Site Perimeter West, communicating via 467.7750 MHz / Ch 2, also providing full paramedic service. + +Ground ambulance transportation services are provided by County Emergency Medical Services Unit 41, located at Whispering Pines Staging Area, reachable at (555) 234-8901 / Ch 1 with paramedic service available. Additionally, Pine Valley Volunteer Fire & Rescue Ambulance 12 is stationed at Station 12 Base, reachable at (555) 234-8902 / Ch 1, providing basic transport without paramedic service. Air ambulance transportation is supported by LifeFlight Air Medical Helicopter, stationed at Regional Trauma Center Helipad, reachable at (555) 999-4321 / VHF 155.340 MHz, with full paramedic service on board. + +Two regional hospital facilities are designated for medical receiving. Whispering Pines Regional Medical Center, located at 100 Medical Center Drive, Whispering Pines, 42.1234 N, 73.5678 W, can be contacted at (555) 789-0100 / Med Channel 3; this facility features an air ambulance helipad, is not a burn center, but is a certified trauma center. State University Burn & Trauma Center, located at 500 University Ave, Capital City, 42.3500 N, 73.8000 W, can be contacted at (555) 789-0900 / Med Channel 5; this facility features an air ambulance helipad, is a designated burn center, and is a certified trauma center. + +Special medical emergency procedures dictate that in the event of a medical emergency or chemical exposure during night operations, responders must immediately notify the Incident Commander and Medical Unit Leader over Command Channel 1. All entry personnel exposed to Benzene must undergo full gross and technical decontamination at the Forward Decon Medical Post before transport. For critical life safety, LifeFlight Air Medical is on standby for immediate evacuation from Landing Zone Alpha located adjacent to Staging Area Alpha. + +This document was prepared by Dr. Evelyn Reed, Medical Unit Leader, signed Evelyn Reed on 07/07/2026 16:30, and approved by Safety Officer Captain Thomas Wright, signed Thomas Wright on 07/07/2026 17:00. This form is assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics206_2.txt b/benchmark/datasets/narratives/ics206_2.txt new file mode 100644 index 00000000..c26d63f2 --- /dev/null +++ b/benchmark/datasets/narratives/ics206_2.txt @@ -0,0 +1,9 @@ +This Medical Plan governs the Blackwood Chemical Pipeline Breach operational period starting 08/13/2026 at 18:00 and ending 08/14/2026 at 06:00. Emergency health oversight and responder medical monitoring are supported by two designated medical aid stations. The Blackwood Command Post Medical Station is located inside Station 12 Briefing Trailer, communicating over 453.2125 MHz / Ch 1, fully equipped with paramedic service. The Hot Zone Emergency Decon Aid Station is located at Pipeline Gate 4 Access Point, communicating over 458.2125 MHz / Ch 2, also providing paramedic service. + +Ground ambulance transportation services are operated by Blackwood County EMS Unit 10, stationed at Station 12 Staging Area, accessible via (555) 345-6789 / Ch 1 with paramedic service. Metro West Medic 5 is located at Route 104 Checkpoint, accessible via (555) 345-6790 / Ch 1, providing paramedic service. Air ambulance support is provided by AirEvac Response Helicopter 3, located at Blackwood Airport Helipad, reachable at (555) 888-1234 / VHF 155.400 MHz, staffed with paramedic service. + +Medical receiving facilities include two regional medical centers. Blackwood Community Hospital, located at 45 River Road, Blackwood, 39.8765 N, 84.1234 W, can be reached at (555) 678-1100 / Med Net 1; it features an air ambulance helipad, is not a burn center, but operates as a certified trauma center. Valley Regional Toxicology & Trauma Center, located at 1200 Health Park Way, Dayton, 39.7500 N, 84.2000 W, can be reached at (555) 678-9900 / Med Net 4; it features an air ambulance helipad, operates a specialized burn center, and is a certified trauma center. + +Special medical emergency procedures mandate that in the event of an acute respiratory exposure to Anhydrous Ammonia, responders must immediately evacuate the patient upwind to the Hot Zone Emergency Decon Aid Station at Gate 4. Continuous high-flow oxygen administration and eye/skin water irrigation must commence immediately during technical decon. AirEvac Response Helicopter 3 is available at LZ Bravo near Gate 4 for rapid transfer to Valley Regional Toxicology Center. + +Prepared by Dr. Aris Thorne, Medical Unit Leader, signed Aris Thorne on 08/13/2026 16:30, and approved by Safety Officer Mark Davis, signed Mark Davis on 08/13/2026 17:15. Form assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics206_3.txt b/benchmark/datasets/narratives/ics206_3.txt new file mode 100644 index 00000000..d94908d3 --- /dev/null +++ b/benchmark/datasets/narratives/ics206_3.txt @@ -0,0 +1,9 @@ +This Medical Plan governs the Port of Houston Sulfuric Acid Tanker Leak operational period starting 10/14/2026 at 07:00 and concluding 10/15/2026 at 19:00. On-site responder health evaluation and emergency care are administered by two primary medical aid stations. The Berth 42 Decon & Medical Station is located at Berth 42 Dockside Command Post, communicating over 156.800 MHz / Ch 16, offering full paramedic service. The M/T Stolt Synergy Deck Medical Post is located at Vessel Starboard Main Deck, communicating over 467.525 MHz / Ch 3, also staffed with paramedic service. + +Ground ambulance transportation services are provided by Houston Fire Department Medic 42, stationed at Port Gate 5 Security Staging, reachable at (555) 456-7890 / Ch 1 with paramedic service. Harris County Emergency Corps Unit 18 is located at Channelview Staging Area, reachable at (555) 456-7891 / Ch 1, providing paramedic service. Air ambulance transport is supported by Memorial Hermann Life Flight, located at Houston Medical Center Helipad, accessible at (555) 777-9111 / VHF 155.280 MHz, offering advanced paramedic service. + +Two specialized medical receiving facilities are designated for transport. Houston Methodist Hospital Baytown, located at 4401 Garth Road, Baytown, 29.7355 N, 94.9774 W, can be contacted at (555) 890-2200 / Marine Channel 22; this facility has an air ambulance helipad, is not a burn center, but is a certified trauma center. Memorial Hermann Texas Medical Center, located at 6411 Fannin St, Houston, 29.7108 N, 95.3986 W, can be contacted at (555) 890-9900 / Marine Channel 24; this facility features an air ambulance helipad, operates a specialized burn center, and is a certified trauma center. + +Special medical emergency procedures dictate that given high ambient heat and humidity during Level A chemical suit entry, responder entry times are strictly capped at 20 minutes followed by 40 minutes active cooling and hydration. In the event of chemical acid splashes or suit breaches, perform instant water deluge at the Berth 42 Decon Station for a minimum of 15 minutes. Memorial Hermann Life Flight is designated for critical chemical burn transport from Berth 42 Pier Helipad. + +Prepared by LT Commander James Miller, Medical Unit Leader, signed James Miller on 10/14/2026 06:00, and approved by Safety Officer Sarah Jenkins, signed Sarah Jenkins on 10/14/2026 06:30. Assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics206_4.txt b/benchmark/datasets/narratives/ics206_4.txt new file mode 100644 index 00000000..81da8106 --- /dev/null +++ b/benchmark/datasets/narratives/ics206_4.txt @@ -0,0 +1,9 @@ +This Medical Plan applies to the Cascade Pass Chlorine Railcar Derailment operational period starting 11/02/2026 at 18:00 and concluding 11/03/2026 at 06:00. On-scene emergency medical support and responder health protection are maintained by two medical aid stations. The Staging Area Charlie Medical Station is located at Skykomish School Gym Briefing Room, communicating on 461.2250 MHz / Ch 1, providing full paramedic service. The Warm Decon Hydration & Medical Aid Station is located at State Route 2 Derailment Overlook, communicating on 466.2250 MHz / Ch 2, also providing paramedic service. + +Ground ambulance transportation is supplied by Cascade Regional EMS Medic 3, located at Staging Area Charlie, reachable via (555) 567-8901 / Ch 1 with paramedic service. Skykomish Volunteer Fire Department Ambulance 8 is located at Skykomish Fire Station, reachable via (555) 567-8902 / Ch 1, providing transport without paramedic service. Air ambulance transport is assigned to Airlift Northwest Helicopter 1, stationed at Arlington Regional Helipad, reachable at (555) 666-5432 / VHF 155.355 MHz, equipped with paramedic service. + +Medical care facilities receiving incident transport include two regional centers. Everett General Hospital, located at 1330 Colby Ave, Everett, 47.9789 N, 122.2012 W, can be contacted at (555) 901-3300 / State EMS Ch 4; this hospital has an air ambulance helipad, is not a burn center, but is a certified trauma center. Harborview Medical Center, located at 325 9th Ave, Seattle, 47.6042 N, 122.3242 W, can be contacted at (555) 901-9900 / State EMS Ch 8; this hospital features an air ambulance helipad, operates a specialized burn center, and is a certified trauma center. + +Special medical emergency procedures mandate that under freezing night conditions (28°F overnight) and toxic Chlorine threat, heated water decontamination trailers and active warming blankets must be operated continuously at the Warm Decon Station to prevent suit icing and hypothermia. Any exposure to Chlorine vapor requires immediate inhalation treatment with humidified oxygen and urgent transport to Harborview Medical Center. Airlift Northwest Helicopter 1 is positioned at Skykomish High School Football Field LZ. + +Prepared by Captain David Miller, Medical Unit Leader, signed David Miller on 11/02/2026 16:30, and approved by Safety Officer Carl Stevens, signed Carl Stevens on 11/02/2026 17:15. Form assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics206_5.txt b/benchmark/datasets/narratives/ics206_5.txt new file mode 100644 index 00000000..07e14fca --- /dev/null +++ b/benchmark/datasets/narratives/ics206_5.txt @@ -0,0 +1,9 @@ +This Medical Plan governs the Silverwood Reservoir Crude Oil Pipeline Rupture operational period starting 05/18/2026 at 18:00 and concluding 05/19/2026 at 06:00. On-scene emergency medical support and responder health protection are maintained by two medical aid stations. The ICP Main Medical Station is located at Hesperia Fire Station 30, communicating on 460.1250 MHz / Ch 1, providing full paramedic service. The Sawpit Canyon Water Rescue & Medical Aid Station is located at Sawpit Canyon Boat Launch, communicating on 465.1250 MHz / Ch 2, also providing paramedic service. + +Ground ambulance transportation is supplied by San Bernardino County Fire Medic 30, located at Hesperia Fire Station 30, reachable via (555) 678-9012 / Ch 1 with paramedic service. Desert Ambulance Unit 14 is located at Silverwood Lake Main Entrance, reachable via (555) 678-9013 / Ch 1, providing transport without paramedic service. Air ambulance transport is assigned to Mercy Air Helicopter 2, stationed at Victorville Regional Airport Helipad, reachable at (555) 444-8765 / VHF 155.340 MHz, equipped with paramedic service. + +Medical care facilities receiving incident transport include two regional centers. Desert Valley Hospital, located at 16850 Bear Valley Rd, Victorville, 34.4712 N, 117.2954 W, can be contacted at (555) 012-4400 / County Med Net 2; this hospital has an air ambulance helipad, is not a burn center, but is a certified trauma center. Loma Linda University Medical Center, located at 11234 Anderson St, Loma Linda, 34.0489 N, 117.2641 W, can be contacted at (555) 012-9900 / County Med Net 9; this hospital features an air ambulance helipad, operates a specialized burn center, and is a certified trauma center. + +Special medical emergency procedures mandate that night operations on water present dual risks of hydrocarbon skin absorption/ingestion and water submersion. All personnel operating on skimmer vessels or boom lines must wear USCG-approved PFDs and safety tether lines. In case of accidental water entry or oil ingestion, immediately transport the patient to Sawpit Canyon Water Rescue Medical Aid Station for emergency decon and airway management. Mercy Air Helicopter 2 is on standby at Hesperia Fire Station 30 Helipad. + +Prepared by Dr. Lisa Martinez, Medical Unit Leader, signed Lisa Martinez on 05/18/2026 16:30, and approved by Safety Officer Frank Owens, signed Frank Owens on 05/18/2026 17:00. Form assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics213_1.txt b/benchmark/datasets/narratives/ics213_1.txt new file mode 100644 index 00000000..ecf8a133 --- /dev/null +++ b/benchmark/datasets/narratives/ics213_1.txt @@ -0,0 +1,5 @@ +This General Message form applies to the Whispering Pines Derailment incident. The message is addressed to Marcus Vance, Planning Section Chief, sent from Commander Eric Sterling, Operations Section Chief, regarding the subject Request for Additional Vapor Suppression Foam and Boom Resources. The message was recorded on date 07/07/2026 at time 19:15. + +The message reads as follows: Due to increased ambient vapor concentrations of Benzene detected downwind along Whispering Pines Creek, Operations urgently requests five additional totes (1,250 gallons) of fluoroprotein vapor suppression foam and 500 feet of sorbent boom to reinforce Checkpoint Bravo before 22:00 hours. The message was approved by Chief J. Thomas, Incident Commander, signed Chief J. Thomas. + +The formal reply states: Supply Unit has authorized immediate dispatch of five totes of foam and 500 feet of sorbent boom from Regional Logistics Depot. ETA to Staging Area Alpha is 21:00 hours. The reply was submitted by Marcus Vance, Planning Section Chief, signed Marcus Vance on date and time 07/07/2026 19:45. diff --git a/benchmark/datasets/narratives/ics213_2.txt b/benchmark/datasets/narratives/ics213_2.txt new file mode 100644 index 00000000..c2041b44 --- /dev/null +++ b/benchmark/datasets/narratives/ics213_2.txt @@ -0,0 +1,5 @@ +This General Message document is prepared for the Blackwood Chemical Pipeline Breach incident. The message is directed to Sarah L. Jenkins, Planning Section Chief, from Division Supervisor Alan Cole, Division A Supervisor, regarding the subject Downstream Municipal Water Intake Precautionary Shutoff Notice. The message was transmitted on date 08/13/2026 at time 19:30. + +The message content specifies: Field water monitoring team at Boom Site 2 reports low-level dissolved ammonia readings reaching 0.05 ppm near Blackwood River mile 14. Recommend immediately notifying Blackwood Water Authority to initiate precautionary intake gate closure. Approval was granted by Chief R. Henderson, Incident Commander, signed Chief R. Henderson. + +The message reply states: Liaison Officer contacted Blackwood Water Authority at 19:50 hours. Water intake gates closed at 20:00 hours. Alternate reservoir supply activated. The reply was signed and completed by Sarah L. Jenkins, Planning Section Chief, signed Sarah L. Jenkins on date and time 08/13/2026 20:10. diff --git a/benchmark/datasets/narratives/ics213_3.txt b/benchmark/datasets/narratives/ics213_3.txt new file mode 100644 index 00000000..d3d788aa --- /dev/null +++ b/benchmark/datasets/narratives/ics213_3.txt @@ -0,0 +1,5 @@ +This General Message covers the Port of Houston Sulfuric Acid Tanker Leak response. The message is sent to Commander Richard Croft, Planning Section Chief, from Hazmat Group Supervisor Mark Ross, Hazmat Group Supervisor, regarding subject Authorization for Sodium Bicarbonate Neutralization Slurry Application. Sent on date 10/14/2026 at time 08:45. + +The message details: Hazmat entry team has contained deck pooling at Berth 42. Request formal authorization from UC to apply 10 tons of dry sodium bicarbonate slurry to deck pooling to neutralize concentrated sulfuric acid prior to washdown. Approved by Captain H. Vance, Incident Commander, signed Captain H. Vance. + +The message reply states: Unified Command approves application of 10 tons sodium bicarbonate slurry. Technical Specialists from TCEQ are monitoring runoff pH at Berth 42 outfall. The response was authorized by Commander Richard Croft, Planning Section Chief, signed Richard Croft on date and time 10/14/2026 09:15. diff --git a/benchmark/datasets/narratives/ics213_4.txt b/benchmark/datasets/narratives/ics213_4.txt new file mode 100644 index 00000000..f2d3237d --- /dev/null +++ b/benchmark/datasets/narratives/ics213_4.txt @@ -0,0 +1,5 @@ +This General Message is filed for the Cascade Pass Chlorine Railcar Derailment incident. Addressed to Lt. Colonel Alan Vance, Planning Section Chief, sent from Rail Tactical Specialist George Miller, Technical Specialist, regarding the subject Urgent Transport Request for Emergency B-Kit Capping Assembly. Sent on date 11/02/2026 at time 19:00. + +The message states: Capping team at car BNSF-77402 requires specialized torque wrench set and secondary seal gaskets for Emergency B-Kit capping assembly. Request immediate dispatch from Staging Area Charlie via all-terrain vehicle. Approved by Captain E. Miller, Incident Commander, signed Captain E. Miller. + +The reply text reads: Ground Support Unit dispatched ATV-02 with requested torque wrench set and B-Kit gaskets at 19:20 hours. ETA to railcar site is 19:45 hours. Signed by Lt. Colonel Alan Vance, Planning Section Chief, signed Alan Vance on date and time 11/02/2026 19:30. diff --git a/benchmark/datasets/narratives/ics213_5.txt b/benchmark/datasets/narratives/ics213_5.txt new file mode 100644 index 00000000..34c0713a --- /dev/null +++ b/benchmark/datasets/narratives/ics213_5.txt @@ -0,0 +1,5 @@ +This General Message document concerns the Silverwood Reservoir Crude Oil Pipeline Rupture incident. The message is sent to Rachel Brooks, Planning Section Chief, from Skimmer Operations Leader Frank Gomez, Operations Group Supervisor, regarding the subject Skimmer Vessel Relocation and Sorbent Boom Realignment. Prepared on date 05/18/2026 at time 20:00. + +The message content reads: Due to shifts in surface currents toward Sawpit Canyon inlet, requesting authorization to reposition Skimmer Vessel RV-LC-01 to South Arm and deploy an additional 500 feet of hard containment boom by 22:00 hours. The message was approved by Chief M. Thorne, Incident Commander, signed Chief M. Thorne. + +The message reply states: Operations Chief approves relocation of RV-LC-01 and deployment of 500 feet hard boom. Work Boat 3 assigned to assist boom rigging. The reply was executed by Rachel Brooks, Planning Section Chief, signed Rachel Brooks on date and time 05/18/2026 20:30. diff --git a/benchmark/datasets/templates/ics_206.json b/benchmark/datasets/templates/ics_206.json new file mode 100644 index 00000000..140242bd --- /dev/null +++ b/benchmark/datasets/templates/ics_206.json @@ -0,0 +1,58 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_medical_aid_stations": [ + { + "name": "string", + "location": "string", + "contact_number_frequency": "string", + "paramedic_service": "boolean" + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "string", + "location": "string", + "contact_number_frequency": "string", + "paramedic_service": "boolean" + } + ], + "air_ambulance_services": [ + { + "name": "string", + "location": "string", + "contact_number_frequency": "string", + "paramedic_service": "boolean" + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "string", + "address_latitude_longitude": "string", + "contact_number_frequency": "string", + "air_ambulance_helipad": "boolean", + "burn_center": "boolean", + "trauma_center": "boolean" + } + ], + "6_special_medical_emergency_procedures": "string", + "7_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "8_approved_by_safety_officer": { + "name": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_213.json b/benchmark/datasets/templates/ics_213.json new file mode 100644 index 00000000..749a77d1 --- /dev/null +++ b/benchmark/datasets/templates/ics_213.json @@ -0,0 +1,27 @@ +{ + "1_incident_name": "string", + "2_to": { + "name": "string", + "position": "string" + }, + "3_from": { + "name": "string", + "position": "string" + }, + "4_subject": "string", + "5_date": "string", + "6_time": "string", + "7_message": "string", + "8_approved_by": { + "name": "string", + "position_title": "string", + "signature": "string" + }, + "9_reply": "string", + "10_replied_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + } +} From 559030d23e80bddf0085c52135e2cf0fd7b6f168 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 17:15:22 +0200 Subject: [PATCH 79/85] ics207 & ics 208 --- benchmark/datasets/ground_truth/ics207_1.json | 81 ++++++++++++++++++ benchmark/datasets/ground_truth/ics207_2.json | 81 ++++++++++++++++++ benchmark/datasets/ground_truth/ics207_3.json | 82 +++++++++++++++++++ benchmark/datasets/ground_truth/ics207_4.json | 82 +++++++++++++++++++ benchmark/datasets/ground_truth/ics207_5.json | 82 +++++++++++++++++++ benchmark/datasets/ground_truth/ics208_1.json | 21 +++++ benchmark/datasets/ground_truth/ics208_2.json | 21 +++++ benchmark/datasets/ground_truth/ics208_3.json | 21 +++++ benchmark/datasets/ground_truth/ics208_4.json | 21 +++++ benchmark/datasets/ground_truth/ics208_5.json | 21 +++++ benchmark/datasets/narratives/ics207_1.txt | 11 +++ benchmark/datasets/narratives/ics207_2.txt | 11 +++ benchmark/datasets/narratives/ics207_3.txt | 11 +++ benchmark/datasets/narratives/ics207_4.txt | 11 +++ benchmark/datasets/narratives/ics207_5.txt | 11 +++ benchmark/datasets/narratives/ics208_1.txt | 7 ++ benchmark/datasets/narratives/ics208_2.txt | 7 ++ benchmark/datasets/narratives/ics208_3.txt | 7 ++ benchmark/datasets/narratives/ics208_4.txt | 7 ++ benchmark/datasets/narratives/ics208_5.txt | 7 ++ benchmark/datasets/templates/ics_207.json | 63 ++++++++++++++ benchmark/datasets/templates/ics_208.json | 19 +++++ 22 files changed, 685 insertions(+) create mode 100644 benchmark/datasets/ground_truth/ics207_1.json create mode 100644 benchmark/datasets/ground_truth/ics207_2.json create mode 100644 benchmark/datasets/ground_truth/ics207_3.json create mode 100644 benchmark/datasets/ground_truth/ics207_4.json create mode 100644 benchmark/datasets/ground_truth/ics207_5.json create mode 100644 benchmark/datasets/ground_truth/ics208_1.json create mode 100644 benchmark/datasets/ground_truth/ics208_2.json create mode 100644 benchmark/datasets/ground_truth/ics208_3.json create mode 100644 benchmark/datasets/ground_truth/ics208_4.json create mode 100644 benchmark/datasets/ground_truth/ics208_5.json create mode 100644 benchmark/datasets/narratives/ics207_1.txt create mode 100644 benchmark/datasets/narratives/ics207_2.txt create mode 100644 benchmark/datasets/narratives/ics207_3.txt create mode 100644 benchmark/datasets/narratives/ics207_4.txt create mode 100644 benchmark/datasets/narratives/ics207_5.txt create mode 100644 benchmark/datasets/narratives/ics208_1.txt create mode 100644 benchmark/datasets/narratives/ics208_2.txt create mode 100644 benchmark/datasets/narratives/ics208_3.txt create mode 100644 benchmark/datasets/narratives/ics208_4.txt create mode 100644 benchmark/datasets/narratives/ics208_5.txt create mode 100644 benchmark/datasets/templates/ics_207.json create mode 100644 benchmark/datasets/templates/ics_208.json diff --git a/benchmark/datasets/ground_truth/ics207_1.json b/benchmark/datasets/ground_truth/ics207_1.json new file mode 100644 index 00000000..2fbba43b --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_1.json @@ -0,0 +1,81 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "Chief J. Thomas (County Fire)", + "S. Albright (State DEQ)", + "D. Miller (CSX Railroad RP)" + ], + "command_staff": { + "safety_officer": "Captain R. Mendez", + "public_information_officer": "A. Cho (County OEM)", + "liaison_officer": "Inspector G. Sims (SO)" + }, + "operations_section": { + "chief": "B. Reynolds", + "staging_area_manager": "Staging Area Alpha Manager", + "branches_divisions_groups": [ + { + "title": "Hazardous Materials Branch Director", + "name": "Lt. T. Kincaid" + }, + { + "title": "Hazmat Entry Group Supervisor", + "name": "Capt. P. Gomez" + }, + { + "title": "Decontamination Group Supervisor", + "name": "Lt. S. Baker" + }, + { + "title": "Fire Suppression Branch Director", + "name": "Batt. Chief E. Walters" + } + ] + }, + "planning_section": { + "chief": "Marcus Vance", + "resources_unit_ldr": "T. Jenkins", + "situation_unit_ldr": "E. Brooks", + "documentation_unit_ldr": "H. Martinez", + "demobilization_unit_ldr": "R. Sterling" + }, + "logistics_section": { + "chief": "K. Dunavan", + "support_branch": { + "director": "J. Myers", + "supply_unit_ldr": "S. Taylor", + "facilities_unit_ldr": "C. Adams", + "ground_spt_unit_ldr": "M. Evans" + }, + "service_branch": { + "director": "B. Foster", + "comms_unit_ldr": "R. Lee", + "medical_unit_ldr": "Dr. N. Howard", + "food_unit_ldr": "L. King" + } + }, + "finance_administration_section": { + "chief": "Robert Sterling", + "time_unit_ldr": "C. White", + "procurement_unit_ldr": "G. Scott", + "comp_claims_unit_ldr": "E. Green", + "cost_unit_ldr": "W. Harris" + } + }, + "4_prepared_by": { + "name": "T. Jenkins", + "position_title": "Resources Unit Leader", + "signature": "T. Jenkins", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_2.json b/benchmark/datasets/ground_truth/ics207_2.json new file mode 100644 index 00000000..3542b22b --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_2.json @@ -0,0 +1,81 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "Chief R. Vance (Blackwood Fire Dept)", + "Officer M. Ross (State EPA)", + "J. Vance (Blackwood Pipeline Co. RP)" + ], + "command_staff": { + "safety_officer": "Captain L. Hayes", + "public_information_officer": "E. Wright", + "liaison_officer": "Deputy K. Miller" + }, + "operations_section": { + "chief": "D. Kowalski", + "staging_area_manager": "Staging Area Bravo Manager", + "branches_divisions_groups": [ + { + "title": "Pipeline Isolation Branch Director", + "name": "Capt. Donald Kross" + }, + { + "title": "Hot Zone Entry Group Supervisor", + "name": "Lt. R. Mendez" + }, + { + "title": "Vapor Suppression Group Supervisor", + "name": "Capt. A. Ross" + }, + { + "title": "River Spill Containment Branch Director", + "name": "Commander Thomas Blake" + } + ] + }, + "planning_section": { + "chief": "Sarah L. Jenkins", + "resources_unit_ldr": "A. Patel", + "situation_unit_ldr": "C. Webb", + "documentation_unit_ldr": "Dr. H. Thorne", + "demobilization_unit_ldr": "M. Brody" + }, + "logistics_section": { + "chief": "T. Bradley", + "support_branch": { + "director": "G. Kross", + "supply_unit_ldr": "H. Lin", + "facilities_unit_ldr": "R. Sterling", + "ground_spt_unit_ldr": "D. Miller" + }, + "service_branch": { + "director": "S. Norris", + "comms_unit_ldr": "K. Albright", + "medical_unit_ldr": "Dr. T. Vance", + "food_unit_ldr": "A. Cho" + } + }, + "finance_administration_section": { + "chief": "A. Patel", + "time_unit_ldr": "J. Mercer", + "procurement_unit_ldr": "Laura Martinez", + "comp_claims_unit_ldr": "Inspector R. Sterling", + "cost_unit_ldr": "Sandra Keller" + } + }, + "4_prepared_by": { + "name": "A. Patel", + "position_title": "Resources Unit Leader", + "signature": "A. Patel", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_3.json b/benchmark/datasets/ground_truth/ics207_3.json new file mode 100644 index 00000000..8f6442da --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_3.json @@ -0,0 +1,82 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "Captain H. Vance (USCG Unified Command IC)", + "OSC M. Reynolds (EPA Co-IC)", + "Chief D. Garcia (Port Authority Fire IC)", + "K. Lindqvist (Stolt Tankers RP IC)" + ], + "command_staff": { + "safety_officer": "Commander Thomas Blake", + "public_information_officer": "Laura Martinez (Port Authority)", + "liaison_officer": "Inspector R. Sterling (TCEQ)" + }, + "operations_section": { + "chief": "Battalion Chief A. Ross", + "staging_area_manager": "Staging Area Bravo Manager", + "branches_divisions_groups": [ + { + "title": "Vessel Salvage & Entry Branch Director", + "name": "Lt. J. Thorne" + }, + { + "title": "Deck Neutralization Group Supervisor", + "name": "Lt. S. Baker" + }, + { + "title": "Manifold Isolation Team Supervisor", + "name": "Capt. D. Ross" + }, + { + "title": "Ship Channel Protection Branch Director", + "name": "Capt. Donald Kross" + } + ] + }, + "planning_section": { + "chief": "Commander Richard Croft", + "resources_unit_ldr": "Dr. V. Patel", + "situation_unit_ldr": "Sandra Keller", + "documentation_unit_ldr": "Wayne Miller", + "demobilization_unit_ldr": "Battalion Chief A. Ross" + }, + "logistics_section": { + "chief": "Sandra Keller", + "support_branch": { + "director": "Capt. H. Nelson", + "supply_unit_ldr": "C. White", + "facilities_unit_ldr": "G. Scott", + "ground_spt_unit_ldr": "E. Green" + }, + "service_branch": { + "director": "W. Harris", + "comms_unit_ldr": "J. Myers", + "medical_unit_ldr": "Dr. A. Chen", + "food_unit_ldr": "S. Taylor" + } + }, + "finance_administration_section": { + "chief": "Wayne Miller", + "time_unit_ldr": "C. Adams", + "procurement_unit_ldr": "M. Evans", + "comp_claims_unit_ldr": "B. Foster", + "cost_unit_ldr": "R. Lee" + } + }, + "4_prepared_by": { + "name": "Dr. V. Patel", + "position_title": "Resources Unit Leader", + "signature": "Dr. V. Patel", + "date_time": "10/14/2026 06:00" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_4.json b/benchmark/datasets/ground_truth/ics207_4.json new file mode 100644 index 00000000..89685ecf --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_4.json @@ -0,0 +1,82 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "Captain E. Miller (WSP Unified Command IC)", + "OSC R. Brooks (EPA R10 Co-IC)", + "Chief D. Olson (Skykomish Fire IC)", + "M. Campbell (BNSF Railway RP IC)" + ], + "command_staff": { + "safety_officer": "Captain Marcus Vance", + "public_information_officer": "Jennifer Hayes (WSDOT)", + "liaison_officer": "Deputy S. Kowalski (KCSO)" + }, + "operations_section": { + "chief": "Battalion Chief T. Higgins", + "staging_area_manager": "Staging Area Charlie Manager", + "branches_divisions_groups": [ + { + "title": "Hazmat Capping Branch Director", + "name": "Capt. P. Gomez" + }, + { + "title": "Railcar Entry Group 1 Supervisor", + "name": "Capt. D. Ross" + }, + { + "title": "Decontamination Group Supervisor", + "name": "Lt. M. Turner" + }, + { + "title": "Evacuation & Security Branch Director", + "name": "Sgt. H. Lin" + } + ] + }, + "planning_section": { + "chief": "Lt. Colonel Alan Vance", + "resources_unit_ldr": "Patricia Ross", + "situation_unit_ldr": "Battalion Chief T. Higgins", + "documentation_unit_ldr": "Sgt. H. Lin", + "demobilization_unit_ldr": "G. Peterson" + }, + "logistics_section": { + "chief": "David Sterling", + "support_branch": { + "director": "L. King", + "supply_unit_ldr": "J. Myers", + "facilities_unit_ldr": "S. Taylor", + "ground_spt_unit_ldr": "C. Adams" + }, + "service_branch": { + "director": "M. Evans", + "comms_unit_ldr": "B. Foster", + "medical_unit_ldr": "Dr. N. Howard", + "food_unit_ldr": "R. Lee" + } + }, + "finance_administration_section": { + "chief": "Patricia Ross", + "time_unit_ldr": "W. Harris", + "procurement_unit_ldr": "C. White", + "comp_claims_unit_ldr": "G. Scott", + "cost_unit_ldr": "E. Green" + } + }, + "4_prepared_by": { + "name": "Patricia Ross", + "position_title": "Resources Unit Leader", + "signature": "Patricia Ross", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_5.json b/benchmark/datasets/ground_truth/ics207_5.json new file mode 100644 index 00000000..6c0b7119 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_5.json @@ -0,0 +1,82 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "OSC C. Martinez (EPA R9 Co-IC)", + "Chief M. Thorne (SBCo Fire IC)", + "Chief Inspector A. Kim (Cal OES)", + "W. Vance (Pipeline RP IC)" + ], + "command_staff": { + "safety_officer": "Captain Gregory Hall", + "public_information_officer": "Samantha Norris (Cal OES)", + "liaison_officer": "Ranger D. Stevens (State Parks)" + }, + "operations_section": { + "chief": "Battalion Chief Kevin Ross", + "staging_area_manager": "Staging Area Delta Manager", + "branches_divisions_groups": [ + { + "title": "Canyon Pipeline Repair Branch Director", + "name": "Capt. Gregory Hall" + }, + { + "title": "Clamp Repair Group Supervisor", + "name": "Capt. A. Ross" + }, + { + "title": "Reservoir Skimming & Booming Branch Director", + "name": "Captain B. Walsh" + }, + { + "title": "South Arm Skimmer Division Supervisor", + "name": "Capt. B. Walsh" + } + ] + }, + "planning_section": { + "chief": "Captain Rachel Brooks", + "resources_unit_ldr": "Jason Wu", + "situation_unit_ldr": "Battalion Chief Kevin Ross", + "documentation_unit_ldr": "Dr. L. Arispe", + "demobilization_unit_ldr": "Captain B. Walsh" + }, + "logistics_section": { + "chief": "Megan Taylor", + "support_branch": { + "director": "Capt. P. Gomez", + "supply_unit_ldr": "Lt. S. Baker", + "facilities_unit_ldr": "Capt. D. Ross", + "ground_spt_unit_ldr": "Lt. M. Turner" + }, + "service_branch": { + "director": "Capt. H. Nelson", + "comms_unit_ldr": "Sgt. R. Hall", + "medical_unit_ldr": "Dr. N. Howard", + "food_unit_ldr": "L. King" + } + }, + "finance_administration_section": { + "chief": "Jason Wu", + "time_unit_ldr": "J. Myers", + "procurement_unit_ldr": "S. Taylor", + "comp_claims_unit_ldr": "C. Adams", + "cost_unit_ldr": "M. Evans" + } + }, + "4_prepared_by": { + "name": "Jason Wu", + "position_title": "Resources Unit Leader", + "signature": "Jason Wu", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_1.json b/benchmark/datasets/ground_truth/ics208_1.json new file mode 100644 index 00000000..8ad9a4ca --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_1.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap Benzene vapors closer to the ground. Mandate Level B SCBA PPE within the 300-foot exclusion zone. Mandatory buddy system and continuous photoionization detector air monitoring required for all entry crews along creek banks.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "Mobile Command Post inside the Forward Operations Briefing Trailer", + "5_prepared_by": { + "name": "Captain R. Mendez", + "position_title": "Safety Officer", + "signature": "R. Mendez", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_2.json b/benchmark/datasets/ground_truth/ics208_2.json new file mode 100644 index 00000000..82b4e974 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_2.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Focus tactical operations on maintaining river boom integrity under rising night tides and ensuring strict atmospheric air monitoring during the overnight temperature drop when Anhydrous Ammonia vapors hug low ground. SCBA Level B PPE is mandatory within the 500-foot exclusion zone. Vigilance required for reduced visibility, riverbank slip hazards, and toxic vapor pockets.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "Mobile Command Post Briefing Trailer at Station 12", + "5_prepared_by": { + "name": "Captain L. Hayes", + "position_title": "Safety Officer", + "signature": "L. Hayes", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_3.json b/benchmark/datasets/ground_truth/ics208_3.json new file mode 100644 index 00000000..41aa8307 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_3.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Primary tactical focus is placed on safe chemical neutralization and pH stabilization in the ship channel water column. Mandatory Level A chemical suit entry procedures enforced for all vessel deck operations. Due to extreme daytime heat (88°F, 78% humidity), entry work cycles are capped at 20 minutes active entry followed by 40 minutes active hydration and cooling.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "USCG Sector Houston Command Center Safety Office", + "5_prepared_by": { + "name": "Commander Thomas Blake", + "position_title": "Safety Officer", + "signature": "Thomas Blake", + "date_time": "10/14/2026 06:00" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_4.json b/benchmark/datasets/ground_truth/ics208_4.json new file mode 100644 index 00000000..c8d4f68b --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_4.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Command emphasizes absolute safety of capping entry teams working under nighttime freezing conditions (28°F overnight). Enforce mandatory Level A PPE compliance and buddy system protocols. Heated decontamination water and warm hydration must be maintained continuously to prevent suit icing and hypothermia.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "Skykomish School Gym Operations Briefing Room", + "5_prepared_by": { + "name": "Captain Marcus Vance", + "position_title": "Safety Officer", + "signature": "Marcus Vance", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_5.json b/benchmark/datasets/ground_truth/ics208_5.json new file mode 100644 index 00000000..90bb9bad --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_5.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Focus tactical priority on preventing any oil migration toward the municipal water intake. Night skimming operations must maintain illuminated safety perimeters and 100% life-vest compliance at all times. High vigilance required for steep, oil-covered riprap banks and wildlife hazards.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "Incident Command Post at Hesperia Fire Station 30", + "5_prepared_by": { + "name": "Captain Gregory Hall", + "position_title": "Safety Officer", + "signature": "Gregory Hall", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/narratives/ics207_1.txt b/benchmark/datasets/narratives/ics207_1.txt new file mode 100644 index 00000000..86cb317e --- /dev/null +++ b/benchmark/datasets/narratives/ics207_1.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart briefing applies to the Whispering Pines Derailment operational period starting 07/07/2026 at 18:00 and concluding 07/08/2026 at 06:00. Unified Command leadership is held by Incident Commanders Chief J. Thomas (County Fire), S. Albright (State DEQ), and D. Miller (CSX Railroad RP). The Command Staff consists of Safety Officer Captain R. Mendez, Public Information Officer A. Cho (County OEM), and Liaison Officer Inspector G. Sims (SO). + +The Operations Section is directed by Operations Section Chief B. Reynolds, with Staging Area Alpha Manager stationed at the County Fairgrounds. Operational elements under Operations include Hazardous Materials Branch Director Lt. T. Kincaid, Hazmat Entry Group Supervisor Capt. P. Gomez, Decontamination Group Supervisor Lt. S. Baker, and Fire Suppression Branch Director Batt. Chief E. Walters. + +The Planning Section is led by Planning Section Chief Marcus Vance, supported by Resources Unit Leader T. Jenkins, Situation Unit Leader E. Brooks, Documentation Unit Leader H. Martinez, and Demobilization Unit Leader R. Sterling. + +The Logistics Section is headed by Logistics Section Chief K. Dunavan. Under the Support Branch, managed by Director J. Myers, key units include Supply Unit Leader S. Taylor, Facilities Unit Leader C. Adams, and Ground Support Unit Leader M. Evans. Under the Service Branch, managed by Director B. Foster, unit leads are Communications Unit Leader R. Lee, Medical Unit Leader Dr. N. Howard, and Food Unit Leader L. King. + +The Finance/Administration Section is directed by Finance/Admin Section Chief Robert Sterling, supervising Time Unit Leader C. White, Procurement Unit Leader G. Scott, Comp/Claims Unit Leader E. Green, and Cost Unit Leader W. Harris. + +This organization chart was prepared by T. Jenkins, Resources Unit Leader, signed T. Jenkins on 07/07/2026 16:30. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics207_2.txt b/benchmark/datasets/narratives/ics207_2.txt new file mode 100644 index 00000000..ad79b233 --- /dev/null +++ b/benchmark/datasets/narratives/ics207_2.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart briefing covers the Blackwood Chemical Pipeline Breach operational period starting 08/13/2026 at 18:00 and concluding 08/14/2026 at 06:00. Unified Command leadership is staffed by Incident Commanders Chief R. Vance (Blackwood Fire Dept), Officer M. Ross (State EPA), and J. Vance (Blackwood Pipeline Co. RP). Command Staff includes Safety Officer Captain L. Hayes, Public Information Officer E. Wright, and Liaison Officer Deputy K. Miller. + +The Operations Section is directed by Operations Section Chief D. Kowalski, alongside Staging Area Bravo Manager. Key field leaders under Operations include Pipeline Isolation Branch Director Capt. Donald Kross, Hot Zone Entry Group Supervisor Lt. R. Mendez, Vapor Suppression Group Supervisor Capt. A. Ross, and River Spill Containment Branch Director Commander Thomas Blake. + +The Planning Section is led by Planning Section Chief Sarah L. Jenkins, with Resources Unit Leader A. Patel, Situation Unit Leader C. Webb, Documentation Unit Leader Dr. H. Thorne, and Demobilization Unit Leader M. Brody. + +The Logistics Section is headed by Logistics Section Chief T. Bradley. The Support Branch, managed by Director G. Kross, consists of Supply Unit Leader H. Lin, Facilities Unit Leader R. Sterling, and Ground Support Unit Leader D. Miller. The Service Branch, managed by Director S. Norris, comprises Communications Unit Leader K. Albright, Medical Unit Leader Dr. T. Vance, and Food Unit Leader A. Cho. + +The Finance/Administration Section is directed by Finance/Admin Section Chief A. Patel, supervising Time Unit Leader J. Mercer, Procurement Unit Leader Laura Martinez, Comp/Claims Unit Leader Inspector R. Sterling, and Cost Unit Leader Sandra Keller. + +Prepared by A. Patel, Resources Unit Leader, signed A. Patel on 08/13/2026 16:30. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics207_3.txt b/benchmark/datasets/narratives/ics207_3.txt new file mode 100644 index 00000000..9ef2699b --- /dev/null +++ b/benchmark/datasets/narratives/ics207_3.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart briefing applies to the Port of Houston Sulfuric Acid Tanker Leak operational period starting 10/14/2026 at 07:00 and concluding 10/15/2026 at 19:00. Unified Command leadership comprises Captain H. Vance (USCG Unified Command IC), OSC M. Reynolds (EPA Co-IC), Chief D. Garcia (Port Authority Fire IC), and K. Lindqvist (Stolt Tankers RP IC). The Command Staff consists of Safety Officer Commander Thomas Blake, Public Information Officer Laura Martinez (Port Authority), and Liaison Officer Inspector R. Sterling (TCEQ). + +The Operations Section is directed by Operations Section Chief Battalion Chief A. Ross, supported by Staging Area Bravo Manager at Jacintoport Terminal. Field branch and group leaders include Vessel Salvage & Entry Branch Director Lt. J. Thorne, Deck Neutralization Group Supervisor Lt. S. Baker, Manifold Isolation Team Supervisor Capt. D. Ross, and Ship Channel Protection Branch Director Capt. Donald Kross. + +The Planning Section is led by Planning Section Chief Commander Richard Croft, with Resources Unit Leader Dr. V. Patel, Situation Unit Leader Sandra Keller, Documentation Unit Leader Wayne Miller, and Demobilization Unit Leader Battalion Chief A. Ross. + +The Logistics Section is headed by Logistics Section Chief Sandra Keller. Under Support Branch Director Capt. H. Nelson, units are staffed by Supply Unit Leader C. White, Facilities Unit Leader G. Scott, and Ground Support Unit Leader E. Green. Under Service Branch Director W. Harris, unit leads are Communications Unit Leader J. Myers, Medical Unit Leader Dr. A. Chen, and Food Unit Leader S. Taylor. + +The Finance/Administration Section is directed by Finance/Admin Section Chief Wayne Miller, supervising Time Unit Leader C. Adams, Procurement Unit Leader M. Evans, Comp/Claims Unit Leader B. Foster, and Cost Unit Leader R. Lee. + +Prepared by Dr. V. Patel, Resources Unit Leader, signed Dr. V. Patel on 10/14/2026 06:00. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics207_4.txt b/benchmark/datasets/narratives/ics207_4.txt new file mode 100644 index 00000000..8262afe6 --- /dev/null +++ b/benchmark/datasets/narratives/ics207_4.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart applies to the Cascade Pass Chlorine Railcar Derailment operational period starting 11/02/2026 at 18:00 and concluding 11/03/2026 at 06:00. Unified Command leadership is held by Incident Commanders Captain E. Miller (WSP Unified Command IC), OSC R. Brooks (EPA R10 Co-IC), Chief D. Olson (Skykomish Fire IC), and M. Campbell (BNSF Railway RP IC). Command Staff includes Safety Officer Captain Marcus Vance, Public Information Officer Jennifer Hayes (WSDOT), and Liaison Officer Deputy S. Kowalski (KCSO). + +The Operations Section is directed by Operations Section Chief Battalion Chief T. Higgins, with Staging Area Charlie Manager at Stevens Pass Yard. Tactical leadership positions include Hazmat Capping Branch Director Capt. P. Gomez, Railcar Entry Group 1 Supervisor Capt. D. Ross, Decontamination Group Supervisor Lt. M. Turner, and Evacuation & Security Branch Director Sgt. H. Lin. + +The Planning Section is led by Planning Section Chief Lt. Colonel Alan Vance, supported by Resources Unit Leader Patricia Ross, Situation Unit Leader Battalion Chief T. Higgins, Documentation Unit Leader Sgt. H. Lin, and Demobilization Unit Leader G. Peterson. + +The Logistics Section is headed by Logistics Section Chief David Sterling. Support Branch Director L. King oversees Supply Unit Leader J. Myers, Facilities Unit Leader S. Taylor, and Ground Support Unit Leader C. Adams. Service Branch Director M. Evans oversees Communications Unit Leader B. Foster, Medical Unit Leader Dr. N. Howard, and Food Unit Leader R. Lee. + +The Finance/Administration Section is directed by Finance/Admin Section Chief Patricia Ross, managing Time Unit Leader W. Harris, Procurement Unit Leader C. White, Comp/Claims Unit Leader G. Scott, and Cost Unit Leader E. Green. + +Prepared by Patricia Ross, Resources Unit Leader, signed Patricia Ross on 11/02/2026 16:30. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics207_5.txt b/benchmark/datasets/narratives/ics207_5.txt new file mode 100644 index 00000000..b4f9b25b --- /dev/null +++ b/benchmark/datasets/narratives/ics207_5.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart briefing governs the Silverwood Reservoir Crude Oil Pipeline Rupture operational period starting 05/18/2026 at 18:00 and concluding 05/19/2026 at 06:00. Unified Command leadership comprises Incident Commanders OSC C. Martinez (EPA R9 Co-IC), Chief M. Thorne (SBCo Fire IC), Chief Inspector A. Kim (Cal OES), and W. Vance (Pipeline RP IC). The Command Staff consists of Safety Officer Captain Gregory Hall, Public Information Officer Samantha Norris (Cal OES), and Liaison Officer Ranger D. Stevens (State Parks). + +The Operations Section is directed by Operations Section Chief Battalion Chief Kevin Ross, supported by Staging Area Delta Manager. Operational group leads include Canyon Pipeline Repair Branch Director Capt. Gregory Hall, Clamp Repair Group Supervisor Capt. A. Ross, Reservoir Skimming & Booming Branch Director Captain B. Walsh, and South Arm Skimmer Division Supervisor Capt. B. Walsh. + +The Planning Section is led by Planning Section Chief Captain Rachel Brooks, with Resources Unit Leader Jason Wu, Situation Unit Leader Battalion Chief Kevin Ross, Documentation Unit Leader Dr. L. Arispe, and Demobilization Unit Leader Captain B. Walsh. + +The Logistics Section is headed by Logistics Section Chief Megan Taylor. Under Support Branch Director Capt. P. Gomez, unit leads are Supply Unit Leader Lt. S. Baker, Facilities Unit Leader Capt. D. Ross, and Ground Support Unit Leader Lt. M. Turner. Under Service Branch Director Capt. H. Nelson, unit leads are Communications Unit Leader Sgt. R. Hall, Medical Unit Leader Dr. N. Howard, and Food Unit Leader L. King. + +The Finance/Administration Section is directed by Finance/Admin Section Chief Jason Wu, supervising Time Unit Leader J. Myers, Procurement Unit Leader S. Taylor, Comp/Claims Unit Leader C. Adams, and Cost Unit Leader M. Evans. + +Prepared by Jason Wu, Resources Unit Leader, signed Jason Wu on 05/18/2026 16:30. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics208_1.txt b/benchmark/datasets/narratives/ics208_1.txt new file mode 100644 index 00000000..8c0c6d74 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_1.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) applies to the Whispering Pines Derailment operational period starting 07/07/2026 at 18:00 and concluding 07/08/2026 at 06:00. + +The primary safety message directs entry teams to place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap Benzene vapors closer to the ground. Mandate Level B SCBA PPE within the 300-foot exclusion zone. Mandatory buddy system and continuous photoionization detector air monitoring required for all entry crews along creek banks. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at Mobile Command Post inside the Forward Operations Briefing Trailer. + +This form was prepared by Captain R. Mendez, Safety Officer, signed R. Mendez on date and time 07/07/2026 16:30. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics208_2.txt b/benchmark/datasets/narratives/ics208_2.txt new file mode 100644 index 00000000..a297c3b7 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_2.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) covers the Blackwood Chemical Pipeline Breach operational period starting 08/13/2026 at 18:00 and ending 08/14/2026 at 06:00. + +The safety message mandates: Focus tactical operations on maintaining river boom integrity under rising night tides and ensuring strict atmospheric air monitoring during the overnight temperature drop when Anhydrous Ammonia vapors hug low ground. SCBA Level B PPE is mandatory within the 500-foot exclusion zone. Vigilance required for reduced visibility, riverbank slip hazards, and toxic vapor pockets. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at Mobile Command Post Briefing Trailer at Station 12. + +This document was prepared by Captain L. Hayes, Safety Officer, signed L. Hayes on date and time 08/13/2026 16:30. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics208_3.txt b/benchmark/datasets/narratives/ics208_3.txt new file mode 100644 index 00000000..ac9fa901 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_3.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) governs the Port of Houston Sulfuric Acid Tanker Leak operational period starting 10/14/2026 at 07:00 and concluding 10/15/2026 at 19:00. + +The safety directive dictates: Primary tactical focus is placed on safe chemical neutralization and pH stabilization in the ship channel water column. Mandatory Level A chemical suit entry procedures enforced for all vessel deck operations. Due to extreme daytime heat (88°F, 78% humidity), entry work cycles are capped at 20 minutes active entry followed by 40 minutes active hydration and cooling. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at USCG Sector Houston Command Center Safety Office. + +Prepared by Commander Thomas Blake, Safety Officer, signed Thomas Blake on date and time 10/14/2026 06:00. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics208_4.txt b/benchmark/datasets/narratives/ics208_4.txt new file mode 100644 index 00000000..154fe178 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_4.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) applies to the Cascade Pass Chlorine Railcar Derailment operational period starting 11/02/2026 at 18:00 and concluding 11/03/2026 at 06:00. + +The safety message emphasizes: Command emphasizes absolute safety of capping entry teams working under nighttime freezing conditions (28°F overnight). Enforce mandatory Level A PPE compliance and buddy system protocols. Heated decontamination water and warm hydration must be maintained continuously to prevent suit icing and hypothermia. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at Skykomish School Gym Operations Briefing Room. + +Prepared by Captain Marcus Vance, Safety Officer, signed Marcus Vance on date and time 11/02/2026 16:30. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics208_5.txt b/benchmark/datasets/narratives/ics208_5.txt new file mode 100644 index 00000000..a8551d89 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_5.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) governs the Silverwood Reservoir Crude Oil Pipeline Rupture operational period starting 05/18/2026 at 18:00 and concluding 05/19/2026 at 06:00. + +The safety directive states: Focus tactical priority on preventing any oil migration toward the municipal water intake. Night skimming operations must maintain illuminated safety perimeters and 100% life-vest compliance at all times. High vigilance required for steep, oil-covered riprap banks and wildlife hazards. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at Incident Command Post at Hesperia Fire Station 30. + +Prepared by Captain Gregory Hall, Safety Officer, signed Gregory Hall on date and time 05/18/2026 16:30. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/templates/ics_207.json b/benchmark/datasets/templates/ics_207.json new file mode 100644 index 00000000..0ef33901 --- /dev/null +++ b/benchmark/datasets/templates/ics_207.json @@ -0,0 +1,63 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_organization_chart": { + "incident_commanders": ["string"], + "command_staff": { + "safety_officer": "string", + "public_information_officer": "string", + "liaison_officer": "string" + }, + "operations_section": { + "chief": "string", + "staging_area_manager": "string", + "branches_divisions_groups": [ + { + "title": "string", + "name": "string" + } + ] + }, + "planning_section": { + "chief": "string", + "resources_unit_ldr": "string", + "situation_unit_ldr": "string", + "documentation_unit_ldr": "string", + "demobilization_unit_ldr": "string" + }, + "logistics_section": { + "chief": "string", + "support_branch": { + "director": "string", + "supply_unit_ldr": "string", + "facilities_unit_ldr": "string", + "ground_spt_unit_ldr": "string" + }, + "service_branch": { + "director": "string", + "comms_unit_ldr": "string", + "medical_unit_ldr": "string", + "food_unit_ldr": "string" + } + }, + "finance_administration_section": { + "chief": "string", + "time_unit_ldr": "string", + "procurement_unit_ldr": "string", + "comp_claims_unit_ldr": "string", + "cost_unit_ldr": "string" + } + }, + "4_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_208.json b/benchmark/datasets/templates/ics_208.json new file mode 100644 index 00000000..a49113b3 --- /dev/null +++ b/benchmark/datasets/templates/ics_208.json @@ -0,0 +1,19 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "string", + "4_site_safety_plan_required": "boolean", + "approved_site_safety_plan_located_at": "string", + "5_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} From 2a6cd2b3abe16acecb5dc47d1454ebff5ae048e6 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 17:16:01 +0200 Subject: [PATCH 80/85] ICS dataset generation markdown --- benchmark/ICS_DATASET_GENERATOR_PROMPT.md | 183 ++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 benchmark/ICS_DATASET_GENERATOR_PROMPT.md diff --git a/benchmark/ICS_DATASET_GENERATOR_PROMPT.md b/benchmark/ICS_DATASET_GENERATOR_PROMPT.md new file mode 100644 index 00000000..aef2b99a --- /dev/null +++ b/benchmark/ICS_DATASET_GENERATOR_PROMPT.md @@ -0,0 +1,183 @@ +# ICS Benchmark Dataset Generator — System Prompt + +## Role +You are an **ICS (Incident Command System) Benchmark Dataset Specialist**. Your job is to analyze an official ICS form PDF and produce a complete, high-quality benchmark dataset for that form type. The dataset consists of three deliverables per form type: + +1. **A JSON Template Schema** (`ics_XXX.json`) — the canonical field structure for the form +2. **5 Narrative Files** (`icsXXX_N.txt`) — realistic, richly detailed incident briefing narratives +3. **5 Ground Truth JSON Files** (`icsXXX_N.json`) — structured JSON objects that exactly match the data in the corresponding narrative + +All files must be internally consistent: every field in the ground truth JSON must be directly extractable from the narrative text. + +--- + +## Step 1 — Extract the Template Schema from the PDF + +Read the provided ICS form PDF carefully. Identify every labeled field, section, sub-section, and repeating row. Then produce a **JSON template schema** where: + +- Every field is represented with its **canonical key name** (snake_case, prefixed with the section number when appropriate, e.g. `"1_incident_name"`, `"2_operational_period"`) +- Every field value is typed as a **placeholder string** `"string"`, `"boolean"`, or an **array** `[]` or **nested object** `{}` +- Repeating rows (e.g. resource tables, radio channels, personnel lists) are represented as **arrays of objects** +- Nested sections (e.g. branches with sub-groups) use **nested objects or arrays** +- No fields from the PDF are omitted — capture every labeled input box, checkbox, and table column + +### Output location +Save the template as: +``` +benchmark/datasets/templates/ics_XXX.json +``` + +--- + +## Step 2 — Generate 5 Distinct Mock Incidents + +Create **5 unique, realistic emergency incidents** to populate your dataset. Each incident must: + +- Be a **different incident type** (e.g. chemical spill, tanker collision, pipeline rupture, railcar derailment, industrial release) +- Involve a **different geography** (different U.S. states/regions/waterways) +- Involve **different response agencies** (USCG, EPA, Cal OES, State DEP, County Fire, etc.) +- Involve **different hazardous materials** (Benzene, Styrene, Chlorine, Anhydrous Ammonia, Crude Oil, Sulfuric Acid, etc.) +- Use **different operational contexts** — day shift vs. night shift, maritime vs. inland, urban vs. mountain, etc. +- Reference the **same cast of personnel across forms** — if an incident has a Planning Section Chief named "Rachel Brooks" in the ICS 201, she must appear in the ICS 202, 203, 204, etc. for that same incident + +--- + +## Step 3 — Write the Narrative (`.txt` file) + +For each incident, write a **formal ICS narrative** that reads like an official briefing document. Adhere to these rules: + +### Style & Tone +- Professional, authoritative emergency management language +- Written in full paragraphs (not bullet points), as if read aloud at a shift briefing +- Dense with operational detail — specific numbers, times, distances, frequencies, names, unit identifiers + +### Structure +The narrative must organically embed **every data field** from the template schema, in natural prose, without using JSON-style formatting or field labels. A downstream LLM must be able to read this narrative and extract every ground truth value from it. + +### Required Detail Level +- **Named personnel** with full titles and affiliations for every position +- **Specific radio frequencies** (e.g. `462.5500 MHz`, `Tone 114.8`) +- **Unit identifiers** (e.g. `HZM-01`, `ENG-41`, `RV-LC-01`) +- **Exact times** for all actions and operational periods +- **Specific geographic references** (mile markers, lat/lon, staging area locations) +- **Specific hazmat parameters** (flash points, IDLH thresholds, PPE levels, decon procedures) +- **Specific quantities** (gallons released, feet of boom, lbs of chemical, number of personnel) + +### Output location +Save each narrative as: +``` +benchmark/datasets/narratives/icsXXX_N.txt +``` +Where `XXX` is the form number (e.g. `201`, `202`, `205a`) and `N` is 1–5. + +--- + +## Step 4 — Write the Ground Truth JSON (`.json` file) + +For each narrative, produce a **strictly schema-compliant JSON object** that: + +- Uses the **exact same field structure** as the template schema in Step 1 +- Is wrapped in a top-level key: `"ics_XXX_ground_truth": { ... }` +- Populates **every field** with the exact value as stated in the narrative (no paraphrasing, no invention) +- Uses `true`/`false` (not strings) for boolean fields (e.g. `"arrived": true`) +- Uses arrays correctly for repeating elements +- Leaves fields as `""` only if the narrative explicitly states the position is unfilled — never omit fields from the schema +- All string values preserve the exact formatting used in the narrative (e.g. `"07/07/2026"` not `"2026-07-07"`) + +### Output location +Save each ground truth file as: +``` +benchmark/datasets/ground_truth/icsXXX_N.json +``` + +--- + +## Step 5 — Cross-Consistency Requirements + +Apply these rules across ALL files you create: + +| Rule | Description | +|---|---| +| **Name consistency** | Every person's name, title, and affiliation must be identical across all forms for the same incident | +| **Time consistency** | Operational period dates/times must match across ICS 202, 203, 204, 205, 205A for the same incident | +| **Unit consistency** | Resource identifiers (e.g. `HZM-01`) must match across ICS 201, 204 for the same incident | +| **Frequency consistency** | Radio frequencies in ICS 205 must match those referenced in ICS 204 and ICS 205A | +| **Incident name consistency** | The exact same incident name string must appear in every form for that incident | +| **IAP page number** | Each form type has a conventional page position in the IAP — assign realistic sequential page numbers | + +--- + +## File Naming Convention + +``` +Templates: benchmark/datasets/templates/ics_XXX.json +Narratives: benchmark/datasets/narratives/icsXXX_N.txt +Ground Truth: benchmark/datasets/ground_truth/icsXXX_N.json +``` + +Where: +- `XXX` = form number: `201`, `202`, `203`, `204`, `205`, `205a`, `206`, `207`, `208`, etc. +- `N` = incident index: `1` through `5` + +--- + +## Form-Specific Guidance + +### ICS 201 — Incident Briefing +Fields cover: incident name/number, initiation date/time, map/sketch details (area of operations, impacted areas, trajectories, shorelines), situation summary, health/safety hazards, protective measures, preparer info, objectives list, chronological tactics table, command/general staff organization (with additional positions), and full resource summary table (identifier, leader, ordered time, ETA, arrived boolean, notes). + +### ICS 202 — Incident Objectives +Fields cover: incident name, operational period (from/to date and time), objectives list (SMART-formatted strings), operational period command emphasis paragraph, general situational awareness paragraph, site safety plan required (boolean), safety plan location, IAP attachments checklist (ICS 203–208, map/chart, weather, other), preparer info, incident commander approval info, and IAP page number. + +### ICS 203 — Organization Assignment List +Fields cover: incident name, operational period, command staff (IC/UC list, deputy, safety officer, PIO, liaison), agency/organization representatives table, planning section (chief, deputy, unit leaders, technical specialists), logistics section (chief, deputy, support branch with sub-units, service branch with sub-units), operations section (chief, deputy, staging area, branches with directors/deputies/divisions/groups, air ops branch), finance/admin section (chief, deputy, unit leaders), preparer info, and IAP page number. + +### ICS 204 — Assignment List +Fields cover: incident name, operational period, branch/division/group/staging area identifiers, operations personnel (ops chief, branch director, div/group supervisor — each with name and contact), resources assigned table (identifier, leader, persons, contact, reporting location/equipment/notes), work assignments paragraph, special instructions paragraph, communications table (function/name and primary contact), preparer info, and IAP page number. + +### ICS 205 — Incident Radio Communications Plan +Fields cover: incident name, date/time prepared, operational period, radio channel table (zone/group, channel number, function, channel name/talkgroup, assignment, RX frequency with N/W, RX tone/NAC, TX frequency with N/W, TX tone/NAC, mode A/D/M, remarks), special instructions, preparer info (name, signature, date/time), and IAP page number. + +### ICS 205A — Communications List +Fields cover: incident name, operational period, basic local communications table (incident assigned position, name, methods of contact), preparer info (name, position title, signature, date/time), and IAP page number. + +--- + +## Quality Checklist + +Before finalizing, verify: + +- [ ] Template schema captures every labeled field in the PDF +- [ ] All 5 narratives reference different incidents, regions, chemicals, and agencies +- [ ] Each narrative is rich enough that a downstream LLM can extract every ground truth field from prose alone +- [ ] Every ground truth JSON is 100% schema-compliant with the template +- [ ] All string values in JSON match the narrative exactly (same spelling, same format) +- [ ] Personnel names, unit IDs, frequencies, and times are internally consistent within each incident +- [ ] Boolean fields use `true`/`false`, not `"true"`/`"false"` +- [ ] Files are saved in the correct directories with the correct naming convention + +--- + +## Example Excerpt (ICS 205) + +**Narrative excerpt:** +> *Channel 2 (Zone A): Functioned for Tactical operations, channel name HAZ-TAC-1, assigned to the Hot Zone Entry Group. Operates on RX frequency 467.7750 N with DCS tone D023 and TX frequency 467.7750 N with DCS tone D023 in Digital mode, restricting use to intrinsically safe radios only.* + +**Corresponding ground truth:** +```json +{ + "zone_grp": "Zone A", + "channel_number": "2", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "HAZ-TAC-1", + "assignment": "Hot Zone Entry Group", + "rx_frequency_n_or_w": "467.7750 N", + "rx_tone_nac": "D023", + "tx_frequency_n_or_w": "467.7750 N", + "tx_tone_nac": "D023", + "mode_a_d_or_m": "D", + "remarks": "Intrinsically safe radios only" +} +``` + +This demonstrates the core principle: **every JSON value must be explicitly readable in the narrative prose**. From 2dd9e3aa2590e70ae23273270b4b2a1f0669ed99 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 18:24:46 +0200 Subject: [PATCH 81/85] refactor: :recycle: linting errors --- benchmark/compare_benchmarks.py | 1 + benchmark/evaluators/accuracy.py | 1 + benchmark/evaluators/reference_accuracy.py | 1 - benchmark/pipelines/base.py | 3 ++- benchmark/pipelines/pipeline.py | 1 - benchmark/runners/runner.py | 4 +++- benchmark/test_benchmark.py | 4 +++- 7 files changed, 10 insertions(+), 5 deletions(-) diff --git a/benchmark/compare_benchmarks.py b/benchmark/compare_benchmarks.py index 256d28dd..25e1d15b 100644 --- a/benchmark/compare_benchmarks.py +++ b/benchmark/compare_benchmarks.py @@ -2,6 +2,7 @@ import os import sys + def main(): if len(sys.argv) < 3: print("Usage: python compare_benchmarks.py ") diff --git a/benchmark/evaluators/accuracy.py b/benchmark/evaluators/accuracy.py index c4bd30d7..3024f901 100644 --- a/benchmark/evaluators/accuracy.py +++ b/benchmark/evaluators/accuracy.py @@ -1,5 +1,6 @@ import re + def calculate_accuracy(extracted: any, ground_truth: any) -> float: """ Computes a score from 0.0 to 1.0 representing accuracy. diff --git a/benchmark/evaluators/reference_accuracy.py b/benchmark/evaluators/reference_accuracy.py index a8a608c5..a738d99c 100644 --- a/benchmark/evaluators/reference_accuracy.py +++ b/benchmark/evaluators/reference_accuracy.py @@ -6,7 +6,6 @@ from benchmark.evaluators.accuracy import calculate_accuracy - PROMPT_VERSION = "1" SYSTEM_PROMPT = """You create reference answers for extraction benchmarks. Use only facts supported by the narrative, preserve source wording when practical, diff --git a/benchmark/pipelines/base.py b/benchmark/pipelines/base.py index 80367534..4abce9ec 100644 --- a/benchmark/pipelines/base.py +++ b/benchmark/pipelines/base.py @@ -1,5 +1,6 @@ -from dataclasses import dataclass from abc import ABC, abstractmethod +from dataclasses import dataclass + @dataclass class PipelineExtractionOutput: diff --git a/benchmark/pipelines/pipeline.py b/benchmark/pipelines/pipeline.py index f6883d5e..44dcf42d 100644 --- a/benchmark/pipelines/pipeline.py +++ b/benchmark/pipelines/pipeline.py @@ -1,4 +1,3 @@ -import time from benchmark.pipelines.base import BasePipeline, PipelineExtractionOutput diff --git a/benchmark/runners/runner.py b/benchmark/runners/runner.py index d6da6c35..6e3b7fbe 100644 --- a/benchmark/runners/runner.py +++ b/benchmark/runners/runner.py @@ -1,8 +1,10 @@ -import time import json import os +import time + from benchmark.evaluators.accuracy import calculate_accuracy + class Runner: def __init__(self, pipeline_class, pipeline_name: str): self.pipeline = pipeline_class() diff --git a/benchmark/test_benchmark.py b/benchmark/test_benchmark.py index 44262287..38fe9404 100644 --- a/benchmark/test_benchmark.py +++ b/benchmark/test_benchmark.py @@ -1,9 +1,11 @@ -import os import json +import os from datetime import datetime + from benchmark.pipelines.pipeline import Pipeline from benchmark.runners.runner import Runner + def test_pipeline_execution(): """ Standard test executor that finds the available Pipeline class, From 157e01a12af8f44c984d798b468b8790aa9ceb77 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Thu, 13 Aug 2026 18:25:44 +0200 Subject: [PATCH 82/85] linter errors 2 --- alembic/env.py | 14 +++++++++++--- alembic/versions/001_initial_schema.py | 3 ++- alembic/versions/002_v1_models.py | 3 ++- app/api/deps.py | 4 +++- app/api/routes/__init__.py | 4 ++-- app/api/routes/forms.py | 22 ++++++++++++++-------- app/api/routes/input.py | 3 ++- app/api/routes/templates.py | 16 ++++++++++------ app/api/schemas/templates.py | 1 + app/core/celery.py | 3 ++- app/db/init_db.py | 2 +- app/db/repositories.py | 3 ++- app/models/__init__.py | 10 +++++----- app/models/models.py | 6 +++--- app/services/controller.py | 2 +- app/services/external_apis/weather_api.py | 1 - app/services/external_apis/zipcode_api.py | 2 +- app/services/external_apis_coordinator.py | 3 ++- app/services/file_manipulator.py | 3 ++- app/services/filler.py | 4 +++- app/services/llm.py | 4 ++-- app/tasks/fill.py | 7 ++++++- app/tasks/purge.py | 3 ++- tests/conftest.py | 17 +++++++++++++---- tests/test_api.py | 4 ++-- tests/test_deletion.py | 2 +- tests/test_jobs.py | 3 ++- tests/test_migrations.py | 3 ++- tests/test_v1_models.py | 1 - tests/test_v1_system.py | 1 - tests/test_v1_voice.py | 2 +- 31 files changed, 100 insertions(+), 56 deletions(-) diff --git a/alembic/env.py b/alembic/env.py index dd2f3739..55888cee 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,12 +1,20 @@ from logging.config import fileConfig -from alembic import context from sqlalchemy import engine_from_config, pool - from sqlmodel import SQLModel +from alembic import context from app.core.config import DATABASE_URL -from app.models import Extraction, Form, FormSubmission, Incident, Input, Job, Report, Template # noqa: F401 +from app.models import ( # noqa: F401 + Extraction, + Form, + FormSubmission, + Incident, + Input, + Job, + Report, + Template, +) config = context.config diff --git a/alembic/versions/001_initial_schema.py b/alembic/versions/001_initial_schema.py index 12db15df..04e6a1fc 100644 --- a/alembic/versions/001_initial_schema.py +++ b/alembic/versions/001_initial_schema.py @@ -7,10 +7,11 @@ """ from collections.abc import Sequence -from alembic import op import sqlalchemy as sa import sqlmodel +from alembic import op + revision: str = "001" down_revision: str | None = None branch_labels: str | Sequence[str] | None = None diff --git a/alembic/versions/002_v1_models.py b/alembic/versions/002_v1_models.py index 624e5b40..c943e9e8 100644 --- a/alembic/versions/002_v1_models.py +++ b/alembic/versions/002_v1_models.py @@ -16,10 +16,11 @@ from collections.abc import Sequence -from alembic import op import sqlalchemy as sa import sqlmodel +from alembic import op + revision: str = "002" down_revision: str | None = "001" branch_labels: str | Sequence[str] | None = None diff --git a/app/api/deps.py b/app/api/deps.py index 36ec257c..7850dab0 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -1,7 +1,9 @@ -from app.db.database import get_session from fastapi import Header, Request + from app.core.config import FIREFORM_API_KEY from app.core.errors.base import AppError +from app.db.database import get_session + def get_db(): yield from get_session() diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py index 2264aee0..d99c8894 100644 --- a/app/api/routes/__init__.py +++ b/app/api/routes/__init__.py @@ -1,3 +1,3 @@ -from . import templates, forms +from . import forms, templates -__all__ = ["templates", "forms"] +__all__ = ["forms", "templates"] diff --git a/app/api/routes/forms.py b/app/api/routes/forms.py index be160291..60b8b577 100644 --- a/app/api/routes/forms.py +++ b/app/api/routes/forms.py @@ -1,7 +1,8 @@ from datetime import datetime, timedelta, timezone from pathlib import Path + import requests -from fastapi import APIRouter, Depends, File, UploadFile, Query +from fastapi import APIRouter, Depends, File, Query, UploadFile from sqlmodel import Session, select from app.api.deps import get_db, verify_api_key @@ -11,12 +12,17 @@ ModelsResponse, TranscriptionResponse, ) -from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, BASE_DIR, RETENTION_PERIOD_DAYS -from app.services.whisper import call_whisper_asr +from app.core.config import BASE_DIR, OLLAMA_HOST, OLLAMA_MODEL, RETENTION_PERIOD_DAYS from app.core.errors.base import AppError -from app.db.repositories import create_form, get_template, get_form_submission, delete_form_submission +from app.db.repositories import ( + create_form, + delete_form_submission, + get_form_submission, + get_template, +) from app.models import FormSubmission, Template from app.services.controller import Controller +from app.services.whisper import call_whisper_asr PROJECT_ROOT = BASE_DIR @@ -175,8 +181,9 @@ def get_submissions(db: Session = Depends(get_db)): @router.get("/submissions/analytics") def get_submissions_analytics(db: Session = Depends(get_db)): - from collections import Counter import re + from collections import Counter + from sqlmodel import select statement = select(FormSubmission, Template.name).join( @@ -194,9 +201,8 @@ def get_submissions_analytics(db: Session = Depends(get_db)): "the", "and", "a", "of", "to", "in", "is", "that", "it", "was", "for", "on", "as", "with", "by", "at", "an", "be", "this", "are", "from", "or", "have", "has", "had", "but", "not", "he", "she", "they", "we", "i", "you", "my", "his", - "her", "their", "our", "me", "him", "them", "us", "about", "there", "their", - "were", "been", "would", "could", "should", "will", "can", "no", "yes", "any", - "so", "very", "patient", "presents", "with", "reported", "history", "shows", + "her", "their", "our", "me", "him", "them", "us", "about", "there", "were", "been", "would", "could", "should", "will", "can", "no", "yes", "any", + "so", "very", "patient", "presents", "reported", "history", "shows", "left", "right", "pain", "due", "after", "before", "emergency", "department", "medical", "clinical" } diff --git a/app/api/routes/input.py b/app/api/routes/input.py index 2cd2329c..d0afce64 100644 --- a/app/api/routes/input.py +++ b/app/api/routes/input.py @@ -21,7 +21,8 @@ INPUT_POLL_INTERVAL_SECONDS, ) from app.core.errors.base import AppError -from app.db.repositories import create_input, get_input as repo_get_input +from app.db.repositories import create_input +from app.db.repositories import get_input as repo_get_input from app.services.input import InputService from app.services.whisper import check_whisper_available diff --git a/app/api/routes/templates.py b/app/api/routes/templates.py index ee7189aa..d5cd3c2b 100644 --- a/app/api/routes/templates.py +++ b/app/api/routes/templates.py @@ -4,21 +4,25 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile from fastapi.responses import FileResponse -from sqlmodel import Session +from sqlmodel import Session, select from app.api.deps import get_db, verify_api_key from app.api.schemas.templates import ( + MakeFillableRequest, + MakeFillableResponse, TemplateCreate, TemplateResponse, TemplateUploadResponse, - MakeFillableRequest, - MakeFillableResponse, ) from app.core.config import BASE_DIR, DEFAULT_TEMPLATE_DIR -from app.db.repositories import create_template, list_templates, get_template, delete_template -from app.models import Template, FormSubmission, Job +from app.db.repositories import ( + create_template, + delete_template, + get_template, + list_templates, +) +from app.models import FormSubmission, Job, Template from app.services.controller import Controller -from sqlmodel import select router = APIRouter(prefix="/templates", tags=["templates"]) PROJECT_ROOT = BASE_DIR diff --git a/app/api/schemas/templates.py b/app/api/schemas/templates.py index df39832b..351b1870 100644 --- a/app/api/schemas/templates.py +++ b/app/api/schemas/templates.py @@ -1,5 +1,6 @@ from pydantic import BaseModel + class TemplateCreate(BaseModel): name: str pdf_path: str diff --git a/app/core/celery.py b/app/core/celery.py index a91146f7..e42504b8 100644 --- a/app/core/celery.py +++ b/app/core/celery.py @@ -20,7 +20,8 @@ # Optional Celery Beat schedule — runs purge_old_submissions once a day. # Enable by running: celery -A app.core.celery beat -from celery.schedules import crontab # noqa: E402 +from celery.schedules import crontab + celery_app.conf.beat_schedule = { "daily-submission-purge": { "task": "purge_old_submissions", diff --git a/app/db/init_db.py b/app/db/init_db.py index ebfe09b2..54ba084b 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -2,10 +2,10 @@ import logging from pathlib import Path -from alembic import command from alembic.config import Config from sqlmodel import Session, select +from alembic import command from app.core.config import DEFAULT_TEMPLATE_DIR from app.db.database import engine from app.models import FormSubmission, Template # noqa: F401 diff --git a/app/db/repositories.py b/app/db/repositories.py index 0935c133..d53b197e 100644 --- a/app/db/repositories.py +++ b/app/db/repositories.py @@ -2,7 +2,8 @@ from sqlmodel import Session, select -from app.models import Template, FormSubmission, Job, Input +from app.models import FormSubmission, Input, Job, Template + # Templates def create_template(session: Session, template: Template) -> Template: diff --git a/app/models/__init__.py b/app/models/__init__.py index bba2eecb..9e736ec7 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -12,12 +12,12 @@ ) __all__ = [ - "Template", - "FormSubmission", - "Job", - "Input", "Extraction", - "Incident", "Form", + "FormSubmission", + "Incident", + "Input", + "Job", "Report", + "Template", ] diff --git a/app/models/models.py b/app/models/models.py index cb9a5ff7..8358b08d 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,9 +1,9 @@ import uuid as uuid_mod -from uuid import UUID, uuid4 from datetime import date, datetime, timezone +from uuid import UUID, uuid4 -from sqlalchemy import Column, JSON -from sqlmodel import SQLModel, Field +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel from sqlmodel.sql.sqltypes import AutoString from app.api.schemas.enums import ( diff --git a/app/services/controller.py b/app/services/controller.py index cccb62c3..2a9efcbb 100644 --- a/app/services/controller.py +++ b/app/services/controller.py @@ -1,5 +1,5 @@ -from app.services.file_manipulator import FileManipulator from app.services.external_apis_coordinator import ExternalAPIsCoordinator +from app.services.file_manipulator import FileManipulator class Controller: diff --git a/app/services/external_apis/weather_api.py b/app/services/external_apis/weather_api.py index 61cd04f3..8f0e0ab2 100644 --- a/app/services/external_apis/weather_api.py +++ b/app/services/external_apis/weather_api.py @@ -1,5 +1,4 @@ import openmeteo_requests - import pandas as pd import requests_cache from retry_requests import retry diff --git a/app/services/external_apis/zipcode_api.py b/app/services/external_apis/zipcode_api.py index c1a043a3..094b6d5d 100644 --- a/app/services/external_apis/zipcode_api.py +++ b/app/services/external_apis/zipcode_api.py @@ -1,5 +1,5 @@ +from geopy.exc import GeocoderServiceError, GeocoderTimedOut from geopy.geocoders import Nominatim -from geopy.exc import GeocoderTimedOut, GeocoderServiceError from app.core.logging import get_logger diff --git a/app/services/external_apis_coordinator.py b/app/services/external_apis_coordinator.py index 07952ab0..1e7654f1 100644 --- a/app/services/external_apis_coordinator.py +++ b/app/services/external_apis_coordinator.py @@ -1,5 +1,6 @@ -from app.services.external_apis.zipcode_api import ZipCodeAPI from app.services.external_apis.weather_api import WeatherAPI +from app.services.external_apis.zipcode_api import ZipCodeAPI + class ExternalAPIsCoordinator: def __init__(self): diff --git a/app/services/file_manipulator.py b/app/services/file_manipulator.py index 87142e13..e4267971 100644 --- a/app/services/file_manipulator.py +++ b/app/services/file_manipulator.py @@ -1,7 +1,8 @@ import os + +from app.core.logging import get_logger from app.services.filler import Filler from app.services.llm import LLM -from app.core.logging import get_logger logger = get_logger(__name__) diff --git a/app/services/filler.py b/app/services/filler.py index aec97560..148b2ff6 100644 --- a/app/services/filler.py +++ b/app/services/filler.py @@ -1,6 +1,8 @@ +from datetime import datetime + from pdfrw import PdfReader, PdfWriter + from app.services.llm import LLM -from datetime import datetime class Filler: diff --git a/app/services/llm.py b/app/services/llm.py index e2d1639d..85e168c7 100644 --- a/app/services/llm.py +++ b/app/services/llm.py @@ -1,7 +1,8 @@ import json import os + import requests -from requests.exceptions import Timeout, RequestException +from requests.exceptions import RequestException, Timeout from app.core.config import OLLAMA_HOST, OLLAMA_MODEL from app.core.logging import get_logger @@ -92,7 +93,6 @@ def add_response_to_json(self, field: str, value: str): else: self._json[field] = parsed_value - return def get_data(self): diff --git a/app/tasks/fill.py b/app/tasks/fill.py index fc7b3115..61e4cbff 100644 --- a/app/tasks/fill.py +++ b/app/tasks/fill.py @@ -3,7 +3,12 @@ from app.core.celery import celery_app from app.db.database import get_session -from app.db.repositories import get_job_by_celery_id, get_template, update_job, create_form +from app.db.repositories import ( + create_form, + get_job_by_celery_id, + get_template, + update_job, +) from app.models import FormSubmission from app.services.controller import Controller diff --git a/app/tasks/purge.py b/app/tasks/purge.py index 83256751..0639f525 100644 --- a/app/tasks/purge.py +++ b/app/tasks/purge.py @@ -8,12 +8,13 @@ from datetime import datetime, timedelta, timezone from pathlib import Path +from sqlmodel import select + from app.core.celery import celery_app from app.core.config import BASE_DIR, RETENTION_PERIOD_DAYS from app.db.database import get_session from app.db.repositories import delete_form_submission from app.models import FormSubmission -from sqlmodel import select logger = logging.getLogger(__name__) diff --git a/tests/conftest.py b/tests/conftest.py index 62fae518..ecdc4e9b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,16 +5,25 @@ """ import io -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient from sqlalchemy.pool import StaticPool -from sqlmodel import SQLModel, Session, create_engine +from sqlmodel import Session, SQLModel, create_engine -from app.main import app from app.api.deps import get_db -from app.models import Template, FormSubmission, Job, Input, Extraction, Incident, Form, Report # noqa: F401 — registers tables +from app.main import app +from app.models import ( # noqa: F401 — registers tables + Extraction, + Form, + FormSubmission, + Incident, + Input, + Job, + Report, + Template, +) # --------------------------------------------------------------------------- # In-memory database diff --git a/tests/test_api.py b/tests/test_api.py index 32104ae7..c794431a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,9 +6,8 @@ from sqlmodel import select -from app.models import Template, FormSubmission from app.core.config import API_PREFIX - +from app.models import FormSubmission, Template # ═══════════════════════════════════════════════════════════════════════════ # DB model sanity @@ -298,6 +297,7 @@ def test_fill_form_passes_model_override(self, client, mock_controller): def test_transcribe_service_unavailable(self, client, monkeypatch): """A down whisper service surfaces as a 503, not a 500.""" import io + import requests def fake_post(*args, **kwargs): diff --git a/tests/test_deletion.py b/tests/test_deletion.py index 0d036dcb..7dcffc40 100644 --- a/tests/test_deletion.py +++ b/tests/test_deletion.py @@ -6,8 +6,8 @@ import pytest -from app.models import FormSubmission, Template from app.core.config import API_PREFIX +from app.models import FormSubmission, Template # --------------------------------------------------------------------------- # Helpers diff --git a/tests/test_jobs.py b/tests/test_jobs.py index c79e4a42..5c6e06b5 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -1,6 +1,7 @@ """Tests for async job submission and status endpoints.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + from app.core.config import API_PREFIX diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 6c404b80..25c10dc3 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -8,10 +8,11 @@ """ import pytest -from alembic import command from alembic.config import Config from sqlalchemy import create_engine, inspect +from alembic import command + ALEMBIC_INI = "alembic.ini" diff --git a/tests/test_v1_models.py b/tests/test_v1_models.py index 6584ac36..396f537e 100644 --- a/tests/test_v1_models.py +++ b/tests/test_v1_models.py @@ -24,7 +24,6 @@ ) from app.models import Extraction, Form, Incident, Input, Report - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_v1_system.py b/tests/test_v1_system.py index b7b8dfcb..d8851213 100644 --- a/tests/test_v1_system.py +++ b/tests/test_v1_system.py @@ -12,7 +12,6 @@ import app.api.routes.system as system_mod - # --------------------------------------------------------------------------- # Low-level helpers # --------------------------------------------------------------------------- diff --git a/tests/test_v1_voice.py b/tests/test_v1_voice.py index 3c512a52..c911db42 100644 --- a/tests/test_v1_voice.py +++ b/tests/test_v1_voice.py @@ -191,7 +191,7 @@ def test_success_input_becomes_ready_with_transcript(self, db, test_engine): assert inp.status == InputStatus.ready assert inp.transcript == "Incident at Main St, two casualties." assert inp.character_count == len("Incident at Main St, two casualties.") - assert inp.word_count == len("Incident at Main St, two casualties.".split()) + assert inp.word_count == len(["Incident", "at", "Main", "St,", "two", "casualties."]) def test_success_job_becomes_completed_with_result_url(self, db, test_engine): inp, job = _seed_input_and_job(db) From cb25b47aac71eba2a7d111fe88b88913915e2523 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Fri, 14 Aug 2026 20:39:11 +0200 Subject: [PATCH 83/85] feat: :sparkles: add ics201-208 & 213 pdfs --- benchmark/datasets/pdfs/ics_201.pdf | Bin 0 -> 124725 bytes benchmark/datasets/pdfs/ics_202.pdf | Bin 0 -> 351677 bytes benchmark/datasets/pdfs/ics_203.pdf | Bin 0 -> 63774 bytes benchmark/datasets/pdfs/ics_204.pdf | Bin 0 -> 364179 bytes benchmark/datasets/pdfs/ics_205.pdf | Bin 0 -> 258133 bytes benchmark/datasets/pdfs/ics_205a.pdf | Bin 0 -> 55143 bytes benchmark/datasets/pdfs/ics_206.pdf | Bin 0 -> 107649 bytes benchmark/datasets/pdfs/ics_207.pdf | Bin 0 -> 37300 bytes benchmark/datasets/pdfs/ics_208.pdf | Bin 0 -> 143117 bytes benchmark/datasets/pdfs/ics_213.pdf | Bin 0 -> 26863 bytes 10 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 benchmark/datasets/pdfs/ics_201.pdf create mode 100644 benchmark/datasets/pdfs/ics_202.pdf create mode 100644 benchmark/datasets/pdfs/ics_203.pdf create mode 100644 benchmark/datasets/pdfs/ics_204.pdf create mode 100644 benchmark/datasets/pdfs/ics_205.pdf create mode 100644 benchmark/datasets/pdfs/ics_205a.pdf create mode 100644 benchmark/datasets/pdfs/ics_206.pdf create mode 100644 benchmark/datasets/pdfs/ics_207.pdf create mode 100644 benchmark/datasets/pdfs/ics_208.pdf create mode 100644 benchmark/datasets/pdfs/ics_213.pdf diff --git a/benchmark/datasets/pdfs/ics_201.pdf b/benchmark/datasets/pdfs/ics_201.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fe26bef5c0c67f825ced4878a82bc1c39b19726b GIT binary patch literal 124725 zcma&NQ*f`(w*4L3wz*>4&Wdek#kOtRwryv{w)2f`J9&TmU++0rd!O3XH(h=4RL@cK zsT$)`v&j`i#p#&n+2P6e4`vtPVVIa08HpH)?2Ihod3hPcEv%hQ9T~)}4V_IzO^xkL zO#eA@baEzQ;^bhEF|{>wHYZ|cXX9XyBVyuWQ1P%gWso7z3?Wd6)uIjffF^2{O zAG$JhAc`whbuBZ*X)T}`#zW8=7{-Uvs{9(#8yeIQUqhvpDe2EOQ6xftMvEF;dN#s{ zVEwB%C>lS!_B&%N5X=iZsbEMCgOM5~c}$q%0O`pTIjN$bjTxBF)GM)qD0NMlHkM=w zPTH8d+IkShbahNAbx69tO#W@aJFJg!8S-}58rUz@I*qV9AmT5#U1X!4@XO)uY5q80 zfS7@GKj8A_p{VrQ5HKMj6|M4J#@}wloDd71A+3zQ-SLEBKBIYF+?%9G3F}SSjGw_N zoXnX)RpNkyqN|vtn6=PdoO*0=N9E*A62FwlucZN2%<({X7GTfA*NgzR-BK(NCB+Z~O>jQ_643(h^@%TU_2dZr?7{OZ{g6b9gTx2< zEKV{Gii*&rkBBeuko@j{G%}eEeW!vkzz_>U&6Zd&&ctc^hcy#@I7gy zTCwvpH2uMbd^%EdMCie;+gI`}n86yj&-BX&PI4WXxZfL?%(tFsG1Q0!gvta^JIuGv zeFSI3TT+8XWLv)n3&^s6|Fh$r2!Dn4OMDI~_8}4xkbj4&OSJz5J`TMg!I>2J3T>Bo z8rUjESaxM9U8iyFi zkq4QlPN+`E(U^7~75(W;(zn&yb69qCU%oII8}jjS@_bkrN$V}~9($i-S9g%Mm_+d? zF|96-DP5@v z=LPMfc(&dCM_?wch{g9gf{?C$VdTaAIH8tIk6x8qc{c=P_4Y94z&h2`jRh4#`}$Jy zS=vr+Cxc(Vp>d|FfU(-?ukEt8Xw*lFq0iz)WB|@|Cb$)Q>9WuIQcez7M`J&DXCp^y znU0+^AS>{!69|LosdR++k$c6P@v87wT2*dKDQ+dfQuF)HuEJt!X;%UStd?>L+gxTU zTfN$3Y7L-zqT4uRJ=r4;Vz4oY4N~UPa~m;77t@(TBn!8+t5#>ls@^^=uV*`fF|b7{0ofZn?P z<6FgBFD495iH@fTeVrgVP(Y)J(LcR=jSCOIXi)fDg+sH@ww>S;rZx4)kJU6uFB z^@;9YN94AL&lp7W(sLbY<0g+&9tw2V)L55YmOIFI)*APK`uczauVC_W%J#w29cuL| zV*8u3`3{iTu7ov3V6Sc41+QgA^;zTO!Tm19T)B_Ui>Dfj$&eD9zCe~)qRoNUaatz)N$<#kPAIE5IU7NBZxVHWq;SzYUPOsCSW`+Feg@73#gQ|T|g9L-csyzGnA291@H)`1ePfpBf7nA#$s4!y z7Rz7<)n+eh@kS*~H=kzJJ3~Tjty~4Dj3Rv(hfpfAUh`Ul8lI!Y4W-{z-EzzQzs3;# z-cfHq`*#S?fuQ@)2#W1PM)ftXvcLRGZ&x+;M==B#9$IEEgz&$k{>Gas{Iah(!yJvs z`JK45E_AWRm0xzFd9OilEy7J9$?CcH(l*slrV$%LRVL|+xCxK$}Al__B{dRfxV4;Otz z)MBh~O$HzH#2g`cd1LE}5JQ4`rU) zs$UV(^NqK;HS(NWf@|n25)oM-S`R|RT-G(^!{;-F&(iat?D#~_U0@OroD}0p%D%UGDO)-E4 z`qxOKG)3%NXJ7Tg?z}16H^XG5B!Q~hjhgJw7oMq0VszNVIp&_csY}%L6U4yLm2j}S z&0<=|aVyOJZE13U-fd}Y`-e}5|7b_W9<>jFl<)H3LFP$k$cemO>s!5^$}uWKrrwE0 z&YNX%U9zjwP<~>qC$(-n7sHiKpHWQmwPC-DU(L6#@p+vJUy8{{ zqK|v>68`?Gr5DnATq`5DGt=I!@URE|ga|f4Kk)qz1;c1Me~CiTSZ`!}z()nK%Dh#w zIkK$GbQUpSK@rl}hS(z9muZQ2(0Q$1;l9!S3}`pe^4O1{dDl2u#cQ^X+{&^!)2n@r zmf{i_kmFgs78PMja1-v1Hxp>2!`NDYmjWy&Wfj#@j^kev2ro4+=m~#WA!TPXzWL74 zNgBA7RE1~<(zItoNg)}BXW4O zT59<>THLwSS5+Qtj9YTlP&RJRRY{<4gbMwAel%p1TjMljL1r&fIdhjDRhDy~Nccww z%Ql^ZHQZ5v%o^@L-;PA_(>jQO9oz+jq~w$V3sc4`XU;^8dTde|Gvi zibbVjP)4dGwVY(=R6C>$FWvKMPlF?;R~v%bkSr9oK}uBOc-GoJM|x_(;47UpDFYH0 zPo(FI%HE#O$`{-`B!ZLYr`|z3C!g3?*Z^ZrE{MhTzTVhOffZ$h>4ho_}?qR z{9h=+^1o6rvvB<*1>1j;A~{az_g_Jzke>;>tVI(yN?8)}{9q$w&+-egyLzV0MuSzO zR-+#u$}Cj{#B)=?Ef(M!yi#z5oCf#^oEBqEZ{~J`zYkddf-X)s z^moiVzMYN$Z+Q2wwJrZ@6O|GbmBLej!d#2CHth4R_Wp^kU732H71q9!-?q%da#+8P z6*GPlzR{Hpe>%LG@t=$>>66Dx=Ffi9{2Y2-fL6m32Pgr&qlytLqw*GJ&|?7FaY{x6bAn9^#&6iyQJrvnTW+2IX`R*-lPY6b_MKc`SucHu#{bUn$ z4C+h88o6yF3YiPmmd08rlr(Eb9Zip^ih)EE2uujK}q|$6>%A`7A*TO15J3K~)#DlTr#Yk$=He7bI(o z7{Ht2+cKtEOl)U0gt8-10E0~_%qdD%8@5A`)T4s2)WTa~P3z#UFs>l$8^QlZR|sT} z|24Nd4$%Z-1v7@xq}9P;46og;#~6+-V_YWK4ufyf1C9COclXO64Vklh|Ofr-hft}08%uS23i#jiK!Ou^zCW&?QA^h z^Je)T^e|M1Hh)N6{sB|;_~-TcqOR|a!cP`!18`V3>C&=+VljA-W!d_r=$O-?i(SbK z;}Mi;3X#m7ZK{%yci*=8WZSweZ_4i1)wKoKR(tzdd8o3?(r@3PtWs_II>x;`JY@U! z0`IY>F*sYBk=rqtTDM%8x>!c{?dA7z*qGXYV8}AOkNVRh)|Yp42x#kc74&jQ(EoH5 zJT8FB2^dIRpQ4VRAi`A&m)RtMqK6>L)mh?L&N>$OwD7%~5%7LuA@FmZMFi%j;Yis` z+fi+)F*^c|Y_qWCxZ2!S!O+_+b%q`~Iw78m0(Y4BidRVdGp7|l5eWwZPCb!-vVnnZ zt(d3bEJ-Vpd$ZEprZYW$VcMak>g4GAX4l>Qtzg58Ec-lGUYAP|9F|2LJZi#tp6Don zOvyJGkvQ(0NAQc9w~&&T1Ug7E8UZu zl*r`I;{ePU$zo$%5;^v`XS4}*aG>nyn1B6|2^uls0U53%a|G($Fy#oBz2YN5eIcXF zt)$8y`dlTgKlY;l5vdKx>Bs|Kw85fT_y{+mfsRhBA%z4fLLbR=LeYF3lgN913Li2G zo&?U$TV4&hliyp)Q;z3M4rn3{GMlqT%zsI$uW>LYNG#0~L&cuqqGWI|crpyvNv+|3 zqZQyE6yrHMhCejx(f;bTR{VtIbm7uUc)|o;K3_N}bKJ;mR@`|MYC@ z{I%MZW!kZMJgwSP={vqEm}Pfy)GZ8>@zZJ5x(p#Z33I0A+d?m(GUj0I+o4V0Fzn&s zqFVFfcbqoatwn#aDcdt%mF3aVF;kWD)1<-f?$E1|Mj+(dQkC}IGHKb+{zh@w-y4v(Asm$Xaozz~Jl4G|wp}>tZ6{yo-;2fg9PQYo z+J>E&y=@9s-kh+7pE784uou7BR{YdU#hvaeR3M8Wu9s2(x5d9Ujbzv~7Td4&EPFO%r2t3N>)RC@R- z&wPefR=KDiwM}kQ%vpZq{b>RnIREnxH<)*G|7F%?ld^NElbH6=+F{(|w^^OJI%%2_ zbQ5;BHY?kaah2*TveG%D?{=B=`bc5AxD{v-OzutW^Vro>@r&E`IYO6 z5Dn7HY=uO0%?xcDj_96ld6I0kObdW#Ep1FgB82)p?+R{h8qS!O(guRgu>6Yu#@y00 zA^dqH1$8pv=hu}c-&MU<{w0G820?Pg7?t~%RtZ5l@=Tbpz=ZM7FJ|az&hXsC zKtTo*3u+S@E*Rx+zhLxn?4R&OGKa`Bd*Let>PCMgL?sg@obe4}nJhGI{(?1#Ve4V` z()1z+J*J@y6?rCIYNC24!urwm4EnRR+%GAX1sRBV%;uy-s88&3(}Y@=Ip2mL3Z{i| zXUDh()d?(dDTyk4R;6o6(v+l*-?i4Gx{jm}TGWe2Rx5S-O(qU<^JlcP1P6e7QKndK z6pRYt(gYa(oGYegW3TE+745~$3eRp8DSyk(UHV{t8+%Mxp9NF3ZP0W$&*IG3AmURC zO)PlNg?DpLsHDAElf@21^F1N)4!Th0UlPBLf|NWOLusF!0e?ldc;YyR?vy;L)pGgK z;uk||0f1&b=0A2rhmf+lnFi)%HfShP`ch<~5@WAu{N zV}`zr#|#lkTLL?Tli*o&W_?j`*;xvjrjsLuGiFOg>EKNL&_9?w3VcFNy$iOsxMo{S zCjyUp==p+=Hp8>xtP9J%-zW#kgJ(J_Wv#N*k+o-Ya%K4dPTeM4+VKicu zA-60pfT85pBt)nR`ax-h?=sp5PxTK$MM-f&rWDzBPM7vRy+qi>p;F?YlpmovS%Sy5 zwPo04A%MOrPb8<)X41zvu_CFR2f13V`pYYcduKYL-%fV3ORF zzzM^kowB*zX?nW%Hp$k@u=?7obMWC9bMrIK#?PnARCo$ViR4MUyRkMR+#u79&Ny`9 z8G_L=U1_}@MuWmhg2qIxG`k{JEERtxc6qKqn@+J$ZsCtSlMV5TIpDm^(SLgoqJ4 zc{m=yCR#I^?Kk}Rpv-!aPr6*n%gNb}i-<4UOwFT@a+q>a9|F5b@+TZuxLdz-W`Jcm zAbItdAmWiAKu(btAq;ppl^Ak7BsDth^Rrv{~JqI{mz?tFDL>Q zXhM(tZbeBE*|p!X(u_!K)ZJG#=^3D^Fnw!Bd_QcoATbiR{7f)~YF4CNS4f|Ugx z1yrS8U(LXw7E2Dbp8!?>K1RvvS-%UO4~?k&2T8st?0ZdInq_zo_4wgQwWIP!o7+tW z11e9lhvdln8ui0bD$~}lgZSv`sWh+~|qP+!1Z1fT{OPdZt3X@T02;#ot? z>a)_3)SG?(tG`r*vc5XLCH9zQ<8Ed*wdts6bEAb#R_B&B=^3g^hclGmtFyU`y87aG zw?)^;i472V;}q`pdq|*Dolv#Y0=JFgKn6dSShgLKShcOr z25LH_cT5=Je25NSed7@$eYYkFiD_qSG3_)r>*W8s53^(Vc6k?&WSBhC&tsc>6QI9e zv}ADt4_{t^Uq>RS^n?^9@)!b~Uv2CEeCMaiK13aFSB~UIzB(#)x7rM&5ili-^dz?| z&xI#Pj7AphlSUSlrh`x!V<#5$SW6XngY8~tz6gx5Cm3gw(l$``Ghv*cH8g16-2MpR z1^Fn8B-6YTlPO`NdS`f2IKYt0G z4Cv2L^k&O?b7j3cvfrKXW*I!XX*f|EujsVuqf$zfT<(RS7gIDc*7_pGxXdTg>_63T z8zf2gdo|Q;n*se;!(#cP4OX0 zB1z@v>8m&Ck2M{)*~8up7%zEX=0$8n7XOf0>l3#w5H8qXSbYa}ky4k)l)eI46hpYW zI)Kzdq`zKiW{X#p36D41Yn~I3n}zTsL>3i`X%$OBDPbz?f}Tt6kg6ywRz2q;c`e5- z4#ZZr;#In$Fco<#jY)eUtFwz%e5*)k@(AKpK=(M_A!)L+5JskjTi2>_VD+r0Zf^g{ zLkSVO$!q?5|KZ?)_4<7_JKnziiEWO0SWqIX^EE7yB=PNgkLJxhIhN74kJ(Mswv~f4 z)RQk~Hht*L=Z1VZfJ<|ZJ<;u#AgpLmEM}3K3!u4WgSZc z#1}d37(DIB`woncNQ{trF)R?hAXoRDj2u4$MiW0CCBqT|B^hcK4>Jqext0JW$rtS} zeyAgZhu0k0i-@rASQ`tEuw-t3tAS>X7~Seh1b)sBmzt*rMaEv(Jk`F71cE$hWG+b@ zwiif%405Lcm8k#DR9o8mywRS;3xF9DHnT*!aq>+@pCeTxdUJtMIg^in_CN`qV-y16 z#=$u^G*1_wqEz>4_?cU*>RIx1=L&Ujs@}*_Y~irD;Ar`=8b=}%kOTKhKFmTMmq+!N z%>llFu19o2L8DSKjP*b|P9NR+?16Se56ulQD?B(B2nvCGV}^d&)N-$YdD((wIQ=K@ zA<}&sgF(GWfQoXH0o}xh`MyiXiEfA>dqM~M2y<4AG7C-ne>esMy%PhA%Xl-j48YRc z+TH9!~ptq{66B-!Shs!!S*w$hi12BSRg_&dSIE3D+ugLaaT#<{3?SJ433)lZ6cUQ4P3KsQ*2+$lu@B+O9fA;iKp@;`MmRWOQzB(IW#Vgf#W>L<;I*kY)ayG|X1alh43nC{bihxL9DS|+w7)s}- z6(b)j$dkcU)yoO;m`EEyZ9;rI)pk{dQ^6^hvlc>)5W$Eg8&yP+PNdKiBdfxR1d78c zZwHA*mvq8cYoz}|W&-yMPedNoNh=R$NzW;@7y>Vf7fg^uhD+U-G?*Y3A}OCIXGKB5 zSx6RRK7vExO$0kb0SbcN))hxNCn_K@-4JXc!Ky@xL^B3K;Z#BiEn|e2R@ML&LsJUA zP=iy-wi4zlMiZ1t019VX2(qZJ@CPRq#D!jig6StJ1|o$CCW=mwGn7>1BM9YUge;`4 z1~wcvu)#Ve(uj*TL=Xm6j5I`8Q{F~61}b#nNeBIvk3SVewa5oGB?KPi0X;RK)bk6( z3Fx>J;}~ep1$$}$Y~GYHEFcFZJ;->CsHdl&M5Qf=t9=glw;@8p;EW=S6I5YC8DkhL zE*LHlbq|I#o4Q4Gfk0v=6IHVqj0G#zQ~_%w+DZ^w7;gv@Fi{LrHEx;Y>?Bh0Rox!$5vp+dPqDqATGGyA?S?I)TR?E!A}AP66v598C1~` z4Ff>#@W$YHQe0C)j4RaXpy5l@=|ND+5KcgSgO!5uL?ve-^GvsJ%h3qbR58AvOkAFj zegKd#Nu-$<20gM-iP=OBk2mx!Zj{2=YK`51EMr1t}<3m^X$^g;(wf z2%lf{#HY7%<%!l$|BDz7#byAL>WyTOY*Azviu^Io7&=redqKA_!ISZKeV;$Mpl8`A z*v{CsK^ptU7GjJ?GIvVXp# zmVNg-6JP&%z-6u5I(2;Z$KOz|_dR*!?BwuBJ}>^;$K_FmKaAKFiIf%tf*GCWYchPA z2D%SaOymx!U7h3Y((1ACeG#NN1s*Da8;rOyvd9#r?wIIwK$z?sg!o+YbLQ2#{gXx* z1DCd9fZ~YBohaB~j&@@^{0s>CYB+cgv;`NH=*03# zw5P}+g0LNpJa4d=bXi?a>R~P#fmfmqe+I7&#yKKW41+}Juz?JF5|jR3p=xMZw8+&= zT^rd=fCQ-8oTMW%GJYMOqwDX{y8qe+;WQZ4AUw~}20BsCA@u(%u?5m`g%Qozlf8Dy zMPWXjJ8CCNoK@Cp4G6y=rD*k_E7Hz7A!gd1+^s+k;vhLb8!`fDc~c(y^2t4?ICK3v zBX!Lq^mi?zT^S%P+J?sR3x%u}ltAZ|4#NQ?gNLgrSGoUg*!%z*x8CX5De!fB5D|3t z%za|zlIZfx9oM^3#``Ic>`)M&EfWq44VgjQO(69I5I5!^-VO?hLL*&?DMFRxQo$I< z*?S{;vf-UJ*9uG71f$2ACc&i3js_ zqJ*{O5cmhw@9%{_|CZpEW4%YhKX#cYWk*WgUlDwj68FM|Vznh26tw4Und2dTM#<4eP8x24zz%}!?o8mL>ngO~Ii(E&=dxxT6GPFd6 zEtNchJX~Zl1G?e_C1tlgH!o{)xYGNG$%eXi>FilG{5^s|e0dda(?cN8aL%MB|1MZ} z!jG8o;%z|e8Y28?d4%ddrctTUS7>fZf_``F8C+&e65u9EV*T^BREh+eQOQGTEXA8f z>V4zHOma{f9~{65Br#@ah+hDNc2KZ`|9(TNRaoJ+)817>c=+Nr49JQb4I@qfz$yYtB#f(yTS$-zW_lD{MMIn9la*?@`2*k?P|}FGh0K2 zI6hd_4)=8ChDV)7?0<3Mx%BSqM-Vu!T($ZB?$uA{Sz1yDi)GVV9i7GX(ucUNAMbw; z;R+=bj$YF0f3Dwc@yFu}HM|AI{xhVmrksUvLAP!Yu_G1#p^3ozhiee3T*STPe5iDp zPOkwu&cpSD%-l~6Ix0E){T`jVyTg0f-ToHCD|BHUtEk=!Pj=bMkZ8HKsN%F#?e6Br ze9+NIn0Ojy6t|}Ig&BD?i^tV~Prh6h;1n{*r$>Q^9J5#syx+}o`C;S)E=~Oer@#>F;0F~u%FUNX zQ-1#1&)r%_#>alaiU_-WAqS|KzIXNGdfeDDfZq$>9#$41>6GWzVN{NHbTkK*+t+$eXr# zSZ*JW9BVbkVT;?VC|EXI{L#7lK$Bu{IHgbTf_=iSf}f|_(7gwbF`TbSH_a{jS8^Zl z8n3!=|L|7d9l1$>+zlXNPvu|##3k#eW2?h0@~5zG{+2i)aClA+!fzK(s9CXW6P~yk zhV^qrWP>}wGF#EF2I@;5qa__;O{cQB&U_D$-Y^bbynWmcr#4dqM2#V*F7CnkNqefD#!n@@ByYh zJ#v;W#4>8KiaTpl-&TtuniET@PCP_a?mx5`AR{MuK}2gWF12g>WM%1BO`)Bqn=UT8 zT?HN{na3v^+jY^a$n>Y0mSqzere{?(2l~fVbEe@GS1$@#|E9jFHjOV2KglN1Mjm;l zY#qBeDc7j4JKClJ|JDCf)jgSBYcul9+9B&vHwS0e$>G92i>dj=*73_gcU(ZdL~n0h z%F(vq-4p>}R%`LPi~(d|iqI9G+-i|RNu&Ls|6rWkpHqD?!JC#|W7PxhcAPV2s_QeI zmZoV_yV4b4wtSVNtD`zw=SbA9aZDktrnWjvOJgXJg!Mi3P*prr7o}PRPNr$X(v<(% zIrDJXs#8a+Hchx}3Ga`tn#0PWuzJ+Ej#F&iz>W|}opn*eQ8az;p_uy55E`H*?1{E< z*;+RfJU~$7=;oq2lz`hKDMj5CLcN_i@_UR?+|k60=bULX!La18ob+pqacy4ezB1=~ z$d$h#N8U3>zEiKPb-KdFX~W8CqlL@DHnGjMlV$UBie2D>O~47hQ!mP89j)z1*YOX> zOVHK(fa}-X#e2cULsU(7dxfRJQMVpYoL*`bS(6WocFxoLAJ>g)ToM*W6QW!?xQ?sr z%^8n6C&1G$>#V_6YEPTUT3G}}+AHT28@=^o)jE$O9Os_n;WlAR4WyOUgAG@m7w+0F z0=L#iCZesD;k+6-%cok!-wZ;R%`0r&9CrSFs5U#Yt;)%rxQ#h3n^!;>nPjN}%{J=H z$E-%l>A&H@*zHeY8td0T8lCjn=GO5!PZ)Oa@by2hjD0Isd!%C@mSwDFO^d1qHrr;0 zxs_18RBW$(-J8ITGIdl4Q^8q5fZ64Ypq6j0#a-OFi_3k6%=S!y7~(`N0b2IhWinzQ zI`A^Cc_{7(^S+P)yP)<+oJu_};@OpZUZhV3KL&oQ2B&De@EGsrT)d(<`WaF!9&RHp zGWI}Y?Nyg=v9zy@U;@i0E5>D@qoA=y46s{H;OAVp!PI|H3E}ZNp3QeD-DYpU*9LZT z>!H_q$w%B@^w5j}RX%~U5uBwBCb~<(%5dwt3gQRGJj6-2S2Ig_={$F?z=nO^H~5GI zejEZMPtHcz=8E;qyWoa>i+5{Y?a1$CzB~KQ{!t0 zHk@N7HIx-0qeu%;@>5v|kL(e5pxP}=9qtB8YxpAN7`=kEpNyebSxpVL+X_|Eq%6pM z83cfM$%l_I^K%3=w6egH#B!`>8J5o>qyWSEcc)44h)6rD14KRF%PTFBG@-Y}J@^ly zM4k;~f-S+D5qG;fX^$7%I~W29=*0lg$Bc-m#tCG2%y!k4w>IuG!s(Jc8NO39AYhdb zMlZ$APnlEr2sryQyLmGyPkwDwAoIza1sB7M`w4Qf#;EOhTH*?z|2xi)_G&;7ax~hz z{&4y1&I15M@ErZ}tIH?pVwc+;wF0(?Z5|P;Z2W?`OV6Uy|2U0^DNjkc_P}!}`e|LI zX9h#ex9)Crl|H*@5jas;PQ4_UUTAB32$*hst>wXupVOPZOWfZ#oh!fF;9_`WD zT}fiuhq6$Dxh!8cDqz;8JIBcNT@Eehb{x#lZn9>J`?yT^cz^pRVSH2`s;s{F@xjav ztx0>{7A;Uo*BNV8%12hpOdvk)_@w?rD?Mzjg zxIc!4#gMz|1Lds3^1RPp$j+-^(pwC>bZ@qq{)Ks77d9qL9VlX3jeRD;a9*k1-y1Q# z@|VZ!MXNuJWfF;#F`ft#kJ9%J2WNS&w|+YrC!fJ)wR~K6>bGF}_Q!b-Z!nBNm@W$G z|Gq?y|Dr@T#{a4DpCOTd;g5I6K~ zU$$I#^54*rU57S-x_QtX@A9jQSo}MmU2j9;-dcAL(%TH1Ic5dsXUqv()FDbJYKikts zQEpxJKi5xHn_-ChKPR`Ujv-a9GgyCfbnUO)(irOO%x~LP(>U}ek}k4n+KiXpzQZQd z)IZGRzrW-1dcR;fKF;?pvQBmc4kpUl?CNa&-dukEUbHbV*uBoj!BZMP$JsI1)%v|1 zFGpon<*WH9 z`99sOPQlB>kH+Qpd|w<$?CAHd(*(T4`4+FE&|mDOJKfGK?)dhCvp>>n0kO30w*A~0 zAaEId>C(gPX2$=e9~n{z|7vvNIVp(^$Q?%U^zu>PSqHg-^r0fNa$}PqunHEXjq8;8 zVj-}>t1UVSdEaY&#ItG9^{dlW-}m%g$)y3{$Aw}_FU67P9^f#}pp%~#xxmQe?eUGc z6(crKZL}M4)17c#WGettY9kk+SZq&#ZXgD&TIA-PxlJ~%z%bd@MD+U<&iDzi~FI0gt(rISUI117YftF z-)RwCSZj&DNiq_`F#?(uO3l*cnMJcQB^g)j_i7-G6|l84c#4+T&1rM>jf70V8i@~e z5AWq6YmE>v32=)Scp>NBfUAk66C0hoEF+dJxvv`1k|+G|A(;c*5M=KaZO z9%++*k;Q>{`b3tfS+l0(=pH*7MS0-P9YP8EGvOXe@>e1~p8CFf1+U-i`*ngiI?&Kr3`(p0;PDPj=Mh zWEvhWA0xQpqh(@VgKwejkixu1&WdJ7w>7yJ3m4C#_t;jMQx%p0n&hdREU?jpTbHKL zK?gC8T%b%5yDSIgEmmUH~t(ZcGAbZndrc>*coF}Mm zQ2;P5&VH3sM^9+I$)R;&1`!z<*n`7K?(0S6q^Ezk$B`upgF~T=4Yr>bOcf;Sf71OVnq;-Ox(oux2GbX%sD)haRo9&Gt{cTdN%D=aOU>M z-5DH=IcAwSmSs~?818N>yO~7eDRNM_aR9}3Hz}oEa{?NY2{$aZp^gt1k?X_MUl>={ zhMM4jK{~_>GK4t0?xv!%)Pu2SG{KHT0-aK`y3xcoM{HiJiNem#A z%U^+Sx!~+Srnhpfl(~-$v%nVEb1)_Lv+w*#&%o{|ivaLr1AjnKPAj-G4k|wI7Z>c! z+U9KYaQZvF)yT-e1J2z?ya+jTtuojB+0-~;CGOiNJjSoLkam zGVe@|#7s(5>x{LnD!*!^3-#!RYpbK9NscZTKH|Z8vk3K9*|LXJ$ure*Cq{a|vzN@) z-$7?D6dknCU$Xc+fDBsX!LInS#`7jn>Z-JXZ!hEc@IvjJ5jM zo*QDk$B3B`=eo4%`I;@gqIce-Yh^H@cnfwnN6emZuB%Ez+~OwP#an_JiKwv0 z_yvULb{B`bTPJ`(VZXw>*(-CqCfzx~|39_$%@ zC-=F~s)9f%_^sI2E6m*Bv5lQcf;W)z4&nr#3#pLWQ_c-ncRmh9DYg;Ok+uQZJubT^Q#t^y zc@}Y%aOBRgQAjxHA}$L$jmEhEp!3XiHd3yTB!&wKI|D+pB`-xJvq(+IflJHEHi}`5AqbBq)SeYD`mC?1- z5j75;A;mVHsgpY^l`@IwTC&dR$Y=FL=kRUmwTdjdV95n9DUQ|O1L1KCticT?e$D~L z5vANDHl{vhO+^w4#N2K8!B3kDe0$88VTmc5kdH+J)CtXx%?pA(PymQgn1 z1m%NBbW+gloXRhycpa(}OnTwXx;~p&2eGy3{Bwx>i}&~A+wo5USF}C7{sq*}Ay&NT z-w4*lF6}9FB#7B0aDfI9qbtQUFWHe@2Rgf$x&AhmO*nK)qA;B?k|k!?T)vaZ4tj($ zAObOdR%|2LgiA57wo!#(ZYHuAurg)jYUEmeb?j>&h{6)H$}Nm0W^#5eS->aN2-Q1-TamQG>5Ge@6M$C*L|#>_5Ui$=7E#i~0GysU8` zKKO+KoiK_T(QE#xhGySy+af z#kLknURjYH1L|;Q!B6#lB!Tez-#msVv$Zbp4Xuf&ot7v(OHmOBqXxpU)RY(@ZxDrOO(ZT6W?Wev$ed&t98GH%z|d z&c_asNG25~4%V|eh$;iE4wQ+z3*j)Lk!gkJO((M2&yiCzf{`h5NkWYJ2)*B};-!<4 z5iH;+?vTa#n?l7i_mjT0sQZ%}pll^d_Au2bwmhb~G-cR!8jjht=z|~`el7pqMaM7` z=FKSNHv6es;$%MDd{aZ5mtcM#{J=sarD@-@E+Vn!cZxOxliI)>4$)~lhz?1WKbxs= z#Iz+4kP_7WXj;4`qhW?XaOCx#v(@tNorws znulZ{$V{wn=8EOOgEZP7NaoB(LM|!xqQ(q-fdVw(#+$&NWQV)cBD*fomj7Ii+SYwg zJvR1-1}PlRz5kC45*WV zAF^gsC;a(el$_Z>yF;&VyRS~E;Am;2>Fk)doabH8_-ucV8-R4=`)v}AR?L|}nVlrK zW9C3PfG}6x16SbFLrIB>ocx<4U<#k0WSqbsH-R2Txc$FUfURlg)k|6j%}H zT5w7j8qA@$2ecwR-|{o31IiYn-oURK_q;}8K&?|8!c|9&eQ0T!5`1@*MlaxvGiMBDSRbtjmVYLVa_CrE){b4BESEYCBb;8G97nQE8bVg7 zVLel*Ky5-~FU986t^aJulo)}MWNLh#@79ao)OM`7<?6M@7GaWYbN{?ic}CzBg)0JcC%V(}dY<8AFzFdanjtCOtEi zZZI;+8|qam2OJ<_A(heN0RE=iOoeQDga3i?&XDbYX{V?V5cn|gAn6)5r7Y1z7-$fm zslW3EqlSB}-4#JH29;dg^L#Mp7*IoaQk!|q2IbpPMYHey~aFJIM}G}<&j#i z>uuS6vjv((zN+$WGlHd6=7f8_m3M_PyEtX*?KifiydGE;ef?~Y)UJaVciU%x3-Z=n z;;$o*6!fg3S+$DAJh&HEE2Kr!4S`jFfOt{mdy^lCATmPmo_n8a+bO z<(WkIn#yqx5jw=t$`|qMog&Ih)0IW}Ms*N!vmd4G7Us*XPOV-SZ)*n1PJ$Q1F%*F% zT=vn!PjPRL!$XhrjT!Co&GOC0AU}`hrVEVojpMa%H6{kk0-EO9{#gxk4ZCe~EMxGF zkOYu=VLdPt4v@gUkbDvkyH4#*MEtL=?hIhpbkw8U;MderY46EF>igdyFR8cWcfOMQ zzqz#Wi zvR{P>sBcHrG28?k(I155*~k3?S(SCc))-m6qN`TBy7?-SiMcB^D&{U;>FivY_Gx_> zbz)`Xl^O25yVFi^O@ygiUVfN9H`TQ6LtE|oYCL>)?%w@&Vm!e)(fqyke}Qv zxs$n-y3rsjD1_i~lhhh2IJrQw8|8oROxnD`IRu zTWNY%am!^L4aZ6`UGEcCJPP5Nu@jCmaz`|g8abDIdur1@TfW2LeM2z<;kR)~{`+3! z`Y(Esi}P{zz*!%lLglLLUx+Z zxgpxsN;Zaq^z%VCqoOR&;>3*jeDGnwCJ-qm*CM@Wsw-U;_5U&U4bZtY$=b1P)o=IO-Fx?{uDAQCvS3S1VWf(N zP<7F6^y?kTYUl3(gZ)O+!I$9$1>rl`UaBnjwj&BW=qOI+&3A&@8nZfPlC^WC*if|O zPcvmRY`qb+LrMI$EHXUDb^lCua8QAzwl{01$7 znwQ@^_#2hMA$r=cpn0%MD`(D7&r1G;fgVk=G8zdY2a~}AAps!|9lbv^TL}?(vYWhb zF3tl|u_Q$!wk*^iF9O9$C*lE#y#SOUf79~Vstm)MhDX#oRNez7#!~*A0jp5Aey}PW zW5tkt;Cua`-IZ)N!*{9^ieMdu!^Ur{5?dXV2Kj-!f7GD0Rk5YKu%SzWveEdw_3qEgG*T<)q`%=~S&5Lu9 zeuCZ7jv4pyHZ+~Y#SzQhdAIvsR3VQhq{{zIh?W0DvTA(QJ_hf7FJbq{r(_GVWm!v^ zeGkMt#(zSnJO56{fU@P+7HxPNrMKiE!qySNl$ypD7eDtp|~%J*$jH4-E+Nmt87!z!LA&j`mdY& zYUQPHj;)}B;=bJO9lheS=U%tz!(N!2I%fFd+E3^hi_T4!lB@VMKR5oDOo>OtLe@AIyK>keE9`SgyB~hDa6-<_ub`frAM+RQ7w`9jgS{%Q zx2wq!J%wnj?P`%&>OIQJO(i4x%Gwu*9j)!{o^2j&&8_S0ot-V~-j7#qw^^mp(XF~J zwl+TRPL7YSz4Pz*y07E#jnVMBuA86s_g77?*9VV9A*bt`HWgPNw@uznt)Cw!yf(f~ z@332otywR=d$60*Sw8PK*V_j^ zYa6s~rvTi*&!5 zI?Q&kq_y(G@+qOFTUa#28gi#{_2li)h|!8bYW^2FK<%LnQN$<`6!FXWrvq_-{zbtN z<4ADGKjEJa%mMBIw}&(Iha#Du)?W*#8N>l{4{3-bM)EI;G9eJ)=c=Dth0vRE3ytab zG@9_T4Ux`BF0Y){x`*fEIm#Ni6 zg}y>#Ax$Amhk36KPM>Wnw6`K=A9%Ah*0xb%vm++V11WI?3mjHjuhA@_0uBtbO|;r?{JgbEcinBnK3q3A z-TOnZUki9N{8Y3~H`txSGM#5H9Z+YQD6wb#S!>2EjU|ix)#^2~KDRW6tmeRM z()ExYia{P2_WmMyi)cGbNtvQ<#>feZN>K%;@qBTKjJ&f&iK0SA_z8+qQ6}g9Tydcc zgR@1kqC|$o35r5d6sPfAajp!av&C;k!3?Mq6uBZF&i&cqOqpvZiy}qNjFDp$nIaob z>*DfSZ^Jirw~?R z4c4L%RZ?&+^s&>p>!ZZA8f0<5C)D6qqkQfbQ&MS~qcy=NbBKR<8$J3wc&K@K|F&D3 ze#ph8+ds);bL#PNZLf-rZnE2BWy$d@D4RJKPewMzpE=p!I=D&%kvm;p7}gObg6d74 zTuCi|s=)4hj;MZuU_=j95KfRblnB%yN-jzvg%gFBqPU`3kzA3TETNo!wqQt)P!LMc zPbik$c#r0H4hZ{_)J|~ER3xZ!s4P?mR23=|sx_5R6^x3h@+&PuZT(ik_8#}3j=*QA zFvw3-N@O(ZKvFF=D^vYG12Jeiv^0&S>ZQe=*!~5yLTZ=BE92_!AeM+{cvqEKdb4gn zPVO@O*#MyJxd0&jg)h1IC6@z$dh4~bermcu*v!^?r}cJwGhEak{$ju2u|;et?A<)a zq<|jvYHH&l6QBTQ={Y@DCzDW-ZFV~|>AgG0WFC>9B$p5H;3T0=XW)dam-% zxp6+b?1c#)3&CHjji{bJ+=hCD*~Jn&-G&uf?8Oru>?Qn4#9xU_bnujY4YqM>c^U zZ`!VLq=n5|e^*BO=zQ$Xb8#M0hPG?)9%4gJieK9( zINU!|3d}dh$ZI*$#AAwKA~xJho}7saH)@!LYP^<-46~a@*4+DgIF4 zpoQLKci7SsB+&Rt1UPVxh0dyt-fVZ$G7_Y|_=!RA){=<|KU#4Z*jr!gy!28d`!cwY z8Qk7E`5gRX@iUlEsx5O79>3DGzIUXSmD=~%w-g~*XL2>9Xt8!d|Fl^cYHNQ?@66bW z@SPS3WXGKtvTsxzW#}WCj#YD7^X>I{@zdbd_}k0l{?xLN$@3xGr`zMp{f49M-I{Nk z=TqD3RdQ!XnU9*`rq+8Eb!YR7{=S#vy>rEj{y`^C%Y<@PBrlsiBb$BcxQMLdym7?~ zvGsu{@Ay5BG;7C%NLJb-^Pqd`Tnop{wm`~KLxlGy2+R$OM`=nz!o#Soh;Q`O2 zv{vR?_Te1qLCGX+#`yHWjMhD)s`g>Yr2WPU zj`?A3WAhQO7xh&ogS>7b%J?Ef%mZm)~$E9Q0Z8MEShS7Su-0NQDP_trzlxP+g zT2k`~r)$kjGpZ_C=_E_XM27k4T3E}(_kxLx-_93SOp8xtjv7Ud5~Yqs5gZG~88|H* z#p!v{nVOf+*hyobpcnw_IoY&-x^rUqZ-D&Rzb;Vx4akpyp6xGpKK^f{igcCHa9lQo zcT}2;`NWf)xUR9Dz?da4`1oZ^TPZ;T1e}`N&BKJ279OU>*~H-;1&jIOJjLjRa~lnd z;{{1F462_XE(wKTT$0$eckN-4@&1A3A|=%i%|UxFUso zlerUomhy*#nKHg$Y`uKVcW~SfWt3%T$WJD8U^Lzun6L_m(-<;HQ{4UDaW z3g)QrDwxm@xZSy{xz<?+YhK`E)E&SkcwfmJ z7@9r;2~7_zdA{rH8Eeg&z+I?%MVRDg7dPs0j+AvSZ?JZW8>0cWoUstGa>A52v;Za= zE;O-?coJ5L74sBk6_zyho700%4q|WsQY$@JZQ~%u*ap6kh^rX3dGLi0iA^_A~Zk6|4kj&VZApC0Rx z{;q{H>50Vlk*a9^ff=dhml|6kfLwdKHF(CIf`N8pM_+m1LLc#n`%8^yt*eYz9L(h2 z4!G8%$$uI8{^foj*9V{XGYkiWZf$-4A$n$E=i_vxuq%MROT0)m+E2-BYggz38R7hnU<3+mQ; z8eOONx`506sFC)$vVfVVPs@)?Dh^bvbyQ~AWIdq!mZfyP5kiiuxhLk63(ks^Ciluzc3 zxbB#8yw65J>bz0K&Esus$}>ro#`>gbowBsMivPY>q^2b0+w|vib?wJfasgIDP(c-r zN{_`7-a-!;J}5r;zf%^-U&zYq!?@|p<+zNldpy|J^LjCP)5O*5YH_PHr!u$$tK~$e za%viA6KE0bVm9xy#ng74%FhU36o3iPZX6(0ybz`flRz?_1^5nxAXR)=Sb|i6F*YuI ztc8LI*ywzrZ2ecgX03#VOQ5lsng>XcSYLZv$La33xBVx!_}*pL!|hkJrudD|Cfz6` zy4Jq^?CTjzEnY0T&H>{bdq^`a6RhOm0pr5%qVC1VQEIz(>tt}|H5%B>*!#_fjq=M+ zQ^r@`j%f6qSLo{-LGE^dPZ-ck6wWNx4%`m*qwT&sL&~`dpjAZjMSmBNli4oL=q?{z zH}*z-4-~f;(P%ud6++Xr5ix%;ECv{O;8R4ix4vfvUbRQtnfF@9&ziTo+`VIamxQ@H z0lE)VJ%HluzWG02n*JMjCiXAjoWCtiY+pCL{&C>8r^^-gRM3juI*@LIR0b}5j@da1FSDNEhXnKMp{Skf6Hd$=?ZWKt-Q2TmH( z62G)!-!KU&uxO$RuxP3cFt4P+WulvSz5(#Q(jeFf$GsJKG8?O$4JLg;D;UhaA#m^w z2k=D!7g!`@C=g5l)Qfa%!AVyrElXRL!BQk#xFXvaK&^t9GesC+p`M~-r3XEj^cV@~ z2&!Cg3u~W3&KhW>;4?;)9C_9>;H(QA`&J+vd-XBkVofQY)VLM=lb+&hP0tzt>(fDs z*-n^9_TFNt$+?>spPQORR>U_ z_2ku*9slu^0Lpb!2%V)!imP5jsq0!Vlk}9(Ll~xPEkmfNYrG7<8)aGkXDMC4eM?^C znhao3sD7|WwIMakq3Vfa4BaIsK)xOmEH@UfLWbq&R4`(Lnar>{Sy_&pvebdS)`Pn? zUI1X;cr(A#aEdOHcy7N+rEz8pB&OLWFA;RxjWyn_>+4ZhcKfSk&o=Lu;NQBfMSPtd z8@wIfpHEkIuYnJz9{0EB>yw*&HnNpEeB5pMd4%B=D7fqLMUHnn;gef3Y*Dm0>l~bm zo@gSAhhv+)aN*}Dtr!i~;qP<|KJcp)E^=0ROA^)b`dln5MTG}x25kL>IJZI^qRKXN z%M-@sziU(TJ>)izn*8jYPLkI$s6IGOA5nGJjzhTC*p5%meHR>J1^v-JJ?_jU14PYd5K(_m$x@yDNkfBy|A z7&`~k-@iWw2G&1d!8n-zuTz)_YTBhZY%uRsAE31hMb;4hX+?ri?RC+F@Pzp!%+}d! zg0GjJd$Z_n42tR_YJ{~A{e4XXpyzCm;>`>Q4J=xQqV*!4`H!s za&NTKDv>J*h8w>)?F!kny9CNG4b#UdGF5MLtjjlow=mO*-q0E`z4Vb4)K`Ang8$fN z*rgAE@)ho`k0AnG7K{rNKWQiOhMSbBvI^?2@`Hz_?Ezc@q+{*v$s>SPgHVLqfCH;X zMAJfP?g2`$x08c65BPy#!o&`7HPP)a5~pd0knIHqzHM)3r)SPg%}#*#6B7p{dAM7E zumMzzdjb;&vB4l0{HSU5dZq`LK{VSTNBLd@(SIE+7px^n6sA#h5%3Ad&&ae&~U30nesnb}cr(B|h* zaqt41K|?I<$l19qDt2SVAXX6vgnj^Dapy8}&1_*}PsP&8!opEU974cRXQ^5#g-EqA zvlEb2BI3koSE%cI_C17G{UrbKzG}9k`8^@%X#+GNTFZ6HGrrmq_@1B8|V7eX84qS z(%!SPgPXNoMjOx9qU9$(Eqo_W`{(ueYSG~&;5(9J$m$C2N9yYC7clggB!g|^0lv%T zWr08m7n08s_y8vTCCrr~w-Ro+lrg@9W^wVM*&|gUk4hlgQx+-}`mKlMM}I`LFM&J< z%HTKL_HA+h^WLbBmz(wdRW@IrxA)_lr|t1o*Uq-r`{SFP-oZw`Pa>x>V6r4+Ty`@q zZTz#UN(k0Lgdn$U__cwIu~U9aN+j)ZFnr|VSB4A7{v1aL9dNwU^Dn*IrEW4)UwG87 znyiC%2WKAg+~+OX?gMc2ZaQ5WuGaAdXW>y@F-|ew!PgsPT4wN4zH4S10R&z(v%`fz!FA+N}LiLa}~IG$|hAfB@QLdDkZo? zIjcmW0%BDOhd-D^LB7(TGK+$I6$uM>b|s0w=KfOrgJ~4xEB`6;D9Bfp_!ovQ(GgB{ zWcnfVkcN7~rELxM!x-~p+fM4HM zta#t{py}KodHjT!02->Prp6xqgH6_4f3bi&YVj{7+@XWmp``J8lk1dSY^NFaS`W*E zG1!g3BfRhhDWMAl1AxiFarozp-hYGK$Ii<9KZ_nc>;DJ2FEXXuMvDzb#8h!iAH37VeYJNN`D>8Uq zmzWR^)}dWhcK5L`g6POCoon3Vz3Sk#z3PFz+NPAI&O-8LNnZrW|CX zbwgJ>12m;kvjRFi*RO`gA#<+l3plgy5tS4PN z(O&C$Xf9io;BEtE_319bpMJpOD)7>B_I1^WE)!t*qn#Z^9D8))`)o72VtjnLPu<>5 zfUnon)m`fP$DJ3|3sxsL&*yk>OSzA?$H&ukQdK2wcMHfRl1Xw^OW-<*aB_^cpfhB~ z@R5|LDI;h6bZX>;krQbi6XxGkT=C1PV-rR$q_s?`8N+8%CZ>#ksLCdcoJmWW)H8-p zq*P36IOFI4s+vv}Hbu(#?nL@8nVE{x@haBd)yQR&GFHPz%xM#&G$(>aZz1bMkzf^28ohZKDhptaK{+@f4kB?^I z73`?VJ1Q{!N+`Brjs2@T*4GVgnr$>2Xtoi56ocFdbwbhqS%fF=6u437{6i$Vk?4e} z?)*dG_5E-|fZq||4h6p^!0YSHaQ*Q2iY`0~SG1?F>6er0S3sHUW4e~&M*b(E#f?7M z?|A(WVH?iYpSB}?UFfFRMzMh+dkZ)Y&Wu0h#s8l&{;ytM`|OpTr|p=xr{k_1dy=bs zX%Du1Hy5zCE}!?;_ikMHw!7C2EOd=U`s2Hk`4 z{oV5A?tDM1)8}({{}cS|-v9XryjCTA8NS=rZSrj=`y;-k?&jp@`Q#-X-}~$PePAQ@ z(4)*#76357wi)BYpRYv!4H5@C>;ITv9IStdu<(C7W&%}OZAiY%FVuJc{t$8~H7mq? zyDb{p^z+DbpnR!}&dHpLgO$|k{_yneX+(E%^TJ0Ji{tfh3@N(uZa4cN%pj|Ti~1=$ zHOevEBxts5Z78m-E+{~FT_rt%1sqy11~ggW;Xq3F3p+ic^EvVnA8xq20|-qel_iqYPW<+32a%Z7^W1$r<0F zIHHe(I`%rT7Y4vexB=IS(9y$N5oY)|3(*W}Ubs1w?WHg@rf8C%4EY%b5lOm4=Hwya z$*t!Q!B@5Ty9;;9bvUhd#@43usjmL|Ky?9Q8ZH?pV4LxUmZv3th?|+;<9VGL!gg!# zF8h(Jcmlt)4hYfG;Fl&E)<4lsd=4J9oiULTuXM=FEhMb`qHmR52(i}Lu-mp!%C-!& z@!dU97oKc$*`@<%L`^2H_})fhFS)}L=81&=JzdbRNq5o@zw`Zeu@iiVRQxg9imD=B zgw`)>5?oLr&=?|&c?Y^~w7wp41%2*9${M;RmGSbv_Q=4?#Fe0EmofD6%6vn$LX2}T z3jDY-RhS2N?#Cf(=WfvttgJOIMQs&Y@cWuGro!jwYvrBWPDA86Z8^MUUD`phABUCw z#a6tu6Hlw`$uxB%?YcPEul5wLZD)?88a>kn`AoB;Z{}PI=E<2OCpkycLhWAXJD-ZrFHD?l}j&Bk9o zo>aLrOaes(MiP=gXG|n4iVCxN85G5-alih?ut`3V^#JVqb@TpwsrYXYahMtZZnH3Q z{DEZsf8HZoi|@ARrH2XD6_KUa3`ZW61JV?={Sot|A~CGM1V#iG2ED#2`yIF-k0@DC z44(Co*8kJ^vhy5F7`{pCnZ^_1&d@ZP&%9qf;~9hEID_J(rT zZ8K9TiN*%wLRA*$`6a~*8aO|UWGFl~m1wPmuUpU-aC?*bXF@pHV{)P1b0CXO!K19- zc|#?ZE+?+#@M%XUjuNIwICvmfY4yaCm##Ca9ap}G&b=L(h2U(Rbz8&afC`OO;@e$@ z&~xM_v=l>5J0nlb9d~BS%%mxlf?C8yi{t9%d?4xpfML{H{K*vj=biHZ#K7TT|3}Ch z@vwANZQFHLB=5({sl2+Qf@kSrawuk(mQ$edLTo^9yVtEHwxEasb8FLx_eZYY9Ab&& z-VzZ%EnQsL!>g+?n9I`Zvp3z6=bO>nOWMjxtYxqJ`O?zSgR{4r46BRGZGl^_M>iMV zs2ch4r|mO>po9j@ORTp%+W|f-V^HqctLTrJ8OXylJy;)XzRdNy#q}KY5^u-Uk0y_G z+(?M?v}*VJtUGF+mxGr0o_gDlFx}UN=jwAf6%4hVqH9fw%ECL^!sH$2KWjRm>;0&(p2e08v z181eDRLoW?kk$pdlYUvgc%8u!eD{3h*S$PGW#DZ%mvp_lnr;J}1G-&}ZZh1&qhpQy z!iw--F$cC43VxEv|L_=FUJZM{G#!9uhcyqfR)YBUJBI$=cj4^`;bmx zQ%?S*xO>N-AGKl%V>$Y?Mz{}R!FzkLoePxOzNa^aQ@h(-3(YnD`Cql^uVzDhjk3?l5I!Wc2aAv)Y;e5b zEX~AR+G|j%e1#pWs&4jfjm_z8Gu`Qz62htI)WUA6R^;doK_OfL0c0XUsmrCH6^(ax zo2i*7M$8Pfz!YZ~n<#YnCO6%;GV4s-hT)+!fK=?4wEKM*JoFs`=6PM&sk45YW$DGN zNVm2J*%JEP7ULSWo9C=HYQx)pVqxD~qX}K&8LoN1wQg%XNWa>uS^4#ED0Y4Xt@K^f zTB2ONcT((MyK{X@q(k@JPn^fk~@}KBS%DJ-f9YB>8kaoU$RlXw^`~Y z!Q0s~`-1g`T<66WufC?4HlEB;j-uJ4teFlhIQCUr!!oREs@5K}2Uaqg4FWUWaXQ2u zutaJOl+Vw5M`|ETW?q@!L1xW8$$69{q$)BK>p;vI6ZRvLW@B7=vo-re^}uxov~^BH zW|CRj5siZ^x*MtSzfc9@7)+ADLLHx?R?KV3O*SH$fY^Z3JUy@M z4Eph-Lj2TK0$S-NPK!bk!&FTWv-c~GQbT$IM0Zf$3>XAWqrbzR=)9cbJWS07*h@wU zP-5G+#bA;c7V7SFeMn>2QR1jFsY}atf^OqchiYu+kdhX=?2Fev*9HWRTC$a*>p(QCEiLmKt&I6!3Qa5$! zXVrYOaW>#>$pwyvc;Go;pnVC_$ZrXPA`1=*#F>@_;BO;l8c}QF8u%Jl5dglYHGioX z^sXMmZ(~N1f~f(yAB>iqqSlR+D9Uek&D$kxlV2`F&9h{IUF8{qCa1p&p#R51!$=aC zE;-DIi}#8Y&43|kHBh6sqAjQ#Ccz1b%x4HehgI=Ge~lt*cceb@2xgL`R9Qgz9grZV z{%9<=cB!MJr{X!N%i)?J<(fBv{#P9hRx2RPUdHcX2wF~0tT9st*m~dCN>oCU8ag(> zTqbDpdp@Ye2`tqDIC_yru#&z@m2Fiho`QlfB^evAov0ip2UF3~olk5B2{%y*^ss+b z!bswusM^mL7Jt>i4kgbs=Wrl0_qL%>wFb8NqhBj*f=IC`nTPqf1rH}(#X-*7+P|e6X$Q`Djowu(!M4}tV3vKXqK>{ zD!anb5ck~s1u~S7)|FWRVJfHQ3zOd^;_?5jTW2#0{1YC%!zn?^v&jGu2O+BNu_+01 zwz{UizL8T2BJqC=^uAF$?Ah1U{pD?dkt8=%1LElY+Z3;SO|kJ`r??<;SNWbR1Pw{w z7mfIzUPMwA(FG3y;Xj7iQrp|xrZ{H>`fZ{)NU7+z$DmF;e(N}c<6gPvE?pVKlq|=z1}5_DR3+gzt;qy!elxu zui%@Q-vE8tn+S!SZ8fHGBjv1b8@R|x1Ye!b;51Sm8ZQt6t!=j%-iD;2ojs$YwO89h z4uRjUJK+=>(br{n8;2wj>B623OoAZWue?*DYE!@N#U)CVwPmTbcUuX0z9RC{OtI_m z^X4!2N!fdc$s55NZ(V8Gq@;8efatZSbgggzTA|Z6o|CP$+hp0_p7%m~xeeXK;PLgi zf~WCa!lB_s^Mk1kJYKz4ig}y|bU3VSJA>NbUqOl1_pzsuiftGXqgY_*_am+4uol>=-_VwSwcyi%doJ*ORi^4L)-Z9q_oW;QGii7T{R9V1ab#kJFsLPi;# zSTu3r=Q9qR{`rfvfGG=h4`Wz^2)2R#mO!iWyhYK|6zJJ&e4I?Z>ICL<3``&iESgl< zcAc~m!;NR(l6QiljGnzVca6I~cWutpuaQ&IvL-9o*Um5R19D^L*q_+5|9L<0cYBtZ z_H^&L)AMXvL>AS4Wg4{D~>}sNrv2h_35EzkRW@3dLa--)L&ywV2 zu_z>nUzjfU3eFZ>-W}=bUPMc!s$`dkwsaV#rlyDJj;tK1bnKdZ27WvhuJQ5vaZKGw zZ1I?r9E(=XCU^2x`T_uXKDPcTVo8`08L ztC*zNEGG;$HjTWIu2mM*muPSrFW=nm-FxA?~rzS6GUh>rq{WOv} z(E^(l$sEtWUFen&xYTB4GFtb*2Cvxd8jxg8!>dg#V0-n${1vIT9{(=q<9Ooyh}EIP znmG6UI~|wpuvXkWYvX;A!k)@vxGc%G9EYb$H%uo+hTcQNuwI&~Bl=sz1dO{iHdtlv z7AedCh1imcdySyd3?iwjacq=Z_>$Y}^#^Jwd*^!2q40&#-H!QwKdb0I6Gqs`C~mSt zEZ3@^==8xk1PZR1pVTj8Ys+jGz_~Wr_%t>$285*HCKSxXMRh7SCmy16flgG|KyAd2 zv{N{K5uK4-a5-du%D|Yb+}NvOKEY{TrhF__INQDE^VPk3!SrDG0ZKmT77*f`bPW++toabrd*D>Vk?~9krCwv=Olx*gb?9l4(R;VvVd(K!zYcHQ4JM{lZVV z;izQ%HwO&VektK44Jd{LWt`RiT-pV$Mz;{ET(K~b^luRiQ$HXQf%YGY5Vg%#^BD1B zV()|D%2q$}iG7L6RRd!Hv*~>*VWCQD?u-FSJi-8f+)mIYLlsIZt}QFCje3_N+!Sol zk+y9CB_SnhNM=oKDBulOybK7t?r+#`H!QWR!y+8cW>1=7DPOx(@d++by@apMUaN5V zo>cgmc${)eX-#P!FW?Bi_PsSH4unWGdc%93GS+eRYy|k!oi;;qp%M=dt{UqiJ{SJ` zvzcxADQS%#=h{g;V*L(uhH#S#mAOQCl5jsrtb5LBAUeaY;lcOtgX%gQv|uRKZy?vP ziBrv5CpkB)ZR{#lb#7$=Khtp{l-gI8Y+|uO@(6JUfP0XPXd*Rq;`GaZisef`D&YWt z{#KRwJ)LaCFpsP$qTTN{PXJEI-oHR1D2d?BWAz(b;m(*zS6L- z9ZG&2cSq|yyo5Mmo{61!a7KQcIq{1~YS`2~bMqK?+!b{;YHJ;*66`J8d>Dl}6adL@ z9_ENa@C$E4E*+k#iH_ty%d)W&DttSJ#|+6u52yO6n}Q-fi5pU0W{p#7Tgsn!6^8}z zbqm**GQb?X$-2cO!y~#EX_A9dA)!z8S!*Nyd51EBxaxZ4cDN_{H z-YIXpyB8k^MqN8rUH$>ITln~Z_QCu~E5X`JvM3C4Q`dOV0FpkZM;I16QU8)4CaC*; zWcIXSB$BITCZ|uB?qckd8Vruyl?}hG=?kQSIVT@T8EfKnEnAvmt6#}y*0}AfoxzVP zGVN05sm`$Vy_MVLu&&MCu`5i1IrQzD)BN5sdoVSr*)<^gnny&d~`GWEY(o|gIztfaN)?-0R!VR&~^X_zJjv@_xEF3oT z=e0fru|UYgRXK9*#!7_?DVa48TlVhvP;m;nLN~W`e-MXXRUTfy->Xl(8a4ZB_`Ym! zqeFErF0K-J`hEwzY|CkZpcb9M4GEXA*|WR#A*DYGQ1SyM-M`?uR_xF>@mCMLVh00% zAzucEAAEwazotOu4F7Rx`-|77dW7e4WM(!{V=~GW9llh1pST~p1C|#kZ{`3KJ*%;z zuq2mix*r4=CQuN|6&t}>L74M$;usY?ws0^2Z*yuF+(T<>SIompGWzoPIu`FU5Bvi) zb*ucLTgdshQfwX_WfWfS?~+$iI%Q!XD6{B(6YsSPS98!8Y?T;84NWONYc@7OmKuzy zch>0|mvGs(Bw5biBcvfh5y%xzEs^2dj4EhI^Qt>!Pj<5ha5SN);R#bl`w-ufqqw47b(v%GA2R(R0oqGRKOw8Pm-*VTIP==Sw> zqX3LK!dZXL@wbFT^dH9ZOzRg+{?)kIjd(BgQgkOB`lUa8;XJm27VfASt~#wTZpwg_ z76H+gU)q43CIQhho`c|~P*aZs*(`_?s@iUP8W9^K#Y=p7FM6jYUV-S*0ZkHhSY>o= zg8_Vs8-NLQ8WOFD-t2FDzd+R6B_*lK^b?->8tTluRiyQ^#4tgEa2hKv6T zxDCb;=+BPr{bxwCzqQQ3&h$T_!5Iix=vi3)r7I~FPbB5<*>mmz=DtSioI9u9ivqX}4inE49L<}L?>?K4t)du&M#8|Mg4+kMiSrYE z4bTwQpG0R3=Wg~{bg$E%li%uyA`uoBmKW!JgA;B!n9*4w#OuBR}TGB^FT zJBz%1vrJ>w!>81+*B;xaI*NcdKOA`lHO;R+?Kd}~cuSfeaMIxkj9%Hkd0fGG5qiM! z-12r~jTVSYjSIn)%MW!;V~HY7GGgUaY!M(`fu0Fu#;~J~3onMpPZvs|^y|?~bjhJU ztiClMVbJGJM@F~s<}n^I+)OVb_AE<10IK(SL3NxcW9qks%fShfLjZAai}j7lMaKwt zHmo8Ch_MwchAT>&yLnJ^_9+Fzg|n6R;6&*`@qHG%Xc=@oR{|3T+`&{ZAR}}F(&36} zJvb`=*37Olrnf6Tb5Ap;LN!0 zmAp+gvNB}Q1D(PM0W=PY@rnR`!LE{Rj^E3P5xfsuqN-ysrH#>EV?a)b(mx{zr7KTE zEr#)HAe60nT_@q&c}X=yrhl&+z>aLJA0-MTJ2ec1a0&sSa4&54pq?nB&1$S9-*iH3 zA=2=$@JzgVYg^tnF90GcU_@cvct9n<9hi_Rw*MEev5-|l8dKV>@uW+&C#2(-RQ5kd!!g*OqJOUZn@r@I}s@})P-G4WEhcWS5lA1?0Fg#5mqq|a1tI2=2v_4 zuf8ji!0eb0j3S_H({F;=nn7IXugvsqT&4!_HP`VE?R0L|)b2HG0>oc{Ggqy*KS!u&@x9*J>}>OTrhT4WNA}Lk30i?(Nn4>PNAC=1 z?VPV!J{!9;#0^;l+%@0Q9n8_C(NDD>Yzpy6>ee1Jg{fYZ7(;^|L3jQr4-L6vFJs$F z(F;@weA5zfc>3-WNv)A}kiT`TV1^X0n6rmWF@!28V=4D7b$9U4r8ylgX@61jKbM#iI7%z>#!e82W9&K&nfQY zXZ<7FGieeFmv-46rSWKneQ_2{Tm-hx5UK=!h9cMxVybrk zQ~=1b7-v98&9V^#Od|CJx}nXHI_k+@#QdI$2^N90Hw%B71Jzp*3~rtNtZ%Qh)9J%j zDMdt3rp=g9j!bJQ;A~3s`qX7mH~WIA96ClX&0#31b_;>hCH(~{ZR1cXE7__(NCOyR zBkz;V*SKspE_{a<&Wbh^Gqrc$jnAzmWJzZs+ew6K7kIQEX0y@xA<5;<7!g~NEvKf+ z-Xc{-*fC`%v3AL0+I={5Ran7Xz{J0yEY^bwN7nF@ooXjZ9cd*kEQpr)ON?wgy!v-3 zB++Cdm&xDll&8_Cmerm)HGP|}Qr7&zRg)w8VMkK`YlLpPgEbtd{^q=IRh8}YY+kqA z{8+mk(YExRQ*Lpk%EVomIj&Q=?Roa;%9zIa?&?+fE>R%WiPXg+Vl;`A{e*bQ^F zFj|3}tvHT+9hon_J-27I30-AtW%b+vC2L{}G#5Cc!XYPyrs%JmDi;5#^rLusnV_8z z0`B*!{J_!t=%T?MwMur+7Hwn75B8u@Yl(3CS^S-i| z91x?1GG&&>DxP3G&!W=VS;a2W9UtHEcn|^b^()Q(Z5MB{P^Z=hx8r`AnPd=})$t^B$8EaUB1y8<4s<3h>kEolUJ{ue77w6~R=eUF2-gcw*sE?DFMG?piU@cBrk$9&>U!68Dtj};EoiG}^Ta~f| z&sdP>qWA@}(DAvE-`v|n7&$k%#Z&gSdkgz;7m?#t2@ z9s!YkX$tw=?agCY$}uc^S58FQLG=fyB%1r=CXF0UXck_f5Vg&_M5Vna|sJP!6 zgjY;K{oiMUcRA8h5^F8G>RNEgK{+7=hN{k0MEvQqa;(F{dvWXSg&~xi90^Jw8o<~< zLw-0-;nf`F@<0MkG6@2SwSoRPAzHJ*S}2>*F3~xWDE63w`}7M|8dFu1V=gM(_DPb{ zL%s?OU;~DyaBt~(xEsskXDNhW&?VRkR)}EpUWpiA!rXXCaM!?>Ac1&$a~}Ifb8r&} zCm(l}i=?A7L#P(`EU!pu4OkzDGv7oaC?-V!b8SGQ@`FJ}ju0Nq4|x$_&|_FOhV$@% zVmE8^Ot|9FmM|z|p;)!!?at!o%0^vb1zgTPs*u=Z`vZEHU{90P+uQzz$Wc(`rInf6 zbYDkxqzzN6IOt1Thl}QRrl101Uq#8ffh($ZQDq!eZvD&?&H4{DjRm~jpgGv3?44vT- zQIF4WA03oRpd{=Snqenq;@8T&WgXEs_hg%KMU}|SH^9Nmaf%5{Jbv_SrnEl4(Bn*A zVubwH1d%MSyVZ@>7W|_$Sks&>FP)r|%Ay;|IH|I2!qDh;uP5w$VH50l{WrdFV@YCT z4b0GF7V|WGAm=rqB-29YhFnl7Q=eQqT5&;FdSYv#Yf}2EZsC#l1SV_ z4M+5)kG+UwU2YBENIsx^(@evkSbfO&fY33*_w^ET{ra7DpsoqqmVs{JMyqhFNjSEo zn9y^~;vUo$n*B&`gc6Hg_-}p%s7|nr&_I%rD}W514w*_3;Ctgu56l%!PF2f(LL7V5 z^`fc=_}dO%f24>8pP3A#NRFW%u=b)bSdYn=fY8!6Z%5=Cgr+P*M(TYw#=ou1H#?`g z@(y=taOnl(g5+D8`6Pw*uF_PYDJ5;0@Ce39XvA4C#*w0{q5VD%m6KBLQ@ygjzId{G zlYU@-1m4E+l);G54fDlF4s{c0L$uL~)QzaMU&j|O+-h5G8@40-b&Kvn&tHzC!s`)p zU=$)m1p#7!WoEKKJBSj}(NZ|>&njRyFdS*~h~5VUY-j!dD0{~kPlGm2x6Nry+qR}{ z+qP}ncK_S9ZQHiZY1`J$`))RS&dGk*lPi^~`g-R{Do^UJ>!$?N3@#5;lT8~j%)BKc ziyUnFn>u$uH5c?_Wluq-HmndnrYvRYah5y1YHnw-#v)0FdZB|QaxSu2@+v^os%N2h zbvnEdX9}6)b2Z>w)}K2&IG}*# zaq!k;I=4POmkV*(tNe@K+W(QdA6lS z0zsB7v0BV@mGB zmy(GF-XT9IQCPKZJ?|@l;+XL9ulYSn0A8P%KPrJ`jy>}mu8S~f=Moo{hn7giG8!@o zj-CR_lqqO!G}I~lpVZmRQIsKSd`fdddqxZ0Kg1?6tq5Fhrw%8$rg4EqnYOHC1(TNO zek{OyW}drtn3|fx5OOt{t7}dh6a_|MGpx>!-k=Y+j;G^7d*x6tw)}d|pfc*$95?M} zlZS8^vfk0LMP%={R1ZiIVqvN4Tx}xLVr88#I#!E{S3f?}2U1 zJA;gb*(Uy~?jL|4S^X>ly}-7Wx-ZXt2YUT7pD%BexDi!ng&k$lT4S%z6?JEC zgD-ti%N@r-?MM@Y#V28-<2zY0&%@xLSqwfl1phT5lx5A?N0}~XAAznQb(*Uwwa~v` zy(0K>sr>3L9g5#$y!!$CCBelYl+lTskL;tsJ(LN}DZjg>)b4e-r$5}N24U7KE|p~| z)lf9!BQOf2e*ILr>W|Y*!AYRVQh?Lzd9hAz8Q0zOtgy}1*RlZg?_@f(8&j_wY4d0kUwldC+_T3 z_s)qunQ^u7ed$`n)(Xr|HJ9T2L#fLySq6izrQE5u9$*eN^;CJ}b zXaWmr6%f+jzlI@1fdXcW`v)VO7z1YnT!YhSdEj@#uK-hmI940N#84oMj9)V5f=U!5 z@UoyJO$^cl%oY^#6r|ThPtJL8N$DLZAdC7syV=I#W(n!iQk;wF-HUD?t~Xa+opvv- zYU=)UCqoGxzY$p1zcA|jf-THb(+YVn~9R zYjGT*vfgW>m6XT(h0nRmUR)5cEe=0A!eXTlqi{@jc!GnUe`TFkIq@dfPrs!+L@z4- zZ8G_D?0?7sR{=oQYW2Bvmd3N3?rL-njPD3r*nw1!u+{dr#^eqDq5w5jvc>%BXJmeW z5l5ci2KtrS;7f+7Lt&qI7HHX-dAYG|y!1~&%G`fVSDPnxX6_lcP4i*@q3AU>a|w%U ztJ&R%)7F>IRb+gkmZE1Pduehq3AkLoKfE^0Y6R$$=(47V0jo=>IW>AH&^}4@^=X*L7dS z^>1(HlquV>R`uzh%9h6^2a5xL*>5R}WCA!hjr_}o6S}F~8s5sDs)@;^u%*%EM9ba^ z|BPzKWS~N^4SlZgAzX|6a zoT7)yX7eUh3~78sVk}0MY=OrGamUeRoC3(`RR~TaVw-InD>X%66x`ugGJVaZ^=d0AyjX)3&JUqtYPD;35 zV@=Ys6O75biAk8_)CmnCht(46FtjN`b=d6DVUlHmSP9&75Lm@_%iXMGSWqE8&1mbLAqEuS4PezC=PFTfQ7PSSXyGv7fUT8;}Wy(Rz%8a{BI|9Ct zUsI$PR%5Q@>-)xR!mOZs#Oq>?X*(jeZ~Lv9deF-x?Y2i`{W_-DUB-mc9a*2AMISn&&&3h0tI$3)OXOL?nP-UL{u%lVRGUbq^u&$ zE8jlrR`warr=Dt{T}IshG;!}8^vJu*43!PSJtQx0nUEx2{{`&mw*hAFU0(%zT&b!N znq6)+vcJpr#~0DU@At^BnnH=ZP4h@GZaspy&*Gg050k`%vXOE{C9JHQ*$VDv;Cs>b z84B?mdAx1=N?nATDY+{ww6*37-o7$&A(4x>YDIkF`PWYK&IW6Ty8`r^1{T@PuK#Y_ zW5)ZlvAdd5oZ8)b_Yf(uW*i^Yhp*OF-->Q{lsz>LzVm&bE=muD{rueTdr|m0jU5)N3BdN|C8`x(* zzoZ%fwFF6K`~G$^AVKL9OanwaPwXpri|Q5Y1=9~A-x7ZT*)F}zecbdjvWv}%osS#mClRu_iV*9#pA$2(Mc|X&$io;p}4VqVpBw&&kbi zjgx%Pl@*5D=4sT!?gTFyfBC!ueH%~VH}HYqjMm1dQuso)?(=$M%k7h%lcu#QWf1v< zbcGf{4O`iDP`ps`Dfm|4sJzoE(fUfQ-RHX$K4ntgz(v2I?=FL24?BeZr2o57!RCue#W#cU zqH$H+Z3lyT4G4(3K`N7qtY>f=p>^{WZV^Hb3_z=dCIL>zX@QZD6d-AcRaQi2bjW&_e*skwijAA+>pQS|1Xu0v&$={t)tLdQ%BVKM)*!Yrp|7`nrs5jd z(f7IJ5%$MuL+$B&J?KgibEn>&?DU|=@7vYz4fqMs+b6!|R$CxRZ4%ccW)aUUGD946 zJp;F(XY>+3PvuRalVU6KlO$Ge)_x=O2-_mXPnH`4y(c}|oOrogE%na(MU|TLG2sK- z%1+}%151mWl9(b_jdsC!!FfUbPyXm;t+kG$`fFAt`pVCYPeMHb zKU0Gg`rNo21>V21o{**U0^iiWF)H&wwTDli7`#|Djfu{2&ia&avxttZ!}C+z@_~dtPxZw{gxnT~h(6 z>n6gAk?5w339?i7zZ=|seY|l$kJw}(10H#G46=@h3QX5p4RS0GJ(!yO0#Lovvmr{s zOI;bhU>(fr8GCS_xzo=6$W{xyIw$iAwZJXXv(13xSB`4Gm=ibm4KQks8mb5ADRRiF z=018IdI2{op=Qqd#yA*U&e3E@9Zmc>IF#kKNV;=9vuUY=jB`==pXe&l~BuQm5f$jE_~ zF;$~3Zy2h6XHmhwx3J7&SppMyd2foVh)sLcOyo6RNYBfb{U!h9VZ*CpX0Tif>DnUl zWq@B2B_s7>xwG>4n{9ZZG$0E?a(208MEzRQ^f&ja9@2sFWUx|)(8Jr4_cS^8T(O{E zk%*oe^5X|g){K0Fmf4PJwFG&!PHxWTQ*0M~XPS2?+ZvCCCV|M8$5q9tXyJARuR3xTjZ5UNJd~3@ z4gIg+Lg2J(f!~!@D(VLxf&$rBK@uv$Lsfg zj_llK(N>4ImGd2W$4hFc=K0}vvey5kB{!VeHwq7>l9F2tACJa{MjGpsM8r;v^PGfC zluA1C%NOK~lfhyeSSZ0Ytd+MUWrQV+cZ-_;x&ax>_MsV~7qse#t~@i?aGKSidN69K zLtJG5JEY3~td3SxXbcC_5A=^`T}|0Fh8zNNYo zJo^Yy43bme9$+k~kOV-UC_tW?I}-fVnkulJCq*e9*SF+v4O1ic6FQSUS&fO%vPpy( z25cH8t$TCO9R17F%-GQ5E#@2Uj4lK7J0jB6Sw#et$i;)(1Bny@%fJZ_&WmrYEf$r# z(t3vG?vWR0eeK_Y1F+2yP)-AK5ChGPP$^VpRTuzQwGkd`-y8qbB3!)s&#T~a4kO+B z74p`=#{EofweHe%=-T}#xGK(N8Tm7E4w>b8_9H?~qe&BFgO_ZhnCaY`?6C??S>zti zMfpUFagc1&$8A|j?jy}6x7HFnIYrOJls4ETu7n&??2F?lWcUu|?iYcH|DLQG<^OhI zK}s#1&8Zkq-)-dRysx3%HSMYY2&JB;ua~lej|If+;5R1PU^_1YrF1Rq(KRsMk$7A& zb8>E&l%n|kt$)!}q|wb0H;;R}D+ zOXV%91@@8cSjqs>f#Fme(1m>k(d0tu_aN=59?f8c%Fsh(es}_YivWBP0NO`(C>HI9 za&Vl#3+svXDJhf*Y&3Ba{2h#|rgB2Fe@JW(y+J zhfn_Ytnw%;KyE84HT8tdU-SxZOtXKhYW3HUkWEy%c3_{5UwPG694u z;Kv|uQIMGC9i5Zh(}(p-KK1C&*DvqCQT~2PguNp0QiRDwegadyNan*?_!&=gMBaaF z^RvnI$RUqmKymAQ47lZp*iLIlIw11B^e&+eUFiRX(&7NOK>v&E%pc=e`NFG|>w??1 zs#Br`%6yO}cs}e{fRFZrcVbwSb;M1e1@eG&e8;;5zO~vPWS`=c>d&goCE2mt65v9% z2NLQdRIua6pg`xW%Z2}PZq)r5^;w7XP)T<9eB1WA?&f0^o%2T%7nU$_aBw&vwGSi( zrv;?Cth+O0p;-9iU}RyG$A@DY0W-v*qoa-WlNcHR-64Qf=p8q~04LXwa=_Dw_Q7L; zN{`X)@Xo$%cYbwEcKAQbxgB%Odd&RGF7}vtaIM2^mT5+4hN~UfI0vQjdj7S7jwZtU zfbl@?Ka*G5tAQ6imsnqs8@6n^toOHhxn;@J(9*^;qxoa?G0Rek!?`^*ex5358+|jf z{2ljVMs;FS(nRxWWusB8qI+2Rsi39gyNnlUUV+1mHrl%=?rpoO+9-+v;4&|aFk!hu z^O9;<`ouVZlYpfBK-{%HXU4HxdwRefsuu5+87*yAm=&ms40 zg&f#ZId7hq^qC*@6nOVSYcbX0q9Rui!$OYPg~p%lq%ne8VTiQoUktb&!{J-fKNvrF z*lwwHzA_cMXij>3(43M8FJSfY-C3<;2>h#IzD%PryrxbPT9FpkwGtJqg%0TF0$c}f zoqe|U6az@|VLUsq*YjI>ck9QHhRo_X>la&OAKp4E{J9ZVOa`TV(radMO_L{fldog7u>AP)Ls(t~>F-KeQYvy8=m*^A+eC?l?kyWJ zCVrV>lQuYreyrbYDtM;be3rT@e9o(TZl{S)$LWB39wleT!1>R(qz@$baT;Me;=) zpSqfkQB{n(3a$`VXVGtQ1^e$8;Epeme9@zU(}()*EXx@>oYb}U>b}}JV!5;a?Vb5s zAr=6Lhp82M&2RphHaOMo3p>A~2QMyNRp9h`QVgM2QU5YNBYMJb)#a=J*!M7M$t@Wj z&`(JBYUVsMt?AIV=cRa6p3p2FQa7b$oYw`^52_0}+r5izz6(DnOT3alr4kUo2|#}^ ziJH_s;eT}e@b8!?3eaF?)TE++UN4BzUBI%LSww;`ara@#2|}{RPFRWW&&ih4r|JFm zQej;B3OP(4UqT=^$x$MC*hX60rgRLYmZP$RDK7Prymp%1{AAjlOayk-Ccl4mz%&_Q z;3H0)l42Z(vp5MtT_b+z%4Xd03g)yTx)8R1XDS6m-=ojd4Xk3Cy|Cl^>KK?x)n})I zQd|8>_V9_BqGn%?t%pobb*<^5vP#T~k4{aP%~4u4$-#Ya!;H+?0cJBh6D!OfR)ouO zgI5SMGnTb9X@ez2klWkQ-kFk6@mJ_e<7j~2!1nyT6v(2F;J*8qSgOtix}ARm67qm; zh@SLAAE+1PcHs9fB$9497uJbpD|ymum{EQo<@X{i-}h{c9=Xpn@;}Rued<6qsRzst z7A4ez;@;A{-b@$R%~R6Jl=DbR-kv4;-*JzTANHKfqB&|(Kqul%4ocJgv9$GCS~Q*52U9NeaWBiQ|_gIMk%m5 z@L5pi1>$Lh)p8l#-RvrHm|$c}YOvmp&L(`Zot0htKqr62{5U&fmxfSbjt^wgLOmMONdWvW8mY!k&;mse7A*# z{GMzVF;(W^!#ad)7whh;RrH`$m_F5~Qb2WK zxrxnm^^=jXO}CEK#duORC2=ILN=%C;m(>@Hj?u0MWc-OkJAV5_wYI~~SVXmKJ~s@f z2*LHZPV^h>{b$tLV$7#!%!XscK#6*Mghuekn9fyrd#}tD^sKyHv(%@i=E5OHVWBi{zW}cKo27n)iU;B>6=`nq0y9&v8p?wOPlW9m8*5x8gpz zUel5=yU}l~$+*V@GQTMIA2CNZ479~1(K87gN+3DVoeBd43XVUY0mrS*g(#)1;tbqx zY+Ep){toL~=&sV;G=D#}Ci7x*0uiQsxm>M-9je!z7Hpe&Tb~Fy8yo2N>1HI6+K{y& zTY^S2t~1z3yzpJeyWVG9c424hSK3o7`%@@xy3*fwg6tr7W7M~ncj>mfqLt@!5T=8U zk#4;`dF6E?tW<>~J$QhUwLD*_0A?&>GmYe+3CAm3DxixIMFnogMm)lyGCgG>F|mGy6-ofKrV zz(WbmK}=5MA4sn4ybhM9`=;c2tgb-ZFvZB4m@o8KRpbGql8kPYQ>mi}c@t5sk4v0& zQ#B{uYbNaKY3a) zWJ5-aLPa#{{@u32uE+H^_U-+(U}mJdH)1|*aZU(E*WBgyvQqU#Ptxiq7#mOp_9BsR z$sF}!ia$w^+dhn-J}QAXK}T69(SoDv^_fTcP~9jqB};(MUAVu`Cyh3nt!xA7P}q)r zZMRV7_OkUafT|mU)sG*8Zf*T0sv36R=g!(#@FOI=5L#FiW_bmNVpW~|0W28vxKX+! z{(Mq2kwyMK5KEf^MgESwxKO$Q!(lV;`^tSDL-WsxTNd643&NEAbZRdtcKl3pga z>+4Yye2aHS$_qFBb3{X(#tN+!wP~?v1F0 zws%{LGBqw{xF;d(yTWo;nYVf6uVIU_-dFC|dF9^jMfu%M%pY56p)bHs5WdW`Q!ew; zi^WK+f<+mA@^jFuFx~7|JlFY+&Q5Nu4=@)L7c%SpRQIa1+fjDSwhi3r?4$h_kdlQE zFDtn|T(kxMC}Z|tw*-eNT<%rFUi{eNn`7O`(Uvv_CpiwiAAcXx+NvHV*K2X^99(QZ zM=c{xdF+?elNFEktx<@|Pj4h_H~1%0^5&D|J9K>+wunj7Pg=4j_!>9vk3BxbwOy z@q!2^h96Ra%^bS1+}97rIDiF=Lh>H{hnBx^R8~s>rE93hhA7Z#s8M%d3~44yx0>rYEIGf%S8&{(^BV0=b(N}9 zTRb{C6gHE5ZRYB)vq)K2mZLK2kdb}16RKBtW!5~qcM@>zqDRVyM5Ra-;WE_GM>m+& zODD0fpa!0V{Qp|yuv>h2A52|4zVM;*QR zFgGy_SPoQ~WWf&{5(;%*JELB5;@hVKOjE;|v$9Ql>t&q`8ZS+8mIPf14`>dv7o(jv zHaJPsUySh*J@z+aHl8HCWk=k;zI!jkbCaj%A3M@GkGy@{oO&R+rnv0ob2+3Sx4OZs z&Xba6C#!bx9{`9l;qwpDo0?*{5AR5yIz~wca<{*|3%n+c5GbM#^s^p$%beO8azsc! z*Dhm4GlD$fyQYtsM4FY29+1C=x(4yMF{+2IkFz(ga3z{ofoCl9N8E;odvvRJi2RUX z-LK2rc|W+lw8HsnCmtJFaxqT~Z#mzQ{)&X%icr2dG7@UfZnwZOoY@^4h18?5yKjAe(+LkkHhspMmmuvKZIO6SyvcqZ{GOCoIv3z53WxUFyyF z7&SfL-RVwX-L%Eis&8GtR2#V}E}^b*3*R{zyB+oy^kzbsAAwHLzEV2Etk(OeR-IH!1j5i?Pp^*=_t=Y)-TAi6~z} z@w}1w_^5G0lt@K0b86FS*USS;hg+a8V^Yo-FGQx$EG7@aK{%YJuUQ)t_n@DA&Rq9V zQ=a7%oUU2B+USfj142;IS~nMtXdnF+PE65mvjDUcSo-%N{}{EM%8c8(-sD?`oHb_O zgTK>c)yZ^B;|eQP`~~U{;#HC(=BbTPSn9(}e9EK&SG)H3RDt%g_?zGvx=C@akDK@8 z(G=2IvAa?FLH$VzreB@n&o*sRH_L*S#Ix2{j_vt})l_9gK1+<~_S5=h&?T|<> zFT$EsxA_bIF^SnT(Az%s8PcfzUjw+0Ys}Yy>s}EDzte}PaF0`qjj08kd@OY%>J68U zb49z#^%@7+=7Qy*Q$C;7z!~z99qSvDAd_f7^N_OldX1*?jRh+Ns-_@-2`dOjR+2D$ zBz=lsbCpP>bX@tOz>PEWw+8L+OdTME{=e1;fQCAf(T!=WrENn!6Y@*uTattON@o;L>p`*lEWuT1Q&DPGk2Le~ zvvWJCj(DcdThY%8uQA+2G4`7&P{#2uUl&zcJ-?d_;%*kRFtv@3NB~!ri9+|xYF4R- z;;O{zg@A>eG$rSW?%uex&Ssvbr{%R_&cA3z4_C|R=|#?1F`FuyIQ$KUa0lDn5Y^Sw zFWDRT*^JivYu|C5n#N5npSPCe!~WrZG|Z#>ugS-3C+_^R-F`ZaLO0gtCESv2pp#YJ zH3%>LpY<${?d=1y<=wCrC`^u4PMU&mALz!!GCq?0ZRFjg$iPBIjs9L0n5WE#+t)(_ zdsJ=Ushq7n8iv&zRKP>(YwV=)4c%K#uX^amK{#a8A&ei@v)ZE%shJPF=K*1k*@XKI zFXhus;)Q-!Z-$R=ZnSB3$y@O$tWB#hRp6+J^R3!->doYY>TXK%nxvzEg*<8H7F&v0 z$K=;HO*Y+HHWp?w;R0cMjZ<`Z9Gva)KH^vwu?DIIf;~S&tSWP^_IDo zUTxf8Cm*TzGn8k@O(JfU2!MQET4!Dk#=_~;*T+B4JnTuY2_x_EvG4KM*pFOSZU<7H zG{ZB41gj};{*@SR2;N|&wXT2A859iuvMt!Y7 zob(WOt1t9V_%vSV@@DTkUg~yV^(HF8StBnZcRcJ|NANFoqU%>jOj-Q_gc$Uo;d+r~ z$RPPcCW%1R6e_CTo4?bOU(q)9e&+|;;0XDH{jOtZFf#DXfQF8d`IE0$zz<_?}^iV^A+8uaEh`2w4QAFAj6pjX*wnn-Vf?o=^v zXd5r2&H213^2Sst(UaBeE`6WG58vo_D6I{?VJmnzLw9>hhEkFE&qJ4o+&kTh-Ya}& zPf*Sc=fv!SVppy1LQTF8zM(j_2~Qt*Mzs&!DrF*lNUWGS(XCM)R2k5{zk%+XyZ$fv zqa)JE#t$9BnOC>r(R(~`5hFiDH~%n7yjUs9M0u;Nt8xzyv#QZomyLl*k&W)fTwDkG z?O5y{@T#RNnH`NYAAPwpeuy3Z?+-T(s+#>C)RSSZSxPIy)&5mUqk27fD0BKn^RC&J z_itgXbcwKl?mh&6#5xd+QDGXZzdjj#gZ$9)5YgZ;0hd`O%uK5&yFf28KZfH{z+Es~ ztNv>5Q2~oa-NwJtc4W_7=!sN6DPPIlqiO?nhgyj)d@r}<@a{@dU~HJCf$(G;{d|3d zq48@y74HbKT8bBHYSt3#1h$t(C?x8->V1P{eX3+ zdxd{Rxh45SO$(^@sU_~ew$}%{6+-A-0Qz|01(O3BAwBRaioI83kefvM*)E~&AyuZF z@SnGabOEkDr@zI$L@@N7%ZVN0!6@-IAqIp1TsBOwE;=LZhP3+l%qbUOP2RC)hD9w= zEqu_f;#~lzm5G{9HcJ(NeAFIdb0rZ3gB!c}j?ASzxS(hFd$u|^F7u$K=3L8WSd?Ya z1zipL3E>ZRYg>sXHR3uC?A;B+p~#2+4|BrjPfg&Xg3chyYmS!al*T$QVcPt#Cunvh zHhbTc{u#Mj)o!@YY_tlyyeT;@q>l;C`B0g{?04ZMJu%%Tq4bU5SFIwxV!#+%gN1FF z=jN5-YD`K$iA$GDQT2 znRU-ZU!f=keekX!cm>-rbocgTNpWr`FTUmbjZ*}(Zu%}WA0 z#NqiQ$$_Bmut79|WzqP~PdQ3lu(DF=*Xi8nE?qz3(l~Wf)UH`CJU!5PWDXRzOlHxC zbz)cslem2~11m=)T9|$>d-hleM-o^4O0foI>VMuZLoF$jtp4TOE@H_jdSA^aU%D>%V1D*RL_mTFARo-rb zv;m1#z`58AxnuJgLq_wHMBx`r108&f*HRewy-UgR&rI`<@{KhS0r!Lz02L;7g}wtp zG;$GcO8lBWSxY`RM454DaGVC(VCy@BU|;a3)4Ho1>jDpdhMUO)G5F@#rl`@$0J+@>@37orXl#h35`*p9P!U z8)g<})v`Jvd)Kg1i)Ll`E9NfGdiQ{n0W`DMVS|;AXFEQ=du(h9`_r->(0`2CgDRlU ztGS%7lE2S4*jU$~;Yjk3HHFj2!RS9b_~*nx^Ve&ceu`Cbz9x1k5$IcCS1^oxYeMRR z{;f2x@_pqeq9AJ!zWsz$bPwKlpz_sk(fO~(p_Pp5!@~@pcxhhTW^xZ?v^)vdoy#OS zR!YSgYKmWfrtVCOSSMxlVwHJRtMRB+Vv{Svl%CdnhFS|vxi*vuV*n3NOTU^3Xj_)` zv5_}uk%_6MY;}i%($e@5N)!bw>J!tcxnVavf{YvUmWIVG?GIT zPrKTUotjQpt;5JROjJo$28B%yB=5uHQNgN&l7(;z#sgUmfG-T#`)YvtlJDCDZiA}x zGi};dCd!I_M=?Y=FjNfMRZ0)vVy99L*zNgV?HX5P*c7(|1p&+grulJitrYJ%k=DRI zf4?TvXnd1c43`|Ts!WEuVcO(+-G1x$b6}PBWOI-lPsfq=D=gut(JXjRW?dg@irgzswe4wEv>|TLbaGNEjcDOcQ|JJ!jrm z%K;oSH?XdoCupvcIE7e?WfNV>_2ROYX2-jhX2)>La}(m0DE}r#-W8QVFP04oKE<1A zgFqnR6WbEt5e^A0zz8Ncr-t((YDwlU?^mcnE{mN9b`+@6myxQJyCHPox1(=SU!Q}! zjaQ$>s}{@ZiaA&iBA9Rl-LrtOAtf1Tz*2XQZAM*K(sRu(8U7x?5{AH?7uOH`9rQq{n^HM zNHr0{Yw|F{^Nc%EFhcZ^f@^>f2o5*>!f@lo7&3T$3Vsyf5;xsYNQ1kjPwAQhXf$vg z=IJ;7cEh{ z+^r=z@{2F%I&LeU9}E_3yD+5$+X!vcWg_&6ehTU@76_xWYReT2ggPldzxfZY(t-7C zaQ;o?)w9WdY%jY#T6uD(DZN-QLTsSpxq$E(48jFvO?h}LnyggPYAd^1J{saY;*akC zZM-0Or%euNs7!XC(}`ASc(z&pIIgYYygE3MQ!=>uZ``K+JfuZuFz;rk>qg0W9vJPs zk{Cw&xm;+%=4Q`5Z8?iNpLR{Ku{u0u%-u=k+{uo-HnEkQbgVv~>LPD%@2JOA4U1iE zWw2m6byQ!sxpAjVg~@Hi`yF+(CQkCUkLr^T>$i-d#$3+hB5fPQQxiFg2-no1rnLWE z9q!>XJ7NAUlhdKS($!Fl=H*`1%!6t3yoG~^?OER2ZPM|S9yK>=oST05(XF5xhN#mo z{)edJ){m?MKCiuJVVQAzcjrBtx5n9HZfg&aa8*n?khJF`1ii!SIFF_kqIKYv+nB^| z>;@a57rT$JyU2L!u_&SFg?H8+BTbh0TVzBUDPb=l9*mDQIK7NY5ip!!&egE3szSQy z$ii|rutLQtBOO>G!l}Si9)a3qdACwbrQxQkGE^zZ=~q^LGE&J3#UUs$CogqX}>M8}RkfwivBdMJAFmm6!nt1X#R z=Rt+UrlbWOWZC(56u=_Fn~(jbd3;DifnmBRy}+^iyK~LiYu*iwRRxoUG)&GeH_p+k zf)Q#)n8V8yds3NTJw0CDBksO6+weu)Y)Iaa$07M!?4KtnZ>1*|4#vdR*vZ+^#J~pb z-^$L=5{{kV55a$CZf<%}3u|W+M|x3f17{Oq6C*oglm9KM|9^RbwCATSga04`?Rn)G z#&m{}p1&^!LPcE#hJDL#LWYmG!7X7JKJRU^nWoS(=iM?e#iZ}syewf}1@v}mJf9jY ziE?NkS0+M%>NXizKuAjYDS1yysE-Ia^wQy#&O|d+TJ7Nzwz;IBKvVUqj`-M7X#H zT{FIfSivyletpJfDu%_De@(+Q%ew98Zqnq~yK()E%ZZVcf_7q253(Q0R;-cbuRO4g zx?^|=chJ+OWfo>s85WUDdW(Rcm91moma>TYuh1G1s%O}jJ?FLvU0Ts^{YcG0P!%vF z@ZUo8-5ytBV0aVo)sGHGD?znyldsxvg1>PV{}?8)XX@r%J;7;Uz9|W;zbY{--Rp7B zlwWd&-|U1w7RtpIkW{b^LzhEKF61$jWs7r^6D|u-nj9ycDFwz91tulrh-02fQ;D;j zp(KeE!BUd(B%04;9Z!|%34Af0s}&glzwm9V?%Vys^FoVi_8s2W9JFn%rUJxfvLyoR zHS=7m{rk4dhtksi4<%>#zlsB3{=>-h{}gOhrRv0Pb-;z*yr{h{DCu{tw0a|gv9A}| zz+^SaK%9ywqMTiqh|f0)ut`LHe<%`>PU0=x0Yw3&#c?9;o{6OMc3KP=RpK2MO zQEu$v%KG+rI!|r7;ak=|O^CWwLeHgImmNHOhWT_kEY zjCq>c3(lrBW5rphAyhj%Ak6)~>hE9on~}S|PyF}mgVFgPZ6}J-|+0GX#mUEL?azU!Zs0_Ym91LPEjNEfy{WLExOlxqdC6i2EOmy;G2AQIMuv)+yVz%~Q5*Tc>Q>wr$(CZQHi1Zq0PxiRp-riGEx!J9qqh ztsRM#`Bfa1&C9G^z|B`KR27AoofN1#J|~K0v6AAz(;28k7>Fq=43>i7r4gQ3NfIQC zqt)*^Yk+_@VXhQWmgLL7Y5ATSrcV4tdonN<*Fo7Dgkz8@3yReUa!KZ#{`jWR33$=f z=eg+d^cl2#XZ7mP-BL@=l3Lfd*n|LS@07)mquotvJ4&OS;0Kl)84lK%+WHz`v!Kp_ zJNuE1u#tL*vG#yg!iob<;c1Mk(Hv3tVP3_}BieAIV?7_z`F3Rod)*>p9q;gXKd2P! zhrk*zxt$0FCkZ<1!Rz)UEg>tH~MTQ+wEw2 zyM+EkmHB-m1TtB+i9Ci8DjGdRxHbbHo<``OK5yS;Kw3tFFA{tv@`5 zlxui1#_2hK**~Q+=Ob6|U%)KSB+H6wDiY=bRGYcIXiWoPiv)Nce>7yBk;e@NKY%jN z;=q`a#{6EGLJ#)dD?c+Sf!9a4B~7y*d}@yue-85EWAlQTCOXwVVcO{QJ!jx%bPoFA z2Z-5-@Vqcv&cKQ-8J&!14FgT)UV#-A~@ihn)~ZJ_m}m(l2) z9BZlS)dhmYsPx7&LrNxsQk%+GDd`HK#iux!tvNwzc&{<@PG^WCw1(+x(32LswJ>xk z#DaC!#8#ttTdzH`=|nndhi)^u0ODD<{dslD!b>+c>6SZ#=G zAk!smRJNgCcu*ie=bH;!i_^|FN@GD2qa-j_gf4D2Mj44`BvG65Doj$L51>ZU1{%>0 zpws!qn;arfzdxkSD?B6~zro7{+D9Q#^IaRhS>unMrdn zBnOEpQN<}|S5&Q69a?CmR&wPl7{GdaW7(rab=4LZ<{7lGWxa}LNP{uJmN#d zQ|BB3{v=i^^@U$z40QUA)_+8>*w*tN&|=Y_6VP80*ttoG?&`3T3L z!wMmY@12DR!+^pXCb&eR5ym9F zLx&6&VBR|U%ShrsS@H@D;tlTG7@(5tl}6$paRxel>{k7!((i=*=p$eBB28Gsr7i>L zQ;PlQSD^I8AeIvYYRrHl@fhgh2zrNC5F&`6=%$KbB8V91ZZZFRZ=zvX*$-29dHU+%kr9h&E5?a6-3BdwD{8ePZn{ab0|ecwH&Y zvA+WsLEeQQKix$TPuxWal?=+GRFpEfmg*r%oa7;m+=&VxpQM9KQWJ=eAz2fykS(v| zY^!k$Zwx8SU!8d>eIVO10#x-`^#IQo))LJWnwU*pk_cV5x`;V9+-Z)WCuQI$a+8{J%!} zN>q7F74ef`<@iE<9_&m2QpNL?NqPb(l14^@VpMnIw*)Cdw08&4@fX*v}fUoKE?WGu#^ z7NnGRLcG4FpZJVutnLS~!|fSH8Mj%3`r8t~a&u=sz`%30crb;)bHL#lZFdcz9iys@0{mhwC+sTSWKBPRHproyDJnB~vo< zws7XKSN}NVaC!iWc4jDD5JQXTRcQ7YW|zi;$nq{xDufq?=w&c0rR%*g@i>N&#sj-_ znsAcb*yp?l;Y(}@O-U)MAPek>lf@^(N$CPtt zM?WR%$}+$>KIFlJF%`S z*i+3UfwAb$Ba-ly+Iuvx2&)jp52;rlpX9ci*w-0Q?*XU7||N|p96#1D0(+_XhS!j z1PyGyNT=z1n|dhoeWkHA{KH)eqcK%|BW(MU0Nq63wW^B4ODcO?beA|q`_!z^Y(+Zh zb?hcQih*J$-~{pr&ZPhe1!IGw(@!Kc&i&>3*Wb}T{!IIz1>0GY4}t*> zI+$w47Ig?y8``X}JJe)JP0sH9Z>j*CK?I1+e8$;%w#LRKh#w{W2mx))s1}!N=qh^&N%aiU7_BDpV@ZkJn+&B#FN!Zx^#Tf-&b9f*5>p5qEs+>cU ztUxM8Xa_y**^RWO(@kR=j7w!w@6efV) z)knoPP)s>@hlM?H%fsy;2rW+)aSYtK=)+JlBN>+ukdENzEN^ui;SACx*zEfRekR0F z(4^(04f$~(G$zt$GLEP%A0+Mtb3cdh*ewc&7;U~MPfx^cF0wFCNkfP>O?*;$_j%ND zVs37L19bc7My4Y{QJl~v9%r0`$K`(c6l-J=#}jXiXhe)Ks4!E~ET>phJ4I_|o~?+m zJ%#zaaA|Qmxs~!1yfKNk7HM8&S(NIGKC2kSX;({1w5Y3<5EhkdLrYqt__DC;GLpF$ zu~IdAnEE63tu@nV$0Xy7qMU51DV*|%a77q1p;`Vo5CarKXx-yoAWuhjEykyzz`npA6^^jiZ zmQkn%cJXo>18U5=zNfVjK-`$-Z~1w&_aE`Yy@*9+*>Ui2RRcj(juD(wP+t$`JmF@$ z#RF^x6-qNBUieH1!X`Nuv|WU7n|{+Z1~ywh!C^+KD*B?oiM}+8H1afX7PyJB1rbu^ zJ)j9v*?g;w0HITN>T>*-2(`YGb5N9A+YDNr6>_74`gQJ6ox@#XY1;Ec*!kEUFj*IJ zj0y@#qtNAEEy)PInHKBt2wsasd1aEus7Bk3Jx20BjU|oI+Hz)M%bV=q@j22%4%d%^^2Qf@&pf!-MTtw8J zM8p<{qVCMdb#q6HoPuaVh@mY)d(c#GAm+#HofL=`FLngdb z@co~K8*4ETIttyYlF%5kX0qpSgPOJwxsj+q~eIht5+?R{H1oG-%X{S)GLsShS7z1Czja z`O#J1*@dEb_Rly<80LM!{Z3zh_u0jQVvv_}c1Db(nx+i=ke|fA2#JN2xF71*glwuu zmZU;!J*IygdN{jIw}=MSrO)hu;*l=+t<VebL&%- zxw-{IBLLO$&nhR%{Js#~b{vPN`S^WEAL6@#e>Qq`eOL5#qZwX+cYw|T>ZW`8TKdP{ zM0Za>YZQQZE+MhNihW)5e874=A3+=$PFQwZnM5YZIKFj(P4D$$wgI}1cHK^^+-BK) zbAZ5oP5sb(Z#eB{2#S&JFG}fPVR<*8zJLx>@GFFW2zpKz{kuwts?oJked*4>L7OP3 zAYOp=ZB9e_!OjvwPH)ED;#41X?*+r3>{Y=x0Do@!TKIK>K5=eS7fKiax?&~r3Ti43 zbqU-#CS>^xZi?uD^T~JtzX3wG1DGZyOR`S*D-UKu7w#Ww=??dxIJ|xU-tF!-*}aqj zH+>$*{tj}}!R&G%d$c}-sWrv>;5Nm~uHF4S^!y#td`9%hR9W+&j7vE$MX53Du^)H4 zL7ii2xY=Kt{=jkJrSVNZfpTpa*xJp4eVIKmCnFT6@J-I;<;a|4T)wmo3ty9pg#L&f zK)L;bbqT<8Xbt)9)8QRfU0GqfjaKp@QDp@Sd-tj zicU}CHpy=;DXQXF)NERcBvTjwE^tvXCH}4SGbQ0LGrdOe7{V5cG?)xa-Tos@rVlYl z6Lr;awN^-IsR=6y=9s_j-cv$7@Tjk=tp5ezlvB*1u`RafvT;JY+1@I1aXN>yo@cX}=VCp}x_U2m{-rm6DPlb~!?;_@vH81` zm)e-k1<<@dn{>6P+t@5?raP5a4i2bkh3!wt zYER(^@DZDbf|Z7z#Y#UxPRR)L!8<<9ATJZS4Tyln;ftANh~JpX=ZE9WCEq9F;IJYE zDh5YQFmGG{DA;ErCz9|R=#qm$&;tOcB!mBV&fEWtrpHA8e|v;lvHt<#zVU{_*$}i_ zkIEL%bA+}xV@tjQ#MT^xj`)Xs65En<83apO%m4i&`0fR+dqX%Krn<`AHhZ9Rrs_PJEwbvR)6RWr z9d?IBRtSQ^48x9XG`lpIkp}PB2U~@-Rq}*CnR8$m1yM4O=QFUnYa2aGh=nP?l%xdw z6rofQyUFWS@ZRT*F?~Ja;H-KQvcJ8}M79&)LEb?xnOz@%NGy6A_`)&-&tK}f81w7s zRzTY{{dZdZkLtp#%#8oVw{29_wApM&@P4V;Jt!ZgBoy8X$LG5Q;ii>Y&)w!B5w3ft zl)==r9Nac6?CEC0o{r5g+&avI3sdccKDnQoWIM@dNa?|NI$xT2K})ew&eQ4P@$tx; z*3r>IIX^}~{f(P#pY0nc5>+y0OfLxO!Zoa{k-yEBj@8Y9N1#3xm&@x|Z}fh?FAvTc zHCrs4;;O1Nw2d!hsuIVR+vO?*On=rtQHCrKHQB?+t0pZITh_9wl$zgW);eBFWeN=q zg~#et(wivdr`I7hF0@q1C(RYhmuwTNsG8o-d7QgG(6bq{3Y<^UR4Q)P)^7pVe=^7V zZ7-1F&W9zu;c=u3^IK{KTXJ(Y;T{vO?XUi_&=q8+hYhQHx?lNl2mEE8^N+IW8-`QQ z2xx#)k5e@h5733XRjyddM#XEOw8=7df(1KDb1Eh?S?esnV2AF5e`(g&wJZ^`P7jvx z(XzrHgz0`PQr&f!jqcp$#A|+Cug23G*!__G26^4~3V3mf;O*w@eazYG7zEa3dNFNP z&QU4lLLZ(7)L&P(6J|`LQ2XuQa}!(|aPhIaVI?T^F=Aaln>#C2E1;u3NX3w?%myNX zroFy-!~LX3#oL7K$i{nFPiY+ociEfDUQuv!rk3sNQ74YhFVH4cgNRwe&h9a~0LzI_ z3C{M6_P-e&gTgTvv~5NP4Jg~a@^Y*>uuOlL<_C;yWa$Z-UP)p)!N^p}<0fyo{l1bb zL==ZCUrw5?nBVKT7TbM74H&fboj`^c*256$_;TTM8=wWIwq8SqEBMOo{$LL!vTngH z#32&mBh!bBQ#i{5wapES@t(jjLI6j_q!4xC@2Cn4ncX|=EvU>Fz4-3^^T-*XR{asW zU1!8eO82Y?eJy(9HnTkl7V7a1&k61yQvnzb{rW|bgN&SowJ?Noq=XY!h$L#`!lkkj z!fTJ+CVg*rqwTxhV}j@QWf~qnm_iL?MRjVgr6He{<&YJTK};BgzM^A`5piobR%1)) z;AS7w*Y3wU%4vVj|0%@}CP!Xm-)cc}_vACgG5`^MYntki1c~kdm#zjf#4e58_lxOE zqe8NLj&dR`>VD+@Gxk*=AyK;c5#~hndarx8BslYinbf!Y6FX%zl-=WEK{qEeQE8VH z08f8V#dv?5j70udh?3_L+-3}2U@@g4`2C)}CH}e}0mFK@$qG%v@%Dth;=FVIDv<^h zkwD8_T2#HP=wS({r9QL7p%7SWA6+D-mtKs0x!0=p{2i31cSqKY{K*w8%@A79KnJBR z)^k!oqzFhpKgKbha1`Sq!7}>Tz&Q3ecmFsZKTiHoo=U2~CpLNhX0eG2_Ql(%_d^B| zMo}s~44A=m5RtzWv7ewz^L~kH6yG^G;?oMmD-QSOP=D) z&B`D@;ul5cSn%wVi6kBrYtSCkKO(_ns=aAJ0+DA-8Als;y;PmDTwi2;aACU0Kh!tx zo+>Mh6Plf6&Tnp=!AK|VMBQd7Zte4|>)kI3lcP#ay&mD9hx1Z(2G*UDmF(FY>G!?sLsHy6q8ArLY zf{Z$!w6S}}nsEM=Nc^SKj2Pb6|DF6bL!}YU*bDD9s#2QrD}XMXR?{_wjpSUQ7q?Iq z0?9Mf(4x@r^z3i#c-BuqT)R#-z@!mSC|6T2K84CI>(y5CyOGqJH$VMr(bD(kbD#N8 z0To;wKuU`0pOrlf0(b$Jpkswa0UXx=7<8z14^EB4^G6TGU1) z>QQELeKfO}j0!&U$ZxpVDfS+>LK{#@;9Ioe;S4(o=Qbw$%%mEg-B9NH8?>PH4GyivVL$Rx-Vg+DUHO&hi7s3GT-%cNL_q z_yXWsX3@kPwlxx;@8(Cq@A#Yh$sr_JV!;vdrh30HV~cry)ni3gnMK0Jpdn1o@ufCG zT#JG$WPNB??>prWc{g5PU{x z&H9(ENhAHjC31*AfHMTnZn)c; zt+tun6GysHajw@c^ecfd+J~m_7T6ns0Ck^Y`g4~BbYvG{mJM##WRiLfNLsJRygH;H zXXAis>&=&4+{WC$w3b5x6Cb-sL?FyL_R9it)v42|kj+8-PfiIrl&5R{7s}_kqAez5_ zym`L}Hi9M(p6jIgczF3`SRCxyMDa7!gy);`cq>mDpO=$cmu)0c(i;@(tZY7q|06-n1SH z`~=JF@~Z_y#XAo4`+0HfAu265l!A1OBkHSX95YiRd|Zg2l^y|M_VPEGp_Gn0w?uSm zMSBt+;yzI%JB^8H5k%jG2YSR9Ba4=8Xn3h4_YVZs~CuMykT4-93#3xD6)00{y z)*+5*sjmF$$98r-%!UN>6O2dbKa{5E?lO6=$JxvT;nZ?(WVmZ3JZ&X*sisr_>p0Ek ztSOu#ltpjJVc;(2VGhI)5EyXd9BuMO^JgKTZ=(`1DYG#<1HM4SE!P~RQHi!YaBTs+ z{U&YcW%@=1P%j(F*sBDIHcGV8Z>ao$;$bd`=xNllE4 zp9h4u*VOPZl}0+3CpeWrS6K%7RhrA>R|q@INxt^7d+LDK(lOghMSa7z(<#W>2UZi^ z;MZ8kCx>KZ(tfB0uIUHNF9F;b>v-$+z~}YAvw;3&`C3l@s=Q68Dy=EJpv*AIFMDr@ z9c+&n49hs}j^r2wb1#NXt$U-9>$}Su<wPunnSefpxVxo-eFL`M z!Obf2$c_%3>|MYMg=H1>{rVnA0C>AC%nc2{n40jsg7gvPWK9O1SllN^&6T_NQOxLO zxdu!^574peo3Mu_--nWtW(Wo>{2531{m5;JtxTr0IotaBm^6KTcIk-kcR8X%u_kSgba)NeB7ahtQ_Fed4d*g0nszpwKbhYDVgmcJ8fx3>;#FYurA z4%y;+*SOat1$=a#BPZKj@9O#)TZLdF$5Ovx2rIYq@nYz-AGMEdzUUHmgh(S75^T#! z0(y#c7O>z`w*VXMrP~9l^H;{74uCs$mO{G7IqeRQW{TT6~yg-^-?Rs>UNVU+6Y zLY!!(!(hh2l74F?a&TTxa$TjC#>j{24KX)*HMN-6ej#xjX#w}@8MA#Hr=qO)_1Ni~ zP)XSN{Gemo3Rofod=*At7R72Ig_CBNNnMb!_rzB(x5BXlpSq&?&+g> zA&3_#3)TuMs>C7evGe<%oF(ve;c_KmrLcRdBMJ20XS47PBnp|fMSR|$Ht3jm>sYsb zIRV`75%;F6*0{}C7aw8XcC=&>WiL3?kKH;ymRj-7I}o>3P*j=;Y z&c!0ldv*xu$fY4l)G3H4Yb@|s1{s8>fyM$e!-v?88Q%G zFwB40tRU%+uemzYazM4XeD{AP_zcS~3*GsqEYt zYb2l5y^8@^BAv1`fN}y9{|IJz!%eqIkT$}a(Y2q1u;y{*;(1)A(%m)h$PP+3+4-UM zb?>>(?Mjyzs-?ZbYXnH(;`$#teF?U6U#jQ1L4y@bSvZM|D55xd!+@@i%a@YM2rYmV(ykL*5-gXl z-hM$?LCjG;kg1t>&?E9MZr<0Mw$!y42Oy@1%z6|%O8OkwSj|Ul73)ZR*)9$e0FG-W z7{T7|e`(z-=UOIu_G#r~);hfr*u@F1X$WoYB!*<~?+`jxx0;5r{$@?A+IE0~R3oa$ z2~r^W#Ee2$Cq0n;8f|`WXBotW! z42?sh^mH+U_750G47tfeM1W2U2AMdy$tQqK)F~6I(*RF3@kd_>b9MGl2lekTOV1>y{gVa%LTo~n#OGtz%i68ODmXgs=JuZa1BP2KpZD+0IQ`T| zOwxUzCD*6<`9efVi(;;y-LW4t4W|&SahQ3Of#Qt7Seb&;n{~uKl&Uc}AS)D@Y_Mlo zWsJuw7XdM+7?IW$O(+V?RTM$7dxeK96Mu;9!mqCE&1%l zU>KB7y=IwHr+w|g9<}F)1(|!*Rsng8U%?a-7at2Cs9g>2oByVtQq=i)(ZS&gzwJ%e z_7~?9SSSR)0^>f2Y1HqD+&JTrbkF~%gnhc8!ezeq{3!#CtdU2ks%>y%AVV5+lR>V| zV{}rCHG`O#$!BVuxmn1*R&`;)lM~ph?Pd5)R0xn-fi-JlB}zV`=OtJ8K90=bU}m_! z%g{OIupTU*2E@a7;NYQt7Na3ON92%70Z^i52f`ya|N%gN#Z>UhF_N=Vxp{=IpfP;cc2mU#%Fy-!v*mnvs?7 z`Mq3x&Sz-tm6hPLrrORh#FOUu&P01<=*6}kA0QPN`8s5?8NuuyncI)~&9eU-6*`HE zyJ!P#(37Cush&?|ClbfzECK8|!4HSL(7j>D3j%|j{)XwQu(-gz#_ z2QnmZfX7iUe~O~{-zaLnJ0|02i`MpvnFhWvp*qrNPCH0S5`EYNMgnU$+`bk@!mQ@{ zLP{ijXnfE_I^sB?Z#yQCk;oj=yq9nT8RpGUb5F7h+)EO42;y4Bl7S*6^5hZ9jx%3Z zd8le6@D09-M=vjs$$=;#Gg4!!5#(T=DuY@PCpxpTOI*}7mMPmQ z_b>S^CbKjq+}cuG%_1hFHmkS9X)8O-S&&ixV%FpnW_yRnInUt!u|}M!;iF9exe*^% z%vkt%BawJuO``wJcP#Y={@ILKFRQhRi0f_(NU6DBBMQRgnuB1|KTnhZq{Q?PxYCXo z4qo3mr0G*`8%Eemw85r4O!_!Hf>g20M9-u@VCsCKWt-%R+88n=z?1y+%$GD*JGB;kZnb&{@R>vVJ&%nX%wx6) zb??E=nRT=0j0>RLeQ6szl}34eV2&t&#V4|o%i)I#UsRt@C^=GdNS9nlBax*d0?L<| zrg9KsF*QJaP_Q_T>v&Wjz8hH8mi;Eh=8tgg170NOXawIV?1Gk==8O_~n) zCQI@jjLUoER%T|ZTj(ItRv-@7ukC!1Fr#p0w!WmzI=ptGs92WsQObxo1B_%+0wU4K zz^vU@K+4GK_zYd1-|0zxa{BOVzmjX*Sq_bd#Ap&5ug zxaHAvAcricdpf!xMtM+dj85f=T_2H@H zgw$uLNQ~W~Kr|RQoTPv*ll~Bq$TT6na>X?_7r;%|wR&NZT^C#kE3fHmOb8Hv?nwb3 z3hT6W2&^Z!=$i^lTXyOBj6-_!Tkm!qzh{Y<-;Y zr6TjS@89wYT%g98dLiN>)KpY9)}H=m?CI zvdrFL4TJ$flr!PpVX11d_Cci=<~?W#RkHNARa6i<2`LmF1k ziON**V9!7mEBZ@lu6!nTnLJR#(ZfLwpGf5-aLQxFGB%(P-={~4Z@VBC*V*~#ESL`& zJPqxc6KK~DSp<`Gq~gLeDO+${^3?e_dTaE!B~>}UZTbq})|s0_Y1cS-!(+=R{(@9; zjc&Xnnx9`CaGGu66d7w=*_i?(S47sJX`?u>C4F|5d`7(}H_z%?_D<ADsKK7N3 z1o5>7gM>k?p;~&8F^H3?)-wpCQ58$%w+F7~v(HoErE1(uN44hC5r>f{uyERt*JxP$}*$92XHy`dW4};y%WU!|T z^2u3)8*K(ANt7g9^f;Bwzy-F!w(;T-=AlMrKUzX&yoZv#vYjDgzRtZ)eO8QS&Wm1B zocS{Pd4Uyy`NQSsf{GJO%OhvVWjwN$1dO2KMoFw6saupJ*;TJ8N9H{b0cW8DCS(!) z-(}}~anFr4$=CiSY z#DQv;e*e;t0b0$VhS}U?B;lb;rE9k9df%u7LmtfrlAn;Qk`tA)wsnh%M|reW6jPZ6 ziyn3?5^eYn-0jOCo%A>PO;~HrLn;6&UbAEO4d9e5_rheOvT2i!Jv{xzMy?gmkK8tWM{89SxNCA~#jVYQa3_Za z@0O7wJ=|H%c<7v9ez0pa@Gm5y_(Y#CCopM^eFDTvoO8+iSSqX(_F$x=8pB`afgUuv zr>M^D!rJ=K&9DKYZ`@@ii4mj@SMj&61TNYfLKYKQa?jSkM2K`poyWX#K zdq>H@_z=K-Nag~70l%cmlvrm;{)O|G!7CztslB&$Yd+T2JE-#mUiKrOyw{Dc!)@~8 z?P?@SKleVM2OzNhrN!Qk2Sw2i-#n)P@1U~){os@NuF^o8F{6_+-uO3A{m&qM&qaYh z8vo$wZYNPwEt**YooJZm5y9^W%W5G(jX9r}RZ2JRSs7zf-iE9J!2mR!4LSas;tC`E z|D+d;nU(&(^d&W_TG?!{BYt1&_Qj~kQxTG!gOWo%UR}Gd>O@rCD7v$g*X_Gzi1B0J zSih|7K#yT4AmX%`!2%Mh52WldG0pt_tNA0?>1y}9H}%m%h(xE`?foG_J-zKYJ$R&J zWKJ;nU9Wp+tU)1PM!8fp%QdWDNM*B~m{0?rhiXnN_K@ z9(p8QA76$%L7;x76mfK<8uw}XH8a%l%T?DYP5iI5(^q#_H087rb;9F7$|O2i^4NUI%yWx6nNa;F*jocS!0Zr& zFele90L>AoOkeZK3>``%uMDA%a)n(N$Q$u^LWzoPE@uZ@Gr!=#Z~+QM#5Xqu%)Pgh zK6M~&uTu9)u(D5wkgx+BS^}1c3l6vto_G-wWE=yiymUSoi6I0WM zb^nyq$#QHn{CbUF;o-sdm?udg^1 zoYo^tK0}uQ?UOyzW@O4!PB;y9q!S_%ftNg_a_LBJGENU4eXBQ(tlgcO)Q<%f+0qd- zYG**JSr3Tar1{VrqX~ z4WjX}R(t$b8w(hhwbty6)saTc@RmfrPg*Z6NR{yy>hv3j{3%MUuDL9^UD@&{80%$o zwQ67TJpTMuP6w;WRzOO#O91P&x!DrmlFqyY7u$tB?;hevC83Lt^l+W zlf`dAq9U|`YP3wEV1xr3?AWJfB&|60EN!GFKB?e5i8+=>&nDmmHa&AO6&*enn?J`O zDxKIKQ5Qe_4{Vmx5 z@m0;zHVd#x;xt8NWe_P#75}1-sMdlPEOH}7Yom*^stq9~2EtoC^NY`bGN7Qff8qM4 zFIu0&Ih@eQXye&r2eO>56BYVN>d>3*Sy_F50?7{4{bWhsoy))2VLG9G!?BSP9H5Ff zDAh2bgiuJ0vhSgJs0zu%s0mRsZ-9B7NI>1LSl^OI-x&G5~J zz)p)rFgHU15$;B@T_f*iYfFKB>?xt)0fx4j&CAvv!1o87O_3^NivD_G(t105J03=j zEMt1hLPdK>*qwlGchIReR++JR4xcFq?gX#J6x?WNm|r}@SN%@R^0k2bnCsU(nsq2& z57bgcP3R7Y-`~e}J zM}V@h7Kl^b{YWACq|bU)H0W;9^31}5sL@3PkEY`<1+Q<$f;u4}x60v*uRQLKI(7;UuPo-WCAm-XHUmW0#)$#ZUU&#Uw2B}7u4#BYw9$Li!_ znwEFSGsAHz)0lFu%W&lv#GpqsJ~FY;GR1e3Kyj@z5T+ee+&5i>Ht`!@R&wAkm(EUW zF#E^UB_<7KBr^ERO-~^3Oj{7#n{Z0$!B$x>7)r)~*2Y)TFI$q)8)&prGf@R*90;hD zwZbp+k_7VFhcFd1i{zfhEz`smLw!D>71Ct%e-x`WF1yeriHwUSH?tV>W32^mnn4-3 zI1o9T+zg8oI4QuTL{+KeDpRVcI+LxUq?qWe#}{3BiH>M|Q87SOk>uY-xGC6m{toVupi=hsU{ZXNFC=SOEc#* z53@jWA>kKjjdYkCDOK1z8M$^S;bN_TEZ6npPi-0IRU8>!%nm7m1Psbl`AVU52%V%$ z9;uH>xj`(|?KwE1OAcC1n!nf#cS?H@Ah680l*4X{+hO15?WCUOK>5^lG6B&zCpuUot>(f}*(Y~%8b9;8143$4EBp5*u9ep zx=TpH4f(Uv@Q^0K=05c798GE)MxadRB8XD$O$myxOV;9xE)o=Z%;{V0jZ6Z~eZmJq zRBA3*@VpFFNKs^t%=PJC+X}aV?$J?^hUBL0Dwxm*W93{Hs3r?6&OP+pGsg{)DoS*x zRZz(Mw5l&UhVXbdE>|V&fVfHC(N7`CKC@7aZ#;(cX9_<_C910{o%u5orx8;hU0~D^ zdnpA0zz2jSY^sF^QZv&>Swljy_r9!serMKUYH7Kpi;7fWZ!+&;+^>00f+XwO!3k=@ zI;MUQUrQ~|s7Ufc2+C^*28@km1baQne+id&c07tu-|)>@gVa!-sPcYa#6!B+8j}Iy zUy&@R6rMS8>JBeLjaUzM^O#!gm7(~*n5j4Gn^FSfQeVBFnZ&)L54d=6d?s)fS*+N8 z_{MT~M!B6eBmJ)-=za?n#9+IQ>rYdaej?C+h>*;-r7~Vz0raKN$>!#a6Fly1#qciq zHf`ISXw!ZhIXSbC>&-i}8#gBGU+dT+oY_bt&FN1}i5ni%>A8S5AJNgxdobXC01cMn zv%8LMTn4tUrc}e_6Y`(l6J=Qa4gv}Yr(y1Sg%M=a==)1`IkrVeSZNj=j zK<9Gv`ja#NI3Wb4*YJV}i_$xP?{!qk2$&ZG zy*uY@bq~HguV`P?Hx*REdyR~2IAZmUu}dAPW*51U%KCQN7o4u2KnR1TP`1ivI12hx;y+t~799lTN9#rQ`P?J?+-@&yi#{G9G#Ez;W0#<%-@BC5w{K5WVzt<# zr+uoK73ZHn#1(scOJzSLhMg(JQaUhj4`CQbGAU&XR1q1Mo|lmybUADuyxF9zb`>n2 zaf6MAfsVx;;uY1ko~Axid}`X+nzy=5=OP)oAXA?7#!UzO)1$Rae7W2+$=)0`o`O9& ztYw9DNz9|Yr50%0XNaK-=8XYCxfYD#L|LGW7(yl%)8V$m%1t#$feA!yV4#AKo!x+C zm_)Gy=j&VQeWEh3V%7<}IV(^+N5+WWWM_;aMj9D5UT9^AOsjve8h+`iEsXL2ukGvOr;K9!nNKWpZn( zG>E@Fz`BB$Q~iDOX$6V{CI<)}pHH)W@l8{R+JEoG%#nR(&@} zW&dElq1e)oc^B2Dwa5pP+tfOHhjeUZFhg~>239VfAyG;g9~a;Ij`-Zeh7GX8l|!^@ z--K}e$JQ+JvB3QAW`uzEf;me4bY+IekUPVyLWsQXc$VcMtE9a18K02znNyOo(l@-! zCwq7FYXi+!)^n$*58d1neJ7yfjpbD(L8g1*V z{_w(3gI?U^+`riQp8rfPXk@{?5=+JzeO_~1)3+Acn9nXwLHf%?*Ljy(t7@N~lQk@K z!QKm@@dKon(hM16Z#@LkM*$(+b|j`As-ReNj)&JP0LT64Vw?{`esjGd9x?Wq zeMPy#58HhnU63jK*JNK^K|Ga;Qemg;cjH5#6K%e*>AaD9FR4@2w09sPECC_ z>=67AQFW^EPJUrT4TRAe#iy$qwVwIxKN<6*sqcEV9f|Do5f&poPDOu?-5XjFL%tJh zfD$}omoAw!R)?E=rrBA3AVeZaLrnW&_ZN+2G38)WG z&M&9H1w_3ZeHaaSbqZ?{QdMN3bb|;t^;F8XfA`01P)*%_6R+T_)$o0X|0DK#t}Uf9 zr;qTcsIJuQi)+L~!`rXEqtSE9qWs)LWj2_nu#8+|+ zn8T7aV3?6N!Ub@f+PsA7VEqu8=1PqujQ_8_FM+3Od;2#=hLWjLbckqh=FwEf5Ry5e z49Ar5IHoceWh!GbL})-H5{+aivk1);qC$p9spP*k+;s2R-Mqj1`fvCBbw5|9WAC-s zde&Oc_xn6+?eAL3+7Q743_T&HaGoCe^oOG`VCdyJHnCC<+zm5j?G}+2+i-LdY)w)p zLHS~rL}Q#|Ba7_>Ax`0`QQ7-*5h1P}hQ6&Mt>L`VlXB6Y?(9vr%U2I|H$Amqqm(7q z^TaS7i)(xox@D2#yTtxvwoBzJ8V^|nyjRm4u$u@mv0ats%zl4`)HKdtTk=L2A;#0K zmB^Alx+^-yc zg)Lc!k6(V?%q(_fXQQyzQ6~=ccnwb?r@h?Rlj6Z62afwpFt2f}I8z;+PuY4oCVdY$ zGHbj(hh4F>%D7!VpIy7jOiD5648D_jnr9E)WOm3Dtqc7#aygwnS*ub=Ic)pX!Rj zW8g>>38*U^i6ub7tTUtjn-^eCFjNA19!f40vp^sTvp^sT z^D9V#5*fvSVG`745*7@UAXiZ^RDzhZ$AW>ao9oBq&86 zSTIO}+=TxHCPB)e3k8#~zrrL4&3N3eAPGwKTp)-9OcvUC5va|092g`)4B~NMkOVn{ z{sNPr6ei)oFbQfM4+cq)>v%9sf*5JSfk6_)Iv)QkNP<$_i3h`^nGp&h?8Jj%64W|= zfj|--43Z!=;qhRI1hw(GKoAMPP!I_ZhDam=99Z+hz|l}xLd5?Cj{xe7LL%WLVEquR zjw;R+M?G&BG7R;{c}pmSra6^j=e29b9RnrI{51V(z5Y)t^9Wsc3o1|su#Qi0M9h?i zFxqZmPqw7eShMe=Fk8%s-14qtxKCrno&W|`XdDrag^sa-Z4R{K5)vK`B+fzY{avMZ zo;E-bz@Q36Ai#kmiclmt9ugZM2w+$>Q_&fNLZ+f%V1+_s;6yBpfQLiFpkQEyLSx}T z%mUOFg@=OM`m*1L=FSHU9O!po)?-kRs3#a&%@`C|%|#O-((zzmHRC}6cMgUG^X-}b zf>uD~zU(6VV|?Nu0joqXL;}Dcm`6~tClL&gAT~USV2Fgn;AfV|9`bO=||-94NJb@$&b(fKoo-wE9VSAaxhM(its3@X|Ix6gjh!>7r@l9J3}&W z(`FwV^QFhuNx?)?Ym& zn#CV!bPWj%ZLvUD1)rMmL_GaX7SwqAhtaa&$`&Iw?&n`gWZ5tJk0)8e>&&#xK6K<)%c!+1$&<` zCRqx+e%fzahSTt3T0yskjlYN1I0FUuIeSMU@qgbiXuWT<3VR<@sD~^9rkdXDiG{gv zkffJ!{9IJMHa)9md4;uhm&_*$5ua00uCB~wgISI~OKuvO(%VOJQg&@ph(1qm$Q zPs=XgWCDn5fnW?O@J9LznfT_+aCkVBTsRnAnDhL@;^%Rg{f+Z3L$X%U8a)3T<-pku@YwSO;(e#C!e=o_CJ?7br0FI zY!6*AjxU#nNr3D)2tLk`mbow&Eb@QfB!76ODMTZ6cUWEKrL=TA;BmMv^+Zs2}U6xMpaQrP)wgQ*TK#M3H)O= z{=GwH_ZLY z%x|Yp#;Y0`Ss%+PvQNFDkX zjVe9-uU|aP>RGi`2oS3?%3dWw(?Vdp1agBHj8j0*Z-LPOh=qhG)IWvI#QDJHpGg`m zga%@qbOx@ZKrU@q%EqJhtW{0D4BRSgBonS|lamsM4IB7sn7Q`Qg{|UlpfwdxU>FXy zsQ~Z^+Bt{+iC4qTLr(7xPmXTG;0lWLkY7CdthjY8V^-Wsy__!zA($^)GtKS+qXU2B zF@HA+Hm?!XH}Lu?{e}6F{whmlt)tmmFj6t+*3L&-LZ-H0CTXkG^7+i=#TgHtG2K3C z!ElmZwB|E6@np@_ieYwwl8X8gDV;@~>;_eP#?^9?OxfYAPcPcnI^JIFu6ASeW&)OK z{bV{{*)e@j#oC*6sdme+(X`9oa+u#81^*vXp?@cpw*20y-bG@d#pp+H7U!pClIk zC%8E;Yhgb{0~Y7TSEl*|?%N^%(f`8NXkdk>M7&ds?)oCN>mxf0Z)cND1Y)HCGJ64X;8gFPY`tv`r4vsJn*_xjx*za%) zYiSOeXUC$!7!~B^8XAlRL2a&q@fXO2YiKY=0kv=q4N3#Pr*D3j`jF;f6Ab+00pvPy!Js;@H-t8-Lv99xN+&_g zCjv5av@Hs?K?*9JG-sYYABmywOX9Qwj$ba?V8F0+uFYKNaTNv(tmkToe@7w^1I7p- z#|A+aq<+)$p?`Yo=s!{kT?B}P@^mn~riIR+0#b7wUkf2_2Unr`&7EuW)hS^3{=HKO zB--!U{B>6$b{jM4Vh{Y>v~u>DBZy)U6Cm1XeuAT;^*p+oB9%Ul7nTV!0(vtAH^Qs4aPb>SjTXjDjj> z&3&NF%hJrxNt7bGUrnwh>1@68z3-zJr3czB0$az;VDgd!D3ab6}y8-k(4N=@efmKlPuQ{(W zFPzm>P#v-F0R6jJ)P)}Pg9?@Xw#T0@Y{G#CxlfLd${s$uurI5S@?6;#A-&U0}--thc$ z-Vfx3H?ZA_f7QbK_S)_5hT`+pE1-IJbM2Ld9`1n(>dn;<^TJO5!{G@o%lCgWVgwcc zgWPNdT}go2d$a`5oCM@*3I_F%Lr~ywOWGTx=IWyU9eExCXhLC*P7K=b&J(~^ALiQY z&~7HE%;8*{xzLFSsE{JWLL^WjMW}^HpgM|>t0)+{|27iMQ_hgMV8H#aT8iJ|u(^7b zg}~}TH5Z{qW5G2S=ZM_@4k81pvpCm5`tL9YsGK9j01#Az5o*&7RD^M^;|tmhk_49Q z|IKkTPvA)cL-~JMZ-8=yIr{j8&gVfD80XsG3*Hd_RQh5ea4`~Ca^knS{Cuqd05V@7 zStW>N>d>HKiNERK=gFdIhz1oqoU3d4R}fD$sG{O;9=I?+1OojZ3o6c6J^v{N z@Glu%VCxJ}^IrcLOhb9k3>par&~x>D3!Ijrkzn}$+e~Gi{K;SS7y;t4(9Rb`gUTrW zwz=nv#(?T5&UJ=BI&FYPfu#+8H;pr2!yhbf0I_8ZGztv&A$3|Q8aO5eB<}gYm7oCp z4wxZ+HvpWk8Sqa*_5w&Cpg~mw=j?r{H2pn8H(3t&O@{-DN2L6!I*S5dH78*&u|gV8y=4oD}z|6>up`HHtN|8a79zG@1t zp!d63lzFS^LRp>$RkfSzWc+s!#lMx#`#+-0gT{h|$#WeS3xVG)5P>}OY=Jx)XRUwB zDWPz3a&A-#+1!znO*Pbh&lY-4I^g&{)+l;fNBf{>i`jt}iXi~8`PT=aU^o&AHTxM6 z2K%YJy?oOh!^sa#W47R7Q7m;1hYl)m3kAu;(N@y zgfHPx8|9_~^C|l9A~K%)8Yz!UyUmSFvf4oFXomi&n)D13|7qUBR_SD}#s1^ep{9nZ zj{RlBa-|;|{a|#VwgFX#6MHMyOiX{MPuw)g_|#3|oS~iK)9W`DpX?0C*_4xSS_~V` zVRD#SF`Vyf(UF0B#9r+kWL@l>b8fN0^jp4n7q{q0_MqH1smj6p8{RA5KG1(G&-mII zs=aWJZouWSs@U{B_VV`hQJ8rNV(Iie@B2qnivyE=Kec>{jn(U2Jr-)6)W>^=$il>APOH z%`b29xN7%VNGQ&YV|zODn(Wv%$(IMxy2-1{*MB+?vSY1j?C#@@eLKg}Wb9d0EI9ho z!(Mq`ukt|j992jt!?re_ZP=YSyoYY9f80g6V9emas=KyJf~(B0z$ezYUGa_^h^j)l zjcf3bl2;l9D>``7mt?cFE=mtwEW*1D^|A6uU~^RCXya}4C$8jz$cqmSOc6uQVFUYL zuJ#q)7q-Y%NAl903wK_+7EZ~!6;5(~R1FHxJQ04{sD5o)^_>ghTT81-JFV;$r6bdQ zHk``FhNotxH4A?zso`I9_;F!^#xc@e1S93fbx~(h+eBhPN5PnN_LXCM)l9~i%+!{? z*cmvPRjr;|zOQp_a={5Z7QC8=C| z<(hLkz9`u&Q{u!)3^mf}aEnF63WRmo&J2ULaD9tINF}8d-h^9a#5$Q*@f&-OKHuSO zT)OsY{P0HYGwbEa1#f)GXDuX#*E`m6ik(qtPYlVHf4k*lxsjSfY9hHWS-%hO#?q>P zepE3?e>QJs92cIP>Eh~0N)l+5t<+OZ^x zr5Br`pP+kX(`oy-jn`ba`ZCJnHYJ_Xt}Hy^6q=b;=}}NLb@_#UV_pdaz2P2C+wSGu$bD!s*(DhM z$c93F{bt2unMeiuh||vw_b;axfC<>``*?eztoBBqpE^%Mz(m|cXho$U;jxc$%FToa zg4a&_MDTPoHuUAINR+yjcHbxoDZ{$CY#o0-rPuT{dSA&k3x%#s=1K3K>PTO!@vK8z zkF1c}L=fNqGHQ59Q{C!Q{?(^BeO{K+uY6^fJ@xUe@5$}Myf&2lRJG8>va3Ckb5!MP zM*DBkH`cJNP(LO|RzDy77RDa*j+G(gc1HIQK7{{pvR#K{hr^YZa@#x)oxWBdlVVKI zMsdUDpI`YRj4G)fPnPQ|;d|xRcv^flrOY}h{Yo0{Rhk>`t`gg0ks*JzuQ)XH%OWLs36!8YINHkxNFy=LREiB1tD1d0#czTnNV|4C%=19g^NMXFkRz4o?+=4|oM zxnw4oGK{fmcAiwNwJuXunO1po1u2=nhON?0_$>@_bVHT+I`my2MSaD zHs2|?RJL}7nKYa)q3~_1!%eKiPx>x$v`9#b4~^H~>W8~}KZfa;t;$m07_;SO+|Mql zEnRi|Qd4_e>W~v^v&vONcEk771eCDiJC^W8NHN*UZ&v0ien^lU3j3q7J zjU-Vbf?YqUIyLoY9nZQijq`Jz)ULb4`C%-?ZPFCJH?TyhBgadEVjNxdeito{K*si-rXq&XpioIonM_P+)~sIT zqoZ`Tk-UuH2+tqCM4a@wdZpsNsHzu3?5Ak?+_6ix0n%_jQyq3Yeuc%ZmH5S5P2yX+ z7YTG5i@^`~Xvc9SkcR45)3aVik$TXjl*yMhEDom)dN0DKZ@jp+<^y6F%+e{#5g366C?yU9_~a+h&jv4^5(f~2JBRRzymPF$~s3gay5@kQ8dz90c!1>0j$ zbSQyH?qG|C;(&+TZ6BV;Zr8LIL9F=fvn)bhgV(4``UKmCF8SRWDYc$uZg;M{NR{>+ zdU9o}LH7fyq>jlclZV{PmCenMh}>E!sa-#?dSCOt)(($q!Jdsk!>OCMcEQT;CK|E0jgA(~K$5ICGG_TU>Q%n7FEyjm#K@*O+K%hO`-qwCKze zy6$j8J<@Tr9)j%g@KV~eS;saMd4`n}&@5kx-4}UQEkaUOn z{O)LxfP*`(I*=51gk~u{(inKY!~WPnwzAKO)IdoNy_+7a#oR?to!$ti%OwYw^1WZ? z#m!1Dz{MzXQ{Wljo)g;J*D$g?8|eF-a_c}_Il11@swDTU@}6XBFu!_2mInM3zg?I~ zbG*oBbp2{J1$e=r$TgiR-@>Pvr7Cdw1BV&p2r{*0KAxhmPFQp?~Iq1(-CATp>ZI97vwAh&H9Jb-$J3Fzw|n16GG+VpgWWXzFM?q4SE;bXXeYN&!46AvN1PL-`>fe(84e0Ni=d#c9UY7mOR{Z z>%(K~CRDYB5c}R2Iasl_b8)hVZSFr`>-FMV7K7VZ1||4L$Q3pLqQVZBRV*LQG1><% z;<;ngbio9+bb_ltk0O1G#4Fx@vxK{#!pHGuF>eoc&k_GflHUPT(}sp~p7(Y;ofaEu zQC<~1#5fh0Ozi7W+GmH9UXpfVWVf(h&)SD#K14?2^^XbGQCV<>!SHyiuFv@#naOq>) zboo}sX?0aU#WhpavfeGaOD_v1!M&Dhm+jaVz+)?YnERpNWEQjI)Zvqvc%>V>ic4h= z^Wu#?TeR9TM!dWp` zV*6vAMW4pmXP=~Fzk4iyAA0ca**g#221%E)IX+hC=AV+&)j1olmk{$P_f)E(&bhcY zikN=(DXN%$#wq@d9_?q0+r9f&;|%rncgE|pWs7j95OLaq$ANd*g+PJfxY1Vu7V+ z7b*0?;yT4MnCjkbcbG3{SM2KQUAJk4QxeNtpVsFOI|O#|rZEZ;=YI&lJxowYU!9`K?u$k`#TNmRLgC6Xg# z*pE%{v7&2#y$5Sx3#ul)!>E|W#X$_Ssc=V%Vy9o&a;f7t7fnuY9^Lifu3QN(`X%AW z`AQB&*d1bZu%N+tX@@uvs&OOznDr4}CGwhHHB z9p0;s3e1H>-D!U3bQpl2(}e#OKltcb}G?<-Wa^ zBrB24a%?hQQeKWOjzDQJFGmL_Hz_Y!5pyeN3$hgOdFE?`2<*!x)V;DI@-x4H8EI<6 z6rCw#7!Hn?v_t}%4oN6Dpi7BI;Wxp6H-(4@6cT|ZNuuylIHDAm1pD?!M2-#kBtx;X zmeNsD`Sv*AU$P>$RH};<0^#ZD3HQXnohdd56p2JaAkhdkS`xTI(#_k6YVIZJYeUBD+~q>|CgJ&Q7qIa?LHA-Knx7A~O&AHCe_sZ<5rrFb9AJ6r^_sR{B$4i z3*d1Wnm3tS**aT#{xIZEH_BU5oGr|$@-x>MI#c#qJ2-oOZSa@=_-ZB@pvF>~&Q^BT z-b&_FvK-KjBuOM*5{=SB5~R>*DJ)VFiIqYkWe|V5@`tIvl%WVD!l>l$We|XyB+=NJ zGO#EqEOvGov#$J88Q;hD_fmdYlW+6j%e8+h=gSkEDSFP%4szeD0Mk}d(KO#nroezr z>($wme-Gv*!aelGOuw?9AYOI`n5Eg7KxmNVVuO!-p@GsWtXU4Q=eM`h(y zlr`ml_3)qX{qpd?K4@mRXlhC+Ia|8VOeQrYId^wED=AAH)|!k(VI>JDB1RI6G`Es8 zx3sX3M4~OMNEUc20t#jRrNe%{cUBL4EmzISjcV>>N&cf;b0UUdjU z%#NQfnsxKHDJlh23YcCf8N|p5e`3C@2c~Mb1(O zp#iW|39{3Agr+%VubPv!vy9A_k7O$tmEum8llvn!g?$ZYbr9O-He@&8z1A-h;yMUj z;5tjH9)(QSadxJ{uwR6Ab-u>6ia<>3;B131up@htDcTgWHJL&NXq+4H6eT-17YB21 zfHCULFzj!Slau2Fn&WI?&k58V>^;4icWS@BSxb9I$@lj|kh71+1)B0b?w&Eh*9sbK zx3DK$Qh&tVs-ZgiE{i?t)Eesh7=R;jml!hKp>5V#8}WXT2tg4DvkQ+i+~wR;%J9TZC%A0I#oV;cKTr&f>_xu?wnHt;vK0_&&d>#EqJ@Z;FHIw{=IkrA>w` zKX>clE}PPiLxWSj*EZ@NA1*7qd=(phu|B!tiCkrncc~!1%{hJZl${-t$ATrp?&dmi zmKIsRy>PB4Cz0-C+cCyI>4yEIq z0?frEFOErWpO_B0kryGG_Z!16Civ{EtTbV~#MFL2t>wAYz#TQR&S2w)_B_3g!yc#% zoVxUJDTDjh?iCBIJtbfxB*?Y)T108Es@~}j{1Ft>3qlbJk}P4Zj%M<4LbhbZjk0;j zx6BS<=Hc-1nyZE7?%mUdy#BdX(Zow3arSyVZ??z#G6W06y~i%jDDv53NN1j&>KY+E#Lld@(bO5?6_Yx${%dK>hc zx(x%i_wg0_VO-VsO8XDSwe=s@*B*kef9Lw_n6`_u?X$(DSw{!A#DC~9cBkT{2`U|3 z0%GmCW)Tm!Qn!q}q(jP5U#{H4v63+iVamxJplK|SUU>6#;MT0ji)1@((pVc}s8sDR z`JB>w9t+2%MQ5rc#Px;`Z?C(2VGo;*Za0VJu!3lOyyT-ldCOwYqXH#vA6a zh`gt*p*d@YJL?1kQ%1I6r%O+A~6 z^&)zUnvaA>C#~@8dpE+sMjntQSgya3{6sNns&gA(hm*KIQ|)pEwpE9W*XKH0go``~ z%8(iI%h5F*=Lhf%U@YzkjYIRO?vW>}<;Pk%ijgLyEj%_|(+R@^d=G(>X1A`yf z)VWIP)y2F`<5w^2Z}BUqL^>F4R{LlaE@bVwRa*A?>yP42_`R}?-5!+~%gZ@uo;&9I zC?g7=(GwU=#t+nRjZud&&TLPfWTmT8?}?NdI_krQKW{>HQw3pdB34Z63KLm z{>Y)uTT+~Nwd#hb-zROxUL-mBz{rruv;4PO0ZU>WnSJ3M)0NT`4TwMmNF3`8MjI=GgWK z+`F79vySo)Q4^vMtJEJ{?8z|iirW4z>onr_!#7ujSa!zf@gI6ybIak(ph@`E{9JlR z>Tx`!O5@G;hRz#dxbDi-mc{389AV^3H<3@&t8j4-PGpMRVDwSu{yJ8~3CBk#9lQOW z?df_Z@u5yz`u;}q>yDNLscLI0EzQ{TD;pKU7(0u5QOCK>7&rsph?=qaF5wQQa&x~p zx}hKSR_b0SAj=yH>as={2?^|Je8WazUz{IFKYH!d#%mnvoUryU9LCwM%4E*a@5 zcB-y^V`TeYfr8y${hhsDWnJF8uB}SzYD{wzpB9^7(`XeJl9;X&?1(FpUU%SQ$nm6- zCxn1z*2!qAQiq*S8lsgBz-)$&m$OH!bA^ZZ1$k}mMZd&95cK+Z;0fZ`am{8#V!r5y z!9D7!VW^qu!I$aL4$= zkId48eNG$HR}}BN*7Z>4#0CZ1yMdM-Nvi9P?)Ku^v-&~D(z`j+hb~x6a0}_1U7JD< zokQ|!Zw<;lf6}!pHOG9y61LPQxwlg;!sQ9sS(!&UYuOpcr5*Y;tK4D{(hWr~_qMa4 zFUeWw`m|tA?82|EzofNg)x{QT&6_eh%EIQ(i#+b1%IC?ybcEHvS7>?U?YPl3qbkBW zrICH<8NsO!qS>tXYo|2a4O9@!mbkjS^p({z8}YZd%y@JPY{Q)QxBCl>QMKuc1>YP^ zAY4xfX0$LLUsmv8v+b7Xn&Hfi*zueX3D>uU)RqtKk1zF)BlQME$5pzV8+_zNFT-&n z3H}V?wCo14Tfm;w>4y;1(xEZHbWY$6GqmD zNqs!Im3s8S(gke?vCDM zZHnAfzNJb*;`GfcuTD}>ECt~*&4uDmaszxqV8excP1URgh)+`w#){oOW36-ewpdZSbgG)82c5`!h~OT`2#>p6Jt+8bET)y?03By-_%!?36Gj4eSu*lba>qa2a1{ti zn?9{|R{g$wA2l>2MFtiS6eCr64x?Rk=)FR1xI>oDicImsYKnrqYxes7=b;<3v9CvL zKAD8(^@I<(3Vr5i$&;k6K(6MvS+7ir)%BVRpmU2#@7SV2(kJ1br1PD*e=NWGSi6cLCqiAUGJJy$i&j-(R_t>&c7WPWc+2Zcvy!G7AgXt8HcO4rNo z=XIhtZ$>2>>O2>EzGCIFm8Wm|r3OX@Cem|o$A+`7W@PZ$GI;&db-hI#Bd-M**B&m- zWk<8?3L*Ar!X-A)-(?ASAR%^%xKh=hzPsT~48uvHB}0^COdO~6fwoYqLrf&bhk{%1 zyVdNORU^ttWhVXf6zHm}zgzFc@&6jx$YjzN&hvS4Kr1)@0SEPi_y z){$$=#SAuF7Vqm@w_(MrmC-LLB6Jd)gmdo*KSNsZN+0D?40wJw11%97U3)x`9(I#X z?KTlX%SNc#S`12LOJ^&xwmHRdhQ=y7JGeVKxxsJlUI<(6O|M&Sd2Vc9;tvPC@LzG2v`N2yqt`ZGC@&EUInL2K&mKVv1kPqB#D4T zt6+h}8I(LwwkcwUr~?fA%h@;phbKGPP;G&YrGU)A3odl#dkAto9uYGvK$KNB?Pb`qjGYMDaJA=zDHs_uQ&#c3opp z**#DY567HaHR$#!_-*!zNzLZ=Fza_Jk=Ok%W~ynLa82unu3T){k{jyq*1e_8AO2XM ziITR+Q2Efv`lB+rcg(TO4=>uMv6jfM;UBDsT& zP4>4joQn*}?IBuLxp^-m7>-X|4U0v`w`ZYqv=T41PPsE} zzhj=gW>U`Uv)odlgq=s<@2YQoYMeUFs=L`R)jxm_`1!f?&A40dWZIjLVV{IVJ2HnZ zej*RH%T|u-9iZlz6;H+wR_Wd=`&=VuDV>{mMbw$hK5n=d?|#0dNF=A$A3niZwqf{S zc*7&NmfXXiURg+t-)?$WrsyEoP}?(XjH?hTE*yVE!{4v$`Y?|0X^W1V~M zk2hY8QI&J%%$bo9`9)-8W@M2oh=|cMF|fmv@9a*_!o#q#auP8T*%?~G^Y8#4kx4}CGm6hY8iAn$&6I(N9b0SWrKkae-i%%dx$gOW%I7`6#~ zJ%;+$jz7=_ZAcGNTNLo^f{1*Shy+c7$KFWa(D)Xz4hsuwgy@Z>#0MOT5#)jcjtU;; zL%RR2{oitSayD>uc6T%}g@<8;hao2yl^27D`H$>BgwFAYqkng-FcC8c@E>LRA%c&t z{ukHG>}&vKB2E^78WA%a$A95QUtj-I-Xk0x99)McTn8Lf4=kDyq+wJbTxfPF6QhV6 z2<+j;Ξ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�@Qx7Y^$r((D=BoE@OuN+O{UFn}RvWE7PVBPIaO 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%0D?q6 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 O>1hN7mCh;C)BHc)Q0|fd literal 0 HcmV?d00001 diff --git a/benchmark/datasets/pdfs/ics_206.pdf b/benchmark/datasets/pdfs/ics_206.pdf new file mode 100644 index 0000000000000000000000000000000000000000..885aa89b22dbd381201a430c2d7a21dafaff6514 GIT binary patch literal 107649 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 literal 0 HcmV?d00001 diff --git a/benchmark/datasets/pdfs/ics_207.pdf b/benchmark/datasets/pdfs/ics_207.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b39f64f55dcf2b436f1fa178e0fc2d23c5179b2f GIT binary patch literal 37300 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 f7b=2{`+>%IVI5P+h_Nzigi literal 0 HcmV?d00001 diff --git a/benchmark/datasets/pdfs/ics_208.pdf b/benchmark/datasets/pdfs/ics_208.pdf new file mode 100644 index 0000000000000000000000000000000000000000..362cbb4e823bdb174fde22f06dfab8fa7ff49a1b GIT binary patch literal 143117 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 N?>iz~p=m883Ge(ZW_8b+)k+P@ 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 literal 0 HcmV?d00001 diff --git a/benchmark/datasets/pdfs/ics_213.pdf b/benchmark/datasets/pdfs/ics_213.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6fccb255379480a9bfaa43af561673ddb6996dfd GIT binary patch literal 26863 zcmeFZ1yEeewm*y$G`L%^K(N6bg1fux;5xVl2^!p;;O-8=-62TO;O-tEz(U*#1P0dX8?%lmsuiskTdv)*bMIkRNO2Kmq?d2S)xoHU8*0fJw7_hPsbO{ zCe#HEmhd-i^F4s)7E)C&J9~J@-1t($L6CTZ!3k~~2n%*~C!JCS=thYfPe_rvL3s&& z#<-6IV7&$my!e2&YVyi}6jGt@g_J-9bA-Dc7zB?$F^s35KL(@)sQ`a0IlPdzq<9Z> zXaRYs(p(IXT#`fnqlg9~#+bg3Vp=TDwU25&A3nGz3!gK*&mtXgIgWv<%va zUAg8@(E7ue-%AJ4-OpjLWJ^BLOVWYXY9)&-hTu1ZP6*_G_5p8Ea`Brh-vb5t$f7~i#=w6>YEO&`kU)?vw&UI3*jt1iw z`yMEef)?E)08>MSj0VTIpTc)SuaZ*|VRu*GetcY?@@N=Zcf5NZ9DVVI9lE?t6<>zF zKD~%Wg)eK^MT|D7td0Nt;3Sg=5=*(Ru(+9E&S!W=Oa6XSH`8j|5oTx=}nn z@yRoC)oUw{SrhMip{N_4C?iRFT&p1nOchGAKT zPKi=$ww_rqkb&U$a>lXc=}+~F6q67ZND`1B6ZDM#c-MJIFv#&@ClRnHk{r|Pyg1#^ zO!CEr?UhEOIUtY!0s+?U1E6z4fr7zC~e5oen3+kuAxjYb4Q7S-Yd{0F%N zjR~?ffZ{C}s1n$Rm--xFH-?6U#K^51aSJlWTcK$c( zn3+<_Jl}_eHJ;_iM=Ixc@Xr=TkMxk%T~(f677gBL9cjEl+DjY>gZX0 zUl&fLws*@UAfujVyM@m?I|)58s?({P7=@5TZ>?qU$*wk9DAbvxWhUnz>3%JQVRJ8> ze=PGAxYIx)&|V_00Zv(3j+vqN<+hktprOX}Tk-g+A%Asgk{2tqgB6)N^H z=F+BUwu#o|972DcxsAJ8F|LRznJUA`$1}?|jVI~N(pIIXj!rS6jKCa*tXVrIAp#!a zZY?IKGzyYXdp}ZVE!2p0RcJ}%1FEB>*0?W05mf~K3zO6}AG4}-DMR${jVlqtiOmG< zsAznMOmBjqohV*H+NNS0`-q&)d3Id0T3+JJOI+A_;Ro39SejWeZ6#+^uNf0?)$O6o z5cZE;XJ!VP3-ur7tz_1}Jk~k8WsQTqpuzUIp!tUR#nT&UEAiq0TglRsq%MDt-FVhk zws=yHwP=BxqQ^hyeLvE0AU{n}#P`_TeH&i%joU{nRYEkMmbjG!m4X|Rgn9qceddMf zRr@fux7|*MQ@n5k`*6f2{l-zaN`&O7^g8XaD$y<}!cY3Dgwg~Xb^ImBu@z&*=kG>} z70y*OoHAIv?is81$#H_J_e~?8qEuXVb!~CHU6X<;duG!IG3z8y3T}#^yyv`#?B5Zj z%t#7Rq#s;|_FpznHl%D9`;KQWvADHs#NVOQ?aOW@rR~Y+m8neGO3t{{>VAoT6vrR2 zO{YeX$qsw``X${ad^G=*u+3(=EHH9EPWJ`~cjcBfYq^Z5W3QvAlVGx#lCUq6wSZqt zvsgBShcTs+1G;CeKbA3{NpPQ89>*K$cQh()%hOtRWOrIAF19oAxwAYS;i$^?UMD{~ z%;0;ad8PWvQaOvg_*PsE<}jPTfkM;OUUG#??LiaAj@0nmC~Aj6o{g4}RKKR>LUa7n z9CDkMp!)(lE23e_4N~ALG9{Xz{JZHHRvt*{d`IQ9FN@r!+RGR;NIo*q-ZC6?2dq6U z?``q$I^?p{Y9}qHAaHah!iUNP+?UN{8h|TYdQMN}i$nw_t11UJ`qpVhpMp;EWFp_S zC`!=)E>*S45tI@_1((FUD@}72J~5ODoZ5A*pR`)Mg$QCV)@~w%F+`|~4)^p@BJX{yZew&8NlzlHJR^-s{aO^tvnrDY>+#HtGck#HZN{{3E z1KfRY2SC#Lu9|uAJVLi|wap>>ByG1PWVc0OTdkq-QxQJL5Z+J}e1yJKe5{I%hKzCi zqH6e?I46I6Ol1?xdw+)Elm08;s~g%;#oyLRwzqcqpYih|_@bNXN^%$hVCQxvK0z0e)F{QrtyqDLw z!pl!jN_}901h9LnHnh4`)JpsV?@KLjleNTj+hz+91G$9J%cenrYf$#*Wqq%C5x&d{ z#AD~R#`-AQ8Tg`0ZNO5nzcUGtDx27)n&!_(vk7Dlm+fZt!kBBQQ+ErWAb41vYmWG& z?U0YSmqcP``-b<0{E{0!OHqiI54rWD*@yyt0rB> z7)EIn0H1Hhq^Tp=Y5`E8Fw{PS>JL#ve##F%J(tsl+}u83kYHRLNV}391R1z&-+Kve@OAoUkLbH(ifFaf5NUQm& zNG$X(1v-aO-=D-o4Ylb*MHJ{iq^c++^n&W%93QVT!`$r$zE~L`qQ(o5M;bC`uFbJf z`0fLHNFB7sICJJm;=10?#BX5Cp6ZsZ#=~+G_65hkvcgsGDY2DZMkoVDWX;qKx~_oC zSMUTzzOQ7OJ>k*E88x1ieljmhB-8x^wV&nkTiewpa$zG@%n~hEIeJC?#Ju%uJk3b4 zHwjyK`^I!46La;O;|w}lB$lJElE9`d5eX-v^unFeaPT7E;Mo+&^ljHBqxlCJHh6j> zjA8@ci1_hg4qw4n%SEbX)=+=$(}Zffkr;ot#8Iwp%DQq#YeYhVKu?jGf z`hC9NdV1b+x zP9w*;K#|41VYnsNY4b`Q37>^VrKj?d-`{_-6KD2(G5WP z9{=E1p4;q<*cnB+sR~H2>BE@O5PYRBsz`EsUy4DvsFekKB%b3nQVInQA`?)7sMlI* zq48E`%R$0_Wr=umX{r3e!6MuZKuwI^sARm92IKZ zO73Rhk|A-^POh7`A;J^P(s?@!pQs|z{vVLij0i^JaE-2|2l3-(#kpwkJC)PbUwFpgquVz zACVavKSP>yk)V^69hH(8p#hh`roqt!V};sl*{0YjFy78P@W{{IjyFzF-gn=HO{9!h z#>(9f0)vBmi26S9N%~{9GPF`9a#SJT0OGHMUeykz$KOiC@*haV^oK~yj6bH;|8`bw ztT1We|MuB1yrijsCt;LyPy%G;p5pKdDSqGVB|})3A!B7(|E_8{0M;VCh^XZznA;sw!CbcXUmF zvLDys$JBjNSq29>y511jlPwSq?wjcM;!v`z5%ET@sfHB85;rt6+L!VpuwDB)wBfva z(?6*?t%y`(RXDCWT9BZ;>B|q_ct>KYFtOZD_F< zp-Spz=PN=XG7DYdmqLsLC5RR}EDp}@BCoUyL0Jvqf5;%%%q&LWzq~G~y`AXrH-|Hi zIY?xg|A6%_A#`vs!1!IQ4=2aJ>$ubEGPd~aC=WC|_1FwS1m56*z8o?WEap~o z$AhN=_Vpon8fcrWC{Oo%pvLMFDs7hLTG*q*xU)r7WXBej!R1j$CMTsDv(Kb&YsvbcR!u_@JnG?!Z8#Z@=qc$HrBXPF>AJX+Xq1hg7jzmDV&Gc~SG$ z2n9Fe{r+5cKy5ElFxhCM092Roy1cTR@CW-2nGp`Kh`4ZH0>&=kw-JS@QCP3Ldb%Ug zO<+1B&vzdSK_;-XszlOu9*tFNsRt6<~NR~=9`x; zH(&0!B2|&&Zn1HT!&D^sl0Bl8^NK#J4yrW2p&?S?DRxyplkR2499QO1I(u(f=c!I> zvzsz{P0fH9fXApG(vKEFfhPq>P?zQBO#G0Oj(t9R=w<1WPEj7)D7(%TazOr^&|P;A z*@pg#kb#ZCK&31H>7%Q_nI9Sy#S2N4I1(xWdB2ZP0&u@(ENzh-|M~@?P}syZDy)l! z;8WO0bMr}@_{(}si9df^%*nmkXhMg>aZ^7>SPvQ*HCM4d%%vGvjdQS>;b~z^Ek`iB zSt(23E|eJeJ5vyGW1+XRSwB)qtR%>KRB4w7&I9!5T86Kz=3D0wFZfLxgj~L+kMYxP zm&m2g-(47ayVen~MJ^AI*NjfIX1sHsyIf|tz`b-?>>ssw0M!!_>(TK;d^-8MGi-Tz zGc}dEW!QGez&~CIqkK>k^r}6UrILQ6{jww|i;vH zZ**I=ViheJZF|Oje~Qhw=j3+xzJiapOwhETT_-KgaYyp5rA4?twdHufAk$^(!Qgle zaO0Zf!8M?hFh}fC(7iNjaC{AL;Tw6V)DR0A<>IoMoiwP4@V1)OF`~h#s*&FtY{z@F z=n+nC-M=WVlc8g;D=)f|u{N_XQc6wH9D?4_*^hW!-rL{UFx&Wm)L<1x&sJ_el|PR0 z*yS=)UUqHmWL9FNm7ZczvS?vZBBN!!xX)%;Kbm4)W3pH}|09Xlps~SbxnvT)SZbfj zYE#o^J=^$OW;qLOs%lQt=gK833q8DqRGFhi@;kPWHkoYO*xg@}ujGi}WfJ^o*klp} z-#q_i)3J@_G1P~8ML#J-8C^eJgo#w&K4Axu7yEAUTpXtIfIy+?H>^{r$mKn21FHyI zetccG39)?VjLVLu%PsRpUq>?;Od1|^+6kJcdiSrT9_IMdFDR6G<)3ynxSsNqh9Hfwmk%+bWOrFj%_*w={ zl1}sqOCRjLv+n8NJ{8#hlcxg9A6^LCFVo0>JCA%H$%)(zK!j?q=8tlns|D-!ioy_7 z#4EaZB7V=M-Kz?3AToANiY1IfBP<}9gpCao&5!v`c1B?k-}m$3kUm|NPg##H;j$6B zFwOzq@ka_{dGeD-#M=$~`!>O^_1og_2s|`;n4O~YH7}2x2AgjtTdvl%BX@V50Yrc% zV-L>Su{yq$+5^WXu~0Go`{a1D4%d#&I4@`%u#zLK?7suxAI1Oe1pq4t3){ce`^7^_ z)fPcZeW+eLY9nN;pZ$_LR}&PAB;zo|%BiCBoVI*Hc^%NgO!+>nE^5_}e7s(`h?VM! zlHaSb_47}>?k;=p4!_?%JYE~s%$F3%jc1z@9mY?gSun7r8KsobR!kJ zLa$2#t4T7Jg>KqCXDL#$tewhQ9>df3kWmCe$ zyPS%*h71=jHnq2GFSKSPuXTA^^=a7#ppseae7wMU$C2rjo@7DPdS+5@)#jwBWa_T^ zR#n&U$adpR)#NMLSf1ucsc)?TD%dl|OKs z#lH_@3JRjnVa?5jnq*JEW7Lfgan8~*hlD5bEG%sLPy@9d6e)9=zj4EzsPo;docrn( zsGw|^Gq(%4OwPKwoSs|4#T0Y50T3o$R}$Ul8Ki9i;V%0zrc4(@dOOkK=a#=M3Lw@q z!Y|^*g@~wHbF5iu%a+iD=p>Mk2S>FzgmQ9SetgA9qnA5LXj~>Nl&D)h)_W;&hU4%P3GcmWFsH9AL;S|HF%b1(NZur3Oc&P+R9PUs3p1I=<$r+? zsbwgpKwOA2I78FM_5p9U{aE!IwRT^Fn>i{+l zpfG`#uyF#f(=4Q4AeHY00kjE^M$~hV;Yk+YM!_^FMo)@$QI4N6ev9UUu~A#5igQr<#%t+E=J;DOvqH|R8XnS!PKyIhWKH^buzX;k23l}UE8r27Pi8r1` z#Z`ne+uG?t#7-2;I0sL>=q9&~!H!4^Yt^vP*b3(U5bn|ghGmaPoA?bOjTaDIaBA2w zmHnu98o>JK{lZ;YZc=QSr7gO^B4;N04W5UH;kmRjDvlK|@H4dC$K;aSYvygmx5#po z%<<4BkKpH450dLHQJA!BDj8%?qeD|_7lZo-6t3d^J77Ui0F}DkQ>Clbxr>Z*2>H5s26hI`nyY$5L8pL&qfWky-H{X zKcx6wanLjCeGOj)k4DYZ;aB0=NxAAoRB2Cq$xiz53P4UM+}nGM>=1^1m|9DGU9}>d zkN*OU=E>p?I+15}$mimeuR^n=f7Gz)Iy9iV4yd^81ZYpFNO(!>rC!fqB$2R@AIE~=)5$j1nB$AOH5ElXoQ75k%(H~<1pq@6C#4-ZDC%za7bHyh(ZK(_hYRb<dIx~3?o=#1oG!lw2=Uik?0 zB7;m)iY&r7wuP$;M{+(+phfzfzqNB3V~p9(gdII0}cAb?YSx-W1);ExqkrQoilGIL?9uj-)RN@c|-Mtg|Zc zMmUSo`})G4!T!i#n3)8;V1uA-b1aF=j z?0bg$Q^H^7Gt=B(F@5DfZqV=th3)Y4*7W%7k!-w-vcUtxk*ryEFj#=xj3?^sFTS4z zkHP@*bwAB2(X}4|P}Boyic+CT4`9_9)*8`Blw=kq*Xnkiz*9R(l={Y_Y+r|P6BMA_ zmVr$|Tq9b+yR*>$jG@t|a17{Sasu?c?I5*`fp1a2pD@PTg>ZvnJ0|seKi9 ziB`N%=?yH@AB~C@`Kw;d1uu}JWKIO14ScidgelbK#(S~$zK|Z5F~b3VcPSbZ=3725 zQ1lekiCpyBUFxdVSj-Mtd7_Do`}{mB4azn#Mv$Q?aXCo9Y|~DGR_+rfw%jMI-q*T8 zI!cmXzSHkD0027IkRQJ#?&~auJuq2EKoo}iJH96~ix79Xdi?%Qf-dVFWL9j#3!TCq z=mO*>+OH_*9$&>K>~N7xS%vdW+r9;6Q0Db7qZn2`Va zv>frl1N)#?EVibm8o5opMQhYBy7WZh@bFOcjZgf9%xZI=(_!cXtXB4;Hq@$+RaHEsFXI`b=l64)-s=BPfaYzwfY`>M$=4_d9@q5WGcgp(1;TWojjQHVuxKeM_lj+ zSZBb64K^Q$P}^3c1`DOM(nDU8*d1NCSvGsmY_BO;+u6@o(<#h@(WAWBV1OuYaocd}Ebe<%$(a^W3F)r7ZR1=n}g; zA67eAvAbL zk7+g;+?NaJbDe?W#v$!@=T*kGuIAJqAm-orA%`$jUtr%73(9=DkrVhbhxZk_;(kWW zu6=y(ehZR*7sFJ3vSyVfK{>o)aQ3B#H?V{3$&sf~PSYh#4xf2<-mmyfo@U1P?tag2 zAB=b0Vj%#wm(mc?D?1Xd#V|}o%&3eKikRXo$<--6BwM}!D!nzct5$M&UXyw@OmLn~ zvsWA*5G>^>PxJXBTu!wqpDii$k(1)YSgWLfR)XKLCiOQwiVmb^QdnZPZ7zk%MSBaW z?6-6?H3W7w9x7C5+ho-wxf)RckEj|X-nI?a?0RGf(8rO5o|l?VYOtoQ9}yCqvY>U; zq3&jeu9-@2?r6}^qo`Nnq0l7Df<@xDjLNz1&gnG1cx(?h1VP<6^&PL{Fg1Bp;4b!+;we$_NR+3+Dx2Jo|+$v9Z z9`B)3pC4@8aYAI;lKvn;F!XEY-Hzfcc5LzjTv9yt=xup<)_&n^ApuOn4V54V_ZddG z!IxEsN#ZV4c)Jl2!X$c(FpCCnXkJ1i3~l&xZc%T_#F;iKy4?xH7tqj*Q_VB=pi+;& z#Qy^S^8L}5>2iK%@b)G-JP6f=;c0$eHq@xIz#POQ6*6D;1SlJ*6-bxUgUq6nm$ri9 zCWAb$W?oV#I*qdoY(=3PIg(K1-(*2*AQK+}u;_66s}sVEETt&n5E5wB8|sY-Xf>(@ z%P4wMcGjIYf zxO%6hl|*2wFTZJ5Jf5Gw1O~voh*eE2c@6-oN5)Hyv6 zu;$); z=jgn{Q90|p=0WEx%F6e3LXi+ZYqYSVPw(kh313}Z-Rp1Gm2}nwI9^Z~ z>2Ft8<*O4}VhrntqbF2ue*spCc9PDX*D03n(c@Z%CQNMSXf$FfzIIJ5?@kQ}ol}lin#eB$t|FD9-ZJ%U}$Bo-6F9pyG`jIG{I<}?nELZ>FcsTIG5g>7G$Y5Si%e* zdV`8tZ{BcJ*xwe!wOq(P=sl@lXV!k#_4<6|aL-?bq93f4q-;+6{_34|iv1ma?x}0g z>)jk`KsVc{a|R{#Id=vP57ShqAq*R zhg(yh`%H@UxLF~;um!ztENNd^YHZU@tFj>E#``$oahZAAE$Dp!)qaE0M!|gzUsT_J zvUGBy^zJVC1^}^!+~hI9HJ#-pd?~GUUK}AFxRTFgldsC}RjQUFZilewIwY`jv7EH;j$_4v4M_gi(gmqf>?4v|*q0ShaX}yChF19+ z{>5c%?`oOcu$V@MKs2Dh?BglrY$QY%=a=FJLyt`=%1X` zia(VkRdQmhc<)xQ&OA{jbEgq{yR+vSOy#bpvCnp&HuY(i(z3jk%jg2Uw%KlV-SDC? zKI>y1N~=Lb(?i!uw4oek3%CQ+jc*6@|4aBx+QxLvI9e zWRG-&NN-eo6RWgt;&^k|^OXgTo10W)eq4WQ4+6U5BOnZ$%EuV$ENP#qGH9YAj!$QcbSCz;Y)+bdlw?g`Y1WZ zn?aG48M9>%HWJ)SzCh$Ve2eCZyvQo4@oPplfSaR;;0cj<7-!yAf~rURzIEkp8NGYp zJ(N=c!`w@M&jZXBo{S!o9eO;^>^@++STszlweM_yh2Et_55~hH&yVnK z6NcHaakzK&Hh~*@y$r`3w~_evj$fUYKh%qKl#HEuNg_A;v?c&Kr}M(?l<;mHH9f%9 zzB{9SP|FIZz;9k1Zzo~JFXN~)dU-n+j@A-Hm z`RV2{oBAue+7bqB&pTJ(lq@+?wU%f^w7AzA_{MJ3n`w|tWA!2)={vyWhU>-tW^MeR z_b2?eHqOZU^UAn|{#$S)u;;xaFQUM~|Ayp0ZxHy6Br`h;;I|hJj3lg_jEp~X2vhZN z#Mr`oT0Tjg*NJ({MiK=(WEJ#Y5RJ%>Ck6+L4WCFrT-;~*)Q&wh3e1mMo(OXh0~s?h zXd2WZ2%QW5p%%!7*1}naB0uc_PuH(18=0+=JCK#liIs;@J;j}h0{8CH@px?HVcDbo zgs<&>)SCn>#LTBBIC-m^`1YTgM9o}iot;CiS+(C2*=wSC}_7Zx!h_7@cUv`AFW4d=u&O-%lvhY{_@Dc(tMA z(|FMno273dU;!TWo$G$ClD4s4H+Q}df*O5JSBpqnwWjE?kFzhbrnSLaMCKulEJ*!J z0csSDz!2vGI3}`FBv+{Ez@h6fq&m6#YY7z!0q0ctqsEhB*O8pg<@~zd>9Q(Aye;wE zDWr5RCvef?DaNQ1aL{Weu#4ESd(eoju3DmCBpo9IvaHF*M!HKsrZ6Rz;fwq=mIXV% zz-)}NZMXNU=$28#=R2p|wd-UUS$Gh8wL~em2v#CkZ0lfsG_b31u~sjy`<4?jf+ekix;AXh}&7Adv2e4oEbJE1rys@ zrbY`!^pa2X!NlIh)$j!{A0FU;HbV~RF=(Q{CU$6Yi#v$wVRX2ZM(0e$RSyudaszKD zf}MiN3V|cVkYc>=j_{p`8a*5BWBQJgS`}V1=l{SFSi<7Fzf0it$~_duUXiYDymEZk zzWRaJq52l-Ufl2bee#Dq^0zuYLKj3^Ht z)TYcTL0T8^YVW*YWv=@yGRD-0JBI3AC0~kl(92e(b%;rqt)jge56BBwBUO=2pa7Mp zy~Z9=0VR40JZc}u-UF5EhZ-=lHmUQ5;DI^SLY##JN?%nUv$d7Q3U4iv2IW&{zRb|n zf2~JcG}CILvuNO)<>YhdIeqh06l(P5#B)JfHguK!GFjQ!pM%DeR6+YA(om*3pyDmK zz;eXaw@@9XBi6_BG~w^-6t5|x^0^DN?MTI|KVAO%=yQ*!0I ztJ7^latgbht7;#D_X>2W3PkObxVOzQ^n+s}5DY(k7ECq_<}iPv6Hzd07(8Pdj~*#h z@E$Tq;EvhD^E*d7$&_=o9YqVeO|=W4UoZtZ9-ZSX@`MXogypRU?_-1Gk>HuT-}ha& z54LW5OQI+GT*qC9bafN5FM5ikbtKLaw&Zz+p|3-ZY*&aAW}vpCp^U2Z-o zkF7+zH?$ZEuH``Ax^47_dl_FA28l)B@X@lr_SlU&K^_|-$bHwJhvd22368sGUi5H( z<@HhS1j7_mks%QR$4lUxS}ZOwaJJtIJ~{x8BbmSllq?UpB0=!Gc>xjFm%V<>7Kjy` zYQ9XjY}K^OC*=~9*D@TFrU?!Ya$u<_$K45aq?3#X@@+R@?s<A%cd^(_BFLiXArky|4*-GV|TR73#j8xV`A*&9(>> z0WaqpG98BVXGEeg{a!J*c3R}rj_lA4*)@G{;)HP%bdLx3iiu7mWcPi2+*j#_=&=-D zKPL&RRVyxQ>1sJ0QvB9gu}+sT?S$EQ!uD2t(4<{N^odwo_5tgBvz52a^mxW(ppxCG z%B0io8}2BF{C11lL3M^V|9Gd7h>1Ok_DFdH2vcf^^_r6H)(ghkFfk=QBwj_#o3;5% z3{--d=u58{U2I*n)jEln_(^qRGjSQmHjMnD&1wj0-y9PeUkTm7et;1sdh0sdtTwfz zsp%qPBiobwSz%7RVpEo~Xs1e99+u^mgjz+_ zBfYJ$DZWsUCY|BUX$mMT!2Y|q&DefnQsEbFR&V=T`Jp~Q1mXz`}@O185j#4rmz3%9lD=FvegN?Fb=E9XH z`7e&UQbSiU)smUpNU&U%T8fiMPP_e1dt973k^Qfkgfc8r;mJ z`#L?N(Bj|1ME|O6&JHHuk>>%9{>u|5l|{Wq&U%GykW--fCeWq-cG<4sG^Q&CQngxG zLIlW>HX+lG@2MzUFbYIvx2CVStmC-#Y;NlMLN4wK5&Jz_p!A~7%!ivEQ%4cO97_<+ zzMBnBOgnyZ9R_b*lY%~YLp}~Ak@I-yMONW%6z2w08|Zd-0M;r`b*qN{sdcA?_>SRL z1sh)kGhjSDIX$`lyVaLEdKRYwT5CEZEzP)kXCaqFIy#+ZL6=?|l)dBXIKyGKT9zVA zKRgUUvxF!xM!aye0K<`bpLw+DyUkcR84FSdoJUX8Fac={^>*%b_%~|;deUiwF6|EE zO*J8N``3pSj8^W$;RV*Ea<(z}y8?Yoq5 z=yJViQlIw<%~qU33L%BU3(^Oh3Kj_mMIxT*8NPIg${o%x>d|UfY|dY0@udc|VSYn( zMSB5`hXP63fwzjOpDr&)3<u>F|U>p$HI99A=KC@m1tPjw8yl1wa=rq9JP57nNC zCw*iZCV&^DuR0`cQS9-mUdd?_r58v&z!Bn)m)t2rZbM3FRraZ3?^ZRXex3OpVqGU2 z;_@QeGu#@oLG$~B!gZkVdc!Vvm^~tRNl)rD)XGeZ?5H74hHdDZ_c!Jgjd0guz7~7d zJh8HJZ?`up0tmoea%522xDH2?Q1Q+Tu@JR!5h7IizZ`? z{f0ycmOI5P6w@VfyuqelHpyIfS_&Z?F0w%Zt$|JOcpQQXcKEP&C6E@rFzDmzW0l$@ zFRxMd%Rt<{oEWcXIvUWbx;(8r>M=H0^dsJdb9(zTj=dQvahcV#$)h-(6u}QThJ5f% z<=9>pb~gubBWI?c>b+9lAe*P~}rg2GB!kAS851?gUi&jS%<%S#3wrG|mFa`M}Def&q{Ls(q zHmUTokuZhc^`(5ff|L)s8Fw(+JmGC2wDm*q#~e1{Rt%w+yulc3nl6*F)7hu4u8Z#s z2oV-PIcFid6v<0UsQS!Q4pL>Z4vFTJuII_K>TWrjydOHd;o#-eRYul49K5&5s%akC zc1%3GGs+0e0PXZnLXVyJC;?f=ypHj94&Gt{g1Kzf*ffu&6DT}lZOK>SEQ&sYA$(4# zaA!LFTrfWGaL8F#gm~YjRFTW4q*xHIoEWoEnwz1fSQ0l4qEIQyb)--1(JyMv4T#6o)*=vx zK{H(ODEU*mI> zalE9_#`j|bnki|gGSx<8Uq-(zB;(b@koaQvN(;lFpw*POn`e06k(I({?9#WhEaEaF zDL>q8EM-s4+?2L0-hm~W??Cd~6j7oc{sl6B2hYf;ZsNPJz|<~6qwb9N@0JeE*qX8* z2cV!etgHqHGP{m4k=O;K)DKU1P1M6&n%;{cM(~iVzhH-T>p>;5f4+406Ya^jd4%}l z99FdXVb#p`)ch3#_DOb4CC55V<#icAS6Zmb$YqT)WDagfD4)51OKKdtv8YRHZHtl? z$0h~#%5!RSZdqR2H6k4#qgr^O!}r-`|6SAFtw=<^4L__XQ*=7Ub=YDWtGn=%&hs9B zqw^@EznSj-^Oo!1kC!t8exBfJM|D1*hd>NG@rEZ{6tdqRc7EYI$vBX;tX>amXGOtG zTdj~L^wm2Qn=7dU=JtvIOzZA5UjqiyE_+0IH=+>9I7fc~p^I#x zjWR%K`;!y=*DL-WR1E;BjB0;0;}0k=PAnxEedQMw_#qI%@~~{P{st<>pAJm@z7D{` z&d8wTWa#+g)V;cl^sgJyWq^(b#s-cC|H6!i-__2*$P(yCVhA)fx8Wr{X=x`VF*oKV zRb!O_$k+)1&CJE!K|m#US!E-4OCwHWQhq*o9#<|`Ydh;_Y9y}KRyGb?uDqlM&qwQk zT+itr*$kv4zZ7w_L9P#llJQ+lQ17{yD`1GB)8-6c+uB-SZVMshOjr9Tx+Gi;D}r3p2ef z$drMRlarI-c^5tt6WwzOItMo!M*~+n8wavqiTqAS80cUGGPiRyx3wYpLD#_0*2$5V zl=KIqKR&<7Wo`EdBO3?$Uu2{=vbAP#HLzn~qz5qkT_t0qKkC>yfvkRMnz0cB&k@=(Q-?Fo|{@a>%KjHDGde1kW*)cQx0l>3-JkNZ%WNeMiP27Z^ zBRB9pgU?9^V54JVR0gnfF|l&7Fwp^6xBvhihQE~j7eK$z5PFW`@ErN!HyZ5EmFSpQ ze$Zg$;0wdU+Db8!4{-!Yiq?PU<9%?G;kz&j$I*R zUI)E{;R2cq9QT^|HS;yb$?<0|1$bvEHW}&!nQ_EKRSSfFrSl?xiOcKF{23!(14BZ zc?dA1V_|3IpyL1-veU6JvNN)Ca55XRn;89~u0PlP3IAW|O4v9!8rT>Cf2Rv%0RjO= z?2L3KfagJg#l)D6j)TR>n9hjN1i;K`Xb3PdVE&!%Pj!Ey`>b&;d62EKlMxWa_v`TW zY)d54=7t~xklV94(LeW%KNb3|@~=?+G`{=-?60}cqu2Am$?(U>`Tt=4hXMU}CjKSW ze<#(y(E2|ne*xs5FaHm&f5zu8cmEHrzX0;jm;VRXKjZV4yZ;B*UjX^%%m0JxpYi$2 z-G71${$F#i=eQ)iq%O}htG}Dvar_jc9%0IXtrvHg~YIdfxnGYoz?c0sf5_A0N_h z5mp#Eeu{G-C}``d1)zTx~hG&;e=UM*WC&52p`*jwq{v3r4Xyo{> zNw9dx=a)tDcIQs2tDq~VZHN~DFsx~ao4-}YPcQcWBo`|)Gd(LS$8)4RR(clZ|DCe^ zw8P*}vT;7E>{(~b^i2ONRrb?L(ywAYTRtOz>Axfp;~zHoT=}{BkN^KZyW0jq8~%uh zgFy-L3K}~lVCr(6M&QDc|hW%2LRarSK8fA zp)&s@+q3P_v$Hb)m+X)E5BvKcFb4hbrM`RUR7H=$>L^A=cHn0|h+zgOyvm;#`* z?$B(Tk@Z#)E{056&a>fiprN4j?Ke|`p&z-kq=F0SJm%*@ii>-XozC| zkmJzBiMgX!gbhEi`F766AXeo8a0%NuTwMHwoVw>4<9+AVujwN74``rJ~<=cW}Sf z?}ad5(?+~NW^ALqJWYJ9!+Us-OwUkK!bv{wIhy4LhI~5VD6vfd`~UTIChkzbUmwrd zvhVv|Xb@(x4bfP~uIv&ad!($1WU@1aQAU<5SwqMkV}?PtWRiW$mNklqlxO<=zSr~k zUYDN#;B(!tbD!mYpU*jDx6{*5Qd3JHDr>3F<>qsYG-PXPVhK;&+L*DVe+`49GSH%K z{5)Uy&T`0Wh&DM}YUKI0X%sR9V;hU&FU}Cn!A7p;wBkKdWp^SujI@*RPOU`}IUpPW zw)?Ktuu--e?`n#tZxUN@Rp#)S8>V!NdO8+4>4Yy#XLU|Wg%RfnH7PC#&ey#fu}y5)YRl$t%Ql!fLNJ7pCIf!*-c&d((vm{X|GI#`8<`yF zUwxHnXd{CTICJ#Y0f*ld2V{C{T6Z;1=#fTY`y=Z0Ja;-;?`qoL4=!ZZidR>p;anA< zDm(8s*uoKG&K;N~KvXjGja&t69orgxOh2otQVWo+{d5NT|-{(enUsQ=adG|E-zai z&REXs4qSpQG-(ce39hVipfgqm*nW7I>u6x@Wsf9Cq` zw^^2JCakRrS+C-k6s8W z6HpUXCa-*7m^AC}BGX$cElHFq#Q@^E-fOVxO|6xU*eegHhsKvqjXYP)%CcDKDF$$s zJ=l$G7|I!M2*SKDLKY9Peq^37pFDqTB+s?g*Jt46aXU_IF&g(MGjcEKC;9R%V~xk{ zVV1-;6gARYz^=>IfAoDj>NDlx<$D*J{fHmB_~sBKF>x+sG2>LOCz8OwbA`&wopc^q zW=00&V3PB&>Ibs~HXj)4ldw`pJ#xZD+CtF* z+#!4!Q~AKy3UDLWp|qr~Wi%&D;JV{gaeSGg3c96KG#&$<#q2iVDR|-}WDYegDj_ZS*KKRW=9&^~mTPC>H>g$RluQJkf zSCyaTui^Szz#lg%X%;`NU2{YEq$NWiPvw?~>NDp{yD+)rN^?%~PRL%iNb$-j#&BknQ0Yi#sN4pn~y-W6fCQZB> z$|zyi`JL4Y4Yj}o>Fu{#+NW2%3*j_5;;WcDZ;5|QN_C+oQ;esAI+5!BKcEzqPJ&7^ zGr9%%2b9DHW^mk#g(-~x8nYzD{bWJ?A>yN`5zXT;bYtb+yPc^9a|10I>H7d2Co(a| z;gX=V8(KUV$HDE+BBTXtp*izRE0>oDz@+Gaptyl2?v6 zGEbjYLD|9eL;A+F4b3dpT(2x6FFKAIMdiD}JIw%JMQJx<(_kEaiE?i!p0_pFg3BYWUwGvpuiiLpD2+8DS>EhENE?a)ML{tkDu0E z(|O^j)nH*!*cU64NOYVWI~WfgU4H2uH!x7(pP27k$1bENwdYY#^*UM8=zB|%-WSDn zmzg?-nzWGBikS}sLe+BLntAr`#y(Ba7+X3YGqW$mzFqrDPI7LOtEr~q_V4j}bxar? zOGX{7u0~^}qfd@M13=b`Pwcw0z~QQ#$*&tPMZ95+G4&mo!wd!31i{9~$)Pyi@;4bi z0egvmTD-A5{f<^oir_X)^f9Sd!8T!D;8goKQ5OB(KA?*Kim-n?F)6~fZoR_XJm=p$ zoB4#Od6|)xQi@KHfUc#!dR2D5Eyxv3iX@CrajxiwjQ!;SI^5MU<{?{qgUq4%L<~?#Z%w6c-++Bd#NH1Ns@8YP zNu{7%1)kGkO08_Emi&2(-b&XeKi1_w%cSVuafJ#R_#|qVz6G3&{S+y=O|Mec4d}~& zl+ZR7Cw5x%uT&1P3;8p-Cye?Q_4LhuZVuIN;OgP^EHTV69F>vVTn26iE1_)ra-O5= z1hO*uSI?Rvyy#1JwBO!q6n8m77ASM~bPZeIGI+J3)SL6YeQEMeVv)8*dC}{~t>>9; z9ZDWoXTURK@k|HBb7~>($rE+c{uJEaK7DDo#ISK@{{;>o4SkPeeX%aUU(^lqvVZHUd|or$eW zf{(f2HU*R2WeV=7s69So-H#Y6q~D2j z4IQwlK=%irk8|*E0@RnM4Hf~O0*4#N2t`LjW_`d`F-7__E79+Eql|jLoPuQfd<64{ z-F~zz1jCChoA-{IT)2xiMthD**Bv>7oJ!dtO`y5*zPMT8*XR(rux}!lPcBgstGcRez7R9Ov$^6UaIyL5m-M3tYG1s)46-A zWp8>}@;r3F`EvtH-ZolAESF zx3Vj_PGoSiUXBP>J|C)@r0f6o^#wiGR91z7_HPfo185M4mk9$05@S?Jk4=mZ-d>JU z)7Um;oZsikh|0Xa7mBnCAodzSmc+i>fz{L(7Ksp7kDF$ILCOWRc-*8LcEfMg?xg~; zz{Za3`!SiZ%>#yQ6H?Zd)v#1~ip4-cCkpDnTMQKXuafOWitV4f9yPmf)la(YsoAYM z`a<`EuK*4IkAdz~>%aB-(Qn8Eze0(XVXA0sZq|eg~L;Kl8z( z&VEW-T@jQykbY#g@s>qjx*|`67nC(@Jxa04;f#NG`OX(ZFWwewgw%*hXH6EfLT6tP zY^Wr``^jg58QiMnNpw(%8o{`8Pr)qw+W}^SBP!dmom$PHQ7P=c(jE2XqVP9SRCHyAnsRry&rC&kLQMrNrQUj};V?g9WmI2_KDx)8pe zisZ!ez~7NTz<*|H|LY<{LW zpBxYh-)K2%F{h&6-sB}eAB!!ggV31?$~qXy042$Y)Zt@5u{tFl-G^lJ{ohgnxk@(y z$!(tG|{t0N~b`yasu{2z6Q zq~mzpcOhvH{|ub9Hg$28wXy{ZliuP;8p!``+i`L_0}vOFHla5>7OUmKa1&g@DKtUJ za}Sx=RfcKjAr8!_y}s}o>Vo7>3^z911_dM>x54Ly$|(9S8wM0m)WtodpHguI?|i(O zD?m^ZT9&LlwnnpOP_T6D+-p&YrR1?}ppYQLvaHDWFPnQ|bhCOQnX{{!4`-(~;+ literal 0 HcmV?d00001 From 13806d2b2c410d3b4c69e0a1f083133156c5c4a8 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Fri, 14 Aug 2026 20:39:26 +0200 Subject: [PATCH 84/85] refactor: :recycle: delete reference_benchmarks --- .../reference_candidates/ics201_1.json | 172 ------------------ .../reference_candidates/ics202_1.json | 47 ----- 2 files changed, 219 deletions(-) delete mode 100644 benchmark/datasets/reference_candidates/ics201_1.json delete mode 100644 benchmark/datasets/reference_candidates/ics202_1.json diff --git a/benchmark/datasets/reference_candidates/ics201_1.json b/benchmark/datasets/reference_candidates/ics201_1.json deleted file mode 100644 index ca27b752..00000000 --- a/benchmark/datasets/reference_candidates/ics201_1.json +++ /dev/null @@ -1,172 +0,0 @@ -{ - "1_incident_name": "Whispering Pines Derailment", - "2_incident_number": "US-EPA-R4-2026-0707", - "3_date_time_initiated": { - "date": "July 07, 2026", - "time": "06:30" - }, - "4_map_sketch": { - "total_area_of_operations": "1.5-mile radius centered around the intersection of CSX Rail Milepost 142.5 and Highway 411", - "incident_site_area": "CSX Rail Milepost 142.5, where seven freight cars (#12 through #18) have jackknifed", - "impacted_areas": "Car #14 is releasing Benzene into Whispering Pines Creek at approximately 20 gallons per minute, with visible surface sheening extending 500 meters downstream. Elevated volatile organic compounds are present within a 300-foot downwind plume tracking East-Southeast at 5 miles per hour.", - "threatened_areas": "Whispering Pines Nature Reserve Wetlands one mile downstream, the Municipal Water Intake two miles downstream, and an intact liquefied petroleum gas tank car threatened by the adjacent brush fire", - "overflight_results": "", - "trajectories": "The downwind plume is tracking East-Southeast at 5 miles per hour. Waterborne contamination is moving East at 1.5 knots.", - "impacted_shorelines": "Whispering Pines Creek, with visible surface sheening extending 500 meters downstream", - "graphics_and_symbology": "North is oriented at the top of the page. County Line Road is closed to public traffic, and the Staging Area is at the County Fairgrounds." - }, - "5_situation_summary_health_safety_briefing": { - "situation_summary": "The derailment occurred at 05:45 hours due to a mechanical rail car failure. Seven freight cars jackknifed, and breached tank Car #14 is releasing Benzene into Whispering Pines Creek at an estimated 20 gallons per minute. A secondary 0.5-acre brush fire threatens an adjacent intact liquefied petroleum gas tank car, and law enforcement is executing a mandatory 0.5-mile residential evacuation.", - "health_and_safety_hazards": "Toxic Benzene inhalation, dermal exposure, flash fires from pooling product, heavy machinery movement around compromised rail lines, responder heat stress in 88°F weather, and slip-and-trip hazards along steep creek banks.", - "necessary_measures": "Maintain a 300-foot Exclusion Zone around the rail cars. Hot Zone containment responders must wear Level B personal protective equipment, including self-contained breathing apparatus and chemical-resistant clothing. Level C protection is authorized for warm zone booming teams only when continuous photoionization detector monitoring confirms volatile organic compound levels below 1 part per million. Firefighters must wear full structural turnout gear with self-contained breathing apparatus. All personnel must pass through the formal wet decontamination corridor at the Warm-to-Cold zone interface before leaving the operational area." - }, - "6_prepared_by": { - "name": "Marcus Vance", - "position_title": "Initial Planning Section Chief", - "signature": "Marcus Vance", - "date_time": "July 07, 2026, at 08:00 hours" - }, - "7_current_and_planned_objectives": [ - "Ensure the life safety of all emergency responders and the public within the perimeter by 09:00 hours.", - "Complete the 0.5-mile downwind residential evacuation by 09:30 hours.", - "Suppress the brush fire near the liquefied petroleum gas car to eliminate thermal risks by 10:00 hours.", - "Deploy effective deflection and underflow containment booming across the creek to protect the water intake.", - "Establish a Unified Command structure by 10:30 hours." - ], - "8_current_and_planned_actions_strategies_tactics": [ - { - "time": "06:00", - "actions": "County Fire and Sheriff units were dispatched, and a 500-foot isolation perimeter was established." - }, - { - "time": "06:15", - "actions": "Local Fire Chief Thomas established command and notified the State Department of Environmental Quality, the Environmental Protection Agency, and CSX Railroad." - }, - { - "time": "06:45", - "actions": "The Sheriff's Department initiated door-to-door evacuations of 45 homes." - }, - { - "time": "07:15", - "actions": "Regional Hazmat Team 1 arrived to deploy an air-monitoring grid and inspect the damaged tanks." - }, - { - "time": "07:30", - "actions": "Engine 41 and Tanker 5 initiated defensive foam applications on the brush fire." - }, - { - "time": "07:45", - "actions": "Advance teams from the state environmental agency and the railroad arrived to finalize the water-containment strategy." - }, - { - "time": "08:15", - "actions": "Planned: deploy a specialized spill contractor to place 500 feet of deflection boom at Boom Site 1." - }, - { - "time": "08:45", - "actions": "Planned: launch a drone overflight to map downstream tracking." - }, - { - "time": "09:30", - "actions": "Planned: complete the transition to a fully unified command at the Mobile Command Post." - } - ], - "9_current_organization": { - "incident_commanders": [ - "Chief J. Thomas, County Fire", - "S. Albright, State Department of Environmental Quality", - "D. Miller, CSX Railroad Responsible Party" - ], - "safety_officer": "Captain R. Mendez", - "public_information_officer": "A. Cho, County", - "liaison_officer": "Inspector G. Sims, Sheriff's Office", - "operations_section_chief": "B. Reynolds, State Department of Environmental Quality", - "planning_section_chief": "Marcus Vance", - "logistics_section_chief": "K. Dunavan", - "finance_administration_section_chief": "", - "additional_positions": [ - { - "position": "Hazardous Materials Group Supervisor", - "name": "Lieutenant T. Kincaid, Regional Hazmat Team 1" - }, - { - "position": "Fire Suppression Division Commander", - "name": "Battalion Chief E. Walters" - } - ] - }, - "10_resource_summary": [ - { - "resource": "Type 1 Hazardous Materials Unit", - "resource_identifier": "Regional Hazmat Team 1", - "date_time_ordered": "July 07, 2026, at 06:00 hours", - "eta": "", - "arrived": true, - "notes": "Arrived at 07:00 hours and is positioned at the Cold Zone boundary conducting perimeter monitoring and plugging operations." - }, - { - "resource": "Fire Engine", - "resource_identifier": "County Engine 41", - "date_time_ordered": "July 07, 2026, at 05:48 hours", - "eta": "", - "arrived": true, - "notes": "Arrived at 06:00 hours and is actively suppressing the brush fire." - }, - { - "resource": "Fire Engine", - "resource_identifier": "County Engine 45", - "date_time_ordered": "July 07, 2026, at 05:48 hours", - "eta": "", - "arrived": true, - "notes": "Arrived at 06:05 hours and manages critical water supply lines." - }, - { - "resource": "Water Tender", - "resource_identifier": "County Tanker 5", - "date_time_ordered": "July 07, 2026, at 06:02 hours", - "eta": "", - "arrived": true, - "notes": "Arrived at 06:20 hours and provides continuous flow for foam application." - }, - { - "resource": "Law Enforcement Units", - "resource_identifier": "County Sheriff (4 units)", - "date_time_ordered": "July 07, 2026, at 05:48 hours", - "eta": "", - "arrived": true, - "notes": "Arrived at 05:55 hours and are operating traffic checkpoints and roadblocks on Creek Road while supporting evacuations." - }, - { - "resource": "Emergency Medical Services Unit", - "resource_identifier": "County EMS Unit 12", - "date_time_ordered": "July 07, 2026, at 06:00 hours", - "eta": "", - "arrived": true, - "notes": "Arrived at 06:12 hours and is standing by at the Staging Area for responder rehabilitation and medical backup." - }, - { - "resource": "Spill Response Team", - "resource_identifier": "HEPACO Spill Team A", - "date_time_ordered": "July 07, 2026, at 06:45 hours", - "eta": "July 07, 2026, at 08:15 hours", - "arrived": false, - "notes": "En route to deploy 1,000 feet of containment boom and vacuum trucks." - }, - { - "resource": "Drone Unit", - "resource_identifier": "State DEQ Drone Unit 1", - "date_time_ordered": "July 07, 2026, at 07:10 hours", - "eta": "July 07, 2026, at 08:30 hours", - "arrived": false, - "notes": "En route to provide aerial thermal imagery." - }, - { - "resource": "Vacuum Truck", - "resource_identifier": "CSX contractor TechRad", - "date_time_ordered": "July 07, 2026, at 07:15 hours", - "eta": "July 07, 2026, at 09:30 hours", - "arrived": false, - "notes": "En route to begin product offloading from the breached tank car once the scene stabilizes." - } - ] -} diff --git a/benchmark/datasets/reference_candidates/ics202_1.json b/benchmark/datasets/reference_candidates/ics202_1.json deleted file mode 100644 index 32edc668..00000000 --- a/benchmark/datasets/reference_candidates/ics202_1.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "1_incident_name": "Whispering Pines Derailment", - "2_operational_period": { - "date_from": "July 07, 2026", - "date_to": "July 08, 2026", - "time_from": "18:00", - "time_to": "06:00" - }, - "3_objectives": [ - "Maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the night shift.", - "Deploy an additional 500 feet of underflow and sorbent booming at designated downstream checkpoints on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen.", - "Complete the structural stabilization and grounding of breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading.", - "Establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours.", - "Finalize the morning resource allocation and shift rotation plan by 04:00 hours." - ], - "4_operational_period_command_emphasis": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Prioritize the safety of hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery.", - "general_situational_awareness": "Clearing skies are forecast, with the ambient temperature dropping to 62°F by midnight and winds shifting from the Northwest at 3 to 5 miles per hour. Heavy Benzene vapors may accumulate in low-lying drainage ditches and creek pockets east of the derailment site. Remain vigilant for low-visibility hazards, uneven muddy terrain along the banks, and wildlife hazards in the adjacent wetlands.", - "5_site_safety_plan_required": true, - "approved_site_safety_plans_located_at": "Primary Mobile Command Post inside the Forward Operations Briefing Trailer", - "6_incident_action_plan_attachments": { - "ics_203": true, - "ics_204": true, - "ics_205": true, - "ics_205a": false, - "ics_206": true, - "ics_207": false, - "ics_208": true, - "map_chart": true, - "weather_forecast_tides_currents": true, - "other_attachments": [ - "EPA Plume Trajectory Analysis Sheet", - "CSX Tank Car Cargo Manifest" - ] - }, - "7_prepared_by": { - "name": "Marcus Vance", - "position_title": "Planning Section Chief", - "signature": "Marcus Vance", - "date_time": "July 07, 2026, at 16:30 hours" - }, - "8_approved_by_incident_commander": { - "name": "Chief J. Thomas of County Fire", - "signature": "Chief J. Thomas", - "date_time": "July 07, 2026, at 17:00 hours" - }, - "iap_page_number": "1" -} From 2378c2615d545922a9907308b39270d4611b4765 Mon Sep 17 00:00:00 2001 From: marcvergees Date: Fri, 14 Aug 2026 20:39:40 +0200 Subject: [PATCH 85/85] feat: :sparkles: implementation of pdfs in the runner --- benchmark/pipelines/base.py | 2 +- benchmark/pipelines/pipeline.py | 2 +- benchmark/runners/runner.py | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/benchmark/pipelines/base.py b/benchmark/pipelines/base.py index 4abce9ec..e92f9478 100644 --- a/benchmark/pipelines/base.py +++ b/benchmark/pipelines/base.py @@ -14,5 +14,5 @@ class BasePipeline(ABC): """Base class for all extraction pipelines.""" @abstractmethod - def run(self, narrative: str, template_schema: dict) -> PipelineExtractionOutput: + def run(self, narrative: str, template_schema: dict, pdf_path: str) -> PipelineExtractionOutput: pass \ No newline at end of file diff --git a/benchmark/pipelines/pipeline.py b/benchmark/pipelines/pipeline.py index 44dcf42d..becebbd4 100644 --- a/benchmark/pipelines/pipeline.py +++ b/benchmark/pipelines/pipeline.py @@ -2,7 +2,7 @@ class Pipeline(BasePipeline): - def run(self, narrative: str, template_schema: dict) -> PipelineExtractionOutput: + def run(self, narrative: str, template_schema: dict, pdf_path: str) -> PipelineExtractionOutput: ''' You guys should here implement the whole pipeline implementation depending on approach B or C. diff --git a/benchmark/runners/runner.py b/benchmark/runners/runner.py index 6e3b7fbe..31ecaa17 100644 --- a/benchmark/runners/runner.py +++ b/benchmark/runners/runner.py @@ -15,6 +15,7 @@ def run_benchmark(self) -> dict[str, any]: narratives_dir = os.path.join(datasets_dir, "narratives") ground_truth_dir = os.path.join(datasets_dir, "ground_truth") templates_dir = os.path.join(datasets_dir, "templates") + pdfs_dir = os.path.join(templates_dir, "pdfs") results = [] total_latency = 0.0 @@ -45,8 +46,9 @@ def run_benchmark(self) -> dict[str, any]: narrative_path = os.path.join(narratives_dir, narrative_file) gt_path = os.path.join(ground_truth_dir, gt_file) if gt_file else "" template_path = os.path.join(templates_dir, template_file) + pdf_path = os.path.join(pdfs_dir, template_file.replace(".json", ".pdf")) - if not os.path.exists(gt_path) or not os.path.exists(template_path): + if not os.path.exists(gt_path) or not os.path.exists(template_path) or not os.path.exists(pdf_path): continue with open(narrative_path, "r") as f: @@ -62,7 +64,7 @@ def run_benchmark(self) -> dict[str, any]: template_schema = json.load(f) start_time = time.time() - output = self.pipeline.run(narrative_text, template_schema) + output = self.pipeline.run(narrative_text, template_schema, pdf_path) latency = time.time() - start_time total_latency += latency

h`bgr3-|02y+$5&!IxhUCSgkUe(VW!gajoBF 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≪FsEdn-p(0gn)NJ9vy)0Y@>?8 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�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 J`>%}t{{?StkeUDh literal 0 HcmV?d00001 diff --git a/benchmark/datasets/pdfs/ics_203.pdf b/benchmark/datasets/pdfs/ics_203.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b813e25721067d0a621cd587cc951472537f5217 GIT binary patch literal 63774 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 literal 0 HcmV?d00001 diff --git a/benchmark/datasets/pdfs/ics_204.pdf b/benchmark/datasets/pdfs/ics_204.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9444ca76bc851c468351d64de64c221cf858dae6 GIT binary patch literal 364179 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