Spaces:
Running on Zero
Running on Zero
| """ | |
| Model Manager. | |
| DESIGN DECISION: this entire application runs on free CPU Basic hardware — | |
| no ZeroGPU is requested anywhere. All three models were deliberately chosen | |
| small enough (343MB classifier, ~100MB captioner, 0.5B-param LLM) that CPU | |
| inference is fast enough for a live demo without fighting GPU quota/cold-start | |
| behavior. This is the "smallest model that produces a convincing demo" | |
| principle applied literally. | |
| Models are loaded lazily (only when first needed) and cached as module-level | |
| singletons so repeated calls during one demo run don't reload from disk. | |
| Every load is wrapped so a failure degrades the relevant agent instead of | |
| crashing the whole Space (section 25). | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import threading | |
| logger = logging.getLogger("model_manager") | |
| logging.basicConfig(level=logging.INFO) | |
| _lock = threading.Lock() | |
| _cache: dict[str, object] = {} | |
| CLASSIFIER_MODEL_ID = "Luwayy/disaster_images_model" | |
| CAPTION_MODEL_ID = "cnmoro/tiny-image-captioning" | |
| LLM_MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct" | |
| class ModelLoadError(Exception): | |
| pass | |
| def _get_or_load(key: str, loader_fn): | |
| with _lock: | |
| if key in _cache: | |
| return _cache[key] | |
| try: | |
| logger.info(f"Loading model for '{key}' (first use, will be cached)...") | |
| obj = loader_fn() | |
| _cache[key] = obj | |
| logger.info(f"Model '{key}' loaded and cached.") | |
| return obj | |
| except Exception as e: | |
| logger.error(f"Failed to load model '{key}': {e}") | |
| raise ModelLoadError(f"Could not load model for {key}: {e}") from e | |
| def get_classifier(): | |
| """Image classification pipeline: disaster damage-type classification.""" | |
| def _load(): | |
| from transformers import pipeline | |
| return pipeline("image-classification", model=CLASSIFIER_MODEL_ID) | |
| return _get_or_load("classifier", _load) | |
| def get_captioner(): | |
| """Image captioning model: produces the 'evidence' text for a zone's photo.""" | |
| def _load(): | |
| from transformers import AutoTokenizer, AutoImageProcessor, VisionEncoderDecoderModel | |
| tokenizer = AutoTokenizer.from_pretrained(CAPTION_MODEL_ID) | |
| image_processor = AutoImageProcessor.from_pretrained(CAPTION_MODEL_ID) | |
| model = VisionEncoderDecoderModel.from_pretrained(CAPTION_MODEL_ID) | |
| model.eval() | |
| return {"model": model, "tokenizer": tokenizer, "image_processor": image_processor} | |
| return _get_or_load("captioner", _load) | |
| def get_llm(): | |
| """Small instruct LLM used for report parsing (JSON extraction) and explanation.""" | |
| def _load(): | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL_ID) | |
| model = AutoModelForCausalLM.from_pretrained(LLM_MODEL_ID, torch_dtype="auto") | |
| model.eval() | |
| return {"model": model, "tokenizer": tokenizer} | |
| return _get_or_load("llm", _load) | |
| def release_all(): | |
| """Frees cached models. Not called during normal demo operation, but available | |
| if the Space needs to recover memory between sessions.""" | |
| with _lock: | |
| _cache.clear() | |
| logger.info("All cached models released.") | |
| def preload_status() -> dict[str, bool]: | |
| """Reports which models are currently loaded, for the UI's system status strip.""" | |
| return { | |
| "classifier": "classifier" in _cache, | |
| "captioner": "captioner" in _cache, | |
| "llm": "llm" in _cache, | |
| } | |