"""Hugging Face ZeroGPU Space: SatQuery TEOChat Official Bi-Temporal Reasoning Service. Official Model: jirvin16/TEOChat Official Repository: https://github.com/ermongroup/TEOChat Responsibilities: 1. Bi-temporal satellite image reasoning (T1 -> T2) 2. Temporal change description 3. Change-based Visual Question Answering (VQA) 4. Temporal Earth Observation question answering NO mock fallbacks. NO NumPy image differencing. NO hardcoded percentages or categories. Uses real Video-LLaVA / TEOChat official inference code only. """ try: import spaces gpu_decorator = spaces.GPU(duration=20) has_spaces = True except Exception: def gpu_decorator(fn): return fn has_spaces = False import os import io import time import base64 import tempfile import logging from typing import Optional, Dict, Any, List from PIL import Image import torch import gradio as gr from fastapi import FastAPI, Request, Response, status from pydantic import BaseModel, Field # Setup logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("satquery.teochat") MODEL_ID = os.getenv("MODEL_ID", "jirvin16/TEOChat") DEVICE = "cuda" if torch.cuda.is_available() else "cpu" _model = None _tokenizer = None _processor = None _load_error = None def ensure_videollava_installed(): """Ensure videollava package from official TEOChat repo is importable.""" try: import videollava return except ImportError: logger.info("[TEOChat] videollava not found. Attempting to install from official repo...") import subprocess import sys try: subprocess.run( [sys.executable, "-m", "pip", "install", "--no-deps", "git+https://github.com/ermongroup/TEOChat.git"], check=True, capture_output=True ) logger.info("[TEOChat] Successfully installed TEOChat videollava package!") except Exception as err: logger.error(f"[TEOChat] Failed to pip install git+https://github.com/ermongroup/TEOChat.git: {err}") raise ImportError( "Official TEOChat 'videollava' library is required. " "Please run: pip install git+https://github.com/ermongroup/TEOChat.git" ) from err def load_teochat_model(target_device: Optional[str] = None): """Load the official TEOChat model using official videollava.eval.eval.load_model.""" global _model, _tokenizer, _processor, _load_error resolved_dev = target_device or DEVICE if _model is not None and _tokenizer is not None and _processor is not None: if resolved_dev == "cpu": try: _model = _model.to("cpu") except Exception: pass return _tokenizer, _model, _processor logger.info(f"[TEOChat] Loading official model from {MODEL_ID} on {resolved_dev}...") try: ensure_videollava_installed() from videollava.eval.eval import load_model from videollava.eval.inference import run_inference_single # On ZeroGPU A10G (24GB VRAM), load in FP16 natively for maximum speed and stability use_8bit = False tokenizer, model, processor = load_model( model_path=MODEL_ID, model_base=None, load_8bit=use_8bit, device=resolved_dev ) _tokenizer = tokenizer _model = model _processor = processor _load_error = None logger.info("[TEOChat] Successfully loaded official checkpoint!") return _tokenizer, _model, _processor except Exception as exc: _load_error = str(exc) logger.error(f"[TEOChat] Failed to load official model: {exc}", exc_info=True) raise exc def decode_and_save_temp(img_input: Any, suffix: str = ".png") -> str: """Decode image payload and persist to a temporary file path for the official processor.""" if img_input is None: raise ValueError("Missing required temporal image input.") # If it's already a valid existing file path on disk, return it directly if isinstance(img_input, str) and os.path.exists(img_input): return os.path.abspath(img_input) pil_img = None if isinstance(img_input, Image.Image): pil_img = img_input.convert("RGB") elif isinstance(img_input, str): if img_input.startswith("data:image"): _, b64 = img_input.split(",", 1) raw = base64.b64decode(b64) pil_img = Image.open(io.BytesIO(raw)).convert("RGB") else: try: raw = base64.b64decode(img_input) pil_img = Image.open(io.BytesIO(raw)).convert("RGB") except Exception as e: raise ValueError(f"Failed to decode base64 image: {e}") if pil_img is None: raise ValueError(f"Unrecognized image input format: {type(img_input)}") tmp_file = tempfile.NamedTemporaryFile(suffix=suffix, delete=False) pil_img.save(tmp_file.name, format="PNG") tmp_file.close() return tmp_file.name def _run_teochat_inference_core( image_t1_input: Any, image_t2_input: Any, query: str, target_device: Optional[str] = None ) -> Dict[str, Any]: """Execute official TEOChat forward pass on bi-temporal sequence [T1, T2].""" t0 = time.time() tokenizer, model, processor = load_teochat_model(target_device=target_device) if target_device and str(next(model.parameters()).device) != target_device: try: model = model.to(target_device) except Exception as dev_err: logger.warning(f"[TEOChat] Could not move model to {target_device}: {dev_err}") if not query or not query.strip(): query = "What changed between T1 and T2?" # 1. Save or expose valid local file paths t1_path = decode_and_save_temp(image_t1_input, suffix="_t1.png") t2_path = decode_and_save_temp(image_t2_input, suffix="_t2.png") try: # 2. Preserve strict chronological order [T1, T2] image_paths = [t1_path, t2_path] # 3. Build official prompt containing