#!/usr/bin/env python3 """ OTOV7 · Sprint A · Extract 3D data from data_room archives ============================================================ Scanne /opt/oto/data_room/{Pxx}/ pour extraire toutes les données nécessaires à la génération 3D auto: - IFC BIM squelette (aec_generated) - Plans PDF préliminaires - Feasibility docs (typologies, superficies, prix, ambiance/matériaux) - Coordonnées géo (site) - Rendus existants (200+ dans archives) Output: /opt/oto/3d/data/{Pxx}_extracted.json /opt/oto/3d/data/manifest.json (global) Usage: python3 oto_3d_extract.py # tous les projets python3 oto_3d_extract.py P01 # un seul projet Author: OTOV7 Core · 2026-08-10 """ import os import sys import json import re from pathlib import Path from datetime import datetime try: import ifcopenshell HAS_IFC = True except ImportError: HAS_IFC = False try: import fitz # PyMuPDF HAS_PDF = True except ImportError: HAS_PDF = False DATA_ROOM = Path("/opt/oto/data_room") OUTPUT_DIR = Path("/opt/oto/3d/data") OUTPUT_DIR.mkdir(parents=True, exist_ok=True) # Metadata connue par projet (à enrichir avec les vraies faisabilités) PROJECT_META = { "P01": { "name": "Coralis", "developer": "HRD", "location": {"city": "Santo Domingo", "sector": "Bella Vista", "country": "RD"}, "clientele": "Jeunes cadres urbains 30-45", "palette": ["#0A0A0E", "#C9A465", "#FAF8F4", "#8B7355"], "materials": ["marbre carrare", "chêne massif", "laiton brossé", "verre trempé"], "typologies_typical": ["1C", "2C", "3C", "Penthouse"], "surface_m2": {"1C": 55, "2C": 85, "3C": 120, "Penthouse": 180}, }, "P02": { "name": "Coral del Sur", "developer": "Ristitullo", "location": {"city": "Santo Domingo", "sector": "Naco", "country": "RD"}, "clientele": "Familles internationales", "palette": ["#F5F0EB", "#B8860B", "#4A5568", "#E6D5B8"], "materials": ["travertin", "bois exotique", "acier noir", "béton lissé"], "typologies_typical": ["Studio", "1C", "2C", "3C"], "surface_m2": {"Studio": 42, "1C": 60, "2C": 90, "3C": 130}, }, "P03": { "name": "Nakua", "developer": "HRD", "location": {"city": "Santo Domingo", "sector": "Bella Vista", "country": "RD"}, "clientele": "Investisseurs internationaux", "palette": ["#1A1A2E", "#E94560", "#F5F5F5", "#0F3460"], "materials": ["granit noir", "acajou", "cuivre patiné", "verre fumé"], "typologies_typical": ["Studio", "1C", "2C", "3C", "Penthouse"], "surface_m2": {"Studio": 38, "1C": 58, "2C": 88, "3C": 128, "Penthouse": 220}, }, "P05": { "name": "Najayo", "developer": "HRD", "location": {"city": "San Cristóbal", "sector": "Najayo Arriba", "country": "RD"}, "clientele": "Retraités + secondaire côtière", "palette": ["#FFFFFF", "#4A90E2", "#87CEEB", "#F5DEB3"], "materials": ["pierre naturelle", "bois clair", "aluminium blanc", "béton chaux"], "typologies_typical": ["1C", "2C", "3C"], "surface_m2": {"1C": 65, "2C": 95, "3C": 135}, }, } def find_ifc(project_code): """Trouve le fichier IFC principal du projet.""" base = DATA_ROOM / project_code / "aec_generated" if not base.exists(): return None ifcs = list(base.rglob("*_complete.ifc")) return ifcs[0] if ifcs else None def parse_ifc(ifc_path): """Extract structure IFC (site, building, storeys, spaces).""" if not HAS_IFC or not ifc_path or not ifc_path.exists(): return {"error": "IFC not available"} try: ifc = ifcopenshell.open(str(ifc_path)) return { "file": str(ifc_path.relative_to(DATA_ROOM)), "size_bytes": ifc_path.stat().st_size, "schema": ifc.schema, "sites": [{"id": s.GlobalId, "name": s.Name} for s in ifc.by_type("IfcSite")], "buildings": [{"id": b.GlobalId, "name": b.Name} for b in ifc.by_type("IfcBuilding")], "storeys": [ {"id": s.GlobalId, "name": s.Name, "elevation": getattr(s, "Elevation", None)} for s in ifc.by_type("IfcBuildingStorey") ], "spaces_count": len(ifc.by_type("IfcSpace")), "walls_count": len(ifc.by_type("IfcWall")), "doors_count": len(ifc.by_type("IfcDoor")), "windows_count": len(ifc.by_type("IfcWindow")), } except Exception as e: return {"error": str(e)} def find_plans_pdfs(project_code): """Trouve les plans PDF (préliminaires + architectural).""" base = DATA_ROOM / project_code if not base.exists(): return [] plans = [] patterns = ["*planos*.pdf", "*PLANOS*.pdf", "*plans*.pdf", "*architect*.pdf", "HRD_*.pdf", "*preliminaires*.pdf"] for pat in patterns: plans.extend([str(p.relative_to(DATA_ROOM)) for p in base.rglob(pat)]) return sorted(set(plans)) def find_feasibility_docs(project_code): """Trouve les docs de faisabilité (JSON de préférence, sinon MD/PDF).""" base = DATA_ROOM / project_code if not base.exists(): return {} docs = { "json_files": [str(p.relative_to(DATA_ROOM)) for p in base.rglob("*.json")][:30], "md_files": [str(p.relative_to(DATA_ROOM)) for p in base.rglob("*.md")][:20], "financier_pdfs": [str(p.relative_to(DATA_ROOM)) for p in (base / "50_financier_bancable").glob("*.pdf")] if (base / "50_financier_bancable").exists() else [], } return docs def find_existing_renders(project_code): """Trouve rendus existants (PNG/JPG/WebP) dans archives.""" base = DATA_ROOM / project_code if not base.exists(): return [] renders = [] for ext in ["png", "jpg", "jpeg", "webp"]: renders.extend([str(p.relative_to(DATA_ROOM)) for p in base.rglob(f"*.{ext}")]) return sorted(set(renders))[:100] def extract_feasibility_units(project_code): """Parse les JSON de faisabilité pour trouver les données d'unités.""" base = DATA_ROOM / project_code if not base.exists(): return {} units_data = {} for jf in list(base.rglob("*unites*.json")) + list(base.rglob("*units*.json")): try: data = json.loads(jf.read_text(encoding="utf-8")) units_data[str(jf.relative_to(DATA_ROOM))] = data except Exception: pass for jf in list(base.rglob("*typolog*.json")): try: data = json.loads(jf.read_text(encoding="utf-8")) units_data.setdefault("typologies", {})[str(jf.relative_to(DATA_ROOM))] = data except Exception: pass return units_data def extract_project(project_code): """Extract complet d'un projet.""" print(f"\n=== Extract {project_code} ===") ifc_path = find_ifc(project_code) meta = PROJECT_META.get(project_code[:3], {}) data = { "project_code": project_code, "meta": meta, "extracted_at": datetime.utcnow().isoformat() + "Z", "ifc": parse_ifc(ifc_path) if ifc_path else {"error": "no IFC found"}, "plans_pdfs": find_plans_pdfs(project_code), "feasibility_docs": find_feasibility_docs(project_code), "existing_renders": find_existing_renders(project_code), "units_from_feasibility": extract_feasibility_units(project_code), } # Stats data["stats"] = { "ifc_available": bool(ifc_path), "ifc_storeys": len(data["ifc"].get("storeys", [])), "plans_pdf_count": len(data["plans_pdfs"]), "feasibility_json_count": len(data["feasibility_docs"].get("json_files", [])), "existing_renders_count": len(data["existing_renders"]), } print(f" IFC: {'✓' if data['stats']['ifc_available'] else '✗'} · Storeys: {data['stats']['ifc_storeys']}") print(f" Plans PDF: {data['stats']['plans_pdf_count']}") print(f" Feasibility JSON: {data['stats']['feasibility_json_count']}") print(f" Renders existants: {data['stats']['existing_renders_count']}") out_file = OUTPUT_DIR / f"{project_code}_extracted.json" out_file.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") print(f" → {out_file}") return data def build_manifest(all_extracts): """Manifest global pour le viewer frontend.""" manifest = { "generated_at": datetime.utcnow().isoformat() + "Z", "projects": {}, } for code, data in all_extracts.items(): meta = data.get("meta", {}) manifest["projects"][code] = { "name": meta.get("name", code), "location": meta.get("location", {}), "clientele": meta.get("clientele", ""), "storeys": data["stats"]["ifc_storeys"], "renders_available": data["stats"]["existing_renders_count"], "ifc_ready": data["stats"]["ifc_available"], "typologies": meta.get("typologies_typical", []), "extract_path": f"{code}_extracted.json", } (OUTPUT_DIR / "manifest.json").write_text( json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8" ) print(f"\n=== Manifest global → {OUTPUT_DIR}/manifest.json ===") return manifest if __name__ == "__main__": if len(sys.argv) > 1: project = sys.argv[1] projects = [project] else: # Auto-detect all P0X folders in data_room projects = sorted([p.name for p in DATA_ROOM.iterdir() if p.is_dir() and re.match(r"^P\d{2}", p.name)]) print(f"Projects trouvés: {projects}") all_extracts = {} for p in projects: try: all_extracts[p] = extract_project(p) except Exception as e: print(f" ERROR {p}: {e}") build_manifest(all_extracts) print(f"\n✅ Sprint A terminé · {len(all_extracts)} projets extraits")