Ox1 commited on
Commit
cb7a01c
·
1 Parent(s): 4865627

feat (wardrobe): prepare project structure and basic gradio ui

Browse files
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ models/
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ data/catalog.json
6
+ data/garments/
7
+ .env
8
+ docs/
README.md CHANGED
@@ -12,4 +12,52 @@ license: fair-noncommercial-research-license
12
  short_description: An Smart Way to Track your Clothes and Choose the best outfi
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  short_description: An Smart Way to Track your Clothes and Choose the best outfi
13
  ---
14
 
15
+ # 👕 Wardrobe AI
16
+
17
+ Wardrobe AI helps people understand, organize and make better use of the clothes they already own.
18
+
19
+ Instead of manually cataloging garments, users can simply record a video of their wardrobe. AI extracts garments, identifies attributes and builds a searchable wardrobe catalog.
20
+
21
+ This project was created for the Gradio × Hugging Face Small Models Hackathon.
22
+
23
+ ---
24
+
25
+ ## Problem
26
+
27
+ Many people:
28
+
29
+ - Forget what clothes they own
30
+ - Buy duplicate garments
31
+ - Struggle to create outfits
32
+ - Don't remember care instructions
33
+ - Have difficulty organizing clothes by season
34
+
35
+ Wardrobe AI transforms a physical wardrobe into a structured digital inventory.
36
+
37
+ ---
38
+
39
+ ## Vision
40
+
41
+ ### Capture
42
+
43
+ Users upload:
44
+
45
+ - Photos
46
+ - Videos
47
+
48
+ The system detects garments and extracts relevant information.
49
+
50
+ ### Catalog
51
+
52
+ Each garment becomes a structured entity:
53
+
54
+ ```json
55
+ {
56
+ "type": "shirt",
57
+ "color": "blue",
58
+ "material": "cotton",
59
+ "brand": "Levi's",
60
+ "season": "spring",
61
+ "style": "casual"
62
+ }
63
+ ```
app.py CHANGED
@@ -1,7 +1,95 @@
1
  import gradio as gr
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
 
 
 
3
 
