diff --git a/backend/main.py b/backend/main.py index c7f5431..3d0dc60 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1427,47 +1427,212 @@ async def _lancer_photostation(): FLAG_PHOTOSTATION.unlink(missing_ok=True) -# --- API Videos de situation --- +# --- API Videos didactiques (multi-etapes) --- DOSSIER_VIDEOS = RACINE / "data" / "videos" DOSSIER_VIDEOS.mkdir(parents=True, exist_ok=True) +FICHIER_VIDEOS_CONFIG = DOSSIER_VIDEOS / "config.json" + +SITUATIONS_DEFAUT = [ + {"id": "montage", "label": "Montage / Mise en service"}, + {"id": "demontage", "label": "Demontage"}, + {"id": "bourrage", "label": "Bourrage imprimante"}, + {"id": "changement_rouleau", "label": "Changement rouleau / ruban"}, + {"id": "wifi", "label": "Probleme WiFi"}, + {"id": "depannage_imprimante", "label": "Depannage imprimante"}, +] + +def charger_videos_config(): + if FICHIER_VIDEOS_CONFIG.exists(): + with open(FICHIER_VIDEOS_CONFIG) as f: + return json.load(f) + return {} + +def sauvegarder_videos_config(cfg): + with open(FICHIER_VIDEOS_CONFIG, "w") as f: + json.dump(cfg, f, indent=2, ensure_ascii=False) + +def _lister_etapes(situation: str): + dossier = DOSSIER_VIDEOS / situation + if not dossier.is_dir(): + return [] + fichiers = sorted( + [f for f in dossier.iterdir() if f.is_file() and f.suffix.lower() in ('.mp4', '.webm', '.mov')], + key=lambda f: f.name + ) + return [{"index": i, "fichier": f.name} for i, f in enumerate(fichiers)] + +def _migrer_anciens_videos(): + for f in DOSSIER_VIDEOS.iterdir(): + if f.is_file() and f.suffix.lower() in ('.mp4', '.webm', '.mov'): + situation = f.stem + dossier = DOSSIER_VIDEOS / situation + dossier.mkdir(exist_ok=True) + dest = dossier / f"etape_00{f.suffix}" + f.rename(dest) + +_migrer_anciens_videos() + @app.get("/api/videos") async def api_videos(): - videos = {} - for f in DOSSIER_VIDEOS.iterdir(): - if f.is_file() and f.suffix.lower() in ('.mp4', '.webm', '.mov'): - videos[f.stem] = f.name - return {"videos": videos} + cfg = charger_videos_config() + result = {} + for sit in SITUATIONS_DEFAUT: + sid = sit["id"] + etapes = _lister_etapes(sid) + sit_cfg = cfg.get(sid, {}) + result[sid] = { + "label": sit.get("label", sid), + "actif": sit_cfg.get("actif", False), + "etapes": etapes, + "labels_etapes": sit_cfg.get("labels_etapes", {}), + } + for sid in sorted(cfg.keys()): + if sid not in result: + etapes = _lister_etapes(sid) + result[sid] = { + "label": cfg[sid].get("label", sid), + "actif": cfg[sid].get("actif", False), + "etapes": etapes, + "labels_etapes": cfg[sid].get("labels_etapes", {}), + } + return {"situations": result} -@app.post("/api/videos/upload") -async def api_upload_video(video: UploadFile = File(...), situation: str = ""): - if not situation: - return JSONResponse({"erreur": "situation requise"}, 400) - ext = Path(video.filename).suffix.lower() or '.mp4' - dest = DOSSIER_VIDEOS / f"{situation}{ext}" - for old in DOSSIER_VIDEOS.glob(f"{situation}.*"): - old.unlink() - with open(dest, "wb") as f: - while chunk := await video.read(1024 * 1024): - f.write(chunk) - return {"ok": True, "fichier": dest.name} - - -@app.delete("/api/videos/{situation}") -async def api_delete_video(situation: str): - for f in DOSSIER_VIDEOS.glob(f"{situation}.*"): - f.unlink() +@app.post("/api/videos/{situation}/toggle") +async def api_toggle_situation(situation: str, req: Request): + body = await req.json() + cfg = charger_videos_config() + if situation not in cfg: + cfg[situation] = {} + cfg[situation]["actif"] = bool(body.get("actif", False)) + sauvegarder_videos_config(cfg) return {"ok": True} -@app.get("/api/videos/{situation}/stream") -async def api_stream_video(situation: str): - for f in DOSSIER_VIDEOS.glob(f"{situation}.*"): - media = "video/mp4" if f.suffix == ".mp4" else "video/webm" - return FileResponse(f, media_type=media) - return JSONResponse({"erreur": "video introuvable"}, 404) +@app.post("/api/videos/{situation}/upload") +async def api_upload_etape(situation: str, video: UploadFile = File(...)): + dossier = DOSSIER_VIDEOS / situation + dossier.mkdir(parents=True, exist_ok=True) + existantes = sorted(dossier.glob("etape_*")) + prochain = len(existantes) + ext = Path(video.filename).suffix.lower() or '.mp4' + dest = dossier / f"etape_{prochain:02d}{ext}" + with open(dest, "wb") as f: + while chunk := await video.read(1024 * 1024): + f.write(chunk) + return {"ok": True, "fichier": dest.name, "index": prochain} + + +@app.delete("/api/videos/{situation}/{index}") +async def api_delete_etape(situation: str, index: int): + dossier = DOSSIER_VIDEOS / situation + if not dossier.is_dir(): + return JSONResponse({"erreur": "situation introuvable"}, 404) + fichiers = sorted( + [f for f in dossier.iterdir() if f.is_file() and f.name.startswith("etape_")], + key=lambda f: f.name + ) + if index < 0 or index >= len(fichiers): + return JSONResponse({"erreur": "index invalide"}, 400) + fichiers[index].unlink() + for i, f in enumerate(sorted( + [f for f in dossier.iterdir() if f.is_file() and f.name.startswith("etape_")], + key=lambda f: f.name + )): + nouveau = dossier / f"etape_{i:02d}{f.suffix}" + if f != nouveau: + f.rename(nouveau) + cfg = charger_videos_config() + sit_cfg = cfg.get(situation, {}) + old_labels = sit_cfg.get("labels_etapes", {}) + new_labels = {} + for k, v in old_labels.items(): + ki = int(k) + if ki < index: + new_labels[str(ki)] = v + elif ki > index: + new_labels[str(ki - 1)] = v + if old_labels != new_labels: + sit_cfg["labels_etapes"] = new_labels + cfg[situation] = sit_cfg + sauvegarder_videos_config(cfg) + return {"ok": True} + + +@app.post("/api/videos/{situation}/reorder") +async def api_reorder_etapes(situation: str, req: Request): + body = await req.json() + old_idx = body.get("de") + new_idx = body.get("vers") + dossier = DOSSIER_VIDEOS / situation + if not dossier.is_dir(): + return JSONResponse({"erreur": "situation introuvable"}, 404) + fichiers = sorted( + [f for f in dossier.iterdir() if f.is_file() and f.name.startswith("etape_")], + key=lambda f: f.name + ) + if old_idx is None or new_idx is None or old_idx < 0 or old_idx >= len(fichiers): + return JSONResponse({"erreur": "index invalide"}, 400) + moved = fichiers.pop(old_idx) + fichiers.insert(new_idx, moved) + tmp_names = [] + for i, f in enumerate(fichiers): + tmp = dossier / f"_tmp_{i:02d}{f.suffix}" + f.rename(tmp) + tmp_names.append(tmp) + for i, tmp in enumerate(tmp_names): + ext = tmp.suffix + final = dossier / f"etape_{i:02d}{ext}" + tmp.rename(final) + return {"ok": True} + + +@app.post("/api/videos/{situation}/label") +async def api_label_etape(situation: str, req: Request): + body = await req.json() + index = body.get("index") + label = body.get("label", "") + cfg = charger_videos_config() + if situation not in cfg: + cfg[situation] = {} + labels = cfg[situation].setdefault("labels_etapes", {}) + labels[str(index)] = label + sauvegarder_videos_config(cfg) + return {"ok": True} + + +@app.get("/api/videos/{situation}/{index}/stream") +async def api_stream_etape(situation: str, index: int): + dossier = DOSSIER_VIDEOS / situation + if not dossier.is_dir(): + return JSONResponse({"erreur": "situation introuvable"}, 404) + fichiers = sorted( + [f for f in dossier.iterdir() if f.is_file() and f.name.startswith("etape_")], + key=lambda f: f.name + ) + if index < 0 or index >= len(fichiers): + return JSONResponse({"erreur": "index invalide"}, 404) + f = fichiers[index] + media = "video/mp4" if f.suffix == ".mp4" else "video/webm" + return FileResponse(f, media_type=media) + + +@app.post("/api/videos/situation/creer") +async def api_creer_situation(req: Request): + body = await req.json() + sid = body.get("id", "").strip().lower().replace(" ", "_") + label = body.get("label", sid) + if not sid: + return JSONResponse({"erreur": "id requis"}, 400) + cfg = charger_videos_config() + if sid not in cfg: + cfg[sid] = {"label": label, "actif": False, "labels_etapes": {}} + sauvegarder_videos_config(cfg) + dossier = DOSSIER_VIDEOS / sid + dossier.mkdir(parents=True, exist_ok=True) + return {"ok": True} # --- API Surprise --- diff --git a/frontend/index.html b/frontend/index.html index c638b11..232024d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -908,40 +908,21 @@ Appuyer sur Tester...
-

Videos de situation

-

Uploadez des videos pour chaque situation. Elles seront jouees automatiquement sur l'ecran du booth.

-
-
-

Montage / Installation

-
Aucune video
- - - -
-
-

Depannage imprimante

-
Aucune video
- - - -
-
-

Bourrage papier

-
Aucune video
- - - -
-
-

Rechargement papier/ruban

-
Aucune video
- - - +

Videos didactiques

+

Chaque situation peut contenir plusieurs etapes video. L'operateur verra les videos en sequence : chaque etape boucle jusqu'a validation avant de passer a la suivante.

+
+
+

Ajouter une situation

+
+ +

Apercu

+
+ +
@@ -981,6 +962,16 @@
+ + +