diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..05de3e7 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,72 @@ +Keyboard-interactive authentication prompts from server: +End of keyboard-interactive prompts from server +# cosma-qc — Context de reprise + +## TL;DR +Pipeline QC plongées COSMA : +1. ffmpeg extract frames (fps=2, 518x294) +2. auto-trim head+tail hors-eau (detection R` direct (pas de popup) + +## Infra +- **core (.82)** VM 101 Proxmox : FastAPI docker `cosma-qc` port 3849, SQLite `/home/floppyrj45/cosma-qc-data/jobs.db`, dispatcher Python systemd `cosma-qc-dispatcher.service` +- **gpu (.87)** VM 105 Proxmox : RTX 3060 12GB, user floppyrj45, `~/ai-video/lingbot-map` +- **ml-stack (.84)** host physique : RTX 3090 24GB, user root, `/root/ai-video/lingbot-map` +- **z620 (.168)** Proxmox hyperviseur : SSD thin pool à surveiller (a été 100% ? VMs paused) +- Source vidéos : z620:/mnt/portablessd/COSMA - La ctiotat 8 avril/raw_data/medias/videos/ + +## Credentials +- ssh floppyrj45@192.168.0.82 password SuperTeam2026! +- ssh gpu (alias ? floppyrj45@.87) + ssh ml-stack (alias ? root@.84), clés sur core +- ssh root@192.168.0.168 password SuperTeam2026! +- Gitea http://192.168.0.82:3000/floppyrj45/cosma-qc (token dans ~/cosma-qc-data/dispatcher.env) + +## Service dispatcher +- systemd : `sudo systemctl {status,restart,stop} cosma-qc-dispatcher` +- Env : `/home/floppyrj45/cosma-qc-data/dispatcher.env` (DB, WORKERS JSON, FPS) +- Log : `/home/floppyrj45/cosma-qc-data/dispatcher.log` (append) + `journalctl -u cosma-qc-dispatcher` +- Code : `/home/floppyrj45/docker/cosma-qc/scripts/dispatcher.py` (user floppyrj45) +- Backend FastAPI container : `docker restart cosma-qc` après edit `app/` + +## Patches deja appliques +- rm src_*.MP4 apres extract (thin pool LVM Proxmox tight) +- fstrim / +- stride adaptatif selon RAM worker (62/23 GB) +- estimate_vram_mib = 6000 MiB fixe +- budget RAM 0.35 (etait 0.55 trop optimiste ? OOM) +- auto-trim frames hors-eau (prefix + suffix) +- skip si <8 min video total (COSMA_QC_MIN_VIDEO_S=480) +- window_size adaptatif : 16/32/64 selon eff frames +- keep demo.py alive apres PLY saved ? viser natif persistant +- load balance pick_worker : lower-load first (sinon tout sur .84) +- set_status auto-clear error au status change +- skipped status propage dans stitch per_auv + +## TODO / issues connues +- [ ] Frame preview thumbnail : scp frame (fait), endpoint `/jobs/{id}/thumbnail` (fait). Backfill jobs anciens : manuel. +- [ ] CSS dashboard : layout à chier selon user. Passé a été cleane mais re-tester +- [ ] GLB export (lingbot_map/vis/glb_export.py existe, pas expose dans demo.py — patch demo.py pour ajouter `--save_glb`) +- [ ] Cross-AUV stitch final — code existe (stitch.py) mais pas encore testé +- [ ] segment_label trompeur (timestamp 1er MP4, pas durée totale). Fix dans ingest.py +- [ ] Dashboard header: dispatcher heartbeat last seen Xs ago peut afficher > 5s après restart si fichier heartbeat pas reset + +## Etat courant jobs (snapshot) +Jobs 9, 12, 13, 16, 19 done (anciens). 10 skipped (pull marron sur pont). 11/14 en cours extract. 15/17-21 queued. +Per-AUV stitch AUV209 nécessite jobs 11/12/13/14/15 done (10 exclu). AUV210 : 16/17/18/19/20/21. +Cross-AUV final = stitch id 6 (prédit) sur .84 port 9206. + +## Workflow attendu (user) +1. ffmpeg 2fps resize 518x294 + auto-trim hors-eau ? n frames +2. Dispatch multi-GPU balance (.84 + .87) +3. demo.py windowed 64 sur long (>3000 eff) +4. Save PLY (+ GLB TODO) +5. Viser natif via PointCloudViewer live +6. Stitch per-AUV puis cross-AUV = puzzle géant +7. Dashboard refresh avec thumbnail preview diff --git a/app/main.py b/app/main.py index 55640e7..478b11c 100644 --- a/app/main.py +++ b/app/main.py @@ -134,6 +134,34 @@ templates = Jinja2Templates(directory=Path(__file__).parent / "templates") app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static") +_viser_probe_cache: dict[str, tuple[float, bool]] = {} +_VISER_PROBE_TTL = 8.0 # seconds + + +def _viser_alive(url: str) -> bool: + """Fast TCP check with short cache so we never surface a dead link in the dashboard.""" + import time as _t + import socket + now = _t.time() + cached = _viser_probe_cache.get(url) + if cached and now - cached[0] < _VISER_PROBE_TTL: + return cached[1] + try: + from urllib.parse import urlparse + p = urlparse(url) + host, port = p.hostname, p.port + if not host or not port: + return False + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(0.4) + s.connect((host, port)) + alive = True + except OSError: + alive = False + _viser_probe_cache[url] = (now, alive) + return alive + + def _build_acquisitions(): with closing(db()) as conn: acqs = conn.execute( @@ -168,8 +196,9 @@ def _build_acquisitions(): d["video_duration_fmt"] = _fmt_dur(int(j["video_duration_s"] or 0)) if (j["video_duration_s"] or 0) > 0 else "—" d["trimmed_total"] = (j["trimmed_head"] or 0) + (j["trimmed_tail"] or 0) d["has_thumbnail"] = (DB_PATH.parent / "thumbnails" / f"job_{j['id']}.jpg").exists() - # Only expose a native viser link when port is listening. Probed on render via TCP check. - d["native_viser_url"] = None # filled below + # Mask the viser link when the demo.py that was serving it has since died. + if j["status"] == "done" and j["viser_url"] and not _viser_alive(j["viser_url"]): + d["viser_url"] = None by_acq.setdefault(j["acquisition_id"], []).append(d) by_acq_total[j["acquisition_id"]] = by_acq_total.get(j["acquisition_id"], 0) + dur_s diff --git a/app/static/style.css b/app/static/style.css index 46c4163..b7e53c3 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -70,8 +70,7 @@ progress::-moz-progress-bar { background: var(--accent); } .job-list .job-item.queued .icon { color: var(--muted); } .job-list .job-item.error .icon { background: var(--err); color: #fff; font-weight: bold; } -.job-list .label { color: var(--text); overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; display: flex; flex-direction: column; gap: 2px; } +.job-list .label { color: var(--text); display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; min-width: 0; } .prog-wrap { display: flex; align-items: center; gap: 4px; height: 10px; position: relative; width: 100%; max-width: 160px; } .prog-fill { display: block; height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.5s; min-width: 2px; } .prog-text { font-size: 0.7rem; color: var(--muted); white-space: nowrap; } @@ -116,16 +115,45 @@ button:hover { border-color: var(--accent); } a { color: var(--accent); } code { background: rgba(255,255,255,0.05); padding: 0 0.25rem; border-radius: 3px; } -/* Job row columns: id · AUV-GP · segment · meta · progress · viser */ -.job-item .label { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 14px; } -.job-item .job-id { font-family: monospace; color: var(--mut, #666); font-size: 12px; min-width: 32px; } -.job-item .auv-gp { font-weight: 600; min-width: 100px; } -.job-item .seg { color: var(--mut, #666); font-variant-numeric: tabular-nums; min-width: 90px; } -.job-item .meta { color: var(--mut, #888); font-size: 12px; font-variant-numeric: tabular-nums; } -.job-item .meta::before { content: '· '; opacity: 0.5; } -.job-item .viser-link { text-decoration: none; padding: 2px 8px; border: 1px solid var(--accent, #4a9); border-radius: 3px; color: var(--accent, #4a9); font-size: 12px; } -.job-item .viser-link:hover { background: var(--accent, #4a9); color: white; } -.job-item.skipped { opacity: 0.55; } -.job-item.skipped .label { font-style: italic; } +/* ==== Jobs table ==== */ +.acq { margin-bottom: 1.5rem; } +.acq-title { font-size: 1rem; margin: 0 0 0.5rem; } +.acq-title .total { color: var(--muted); font-size: 0.85rem; margin-left: 0.5rem; } -.job-item .thumb { width: 48px; height: 27px; object-fit: cover; border-radius: 3px; margin-right: 4px; } +.jobs-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } +.jobs-table thead th { text-align: left; padding: 4px 8px; color: var(--muted); font-weight: 500; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border, #333); } +.jobs-table tbody td { padding: 6px 8px; border-bottom: 1px solid rgba(255,255,255,0.04); vertical-align: middle; } +.jobs-table tbody tr:hover { background: rgba(255,255,255,0.02); } + +.col-status { width: 24px; text-align: center; } +.col-thumb { width: 80px; } +.col-thumb img { width: 72px; height: 40px; object-fit: cover; border-radius: 4px; display: block; } +.thumb-placeholder { display: inline-block; width: 72px; height: 40px; background: rgba(255,255,255,0.04); border-radius: 4px; } +.col-id { font-family: ui-monospace, monospace; color: var(--muted); font-size: 0.75rem; } +.col-auv { min-width: 110px; } +.col-seg { font-variant-numeric: tabular-nums; color: var(--muted); } +.col-dur, .col-frames, .col-trim, .col-elapsed { font-variant-numeric: tabular-nums; color: var(--muted); font-size: 0.8rem; } +.col-progress { min-width: 140px; } +.col-actions { width: 24px; text-align: right; } + +.badge { display: inline-block; width: 18px; height: 18px; line-height: 18px; text-align: center; border-radius: 3px; font-size: 0.72rem; font-weight: 600; } +.badge.ok { background: var(--ok, #2d7); color: #062410; } +.badge.busy { color: var(--accent, #4af); } +.badge.err { background: var(--err, #d44); color: #fff; } +.badge.muted { color: var(--muted, #888); } + +.job-row.skipped { opacity: 0.45; } +.job-row.skipped .col-seg, .job-row.skipped .col-auv { font-style: italic; } + +.prog-bar { display: inline-block; width: 100px; height: 6px; background: rgba(255,255,255,0.08); border-radius: 3px; overflow: hidden; vertical-align: middle; } +.prog-fill { display: block; height: 100%; background: var(--accent, #4af); transition: width 0.5s; } +.prog-text { margin-left: 6px; color: var(--muted); font-size: 0.72rem; } + +.btn-viser { display: inline-block; text-decoration: none; padding: 3px 10px; border: 1px solid var(--accent, #4af); border-radius: 3px; color: var(--accent, #4af); font-size: 0.72rem; background: transparent; cursor: pointer; font-family: inherit; } +.btn-viser:hover { background: var(--accent, #4af); color: #062036; } + +.stitch-section { margin-top: 0.75rem; padding: 0.5rem 0 0; border-top: 1px dashed var(--border, #333); } +.stitch-title { color: var(--muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.35rem; } +.stitch-list { list-style: none; padding: 0; margin: 0; } +.stitch-item { display: flex; align-items: center; gap: 0.5rem; padding: 3px 0; font-size: 0.82rem; } +.stitch-item .err-line { flex-basis: 100%; margin-left: 26px; color: var(--err, #d44); font-size: 0.72rem; } diff --git a/app/templates/_jobs_table.html b/app/templates/_jobs_table.html index 45a7925..5c5f5bc 100644 --- a/app/templates/_jobs_table.html +++ b/app/templates/_jobs_table.html @@ -1,89 +1,110 @@ {% if not acquisitions %}

