jainarham commited on
Commit
5c3adf5
·
verified ·
1 Parent(s): 4c24702

Update nlp_processor.py

Browse files
Files changed (1) hide show
  1. nlp_processor.py +611 -259
nlp_processor.py CHANGED
@@ -1,324 +1,676 @@
1
  """
2
- NLP Processor Module - FIXED QUANTITY SUPPORT
 
3
  """
4
 
5
  import re
6
- from typing import Dict, Any, Optional, List
7
  import logging
 
 
 
8
 
9
  logger = logging.getLogger(__name__)
10
 
11
 
12
- class NLPProcessor:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def __init__(self):
14
- # Shape vocabulary
 
 
 
 
 
 
 
15
  self.shapes = {
16
- "cube": ["cube", "cubes", "box", "boxes", "block", "blocks"],
17
- "sphere": ["sphere", "spheres", "ball", "balls", "orb", "orbs"],
18
- "cylinder": ["cylinder", "cylinders", "tube", "tubes", "pipe", "pipes"],
19
- "cone": ["cone", "cones", "pointed"],
20
- "torus": ["torus", "toruses", "donut", "donuts", "ring", "rings"],
21
- "pyramid": ["pyramid", "pyramids"],
22
- "plane": ["plane", "planes", "floor", "ground", "platform"],
23
- "capsule": ["capsule", "capsules", "pill", "pills"],
24
  }
25
 
26
- # Color vocabulary
 
 
 
 
 
 
27
  self.colors = {
28
- "red": "#ff0000",
29
- "green": "#00ff00",
30
- "blue": "#0000ff",
31
- "yellow": "#ffff00",
32
- "orange": "#ffa500",
33
- "purple": "#800080",
34
- "pink": "#ffc0cb",
35
- "white": "#ffffff",
36
- "black": "#333333",
37
- "gray": "#808080",
38
- "grey": "#808080",
39
- "brown": "#8b4513",
40
- "cyan": "#00ffff",
41
- "magenta": "#ff00ff",
42
- "gold": "#ffd700",
43
- "golden": "#ffd700",
44
- "silver": "#c0c0c0",
45
- "bronze": "#cd7f32",
46
- "navy": "#000080",
47
- "teal": "#008080",
48
- "lime": "#32cd32",
49
- "coral": "#ff7f50",
50
- "crimson": "#dc143c",
51
- "violet": "#ee82ee",
52
- "indigo": "#4b0082",
53
- "maroon": "#800000",
54
- "olive": "#808000",
55
- "aqua": "#00ffff",
56
  }
57
-
58
- # Size vocabulary
59
  self.sizes = {
60
- "tiny": 0.3,
61
- "very small": 0.4,
62
- "small": 0.6,
63
- "medium": 1.0,
64
- "normal": 1.0,
65
- "regular": 1.0,
66
- "large": 1.5,
67
- "big": 1.5,
68
- "huge": 2.0,
69
- "giant": 2.5,
70
- "massive": 3.0,
71
  }
72
-
73
  # Number words
74
- self.number_words = {
75
- "one": 1, "a": 1, "an": 1,
76
- "two": 2, "pair": 2,
77
- "three": 3, "trio": 3,
78
- "four": 4,
79
- "five": 5,
80
- "six": 6,
81
- "seven": 7,
82
- "eight": 8,
83
- "nine": 9,
84
- "ten": 10,
85
- "eleven": 11,
86
- "twelve": 12,
87
  }
88
-
89
- # Modifications
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  self.modifications = {
91
- "bigger": {"action": "scale", "factor": 1.5},
92
- "larger": {"action": "scale", "factor": 1.5},
93
- "smaller": {"action": "scale", "factor": 0.7},
94
- "taller": {"action": "scale_y", "factor": 1.5},
95
- "shorter": {"action": "scale_y", "factor": 0.7},
96
- "wider": {"action": "scale_x", "factor": 1.5},
97
- "thinner": {"action": "scale_x", "factor": 0.7},
 
98
  }
99
 
100
- def extract_quantity(self, text: str, shape_word: str) -> int:
101
- """Extract quantity from text for a specific shape"""
102
- text_lower = text.lower()
103
 
104
- # Pattern 1: "3 cubes", "5 spheres"
105
- pattern1 = rf'(\d+)\s*{shape_word}'
106
- match1 = re.search(pattern1, text_lower)
107
- if match1:
108
- return int(match1.group(1))
109
-
110
- # Pattern 2: "three cubes", "five spheres"
111
- for word, num in self.number_words.items():
112
- pattern2 = rf'\b{word}\s+{shape_word}'
113
- if re.search(pattern2, text_lower):
114
- return num
115
-
116
- # Pattern 3: Just a number at the start "3 red cubes"
117
- pattern3 = rf'(\d+)\s+\w+\s+{shape_word}'
118
- match3 = re.search(pattern3, text_lower)
119
- if match3:
120
- return int(match3.group(1))
121
-
122
- # Pattern 4: Number word at start "three red cubes"
123
- for word, num in self.number_words.items():
124
- pattern4 = rf'\b{word}\s+\w+\s+{shape_word}'
125
- if re.search(pattern4, text_lower):
126
- return num
127
-
128
- # Pattern 5: Check for plural - if plural, assume at least context wants multiple
129
- # Actually just return 1 for singular
130
- return 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- def extract_shapes(self, text: str) -> List[Dict[str, Any]]:
133
- """Extract shape mentions from text"""
134
- shapes_found = []
 
 
 
 
 
