"""Serve deterministic synthetic UI fixtures without loading dotenv or models.""" from __future__ import annotations import argparse import os from pathlib import Path from tempfile import gettempdir from doc_inspector.demo import ( PROVENANCE_DEMO_NAME, demo_extractions, generate_demo_artifacts, provenance_demo_extraction, ) from doc_inspector.ingest import normalize_document from doc_inspector.provenance import resolve_provenance from doc_inspector.rules import inspect_extraction from doc_inspector.schemas import InspectionBundle from doc_inspector.service import InspectionArtifacts from doc_inspector.types import ProviderName, SchemaName from doc_inspector.ui import ( CIVIC_CSS, CIVIC_THEME, build_app, ensure_local_port_available, prepare_gradio_temp_root, ) DEFAULT_PORT = 7862 def fixture_inspector( path: Path, schema: SchemaName, provider: ProviderName, ) -> InspectionArtifacts: """Return the named synthetic fixture without network or credential access. Extraction is replayed from a fixed synthetic answer, but ingestion and provenance resolution run for real, so the browser gate exercises the same local verification path the product uses. """ del schema source = Path(path) key = source.stem if key == PROVENANCE_DEMO_NAME: extraction = provenance_demo_extraction() else: fixtures = demo_extractions() if key not in fixtures: raise ValueError(f"離線 UI fixture 不支援:{key}。") extraction = fixtures[key] document = normalize_document(source) bundle = InspectionBundle( provider=provider, model="offline-ui-fixture", source_file_name=source.name, page_count=len(document.pages), elapsed_ms=0, extraction=extraction, review_report=inspect_extraction(extraction), provenance=resolve_provenance( extraction, document.text_layer, pages=document.pages, ), ) return InspectionArtifacts(bundle=bundle, pages=document.pages) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="啟動不讀 .env、不呼叫模型的 UI 測試 fixture。", ) parser.add_argument("--port", type=int, default=DEFAULT_PORT) parser.add_argument( "--temp-dir", type=Path, default=Path(gettempdir()) / f"doc-inspector-ui-fixture-{DEFAULT_PORT}", ) return parser def main(argv: list[str] | None = None) -> None: args = build_parser().parse_args(argv) if not 1 <= args.port <= 65535: raise ValueError("port 必須介於 1 與 65535。") temp_dir = args.temp_dir.expanduser().resolve() os.environ["GRADIO_TEMP_DIR"] = str(temp_dir) root = prepare_gradio_temp_root() generate_demo_artifacts(root / "demo") ensure_local_port_available(args.port) print( f"offline UI fixture: http://127.0.0.1:{args.port} " "(不讀 .env、不呼叫模型)" ) build_app(inspector=fixture_inspector).launch( server_name="127.0.0.1", server_port=args.port, share=False, theme=CIVIC_THEME, css=CIVIC_CSS, show_error=False, max_file_size=25 * 1024 * 1024, max_threads=4, state_session_capacity=200, enable_monitoring=False, ) if __name__ == "__main__": main()