Files
oto-enterprise-os-dtp/SPRINT_A_3D_EXTRACT_20260810.py
T
Claude Code DTP Worker 2a6e89dda1
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Intégrité mobile-build.yml (gating portable · activation différée · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / PIE · manifest de dépendances (Annexe 12 · V10.1) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
Sprint A · 3D extract complet · 9 projets · manifest global
2026-08-10 19:43:16 +00:00

263 lines
9.8 KiB
Python
Executable File

#!/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")