135
  text_lower = text.lower()
136
 
137
- for shape_type, synonyms in self.shapes.items():
138
- for synonym in synonyms:
139
- if synonym in text_lower:
140
- quantity = self.extract_quantity(text_lower, synonym)
141
-
142
- # Avoid duplicates
143
- already_found = any(s["type"] == shape_type for s in shapes_found)
144
- if not already_found:
145
- shapes_found.append({
146
- "type": shape_type,
147
- "quantity": quantity,
148
- "matched_word": synonym
149
- })
150
- break
151
 
152
- return shapes_found
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
- def extract_colors(self, text: str) -> List[str]:
155
- """Extract color mentions"""
156
- colors_found = []
157
- text_lower = text.lower()
158
 
159
- for color in self.colors.keys():
160
- if color in text_lower:
161
- colors_found.append(color)
 
 
 
162
 
163
- return colors_found
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
- def extract_size(self, text: str) -> Optional[float]:
166
- """Extract size information"""
 
167
  text_lower = text.lower()
168
 
169
- for size_word, scale in self.sizes.items():
170
- if size_word in text_lower:
171
- return scale
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- return None
174
 
175
- def detect_modification(self, text: str, existing_context: Optional[Dict]) -> Dict[str, Any]:
176
- """Detect if user wants to modify existing model"""
177
- modification_keywords = [
178
- "make it", "change", "modify", "update", "turn it",
179
- "bigger", "smaller", "larger", "taller", "shorter",
180
- "different color", "new color", "instead",
181
- "more", "less", "add", "remove"
182
- ]
183
 
184
- text_lower = text.lower()
185
- is_modification = any(kw in text_lower for kw in modification_keywords)
186
 
187
- if is_modification and existing_context:
188
- modifications = []
189
 
190
- for mod_word, mod_action in self.modifications.items():
191
- if mod_word in text_lower:
192
- modifications.append(mod_action)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
- new_colors = self.extract_colors(text)
195
- if new_colors:
196
- modifications.append({
197
- "action": "change_color",
198
- "color": new_colors[0]
199
- })
 
 
 
 
 
 
 
 
 
 
200
 
201
- return {
202
- "is_modification": True,
203
- "modifications": modifications,
204
- "base_context": existing_context
205
- }
 
206
 
207
- return {"is_modification": False}
208
 
209
  def process_prompt(self, prompt: str, existing_context: Optional[Dict] = None) -> Dict[str, Any]:
210
- """Main method to process prompt"""
211
  try:
212
- text = prompt.lower().strip()
213
 
214
- # Check for modification
215
- mod_result = self.detect_modification(text, existing_context)
 
216
 
217
- # Extract components
218
- shapes = self.extract_shapes(text)
219
- colors = self.extract_colors(text)
220
- size = self.extract_size(text)
221
-
222
- # Build interpretation
223
- interpretation_parts = []
224
 
225
- # Handle modifications
226
- if mod_result["is_modification"] and existing_context:
227
- interpretation_parts.append("Modifying existing model")
228
- if not shapes:
229
- shapes = []
230
- for obj in existing_context.get("model_params", {}).get("objects", []):
231
- shapes.append({"type": obj.get("type", "cube"), "quantity": 1})
232
 
233
- # Default shape if none found
234
- if not shapes:
235
- shapes = [{"type": "cube", "quantity": 1}]
236
- interpretation_parts.append("No shape specified, using cube")
237
 
238
- # Build description
239
- shape_descriptions = []
240
- for s in shapes:
241
- qty = s.get("quantity", 1)
242
- if qty > 1:
243
- shape_descriptions.append(f"{qty}x {s['type']}")
244
- else:
245
- shape_descriptions.append(s['type'])
246
-
247
- interpretation_parts.append(f"Creating: {', '.join(shape_descriptions)}")
248
-
249
- if colors:
250
- interpretation_parts.append(f"Color: {colors[0]}")
251
- if size:
252
- interpretation_parts.append(f"Scale: {size:.1f}x")
253
-
254
- # Build objects list
255
- objects = []
256
- obj_index = 0
257
 
