vikashmakeit commited on
Commit
35d2c31
·
verified ·
1 Parent(s): 7690037

Add chat editing + 3D preview + updated models

Browse files
Files changed (1) hide show
  1. app.py +358 -519
app.py CHANGED
@@ -1,31 +1,12 @@
1
  """
2
- Garment Image 2D Sewing Pattern Demo
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 json
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 pattern_generator import generate_pattern_from_analysis, get_pattern_pieces, render_pattern_pieces
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, estimated bust circumference in cm>,
39
- "waist": <number 55-110, estimated waist circumference in cm>,
40
- "hip": <number 80-130, estimated hip circumference in cm>,
41
- "shoulder_width": <number 35-55, shoulder width in cm>,
42
- "bodice_length": <number 35-75, from shoulder to waist/hem for tops>,
43
- "sleeve_length": <number 15-75, sleeve length in cm, 15=cap sleeve, 25=short, 45=3/4, 60-70=long>,
44
- "skirt_length": <number 30-120, for skirts/dresses: 30=mini, 55=knee, 80=midi, 110=maxi>,
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
- "collar_type": "<standard, mandarin, peter_pan, or none>",
57
- "has_cuffs": <true/false>,
58
- "has_pockets": <true/false>,
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 with the garment type. Estimate realistic measurements for an average adult body.
66
- Only include measurements relevant to the garment type (e.g., skip pant_length for a shirt).
67
  """
68
 
69
- # Model configurations: (model_id, provider, display_name)
70
- # Verified working via HF Inference Providers (April 2026)
71
- VISION_MODELS = [
72
- ("Qwen/Qwen3.5-9B", "together", "Qwen 3.5 9B"),
73
- ("google/gemma-4-31B-it", "together", "Gemma 4 31B"),
74
- ("moonshotai/Kimi-K2.5", "together", "Kimi K2.5"),
75
- ]
 
76
 
