Robustesse DSLR + éclairage visage + fix pellicule cadre imposé

- camera.py: fix fuite FD USB sur échec reconnexion (gp.Camera non libéré),
  cleanup PTP session avant init, catch toutes exceptions preview/capture,
  deconnecter() avec timeout thread + force del pour libérer le port USB
- main.py: thread preview anti-crash (outer try/except), reconnexion DSLR
  même si preview actif, notification camera_erreur après échec capture,
  intégration module éclairage (analyse luminosité visage via WS)
- printer.py: mode pellicule (-2up) ignoré par le cadre imposé événement
- eclairage.py: nouveau module analyse luminosité visage (Haar + HSV)
- camera.js: indicateur éclairage temps réel (score/100 + visages)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 19:11:10 +02:00
parent 182042350d
commit 48f54ab70a
5 changed files with 219 additions and 26 deletions

View File

@@ -36,6 +36,7 @@ from backend.evenements import (
lister_cadres_event, set_cadre_event, cadre_actif_pour_impression,
)
from backend.evenements import DOSSIER_EVENEMENTS
from backend.eclairage import analyser_frame as analyser_eclairage, dernier_resultat as dernier_eclairage
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
log = logging.getLogger("photobooth")
@@ -63,17 +64,21 @@ def _thread_preview():
Tourne TOUJOURS quand le DSLR est connecte pour maintenir le miroir leve."""
global _derniere_frame_preview
while True:
if capture_en_cours or camera.mode != "gphoto2" or not camera.connectee:
time.sleep(0.05)
continue
try:
donnees = camera.preview()
with _preview_lock:
_derniere_frame_preview = donnees
except Exception:
with _preview_lock:
_derniere_frame_preview = None
time.sleep(0.033) # ~30 fps max
if capture_en_cours or camera.mode != "gphoto2" or not camera.connectee:
time.sleep(0.05)
continue
try:
donnees = camera.preview()
with _preview_lock:
_derniere_frame_preview = donnees
except Exception:
with _preview_lock:
_derniere_frame_preview = None
time.sleep(0.033) # ~30 fps max
except Exception as e:
log.error(f"_thread_preview crash: {e}")
time.sleep(1)
async def _pusher_preview():
@@ -103,6 +108,10 @@ async def _pusher_preview():
loop = asyncio.get_event_loop()
b64 = await loop.run_in_executor(None, lambda d=donnees: base64.b64encode(d).decode("ascii"))
await diffuser_ws({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"})
if not capture_en_cours:
eclairage = await loop.run_in_executor(None, analyser_eclairage, donnees)
if eclairage is not None:
await diffuser_ws({"type": "eclairage", **eclairage})
def _usb_reset_canon():
"""Reset USB ioctl du Canon EOS (vendor 04a9) pour débloquer un port stall."""
@@ -206,7 +215,7 @@ async def surveiller_dslr():
_echecs_connexion = 0
else:
_dslr_erreurs += 1
if _dslr_erreurs >= 3 and not _preview_actif:
if _dslr_erreurs >= 3:
log.warning(f"camera_erreur : DSLR preview KO depuis {_dslr_erreurs * 5}s, reconnexion forcee...")
_dslr_erreurs = 0
_echecs_connexion += 1
@@ -409,6 +418,9 @@ async def api_capturer():
capture_en_cours = False
log.info(f"Capture terminée : {chemin}")
if chemin is None:
if camera.mode == "gphoto2" and not camera.preview_dslr_ok:
log.warning("Capture echouee + preview KO → reconnexion DSLR imminente")
await diffuser_ws({"type": "camera_erreur", "message": "Erreur capture — reconnexion en cours"})
return JSONResponse({"erreur": "Echec capture"}, status_code=500)
nom = chemin.name
@@ -547,6 +559,14 @@ async def api_camera_statut():
}
@app.get("/api/eclairage")
async def api_eclairage():
resultat = dernier_eclairage()
if resultat is None:
return {"score": -1, "action": "no_data", "detail": "Aucune analyse disponible"}
return resultat
@app.get("/api/camera/debug")
async def api_camera_debug():
"""Diagnostic complet de la caméra (preview, viewfinder, erreurs gphoto2)."""