viewer on-demand — relancer viser à la demande depuis le dashboard

Le viser de demo.py était tué dès que le PLY était écrit (pour libérer la VRAM),
donc les liens dans le dashboard menaient vers ERR_CONNECTION_REFUSED.

Ajout d'un viewer standalone indépendant :
- scripts/viser_ply.py : charge un PLY via open3d + sert via viser (sans GPU),
  subsample random à 2M pts max pour rester fluide
- app/main.py : routes POST /jobs/{id}/view et /stitches/{id}/view qui scp
  le script sur le worker et lancent un viser détaché (nohup+setsid+disown via
  wrapper shell déposé sur le worker)
- templates : remplace <a href> par <button class=viewer-btn> qui POST puis
  window.open de l'URL retournée
- Dockerfile : copie scripts/ dans l'image (nécessaire pour scp-er viser_ply.py)
This commit is contained in:
Poulpe
2026-04-21 13:09:48 +00:00
parent 6ac3a382c7
commit 468f9084ec
6 changed files with 159 additions and 3 deletions

View File

@@ -310,3 +310,81 @@ async def retry_stitch(stitch_id: int):
(stitch_id,),
)
return {"ok": True}
VIEWER_PORT_BASE = 8200
VIEWER_SCRIPT_REMOTE = "/tmp/cosma-viser_ply.py"
def _worker_by_host(host: str) -> dict | None:
for w in WORKERS:
if w["host"] == host:
return w
return WORKERS[0] if WORKERS else None
async def _launch_viewer(worker: dict, ply_path: str, port: int) -> None:
alias = worker["ssh_alias"]
local_script = Path(__file__).parent.parent / "scripts" / "viser_ply.py"
proc = await asyncio.create_subprocess_exec(
"scp", "-o", "BatchMode=yes", str(local_script), f"{alias}:{VIEWER_SCRIPT_REMOTE}",
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE,
)
_, err = await proc.communicate()
if proc.returncode != 0:
raise HTTPException(500, f"scp viser_ply.py failed: {err.decode()[:200]}")
wrapper = (
f"#!/bin/bash\n"
f"pkill -f 'viser_ply.py.*--port {port}' 2>/dev/null\n"
f"sleep 1\n"
f"cd /home/floppyrj45/ai-video/lingbot-map\n"
f"source .venv/bin/activate\n"
f"exec python3 {VIEWER_SCRIPT_REMOTE} {ply_path!r} --port {port} --downsample 0\n"
)
wrapper_path = f"/tmp/cosma-viser-launch-{port}.sh"
proc = await asyncio.create_subprocess_exec(
"ssh", "-o", "BatchMode=yes", alias,
f"cat > {wrapper_path} && chmod +x {wrapper_path}",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE,
)
_, err = await proc.communicate(wrapper.encode())
if proc.returncode != 0:
raise HTTPException(500, f"wrapper write failed: {err.decode()[:200]}")
proc = await asyncio.create_subprocess_exec(
"ssh", "-o", "BatchMode=yes", alias,
f"setsid nohup {wrapper_path} </dev/null >/tmp/cosma-viser-{port}.log 2>&1 & disown; sleep 0.3",
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE,
)
await proc.communicate()
await asyncio.sleep(5)
@app.post("/jobs/{job_id}/view")
async def view_job(job_id: int):
with closing(db()) as conn:
row = conn.execute(
"SELECT ply_path, worker_host FROM jobs WHERE id=? AND status='done'",
(job_id,),
).fetchone()
if not row or not row["ply_path"]:
raise HTTPException(404, "PLY non disponible")
worker = _worker_by_host(row["worker_host"]) or WORKERS[0]
port = VIEWER_PORT_BASE + job_id
await _launch_viewer(worker, row["ply_path"], port)
return {"url": f"http://{worker['host']}:{port}"}
@app.post("/stitches/{stitch_id}/view")
async def view_stitch(stitch_id: int):
with closing(db()) as conn:
row = conn.execute(
"SELECT output_ply, worker_host FROM stitches WHERE id=? AND status='done'",
(stitch_id,),
).fetchone()
if not row or not row["output_ply"]:
raise HTTPException(404, "PLY stitch non disponible")
worker = _worker_by_host(row["worker_host"]) or WORKERS[0]
port = VIEWER_PORT_BASE + 1000 + stitch_id
await _launch_viewer(worker, row["output_ply"], port)
return {"url": f"http://{worker['host']}:{port}"}