File size: 5,552 Bytes
9de6bfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a11007
 
 
 
 
 
 
 
 
9de6bfb
 
 
 
 
 
 
 
 
 
792ae72
 
 
9de6bfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""
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_damage.pt must be uploaded directly into this Space alongside app.py
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,
            )

        # FIX: "description" already always defaults to "" (never a missing
        # key) inside result["vlm_assessment"]["description"] - confirmed
        # directly in damage_type_classifier.py. Adding a TOP-LEVEL alias
        # here too, since the real integration issue may be a caller
        # reading result["description"] directly instead of the nested
        # path - this way it works either way, regardless of which it is.
        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).")

    # allow_custom_value=True: same fix as Agent 4 - lets an invalid value reach
    # our own validation instead of Gradio crashing on it first.
    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()