File size: 6,687 Bytes
d11fcd6
9e37f60
e569bb9
 
 
 
26e8165
e569bb9
 
 
 
26e8165
e569bb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26e8165
e569bb9
 
 
 
 
 
26e8165
e569bb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26e8165
e569bb9
 
 
 
26e8165
e569bb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26e8165
e569bb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26e8165
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
from __future__ import annotations
import json
import os
import unicodedata
from io import BytesIO
from pathlib import Path
from typing import Optional
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

from dotenv import load_dotenv
from fastapi import FastAPI, File, Header, HTTPException, UploadFile, Form
from fastapi.middleware.cors import CORSMiddleware
from PIL import Image
from supabase import Client, create_client
from ultralytics import YOLO

# Pega a pasta atual onde o main.py está
BASE_DIR = Path(__file__).resolve().parent
MODEL_PATH = BASE_DIR / "best.pt"

# Tenta carregar localmente, mas no Hugging Face vai usar as Secrets
load_dotenv()

SUPABASE_URL = os.getenv("SUPABASE_URL") or os.getenv("VITE_SUPABASE_URL")
SUPABASE_KEY = (
    os.getenv("SUPABASE_ANON_KEY")
    or os.getenv("SUPABASE_PUBLISHABLE_KEY")
    or os.getenv("VITE_SUPABASE_PUBLISHABLE_KEY")
    or os.getenv("VITE_SUPABASE_ANON_KEY")
)

if not MODEL_PATH.exists():
    raise RuntimeError(f"Modelo YOLO nao encontrado em: {MODEL_PATH}")
if not SUPABASE_URL or not SUPABASE_KEY:
    raise RuntimeError("Configure SUPABASE_URL/SUPABASE_ANON_KEY ou as variaveis VITE_SUPABASE_*.")

model = YOLO(str(MODEL_PATH))
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)

app = FastAPI(title="Visiagro API", description="Deteccao de pragas com YOLOv8")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

def _normalize(value: Optional[str]) -> str:
    if not value:
        return ""
    without_accents = "".join(
        char for char in unicodedata.normalize("NFD", value) if unicodedata.category(char) != "Mn"
    )
    return without_accents.lower().replace("_", " ").replace("-", " ").strip()

def _get_user_id(user_response) -> str:
    user = getattr(user_response, "user", None)
    if user is None and hasattr(user_response, "dict"):
        user = user_response.dict().get("user")
    if isinstance(user, dict):
        user_id = user.get("id")
    else:
        user_id = getattr(user, "id", None)
    if not user_id:
        raise HTTPException(status_code=401, detail="Token invalido ou usuario nao encontrado.")
    return user_id

def _parse_bearer_token(authorization: Optional[str]) -> str:
    if not authorization or not authorization.lower().startswith("bearer "):
        raise HTTPException(status_code=401, detail="Envie o token do Supabase no header Authorization.")
    return authorization.split(" ", 1)[1].strip()

def _find_peste(label: Optional[str]):
    if not label:
        return None

    response = (
        supabase.table("pestes")
        .select(
            "id,nome_cientifico,nome_comum,descricao_simples,nivel_risco,"
            "periodo_mais_comum,acoes_recomendadas,danos_causados"
        )
        .execute()
    )
    label_normalized = _normalize(label)

    for peste in response.data or []:
        candidates = [
            peste.get("nome_comum"),
            peste.get("nome_cientifico"),
        ]
        if any(_normalize(candidate) == label_normalized for candidate in candidates):
            return peste

    for peste in response.data or []:
        candidates = [
            peste.get("nome_comum"),
            peste.get("nome_cientifico"),
        ]
        if any(label_normalized in _normalize(candidate) for candidate in candidates):
            return peste

    return None

def _insert_prediction(token: str, payload: dict):
    url = f"{SUPABASE_URL.rstrip('/')}/rest/v1/predictions"
    request = Request(
        url,
        data=json.dumps(payload).encode("utf-8"),
        headers={
            "apikey": SUPABASE_KEY,
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Prefer": "return=representation",
        },
        method="POST",
    )

    try:
        with urlopen(request, timeout=20) as response:
            body = response.read().decode("utf-8")
            return json.loads(body) if body else []
    except HTTPError as error:
        detail = error.read().decode("utf-8")
        raise HTTPException(status_code=error.code, detail=f"Erro ao salvar prediction: {detail}") from error
    except URLError as error:
        raise HTTPException(status_code=502, detail=f"Falha ao conectar no Supabase: {error.reason}") from error

@app.get("/health")
def health_check():
    return {"status": "ok", "model": str(MODEL_PATH)}

@app.post("/analyze", summary="Analisa uma imagem e persiste o resultado")
async def analyze_image(
    file: UploadFile = File(...),
    authorization: Optional[str] = Header(default=None),
):
    token = _parse_bearer_token(authorization)
    try:
        user_response = supabase.auth.get_user(token)
        user_id = _get_user_id(user_response)
    except HTTPException:
        raise
    except Exception as error:
        raise HTTPException(status_code=401, detail=f"Falha ao validar usuario: {error}") from error

    contents = await file.read()
    try:
        image = Image.open(BytesIO(contents)).convert("RGB")
    except Exception as error:
        raise HTTPException(status_code=400, detail="Arquivo enviado nao e uma imagem valida.") from error

    results = model.predict(image, verbose=False)

    detections = []
    for result in results:
        for box in result.boxes:
            class_id = int(box.cls[0])
            label_name = model.names[class_id]
            confidence = float(box.conf[0]) if box.conf is not None else None
            detections.append(
                {
                    "class_id": class_id,
                    "label": label_name,
                    "confidence": confidence,
                }
            )

    top_detection = max(detections, key=lambda item: item["confidence"] or 0, default=None)
    unique_labels = list(dict.fromkeys(item["label"] for item in detections))
    label_final = ", ".join(unique_labels) if unique_labels else "Nenhuma deteccao"
    confidence = top_detection["confidence"] if top_detection else None
    peste = _find_peste(top_detection["label"] if top_detection else None)

    payload = {
        "filename": file.filename,
        "label": label_final,
        "user_id": user_id,
        "peste_id": peste["id"] if peste else None,
        "confianca": confidence,
    }

    inserted = _insert_prediction(token, payload)

    return {
        "status": "success",
        "filename": file.filename,
        "label": label_final,
        "confianca": confidence,
        "peste": peste,
        "detections": detections,
        "prediction": inserted[0] if inserted else None,
    }