"""Pipeline orchestration — wraps the CAD skill modules. Mirrors step_processor.main() but writes artifacts into a per-model directory and returns structured metadata + BOM rows for the DB instead of printing a summary. Heavy kernel imports happen lazily inside each stage. """ import logging import math import os from pathlib import Path from typing import Callable, Optional from . import skill_bridge # noqa: F401 — sets sys.path so `modules.*` imports resolve log = logging.getLogger("step_parser.processing") Progress = Callable[[str], None] def _f(v) -> Optional[float]: try: x = float(v) return None if math.isnan(x) else round(x, 2) except (TypeError, ValueError): return None def _i(v) -> Optional[int]: try: x = float(v) return None if math.isnan(x) else int(x) except (TypeError, ValueError): return None def _s(v) -> Optional[str]: if v is None: return None s = str(v).strip() return s or None def _bom_df_to_rows(df) -> list[dict]: rows = [] for _, r in df.iterrows(): rows.append({ "part_number": _s(r.get("part_number")), "part_name_original": _s(r.get("part_name_original")), "part_name_english": _s(r.get("part_name_english")), "quantity": _i(r.get("quantity")), "level": _i(r.get("level")), "parent": _s(r.get("parent")), "bbox_x_mm": _f(r.get("bbox_x_mm")), "bbox_y_mm": _f(r.get("bbox_y_mm")), "bbox_z_mm": _f(r.get("bbox_z_mm")), "notes": _s(r.get("notes")), }) return rows def _collect_artifacts(out_dir: Path) -> list[str]: """All downloadable files in the model dir (original upload + generated outputs).""" return sorted(p.name for p in out_dir.iterdir() if p.is_file()) def run_pipeline(step_path: Path, out_dir: Path, options: dict, progress: Progress) -> dict: """Run the requested pipeline stages. Returns metadata dict for the DB.""" meta: dict = { "backend": None, "face_count": None, "part_count": None, "has_chinese": False, "bbox": (None, None, None), "bom_rows": [], "artifacts": [], } progress("loading") import modules.loader as loader_mod model = loader_mod.load_step(step_path) if model is None: raise RuntimeError("Failed to load STEP — build123d/FreeCAD unavailable or file invalid") meta["backend"] = model.backend meta["face_count"] = model.face_count meta["part_count"] = len(model.parts) if model.parts else None bom_df = None if options.get("bom", True): progress("bom") from modules.bom import extract_bom, save_bom_xlsx bom_df = extract_bom(model) save_bom_xlsx(bom_df, step_path) if bom_df is not None: from modules.translator import has_chinese meta["has_chinese"] = bool(bom_df["part_name_original"].apply(has_chinese).any()) if options.get("translate") and meta["has_chinese"]: if os.environ.get("ANTHROPIC_API_KEY"): progress("translate") from modules.translator import get_translation_map, translate_bom bom_df = translate_bom(bom_df, model_name=step_path.stem) save_bom_xlsx(bom_df, step_path) tmap = get_translation_map(bom_df) if tmap: from modules.rewriter import rewrite_step rewrite_step(step_path, tmap) else: log.warning("translate requested but ANTHROPIC_API_KEY not set — skipping") meta["bom_rows"] = _bom_df_to_rows(bom_df) try: root = bom_df[bom_df["level"] == 0] if len(root): r0 = root.iloc[0] meta["bbox"] = (_f(r0["bbox_x_mm"]), _f(r0["bbox_y_mm"]), _f(r0["bbox_z_mm"])) except Exception: pass if options.get("thumbnails", True): progress("thumbnails") from modules.renderer import render_views render_views(model, step_path) if options.get("diagram", False): progress("diagram") from modules.external_diagram import step_external_diagram step_external_diagram( path=str(step_path), mode=options.get("diagram_mode", "enclosure_only"), options={"pdf": bool(options.get("diagram_pdf", False))}, ) progress("collect") meta["artifacts"] = _collect_artifacts(out_dir) return meta def run_query(step_path: Path, query: str) -> str: """Load a model and run a single geometry query (synchronous).""" import modules.loader as loader_mod from modules.query_engine import run_query as _run_query model = loader_mod.load_step(step_path) if model is None: raise RuntimeError("Failed to load STEP for query") return _run_query(model, query)