Aucune acquisition. Ingeste un dossier via scripts/ingest.py.

{% else %} -
+
{% for acq in acquisitions %} -
+

{{ acq.name }} {{ acq.total_duration }}

-
    - {% for j in acq.jobs %} -
  • - - {% if j.status == 'done' %}✓ - {% elif j.status in ('running','extracting') %}↻ - {% elif j.status == 'error' %}✕ - {% else %}â– {% endif %} - - - {% if j.has_thumbnail %}{% endif %} - #{{ j.id }} - {{ j.auv }} {{ j.gp_label }} - {{ j.segment_label }} - {% if j.video_duration_fmt != '—' %}{{ j.video_duration_fmt }}{% if j.frame_count %} · {{ j.frame_count }} f{% if j.trimmed_total %} · −{{ j.trimmed_total }} hors-eau{% endif %}{% endif %}{% endif %} - {% if j.status in ('extracting','running') %} - {{ j.progress }}% - {% endif %} - {% if j.status == 'done' and j.viser_url %} - viser - {% endif %} - - {{ j._duration }} - {% if j.status in ('queued','extracting','running') %} - - {% elif j.status == 'error' %} - - {% else %} - - {% endif %} -
  • - {% if j.error %}
  • {{ j.error }}
  • {% endif %} - {% endfor %} -
