diff --git a/backend/services/converter.py b/backend/services/converter.py
new file mode 100644
index 0000000..48071c0
--- /dev/null
+++ b/backend/services/converter.py
@@ -0,0 +1,75 @@
+import os
+import json
+import subprocess
+from pathlib import Path
+from fastapi import HTTPException
+from config import EPT_DIR
+from services.manifest import save_manifest
+from utils.disk import get_entwine_path
+
+ENTWINE_PATH = get_entwine_path()
+ENTWINE_AVAILABLE = ENTWINE_PATH is not None
+
+def run_entwine(input_path: Path, out_dir: Path) -> dict:
+ if not ENTWINE_AVAILABLE:
+ raise HTTPException(status_code=500,
+ detail="entwine n'est pas installé ou introuvable dans le PATH")
+
+ out_dir.mkdir(parents=True, exist_ok=True)
+
+ # entwine build -i -o
+ cmd = [
+ ENTWINE_PATH, "build",
+ "-i", str(input_path.absolute()),
+ "-o", str(out_dir.absolute()),
+ ]
+
+ print(f"Exécution: {' '.join(cmd)}")
+ print(f"CMD: {cmd}")
+
+ proc = subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ check=False,
+ timeout=7200,
+ env=os.environ.copy()
+ )
+
+ if proc.returncode != 0:
+ raise HTTPException(status_code=500,
+ detail=f"entwine failed (code {proc.returncode}):\n{proc.stdout}")
+
+ result = _analyze_ept_output(out_dir, proc.stdout)
+ save_manifest(out_dir, result)
+ return result
+
+
+def _analyze_ept_output(out_dir: Path, entwine_output: str) -> dict:
+ """
+ Entwine produit un dossier EPT dont la structure est :
+ out_dir/
+ ept.json ← fichier d'entrée principal
+ ept-data/ ← tuiles binaires
+ ept-hierarchy/ ← hiérarchie des noeuds
+ """
+ ept_json = out_dir / "ept.json"
+
+ result = {
+ "format": "ept",
+ "entry_file": None,
+ "entry_type": None,
+ "stdout": entwine_output[-2000:],
+ }
+
+ if ept_json.exists():
+ result["entry_file"] = ept_json.relative_to(EPT_DIR).as_posix()
+ result["entry_type"] = "ept.json"
+ # Le dossier EPT = le dossier contenant ept.json
+ result["ept_dir"] = str(ept_json.parent.relative_to(EPT_DIR))
+ else:
+ raise HTTPException(status_code=500,
+ detail=f"entwine a terminé mais ept.json introuvable dans {out_dir}")
+
+ return result
\ No newline at end of file
diff --git a/backend/services/html_generator.py b/backend/services/html_generator.py
new file mode 100644
index 0000000..2924e0a
--- /dev/null
+++ b/backend/services/html_generator.py
@@ -0,0 +1,87 @@
+from pathlib import Path
+from typing import Optional
+from config import EPT_DIR, POTREE_URL
+import config
+
+def generate_viewer_html(pc_id: str, ept_dir: Optional[str],
+ embed: bool = False, potree_url: Optional[str] = None) -> str:
+ # Fallback : cherche ept.json si le manifest est absent
+ if not ept_dir:
+ out_dir = EPT_DIR / pc_id
+ ept_json = out_dir / "ept.json"
+ if ept_json.exists():
+ ept_dir = pc_id
+ else:
+ return "Erreur : ept.json introuvable pour cet ID
"
+
+ height_style = "100vh" if embed else "800px"
+ base_url = "/static/potree"
+ potree_url = potree_url or config.POTREE_URL
+
+ # L'URL vers ept.json servi via le montage statique /ept_data
+ ept_json_url = f"/ept_data/{ept_dir}/ept.json"
+
+ return f"""
+
+
+
+
+ EPT Viewer - {pc_id}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
\ No newline at end of file
diff --git a/backend/services/manifest.py b/backend/services/manifest.py
new file mode 100644
index 0000000..a2e652d
--- /dev/null
+++ b/backend/services/manifest.py
@@ -0,0 +1,24 @@
+import json
+import time
+from pathlib import Path
+
+def save_manifest(out_dir: Path, data: dict):
+ manifest = {
+ "conversion_time": time.time(),
+ "format": data.get("format", "ept"), # était "version"
+ "entry_file": data.get("entry_file"),
+ "entry_type": data.get("entry_type"),
+ "ept_dir": data.get("ept_dir"),
+ }
+ with open(out_dir / "manifest.json", 'w', encoding='utf-8') as f:
+ json.dump(manifest, f, indent=2)
+
+def read_manifest(out_dir: Path) -> dict:
+ manifest_file = out_dir / "manifest.json"
+ if not manifest_file.exists():
+ return {}
+ try:
+ with open(manifest_file, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ except:
+ return {}
\ No newline at end of file