Sprint A · 3D extract complet · 9 projets · manifest global
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
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
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# OTO 3D Studio · Pipeline complet (open-source, zero external dependency)
|
||||||
|
**Directive Michel · 2026-08-10**
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
Construire une expérience 3D virtuelle mieux que PlanPoint, à partir des archives existantes.
|
||||||
|
100% open-source, zéro dépendance externe (pas d'attente architectes, pas de studio 3D humain).
|
||||||
|
|
||||||
|
## Sprints
|
||||||
|
- **A · Extract** ✅ LIVE (oto_3d_extract.py, 9 projets extraits, manifest.json généré)
|
||||||
|
- **B · Génération 3D auto Blender** ← next (convertir IFC → GLB détaillé)
|
||||||
|
- **C · Batch renders Flux via RunPod** (~48 scènes photorealistes)
|
||||||
|
- **D · Panoramas 360° Blender Cycles** (~24 panoramas)
|
||||||
|
- **E · Refonte /choisir-mon-unite-3d/ avec checkout + websocket** live
|
||||||
|
|
||||||
|
## Data disponible après Sprint A
|
||||||
|
- 5 projets avec IFC BIM squelette (P01, P02, P03, P05×2)
|
||||||
|
- 4 projets sans IFC (P04, P06-P09) — fallback GLB placeholder + PDF plans
|
||||||
|
- 100+ renders existants dans archives (à intégrer, pas à regénérer)
|
||||||
|
- Palette + matériaux + clientèle cible codés par projet
|
||||||
|
- Coordonnées géo + secteur pour chaque projet
|
||||||
|
|
||||||
|
Voir: /opt/oto/3d/data/manifest.json
|
||||||
Executable
+262
@@ -0,0 +1,262 @@
|
|||||||
|
#!/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")
|
||||||
Reference in New Issue
Block a user