from __future__ import annotations import argparse from pathlib import Path from typing import Any, cast import gradio as gr import yaml from core.deployment import current_policy, ensure_demo_mode_allowed from datasets.field_notes import FieldNoteStore from plant.plant_loader import SpeciesIndexBuilder from plant.plant_service import DemoPlantVisionService, PlantVisionService from plant.plant_tab import build_plant_tab from plant.plant_tools import set_services ROOT = Path(__file__).parent DEFAULT_CONFIG = ROOT / "models.yaml" APP_CSS = """ .plant-shell { max-width: 1120px !important; } .plant-title { margin-bottom: 0.35rem; } footer { display: none !important; } """ def load_config(path: str | Path = DEFAULT_CONFIG) -> dict[str, Any]: config_path = Path(path) data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} if not isinstance(data, dict): raise ValueError(f"Plant config must be a mapping: {config_path}") return data def build_app( config_path: str | Path = DEFAULT_CONFIG, no_model: bool = False, data_dir: str | Path = "data", model_mode: str = "openbmb", ) -> gr.Blocks: policy = current_policy() cfg = load_config(config_path) root = Path(config_path).parent species_index = SpeciesIndexBuilder(root=root).build(cfg) note_store = FieldNoteStore(Path(data_dir) / "plant_field_notes.csv") plant_service: Any if no_model or model_mode == "demo": ensure_demo_mode_allowed(policy) plant_service = DemoPlantVisionService() elif model_mode == "finetuned": plant_service = PlantVisionService.from_config(config_path, "plant_vlm_finetuned") else: plant_service = PlantVisionService.from_config(config_path, "plant_vlm") set_services(plant_service, note_store, species_index) domain = cfg.get("domain", {}) title = str(domain.get("title") or "Plant Discovery") description = str( domain.get("description") or "Identify plants, correct mistakes, and export local training data." ) with gr.Blocks( title=title, analytics_enabled=False, ) as demo: gr.Markdown( f"# {title}\n\n{description}", elem_classes=["plant-title"], ) gr.Markdown( "Local-first reference app generated around the OpenBMB Workbench template. " "Model loading happens only after an explicit identify action." ) build_plant_tab(plant_service, note_store, species_index) return cast(gr.Blocks, demo) def main() -> None: parser = argparse.ArgumentParser(description="Plant Discovery reference app") parser.add_argument("--config", default=str(DEFAULT_CONFIG)) parser.add_argument("--port", type=int, default=7861) parser.add_argument("--share", action="store_true") parser.add_argument( "--model-mode", choices=["openbmb", "finetuned", "demo"], default="openbmb", help=( "openbmb uses MiniCPM-V, finetuned uses the configured adapter, " "demo is deterministic." ), ) parser.add_argument( "--no-model", action="store_true", help="Use deterministic demo identification instead of loading a vision model.", ) args = parser.parse_args() model_mode = "demo" if args.no_model else args.model_mode demo = build_app(args.config, no_model=args.no_model, model_mode=model_mode) print(f"Starting Plant Discovery on http://127.0.0.1:{args.port}") demo.launch( server_port=args.port, share=args.share, theme=gr.themes.Soft(primary_hue="green", neutral_hue="slate"), css=APP_CSS, mcp_server=True, ) if __name__ == "__main__": main()