Spaces:
Runtime error
Runtime error
| import base64 | |
| import json | |
| import mimetypes | |
| import os | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| import gradio as gr | |
| from dotenv import load_dotenv | |
| from transformers import pipeline | |
| try: | |
| from openai import OpenAI | |
| except ModuleNotFoundError: | |
| OpenAI = None | |
| load_dotenv() | |
| # Configure environment variables for easy Hugging Face Space setup. | |
| CUSTOM_MODEL_REPO_ID = os.getenv("CUSTOM_MODEL_REPO_ID", "your-hf-username/sports-vit-transfer") | |
| OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4.1-mini") | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") | |
| TOP_K = int(os.getenv("TOP_K", "5")) | |
| openai_client = OpenAI(api_key=OPENAI_API_KEY) if OpenAI is not None and OPENAI_API_KEY else None | |
| # Extended label set for zero-shot and OpenAI comparison. | |
| EXTENDED_SPORT_LABELS = [ | |
| "football", | |
| "tennis", | |
| "golf", | |
| "baseball", | |
| "basketball", | |
| "volleyball", | |
| "handball", | |
| "rugby", | |
| "cricket", | |
| "hockey", | |
| "table tennis", | |
| "badminton", | |
| "swimming", | |
| "skiing", | |
| "surfing", | |
| "boxing", | |
| "athletics", | |
| "american football", | |
| ] | |
| _custom_classifier = None | |
| _clip_classifier = None | |
| def _parse_json_from_text(text: str) -> Optional[Dict]: | |
| cleaned = text.strip() | |
| if cleaned.startswith("```"): | |
| lines = cleaned.splitlines() | |
| if len(lines) >= 3: | |
| cleaned = "\n".join(lines[1:-1]).strip() | |
| try: | |
| return json.loads(cleaned) | |
| except json.JSONDecodeError: | |
| pass | |
| start = cleaned.find("{") | |
| end = cleaned.rfind("}") | |
| if start != -1 and end != -1 and end > start: | |
| candidate = cleaned[start : end + 1] | |
| try: | |
| return json.loads(candidate) | |
| except json.JSONDecodeError: | |
| return None | |
| return None | |
| def _load_custom_classifier(): | |
| global _custom_classifier | |
| if _custom_classifier is None: | |
| _custom_classifier = pipeline( | |
| "image-classification", | |
| model=CUSTOM_MODEL_REPO_ID, | |
| top_k=TOP_K, | |
| ) | |
| return _custom_classifier | |
| def _load_clip_classifier(): | |
| global _clip_classifier | |
| if _clip_classifier is None: | |
| _clip_classifier = pipeline( | |
| task="zero-shot-image-classification", | |
| model="openai/clip-vit-large-patch14", | |
| ) | |
| return _clip_classifier | |
| def _to_score_dict(results: List[Dict]) -> Dict[str, float]: | |
| return {row["label"]: round(float(row["score"]), 6) for row in results} | |
| def _encode_image(image_path: str) -> str: | |
| with open(image_path, "rb") as f: | |
| return base64.b64encode(f.read()).decode("utf-8") | |
| def classify_with_openai(image_path: str) -> Dict: | |
| if OpenAI is None: | |
| return { | |
| "error": "OpenAI package is not installed in this environment.", | |
| "hint": "Add 'openai' to requirements.txt and redeploy the Space.", | |
| } | |
| if openai_client is None: | |
| return { | |
| "error": "OPENAI_API_KEY fehlt. Setze es in den Space Secrets.", | |
| "hint": "In Hugging Face Space unter Settings -> Secrets als OPENAI_API_KEY eintragen.", | |
| } | |
| mime_type, _ = mimetypes.guess_type(image_path) | |
| mime_type = mime_type or "image/jpeg" | |
| base64_image = _encode_image(image_path) | |
| prompt = ( | |
| "Classify the sport shown in this image. " | |
| f"Choose exactly one label from this list: {', '.join(EXTENDED_SPORT_LABELS)}. " | |
| "Return valid JSON only with keys: label, confidence, reasoning. " | |
| "confidence must be a number between 0 and 1." | |
| ) | |
| response = openai_client.responses.create( | |
| model=OPENAI_MODEL, | |
| input=[ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "input_text", "text": prompt}, | |
| { | |
| "type": "input_image", | |
| "image_url": f"data:{mime_type};base64,{base64_image}", | |
| }, | |
| ], | |
| } | |
| ], | |
| ) | |
| text = response.output_text.strip() | |
| parsed = _parse_json_from_text(text) | |
| if parsed is None: | |
| parsed = { | |
| "warning": "OpenAI antwortete nicht als valides JSON.", | |
| "raw_response": text, | |
| } | |
| return parsed | |
| def classify_sport(image_path: Optional[str]): | |
| if image_path is None: | |
| return {"error": "Bitte ein Bild hochladen."} | |
| output = {} | |
| if CUSTOM_MODEL_REPO_ID == "your-hf-username/sports-vit-transfer": | |
| output["Custom Transfer Model"] = { | |
| "error": "CUSTOM_MODEL_REPO_ID ist noch der Platzhalter.", | |
| "hint": "Setze CUSTOM_MODEL_REPO_ID in Space Variables auf dein HF-Modellrepo.", | |
| } | |
| else: | |
| try: | |
| custom_results = _load_custom_classifier()(image_path) | |
| output["Custom Transfer Model"] = _to_score_dict(custom_results) | |
| except Exception as exc: # noqa: BLE001 | |
| output["Custom Transfer Model"] = {"error": f"Custom model failed: {exc}"} | |
| try: | |
| clip_results = _load_clip_classifier()(image_path, candidate_labels=EXTENDED_SPORT_LABELS) | |
| output["CLIP Zero-Shot"] = _to_score_dict(clip_results) | |
| except Exception as exc: # noqa: BLE001 | |
| output["CLIP Zero-Shot"] = {"error": f"CLIP failed: {exc}"} | |
| output["OpenAI Vision"] = classify_with_openai(image_path) | |
| return output | |
| def _collect_examples() -> List[List[str]]: | |
| examples_dir = Path("example_images") | |
| if not examples_dir.exists(): | |
| return [] | |
| allowed_ext = {".jpg", ".jpeg", ".png", ".webp"} | |
| paths = sorted([p for p in examples_dir.iterdir() if p.suffix.lower() in allowed_ext]) | |
| return [[str(p)] for p in paths] | |
| iface = gr.Interface( | |
| fn=classify_sport, | |
| inputs=gr.Image(type="filepath", label="Sportbild hochladen"), | |
| outputs=gr.JSON(label="Modellvergleich"), | |
| title="Sport Classification Comparison", | |
| examples=_collect_examples(), | |
| ) | |
| if __name__ == "__main__": | |
| iface.launch() | |