Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
Amazon Trailer Inspector β app.py
|
| 3 |
-
HuggingFace Spaces Β· FastAPI Β·
|
| 4 |
|
| 5 |
REST API that accepts 6 labeled images and runs all 6 aspect inspections
|
| 6 |
in parallel, returning a structured JSON inspection report.
|
|
@@ -17,31 +17,26 @@ import re
|
|
| 17 |
import traceback
|
| 18 |
from typing import Optional
|
| 19 |
|
|
|
|
| 20 |
import uvicorn
|
| 21 |
from fastapi import FastAPI, HTTPException
|
| 22 |
from fastapi.middleware.cors import CORSMiddleware
|
| 23 |
from fastapi.responses import JSONResponse
|
| 24 |
from PIL import Image
|
| 25 |
-
from huggingface_hub import InferenceClient
|
| 26 |
from pydantic import BaseModel, Field
|
| 27 |
|
| 28 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
-
# MODELS (tried in order β first success wins
|
| 30 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
MODELS = [
|
| 32 |
-
#
|
| 33 |
-
"
|
| 34 |
-
"
|
| 35 |
-
# ββ Tier 2: dedicated vision models ββββββββββββββββββββββββββββββββββββββ
|
| 36 |
-
"Qwen/Qwen2.5-VL-7B-Instruct", # Qwen 2.5 VL β strong free-tier vision
|
| 37 |
-
"Qwen/Qwen2-VL-7B-Instruct", # Qwen 2 VL β previous gen, very stable
|
| 38 |
-
# ββ Tier 3: additional fallbacks βββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
-
"meta-llama/Llama-3.2-11B-Vision-Instruct", # Llama 3.2 11B Vision β reliable fallback
|
| 40 |
-
"microsoft/Phi-3.5-vision-instruct", # Phi-3.5 Vision β lightweight, good accuracy
|
| 41 |
-
"HuggingFaceM4/idefics3-8b-llama3", # IDEFICS3 β HF native, always available
|
| 42 |
-
"mistralai/Pixtral-12B-2409", # Pixtral 12B β Mistral's vision model, free tier
|
| 43 |
]
|
| 44 |
|
|
|
|
|
|
|
|
|
|
| 45 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
# ASPECT PROMPTS
|
| 47 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -373,16 +368,53 @@ LABEL_TO_ASPECT = {
|
|
| 373 |
}
|
| 374 |
|
| 375 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 376 |
-
#
|
| 377 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 378 |
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
return _hf_client
|
| 386 |
|
| 387 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 388 |
# IMAGE HELPERS
|
|
@@ -476,9 +508,11 @@ def validate_result(data: dict, keys: list) -> dict | None:
|
|
| 476 |
|
| 477 |
def analyze_one(img: Image.Image, aspect: str, token: str) -> tuple:
|
| 478 |
"""
|
| 479 |
-
Try MODELS in order for a single image.
|
| 480 |
-
Returns (result_dict,
|
| 481 |
-
(None, joined_error_string)
|
|
|
|
|
|
|
| 482 |
"""
|
| 483 |
b64 = pil_to_b64(img)
|
| 484 |
keys = ASPECT_KEYS[aspect]
|
|
@@ -486,53 +520,30 @@ def analyze_one(img: Image.Image, aspect: str, token: str) -> tuple:
|
|
| 486 |
errors = []
|
| 487 |
|
| 488 |
for model in MODELS:
|
| 489 |
-
short = model.split("/")[-1]
|
| 490 |
try:
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
model=model,
|
| 494 |
-
messages=[
|
| 495 |
-
{
|
| 496 |
-
"role": "system",
|
| 497 |
-
"content": (
|
| 498 |
-
"You are a JSON-only API for trailer inspection. "
|
| 499 |
-
"You MUST respond with a single valid flat JSON object and absolutely "
|
| 500 |
-
"nothing else β no explanation, no preamble, no markdown fences, "
|
| 501 |
-
"no reasoning text, no nested objects. "
|
| 502 |
-
"Every value must be exactly the string \"detected\" or \"missing\". "
|
| 503 |
-
"Start your response with '{' and end with '}'."
|
| 504 |
-
),
|
| 505 |
-
},
|
| 506 |
-
{
|
| 507 |
-
"role": "user",
|
| 508 |
-
"content": [
|
| 509 |
-
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
|
| 510 |
-
{"type": "text", "text": prompt},
|
| 511 |
-
],
|
| 512 |
-
},
|
| 513 |
-
],
|
| 514 |
-
max_tokens=120,
|
| 515 |
-
temperature=0.05,
|
| 516 |
-
)
|
| 517 |
-
raw_content = resp.choices[0].message.content
|
| 518 |
-
print(f"[{short}][{aspect}] raw: {raw_content[:300]}")
|
| 519 |
data = extract_json(raw_content, keys)
|
| 520 |
result = validate_result(data, keys)
|
| 521 |
if result is not None:
|
| 522 |
-
return result,
|
| 523 |
-
errors.append(f"{
|
| 524 |
-
except
|
| 525 |
-
|
| 526 |
-
if
|
| 527 |
-
errors.append(f"{
|
| 528 |
-
elif
|
| 529 |
-
errors.append(f"{
|
| 530 |
-
elif
|
| 531 |
-
errors.append(f"{
|
| 532 |
-
elif
|
| 533 |
-
errors.append(f"{
|
| 534 |
else:
|
| 535 |
-
errors.append(f"{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
|
| 537 |
return None, " | ".join(errors)
|
| 538 |
|
|
@@ -753,11 +764,11 @@ def root():
|
|
| 753 |
|
| 754 |
@app.get("/health", tags=["Health"])
|
| 755 |
def health():
|
| 756 |
-
token = os.environ.get("
|
| 757 |
return {
|
| 758 |
"status": "ok",
|
| 759 |
-
"
|
| 760 |
-
"models":
|
| 761 |
}
|
| 762 |
|
| 763 |
|
|
@@ -771,13 +782,14 @@ def inspect(request: InspectRequest):
|
|
| 771 |
|
| 772 |
Labels accepted: `front_right`, `front_left`, `rear_right`, `rear_left`, `inside`, `door`
|
| 773 |
"""
|
| 774 |
-
token = os.environ.get("
|
| 775 |
if not token:
|
| 776 |
raise HTTPException(
|
| 777 |
status_code=503,
|
| 778 |
detail=(
|
| 779 |
-
"
|
| 780 |
-
"Set it in Space Settings β Repository Secrets."
|
|
|
|
| 781 |
),
|
| 782 |
)
|
| 783 |
|
|
@@ -911,11 +923,11 @@ def inspect(request: InspectRequest):
|
|
| 911 |
# STARTUP
|
| 912 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 913 |
|
| 914 |
-
_tok = os.environ.get("
|
| 915 |
print("=" * 60)
|
| 916 |
-
print(" Amazon Trailer Inspector β API mode")
|
| 917 |
-
print(f"
|
| 918 |
-
print(f" Models
|
| 919 |
print("=" * 60)
|
| 920 |
|
| 921 |
if __name__ == "__main__":
|
|
|
|
| 1 |
"""
|
| 2 |
Amazon Trailer Inspector β app.py
|
| 3 |
+
HuggingFace Spaces Β· FastAPI Β· Google Gemini Vision API
|
| 4 |
|
| 5 |
REST API that accepts 6 labeled images and runs all 6 aspect inspections
|
| 6 |
in parallel, returning a structured JSON inspection report.
|
|
|
|
| 17 |
import traceback
|
| 18 |
from typing import Optional
|
| 19 |
|
| 20 |
+
import requests
|
| 21 |
import uvicorn
|
| 22 |
from fastapi import FastAPI, HTTPException
|
| 23 |
from fastapi.middleware.cors import CORSMiddleware
|
| 24 |
from fastapi.responses import JSONResponse
|
| 25 |
from PIL import Image
|
|
|
|
| 26 |
from pydantic import BaseModel, Field
|
| 27 |
|
| 28 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
+
# GEMINI MODELS (tried in order β first success wins)
|
| 30 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
MODELS = [
|
| 32 |
+
"gemini-2.0-flash", # Primary β best quality, fast, free tier (1500 req/day)
|
| 33 |
+
"gemini-1.5-flash", # Fallback β previous gen, very stable free tier
|
| 34 |
+
"gemini-1.5-flash-8b", # Fallback 2 β lightest model, always available
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
]
|
| 36 |
|
| 37 |
+
# Gemini API base URL
|
| 38 |
+
GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
|
| 39 |
+
|
| 40 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
# ASPECT PROMPTS
|
| 42 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 368 |
}
|
| 369 |
|
| 370 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 371 |
+
# GEMINI API CALL
|
| 372 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 373 |
|
| 374 |
+
def call_gemini(b64_image: str, prompt: str, model: str, api_key: str) -> str:
|
| 375 |
+
"""
|
| 376 |
+
Call Google Gemini vision API.
|
| 377 |
+
Returns the raw text response from the model.
|
| 378 |
+
Raises requests.HTTPError on API errors.
|
| 379 |
+
"""
|
| 380 |
+
url = f"{GEMINI_API_BASE}/{model}:generateContent?key={api_key}"
|
| 381 |
+
|
| 382 |
+
payload = {
|
| 383 |
+
"system_instruction": {
|
| 384 |
+
"parts": [{
|
| 385 |
+
"text": (
|
| 386 |
+
"You are a JSON-only API for trailer inspection. "
|
| 387 |
+
"You MUST respond with a single valid flat JSON object and absolutely "
|
| 388 |
+
"nothing else β no explanation, no preamble, no markdown fences, "
|
| 389 |
+
"no reasoning text, no nested objects. "
|
| 390 |
+
"Every value must be exactly the string \"detected\" or \"missing\". "
|
| 391 |
+
"Start your response with '{' and end with '}'."
|
| 392 |
+
)
|
| 393 |
+
}]
|
| 394 |
+
},
|
| 395 |
+
"contents": [{
|
| 396 |
+
"parts": [
|
| 397 |
+
{
|
| 398 |
+
"inline_data": {
|
| 399 |
+
"mime_type": "image/jpeg",
|
| 400 |
+
"data": b64_image,
|
| 401 |
+
}
|
| 402 |
+
},
|
| 403 |
+
{
|
| 404 |
+
"text": prompt,
|
| 405 |
+
}
|
| 406 |
+
]
|
| 407 |
+
}],
|
| 408 |
+
"generationConfig": {
|
| 409 |
+
"temperature": 0.05,
|
| 410 |
+
"maxOutputTokens": 120,
|
| 411 |
+
},
|
| 412 |
+
}
|
| 413 |
|
| 414 |
+
resp = requests.post(url, json=payload, timeout=45)
|
| 415 |
+
resp.raise_for_status()
|
| 416 |
+
data = resp.json()
|
| 417 |
+
return data["candidates"][0]["content"]["parts"][0]["text"]
|
|
|
|
| 418 |
|
| 419 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 420 |
# IMAGE HELPERS
|
|
|
|
| 508 |
|
| 509 |
def analyze_one(img: Image.Image, aspect: str, token: str) -> tuple:
|
| 510 |
"""
|
| 511 |
+
Try Gemini MODELS in order for a single image.
|
| 512 |
+
Returns (result_dict, model_name) on success,
|
| 513 |
+
(None, joined_error_string) on total failure.
|
| 514 |
+
Image is encoded once and reused across all fallback attempts.
|
| 515 |
+
token = GEMINI_API_KEY environment variable value.
|
| 516 |
"""
|
| 517 |
b64 = pil_to_b64(img)
|
| 518 |
keys = ASPECT_KEYS[aspect]
|
|
|
|
| 520 |
errors = []
|
| 521 |
|
| 522 |
for model in MODELS:
|
|
|
|
| 523 |
try:
|
| 524 |
+
raw_content = call_gemini(b64, prompt, model, token)
|
| 525 |
+
print(f"[{model}][{aspect}] raw: {raw_content[:300]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
data = extract_json(raw_content, keys)
|
| 527 |
result = validate_result(data, keys)
|
| 528 |
if result is not None:
|
| 529 |
+
return result, model
|
| 530 |
+
errors.append(f"{model}: JSON parse failed. Raw: {raw_content[:150]}")
|
| 531 |
+
except requests.HTTPError as e:
|
| 532 |
+
status = e.response.status_code if e.response is not None else "?"
|
| 533 |
+
if status == 400:
|
| 534 |
+
errors.append(f"{model}: bad request β check image or prompt ({str(e)[:120]})")
|
| 535 |
+
elif status in (401, 403):
|
| 536 |
+
errors.append(f"{model}: invalid API key β check GEMINI_API_KEY")
|
| 537 |
+
elif status == 429:
|
| 538 |
+
errors.append(f"{model}: rate limited β retrying next model")
|
| 539 |
+
elif status == 503:
|
| 540 |
+
errors.append(f"{model}: service unavailable β retrying next model")
|
| 541 |
else:
|
| 542 |
+
errors.append(f"{model}: HTTP {status} β {str(e)[:150]}")
|
| 543 |
+
except requests.Timeout:
|
| 544 |
+
errors.append(f"{model}: request timed out β retrying next model")
|
| 545 |
+
except Exception as e:
|
| 546 |
+
errors.append(f"{model}: {str(e)[:180]}")
|
| 547 |
|
| 548 |
return None, " | ".join(errors)
|
| 549 |
|
|
|
|
| 764 |
|
| 765 |
@app.get("/health", tags=["Health"])
|
| 766 |
def health():
|
| 767 |
+
token = os.environ.get("GEMINI_API_KEY", "").strip()
|
| 768 |
return {
|
| 769 |
"status": "ok",
|
| 770 |
+
"gemini_api_key_set": bool(token),
|
| 771 |
+
"models": MODELS,
|
| 772 |
}
|
| 773 |
|
| 774 |
|
|
|
|
| 782 |
|
| 783 |
Labels accepted: `front_right`, `front_left`, `rear_right`, `rear_left`, `inside`, `door`
|
| 784 |
"""
|
| 785 |
+
token = os.environ.get("GEMINI_API_KEY", "").strip()
|
| 786 |
if not token:
|
| 787 |
raise HTTPException(
|
| 788 |
status_code=503,
|
| 789 |
detail=(
|
| 790 |
+
"GEMINI_API_KEY not configured. "
|
| 791 |
+
"Set it in Space Settings β Repository Secrets. "
|
| 792 |
+
"Get a free key at https://aistudio.google.com/apikey"
|
| 793 |
),
|
| 794 |
)
|
| 795 |
|
|
|
|
| 923 |
# STARTUP
|
| 924 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 925 |
|
| 926 |
+
_tok = os.environ.get("GEMINI_API_KEY", "")
|
| 927 |
print("=" * 60)
|
| 928 |
+
print(" Amazon Trailer Inspector β API mode (Gemini)")
|
| 929 |
+
print(f" GEMINI_API_KEY : {'SET (' + str(len(_tok)) + ' chars)' if _tok else 'NOT SET β οΈ β get free key at aistudio.google.com/apikey'}")
|
| 930 |
+
print(f" Models : {MODELS}")
|
| 931 |
print("=" * 60)
|
| 932 |
|
| 933 |
if __name__ == "__main__":
|