File size: 12,423 Bytes
ce82348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05bfbba
 
 
 
 
 
 
 
 
 
 
 
 
 
ce82348
 
 
 
05bfbba
ce82348
 
 
 
 
 
 
 
 
 
05bfbba
 
 
ce82348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05bfbba
ce82348
05bfbba
ce82348
 
 
 
 
 
 
 
 
05bfbba
ce82348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05bfbba
ce82348
 
 
 
 
05bfbba
ce82348
 
 
 
 
05bfbba
ce82348
 
05bfbba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce82348
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# src/ui/callbacks.py

"""

Модуль обработчиков событий (Controller) для веб-интерфейса Gradio.

Отвечает за связывание пользовательского ввода с LLM-клиентом и 3D-пайплайном.

"""

import logging
import queue
import threading
import time
from pathlib import Path
from typing import Any, Dict, Generator, List, Optional, Tuple

import gradio as gr
from omegaconf import DictConfig, OmegaConf

from layout_generator.pipeline import generate_layout_stream
from llm.client import InventoryLLMClient
from utils.logger import setup_logger

logger: logging.Logger = logging.getLogger(__name__)
PROJECT_DIR: Path = Path(__file__).resolve().parent.parent.parent

def get_cache_status_text() -> str:
    """Формирует строку общей статистики для верхнего табло UI."""
    from layout_generator.assets import _ASSET_CACHE
    products: dict = _ASSET_CACHE.get('products', {})
    if not products:
        return "🔴 Кэш пуст. Запустите первую генерацию для прогрева сервера."
    
    categories: set = set()
    sku_count: int = 0
    for key in products.keys():
        if key.startswith('products_hierarchy.'): continue
        parts = key.split('.')
        if len(parts) >= 3:
            categories.add(parts[1].upper())
            sku_count += 1
            
    return (f"🟢 СЕРВЕР АКТИВЕН. Доступно SKU: {sku_count} | Категорий: {len(categories)}\n"
            f"Загружено оборудования: 4 модели (Холодильники и Стеллажи)")

def get_inventory_json() -> Dict[str, Any]:
    """Формирует дерево товаров для JSON-инспектора."""
    from layout_generator.assets import _ASSET_CACHE
    products: dict = _ASSET_CACHE.get('products', {})
    if not products: return {}
    
    tree: Dict[str, List[str]] = {}
    for key in products.keys():
        if key.startswith('products_hierarchy.'): continue
        parts = key.split('.')
        if len(parts) >= 3:
            cat = parts[1].upper()
            item = ".".join(parts[2:])
            if cat not in tree: tree[cat] = []
            if item not in tree[cat]: tree[cat].append(item)
            
    return {k: sorted(v) for k, v in sorted(tree.items())}

def format_checklist(steps_state: List[Dict[str, Any]]) -> str:
    """Генерирует текст чек-листа прогресса."""
    lines: List[str] = ["🚀 Процесс генерации...\n"]
    now: float = time.time()
    for s in steps_state:
        status: str = s["status"]
        if status == "pending": lines.append(f"[  ] ⚪ {s['name']}")
        elif status == "running":
            el = now - s.get("start_time", now)
            lines.append(f"[⏳] 🟡 {s['name']}... ({el:.1f} сек)")
        elif status == "done":
            lines.append(f"[✅] 🟢 {s['name']} ({s.get('time', 0):.1f} сек)")
        elif status == "error":
            lines.append(f"[❌] 🔴 {s['name']} - {s.get('message')}")
    return "\n".join(lines)

