"""Pre-generate example cache files for judge-proofing. Run this script once on a machine with GPU access (HF Space or local with CUDA) AFTER the Space has warmed up so the model is resident. The output JSON files are written to data/example_cache/.json and are committed to the repo so that example-button clicks are instant even on a cold ZeroGPU worker. Usage (from the project root): python scripts/cache_examples.py Each file contains a serialised HandoffOutput (Pydantic .model_dump()). The app loads these at click time via app._load_cached_example() and renders them through the normal render_handoff_html / render_safety_html path — validated, not raw. NOTE: Do NOT run this script inside a ZeroGPU Space (SPACES_ZERO_GPU is set). ZeroGPU only allocates GPU within a live request context (@spaces.GPU window), so model.generate() calls made at the top level will run on CPU and be very slow or time out. Run on a regular GPU box (A10/A100 Space, local CUDA machine, or a standard HF Space with a persistent GPU). """ from __future__ import annotations import json import os import sys from pathlib import Path # Make sure the project root is on the path when run as a script. sys.path.insert(0, str(Path(__file__).parent.parent)) # Parse this before importing app.py. Otherwise --no-model still triggers the # import-time 4B model load, defeating the purpose of the flag. USE_MODEL = "--no-model" not in sys.argv if not USE_MODEL: os.environ["DENTAL_SOAP_USE_MODEL"] = "0" from app import build_outputs, EXAMPLES # noqa: E402 (after sys.path fix) from examples import STEP2_CHECKS, STEP3_CHECKS, STEP4_CHECKS # noqa: E402 from schema import HandoffOutput # noqa: E402 CACHE_DIR = Path(__file__).parent.parent / "data" / "example_cache" CACHE_DIR.mkdir(parents=True, exist_ok=True) def _split_checks(checks: list[str]): return ( [c for c in checks if c in STEP2_CHECKS], [c for c in checks if c in STEP3_CHECKS], [c for c in checks if c in STEP4_CHECKS], ) def cache_example(key: str, use_model: bool = True) -> None: ex = EXAMPLES[key] checks_dental, checks_jaw, checks_body = _split_checks(ex["checks"]) print(f"Generating cache for '{key}' (use_model={use_model}) ...") _safety, _handoff, raw_json, status, _pdf, _email, _workflow = build_outputs( ex["story"], ex.get("name", ""), ex["age"], ex["language"], ex["chief_concern"], ex["duration"], ex["tooth_or_area"], ex["recent_dental_work"], ex["pain_score"], ex["meds"], ex["allergies"], ex["goals"], checks_dental, checks_jaw, checks_body, use_model, ) print(f" status: {status}") if use_model and not status.startswith("AI model path used (ZeroGPU):"): raise RuntimeError( f"Refusing to cache '{key}': model-backed generation was requested " f"but the app returned: {status}" ) output = HandoffOutput.model_validate_json(raw_json) out_path = CACHE_DIR / f"{key}.json" tmp_path = out_path.with_suffix(".json.tmp") tmp_path.write_text( json.dumps(output.model_dump(), ensure_ascii=False, indent=2), encoding="utf-8", ) tmp_path.replace(out_path) print(f" written: {out_path}") if __name__ == "__main__": # Guard: ZeroGPU only allocates GPU inside a request context, so running the # model generate() calls at script top-level will silently fall back to CPU # and produce unusably slow or broken output. Bail out early with a clear # message so the operator knows to re-run on a non-ZeroGPU GPU machine. if os.getenv("SPACE_ID") and os.getenv("SPACES_ZERO_GPU"): sys.exit( "[cache_examples] ERROR: this script is running inside a ZeroGPU Space " "(SPACE_ID and SPACES_ZERO_GPU are both set). ZeroGPU only allocates GPU " "within a live @spaces.GPU request window — model calls here run on CPU " "and will be extremely slow or time out. Re-run on a regular GPU box " "(a standard HF Space with a persistent GPU, a local CUDA machine, or an " "A10/A100 HF Space)." ) # Pass --no-model to generate from the deterministic template path only. for example_key in EXAMPLES: cache_example(example_key, use_model=USE_MODEL) print("Done. Commit data/example_cache/*.json to the repo.")