4
+ def process_video(video):
5
+ if video is None:
6
+ return "Please upload a wardrobe video."
7
+
8
+ return """
9
+ ✅ Video uploaded successfully.
10
+
11
+ Future pipeline:
12
+
13
+ 1. Detect garments
14
+ 2. Extract attributes
15
+ 3. Build wardrobe catalog
16
+ 4. Generate outfit recommendations
17
+ """
18
+
19
+
20
+ def ask_assistant(question):
21
+ if not question:
22
+ return "Ask me something about your wardrobe."
23
+
24
+ return f"""
25
+ Question: {question}
26
+
27
+ 🚧 Assistant functionality is under development.
28
+
29
+ Future capabilities:
30
+ - Outfit recommendations
31
+ - Seasonal organization
32
+ - Laundry instructions
33
+ - Garment search
34
+ """
35
+
36
+
37
+ with gr.Blocks(title="Wardrobe AI") as demo:
38
+
39
+ gr.Markdown(
40
+ """
41
+ # 👕 Wardrobe AI
42
+
43
+ Turn your wardrobe into a searchable knowledge graph.
44
+
45
+ Upload a video of your clothes and let AI build a personal wardrobe catalog.
46
+ """
47
+ )
48
+
49
+ with gr.Tab("Capture"):
50
+ video_input = gr.Video(
51
+ label="Wardrobe Video"
52
+ )
53
+
54
+ capture_btn = gr.Button("Analyze Wardrobe")
55
+
56
+ capture_output = gr.Markdown()
57
+
58
+ capture_btn.click(
59
+ process_video,
60
+ inputs=video_input,
61
+ outputs=capture_output
62
+ )
63
+
64
+ with gr.Tab("Assistant"):
65
+
66
+ question = gr.Textbox(
67
+ label="Ask your wardrobe",
68
+ placeholder="What should I wear for a casual dinner?"
69
+ )
70
+
71
+ ask_btn = gr.Button("Ask")
72
+
73
+ answer = gr.Markdown()
74
+
75
+ ask_btn.click(
76
+ ask_assistant,
77
+ inputs=question,
78
+ outputs=answer
79
+ )
80
+
81
+ with gr.Accordion("Project Vision", open=False):
82
+ gr.Markdown(
83
+ """
84
+ Wardrobe AI aims to:
85
+
86
+ - Detect garments from photos and videos
87
+ - Extract structured clothing attributes
88
+ - Organize clothes by season and usage
89
+ - Recommend outfits
90
+ - Provide garment care instructions
91
+ - Reduce unnecessary clothing purchases
92
+ """
93
+ )
94
+
95
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==6.17.3
2
+ llama-cpp-python>=0.3.28
3
+ huggingface-hub>=1.18.0
4
+ Pillow>=12.0.0
scripts/download_models.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Download GGUF model files for the VLM shootout.
2
+
3
+ Each VLM needs two files:
4
+ 1. The main model weights (quantized GGUF)
5
+ 2. The multimodal projector (mmproj) that maps image embeddings
6
+ into the language model's embedding space.
7
+
8
+ We download Q4_K_M quantizations to balance quality and VRAM usage
9
+ on an 8GB GPU.
10
+ """
11
+
12
+ from huggingface_hub import hf_hub_download
13
+ from pathlib import Path
14
+
15
+ MODELS_DIR = Path(__file__).parent.parent / "models"
16
+
17
+ MODELS = {
18
+ "qwen2.5-vl-3b": {
19
+ "repo": "mradermacher/Qwen2.5-VL-3B-Instruct-GGUF",
20
+ "model_file": "Qwen2.5-VL-3B-Instruct.Q4_K_M.gguf",
21
+ "mmproj_file": "Qwen2.5-VL-3B-Instruct.mmproj-fp16.gguf",
22
+ },
23
+ "smolvlm-2b": {
24
+ "repo": "ggml-org/SmolVLM-Instruct-GGUF",
25
+ "model_file": "SmolVLM-Instruct-Q4_K_M.gguf",
26
+ "mmproj_file": "mmproj-SmolVLM-Instruct-f16.gguf",
27
+ },
28
+ "gemma-3-4b": {
29
+ "repo": "ggml-org/gemma-3-4b-it-GGUF",
30
+ "model_file": "gemma-3-4b-it-Q4_K_M.gguf",
31
+ "mmproj_file": "mmproj-gemma-3-4b-it-f16.gguf",
32
+ },
33
+ }
34
+
35
+
36
+ def download_model(name: str, info: dict) -> dict[str, Path]:
37
+ """Download model + mmproj files, return local paths."""
38
+ print(f"\n{'='*60}")
39
+ print(f"Downloading: {name}")
40
+ print(f" Repo: {info['repo']}")
41
+ print(f"{'='*60}")
42
+
43
+ model_path = Path(hf_hub_download(
44
+ repo_id=info["repo"],
45
+ filename=info["model_file"],
46
+ local_dir=MODELS_DIR / name,
47
+ ))
48
+ print(f" Model: {model_path} ({model_path.stat().st_size / 1e9:.2f} GB)")
49
+
50
+ mmproj_path = Path(hf_hub_download(
51
+ repo_id=info["repo"],
52
+ filename=info["mmproj_file"],
53
+ local_dir=MODELS_DIR / name,
54
+ ))
55
+ print(f" Mmproj: {mmproj_path} ({mmproj_path.stat().st_size / 1e9:.2f} GB)")
56
+
57
+ return {"model": model_path, "mmproj": mmproj_path}
58
+
59
+
60
+ def main():
61
+ MODELS_DIR.mkdir(parents=True, exist_ok=True)
62
+
63
+ import argparse
64
+ parser = argparse.ArgumentParser(description="Download VLM GGUF models")
65
+ parser.add_argument(
66
+ "--model",
67
+ choices=list(MODELS.keys()) + ["all"],
68
+ default="all",
69
+ help="Which model to download (default: all)",
70
+ )
71
+ args = parser.parse_args()
72
+
73
+ targets = MODELS if args.model == "all" else {args.model: MODELS[args.model]}
74
+
75
+ paths = {}
76
+ for name, info in targets.items():
77
+ paths[name] = download_model(name, info)
78
+
79
+ print(f"\n{'='*60}")
80
+ print("Download complete!")
81
+ for name, p in paths.items():
82
+ print(f" {name}:")
83
+ print(f" model: {p['model']}")
84
+ print(f" mmproj: {p['mmproj']}")
85
+ print(f"{'='*60}")
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
scripts/shootout.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VLM Shootout: compare Qwen2.5-VL-3B, SmolVLM, and Gemma 3 4B.
2
+
3
+ Sends the same image + prompt to each model and measures:
4
+ - Response quality (valid JSON, garment count, attribute completeness)
5
+ - Inference speed (tokens/second)
6
+ - VRAM usage (peak)
7
+
8
+ Usage:
9
+ python scripts/shootout.py --image resources/sample.jpg
10
+ python scripts/shootout.py --image resources/sample.jpg --model qwen2.5-vl-3b
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import time
16
+ import subprocess
17
+ import base64
18
+ from pathlib import Path
19
+
20
+ MODELS_DIR = Path(__file__).parent.parent / "models"
21
+
22
+ PROMPT = """Analyze this image of clothing items. For EACH visible garment or accessory, return a JSON array.
23
+
24
+ Each item must have these fields:
25
+ - "type": garment type (e.g. "sweater", "shirt", "jeans", "boots", "hat", "bag")
26
+ - "color": primary color
27
+ - "material": fabric/material if identifiable (e.g. "knit", "denim", "leather"), otherwise "unknown"
28
+ - "pattern": pattern type (e.g. "solid", "checkered", "striped"), otherwise "solid"
29
+ - "season": most suitable season ("spring", "summer", "autumn", "winter", "all")
30
+ - "formality": style level ("casual", "smart-casual", "formal")
31
+
32
+ Return ONLY a valid JSON array. No markdown fences, no explanation."""
33
+
34
+ MODEL_CONFIGS = {
35
+ "qwen2.5-vl-3b": {
36
+ "model_file": "Qwen2.5-VL-3B-Instruct.Q4_K_M.gguf",
37
+ "mmproj_file": "Qwen2.5-VL-3B-Instruct.mmproj-fp16.gguf",
38
+ "chat_handler": "qwen25vl",
39
+ },
40
+ "smolvlm-2b": {
41
+ "model_file": "SmolVLM-Instruct-Q4_K_M.gguf",
42
+ "mmproj_file": "mmproj-SmolVLM-Instruct-f16.gguf",
43
+ "chat_handler": "mtmd",
44
+ },
45
+ "gemma-3-4b": {
46
+ "model_file": "gemma-3-4b-it-Q4_K_M.gguf",
47
+ "mmproj_file": "mmproj-gemma-3-4b-it-f16.gguf",
48
+ "chat_handler": "mtmd",
49
+ },
50
+ }
51
+
52
+
53
+ def get_vram_usage_mb() -> float:
54
+ """Get current VRAM usage in MB via nvidia-smi."""
55
+ try:
56
+ result = subprocess.run(
57
+ ["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
58
+ capture_output=True, text=True, timeout=5,
59
+ )
60
+ return float(result.stdout.strip())
61
+ except Exception:
62
+ return 0.0
63
+
64
+
65
+ def image_to_data_uri(image_path: str) -> str:
66
+ """Convert image file to base64 data URI for the OpenAI vision format."""
67
+ data = Path(image_path).read_bytes()
68
+ b64 = base64.b64encode(data).decode("utf-8")
69
+ suffix = Path(image_path).suffix.lower().lstrip(".")
70
+ mime = {"jpg": "jpeg", "jpeg": "jpeg", "png": "png", "webp": "webp"}.get(suffix, "jpeg")
71
+ return f"data:image/{mime};base64,{b64}"
72
+
73
+
74
+ def load_and_test(model_name: str, config: dict, image_path: str) -> dict:
75
+ """Load a model, run inference, return results."""
76
+ from llama_cpp import Llama
77
+ from llama_cpp.llama_chat_format import Qwen25VLChatHandler, MtmdChatHandler
78
+
79
+ model_dir = MODELS_DIR / model_name
80
+ model_path = str(model_dir / config["model_file"])
81
+ mmproj_path = str(model_dir / config["mmproj_file"])
82
+
83
+ if not Path(model_path).exists():
84
+ return {"error": f"Model file not found: {model_path}"}
85
+ if not Path(mmproj_path).exists():
86
+ return {"error": f"Mmproj file not found: {mmproj_path}"}
87
+
88
+ print(f"\n--- Loading {model_name} ---")
89
+ vram_before = get_vram_usage_mb()
90
+
91
+ handler_cls = Qwen25VLChatHandler if config["chat_handler"] == "qwen25vl" else MtmdChatHandler
92
+ chat_handler = handler_cls(clip_model_path=mmproj_path)
93
+
94
+ llm = Llama(
95
+ model_path=model_path,
96
+ chat_handler=chat_handler,
97
+ n_gpu_layers=-1,
98
+ n_ctx=4096,
99
+ verbose=False,
100
+ )
101
+
102
+ vram_after_load = get_vram_usage_mb()
103
+ print(f" VRAM: {vram_before:.0f} -> {vram_after_load:.0f} MB (+{vram_after_load - vram_before:.0f} MB)")
104
+
105
+ data_uri = image_to_data_uri(image_path)
106
+
107
+ messages = [
108
+ {
109
+ "role": "user",
110
+ "content": [
111
+ {"type": "text", "text": PROMPT},
112
+ {"type": "image_url", "image_url": {"url": data_uri}},
113
+ ],
114
+ }
115
+ ]
116
+
117
+ print(f" Running inference...")
118
+ start = time.perf_counter()
119
+
120
+ response = llm.create_chat_completion(
121
+ messages=messages,
122
+ max_tokens=2048,
123
+ temperature=0.1,
124
+ )
125
+
126
+ elapsed = time.perf_counter() - start
127
+ vram_peak = get_vram_usage_mb()
128
+
129
+ raw_text = response["choices"][0]["message"]["content"]
130
+ usage = response.get("usage", {})
131
+ completion_tokens = usage.get("completion_tokens", 0)
132
+ tokens_per_sec = completion_tokens / elapsed if elapsed > 0 else 0
133
+
134
+ garments = parse_json_response(raw_text)
135
+
136
+ del llm
137
+ del chat_handler
138
+ import gc
139
+ gc.collect()
140
+
141
+ return {
142
+ "model": model_name,
143
+ "raw_response": raw_text,
144
+ "garments": garments,
145
+ "garment_count": len(garments) if isinstance(garments, list) else 0,
146
+ "valid_json": isinstance(garments, list),
147
+ "elapsed_sec": round(elapsed, 2),
148
+ "completion_tokens": completion_tokens,
149
+ "tokens_per_sec": round(tokens_per_sec, 1),
150
+ "vram_model_mb": round(vram_after_load - vram_before),
151
+ "vram_peak_mb": round(vram_peak),
152
+ }
153
+
154
+
155
+ def parse_json_response(text: str) -> list | str:
156
+ """Try to extract a JSON array from the model response."""
157
+ cleaned = text.strip()
158
+
159
+ if cleaned.startswith("```"):
160
+ lines = cleaned.split("\n")
161
+ lines = lines[1:] # remove opening fence
162
+ if lines and lines[-1].strip() == "```":
163
+ lines = lines[:-1]
164
+ cleaned = "\n".join(lines).strip()
165
+
166
+ try:
167
+ parsed = json.loads(cleaned)
168
+ if isinstance(parsed, list):
169
+ return parsed
170
+ if isinstance(parsed, dict):
171
+ return [parsed]
172
+ return cleaned
173
+ except json.JSONDecodeError:
174
+ start = cleaned.find("[")
175
+ end = cleaned.rfind("]")
176
+ if start != -1 and end != -1 and end > start:
177
+ try:
178
+ return json.loads(cleaned[start:end + 1])
179
+ except json.JSONDecodeError:
180
+ pass
181
+ return cleaned
182
+
183
+
184
+ def print_results(results: list[dict]):
185
+ """Print a comparison table of all results."""
186
+ print(f"\n{'='*80}")
187
+ print("SHOOTOUT RESULTS")
188
+ print(f"{'='*80}")
189
+
190
+ for r in results:
191
+ if "error" in r:
192
+ print(f"\n{r['model']}: ERROR - {r['error']}")
193
+ continue
194
+
195
+ print(f"\n--- {r['model']} ---")
196
+ print(f" Valid JSON: {'YES' if r['valid_json'] else 'NO'}")
197
+ print(f" Garments: {r['garment_count']}")
198
+ print(f" Time: {r['elapsed_sec']}s")
199
+ print(f" Tokens/sec: {r['tokens_per_sec']}")
200
+ print(f" VRAM (model): {r['vram_model_mb']} MB")
201
+ print(f" VRAM (peak): {r['vram_peak_mb']} MB")
202
+
203
+ if r["valid_json"] and r["garments"]:
204
+ print(f" First garment: {json.dumps(r['garments'][0], indent=4)}")
205
+
206
+ if not r["valid_json"]:
207
+ print(f" Raw response (first 500 chars):")
208
+ print(f" {r['raw_response'][:500]}")
209
+
210
+ print(f"\n{'='*80}")
211
+
212
+ results_path = Path(__file__).parent.parent / "data" / "shootout_results.json"
213
+ results_path.parent.mkdir(parents=True, exist_ok=True)
214
+ with open(results_path, "w") as f:
215
+ json.dump(results, f, indent=2, ensure_ascii=False)
216
+ print(f"Results saved to: {results_path}")
217
+
218
+
219
+ def main():
220
+ parser = argparse.ArgumentParser(description="VLM Shootout")
221
+ parser.add_argument("--image", required=True, help="Path to test image")
222
+ parser.add_argument(
223
+ "--model",
224
+ choices=list(MODEL_CONFIGS.keys()) + ["all"],
225
+ default="all",
226
+ help="Which model to test (default: all)",
227
+ )
228
+ args = parser.parse_args()
229
+
230
+ if not Path(args.image).exists():
231
+ print(f"Image not found: {args.image}")
232
+ return
233
+
234
+ targets = MODEL_CONFIGS if args.model == "all" else {args.model: MODEL_CONFIGS[args.model]}
235
+
236
+ results = []
237
+ for name, config in targets.items():
238
+ result = load_and_test(name, config, args.image)
239
+ results.append(result)
240
+
241
+ print_results(results)
242
+
243
+
244
+ if __name__ == "__main__":
245
+ main()
scripts/test_gpu.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verify llama-cpp-python is installed with GPU (CUDA) support."""
2
+
3
+ import sys
4
+
5
+
6
+ def check_gpu_support():
7
+ try:
8
+ import llama_cpp
9
+ except ImportError:
10
+ print("FAIL: llama-cpp-python is not installed")
11
+ sys.exit(1)
12
+
13
+ print(f"llama-cpp-python version: {llama_cpp.__version__}")
14
+
15
+ has_gpu = llama_cpp.llama_supports_gpu_offload()
16
+ print(f"GPU offload supported: {has_gpu}")
17
+
18
+ if not has_gpu:
19
+ print("FAIL: llama-cpp-python was built WITHOUT GPU support")
20
+ print("Reinstall with: CMAKE_ARGS=\"-DGGML_CUDA=on\" pip install llama-cpp-python --force-reinstall --no-cache-dir --no-binary llama-cpp-python")
21
+ sys.exit(1)
22
+
23
+ print("OK: GPU support confirmed")
24
+
25
+
26
+ if __name__ == "__main__":
27
+ check_gpu_support()
src/__init__.py ADDED
File without changes
src/assistant.py ADDED
File without changes
src/catalog.py ADDED
File without changes
src/vision.py ADDED
File without changes