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