Spyspook commited on
Commit
05bfbba
·
verified ·
1 Parent(s): b32bcf7

fix path in ui

Browse files
Files changed (2) hide show
  1. src/ui/callbacks.py +39 -37
  2. src/ui/layout.py +37 -9
src/ui/callbacks.py CHANGED
@@ -75,11 +75,25 @@ def format_checklist(steps_state: List[Dict[str, Any]]) -> str:
75
  lines.append(f"[❌] 🔴 {s['name']} - {s.get('message')}")
76
  return "\n".join(lines)
77
 
78
- def generate_wrapper(user_prompt: str) -> Generator[Tuple[Optional[str], str], None, None]:
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  """Оркестратор пайплайна с передачей промпта в LLM и запуском 3D-движка."""
80
  tracker = setup_logger()
81
  local_logger = logging.getLogger(__name__)
82
 
 
83
  steps_state: List[Dict[str, Any]] = [
84
  {"name": "ИИ-анализ и граф", "status": "pending"},
85
  {"name": "Инициализация", "status": "pending"},
@@ -90,7 +104,9 @@ def generate_wrapper(user_prompt: str) -> Generator[Tuple[Optional[str], str], N
90
  {"name": "Экспорт GLB (Draco)", "status": "pending"}
91
  ]
92
 
93
- yield None, format_checklist(steps_state)
 
 
94
  total_start: float = time.time()
95
  task_queue: queue.Queue = queue.Queue()
96
 
@@ -106,70 +122,46 @@ def generate_wrapper(user_prompt: str) -> Generator[Tuple[Optional[str], str], N
106
  task_queue.put({"type": "step_update", "idx": 0, "update": {"status": "running"}})
107
  llm_start: float = time.time()
108
 
109
- # ПРЕОБРАЗОВАНИЕ В СЫРОЙ СЛОВАРЬ:
110
- # Отключаем авто-разрешение интерполяций (resolve=False), чтобы избежать InterpolationKeyError
111
  asset_cfg_raw = OmegaConf.to_container(asset_cfg, resolve=False)
112
 
113
- # Собираем категории товаров из безопасного словаря
114
  categories: List[str] = []
115
  for group in asset_cfg_raw.get("products_hierarchy", {}).values():
116
  if isinstance(group, dict):
117
  categories.extend(list(group.keys()))
118
 
119
- # Динамический сбор каталога оборудования (Вычисление AABB напрямую из мешей)
120
  equipment_lines: List[str] = []
121
  fixtures_cfg = asset_cfg_raw.get("fixtures", asset_cfg_raw.get("equipment", {}))
122
-
123
- # Достаем базовый путь к папке с ассетами
124
  base_assets_dir = asset_cfg_raw.get("assets_dir_path", "assets")
125
 
126
  for fix_id, fix_data in fixtures_cfg.items():
127
- if not isinstance(fix_data, dict):
128
- continue
129
-
130
  raw_path = fix_data.get("asset_file_path", "")
131
- if not raw_path:
132
- continue
133
-
134
- # Разрешаем интерполяцию вручную (меняем ${assets.assets_dir_path} на реальную папку)
135
  clean_path = raw_path.replace("${assets.assets_dir_path}", base_assets_dir)
136
  full_path = PROJECT_DIR / clean_path
137
-
138
  w, d = None, None
139
-
140
  try:
141
  import trimesh
142
- # Загружаем геометрию с диска.
143
- # force='mesh' гарантирует получение объекта с AABB
144
  scene = trimesh.load(str(full_path), force='mesh')
145
-
146
- # В 3D обычно: extents[0] = X (ширина), extents[1] = Y (высота), extents[2] = Z (глубина)
147
  w = round(float(scene.extents[0]), 2)
148
  d = round(float(scene.extents[1]), 2)
149
-
150
  except Exception as e:
151
- local_logger.debug(f"⚠️ Не удалось вычислить габариты для {fix_id} ({full_path}): {e}")
152
 
153
  if w is not None and d is not None:
154
- # Используем asset_name для описания или ID как запасной вариант
155
  desc: str = fix_data.get("asset_name", fix_id)
156
  equipment_lines.append(f"- {fix_id} ({desc}): {w}m x {d}m")
157
 
158
  equipment_catalog: str = "\n".join(equipment_lines)
159
 
160
- # Защита: если trimesh не установлен или модели не прочитались
161
  if not equipment_catalog:
162
- local_logger.warning("⚠️ Не удалось извлечь габариты геометрии, используем фолбэк.")
163
  equipment_catalog = (
164
  "- showcase_glb (Холодильник пристенный): 1.2m x 0.6m\n"
165
  "- shelf_metal (Стеллаж высокий): 1.0m x 0.4m\n"
166
  "- small_shelf_two_sided (Островок двусторонний): 1.2m x 0.8m"
167
  )
168
 
169
- # Инициализируем LLM клиент
170
  client = InventoryLLMClient(main_cfg)
171
-
172
- # Передаем динамический каталог оборудования и габариты комнаты в LLM
173
  result = client.generate_layout(
174
  user_request=user_prompt,
175
  available_categories=categories,
@@ -205,13 +197,11 @@ def generate_wrapper(user_prompt: str) -> Generator[Tuple[Optional[str], str], N
205
  local_logger.error(f"Ошибка воркера: {e}", exc_info=True)
206
  task_queue.put({"type": "error", "message": str(e)})
207
 
208
- # Запускаем наш поток
209
  thread = threading.Thread(target=worker_thread)
210
  thread.start()
211
 
212
  res_path: Optional[str] = None
213
 
214
- # Ждем только наш конкретный поток, а не все потоки веб-сервера
215
  while thread.is_alive() or not task_queue.empty():
216
  try:
217
  msg = task_queue.get(timeout=0.1)
@@ -220,22 +210,34 @@ def generate_wrapper(user_prompt: str) -> Generator[Tuple[Optional[str], str], N
220
  if upd.get("status") == "running" and "start_time" not in steps_state[idx]:
221
  steps_state[idx]["start_time"] = time.time()
222
  steps_state[idx].update(upd)
223
- yield None, format_checklist(steps_state)
224
  elif msg["type"] == "finish":
225
  res_path = msg["result_path"]
226
  elif msg["type"] == "error":
227
  raise gr.Error(msg["message"])
228
  except queue.Empty:
229
- yield None, format_checklist(steps_state)
230
 
231
- # Завершение и передача результата в интерфейс
232
  if res_path:
233
  final_text: str = format_checklist(steps_state)
234
  final_text += "\n" + "-"*40 + "\n"
235
  final_text += f"🏁 Бэкенд отработал успешно! Время: {time.time() - total_start:.1f} сек.\n"
236
- final_text += "⏳ Браузер скачивает геометрию. Идет отрисовка 3D-сцены..."
237
 
238
- # Надежно конвертируем путь для компонента Gradio
239
  absolute_model_path: str = str(Path(res_path).resolve())
240
- yield absolute_model_path, final_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
 
75
  lines.append(f"[❌] 🔴 {s['name']} - {s.get('message')}")
76
  return "\n".join(lines)
77
 
78
+ def generate_wrapper(user_prompt: str) -> Generator[
79
+ Tuple[
80
+ Optional[str], # 1. output_3d (Путь к GLB модели)
81
+ str, # 2. status_output (Текст чек-листа)
82
+ Optional[str], # 3. output_svg (Путь к картинке для превью 2D-плана)
83
+ Optional[str], # 4. draft_json_out (Путь к layout_draft.json)
84
+ Optional[str], # 5. final_json_out (Путь к layout_final.json)
85
+ Optional[str], # 6. svg_file_out (Путь к layout_final.svg для скачивания)
86
+ Optional[str], # 7. history_json_out (Путь к llm_history.jsonl)
87
+ Optional[str] # 8. draft_svg_file_out (Draft SVG)
88
+ ],
89
+ None,
90
+ None
91
+ ]:
92
  """Оркестратор пайплайна с передачей промпта в LLM и запуском 3D-движка."""
93
  tracker = setup_logger()
94
  local_logger = logging.getLogger(__name__)
95
 
96
+ # Чек-лист остается без изменений...
97
  steps_state: List[Dict[str, Any]] = [
98
  {"name": "ИИ-анализ и граф", "status": "pending"},
99
  {"name": "Инициализация", "status": "pending"},
 
104
  {"name": "Экспорт GLB (Draco)", "status": "pending"}
105
  ]
106
 
107
+ # Важно: yield теперь должен возвращать 8 элементов (None для всех файлов, пока идет генерация)
108
+ yield None, format_checklist(steps_state), None, None, None, None, None, None
109
+
110
  total_start: float = time.time()
111
  task_queue: queue.Queue = queue.Queue()
112
 
 
122
  task_queue.put({"type": "step_update", "idx": 0, "update": {"status": "running"}})
123
  llm_start: float = time.time()
124
 
 
 
125
  asset_cfg_raw = OmegaConf.to_container(asset_cfg, resolve=False)
126
 
 
127
  categories: List[str] = []
128
  for group in asset_cfg_raw.get("products_hierarchy", {}).values():
129
  if isinstance(group, dict):
130
  categories.extend(list(group.keys()))
131
 
 
132
  equipment_lines: List[str] = []
133
  fixtures_cfg = asset_cfg_raw.get("fixtures", asset_cfg_raw.get("equipment", {}))
 
 
134
  base_assets_dir = asset_cfg_raw.get("assets_dir_path", "assets")
135
 
136
  for fix_id, fix_data in fixtures_cfg.items():
137
+ if not isinstance(fix_data, dict): continue
 
 
138
  raw_path = fix_data.get("asset_file_path", "")
139
+ if not raw_path: continue
 
 
 
140
  clean_path = raw_path.replace("${assets.assets_dir_path}", base_assets_dir)
141
  full_path = PROJECT_DIR / clean_path
 
142
  w, d = None, None
 
143
  try:
144
  import trimesh
 
 
145
  scene = trimesh.load(str(full_path), force='mesh')
 
 
146
  w = round(float(scene.extents[0]), 2)
147
  d = round(float(scene.extents[1]), 2)
 
148
  except Exception as e:
149
+ local_logger.debug(f"⚠️ Не удалось вычислить габариты для {fix_id}: {e}")
150
 
151
  if w is not None and d is not None:
 
152
  desc: str = fix_data.get("asset_name", fix_id)
153
  equipment_lines.append(f"- {fix_id} ({desc}): {w}m x {d}m")
154
 
155
  equipment_catalog: str = "\n".join(equipment_lines)
156
 
 
157
  if not equipment_catalog:
 
158
  equipment_catalog = (
159
  "- showcase_glb (Холодильник пристенный): 1.2m x 0.6m\n"
160
  "- shelf_metal (Стеллаж высокий): 1.0m x 0.4m\n"
161
  "- small_shelf_two_sided (Островок двусторонний): 1.2m x 0.8m"
162
  )
163
 
 
164
  client = InventoryLLMClient(main_cfg)
 
 
165
  result = client.generate_layout(
166
  user_request=user_prompt,
167
  available_categories=categories,
 
197
  local_logger.error(f"Ошибка воркера: {e}", exc_info=True)
198
  task_queue.put({"type": "error", "message": str(e)})
199
 
 
200
  thread = threading.Thread(target=worker_thread)
201
  thread.start()
202
 
203
  res_path: Optional[str] = None
204
 
 
205
  while thread.is_alive() or not task_queue.empty():
206
  try:
207
  msg = task_queue.get(timeout=0.1)
 
210
  if upd.get("status") == "running" and "start_time" not in steps_state[idx]:
211
  steps_state[idx]["start_time"] = time.time()
212
  steps_state[idx].update(upd)
213
+ yield None, format_checklist(steps_state), None, None, None, None, None, None
214
  elif msg["type"] == "finish":
215
  res_path = msg["result_path"]
216
  elif msg["type"] == "error":
217
  raise gr.Error(msg["message"])
218
  except queue.Empty:
219
+ yield None, format_checklist(steps_state), None, None, None, None, None, None
220
 
 
221
  if res_path:
222
  final_text: str = format_checklist(steps_state)
223
  final_text += "\n" + "-"*40 + "\n"
224
  final_text += f"🏁 Бэкенд отработал успешно! Время: {time.time() - total_start:.1f} сек.\n"
225
+ final_text += "⏳ Браузер скачивает артефакты..."
226
 
 
227
  absolute_model_path: str = str(Path(res_path).resolve())
228
+
229
+ # === Обновленный сбор путей к артефактам (всё из папки logs) ===
230
+ logs_dir = PROJECT_DIR / "logs"
231
+
232
+ # Ищем JSON
233
+ draft_path: Optional[str] = str(logs_dir / "layout_draft.json") if (logs_dir / "layout_draft.json").exists() else None
234
+ final_path: Optional[str] = str(logs_dir / "layout_final.json") if (logs_dir / "layout_final.json").exists() else None
235
+ history_path: Optional[str] = str(logs_dir / "llm_history.jsonl") if (logs_dir / "llm_history.jsonl").exists() else None
236
+
237
+ # Ищем SVG тоже в logs
238
+ svg_path: Optional[str] = str(logs_dir / "layout_final.svg") if (logs_dir / "layout_final.svg").exists() else None
239
+ draft_svg_path: Optional[str] = str(logs_dir / "layout_draft.svg") if (logs_dir / "layout_draft.svg").exists() else None
240
+
241
+ # Возвращаем: 3D_модель, Текст, SVG_превью, Draft_JSON, Final_JSON, SVG_файл, History_JSONL, Draft_SVG
242
+ yield absolute_model_path, final_text, svg_path, draft_path, final_path, svg_path, history_path, draft_svg_path
243
 
src/ui/layout.py CHANGED
@@ -3,6 +3,7 @@
3
  """
4
  Модуль верстки пользовательского интерфейса Gradio.
5
  ИСПРАВЛЕНО: Удалена тема Soft для возврата стандартных шрифтов.
 
6
  """
7
  import gradio as gr
8
  from omegaconf import DictConfig
@@ -17,7 +18,6 @@ def create_ui(cfg: DictConfig) -> gr.Blocks:
17
  """
18
  Создает структуру веб-интерфейса.
19
  """
20
- # Удаляем theme=gr.themes.Soft() для возврата к стандартному виду
21
  with gr.Blocks(title="ИИ-Мерчандайзер 3D") as app:
22
  gr.Markdown("# 🏪 Интеллектуальный генератор 3D-планировок")
23
 
@@ -46,7 +46,7 @@ def create_ui(cfg: DictConfig) -> gr.Blocks:
46
  )
47
  generate_btn = gr.Button("🚀 Сгенерировать", variant="primary", scale=1)
48
 
49
- # 4. Область вывода результатов
50
  with gr.Row():
51
  with gr.Column(scale=1):
52
  status_output = gr.Textbox(
@@ -55,18 +55,46 @@ def create_ui(cfg: DictConfig) -> gr.Blocks:
55
  interactive=False,
56
  lines=15
57
  )
 
58
  with gr.Column(scale=3):
59
- output_3d = gr.Model3D(
60
- label="Результат 3D-рендера",
61
- clear_color=[0.9, 0.9, 0.9, 1.0],
62
- height=600
63
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- # Привязка событий
 
66
  generate_btn.click(
67
  fn=generate_wrapper,
68
  inputs=[prompt_input],
69
- outputs=[output_3d, status_output],
 
 
 
 
 
 
 
 
 
70
  show_progress="hidden"
71
  )
72
 
 
3
  """
4
  Модуль верстки пользовательского интерфейса Gradio.
5
  ИСПРАВЛЕНО: Удалена тема Soft для возврата стандартных шрифтов.
6
+ ДОБАВЛЕНО: Компоненты для просмотра 2D SVG-плана и скачивания JSON-артефактов.
7
  """
8
  import gradio as gr
9
  from omegaconf import DictConfig
 
18
  """
19
  Создает структуру веб-интерфейса.
20
  """
 
21
  with gr.Blocks(title="ИИ-Мерчандайзер 3D") as app:
22
  gr.Markdown("# 🏪 Интеллектуальный генератор 3D-планировок")
23
 
 
46
  )
47
  generate_btn = gr.Button("🚀 Сгенерировать", variant="primary", scale=1)
48
 
49
+ # 4. Область вывода результатов генерации
50
  with gr.Row():
51
  with gr.Column(scale=1):
52
  status_output = gr.Textbox(
 
55
  interactive=False,
56
  lines=15
57
  )
58
+
59
  with gr.Column(scale=3):
60
+ # Вкладки для переключения между 3D и 2D
61
+ with gr.Tabs():
62
+ with gr.TabItem("🎮 3D Сцена"):
63
+ output_3d = gr.Model3D(
64
+ label="Результат 3D-рендера",
65
+ clear_color=[0.9, 0.9, 0.9, 1.0],
66
+ height=600
67
+ )
68
+ with gr.TabItem("🗺️ 2D План (SVG)"):
69
+ output_svg = gr.Image(
70
+ label="Векторный план расстановки",
71
+ type="filepath",
72
+ interactive=False
73
+ )
74
+
75
+ # 5. Панель артефактов (для скачивания)
76
+ with gr.Row():
77
+ draft_json_out = gr.File(label="📥 Скачать черновик LLM (Draft JSON)")
78
+ final_json_out = gr.File(label="📥 Скачать финальный расчет (Final JSON)")
79
+ history_json_out = gr.File(label="📥 Скачать историю LLM (JSONL)")
80
+ draft_svg_file_out = gr.File(label="📥 Draft SVG")
81
+ svg_file_out = gr.File(label="📥 Скачать 2D План (SVG)")
82
 
83
+ # Привязка событий: теперь функция должна возвращать 5 значений (3D, Чеклист, SVG-превью, Draft, Final, SVG-файл)
84
+ # Так как SVG-файл и SVG-превью - это один и тот же файл, мы просто передадим его путь дважды в outputs
85
  generate_btn.click(
86
  fn=generate_wrapper,
87
  inputs=[prompt_input],
88
+ outputs=[
89
+ output_3d,
90
+ status_output,
91
+ output_svg,
92
+ draft_json_out,
93
+ final_json_out,
94
+ svg_file_out,
95
+ history_json_out,
96
+ draft_svg_file_out
97
+ ],
98
  show_progress="hidden"
99
  )
100