"""Gradio app. Main UI definition and layout.""" from __future__ import annotations import html import io import logging from typing import Any import gradio as gr from pipeline.pipeline import run_diagnosis from storage.cache import get_cache from storage.database import init_db, list_recent, record_diagnosis from ui.components import ( HEADER_HTML, THEME_CSS, defect_pills_html, diagnosis_html, render_history, stats_html, ) from ui.theme import build_theme logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) DEFAULT_FILM_TYPES = [ "Kodak Portra 400 (35mm)", "Kodak Tri-X 400 (35mm)", "Kodak Ektar 100 (35mm)", "Ilford HP5 Plus (35mm)", "Ilford Delta 100 (35mm)", "Ilford FP4 Plus (120)", "CineStill 800T (35mm)", "Fujifilm Pro 400H (35mm)", "Fomapan 400 (35mm)", "Other / Unknown", ] STORAGE_OPTIONS = [ "fridge, sealed", "freezer, sealed", "room temp, sealed", "room temp, loose", "shoe box, attic", "shoe box, basement", "unknown", ] RESOLUTION_OPTIONS = [2000, 3000, 4000, 5000, 6000, 8000] def _image_to_bytes(pil_image: Any) -> bytes: buf = io.BytesIO() pil_image.save(buf, format="PNG") return buf.getvalue() def run_pipeline( image: Any, film_type: str, film_age_years: int, storage: str, scan_dpi: int, progress: gr.Progress = gr.Progress(), ) -> tuple[str, str, str, str]: """Gradio handler for the diagnose button.""" if image is None: empty = '

No image provided.

' return empty, empty, empty, render_history(list_recent(limit=10)) try: progress(0.0, "Hashing image for cache lookup...") cache = get_cache() image_bytes = _image_to_bytes(image) cached = cache.get(image_bytes) if cached is not None: logger.info("Returning cached diagnosis") result = cached else: progress(0.1, "Stage 1/2: running vision defect extraction...") result = run_diagnosis( image=image, film_type=film_type or "Unknown 35mm", film_age_years=int(film_age_years or 0), storage=storage or "unknown", scan_resolution_dpi=int(scan_dpi or 4000), ) progress(0.85, "Stage 2/2: persisting diagnosis...") try: record_diagnosis(result) except Exception as exc: # pragma: no cover logger.warning("Failed to record diagnosis: %s", exc) cache.put(image_bytes, result) progress(1.0, "Done.") counts = result.get("defects", {}).get("label_counts", {}) or {} stats = stats_html(result) pills = defect_pills_html(counts) diag = diagnosis_html(result.get("diagnosis", {}).get("diagnosis_text", "")) history = render_history(list_recent(limit=10)) return stats, pills, diag, history except Exception as exc: # pragma: no cover logger.exception("Pipeline failed") err = ( '
' f'
' f"Pipeline error
" f"
"
            f"{html.escape(str(exc))}
" ) return err, "", "", render_history(list_recent(limit=10)) def refresh_history() -> str: return render_history(list_recent(limit=10)) def build_app() -> gr.Blocks: init_db() theme = build_theme() with gr.Blocks(title="Project Halide") as app: gr.HTML(HEADER_HTML) with gr.Row(): with gr.Column(scale=1): with gr.Group(elem_classes="halide-card"): gr.Markdown('
Scan upload
') image_input = gr.Image( label="Film scan", type="pil", height=380, sources=["upload", "clipboard"], ) with gr.Group(elem_classes="halide-card"): gr.Markdown('
Film metadata
') film_type = gr.Dropdown( choices=DEFAULT_FILM_TYPES, value=DEFAULT_FILM_TYPES[0], label="Film stock", ) with gr.Row(): film_age = gr.Slider( minimum=0, maximum=80, step=1, value=2, label="Age (years)", ) scan_dpi = gr.Dropdown( choices=RESOLUTION_OPTIONS, value=4000, label="Scan resolution (dpi)", ) storage = gr.Radio( choices=STORAGE_OPTIONS, value=STORAGE_OPTIONS[0], label="Storage condition", ) run_btn = gr.Button("Diagnose scan", variant="primary", size="lg") with gr.Column(scale=2): with gr.Group(elem_classes="halide-card"): gr.Markdown('
Defect summary
') defect_summary = gr.HTML( value='

Awaiting scan.

' ) with gr.Group(elem_classes="halide-card"): gr.Markdown('
Diagnosis & fixes
') diagnosis_output = gr.HTML( value='

Awaiting scan.

' ) with gr.Group(elem_classes="halide-card"): gr.Markdown('
Session stats
') stats_output = gr.HTML( value='

Awaiting scan.

' ) with gr.Column(scale=1): with gr.Group(elem_classes="halide-card"): gr.Markdown('
Recent diagnoses
') history_output = gr.HTML(value=render_history(list_recent(limit=10))) refresh_btn = gr.Button("Refresh history", size="sm") gr.HTML( "" ) run_btn.click( fn=run_pipeline, inputs=[image_input, film_type, film_age, storage, scan_dpi], outputs=[stats_output, defect_summary, diagnosis_output, history_output], ) refresh_btn.click(fn=refresh_history, outputs=[history_output]) return app def main() -> None: app = build_app() app.queue(max_size=8).launch( server_name="0.0.0.0", server_port=7860, theme=build_theme(), css=THEME_CSS, ) if __name__ == "__main__": main()