+ + + + + + + + + + + + + + + + + + {% for j in acq.jobs %} + + + + + + + + + + + + + + {% endfor %} + +
preview#AUV · GPlabelduréeframeshors-eauprogressiontemps
+ {% if j.status == 'done' %}✓ + {% elif j.status in ('running','extracting') %}↻ + {% elif j.status == 'error' %}✕ + {% elif j.status == 'skipped' %}⊘ + {% else %}■{% endif %} + + {% if j.has_thumbnail %}{% else %}{% endif %} + #{{ j.id }}{{ j.auv }} · {{ j.gp_label }}{{ j.segment_label }}{{ j.video_duration_fmt }}{% if j.frame_count %}{{ j.frame_count }}{% else %}—{% endif %}{% if j.trimmed_total %}−{{ j.trimmed_total }}{% else %}—{% endif %} + {% if j.status in ('extracting','running') %} + + {{ j.progress }}% + {% elif j.status == 'done' and j.viser_url %} + viser ↗ + {% elif j.status == 'skipped' %} + skipped + {% elif j.status == 'error' %} + failed + {% else %} + — + {% endif %} + {{ j._duration }} + {% if j.status in ('queued','extracting','running') %} + + {% elif j.status == 'error' %} + + {% endif %} +
-
- â–  - stitch -
+
stitch
{% if acq.stitches %} -
    +
      {% for s in acq.stitches %} -
    • - - {% if s.status == 'done' %}✓ - {% elif s.status == 'running' %}↻ - {% elif s.status == 'error' %}✕ - {% else %}â– {% endif %} +
    • + + {% if s.status == 'done' %}✓{% elif s.status == 'running' %}↻{% elif s.status == 'error' %}✕{% else %}â– {% endif %} - + {% if s.level == 'per_auv' %}pair GP1↔GP2 {{ s.auv }} {% else %}merge final{% endif %} - {% if s._duration %} — {{ s._duration }}{% endif %} - {% if s.status == 'done' and s.output_ply %} - - {% endif %} + {% if s._duration %} · {{ s._duration }}{% endif %} + {% if s.status == 'done' and s.output_ply %} + + {% endif %} {% if s.status in ('queued','running') %} {% elif s.status == 'error' %} {% endif %} + {% if s.error %}
      {{ s.error[:140] }}
      {% endif %}
    • - {% if s.error %}
    • {{ s.error[:120] }}
    • {% endif %} {% endfor %}
    {% else %} -
      -
    • â–  pair GP1↔GP2 per AUV
    • -
    • â–  cross-AUV merge
    • -
    • â–  final PLY
    • +
        +
      • â–  pair GP1↔GP2 per AUV
      • +
      • â–  cross-AUV merge
      • +
      • â–  final PLY
      {% endif %}
-
+ {% endfor %}
{% endif %}