Spaces:
Runtime error
Runtime error
fix (hf): use gpu from hf space
Browse files- README.md +1 -1
- app.py +38 -0
- requirements.txt +1 -0
- src/model_loader.py +24 -3
README.md
CHANGED
|
@@ -9,7 +9,7 @@ python_version: "3.13"
|
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
-
hardware:
|
| 13 |
short_description: AI wardrobe. catalog, combine and ask about your clothes
|
| 14 |
---
|
| 15 |
|
|
|
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
+
hardware: zero-gpu
|
| 13 |
short_description: AI wardrobe. catalog, combine and ask about your clothes
|
| 14 |
---
|
| 15 |
|
app.py
CHANGED
|
@@ -9,8 +9,24 @@ Built for the Build Small Hackathon (HuggingFace x Gradio, June 2026).
|
|
| 9 |
import io
|
| 10 |
import logging
|
| 11 |
import os
|
|
|
|
| 12 |
from pathlib import Path
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
from dotenv import load_dotenv
|
| 15 |
|
| 16 |
load_dotenv(Path(__file__).resolve().parent / ".env")
|
|
@@ -38,6 +54,19 @@ from src.detector import detect_garments as detect_boxes, crop_garments, list_av
|
|
| 38 |
from src import settings
|
| 39 |
from gradio_image_annotation import image_annotator
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0")
|
| 42 |
logging.basicConfig(level=logging.INFO, format="%(name)s | %(message)s")
|
| 43 |
|
|
@@ -79,6 +108,7 @@ def _format_results(added: list[dict]) -> tuple[str, list]:
|
|
| 79 |
return status, table_data
|
| 80 |
|
| 81 |
|
|
|
|
| 82 |
def auto_detect(annotation, mode, progress=gr.Progress(track_tqdm=False)):
|
| 83 |
"""Run automatic detection on the uploaded image.
|
| 84 |
|
|
@@ -116,6 +146,7 @@ def auto_detect(annotation, mode, progress=gr.Progress(track_tqdm=False)):
|
|
| 116 |
return _format_results(added)
|
| 117 |
|
| 118 |
|
|
|
|
| 119 |
def process_manual_selection(annotation, progress=gr.Progress(track_tqdm=False)):
|
| 120 |
"""Process manually drawn bounding boxes from the annotator.
|
| 121 |
|
|
@@ -185,6 +216,7 @@ SAMPLE_DATASETS = [
|
|
| 185 |
TARGET_GARMENTS = 50
|
| 186 |
|
| 187 |
|
|
|
|
| 188 |
def obtener_dataset(dataset_key: str):
|
| 189 |
"""Download a HF dataset and process garments in-process with real-time UI updates.
|
| 190 |
|
|
@@ -421,6 +453,7 @@ def clear_all():
|
|
| 421 |
# Chat handlers
|
| 422 |
# ---------------------------------------------------------------------------
|
| 423 |
|
|
|
|
| 424 |
def chat_respond(message, history):
|
| 425 |
"""Handle chat messages with streaming responses."""
|
| 426 |
if not message:
|
|
@@ -463,6 +496,7 @@ def _get_combo_display(combo: dict | None) -> tuple[str | None, str | None, str,
|
|
| 463 |
return top_img, bottom_img, top_text, bottom_text
|
| 464 |
|
| 465 |
|
|
|
|
| 466 |
def init_combinations(state, context):
|
| 467 |
"""Initialize or refresh the combination queue, optionally ranked by context."""
|
| 468 |
combos = generate_combinations()
|
|
@@ -818,6 +852,7 @@ def _build_custom_server():
|
|
| 818 |
return {"garments": catalog, "count": len(catalog)}
|
| 819 |
|
| 820 |
@server.api(name="add_photo")
|
|
|
|
| 821 |
def api_add_photo(image_path: str) -> dict:
|
| 822 |
results = extract_garments(image_path)
|
| 823 |
if not results:
|
|
@@ -828,6 +863,7 @@ def _build_custom_server():
|
|
| 828 |
return {"garments": added, "count": len(added)}
|
| 829 |
|
| 830 |
@server.api(name="get_combinations")
|
|
|
|
| 831 |
def api_get_combinations(context: str = "") -> dict:
|
| 832 |
combos = generate_combinations()
|
| 833 |
if not combos:
|
|
@@ -852,12 +888,14 @@ def _build_custom_server():
|
|
| 852 |
return {"status": "ok", "liked": liked}
|
| 853 |
|
| 854 |
@server.api(name="ask_question")
|
|
|
|
| 855 |
def api_ask_question(question: str) -> str:
|
| 856 |
if not question or not question.strip():
|
| 857 |
return "Please ask a question about your wardrobe."
|
| 858 |
return ask(question.strip())
|
| 859 |
|
| 860 |
@server.api(name="load_dataset")
|
|
|
|
| 861 |
def api_load_dataset(dataset_key: str) -> dict:
|
| 862 |
from datasets import load_dataset as hf_load
|
| 863 |
|
|
|
|
| 9 |
import io
|
| 10 |
import logging
|
| 11 |
import os
|
| 12 |
+
import sys
|
| 13 |
from pathlib import Path
|
| 14 |
|
| 15 |
+
# Python 3.13 raises spurious ValueError during event-loop GC when httpx
|
| 16 |
+
# (used internally by Gradio) creates and discards temporary loops.
|
| 17 |
+
# Suppress the noise — it has no functional impact.
|
| 18 |
+
if sys.version_info >= (3, 13):
|
| 19 |
+
_original_unraisablehook = sys.unraisablehook
|
| 20 |
+
|
| 21 |
+
def _quiet_unraisablehook(unraisable):
|
| 22 |
+
if isinstance(unraisable.exc_value, ValueError) and "file descriptor" in str(
|
| 23 |
+
unraisable.exc_value
|
| 24 |
+
):
|
| 25 |
+
return
|
| 26 |
+
_original_unraisablehook(unraisable)
|
| 27 |
+
|
| 28 |
+
sys.unraisablehook = _quiet_unraisablehook
|
| 29 |
+
|
| 30 |
from dotenv import load_dotenv
|
| 31 |
|
| 32 |
load_dotenv(Path(__file__).resolve().parent / ".env")
|
|
|
|
| 54 |
from src import settings
|
| 55 |
from gradio_image_annotation import image_annotator
|
| 56 |
|
| 57 |
+
try:
|
| 58 |
+
import spaces
|
| 59 |
+
except ImportError:
|
| 60 |
+
class _NoOpGPU:
|
| 61 |
+
"""Fallback when not running on HF Spaces (local dev)."""
|
| 62 |
+
def __call__(self, fn=None, *, duration=None):
|
| 63 |
+
if fn is None:
|
| 64 |
+
return lambda f: f
|
| 65 |
+
return fn
|
| 66 |
+
|
| 67 |
+
class spaces: # type: ignore[no-redef]
|
| 68 |
+
GPU = _NoOpGPU()
|
| 69 |
+
|
| 70 |
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0")
|
| 71 |
logging.basicConfig(level=logging.INFO, format="%(name)s | %(message)s")
|
| 72 |
|
|
|
|
| 108 |
return status, table_data
|
| 109 |
|
| 110 |
|
| 111 |
+
@spaces.GPU(duration=120)
|
| 112 |
def auto_detect(annotation, mode, progress=gr.Progress(track_tqdm=False)):
|
| 113 |
"""Run automatic detection on the uploaded image.
|
| 114 |
|
|
|
|
| 146 |
return _format_results(added)
|
| 147 |
|
| 148 |
|
| 149 |
+
@spaces.GPU(duration=120)
|
| 150 |
def process_manual_selection(annotation, progress=gr.Progress(track_tqdm=False)):
|
| 151 |
"""Process manually drawn bounding boxes from the annotator.
|
| 152 |
|
|
|
|
| 216 |
TARGET_GARMENTS = 50
|
| 217 |
|
| 218 |
|
| 219 |
+
@spaces.GPU(duration=300)
|
| 220 |
def obtener_dataset(dataset_key: str):
|
| 221 |
"""Download a HF dataset and process garments in-process with real-time UI updates.
|
| 222 |
|
|
|
|
| 453 |
# Chat handlers
|
| 454 |
# ---------------------------------------------------------------------------
|
| 455 |
|
| 456 |
+
@spaces.GPU(duration=90)
|
| 457 |
def chat_respond(message, history):
|
| 458 |
"""Handle chat messages with streaming responses."""
|
| 459 |
if not message:
|
|
|
|
| 496 |
return top_img, bottom_img, top_text, bottom_text
|
| 497 |
|
| 498 |
|
| 499 |
+
@spaces.GPU(duration=60)
|
| 500 |
def init_combinations(state, context):
|
| 501 |
"""Initialize or refresh the combination queue, optionally ranked by context."""
|
| 502 |
combos = generate_combinations()
|
|
|
|
| 852 |
return {"garments": catalog, "count": len(catalog)}
|
| 853 |
|
| 854 |
@server.api(name="add_photo")
|
| 855 |
+
@spaces.GPU(duration=120)
|
| 856 |
def api_add_photo(image_path: str) -> dict:
|
| 857 |
results = extract_garments(image_path)
|
| 858 |
if not results:
|
|
|
|
| 863 |
return {"garments": added, "count": len(added)}
|
| 864 |
|
| 865 |
@server.api(name="get_combinations")
|
| 866 |
+
@spaces.GPU(duration=60)
|
| 867 |
def api_get_combinations(context: str = "") -> dict:
|
| 868 |
combos = generate_combinations()
|
| 869 |
if not combos:
|
|
|
|
| 888 |
return {"status": "ok", "liked": liked}
|
| 889 |
|
| 890 |
@server.api(name="ask_question")
|
| 891 |
+
@spaces.GPU(duration=90)
|
| 892 |
def api_ask_question(question: str) -> str:
|
| 893 |
if not question or not question.strip():
|
| 894 |
return "Please ask a question about your wardrobe."
|
| 895 |
return ask(question.strip())
|
| 896 |
|
| 897 |
@server.api(name="load_dataset")
|
| 898 |
+
@spaces.GPU(duration=300)
|
| 899 |
def api_load_dataset(dataset_key: str) -> dict:
|
| 900 |
from datasets import load_dataset as hf_load
|
| 901 |
|
requirements.txt
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124
|
|
|
|
| 2 |
gradio==6.17.3
|
| 3 |
llama-cpp-python>=0.3.28
|
| 4 |
huggingface-hub>=1.18.0
|
|
|
|
| 1 |
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124
|
| 2 |
+
spaces
|
| 3 |
gradio==6.17.3
|
| 4 |
llama-cpp-python>=0.3.28
|
| 5 |
huggingface-hub>=1.18.0
|
src/model_loader.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
| 1 |
"""Singleton model loader with VRAM management.
|
| 2 |
|
| 3 |
Handles loading/unloading of GGUF models. Only one model is kept in
|
| 4 |
-
memory at a time to fit within
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import gc
|
| 8 |
import logging
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
from dataclasses import dataclass
|
| 11 |
|
|
@@ -90,6 +94,19 @@ class _ModelManager:
|
|
| 90 |
|
| 91 |
return model_path, mmproj_path
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
def _is_same_model(self, config: ModelConfig) -> bool:
|
| 94 |
if self._current_config is None:
|
| 95 |
return False
|
|
@@ -119,12 +136,16 @@ class _ModelManager:
|
|
| 119 |
from llama_cpp.llama_chat_format import Qwen25VLChatHandler
|
| 120 |
chat_handler = Qwen25VLChatHandler(clip_model_path=str(mmproj_path))
|
| 121 |
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
self._llm = Llama(
|
| 125 |
model_path=str(model_path),
|
| 126 |
chat_handler=chat_handler,
|
| 127 |
-
n_gpu_layers=
|
| 128 |
n_ctx=config.n_ctx,
|
| 129 |
verbose=False,
|
| 130 |
)
|
|
|
|
| 1 |
"""Singleton model loader with VRAM management.
|
| 2 |
|
| 3 |
Handles loading/unloading of GGUF models. Only one model is kept in
|
| 4 |
+
memory at a time to fit within available VRAM.
|
| 5 |
+
|
| 6 |
+
Supports ZeroGPU (dynamic GPU allocation on HF Spaces) by detecting
|
| 7 |
+
CUDA availability at load time rather than import time.
|
| 8 |
"""
|
| 9 |
|
| 10 |
import gc
|
| 11 |
import logging
|
| 12 |
+
import os
|
| 13 |
from pathlib import Path
|
| 14 |
from dataclasses import dataclass
|
| 15 |
|
|
|
|
| 94 |
|
| 95 |
return model_path, mmproj_path
|
| 96 |
|
| 97 |
+
@staticmethod
|
| 98 |
+
def _detect_gpu_layers() -> int:
|
| 99 |
+
"""Return -1 (all layers on GPU) if CUDA is available, else 0 (CPU)."""
|
| 100 |
+
if os.environ.get("CUDA_VISIBLE_DEVICES") == "":
|
| 101 |
+
return 0
|
| 102 |
+
try:
|
| 103 |
+
import ctypes
|
| 104 |
+
ctypes.CDLL("libcudart.so.12")
|
| 105 |
+
return -1
|
| 106 |
+
except OSError:
|
| 107 |
+
logger.warning("CUDA runtime not found — running on CPU")
|
| 108 |
+
return 0
|
| 109 |
+
|
| 110 |
def _is_same_model(self, config: ModelConfig) -> bool:
|
| 111 |
if self._current_config is None:
|
| 112 |
return False
|
|
|
|
| 136 |
from llama_cpp.llama_chat_format import Qwen25VLChatHandler
|
| 137 |
chat_handler = Qwen25VLChatHandler(clip_model_path=str(mmproj_path))
|
| 138 |
|
| 139 |
+
n_gpu_layers = self._detect_gpu_layers()
|
| 140 |
+
logger.info(
|
| 141 |
+
"Loading model: %s (handler: %s, gpu_layers: %s)",
|
| 142 |
+
config.model_file, config.handler_type, n_gpu_layers,
|
| 143 |
+
)
|
| 144 |
|
| 145 |
self._llm = Llama(
|
| 146 |
model_path=str(model_path),
|
| 147 |
chat_handler=chat_handler,
|
| 148 |
+
n_gpu_layers=n_gpu_layers,
|
| 149 |
n_ctx=config.n_ctx,
|
| 150 |
verbose=False,
|
| 151 |
)
|