| """ |
| Agent 2 - Visual Damage-Type Classification (Gradio version) |
| ------------------------------------------------------------------ |
| Same pattern as Agent 1's Space: Gradio Blocks, @spaces.GPU decorator |
| applied from the start this time (learned from Agent 1's ZeroGPU surprise - |
| no need to hit that issue twice). |
| |
| Routes by item_type: |
| - laptop -> the hybrid YOLO+VLM detector (needs laptop_damage.pt, uploaded |
| directly to this Space's files) |
| - everything else -> the VLM-only detector |
| |
| GOOGLE_API_KEY is read from the environment, NOT taken as user input - set |
| it as a Repository Secret in this Space's Settings, never passed through |
| the UI or the API call itself. This matches how damage_type_classifier.py |
| already works (falls back to os.environ.get("GOOGLE_API_KEY") when no |
| api_key is explicitly passed) - no code change needed there, just correct |
| deployment configuration here. |
| |
| Run locally to test: python app.py |
| """ |
|
|
| import os |
|
|
| import gradio as gr |
|
|
| from hybrid_damage_detector import detect_damage_hybrid |
| from vlm_only_detector import detect_damage_vlm_only |
|
|
| try: |
| import spaces |
| gpu_decorator = spaces.GPU |
| except ImportError: |
| def gpu_decorator(func): |
| return func |
|
|
| ITEM_TYPES = ["laptop", "tablet", "vr_headset", "lab_kit", "projector", "printer"] |
|
|
| |
| LAPTOP_MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "laptop_damage.pt") |
|
|
|
|
| @gpu_decorator |
| def process(item_type, crop_image, change_score, baseline_image, return_image): |
| """ |
| Core logic, kept as a plain function so it's testable independently of |
| Gradio's UI wiring. |
| |
| baseline_image/return_image are OPTIONAL - when provided, lets the VLM |
| check whether the flagged damage is genuinely new or already present |
| in the baseline (per the old-vs-new-damage feature built earlier). |
| """ |
| if not item_type or item_type not in ITEM_TYPES: |
| return {"error": f"Unknown item_type '{item_type}'. Known types: {ITEM_TYPES}"} |
|
|
| if not crop_image: |
| return {"error": "A crop image (the flagged region from Agent 1) is required."} |
|
|
| if not os.environ.get("GOOGLE_API_KEY"): |
| return {"error": "GOOGLE_API_KEY is not set on this Space - add it as a Repository Secret " |
| "in Settings, this cannot be provided through the API call itself."} |
|
|
| try: |
| if item_type == "laptop": |
| if not os.path.isfile(LAPTOP_MODEL_PATH): |
| return {"error": f"laptop_damage.pt not found at {LAPTOP_MODEL_PATH} - " |
| f"upload the model file directly into this Space's files."} |
| result = detect_damage_hybrid( |
| image_path=crop_image, model_path=LAPTOP_MODEL_PATH, item_type=item_type, |
| vlm_change_score=change_score, baseline_path=baseline_image, return_path=return_image, |
| ) |
| else: |
| result = detect_damage_vlm_only( |
| image_path=crop_image, item_type=item_type, |
| change_score=change_score, baseline_path=baseline_image, return_path=return_image, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| result["description"] = result.get("vlm_assessment", {}).get("description", "") |
|
|
| return result |
| except Exception as e: |
| return {"error": f"Agent 2 processing failed: {e}"} |
|
|
|
|
| with gr.Blocks(title="Agent 2 - Visual Damage Classification") as demo: |
| gr.Markdown("# Agent 2 - Visual Damage-Type Classification\n" |
| "Given a flagged photo region from Agent 1, identifies the damage type and severity. " |
| "Combines a trained detector (laptop only) with a vision-language model (all item types).") |
|
|
| |
| |
| item_type_input = gr.Dropdown(label="Item Type", choices=ITEM_TYPES, allow_custom_value=True) |
| crop_input = gr.Image(label="Flagged Crop (from Agent 1)", type="filepath") |
| change_score_input = gr.Slider(label="Agent 1's Change Score", minimum=0.0, maximum=1.0, value=0.5, step=0.01) |
|
|
| gr.Markdown("**Optional** - the full baseline/return photos, so the model can check whether " |
| "this damage is genuinely new or already present in the baseline:") |
| with gr.Row(): |
| baseline_input = gr.Image(label="Full Baseline Photo (optional)", type="filepath") |
| return_input = gr.Image(label="Full Return Photo (optional)", type="filepath") |
|
|
| with gr.Row(): |
| submit_btn = gr.Button("Classify Damage", variant="primary") |
| clear_btn = gr.ClearButton( |
| value="Clear All (start a fresh test)", |
| components=[item_type_input, crop_input, change_score_input, baseline_input, return_input], |
| ) |
|
|
| output = gr.JSON(label="Result") |
| clear_btn.add(output) |
|
|
| submit_btn.click( |
| fn=process, |
| inputs=[item_type_input, crop_input, change_score_input, baseline_input, return_input], |
| outputs=output, |
| api_name="classify", |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|