mlbench123 commited on
Commit
795f5fe
Β·
verified Β·
1 Parent(s): cd6bb91

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -76
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  Amazon Trailer Inspector β€” app.py
3
- HuggingFace Spaces Β· FastAPI Β· Free vision LLMs
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 per image)
30
  # ──────────────────────────────────────────────────────────────────────────────
31
  MODELS = [
32
- # ── Tier 1: best vision quality ──────────────────────────────────────────
33
- "google/gemma-4-27b-it", # Primary β€” Gemma 4 27B (stable HF serverless name)
34
- "meta-llama/Llama-4-Scout-17B-16E-Instruct", # Llama 4 Scout β€” excellent vision, free HF
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
- # HF CLIENT
377
  # ──────────────────────────────────────────────────────────────────────────────
378
 
379
- _hf_client: InferenceClient | None = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
- def _get_client(token: str) -> InferenceClient:
382
- global _hf_client
383
- if _hf_client is None:
384
- _hf_client = InferenceClient(provider="auto", api_key=token)
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, model_short_name) on success,
481
- (None, joined_error_string) on total failure.
 
 
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
- client = _get_client(token)
492
- resp = client.chat_completion(
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, short
523
- errors.append(f"{short}: JSON parse failed. Raw: {raw_content[:150]}")
524
- except Exception as e:
525
- err = str(e)
526
- if "401" in err or "403" in err:
527
- errors.append(f"{short}: auth error β€” check HF_TOKEN ({err[:100]})")
528
- elif "404" in err:
529
- errors.append(f"{short}: 404 β€” model unavailable ({err[:100]})")
530
- elif "429" in err:
531
- errors.append(f"{short}: rate limited β€” retrying next model")
532
- elif "503" in err or "502" in err:
533
- errors.append(f"{short}: model loading β€” retrying next model")
534
  else:
535
- errors.append(f"{short}: {err[:180]}")
 
 
 
 
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("HF_TOKEN", "").strip()
757
  return {
758
  "status": "ok",
759
- "hf_token_set": bool(token),
760
- "models": [m.split("/")[-1] for m in 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("HF_TOKEN", "").strip()
775
  if not token:
776
  raise HTTPException(
777
  status_code=503,
778
  detail=(
779
- "HF_TOKEN not configured. "
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("HF_TOKEN", "")
915
  print("=" * 60)
916
- print(" Amazon Trailer Inspector β€” API mode")
917
- print(f" HF_TOKEN : {'SET (' + str(len(_tok)) + ' chars)' if _tok else 'NOT SET ⚠️'}")
918
- print(f" Models : {[m.split('/')[-1] for m in 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__":