Spaces:
Paused
Paused
fix path in ui
Browse files- src/ui/callbacks.py +39 -37
- 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[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 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}
|
| 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 += "⏳ Браузер скачивает
|
| 237 |
|
| 238 |
-
# Надежно конвертируем путь для компонента Gradio
|
| 239 |
absolute_model_path: str = str(Path(res_path).resolve())
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
# Привязка событий
|
|
|
|
| 66 |
generate_btn.click(
|
| 67 |
fn=generate_wrapper,
|
| 68 |
inputs=[prompt_input],
|
| 69 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|