Impression Mitsubishi K60 + cadres + interface admin distante

- Intégration selphy_print (backend CUPS dyesub Mitsubishi CP-K60DW-S)
- Formats impression : 15x20, 10x15, 10x15 2-strips avec cadre et orientation portrait/paysage
- Cadres d'impression par format (strip/10x15/15x20) : upload, sélection, aperçu live
- Cadres démo générés : pellicule noir/vintage/couleurs + bordures classique/doré/rose gold
- Écran de choix de cadre avant capture (clic direct, aperçu grand format)
- Filtres réduits à 3 (Couleur, N&B, Sépia) et appliqués sur tous les strips
- Port 80 via authbind, route /admin avec redirection auto depuis poste distant
- Autologin LightDM corrigé (pam-autologin-service)
- Bouton évacuer bourrage papier dans admin
- Fix : _gp_lock manquant dans Camera.__init__

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 21:38:31 +02:00
parent 6451f5774c
commit e3d1d9cc07
32 changed files with 1009 additions and 84 deletions

View File

@@ -9,12 +9,13 @@ from contextlib import asynccontextmanager
import cv2
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, UploadFile, File
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from backend.config import (
RACINE, DOSSIER_PHOTOS, DOSSIER_EXPORTS, DOSSIER_OVERLAYS, DOSSIER_FONDS, DOSSIER_ANIMATIONS,
DOSSIER_CADRES, FORMATS_CADRES,
charger_config, sauvegarder_config, mettre_a_jour_config,
)
from backend.camera import camera, GPHOTO2_DISPONIBLE
@@ -23,7 +24,7 @@ try:
except ImportError:
gp = None
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, FILTRES
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, lister_cadres, FILTRES
from backend.collage import creer_strip, creer_collage, creer_impression_strip
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password
from backend.printer import lister_imprimantes, imprimer
@@ -150,8 +151,17 @@ app.mount("/data", StaticFiles(directory=str(RACINE / "data")), name="data")
# --- Pages ---
@app.get("/admin", include_in_schema=False)
async def page_admin():
return FileResponse(str(RACINE / "frontend" / "index.html"))
@app.get("/")
async def page_principale():
async def page_principale(request: Request):
from fastapi.responses import RedirectResponse
host = request.client.host if request.client else ""
if host not in ("127.0.0.1", "::1", "localhost"):
return RedirectResponse(url="/admin", status_code=302)
return FileResponse(str(RACINE / "frontend" / "index.html"))
@@ -454,10 +464,16 @@ async def api_supprimer_fond(nom: str):
@app.post("/api/strip")
async def api_strip(donnees: dict):
noms = donnees.get("photos", [])
chemins = [DOSSIER_PHOTOS / n for n in noms]
for c in chemins:
source = donnees.get("source", "photos")
dossier_src = DOSSIER_EXPORTS if source == "exports" else DOSSIER_PHOTOS
chemins = []
for n in noms:
c = dossier_src / n
if not c.exists():
return JSONResponse({"erreur": f"Photo introuvable : {c.name}"}, status_code=404)
c = DOSSIER_EXPORTS / n # fallback
if not c.exists():
return JSONResponse({"erreur": f"Photo introuvable : {n}"}, status_code=404)
chemins.append(c)
chemin_strip = creer_strip(chemins)
# Creer aussi la page d'impression (2 bandes sur 10x15 paysage)
chemin_print = creer_impression_strip(chemin_strip)
@@ -489,6 +505,17 @@ async def api_imprimantes():
return lister_imprimantes()
@app.post("/api/imprimante/evacuer")
async def api_evacuer_bourrage():
"""Annule tous les jobs et réactive l'imprimante pour débloquer un bourrage."""
import subprocess
config = charger_config()
nom = config.get("impression", {}).get("imprimante", "Mitsubishi")
subprocess.run(["sudo", "cancel", "-a", nom], capture_output=True)
subprocess.run(["sudo", "cupsenable", nom], capture_output=True)
return {"succes": True, "message": f"Jobs annulés, {nom} réactivée"}
@app.post("/api/imprimer")
async def api_imprimer(donnees: dict):
nom = donnees.get("photo", "")
@@ -503,10 +530,11 @@ async def api_imprimer(donnees: dict):
chemin = DOSSIER_PHOTOS / nom
if not chemin.exists():
return JSONResponse({"erreur": "Photo introuvable"}, status_code=404)
ok = imprimer(chemin, copies=copies)
if ok:
cadre_override = donnees.get("cadre") # cadre choisi en session, prioritaire sur le config
resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override)
if resultat.get("succes"):
distribuer_photo(chemin, imprimee=True)
return {"succes": ok}
return resultat
# --- API Booth (galerie live) ---
@@ -620,7 +648,7 @@ async def api_detecter_usb():
return detecter_usb()
# --- API Cadres actifs ---
# --- API Cadres actifs (overlays photo) ---
@app.get("/api/cadres")
async def api_cadres():
@@ -637,6 +665,60 @@ async def api_cadres_update(donnees: dict):
return {"actifs": actifs}
# --- API Cadres impression (par format) ---
@app.get("/api/cadres-impression/{format_papier}")
async def api_cadres_impression_list(format_papier: str):
if format_papier not in FORMATS_CADRES:
return JSONResponse({"erreur": "Format inconnu"}, status_code=400)
config = charger_config()
actif = config.get("impression", {}).get("cadres_actifs", {}).get(format_papier)
return {"format": format_papier, "disponibles": lister_cadres(format_papier), "actif": actif}
@app.post("/api/cadres-impression/{format_papier}/actif")
async def api_cadres_impression_set(format_papier: str, donnees: dict):
if format_papier not in FORMATS_CADRES:
return JSONResponse({"erreur": "Format inconnu"}, status_code=400)
nom = donnees.get("nom") # None = désactiver
config = charger_config()
cadres_actifs = config.get("impression", {}).get("cadres_actifs", {})
cadres_actifs[format_papier] = nom
mettre_a_jour_config({"impression": {"cadres_actifs": cadres_actifs}})
return {"format": format_papier, "actif": nom}
@app.post("/api/upload/cadre/{format_papier}")
async def api_upload_cadre(format_papier: str, fichier: UploadFile = File(...)):
if format_papier not in FORMATS_CADRES:
return JSONResponse({"erreur": "Format inconnu"}, status_code=400)
if not fichier.filename.lower().endswith(".png"):
return JSONResponse({"erreur": "Seuls les PNG sont acceptés"}, status_code=400)
dossier = DOSSIER_CADRES / format_papier
dossier.mkdir(parents=True, exist_ok=True)
chemin = dossier / fichier.filename
with open(chemin, "wb") as f:
f.write(await fichier.read())
return {"nom": fichier.filename, "format": format_papier}
@app.delete("/api/cadres-impression/{format_papier}/{nom}")
async def api_supprimer_cadre(format_papier: str, nom: str):
if format_papier not in FORMATS_CADRES:
return JSONResponse({"erreur": "Format inconnu"}, status_code=400)
chemin = DOSSIER_CADRES / format_papier / nom
if not chemin.exists() or chemin.parent != DOSSIER_CADRES / format_papier:
return JSONResponse({"erreur": "Fichier introuvable"}, status_code=404)
chemin.unlink()
# Désactiver si c'était le cadre actif
config = charger_config()
cadres_actifs = config.get("impression", {}).get("cadres_actifs", {})
if cadres_actifs.get(format_papier) == nom:
cadres_actifs[format_papier] = None
mettre_a_jour_config({"impression": {"cadres_actifs": cadres_actifs}})
return {"succes": True}
# --- API Animations compte a rebours ---
ANIMATIONS_CAR = {
@@ -769,7 +851,7 @@ if __name__ == "__main__":
uvicorn.run(
"backend.main:app",
host=conf_srv.get("host", "0.0.0.0"),
port=conf_srv.get("port", 8080),
port=conf_srv.get("port", 80),
reload=False,
log_level="info",
)