Spaces:
Running
Running
Add chat editing + 3D preview + updated models
Browse files
app.py
CHANGED
|
@@ -1,31 +1,12 @@
|
|
| 1 |
"""
|
| 2 |
-
Garment Image
|
| 3 |
-
|
| 4 |
-
Uses modern VLMs (via HF Inference Providers) to analyze garment images and extract
|
| 5 |
-
structured parameters, then generates flat 2D sewing pattern pieces.
|
| 6 |
-
|
| 7 |
-
Models (verified working April 2026):
|
| 8 |
-
- Qwen/Qwen3.5-9B via Together AI (primary)
|
| 9 |
-
- google/gemma-4-31B-it via Together AI (Gemma 4)
|
| 10 |
-
- moonshotai/Kimi-K2.5 via Together AI (fallback)
|
| 11 |
-
|
| 12 |
-
Approach inspired by:
|
| 13 |
-
- ChatGarment (arxiv:2412.17811): VLM → JSON → GarmentCode → 2D patterns
|
| 14 |
-
- NGL-Prompter (arxiv:2602.20700): Training-free VLM → semantic params → patterns
|
| 15 |
"""
|
| 16 |
-
|
| 17 |
-
import
|
| 18 |
-
import os
|
| 19 |
-
import re
|
| 20 |
-
import traceback
|
| 21 |
-
from typing import Dict, Optional, Tuple
|
| 22 |
-
|
| 23 |
import gradio as gr
|
| 24 |
from PIL import Image
|
| 25 |
-
|
| 26 |
-
from
|
| 27 |
-
|
| 28 |
-
# ── VLM Analysis ──
|
| 29 |
|
| 30 |
GARMENT_ANALYSIS_PROMPT = """You are a professional fashion pattern maker. Analyze this garment image and extract precise sewing pattern parameters.
|
| 31 |
|
|
@@ -35,440 +16,311 @@ Return ONLY a JSON object (no markdown, no explanation) with this exact structur
|
|
| 35 |
"garment_type": "<one of: shirt, blouse, top, t-shirt, dress, skirt, pants, trousers, jeans, jacket, coat, blazer, hoodie, vest>",
|
| 36 |
"description": "<brief description of the garment style, fit, and key features>",
|
| 37 |
"measurements": {
|
| 38 |
-
"bust": <number 75-130,
|
| 39 |
-
"
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
"pant_length": <number 30-110, for pants: 30=shorts, 70=cropped, 100=full>,
|
| 46 |
-
"neckline_depth": <number 3-25, how deep the neckline is>,
|
| 47 |
-
"neckline_width": <number 5-15, how wide the neckline opening is>,
|
| 48 |
-
"bicep": <number 25-45, bicep circumference>,
|
| 49 |
-
"wrist": <number 15-25, wrist circumference>,
|
| 50 |
-
"cap_height": <number 8-18, sleeve cap height>,
|
| 51 |
-
"collar_height": <number 3-10, if collar present>,
|
| 52 |
-
"flare": <number 0-15, extra hem width for A-line/flared styles>
|
| 53 |
},
|
| 54 |
"features": {
|
| 55 |
-
"has_collar": <true/false>,
|
| 56 |
-
"
|
| 57 |
-
"
|
| 58 |
-
"
|
| 59 |
-
"pocket_type": "<patch, welt, or none>",
|
| 60 |
-
"has_hood": <true/false>,
|
| 61 |
-
"fit": "<fitted, regular, oversized, or loose>"
|
| 62 |
}
|
| 63 |
}
|
| 64 |
|
| 65 |
-
Be precise
|
| 66 |
-
Only include measurements relevant to the garment type
|
| 67 |
"""
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
("Qwen/Qwen3.5-9B", "together", "Qwen 3.5 9B"),
|
| 79 |
("google/gemma-4-31B-it", "together", "Gemma 4 31B"),
|
| 80 |
("moonshotai/Kimi-K2.5", "together", "Kimi K2.5"),
|
| 81 |
]
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
def _extract_response_text(message: dict) -> str:
|
| 85 |
-
"""
|
| 86 |
-
Extract text from a model response message.
|
| 87 |
-
|
| 88 |
-
Newer models (Qwen3.5, Gemma4) use 'reasoning' field for chain-of-thought
|
| 89 |
-
and 'content' for the final answer. We prefer 'content' when non-empty.
|
| 90 |
-
"""
|
| 91 |
content = message.get('content', '') or ''
|
| 92 |
reasoning = message.get('reasoning', '') or ''
|
| 93 |
-
|
| 94 |
-
# Prefer content (final answer) over reasoning
|
| 95 |
if content.strip():
|
| 96 |
return content.strip()
|
| 97 |
if reasoning.strip():
|
| 98 |
return reasoning.strip()
|
| 99 |
return ''
|
| 100 |
|
| 101 |
-
|
| 102 |
-
def _extract_json_from_text(text: str) -> Optional[str]:
|
| 103 |
-
"""Extract JSON object from text that may contain markdown or other wrappers."""
|
| 104 |
-
# Try markdown code block first
|
| 105 |
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text)
|
| 106 |
if json_match:
|
| 107 |
return json_match.group(1)
|
| 108 |
-
|
| 109 |
-
# Try raw JSON object
|
| 110 |
json_match = re.search(r'\{[\s\S]*\}', text)
|
| 111 |
if json_match:
|
| 112 |
return json_match.group()
|
| 113 |
-
|
| 114 |
return None
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
Analyze garment image using modern VLMs via HF Inference Providers.
|
| 120 |
-
|
| 121 |
-
Tries models in priority order:
|
| 122 |
-
1. Qwen 3.5 9B (fast, good structured output)
|
| 123 |
-
2. Gemma 4 31B (Google, strong vision)
|
| 124 |
-
3. Kimi K2.5 (Moonshot AI, good general VLM)
|
| 125 |
-
"""
|
| 126 |
-
import requests
|
| 127 |
-
import base64
|
| 128 |
from io import BytesIO
|
| 129 |
-
|
| 130 |
hf_token = os.environ.get("HF_TOKEN", "")
|
| 131 |
if not hf_token:
|
| 132 |
return None
|
| 133 |
-
|
| 134 |
-
# Resize image if too large (save bandwidth & speed)
|
| 135 |
-
max_dim = 1024
|
| 136 |
-
if max(image.size) > max_dim:
|
| 137 |
-
ratio = max_dim / max(image.size)
|
| 138 |
-
new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
|
| 139 |
-
image = image.resize(new_size, Image.LANCZOS)
|
| 140 |
-
|
| 141 |
-
buf = BytesIO()
|
| 142 |
-
image_rgb = image.convert('RGB')
|
| 143 |
-
image_rgb.save(buf, format='JPEG', quality=85)
|
| 144 |
-
img_b64 = base64.b64encode(buf.getvalue()).decode('utf-8')
|
| 145 |
-
|
| 146 |
for model_id, provider, display_name in VISION_MODELS:
|
| 147 |
try:
|
| 148 |
url = f"https://router.huggingface.co/{provider}/v1/chat/completions"
|
| 149 |
-
headers = {
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
payload = {
|
| 155 |
-
"model": model_id,
|
| 156 |
-
"messages": [{
|
| 157 |
-
"role": "user",
|
| 158 |
-
"content": [
|
| 159 |
-
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}},
|
| 160 |
-
{"type": "text", "text": GARMENT_ANALYSIS_PROMPT}
|
| 161 |
-
]
|
| 162 |
-
}],
|
| 163 |
-
"max_tokens": 2000,
|
| 164 |
-
"temperature": 0.1,
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
print(f"[VLM] Trying {display_name} ({model_id}) via {provider}...")
|
| 168 |
-
response = requests.post(url, headers=headers, json=payload, timeout=180)
|
| 169 |
-
|
| 170 |
if response.status_code == 200:
|
| 171 |
result = response.json()
|
| 172 |
-
|
| 173 |
-
text = _extract_response_text(message)
|
| 174 |
-
|
| 175 |
if not text:
|
| 176 |
-
print(f"[VLM] {display_name}: empty response")
|
| 177 |
continue
|
| 178 |
-
|
| 179 |
json_str = _extract_json_from_text(text)
|
| 180 |
if not json_str:
|
| 181 |
-
print(f"[VLM] {display_name}: no JSON found in response")
|
| 182 |
continue
|
| 183 |
-
|
| 184 |
analysis = json.loads(json_str)
|
| 185 |
-
analysis['_model_used'] = f"{display_name}
|
| 186 |
-
print(f"[VLM]
|
| 187 |
return analysis
|
| 188 |
else:
|
| 189 |
print(f"[VLM] {display_name}: HTTP {response.status_code}")
|
| 190 |
-
|
| 191 |
-
except json.JSONDecodeError as e:
|
| 192 |
-
print(f"[VLM] {display_name}: JSON parse error: {e}")
|
| 193 |
-
continue
|
| 194 |
-
except requests.exceptions.Timeout:
|
| 195 |
-
print(f"[VLM] {display_name}: timeout (180s)")
|
| 196 |
-
continue
|
| 197 |
except Exception as e:
|
| 198 |
print(f"[VLM] {display_name} failed: {e}")
|
| 199 |
continue
|
| 200 |
-
|
| 201 |
return None
|
| 202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
-
def get_default_analysis(garment_type
|
| 205 |
-
"""Return default analysis when VLM is unavailable."""
|
| 206 |
defaults = {
|
| 207 |
-
"shirt": {
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
"bicep": 32, "wrist": 18, "cap_height": 14,
|
| 215 |
-
"collar_height": 4, "flare": 0
|
| 216 |
-
},
|
| 217 |
-
"features": {
|
| 218 |
-
"has_collar": True, "collar_type": "standard",
|
| 219 |
-
"has_cuffs": True, "has_pockets": True,
|
| 220 |
-
"pocket_type": "patch", "has_hood": False, "fit": "regular"
|
| 221 |
-
}
|
| 222 |
-
},
|
| 223 |
-
"dress": {
|
| 224 |
-
"garment_type": "dress",
|
| 225 |
-
"description": "A-line dress with fitted bodice and flared skirt",
|
| 226 |
-
"measurements": {
|
| 227 |
-
"bust": 90, "waist": 72, "hip": 96,
|
| 228 |
-
"shoulder_width": 40, "bodice_length": 42,
|
| 229 |
-
"sleeve_length": 25, "skirt_length": 55,
|
| 230 |
-
"neckline_depth": 12, "neckline_width": 8,
|
| 231 |
-
"bicep": 28, "wrist": 17, "cap_height": 12,
|
| 232 |
-
"flare": 8
|
| 233 |
-
},
|
| 234 |
-
"features": {
|
| 235 |
-
"has_collar": False, "collar_type": "none",
|
| 236 |
-
"has_cuffs": False, "has_pockets": False,
|
| 237 |
-
"pocket_type": "none", "has_hood": False, "fit": "fitted"
|
| 238 |
-
}
|
| 239 |
-
},
|
| 240 |
-
"pants": {
|
| 241 |
-
"garment_type": "pants",
|
| 242 |
-
"description": "Classic straight-leg trousers",
|
| 243 |
-
"measurements": {
|
| 244 |
-
"waist": 78, "hip": 98, "thigh": 56,
|
| 245 |
-
"knee": 40, "ankle": 26,
|
| 246 |
-
"pant_length": 100, "crotch_depth": 27,
|
| 247 |
-
"waistband_height": 4, "flare": 0
|
| 248 |
-
},
|
| 249 |
-
"features": {
|
| 250 |
-
"has_pockets": True, "pocket_type": "welt",
|
| 251 |
-
"has_collar": False, "has_hood": False, "fit": "regular"
|
| 252 |
-
}
|
| 253 |
-
},
|
| 254 |
-
"skirt": {
|
| 255 |
-
"garment_type": "skirt",
|
| 256 |
-
"description": "A-line knee-length skirt",
|
| 257 |
-
"measurements": {
|
| 258 |
-
"waist": 72, "hip": 96,
|
| 259 |
-
"skirt_length": 55, "waistband_height": 4,
|
| 260 |
-
"flare": 6
|
| 261 |
-
},
|
| 262 |
-
"features": {
|
| 263 |
-
"has_pockets": False, "has_collar": False,
|
| 264 |
-
"has_hood": False, "fit": "regular"
|
| 265 |
-
}
|
| 266 |
-
},
|
| 267 |
-
"jacket": {
|
| 268 |
-
"garment_type": "jacket",
|
| 269 |
-
"description": "Tailored blazer with notched collar",
|
| 270 |
-
"measurements": {
|
| 271 |
-
"bust": 100, "waist": 86, "shoulder_width": 46,
|
| 272 |
-
"jacket_length": 70, "sleeve_length": 62,
|
| 273 |
-
"neckline_depth": 15, "neckline_width": 9,
|
| 274 |
-
"bicep": 34, "wrist": 20, "cap_height": 15,
|
| 275 |
-
"collar_height": 6, "flare": 0
|
| 276 |
-
},
|
| 277 |
-
"features": {
|
| 278 |
-
"has_collar": True, "collar_type": "standard",
|
| 279 |
-
"has_cuffs": False, "has_pockets": True,
|
| 280 |
-
"pocket_type": "welt", "has_hood": False, "fit": "regular"
|
| 281 |
-
}
|
| 282 |
-
},
|
| 283 |
-
"hoodie": {
|
| 284 |
-
"garment_type": "hoodie",
|
| 285 |
-
"description": "Pullover hoodie with kangaroo pocket",
|
| 286 |
-
"measurements": {
|
| 287 |
-
"bust": 108, "waist": 100, "shoulder_width": 50,
|
| 288 |
-
"jacket_length": 68, "sleeve_length": 65,
|
| 289 |
-
"neckline_depth": 10, "neckline_width": 8,
|
| 290 |
-
"bicep": 36, "wrist": 22, "cap_height": 13,
|
| 291 |
-
"head_circumference": 57, "flare": 0
|
| 292 |
-
},
|
| 293 |
-
"features": {
|
| 294 |
-
"has_collar": False, "collar_type": "none",
|
| 295 |
-
"has_cuffs": True, "has_pockets": True,
|
| 296 |
-
"pocket_type": "patch", "has_hood": True, "fit": "oversized"
|
| 297 |
-
}
|
| 298 |
-
},
|
| 299 |
-
"vest": {
|
| 300 |
-
"garment_type": "vest",
|
| 301 |
-
"description": "Classic vest/waistcoat",
|
| 302 |
-
"measurements": {
|
| 303 |
-
"bust": 96, "waist": 80, "shoulder_width": 42,
|
| 304 |
-
"vest_length": 55, "neckline_depth": 18,
|
| 305 |
-
"neckline_width": 8, "flare": 0
|
| 306 |
-
},
|
| 307 |
-
"features": {
|
| 308 |
-
"has_collar": False, "has_cuffs": False,
|
| 309 |
-
"has_pockets": False, "has_hood": False, "fit": "fitted"
|
| 310 |
-
}
|
| 311 |
-
}
|
| 312 |
}
|
| 313 |
return defaults.get(garment_type, defaults["shirt"])
|
| 314 |
|
|
|
|
|
|
|
| 315 |
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
if image is None and garment_type_override == "Auto-detect":
|
| 321 |
-
return None, "Please upload a garment image or select a
|
| 322 |
-
|
| 323 |
analysis = None
|
| 324 |
-
model_info = ""
|
| 325 |
-
|
| 326 |
if image is not None:
|
| 327 |
try:
|
| 328 |
analysis = analyze_with_vlm(image)
|
| 329 |
-
if analysis:
|
| 330 |
-
model_info = f"\n\n*🤖 Analysis by: {analysis.get('_model_used', 'VLM')}*"
|
| 331 |
except Exception as e:
|
| 332 |
-
print(f"VLM
|
| 333 |
-
traceback.print_exc()
|
| 334 |
-
|
| 335 |
if analysis is None:
|
| 336 |
-
if garment_type_override != "Auto-detect"
|
| 337 |
-
gt = garment_type_override.lower()
|
| 338 |
-
elif image is not None:
|
| 339 |
-
gt = "shirt"
|
| 340 |
-
model_info = "\n\n*⚠️ VLM analysis unavailable. Using default parameters. Set HF_TOKEN for AI-powered analysis.*"
|
| 341 |
-
else:
|
| 342 |
-
gt = "shirt"
|
| 343 |
analysis = get_default_analysis(gt)
|
| 344 |
-
|
|
|
|
| 345 |
if garment_type_override != "Auto-detect":
|
| 346 |
analysis['garment_type'] = garment_type_override.lower()
|
| 347 |
-
|
| 348 |
try:
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
summary = f"**Garment:** {description}\n\n{summary}{model_info}"
|
| 352 |
-
display_analysis = {k: v for k, v in analysis.items() if k != '_model_used'}
|
| 353 |
-
return pattern_image, summary, json.dumps(display_analysis, indent=2)
|
| 354 |
except Exception as e:
|
| 355 |
traceback.print_exc()
|
| 356 |
-
return None, f"Error
|
| 357 |
|
| 358 |
-
|
| 359 |
-
def process_text_description(description: str) -> Tuple:
|
| 360 |
-
"""Generate pattern from text description using VLM."""
|
| 361 |
-
import requests
|
| 362 |
-
|
| 363 |
-
hf_token = os.environ.get("HF_TOKEN", "")
|
| 364 |
-
|
| 365 |
if not description.strip():
|
| 366 |
-
return None, "
|
| 367 |
-
|
| 368 |
-
|
| 369 |
if hf_token:
|
| 370 |
-
|
| 371 |
|
| 372 |
Description: {description}
|
| 373 |
|
| 374 |
-
Return ONLY
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
"cap_height": <number>, "collar_height": <number>, "flare": <number>
|
| 386 |
-
}},
|
| 387 |
-
"features": {{
|
| 388 |
-
"has_collar": <true/false>, "collar_type": "<standard/mandarin/peter_pan/none>",
|
| 389 |
-
"has_cuffs": <true/false>, "has_pockets": <true/false>,
|
| 390 |
-
"pocket_type": "<patch/welt/none>", "has_hood": <true/false>,
|
| 391 |
-
"fit": "<fitted/regular/oversized/loose>"
|
| 392 |
-
}}
|
| 393 |
-
}}
|
| 394 |
-
|
| 395 |
-
Only include measurements relevant to the garment type. Use realistic values in cm."""
|
| 396 |
-
|
| 397 |
-
for model_id, provider, display_name in TEXT_MODELS:
|
| 398 |
-
try:
|
| 399 |
-
url = f"https://router.huggingface.co/{provider}/v1/chat/completions"
|
| 400 |
-
headers = {"Authorization": f"Bearer {hf_token}", "Content-Type": "application/json"}
|
| 401 |
-
payload = {
|
| 402 |
-
"model": model_id,
|
| 403 |
-
"messages": [{"role": "user", "content": TEXT_PROMPT}],
|
| 404 |
-
"max_tokens": 2000, "temperature": 0.1
|
| 405 |
-
}
|
| 406 |
-
|
| 407 |
-
print(f"[Text] Trying {display_name} via {provider}...")
|
| 408 |
-
response = requests.post(url, headers=headers, json=payload, timeout=90)
|
| 409 |
-
|
| 410 |
-
if response.status_code == 200:
|
| 411 |
-
message = response.json()['choices'][0]['message']
|
| 412 |
-
text = _extract_response_text(message)
|
| 413 |
-
json_str = _extract_json_from_text(text)
|
| 414 |
-
|
| 415 |
-
if json_str:
|
| 416 |
-
analysis = json.loads(json_str)
|
| 417 |
-
pattern_image, summary = generate_pattern_from_analysis(analysis)
|
| 418 |
-
summary += f"\n\n*🤖 Analysis by: {display_name} ({model_id})*"
|
| 419 |
-
return pattern_image, summary, json.dumps(analysis, indent=2)
|
| 420 |
-
|
| 421 |
-
except Exception as e:
|
| 422 |
-
print(f"[Text] {display_name} failed: {e}")
|
| 423 |
-
continue
|
| 424 |
-
|
| 425 |
-
# Fallback: keyword matching
|
| 426 |
-
desc_lower = description.lower()
|
| 427 |
-
for gt in ['hoodie', 'jacket', 'coat', 'blazer', 'dress', 'skirt', 'pants', 'trousers', 'jeans', 'vest', 'shirt', 'blouse', 'top']:
|
| 428 |
-
if gt in desc_lower:
|
| 429 |
-
analysis = get_default_analysis(gt)
|
| 430 |
analysis['description'] = description
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
pattern_image, summary = generate_pattern_from_analysis(analysis)
|
| 438 |
-
summary += "\n\n*Could not detect garment type — defaulting to shirt*"
|
| 439 |
-
return pattern_image, summary, json.dumps(analysis, indent=2)
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
def process_manual_params(
|
| 443 |
-
garment_type, bust, waist, hip, shoulder, bodice_len,
|
| 444 |
-
sleeve_len, skirt_len, pant_len, neckline_depth,
|
| 445 |
-
has_collar, collar_type, has_cuffs, has_pockets, has_hood, flare, fit
|
| 446 |
-
):
|
| 447 |
-
"""Generate pattern from manual parameter input."""
|
| 448 |
-
analysis = {
|
| 449 |
-
"garment_type": garment_type.lower(),
|
| 450 |
-
"description": f"Custom {garment_type.lower()} with manual measurements",
|
| 451 |
-
"measurements": {
|
| 452 |
-
"bust": bust, "waist": waist, "hip": hip,
|
| 453 |
-
"shoulder_width": shoulder, "bodice_length": bodice_len,
|
| 454 |
-
"sleeve_length": sleeve_len, "skirt_length": skirt_len,
|
| 455 |
-
"pant_length": pant_len, "neckline_depth": neckline_depth,
|
| 456 |
-
"neckline_width": 7, "bicep": 30, "wrist": 18,
|
| 457 |
-
"cap_height": 14, "collar_height": 5, "flare": flare,
|
| 458 |
-
},
|
| 459 |
-
"features": {
|
| 460 |
-
"has_collar": has_collar, "collar_type": collar_type.lower(),
|
| 461 |
-
"has_cuffs": has_cuffs, "has_pockets": has_pockets,
|
| 462 |
-
"pocket_type": "patch", "has_hood": has_hood,
|
| 463 |
-
"fit": fit.lower()
|
| 464 |
-
}
|
| 465 |
-
}
|
| 466 |
-
|
| 467 |
-
pattern_image, summary = generate_pattern_from_analysis(analysis)
|
| 468 |
-
return pattern_image, summary, json.dumps(analysis, indent=2)
|
| 469 |
-
|
| 470 |
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
|
| 473 |
CSS = """
|
| 474 |
.main-header { text-align: center; margin-bottom: 20px; }
|
|
@@ -476,168 +328,155 @@ CSS = """
|
|
| 476 |
.ref-box { padding: 10px; border-radius: 8px; background: #f8f8f8; border: 1px solid #e0e0e0; font-size: 0.85em; }
|
| 477 |
"""
|
| 478 |
|
| 479 |
-
with gr.Blocks(css=CSS, title="Garment
|
| 480 |
gr.HTML("""
|
| 481 |
<div class="main-header">
|
| 482 |
-
<h1>
|
| 483 |
<p style="font-size: 1.1em; color: #555;">
|
| 484 |
-
|
| 485 |
</p>
|
| 486 |
</div>
|
| 487 |
-
""")
|
| 488 |
-
|
| 489 |
-
gr.HTML("""
|
| 490 |
<div class="info-box">
|
| 491 |
-
<b>
|
| 492 |
-
These parameters feed into a parametric pattern generator that produces 2D sewing pattern pieces
|
| 493 |
-
with seam allowances, grain lines, and notches.
|
| 494 |
-
<br><br>
|
| 495 |
-
<b>Powered by:</b> Qwen 3.5 · Gemma 4 · Kimi K2.5 via
|
| 496 |
<a href="https://huggingface.co/docs/inference-providers" target="_blank">HF Inference Providers</a>
|
| 497 |
-
|
| 498 |
-
<b>
|
| 499 |
-
<a href="https://arxiv.org/abs/2412.17811"
|
| 500 |
-
<a href="https://arxiv.org/abs/2602.20700"
|
| 501 |
</div>
|
| 502 |
""")
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
with gr.
|
| 507 |
-
with gr.
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
gr.
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
)
|
| 572 |
-
|
| 573 |
-
gr.
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
gr.
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
)
|
| 618 |
-
|
| 619 |
# References
|
| 620 |
gr.HTML("""
|
| 621 |
<div class="ref-box" style="margin-top: 20px;">
|
| 622 |
-
<h4>
|
| 623 |
<ul>
|
| 624 |
-
<li><b>ChatGarment</b> (Bian et al., 2024)
|
| 625 |
[<a href="https://arxiv.org/abs/2412.17811">Paper</a>]
|
| 626 |
[<a href="https://github.com/biansy000/ChatGarment">Code</a>]
|
| 627 |
[<a href="https://huggingface.co/datasets/sy000/ChatGarmentDataset">Dataset</a>]</li>
|
| 628 |
-
<li><b>NGL-Prompter</b> (2025)
|
| 629 |
[<a href="https://arxiv.org/abs/2602.20700">Paper</a>]</li>
|
| 630 |
-
<li><b>SewFormer</b> (Liu et al., 2023)
|
| 631 |
[<a href="https://arxiv.org/abs/2311.04218">Paper</a>]</li>
|
| 632 |
-
<li><b>GarmentDiffusion</b> (2025)
|
| 633 |
[<a href="https://arxiv.org/abs/2504.21476">Paper</a>]</li>
|
| 634 |
-
<li><b>GarmageNet</b> (Style3D, 2025) — Geometry image diffusion for sewing patterns
|
| 635 |
-
[<a href="https://arxiv.org/abs/2504.01483">Paper</a>]
|
| 636 |
-
[<a href="https://huggingface.co/datasets/Style3D/GarmageSet">Dataset</a>]</li>
|
| 637 |
</ul>
|
| 638 |
</div>
|
| 639 |
""")
|
| 640 |
|
| 641 |
-
|
| 642 |
if __name__ == "__main__":
|
| 643 |
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
| 1 |
"""
|
| 2 |
+
Garment Image -> 2D Sewing Pattern + Chat Editing + 3D Preview
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
"""
|
| 4 |
+
import json, os, re, traceback, copy
|
| 5 |
+
from typing import Dict, Optional, Tuple, List
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import gradio as gr
|
| 7 |
from PIL import Image
|
| 8 |
+
from pattern_generator import generate_pattern_from_analysis
|
| 9 |
+
from garment_3d import create_3d_figure
|
|
|
|
|
|
|
| 10 |
|
| 11 |
GARMENT_ANALYSIS_PROMPT = """You are a professional fashion pattern maker. Analyze this garment image and extract precise sewing pattern parameters.
|
| 12 |
|
|
|
|
| 16 |
"garment_type": "<one of: shirt, blouse, top, t-shirt, dress, skirt, pants, trousers, jeans, jacket, coat, blazer, hoodie, vest>",
|
| 17 |
"description": "<brief description of the garment style, fit, and key features>",
|
| 18 |
"measurements": {
|
| 19 |
+
"bust": <number 75-130>, "waist": <number 55-110>, "hip": <number 80-130>,
|
| 20 |
+
"shoulder_width": <number 35-55>, "bodice_length": <number 35-75>,
|
| 21 |
+
"sleeve_length": <number 15-75>, "skirt_length": <number 30-120>,
|
| 22 |
+
"pant_length": <number 30-110>, "neckline_depth": <number 3-25>,
|
| 23 |
+
"neckline_width": <number 5-15>, "bicep": <number 25-45>,
|
| 24 |
+
"wrist": <number 15-25>, "cap_height": <number 8-18>,
|
| 25 |
+
"collar_height": <number 3-10>, "flare": <number 0-15>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
"features": {
|
| 28 |
+
"has_collar": <true/false>, "collar_type": "<standard/mandarin/peter_pan/none>",
|
| 29 |
+
"has_cuffs": <true/false>, "has_pockets": <true/false>,
|
| 30 |
+
"pocket_type": "<patch/welt/none>", "has_hood": <true/false>,
|
| 31 |
+
"fit": "<fitted/regular/oversized/loose>"
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
}
|
| 34 |
|
| 35 |
+
Be precise. Estimate realistic measurements in cm for an average adult.
|
| 36 |
+
Only include measurements relevant to the garment type.
|
| 37 |
"""
|
| 38 |
|
| 39 |
+
EDIT_PROMPT_TEMPLATE = """You are a fashion pattern editing assistant. The user wants to edit their garment pattern.
|
| 40 |
+
|
| 41 |
+
Current pattern parameters:
|
| 42 |
+
{current_json}
|
| 43 |
+
|
| 44 |
+
User request: {user_message}
|
| 45 |
+
|
| 46 |
+
Apply the edit and return ONLY the complete updated JSON (no markdown, no explanation) with the same structure. Keep all unchanged values the same. Only modify what the user asked to change.
|
| 47 |
|
| 48 |
+
{{
|
| 49 |
+
"garment_type": "<type>",
|
| 50 |
+
"description": "<updated description>",
|
| 51 |
+
"measurements": {{
|
| 52 |
+
"bust": <number>, "waist": <number>, "hip": <number>,
|
| 53 |
+
"shoulder_width": <number>, "bodice_length": <number>,
|
| 54 |
+
"sleeve_length": <number>, "skirt_length": <number>,
|
| 55 |
+
"pant_length": <number>, "neckline_depth": <number>,
|
| 56 |
+
"neckline_width": <number>, "bicep": <number>, "wrist": <number>,
|
| 57 |
+
"cap_height": <number>, "collar_height": <number>, "flare": <number>
|
| 58 |
+
}},
|
| 59 |
+
"features": {{
|
| 60 |
+
"has_collar": <bool>, "collar_type": "<type>",
|
| 61 |
+
"has_cuffs": <bool>, "has_pockets": <bool>,
|
| 62 |
+
"pocket_type": "<type>", "has_hood": <bool>,
|
| 63 |
+
"fit": "<type>"
|
| 64 |
+
}}
|
| 65 |
+
}}"""
|
| 66 |
+
|
| 67 |
+
VISION_MODELS = [
|
| 68 |
("Qwen/Qwen3.5-9B", "together", "Qwen 3.5 9B"),
|
| 69 |
("google/gemma-4-31B-it", "together", "Gemma 4 31B"),
|
| 70 |
("moonshotai/Kimi-K2.5", "together", "Kimi K2.5"),
|
| 71 |
]
|
| 72 |
+
TEXT_MODELS = VISION_MODELS
|
| 73 |
|
| 74 |
+
def _extract_response_text(message):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
content = message.get('content', '') or ''
|
| 76 |
reasoning = message.get('reasoning', '') or ''
|
|
|
|
|
|
|
| 77 |
if content.strip():
|
| 78 |
return content.strip()
|
| 79 |
if reasoning.strip():
|
| 80 |
return reasoning.strip()
|
| 81 |
return ''
|
| 82 |
|
| 83 |
+
def _extract_json_from_text(text):
|
|
|
|
|
|
|
|
|
|
| 84 |
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text)
|
| 85 |
if json_match:
|
| 86 |
return json_match.group(1)
|
|
|
|
|
|
|
| 87 |
json_match = re.search(r'\{[\s\S]*\}', text)
|
| 88 |
if json_match:
|
| 89 |
return json_match.group()
|
|
|
|
| 90 |
return None
|
| 91 |
|
| 92 |
+
def _call_vlm(messages, timeout=180):
|
| 93 |
+
"""Call a VLM model via HF Inference Providers."""
|
| 94 |
+
import requests, base64
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
from io import BytesIO
|
|
|
|
| 96 |
hf_token = os.environ.get("HF_TOKEN", "")
|
| 97 |
if not hf_token:
|
| 98 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
for model_id, provider, display_name in VISION_MODELS:
|
| 100 |
try:
|
| 101 |
url = f"https://router.huggingface.co/{provider}/v1/chat/completions"
|
| 102 |
+
headers = {"Authorization": f"Bearer {hf_token}", "Content-Type": "application/json"}
|
| 103 |
+
payload = {"model": model_id, "messages": messages, "max_tokens": 2000, "temperature": 0.1}
|
| 104 |
+
print(f"[VLM] Trying {display_name} via {provider}...")
|
| 105 |
+
response = requests.post(url, headers=headers, json=payload, timeout=timeout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
if response.status_code == 200:
|
| 107 |
result = response.json()
|
| 108 |
+
text = _extract_response_text(result['choices'][0]['message'])
|
|
|
|
|
|
|
| 109 |
if not text:
|
|
|
|
| 110 |
continue
|
|
|
|
| 111 |
json_str = _extract_json_from_text(text)
|
| 112 |
if not json_str:
|
|
|
|
| 113 |
continue
|
|
|
|
| 114 |
analysis = json.loads(json_str)
|
| 115 |
+
analysis['_model_used'] = f"{display_name}"
|
| 116 |
+
print(f"[VLM] OK: {display_name} detected {analysis.get('garment_type','?')}")
|
| 117 |
return analysis
|
| 118 |
else:
|
| 119 |
print(f"[VLM] {display_name}: HTTP {response.status_code}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
except Exception as e:
|
| 121 |
print(f"[VLM] {display_name} failed: {e}")
|
| 122 |
continue
|
|
|
|
| 123 |
return None
|
| 124 |
|
| 125 |
+
def analyze_with_vlm(image):
|
| 126 |
+
import base64
|
| 127 |
+
from io import BytesIO
|
| 128 |
+
hf_token = os.environ.get("HF_TOKEN", "")
|
| 129 |
+
if not hf_token:
|
| 130 |
+
return None
|
| 131 |
+
max_dim = 1024
|
| 132 |
+
if max(image.size) > max_dim:
|
| 133 |
+
ratio = max_dim / max(image.size)
|
| 134 |
+
image = image.resize((int(image.size[0]*ratio), int(image.size[1]*ratio)), Image.LANCZOS)
|
| 135 |
+
buf = BytesIO()
|
| 136 |
+
image.convert('RGB').save(buf, format='JPEG', quality=85)
|
| 137 |
+
img_b64 = base64.b64encode(buf.getvalue()).decode('utf-8')
|
| 138 |
+
messages = [{"role": "user", "content": [
|
| 139 |
+
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}},
|
| 140 |
+
{"type": "text", "text": GARMENT_ANALYSIS_PROMPT}
|
| 141 |
+
]}]
|
| 142 |
+
return _call_vlm(messages)
|
| 143 |
|
| 144 |
+
def get_default_analysis(garment_type="shirt"):
|
|
|
|
| 145 |
defaults = {
|
| 146 |
+
"shirt": {"garment_type":"shirt","description":"Standard button-up shirt","measurements":{"bust":96,"waist":80,"shoulder_width":44,"bodice_length":72,"sleeve_length":62,"neckline_depth":8,"neckline_width":7,"bicep":32,"wrist":18,"cap_height":14,"collar_height":4,"flare":0},"features":{"has_collar":True,"collar_type":"standard","has_cuffs":True,"has_pockets":True,"pocket_type":"patch","has_hood":False,"fit":"regular"}},
|
| 147 |
+
"dress": {"garment_type":"dress","description":"A-line dress","measurements":{"bust":90,"waist":72,"hip":96,"shoulder_width":40,"bodice_length":42,"sleeve_length":25,"skirt_length":55,"neckline_depth":12,"neckline_width":8,"bicep":28,"wrist":17,"cap_height":12,"flare":8},"features":{"has_collar":False,"collar_type":"none","has_cuffs":False,"has_pockets":False,"pocket_type":"none","has_hood":False,"fit":"fitted"}},
|
| 148 |
+
"pants": {"garment_type":"pants","description":"Straight-leg trousers","measurements":{"waist":78,"hip":98,"thigh":56,"knee":40,"ankle":26,"pant_length":100,"crotch_depth":27,"waistband_height":4,"flare":0},"features":{"has_pockets":True,"pocket_type":"welt","has_collar":False,"has_hood":False,"fit":"regular"}},
|
| 149 |
+
"skirt": {"garment_type":"skirt","description":"A-line knee skirt","measurements":{"waist":72,"hip":96,"skirt_length":55,"waistband_height":4,"flare":6},"features":{"has_pockets":False,"has_collar":False,"has_hood":False,"fit":"regular"}},
|
| 150 |
+
"jacket": {"garment_type":"jacket","description":"Tailored blazer","measurements":{"bust":100,"waist":86,"shoulder_width":46,"jacket_length":70,"sleeve_length":62,"neckline_depth":15,"neckline_width":9,"bicep":34,"wrist":20,"cap_height":15,"collar_height":6,"flare":0},"features":{"has_collar":True,"collar_type":"standard","has_cuffs":False,"has_pockets":True,"pocket_type":"welt","has_hood":False,"fit":"regular"}},
|
| 151 |
+
"hoodie": {"garment_type":"hoodie","description":"Pullover hoodie","measurements":{"bust":108,"waist":100,"shoulder_width":50,"jacket_length":68,"sleeve_length":65,"neckline_depth":10,"neckline_width":8,"bicep":36,"wrist":22,"cap_height":13,"head_circumference":57,"flare":0},"features":{"has_collar":False,"collar_type":"none","has_cuffs":True,"has_pockets":True,"pocket_type":"patch","has_hood":True,"fit":"oversized"}},
|
| 152 |
+
"vest": {"garment_type":"vest","description":"Classic vest","measurements":{"bust":96,"waist":80,"shoulder_width":42,"vest_length":55,"neckline_depth":18,"neckline_width":8,"flare":0},"features":{"has_collar":False,"has_cuffs":False,"has_pockets":False,"has_hood":False,"fit":"fitted"}},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
}
|
| 154 |
return defaults.get(garment_type, defaults["shirt"])
|
| 155 |
|
| 156 |
+
# Global state for current analysis
|
| 157 |
+
_current_analysis = {"data": None}
|
| 158 |
|
| 159 |
+
def _generate_all_outputs(analysis):
|
| 160 |
+
"""Generate 2D pattern, 3D figure, and summary from analysis."""
|
| 161 |
+
pattern_image, summary = generate_pattern_from_analysis(analysis)
|
| 162 |
+
fig_3d = create_3d_figure(analysis)
|
| 163 |
+
display = {k: v for k, v in analysis.items() if k != '_model_used'}
|
| 164 |
+
model_info = f"\n\n*AI: {analysis.get('_model_used', 'Default')}*" if analysis.get('_model_used') else ""
|
| 165 |
+
desc = analysis.get('description', 'No description')
|
| 166 |
+
summary = f"**Garment:** {desc}\n\n{summary}{model_info}"
|
| 167 |
+
return pattern_image, fig_3d, summary, json.dumps(display, indent=2)
|
| 168 |
+
|
| 169 |
+
def process_image(image, garment_type_override="Auto-detect"):
|
| 170 |
if image is None and garment_type_override == "Auto-detect":
|
| 171 |
+
return None, None, "Please upload a garment image or select a type.", "{}", []
|
|
|
|
| 172 |
analysis = None
|
|
|
|
|
|
|
| 173 |
if image is not None:
|
| 174 |
try:
|
| 175 |
analysis = analyze_with_vlm(image)
|
|
|
|
|
|
|
| 176 |
except Exception as e:
|
| 177 |
+
print(f"VLM failed: {e}")
|
|
|
|
|
|
|
| 178 |
if analysis is None:
|
| 179 |
+
gt = garment_type_override.lower() if garment_type_override != "Auto-detect" else "shirt"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
analysis = get_default_analysis(gt)
|
| 181 |
+
if image is not None and garment_type_override == "Auto-detect":
|
| 182 |
+
analysis['_model_used'] = 'Default (set HF_TOKEN for AI)'
|
| 183 |
if garment_type_override != "Auto-detect":
|
| 184 |
analysis['garment_type'] = garment_type_override.lower()
|
| 185 |
+
_current_analysis["data"] = copy.deepcopy(analysis)
|
| 186 |
try:
|
| 187 |
+
p2d, p3d, summary, j = _generate_all_outputs(analysis)
|
| 188 |
+
return p2d, p3d, summary, j, []
|
|
|
|
|
|
|
|
|
|
| 189 |
except Exception as e:
|
| 190 |
traceback.print_exc()
|
| 191 |
+
return None, None, f"Error: {e}", "{}", []
|
| 192 |
|
| 193 |
+
def process_text(description):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
if not description.strip():
|
| 195 |
+
return None, None, "Enter a description.", "{}", []
|
| 196 |
+
analysis = None
|
| 197 |
+
hf_token = os.environ.get("HF_TOKEN", "")
|
| 198 |
if hf_token:
|
| 199 |
+
messages = [{"role": "user", "content": f"""Based on this garment description, extract sewing pattern parameters.
|
| 200 |
|
| 201 |
Description: {description}
|
| 202 |
|
| 203 |
+
Return ONLY JSON with: garment_type, description, measurements (bust, waist, hip, shoulder_width, bodice_length, sleeve_length, skirt_length, pant_length, neckline_depth, neckline_width, bicep, wrist, cap_height, collar_height, flare), features (has_collar, collar_type, has_cuffs, has_pockets, pocket_type, has_hood, fit)."""}]
|
| 204 |
+
analysis = _call_vlm(messages, timeout=90)
|
| 205 |
+
if analysis is None:
|
| 206 |
+
desc_lower = description.lower()
|
| 207 |
+
for gt in ['hoodie','jacket','coat','blazer','dress','skirt','pants','trousers','jeans','vest','shirt','blouse','top']:
|
| 208 |
+
if gt in desc_lower:
|
| 209 |
+
analysis = get_default_analysis(gt)
|
| 210 |
+
analysis['description'] = description
|
| 211 |
+
break
|
| 212 |
+
if analysis is None:
|
| 213 |
+
analysis = get_default_analysis("shirt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
analysis['description'] = description
|
| 215 |
+
_current_analysis["data"] = copy.deepcopy(analysis)
|
| 216 |
+
try:
|
| 217 |
+
p2d, p3d, summary, j = _generate_all_outputs(analysis)
|
| 218 |
+
return p2d, p3d, summary, j, []
|
| 219 |
+
except Exception as e:
|
| 220 |
+
return None, None, f"Error: {e}", "{}", []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
+
def process_manual(gt,bust,waist,hip,shoulder,bodice,sleeve,skirt,pant,neck,flare_c,collar,ctype,cuffs,pockets,hood,fit):
|
| 223 |
+
analysis = {"garment_type":gt.lower(),"description":f"Custom {gt.lower()}","measurements":{"bust":bust,"waist":waist,"hip":hip,"shoulder_width":shoulder,"bodice_length":bodice,"sleeve_length":sleeve,"skirt_length":skirt,"pant_length":pant,"neckline_depth":neck,"neckline_width":7,"bicep":30,"wrist":18,"cap_height":14,"collar_height":5,"flare":flare_c},"features":{"has_collar":collar,"collar_type":ctype.lower(),"has_cuffs":cuffs,"has_pockets":pockets,"pocket_type":"patch","has_hood":hood,"fit":fit.lower()}}
|
| 224 |
+
_current_analysis["data"] = copy.deepcopy(analysis)
|
| 225 |
+
try:
|
| 226 |
+
p2d, p3d, summary, j = _generate_all_outputs(analysis)
|
| 227 |
+
return p2d, p3d, summary, j, []
|
| 228 |
+
except Exception as e:
|
| 229 |
+
return None, None, f"Error: {e}", "{}", []
|
| 230 |
+
|
| 231 |
+
def chat_edit(message, history):
|
| 232 |
+
"""Chat-based pattern editing."""
|
| 233 |
+
if not message.strip():
|
| 234 |
+
return history, None, None, "Please enter an edit request.", "{}"
|
| 235 |
+
current = _current_analysis.get("data")
|
| 236 |
+
if current is None:
|
| 237 |
+
current = get_default_analysis("shirt")
|
| 238 |
+
_current_analysis["data"] = current
|
| 239 |
+
# Build edit prompt
|
| 240 |
+
current_clean = {k: v for k, v in current.items() if k != '_model_used'}
|
| 241 |
+
edit_prompt = EDIT_PROMPT_TEMPLATE.format(
|
| 242 |
+
current_json=json.dumps(current_clean, indent=2),
|
| 243 |
+
user_message=message
|
| 244 |
+
)
|
| 245 |
+
# Try VLM edit
|
| 246 |
+
updated = None
|
| 247 |
+
hf_token = os.environ.get("HF_TOKEN", "")
|
| 248 |
+
if hf_token:
|
| 249 |
+
messages = [{"role": "user", "content": edit_prompt}]
|
| 250 |
+
try:
|
| 251 |
+
updated = _call_vlm(messages, timeout=90)
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f"Edit VLM failed: {e}")
|
| 254 |
+
if updated is None:
|
| 255 |
+
# Simple rule-based fallback for common edits
|
| 256 |
+
updated = copy.deepcopy(current)
|
| 257 |
+
msg_lower = message.lower()
|
| 258 |
+
if "long sleeve" in msg_lower or "longer sleeve" in msg_lower:
|
| 259 |
+
updated['measurements']['sleeve_length'] = 65
|
| 260 |
+
elif "short sleeve" in msg_lower or "shorter sleeve" in msg_lower:
|
| 261 |
+
updated['measurements']['sleeve_length'] = 25
|
| 262 |
+
if "no collar" in msg_lower or "remove collar" in msg_lower:
|
| 263 |
+
updated['features']['has_collar'] = False
|
| 264 |
+
updated['features']['collar_type'] = 'none'
|
| 265 |
+
if "add collar" in msg_lower:
|
| 266 |
+
updated['features']['has_collar'] = True
|
| 267 |
+
updated['features']['collar_type'] = 'standard'
|
| 268 |
+
if "add hood" in msg_lower or "hoodie" in msg_lower:
|
| 269 |
+
updated['features']['has_hood'] = True
|
| 270 |
+
if "no hood" in msg_lower or "remove hood" in msg_lower:
|
| 271 |
+
updated['features']['has_hood'] = False
|
| 272 |
+
if "add pocket" in msg_lower:
|
| 273 |
+
updated['features']['has_pockets'] = True
|
| 274 |
+
updated['features']['pocket_type'] = 'patch'
|
| 275 |
+
if "no pocket" in msg_lower or "remove pocket" in msg_lower:
|
| 276 |
+
updated['features']['has_pockets'] = False
|
| 277 |
+
if "oversized" in msg_lower or "loose" in msg_lower:
|
| 278 |
+
updated['features']['fit'] = 'oversized'
|
| 279 |
+
updated['measurements']['bust'] = updated['measurements'].get('bust', 96) + 10
|
| 280 |
+
if "fitted" in msg_lower or "slim" in msg_lower:
|
| 281 |
+
updated['features']['fit'] = 'fitted'
|
| 282 |
+
if "flare" in msg_lower or "a-line" in msg_lower:
|
| 283 |
+
updated['measurements']['flare'] = max(updated['measurements'].get('flare', 0), 8)
|
| 284 |
+
if "straight" in msg_lower:
|
| 285 |
+
updated['measurements']['flare'] = 0
|
| 286 |
+
if "mini" in msg_lower:
|
| 287 |
+
updated['measurements']['skirt_length'] = 30
|
| 288 |
+
if "midi" in msg_lower:
|
| 289 |
+
updated['measurements']['skirt_length'] = 80
|
| 290 |
+
if "maxi" in msg_lower:
|
| 291 |
+
updated['measurements']['skirt_length'] = 110
|
| 292 |
+
updated['_model_used'] = 'Rule-based edit'
|
| 293 |
+
# Preserve garment type unless explicitly changed
|
| 294 |
+
if 'garment_type' not in updated:
|
| 295 |
+
updated['garment_type'] = current.get('garment_type', 'shirt')
|
| 296 |
+
_current_analysis["data"] = copy.deepcopy(updated)
|
| 297 |
+
# Generate updated outputs
|
| 298 |
+
try:
|
| 299 |
+
p2d, p3d, summary, j = _generate_all_outputs(updated)
|
| 300 |
+
except Exception as e:
|
| 301 |
+
p2d, p3d, summary, j = None, None, f"Error: {e}", "{}"
|
| 302 |
+
# Build chat response
|
| 303 |
+
ai_msg = f"Applied edit: {message}\n\n"
|
| 304 |
+
changes = []
|
| 305 |
+
old_m = current.get('measurements', {})
|
| 306 |
+
new_m = updated.get('measurements', {})
|
| 307 |
+
old_f = current.get('features', {})
|
| 308 |
+
new_f = updated.get('features', {})
|
| 309 |
+
for k in set(list(old_m.keys()) + list(new_m.keys())):
|
| 310 |
+
ov, nv = old_m.get(k), new_m.get(k)
|
| 311 |
+
if ov != nv and ov is not None and nv is not None:
|
| 312 |
+
changes.append(f" {k}: {ov} -> {nv}")
|
| 313 |
+
for k in set(list(old_f.keys()) + list(new_f.keys())):
|
| 314 |
+
ov, nv = old_f.get(k), new_f.get(k)
|
| 315 |
+
if ov != nv:
|
| 316 |
+
changes.append(f" {k}: {ov} -> {nv}")
|
| 317 |
+
if changes:
|
| 318 |
+
ai_msg += "Changes:\n" + "\n".join(changes)
|
| 319 |
+
else:
|
| 320 |
+
ai_msg += "No changes detected."
|
| 321 |
+
history = history or []
|
| 322 |
+
history.append((message, ai_msg))
|
| 323 |
+
return history, p2d, p3d, summary, j
|
| 324 |
|
| 325 |
CSS = """
|
| 326 |
.main-header { text-align: center; margin-bottom: 20px; }
|
|
|
|
| 328 |
.ref-box { padding: 10px; border-radius: 8px; background: #f8f8f8; border: 1px solid #e0e0e0; font-size: 0.85em; }
|
| 329 |
"""
|
| 330 |
|
| 331 |
+
with gr.Blocks(css=CSS, title="Garment Pattern Studio", theme=gr.themes.Soft()) as demo:
|
| 332 |
gr.HTML("""
|
| 333 |
<div class="main-header">
|
| 334 |
+
<h1>Garment Pattern Studio</h1>
|
| 335 |
<p style="font-size: 1.1em; color: #555;">
|
| 336 |
+
Analyze garments, edit with chat, preview in 3D
|
| 337 |
</p>
|
| 338 |
</div>
|
|
|
|
|
|
|
|
|
|
| 339 |
<div class="info-box">
|
| 340 |
+
<b>Powered by:</b> Qwen 3.5 · Gemma 4 · Kimi K2.5 via
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
<a href="https://huggingface.co/docs/inference-providers" target="_blank">HF Inference Providers</a>
|
| 342 |
+
|
|
| 343 |
+
<b>Research:</b>
|
| 344 |
+
<a href="https://arxiv.org/abs/2412.17811">ChatGarment</a> &
|
| 345 |
+
<a href="https://arxiv.org/abs/2602.20700">NGL-Prompter</a>
|
| 346 |
</div>
|
| 347 |
""")
|
| 348 |
+
|
| 349 |
+
# === Tab 1: From Image ===
|
| 350 |
+
with gr.Tab("From Image"):
|
| 351 |
+
with gr.Row():
|
| 352 |
+
with gr.Column(scale=1):
|
| 353 |
+
input_image = gr.Image(type="pil", label="Upload Garment Image", height=350)
|
| 354 |
+
garment_override = gr.Dropdown(choices=["Auto-detect","Shirt","Dress","Skirt","Pants","Jacket","Hoodie","Vest"], value="Auto-detect", label="Garment Type Override")
|
| 355 |
+
analyze_btn = gr.Button("Analyze & Generate", variant="primary", size="lg")
|
| 356 |
+
with gr.Column(scale=2):
|
| 357 |
+
with gr.Row():
|
| 358 |
+
with gr.Column():
|
| 359 |
+
out_pattern_2d = gr.Image(label="2D Sewing Pattern", height=400)
|
| 360 |
+
with gr.Column():
|
| 361 |
+
out_3d = gr.Plot(label="3D Garment Preview")
|
| 362 |
+
out_summary = gr.Markdown(label="Pattern Summary")
|
| 363 |
+
with gr.Accordion("Raw JSON", open=False):
|
| 364 |
+
out_json = gr.Code(language="json", label="Analysis")
|
| 365 |
+
analyze_btn.click(process_image, inputs=[input_image, garment_override], outputs=[out_pattern_2d, out_3d, out_summary, out_json])
|
| 366 |
+
|
| 367 |
+
# === Tab 2: From Text ===
|
| 368 |
+
with gr.Tab("From Text"):
|
| 369 |
+
with gr.Row():
|
| 370 |
+
with gr.Column(scale=1):
|
| 371 |
+
text_input = gr.Textbox(label="Describe the garment", placeholder="e.g., A fitted A-line dress with cap sleeves", lines=3)
|
| 372 |
+
text_btn = gr.Button("Generate Pattern", variant="primary", size="lg")
|
| 373 |
+
gr.Examples(examples=[
|
| 374 |
+
["A classic dress shirt with long sleeves and button-down collar"],
|
| 375 |
+
["A flared midi skirt with high waist"],
|
| 376 |
+
["An oversized hoodie with kangaroo pocket"],
|
| 377 |
+
["A fitted blazer with notched lapel collar"],
|
| 378 |
+
["Slim-fit straight-leg jeans with pockets"],
|
| 379 |
+
["A knee-length A-line dress with cap sleeves"],
|
| 380 |
+
], inputs=text_input)
|
| 381 |
+
with gr.Column(scale=2):
|
| 382 |
+
with gr.Row():
|
| 383 |
+
with gr.Column():
|
| 384 |
+
txt_pattern_2d = gr.Image(label="2D Pattern", height=400)
|
| 385 |
+
with gr.Column():
|
| 386 |
+
txt_3d = gr.Plot(label="3D Preview")
|
| 387 |
+
txt_summary = gr.Markdown()
|
| 388 |
+
with gr.Accordion("Raw JSON", open=False):
|
| 389 |
+
txt_json = gr.Code(language="json")
|
| 390 |
+
text_btn.click(process_text, inputs=[text_input], outputs=[txt_pattern_2d, txt_3d, txt_summary, txt_json])
|
| 391 |
+
|
| 392 |
+
# === Tab 3: Manual Parameters ===
|
| 393 |
+
with gr.Tab("Manual Parameters"):
|
| 394 |
+
with gr.Row():
|
| 395 |
+
with gr.Column(scale=1):
|
| 396 |
+
m_type = gr.Dropdown(choices=["Shirt","Dress","Skirt","Pants","Jacket","Hoodie","Vest"], value="Shirt", label="Garment Type")
|
| 397 |
+
gr.Markdown("### Measurements (cm)")
|
| 398 |
+
with gr.Row():
|
| 399 |
+
m_bust = gr.Slider(70,130,value=92,step=1,label="Bust")
|
| 400 |
+
m_waist = gr.Slider(55,110,value=74,step=1,label="Waist")
|
| 401 |
+
with gr.Row():
|
| 402 |
+
m_hip = gr.Slider(75,130,value=96,step=1,label="Hip")
|
| 403 |
+
m_shoulder = gr.Slider(35,55,value=42,step=1,label="Shoulder")
|
| 404 |
+
with gr.Row():
|
| 405 |
+
m_bodice = gr.Slider(30,80,value=42,step=1,label="Bodice Length")
|
| 406 |
+
m_sleeve = gr.Slider(10,75,value=60,step=1,label="Sleeve Length")
|
| 407 |
+
with gr.Row():
|
| 408 |
+
m_skirt = gr.Slider(25,120,value=55,step=1,label="Skirt Length")
|
| 409 |
+
m_pant = gr.Slider(25,115,value=100,step=1,label="Pant Length")
|
| 410 |
+
with gr.Row():
|
| 411 |
+
m_neck = gr.Slider(3,25,value=8,step=1,label="Neckline Depth")
|
| 412 |
+
m_flare = gr.Slider(0,20,value=0,step=1,label="Hem Flare")
|
| 413 |
+
gr.Markdown("### Features")
|
| 414 |
+
with gr.Row():
|
| 415 |
+
m_collar = gr.Checkbox(value=True,label="Collar")
|
| 416 |
+
m_ctype = gr.Dropdown(["Standard","Mandarin","Peter_pan"],value="Standard",label="Collar Type")
|
| 417 |
+
with gr.Row():
|
| 418 |
+
m_cuffs = gr.Checkbox(value=True,label="Cuffs")
|
| 419 |
+
m_pockets = gr.Checkbox(value=False,label="Pockets")
|
| 420 |
+
with gr.Row():
|
| 421 |
+
m_hood = gr.Checkbox(value=False,label="Hood")
|
| 422 |
+
m_fit = gr.Dropdown(["Fitted","Regular","Oversized","Loose"],value="Regular",label="Fit")
|
| 423 |
+
manual_btn = gr.Button("Generate Pattern", variant="primary", size="lg")
|
| 424 |
+
with gr.Column(scale=2):
|
| 425 |
+
with gr.Row():
|
| 426 |
+
with gr.Column():
|
| 427 |
+
man_pattern_2d = gr.Image(label="2D Pattern", height=400)
|
| 428 |
+
with gr.Column():
|
| 429 |
+
man_3d = gr.Plot(label="3D Preview")
|
| 430 |
+
man_summary = gr.Markdown()
|
| 431 |
+
with gr.Accordion("Raw JSON", open=False):
|
| 432 |
+
man_json = gr.Code(language="json")
|
| 433 |
+
manual_btn.click(process_manual, inputs=[m_type,m_bust,m_waist,m_hip,m_shoulder,m_bodice,m_sleeve,m_skirt,m_pant,m_neck,m_flare,m_collar,m_ctype,m_cuffs,m_pockets,m_hood,m_fit], outputs=[man_pattern_2d, man_3d, man_summary, man_json])
|
| 434 |
+
|
| 435 |
+
# === Tab 4: Chat & Edit ===
|
| 436 |
+
with gr.Tab("Chat & Edit"):
|
| 437 |
+
gr.Markdown("### Edit your pattern with natural language\nFirst generate a pattern (from Image/Text/Manual tabs), then edit it here. Each edit updates both 2D and 3D views.")
|
| 438 |
+
with gr.Row():
|
| 439 |
+
with gr.Column(scale=1):
|
| 440 |
+
chatbot = gr.Chatbot(label="Pattern Editor", height=400)
|
| 441 |
+
chat_input = gr.Textbox(label="Edit instruction", placeholder="e.g., Make the sleeves longer, Add a hood, Change to A-line skirt", lines=2)
|
| 442 |
+
with gr.Row():
|
| 443 |
+
chat_send = gr.Button("Apply Edit", variant="primary")
|
| 444 |
+
chat_clear = gr.Button("Clear Chat")
|
| 445 |
+
with gr.Column(scale=2):
|
| 446 |
+
with gr.Row():
|
| 447 |
+
with gr.Column():
|
| 448 |
+
chat_pattern_2d = gr.Image(label="Updated 2D Pattern", height=400)
|
| 449 |
+
with gr.Column():
|
| 450 |
+
chat_3d = gr.Plot(label="Updated 3D Preview")
|
| 451 |
+
chat_summary = gr.Markdown()
|
| 452 |
+
with gr.Accordion("Updated JSON", open=False):
|
| 453 |
+
chat_json = gr.Code(language="json")
|
| 454 |
+
|
| 455 |
+
def clear_chat():
|
| 456 |
+
return [], None, None, "", "{}"
|
| 457 |
+
|
| 458 |
+
chat_send.click(chat_edit, inputs=[chat_input, chatbot], outputs=[chatbot, chat_pattern_2d, chat_3d, chat_summary, chat_json])
|
| 459 |
+
chat_input.submit(chat_edit, inputs=[chat_input, chatbot], outputs=[chatbot, chat_pattern_2d, chat_3d, chat_summary, chat_json])
|
| 460 |
+
chat_clear.click(clear_chat, outputs=[chatbot, chat_pattern_2d, chat_3d, chat_summary, chat_json])
|
| 461 |
+
|
|
|
|
|
|
|
| 462 |
# References
|
| 463 |
gr.HTML("""
|
| 464 |
<div class="ref-box" style="margin-top: 20px;">
|
| 465 |
+
<h4>Research References</h4>
|
| 466 |
<ul>
|
| 467 |
+
<li><b>ChatGarment</b> (Bian et al., 2024) -- VLM + multi-turn dialogue for garment editing
|
| 468 |
[<a href="https://arxiv.org/abs/2412.17811">Paper</a>]
|
| 469 |
[<a href="https://github.com/biansy000/ChatGarment">Code</a>]
|
| 470 |
[<a href="https://huggingface.co/datasets/sy000/ChatGarmentDataset">Dataset</a>]</li>
|
| 471 |
+
<li><b>NGL-Prompter</b> (2025) -- Training-free VLM pattern estimation
|
| 472 |
[<a href="https://arxiv.org/abs/2602.20700">Paper</a>]</li>
|
| 473 |
+
<li><b>SewFormer</b> (Liu et al., 2023) -- Two-level Transformer pattern reconstruction
|
| 474 |
[<a href="https://arxiv.org/abs/2311.04218">Paper</a>]</li>
|
| 475 |
+
<li><b>GarmentDiffusion</b> (2025) -- DiT-based multimodal pattern generation
|
| 476 |
[<a href="https://arxiv.org/abs/2504.21476">Paper</a>]</li>
|
|
|
|
|
|
|
|
|
|
| 477 |
</ul>
|
| 478 |
</div>
|
| 479 |
""")
|
| 480 |
|
|
|
|
| 481 |
if __name__ == "__main__":
|
| 482 |
demo.launch(server_name="0.0.0.0", server_port=7860)
|