def generate_wrapper(user_prompt: str) -> Generator[
    Tuple[
        Optional[str],  # 1. output_3d (Путь к GLB модели)
        str,            # 2. status_output (Текст чек-листа)
        Optional[str],  # 3. output_svg (Путь к картинке для превью 2D-плана)
        Optional[str],  # 4. draft_json_out (Путь к layout_draft.json)
        Optional[str],  # 5. final_json_out (Путь к layout_final.json)
        Optional[str],  # 6. svg_file_out (Путь к layout_final.svg для скачивания)
        Optional[str],  # 7. history_json_out (Путь к llm_history.jsonl)
        Optional[str]   # 8. draft_svg_file_out (Draft SVG)
    ], 
    None, 
    None
]:
    """Оркестратор пайплайна с передачей промпта в LLM и запуском 3D-движка."""
    tracker = setup_logger()
    local_logger = logging.getLogger(__name__)

    # Чек-лист остается без изменений...
    steps_state: List[Dict[str, Any]] = [
        {"name": "ИИ-анализ и граф", "status": "pending"},
        {"name": "Инициализация", "status": "pending"},
        {"name": "Загрузка ассетов", "status": "pending"},
        {"name": "Расчет планограммы", "status": "pending"},
        {"name": "Генерация топологии", "status": "pending"},
        {"name": "Сборка 3D-сцены", "status": "pending"},
        {"name": "Экспорт GLB (Draco)", "status": "pending"}
    ]

    # Важно: yield теперь должен возвращать 8 элементов (None для всех файлов, пока идет генерация)
    yield None, format_checklist(steps_state), None, None, None, None, None, None
    
    total_start: float = time.time()
    task_queue: queue.Queue = queue.Queue()

    def worker_thread() -> None:
        try:
            main_cfg = OmegaConf.load(PROJECT_DIR / "configs" / "main_config.yaml")
            layout_cfg = OmegaConf.load(PROJECT_DIR / "configs" / "layout_config.yaml")
            asset_cfg = OmegaConf.load(PROJECT_DIR / "configs" / "asset_config.yaml")
            
            llm_graph: Optional[Dict[str, Any]] = None

            if user_prompt.strip():
                task_queue.put({"type": "step_update", "idx": 0, "update": {"status": "running"}})
                llm_start: float = time.time()
                
                asset_cfg_raw = OmegaConf.to_container(asset_cfg, resolve=False)
                
                categories: List[str] = []
                for group in asset_cfg_raw.get("products_hierarchy", {}).values():
                    if isinstance(group, dict):
                        categories.extend(list(group.keys()))
                
                equipment_lines: List[str] = []
                fixtures_cfg = asset_cfg_raw.get("fixtures", asset_cfg_raw.get("equipment", {})) 
                base_assets_dir = asset_cfg_raw.get("assets_dir_path", "assets")
                
                for fix_id, fix_data in fixtures_cfg.items():
                    if not isinstance(fix_data, dict): continue
                    raw_path = fix_data.get("asset_file_path", "")
                    if not raw_path: continue
                    clean_path = raw_path.replace("${assets.assets_dir_path}", base_assets_dir)
                    full_path = PROJECT_DIR / clean_path
                    w, d = None, None
                    try:
                        import trimesh
                        scene = trimesh.load(str(full_path), force='mesh')
                        w = round(float(scene.extents[0]), 2)
                        d = round(float(scene.extents[1]), 2)
                    except Exception as e:
                        local_logger.debug(f"⚠️ Не удалось вычислить габариты для {fix_id}: {e}")
                    
                    if w is not None and d is not None:
                        desc: str = fix_data.get("asset_name", fix_id)
                        equipment_lines.append(f"- {fix_id} ({desc}): {w}m x {d}m")
                
                equipment_catalog: str = "\n".join(equipment_lines)
                
                if not equipment_catalog:
                    equipment_catalog = (
                        "- showcase_glb (Холодильник пристенный): 1.2m x 0.6m\n"
                        "- shelf_metal (Стеллаж высокий): 1.0m x 0.4m\n"
                        "- small_shelf_two_sided (Островок двусторонний): 1.2m x 0.8m"
                    )

                client = InventoryLLMClient(main_cfg)
                result = client.generate_layout(
                    user_request=user_prompt, 
                    available_categories=categories,
                    equipment_catalog=equipment_catalog,
                    size_n=layout_cfg.layout.size_n,
                    size_m=layout_cfg.layout.size_m
                )                
                if result:
                    llm_graph = result
                    try:
                        from layout_generator.visualizer import draw_llm_draft_plan
                        draw_llm_draft_plan(llm_graph, PROJECT_DIR)
                    except Exception as ve:
                        local_logger.error(f"Визуализация не удалась: {ve}")
                
                task_queue.put({"type": "step_update", "idx": 0, "update": {"status": "done", "time": time.time() - llm_start}})
            else:
                task_queue.put({"type": "step_update", "idx": 0, "update": {"status": "done", "time": 0.0}})

            for res_path, update in generate_layout_stream(
                project_dir=PROJECT_DIR, 
                tracker=tracker, 
                size_n=layout_cfg.layout.size_n, 
                size_m=layout_cfg.layout.size_m,
                layout_graph=llm_graph
            ):
                if update and "step_idx" in update:
                    task_queue.put({"type": "step_update", "idx": update["step_idx"], "update": update})
                if res_path:
                    task_queue.put({"type": "finish", "result_path": res_path})
                    return
        except Exception as e:
            local_logger.error(f"Ошибка воркера: {e}", exc_info=True)
            task_queue.put({"type": "error", "message": str(e)})

    thread = threading.Thread(target=worker_thread)
    thread.start()

    res_path: Optional[str] = None
    
    while thread.is_alive() or not task_queue.empty():
        try:
            msg = task_queue.get(timeout=0.1)
            if msg["type"] == "step_update":
                idx, upd = msg["idx"], msg["update"]
                if upd.get("status") == "running" and "start_time" not in steps_state[idx]:
                    steps_state[idx]["start_time"] = time.time()
                steps_state[idx].update(upd)
                yield None, format_checklist(steps_state), None, None, None, None, None, None
            elif msg["type"] == "finish": 
                res_path = msg["result_path"]
            elif msg["type"] == "error": 
                raise gr.Error(msg["message"])
        except queue.Empty:
            yield None, format_checklist(steps_state), None, None, None, None, None, None

    if res_path:
        final_text: str = format_checklist(steps_state)
        final_text += "\n" + "-"*40 + "\n"
        final_text += f"🏁 Бэкенд отработал успешно! Время: {time.time() - total_start:.1f} сек.\n"
        final_text += "⏳ Браузер скачивает артефакты..."
        
        absolute_model_path: str = str(Path(res_path).resolve())
        
        # === Обновленный сбор путей к артефактам (всё из папки logs) ===
        logs_dir = PROJECT_DIR / "logs"
        
        # Ищем JSON
        draft_path: Optional[str] = str(logs_dir / "layout_draft.json") if (logs_dir / "layout_draft.json").exists() else None
        final_path: Optional[str] = str(logs_dir / "layout_final.json") if (logs_dir / "layout_final.json").exists() else None
        history_path: Optional[str] = str(logs_dir / "llm_history.jsonl") if (logs_dir / "llm_history.jsonl").exists() else None
        
        # Ищем SVG тоже в logs
        svg_path: Optional[str] = str(logs_dir / "layout_final.svg") if (logs_dir / "layout_final.svg").exists() else None
        draft_svg_path: Optional[str] = str(logs_dir / "layout_draft.svg") if (logs_dir / "layout_draft.svg").exists() else None
        
        # Возвращаем: 3D_модель, Текст, SVG_превью, Draft_JSON, Final_JSON, SVG_файл, History_JSONL, Draft_SVG
        yield absolute_model_path, final_text, svg_path, draft_path, final_path, svg_path, history_path, draft_svg_path