258
- for shape in shapes:
259
- quantity = shape.get("quantity", 1)
260
- shape_type = shape.get("type", "cube")
261
-
262
- for q in range(quantity):
263
- # Calculate position - spread objects out
264
- x_pos = (obj_index % 5) * 2.0 - 4.0 # -4, -2, 0, 2, 4
265
- z_pos = (obj_index // 5) * 2.0 # Rows
266
-
267
- if quantity == 1 and len(shapes) == 1:
268
- x_pos = 0
269
- z_pos = 0
270
-
271
- obj = {
272
- "type": shape_type,
273
- "color": self.colors.get(colors[0], "#808080") if colors else "#808080",
274
- "scale": size if size else 1.0,
275
- "position": {"x": x_pos, "y": 0, "z": z_pos},
276
- "rotation": {"x": 0, "y": 0, "z": 0}
277
- }
278
-
279
- objects.append(obj)
280
- obj_index += 1
281
-
282
- # Apply modifications
283
- if mod_result["is_modification"]:
284
- for mod in mod_result.get("modifications", []):
285
- for obj in objects:
286
- if mod.get("action") == "scale":
287
- obj["scale"] = obj.get("scale", 1.0) * mod.get("factor", 1.0)
288
- elif mod.get("action") == "scale_y":
289
- if "scale_axes" not in obj:
290
- obj["scale_axes"] = {"x": 1, "y": 1, "z": 1}
291
- obj["scale_axes"]["y"] *= mod.get("factor", 1.0)
292
- elif mod.get("action") == "scale_x":
293
- if "scale_axes" not in obj:
294
- obj["scale_axes"] = {"x": 1, "y": 1, "z": 1}
295
- obj["scale_axes"]["x"] *= mod.get("factor", 1.0)
296
- elif mod.get("action") == "change_color":
297
- obj["color"] = self.colors.get(mod.get("color"), obj.get("color", "#808080"))
298
-
299
  model_params = {
300
  "objects": objects,
301
  "scene_settings": {
302
  "background_color": "#1a1a2e",
303
- "ambient_light": 0.4,
304
  "directional_light": 0.8
305
  }
306
  }
307
-
308
- logger.info(f"Processed: {len(objects)} objects created")
309
-
310
  return {
311
  "success": True,
312
  "original_prompt": prompt,
313
- "interpretation": " | ".join(interpretation_parts),
314
  "model_params": model_params,
315
- "is_modification": mod_result.get("is_modification", False)
316
  }
317
-
318
  except Exception as e:
319
- logger.error(f"NLP processing error: {str(e)}")
320
  return {
321
  "success": False,
322
- "error": f"Failed to process: {str(e)}",
323
- "interpretation": "Error understanding request"
324
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Advanced NLP Processor - Professional Grade
3
+ Understands complex prompts, spatial relationships, and compositions
4
  """
5
 
6
  import re
 
7
  import logging
8
+ from typing import Dict, Any, Optional, List, Tuple
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
 
15
+ class Relation(Enum):
16
+ NONE = "none"
17
+ ON_TOP = "on_top"
18
+ BELOW = "below"
19
+ NEXT_TO = "next_to"
20
+ LEFT_OF = "left_of"
21
+ RIGHT_OF = "right_of"
22
+ IN_FRONT = "in_front"
23
+ BEHIND = "behind"
24
+ INSIDE = "inside"
25
+ AROUND = "around"
26
+ BETWEEN = "between"
27
+
28
+
29
+ class Arrangement(Enum):
30
+ NONE = "none"
31
+ ROW = "row"
32
+ COLUMN = "column"
33
+ CIRCLE = "circle"
34
+ GRID = "grid"
35
+ STACK = "stack"
36
+ RANDOM = "random"
37
+ INCREASING = "increasing"
38
+ DECREASING = "decreasing"
39
+
40
+
41
+ @dataclass
42
+ class ParsedObject:
43
+ shape: str
44
+ color: str
45
+ size: float
46
+ quantity: int
47
+ material: str
48
+ relation: Relation
49
+ relation_target: Optional[str]
50
+ arrangement: Arrangement
51
+ modifiers: List[str]
52
+
53
+
54
+ class AdvancedNLPProcessor:
55
  def __init__(self):
56
+ self._init_vocabularies()
57
+ self._compile_patterns()
58
+ logger.info("Advanced NLP Processor initialized")
59
+
60
+ def _init_vocabularies(self):
61
+ """Initialize all vocabularies and mappings"""
62
+
63
+ # Shapes with synonyms
64
  self.shapes = {
65
+ "cube": ["cube", "cubes", "box", "boxes", "block", "blocks", "square", "squares"],
66
+ "sphere": ["sphere", "spheres", "ball", "balls", "orb", "orbs", "globe", "globes"],
67
+ "cylinder": ["cylinder", "cylinders", "tube", "tubes", "pipe", "pipes", "pillar", "pillars", "column", "columns"],
68
+ "cone": ["cone", "cones", "spike", "spikes"],
69
+ "torus": ["torus", "toruses", "tori", "donut", "donuts", "ring", "rings", "hoop", "hoops"],
70
+ "pyramid": ["pyramid", "pyramids", "tetrahedron", "tetrahedrons"],
71
+ "capsule": ["capsule", "capsules", "pill", "pills", "lozenge"],
72
+ "plane": ["plane", "planes", "floor", "ground", "platform", "base", "surface"],
73
  }
74
 
75
+ # Flatten for quick lookup
76
+ self.shape_lookup = {}
77
+ for canonical, synonyms in self.shapes.items():
78
+ for syn in synonyms:
79
+ self.shape_lookup[syn] = canonical
80
+
81
+ # Colors with hex values
82
  self.colors = {
83
+ # Basic
84
+ "red": "#e74c3c", "green": "#2ecc71", "blue": "#3498db",
85
+ "yellow": "#f1c40f", "orange": "#e67e22", "purple": "#9b59b6",
86
+ "pink": "#e91e63", "white": "#ecf0f1", "black": "#2c3e50",
87
+ "gray": "#95a5a6", "grey": "#95a5a6", "brown": "#8b4513",
88
+ # Extended
89
+ "cyan": "#00bcd4", "magenta": "#e91e63", "lime": "#8bc34a",
90
+ "teal": "#009688", "navy": "#1a237e", "maroon": "#800000",
91
+ "olive": "#808000", "coral": "#ff7f50", "salmon": "#fa8072",
92
+ "gold": "#ffd700", "golden": "#ffd700", "silver": "#c0c0c0",
93
+ "bronze": "#cd7f32", "copper": "#b87333", "platinum": "#e5e4e2",
94
+ "crimson": "#dc143c", "scarlet": "#ff2400", "ruby": "#e0115f",
95
+ "emerald": "#50c878", "jade": "#00a86b", "mint": "#98fb98",
96
+ "sapphire": "#0f52ba", "azure": "#007fff", "indigo": "#4b0082",
97
+ "violet": "#8f00ff", "lavender": "#e6e6fa", "plum": "#dda0dd",
98
+ "turquoise": "#40e0d0", "aqua": "#00ffff", "sky": "#87ceeb",
99
+ "peach": "#ffdab9", "beige": "#f5f5dc", "ivory": "#fffff0",
100
+ "cream": "#fffdd0", "tan": "#d2b48c", "chocolate": "#7b3f00",
101
+ "charcoal": "#36454f", "slate": "#708090", "steel": "#71797e",
102
+ # Metallic descriptors
103
+ "metallic": "#a8a9ad", "shiny": "#d4d4d4", "chrome": "#dbe4eb",
 
 
 
 
 
 
 
104
  }
105
+
106
+ # Sizes with scale factors
107
  self.sizes = {
108
+ "tiny": 0.25, "very small": 0.35, "small": 0.5, "little": 0.5,
109
+ "medium": 1.0, "normal": 1.0, "regular": 1.0, "average": 1.0,
110
+ "large": 1.5, "big": 1.5, "huge": 2.0, "giant": 2.5,
111
+ "massive": 3.0, "enormous": 3.5, "colossal": 4.0,
112
+ "mini": 0.3, "micro": 0.2, "nano": 0.15,
 
 
 
 
 
 
113
  }
114
+
115
  # Number words
116
+ self.numbers = {
117
+ "a": 1, "an": 1, "one": 1, "single": 1,
118
+ "two": 2, "pair": 2, "couple": 2, "double": 2,
119
+ "three": 3, "triple": 3, "trio": 3,
120
+ "four": 4, "quad": 4, "quadruple": 4,
121
+ "five": 5, "six": 6, "seven": 7, "eight": 8,
122
+ "nine": 9, "ten": 10, "eleven": 11, "twelve": 12,
123
+ "dozen": 12, "fifteen": 15, "twenty": 20,
124
+ "few": 3, "several": 4, "many": 6, "lots": 8,
 
 
 
 
125
  }
126
+
127
+ # Spatial relations
128
+ self.relations = {
129
+ # On top
130
+ "on": Relation.ON_TOP, "on top of": Relation.ON_TOP,
131
+ "above": Relation.ON_TOP, "over": Relation.ON_TOP,
132
+ "atop": Relation.ON_TOP, "upon": Relation.ON_TOP,
133
+ # Below
134
+ "under": Relation.BELOW, "below": Relation.BELOW,
135
+ "beneath": Relation.BELOW, "underneath": Relation.BELOW,
136
+ # Next to
137
+ "next to": Relation.NEXT_TO, "beside": Relation.NEXT_TO,
138
+ "by": Relation.NEXT_TO, "near": Relation.NEXT_TO,
139
+ "adjacent": Relation.NEXT_TO, "alongside": Relation.NEXT_TO,
140
+ # Left/Right
141
+ "left of": Relation.LEFT_OF, "to the left": Relation.LEFT_OF,
142
+ "right of": Relation.RIGHT_OF, "to the right": Relation.RIGHT_OF,
143
+ # Front/Behind
144
+ "in front of": Relation.IN_FRONT, "before": Relation.IN_FRONT,
145
+ "behind": Relation.BEHIND, "back of": Relation.BEHIND,
146
+ # Inside
147
+ "inside": Relation.INSIDE, "within": Relation.INSIDE,
148
+ "in": Relation.INSIDE,
149
+ # Around
150
+ "around": Relation.AROUND, "surrounding": Relation.AROUND,
151
+ # Between
152
+ "between": Relation.BETWEEN,
153
+ }
154
+
155
+ # Arrangements
156
+ self.arrangements = {
157
+ "row": Arrangement.ROW, "line": Arrangement.ROW, "horizontal": Arrangement.ROW,
158
+ "column": Arrangement.COLUMN, "vertical": Arrangement.COLUMN, "tower": Arrangement.STACK,
159
+ "circle": Arrangement.CIRCLE, "ring": Arrangement.CIRCLE, "circular": Arrangement.CIRCLE,
160
+ "grid": Arrangement.GRID, "matrix": Arrangement.GRID, "array": Arrangement.GRID,
161
+ "stack": Arrangement.STACK, "stacked": Arrangement.STACK, "pile": Arrangement.STACK,
162
+ "random": Arrangement.RANDOM, "scattered": Arrangement.RANDOM,
163
+ "increasing": Arrangement.INCREASING, "ascending": Arrangement.INCREASING,
164
+ "decreasing": Arrangement.DECREASING, "descending": Arrangement.DECREASING,
165
+ }
166
+
167
+ # Materials/textures
168
+ self.materials = {
169
+ "metallic": "metallic", "metal": "metallic", "shiny": "shiny",
170
+ "matte": "matte", "flat": "matte", "glossy": "glossy",
171
+ "glass": "glass", "transparent": "glass", "crystal": "glass",
172
+ "wood": "wood", "wooden": "wood", "stone": "stone",
173
+ "plastic": "plastic", "rubber": "rubber", "chrome": "chrome",
174
+ "brushed": "brushed", "polished": "polished", "rough": "rough",
175
+ "smooth": "smooth", "textured": "textured",
176
+ }
177
+
178
+ # Modification keywords for refinement
179
  self.modifications = {
180
+ "bigger": ("scale", 1.5), "larger": ("scale", 1.5),
181
+ "smaller": ("scale", 0.7), "tinier": ("scale", 0.5),
182
+ "taller": ("scale_y", 1.5), "shorter": ("scale_y", 0.7),
183
+ "wider": ("scale_x", 1.5), "thinner": ("scale_x", 0.7),
184
+ "longer": ("scale_z", 1.5), "deeper": ("scale_z", 1.5),
185
+ "double": ("scale", 2.0), "half": ("scale", 0.5),
186
+ "rotate": ("rotate_y", 45), "spin": ("rotate_y", 90),
187
+ "flip": ("rotate_x", 180), "tilt": ("rotate_z", 30),
188
  }
189
 
190
+ def _compile_patterns(self):
191
+ """Compile regex patterns for efficiency"""
 
192
 
193
+ # Number pattern: "5", "five", "a few"
194
+ number_words = '|'.join(self.numbers.keys())
195
+ self.num_pattern = re.compile(
196
+ rf'\b(\d+|{number_words})\b',
197
+ re.IGNORECASE
198
+ )
199
+
200
+ # Shape pattern
201
+ all_shapes = []
202
+ for synonyms in self.shapes.values():
203
+ all_shapes.extend(synonyms)
204
+ shape_words = '|'.join(sorted(all_shapes, key=len, reverse=True))
205
+ self.shape_pattern = re.compile(
206
+ rf'\b({shape_words})\b',
207
+ re.IGNORECASE
208
+ )
209
+
210
+ # Color pattern
211
+ color_words = '|'.join(sorted(self.colors.keys(), key=len, reverse=True))
212
+ self.color_pattern = re.compile(
213
+ rf'\b({color_words})\b',
214
+ re.IGNORECASE
215
+ )
216
+
217
+ # Size pattern
218
+ size_words = '|'.join(sorted(self.sizes.keys(), key=len, reverse=True))
219
+ self.size_pattern = re.compile(
220
+ rf'\b({size_words})\b',
221
+ re.IGNORECASE
222
+ )
223
+
224
+ # Relation pattern
225
+ relation_words = '|'.join(sorted(self.relations.keys(), key=len, reverse=True))
226
+ self.relation_pattern = re.compile(
227
+ rf'\b({relation_words})\b',
228
+ re.IGNORECASE
229
+ )
230
+
231
+ # Arrangement pattern
232
+ arr_words = '|'.join(sorted(self.arrangements.keys(), key=len, reverse=True))
233
+ self.arrangement_pattern = re.compile(
234
+ rf'\b({arr_words})\b',
235
+ re.IGNORECASE
236
+ )
237
+
238
+ # Material pattern
239
+ mat_words = '|'.join(sorted(self.materials.keys(), key=len, reverse=True))
240
+ self.material_pattern = re.compile(
241
+ rf'\b({mat_words})\b',
242
+ re.IGNORECASE
243
+ )
244
 
245
+ # Compound object pattern: "X on Y", "X with Y"
246
+ self.compound_pattern = re.compile(
247
+ r'(.+?)\s+(on|with|and|next to|beside|above|below|under|behind|in front of)\s+(.+)',
248
+ re.IGNORECASE
249
+ )
250
+
251
+ def _extract_number(self, text: str, shape_word: str) -> int:
252
+ """Extract quantity for a specific shape"""
253
  text_lower = text.lower()
254
 
255
+ # Pattern: "3 red cubes", "three cubes", "a cube"
256
+ patterns = [
257
+ rf'(\d+)\s+\w*\s*{shape_word}', # "3 red cubes"
258
+ rf'(\d+)\s+{shape_word}', # "3 cubes"
259
+ ]
 
 
 
 
 
 
 
 
 
260
 
261
+ for pattern in patterns:
262
+ match = re.search(pattern, text_lower)
263
+ if match:
264
+ return int(match.group(1))
265
+
266
+ # Check number words
267
+ for word, num in self.numbers.items():
268
+ patterns = [
269
+ rf'\b{word}\s+\w*\s*{shape_word}',
270
+ rf'\b{word}\s+{shape_word}',
271
+ ]
272
+ for pattern in patterns:
273
+ if re.search(pattern, text_lower):
274
+ return num
275
+
276
+ return 1
277
 
278
+ def _parse_segment(self, text: str) -> ParsedObject:
279
+ """Parse a single object segment"""
280
+ text_lower = text.lower().strip()
 
281
 
282
+ # Find shape
283
+ shape = "cube" # default
284
+ shape_match = self.shape_pattern.search(text_lower)
285
+ if shape_match:
286
+ matched = shape_match.group(1)
287
+ shape = self.shape_lookup.get(matched, "cube")
288
 
289
+ # Find color
290
+ color = "#808080" # default gray
291
+ color_match = self.color_pattern.search(text_lower)
292
+ if color_match:
293
+ color = self.colors.get(color_match.group(1).lower(), "#808080")
294
+
295
+ # Find size
296
+ size = 1.0
297
+ size_match = self.size_pattern.search(text_lower)
298
+ if size_match:
299
+ size = self.sizes.get(size_match.group(1).lower(), 1.0)
300
+
301
+ # Find quantity
302
+ quantity = self._extract_number(text_lower, shape_match.group(1) if shape_match else "cube")
303
+
304
+ # Find material
305
+ material = "default"
306
+ mat_match = self.material_pattern.search(text_lower)
307
+ if mat_match:
308
+ material = self.materials.get(mat_match.group(1).lower(), "default")
309
+
310
+ # Find arrangement
311
+ arrangement = Arrangement.NONE
312
+ arr_match = self.arrangement_pattern.search(text_lower)
313
+ if arr_match:
314
+ arrangement = self.arrangements.get(arr_match.group(1).lower(), Arrangement.NONE)
315
+
316
+ # Collect modifiers
317
+ modifiers = []
318
+ for mod in ["smooth", "rough", "shiny", "matte", "glossy"]:
319
+ if mod in text_lower:
320
+ modifiers.append(mod)
321
+
322
+ return ParsedObject(
323
+ shape=shape,
324
+ color=color,
325
+ size=size,
326
+ quantity=quantity,
327
+ material=material,
328
+ relation=Relation.NONE,
329
+ relation_target=None,
330
+ arrangement=arrangement,
331
+ modifiers=modifiers
332
+ )
333
 
334
+ def _parse_compound(self, text: str) -> List[Tuple[ParsedObject, Relation, int]]:
335
+ """Parse compound prompts with relationships"""
336
+ results = []
337
  text_lower = text.lower()
338
 
339
+ # Split by "and" first for multiple independent objects
340
+ and_parts = re.split(r'\s+and\s+', text, flags=re.IGNORECASE)
341
+
342
+ for part in and_parts:
343
+ part = part.strip()
344
+ if not part:
345
+ continue
346
+
347
+ # Check for relational phrases
348
+ relation_found = False
349
+ for rel_phrase, relation in sorted(self.relations.items(), key=lambda x: len(x[0]), reverse=True):
350
+ pattern = rf'(.+?)\s+{re.escape(rel_phrase)}\s+(.+)'
351
+ match = re.match(pattern, part, re.IGNORECASE)
352
+ if match:
353
+ # Object A relation Object B
354
+ obj_a_text = match.group(1).strip()
355
+ obj_b_text = match.group(2).strip()
356
+
357
+ obj_b = self._parse_segment(obj_b_text)
358
+ results.append((obj_b, Relation.NONE, -1))
359
+
360
+ obj_a = self._parse_segment(obj_a_text)
361
+ obj_a.relation = relation
362
+ obj_a.relation_target = len(results) - 1
363
+ results.append((obj_a, relation, len(results) - 1))
364
+
365
+ relation_found = True
366
+ break
367
+
368
+ if not relation_found:
369
+ # Single object or simple description
370
+ obj = self._parse_segment(part)
371
+ results.append((obj, Relation.NONE, -1))
372
 
373
+ return results
374
 
375
+ def _build_objects(self, parsed_items: List[Tuple[ParsedObject, Relation, int]]) -> List[Dict]:
376
+ """Convert parsed objects to 3D parameters with positions"""
377
+ objects = []
378
+ object_positions = {} # Track positions by index
 
 
 
 
379
 
380
+ current_x = 0
381
+ current_z = 0
382
 
383
+ for idx, (parsed, relation, target_idx) in enumerate(parsed_items):
384
+ base_x, base_y, base_z = 0, 0, 0
385
 
386
+ # Calculate position based on relation
387
+ if relation != Relation.NONE and target_idx >= 0 and target_idx in object_positions:
388
+ target_pos = object_positions[target_idx]
389
+ base_x, base_y, base_z = target_pos
390
+
391
+ if relation == Relation.ON_TOP:
392
+ base_y += 1.2 * parsed.size
393
+ elif relation == Relation.BELOW:
394
+ base_y -= 1.2 * parsed.size
395
+ elif relation == Relation.NEXT_TO or relation == Relation.RIGHT_OF:
396
+ base_x += 2.0 * parsed.size
397
+ elif relation == Relation.LEFT_OF:
398
+ base_x -= 2.0 * parsed.size
399
+ elif relation == Relation.IN_FRONT:
400
+ base_z += 2.0 * parsed.size
401
+ elif relation == Relation.BEHIND:
402
+ base_z -= 2.0 * parsed.size
403
+
404
+ # Generate objects based on quantity and arrangement
405
+ for q in range(parsed.quantity):
406
+ obj_x, obj_y, obj_z = base_x, base_y, base_z
407
+ obj_scale = parsed.size
408
+
409
+ # Apply arrangement
410
+ if parsed.arrangement == Arrangement.ROW:
411
+ obj_x += q * 2.2 * parsed.size
412
+ elif parsed.arrangement == Arrangement.COLUMN:
413
+ obj_z += q * 2.2 * parsed.size
414
+ elif parsed.arrangement == Arrangement.STACK:
415
+ obj_y += q * 1.2 * parsed.size
416
+ elif parsed.arrangement == Arrangement.CIRCLE:
417
+ import math
418
+ angle = (2 * math.pi * q) / parsed.quantity
419
+ radius = max(2.0, parsed.quantity * 0.5) * parsed.size
420
+ obj_x += math.cos(angle) * radius
421
+ obj_z += math.sin(angle) * radius
422
+ elif parsed.arrangement == Arrangement.GRID:
423
+ import math
424
+ grid_size = int(math.ceil(math.sqrt(parsed.quantity)))
425
+ row = q // grid_size
426
+ col = q % grid_size
427
+ obj_x += col * 2.2 * parsed.size
428
+ obj_z += row * 2.2 * parsed.size
429
+ elif parsed.arrangement == Arrangement.INCREASING:
430
+ obj_scale = parsed.size * (0.5 + q * 0.3)
431
+ obj_x += q * 2.5 * parsed.size
432
+ elif parsed.arrangement == Arrangement.DECREASING:
433
+ obj_scale = parsed.size * (1.5 - q * 0.2)
434
+ obj_x += q * 2.5 * parsed.size
435
+ elif parsed.arrangement == Arrangement.RANDOM:
436
+ import random
437
+ obj_x += random.uniform(-3, 3)
438
+ obj_z += random.uniform(-3, 3)
439
+ elif parsed.arrangement == Arrangement.NONE and parsed.quantity > 1:
440
+ # Default: spread in a row
441
+ obj_x += q * 2.2 * parsed.size
442
+
443
+ # No relation and no arrangement - use sequential positioning
444
+ if relation == Relation.NONE and parsed.arrangement == Arrangement.NONE and q == 0:
445
+ if len(objects) > 0:
446
+ obj_x = current_x
447
+ current_x += 2.5 * parsed.size
448
+
449
+ obj = {
450
+ "type": parsed.shape,
451
+ "color": parsed.color,
452
+ "scale": obj_scale,
453
+ "position": {"x": obj_x, "y": obj_y, "z": obj_z},
454
+ "rotation": {"x": 0, "y": 0, "z": 0},
455
+ "material": parsed.material,
456
+ "modifiers": parsed.modifiers
457
+ }
458
+
459
+ objects.append(obj)
460
 
461
+ # Store first object position for this parsed item
462
+ if objects:
463
+ last_obj = objects[-parsed.quantity] if parsed.quantity <= len(objects) else objects[-1]
464
+ object_positions[idx] = (
465
+ last_obj["position"]["x"],
466
+ last_obj["position"]["y"],
467
+ last_obj["position"]["z"]
468
+ )
469
+ current_x = last_obj["position"]["x"] + 2.5 * parsed.size
470
+
471
+ # Center all objects
472
+ if objects:
473
+ min_x = min(o["position"]["x"] for o in objects)
474
+ max_x = max(o["position"]["x"] for o in objects)
475
+ min_z = min(o["position"]["z"] for o in objects)
476
+ max_z = max(o["position"]["z"] for o in objects)
477
 
478
+ center_x = (min_x + max_x) / 2
479
+ center_z = (min_z + max_z) / 2
480
+
481
+ for obj in objects:
482
+ obj["position"]["x"] -= center_x
483
+ obj["position"]["z"] -= center_z
484
 
485
+ return objects
486
 
487
  def process_prompt(self, prompt: str, existing_context: Optional[Dict] = None) -> Dict[str, Any]:
488
+ """Main processing method"""
489
  try:
490
+ logger.info(f"Processing: {prompt}")
491
 
492
+ # Check for modification request
493
+ if existing_context and self._is_modification(prompt):
494
+ return self._process_modification(prompt, existing_context)
495
 
496
+ # Parse the prompt
497
+ parsed_items = self._parse_compound(prompt)
 
 
 
 
 
498
 
499
+ if not parsed_items:
500
+ parsed_items = [(self._parse_segment(prompt), Relation.NONE, -1)]
 
 
 
 
 
501
 
502
+ # Build objects
503
+ objects = self._build_objects(parsed_items)
 
 
504
 
505
+ # Build interpretation
506
+ interpretation = self._build_interpretation(parsed_items, objects)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  model_params = {
509
  "objects": objects,
510
  "scene_settings": {
511
  "background_color": "#1a1a2e",
512
+ "ambient_light": 0.5,
513
  "directional_light": 0.8
514
  }
515
  }
516
+
517
+ logger.info(f"Generated {len(objects)} objects")
518
+
519
  return {
520
  "success": True,
521
  "original_prompt": prompt,
522
+ "interpretation": interpretation,
523
  "model_params": model_params,
524
+ "is_modification": False
525
  }
526
+
527
  except Exception as e:
528
+ logger.error(f"Processing error: {str(e)}")
529
  return {
530
  "success": False,
531
+ "error": str(e),
532
+ "interpretation": f"Error: {str(e)}"
533
+ }
534
+
535
+ def _is_modification(self, text: str) -> bool:
536
+ """Check if prompt is a modification request"""
537
+ mod_keywords = [
538
+ "make it", "change", "modify", "update", "make them",
539
+ "bigger", "smaller", "larger", "taller", "shorter",
540
+ "rotate", "spin", "flip", "move", "shift",
541
+ "different color", "change color", "new color",
542
+ "add more", "remove", "delete", "more", "less"
543
+ ]
544
+ text_lower = text.lower()
545
+ return any(kw in text_lower for kw in mod_keywords)
546
+
547
+ def _process_modification(self, prompt: str, context: Dict) -> Dict[str, Any]:
548
+ """Process modification request"""
549
+ text_lower = prompt.lower()
550
+
551
+ # Get existing objects
552
+ existing_objects = context.get("model_params", {}).get("objects", [])
553
+ if not existing_objects:
554
+ return self.process_prompt(prompt, None)
555
+
556
+ modified_objects = []
557
+
558
+ for obj in existing_objects:
559
+ new_obj = obj.copy()
560
+ new_obj["position"] = obj["position"].copy()
561
+ new_obj["rotation"] = obj.get("rotation", {"x": 0, "y": 0, "z": 0}).copy()
562
+
563
+ # Apply modifications
564
+ for mod_word, (action, value) in self.modifications.items():
565
+ if mod_word in text_lower:
566
+ if action == "scale":
567
+ new_obj["scale"] = obj.get("scale", 1.0) * value
568
+ elif action == "scale_x":
569
+ if "scale_axes" not in new_obj:
570
+ new_obj["scale_axes"] = {"x": 1, "y": 1, "z": 1}
571
+ new_obj["scale_axes"]["x"] *= value
572
+ elif action == "scale_y":
573
+ if "scale_axes" not in new_obj:
574
+ new_obj["scale_axes"] = {"x": 1, "y": 1, "z": 1}
575
+ new_obj["scale_axes"]["y"] *= value
576
+ elif action == "scale_z":
577
+ if "scale_axes" not in new_obj:
578
+ new_obj["scale_axes"] = {"x": 1, "y": 1, "z": 1}
579
+ new_obj["scale_axes"]["z"] *= value
580
+ elif action.startswith("rotate"):
581
+ axis = action.split("_")[1]
582
+ new_obj["rotation"][axis] = new_obj["rotation"].get(axis, 0) + value
583
+
584
+ # Check for color change
585
+ color_match = self.color_pattern.search(text_lower)
586
+ if color_match and ("color" in text_lower or "change" in text_lower):
587
+ new_obj["color"] = self.colors.get(color_match.group(1).lower(), new_obj["color"])
588
+
589
+ modified_objects.append(new_obj)
590
+
591
+ # Check for "add more"
592
+ if "add" in text_lower or "more" in text_lower:
593
+ # Parse what to add
594
+ add_parsed = self._parse_segment(prompt)
595
+ if add_parsed.quantity > 0:
596
+ # Add new objects
597
+ last_x = max(o["position"]["x"] for o in modified_objects) if modified_objects else 0
598
+ for i in range(add_parsed.quantity):
599
+ modified_objects.append({
600
+ "type": add_parsed.shape,
601
+ "color": add_parsed.color,
602
+ "scale": add_parsed.size,
603
+ "position": {"x": last_x + 2.5 * (i + 1), "y": 0, "z": 0},
604
+ "rotation": {"x": 0, "y": 0, "z": 0},
605
+ "material": add_parsed.material,
606
+ "modifiers": add_parsed.modifiers
607
+ })
608
+
609
+ model_params = {
610
+ "objects": modified_objects,
611
+ "scene_settings": context.get("model_params", {}).get("scene_settings", {
612
+ "background_color": "#1a1a2e",
613
+ "ambient_light": 0.5,
614
+ "directional_light": 0.8
615
+ })
616
+ }
617
+
618
+ return {
619
+ "success": True,
620
+ "original_prompt": prompt,
621
+ "interpretation": f"Modified {len(modified_objects)} object(s)",
622
+ "model_params": model_params,
623
+ "is_modification": True
624
+ }
625
+
626
+ def _build_interpretation(self, parsed_items: List[Tuple[ParsedObject, Relation, int]], objects: List[Dict]) -> str:
627
+ """Build human-readable interpretation"""
628
+ parts = []
629
+
630
+ for parsed, relation, target in parsed_items:
631
+ desc = []
632
+ if parsed.quantity > 1:
633
+ desc.append(f"{parsed.quantity}x")
634
+ if parsed.size != 1.0:
635
+ size_name = "small" if parsed.size < 1 else "large" if parsed.size > 1 else ""
636
+ if size_name:
637
+ desc.append(size_name)
638
+
639
+ # Get color name
640
+ color_name = next((name for name, hex_val in self.colors.items() if hex_val == parsed.color), None)
641
+ if color_name:
642
+ desc.append(color_name)
643
+
644
+ desc.append(parsed.shape)
645
+
646
+ if parsed.arrangement != Arrangement.NONE:
647
+ desc.append(f"in {parsed.arrangement.value}")
648
+
649
+ if relation != Relation.NONE:
650
+ desc.append(relation.value.replace("_", " "))
651
+
652
+ parts.append(" ".join(desc))
653
+
654
+ interpretation = "Creating: " + ", ".join(parts)
655
+ interpretation += f" | Total: {len(objects)} objects"
656
+
657
+ return interpretation
658
+
659
+
660
+ # Global instance
661
+ _processor = None
662
+
663
+ def get_processor() -> AdvancedNLPProcessor:
664
+ global _processor
665
+ if _processor is None:
666
+ _processor = AdvancedNLPProcessor()
667
+ return _processor
668
+
669
+
670
+ class NLPProcessor:
671
+ """Wrapper for backward compatibility"""
672
+ def __init__(self):
673
+ self._processor = get_processor()
674
+
675
+ def process_prompt(self, prompt: str, existing_context: Optional[Dict] = None) -> Dict[str, Any]:
676
+ return self._processor.process_prompt(prompt, existing_context)