77
- TEXT_MODELS = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- def analyze_with_vlm(image: Image.Image) -> Dict:
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
- "Authorization": f"Bearer {hf_token}",
151
- "Content-Type": "application/json"
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
- message = result['choices'][0]['message']
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} ({model_id})"
186
- print(f"[VLM] {display_name}: detected '{analysis.get('garment_type', '?')}'")
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: str = "shirt") -> Dict:
205
- """Return default analysis when VLM is unavailable."""
206
  defaults = {
207
- "shirt": {
208
- "garment_type": "shirt",
209
- "description": "Standard button-up shirt with collar and long sleeves",
210
- "measurements": {
211
- "bust": 96, "waist": 80, "shoulder_width": 44,
212
- "bodice_length": 72, "sleeve_length": 62,
213
- "neckline_depth": 8, "neckline_width": 7,
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
- # ── Main App Functions ──
317
-
318
- def process_image(image: Optional[Image.Image], garment_type_override: str = "Auto-detect") -> Tuple:
319
- """Main processing: analyze image → generate pattern."""
 
 
 
 
 
 
 
320
  if image is None and garment_type_override == "Auto-detect":
321
- return None, "Please upload a garment image or select a garment type.", "{}"
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 analysis failed: {e}")
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
- pattern_image, summary = generate_pattern_from_analysis(analysis)
350
- description = analysis.get('description', 'No description')
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 generating pattern: {str(e)}", "{}"
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, "Please enter a garment description.", "{}"
367
-
368
- # Try VLM-based analysis
369
  if hf_token:
370
- TEXT_PROMPT = f"""You are a professional fashion pattern maker. Based on this garment description, extract precise sewing pattern parameters.
371
 
372
  Description: {description}
373
 
374
- Return ONLY a JSON object (no markdown, no explanation) with this exact structure:
375
-
376
- {{
377
- "garment_type": "<one of: shirt, blouse, top, t-shirt, dress, skirt, pants, trousers, jeans, jacket, coat, blazer, hoodie, vest>",
378
- "description": "{description}",
379
- "measurements": {{
380
- "bust": <number>, "waist": <number>, "hip": <number>,
381
- "shoulder_width": <number>, "bodice_length": <number>,
382
- "sleeve_length": <number>, "skirt_length": <number>,
383
- "pant_length": <number>, "neckline_depth": <number>,
384
- "neckline_width": <number>, "bicep": <number>, "wrist": <number>,
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
- pattern_image, summary = generate_pattern_from_analysis(analysis)
432
- summary += "\n\n*Using default parameters (set HF_TOKEN for AI-powered analysis)*"
433
- return pattern_image, summary, json.dumps(analysis, indent=2)
434
-
435
- analysis = get_default_analysis("shirt")
436
- analysis['description'] = description
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
- # ── Gradio UI ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 → 2D Sewing Pattern", theme=gr.themes.Soft()) as demo:
480
  gr.HTML("""
481
  <div class="main-header">
482
- <h1>🧵 Garment Image → 2D Sewing Pattern</h1>
483
  <p style="font-size: 1.1em; color: #555;">
484
- Upload a garment image or describe one — get flat 2D sewing pattern pieces
485
  </p>
486
  </div>
487
- """)
488
-
489
- gr.HTML("""
490
  <div class="info-box">
491
- <b>How it works:</b> A Vision-Language Model analyzes the garment to identify type, style, and proportions.
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
- <br>
498
- <b>Based on research:</b>
499
- <a href="https://arxiv.org/abs/2412.17811" target="_blank">ChatGarment</a> &
500
- <a href="https://arxiv.org/abs/2602.20700" target="_blank">NGL-Prompter</a>
501
  </div>
502
  """)
503
-
504
- with gr.Tabs():
505
- # Tab 1: Image Input
506
- with gr.Tab("📸 From Image", id="image_tab"):
507
- with gr.Row():
508
- with gr.Column(scale=1):
509
- input_image = gr.Image(type="pil", label="Upload Garment Image", height=400)
510
- garment_override = gr.Dropdown(
511
- choices=["Auto-detect", "Shirt", "Dress", "Skirt", "Pants", "Jacket", "Hoodie", "Vest"],
512
- value="Auto-detect",
513
- label="Garment Type (override auto-detection)"
514
- )
515
- analyze_btn = gr.Button("🔍 Analyze & Generate Pattern", variant="primary", size="lg")
516
-
517
- with gr.Column(scale=2):
518
- output_pattern = gr.Image(label="2D Sewing Pattern", height=500)
519
- output_summary = gr.Markdown(label="Pattern Summary")
520
- with gr.Accordion("Raw Analysis JSON", open=False):
521
- output_json = gr.Code(language="json", label="Analysis Parameters")
522
-
523
- analyze_btn.click(
524
- process_image,
525
- inputs=[input_image, garment_override],
526
- outputs=[output_pattern, output_summary, output_json]
527
- )
528
-
529
- # Tab 2: Text Description
530
- with gr.Tab("✍️ From Text", id="text_tab"):
531
- with gr.Row():
532
- with gr.Column(scale=1):
533
- text_input = gr.Textbox(
534
- label="Describe the garment",
535
- placeholder="e.g., 'A fitted A-line dress with short cap sleeves, V-neckline, and knee length'",
536
- lines=4
537
- )
538
- text_btn = gr.Button("🧵 Generate Pattern", variant="primary", size="lg")
539
-
540
- gr.Examples(
541
- examples=[
542
- ["A classic men's dress shirt with long sleeves, button-down collar, and chest pocket"],
543
- ["A flared midi skirt with high waist, A-line silhouette"],
544
- ["Slim-fit straight-leg jeans with front and back pockets"],
545
- ["An oversized hoodie with kangaroo pocket and drawstring hood"],
546
- ["A fitted blazer with notched lapel collar and two welt pockets"],
547
- ["A knee-length A-line dress with cap sleeves and round neckline"],
548
- ],
549
- inputs=text_input
550
- )
551
-
552
- with gr.Column(scale=2):
553
- text_output_pattern = gr.Image(label="2D Sewing Pattern", height=500)
554
- text_output_summary = gr.Markdown(label="Pattern Summary")
555
- with gr.Accordion("Raw Analysis JSON", open=False):
556
- text_output_json = gr.Code(language="json", label="Analysis Parameters")
557
-
558
- text_btn.click(
559
- process_text_description,
560
- inputs=[text_input],
561
- outputs=[text_output_pattern, text_output_summary, text_output_json]
562
- )
563
-
564
- # Tab 3: Manual Parameters
565
- with gr.Tab("📐 Manual Parameters", id="manual_tab"):
566
- with gr.Row():
567
- with gr.Column(scale=1):
568
- m_type = gr.Dropdown(
569
- choices=["Shirt", "Dress", "Skirt", "Pants", "Jacket", "Hoodie", "Vest"],
570
- value="Shirt", label="Garment Type"
571
- )
572
-
573
- gr.Markdown("### Body Measurements (cm)")
574
- with gr.Row():
575
- m_bust = gr.Slider(70, 130, value=92, step=1, label="Bust")
576
- m_waist = gr.Slider(55, 110, value=74, step=1, label="Waist")
577
- with gr.Row():
578
- m_hip = gr.Slider(75, 130, value=96, step=1, label="Hip")
579
- m_shoulder = gr.Slider(35, 55, value=42, step=1, label="Shoulder Width")
580
-
581
- gr.Markdown("### Garment Dimensions (cm)")
582
- with gr.Row():
583
- m_bodice = gr.Slider(30, 80, value=42, step=1, label="Bodice/Top Length")
584
- m_sleeve = gr.Slider(10, 75, value=60, step=1, label="Sleeve Length")
585
- with gr.Row():
586
- m_skirt = gr.Slider(25, 120, value=55, step=1, label="Skirt Length")
587
- m_pant = gr.Slider(25, 115, value=100, step=1, label="Pant Length")
588
- with gr.Row():
589
- m_neck = gr.Slider(3, 25, value=8, step=1, label="Neckline Depth")
590
- m_flare = gr.Slider(0, 20, value=0, step=1, label="Hem Flare")
591
-
592
- gr.Markdown("### Features")
593
- with gr.Row():
594
- m_collar = gr.Checkbox(value=True, label="Collar")
595
- m_collar_type = gr.Dropdown(["Standard", "Mandarin", "Peter_pan"], value="Standard", label="Collar Type")
596
- with gr.Row():
597
- m_cuffs = gr.Checkbox(value=True, label="Cuffs")
598
- m_pockets = gr.Checkbox(value=False, label="Pockets")
599
- with gr.Row():
600
- m_hood = gr.Checkbox(value=False, label="Hood")
601
- m_fit = gr.Dropdown(["Fitted", "Regular", "Oversized", "Loose"], value="Regular", label="Fit")
602
-
603
- manual_btn = gr.Button("🧵 Generate Pattern", variant="primary", size="lg")
604
-
605
- with gr.Column(scale=2):
606
- manual_output_pattern = gr.Image(label="2D Sewing Pattern", height=500)
607
- manual_output_summary = gr.Markdown(label="Pattern Summary")
608
- with gr.Accordion("Raw Parameters JSON", open=False):
609
- manual_output_json = gr.Code(language="json", label="Parameters")
610
-
611
- manual_btn.click(
612
- process_manual_params,
613
- inputs=[m_type, m_bust, m_waist, m_hip, m_shoulder, m_bodice,
614
- m_sleeve, m_skirt, m_pant, m_neck,
615
- m_collar, m_collar_type, m_cuffs, m_pockets, m_hood, m_flare, m_fit],
616
- outputs=[manual_output_pattern, manual_output_summary, manual_output_json]
617
- )
618
-
619
  # References
620
  gr.HTML("""
621
  <div class="ref-box" style="margin-top: 20px;">
622
- <h4>📚 Research References</h4>
623
  <ul>
624
- <li><b>ChatGarment</b> (Bian et al., 2024) VLM GarmentCode JSON sewing patterns
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) Training-free VLM + Natural Garment Language
629
  [<a href="https://arxiv.org/abs/2602.20700">Paper</a>]</li>
630
- <li><b>SewFormer</b> (Liu et al., 2023) Two-level Transformer for pattern reconstruction
631
  [<a href="https://arxiv.org/abs/2311.04218">Paper</a>]</li>
632
- <li><b>GarmentDiffusion</b> (2025) DiT-based multimodal pattern generation
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 &middot; Gemma 4 &middot; Kimi K2.5 via
 
 
 
 
341
  <a href="https://huggingface.co/docs/inference-providers" target="_blank">HF Inference Providers</a>
342
+ &nbsp;|&nbsp;
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)