Interface locale : rtg interface (voyages, sources, journées, récit, génération)

Serveur web local sans dépendance nouvelle (http.server, Jinja2, htmx et
Leaflet fournis). Écrans : Mes voyages, nouveau voyage, un voyage (journées
avec carte, rattachement des traces sans heures à un jour avec suggestion
d'après les photos, sources avec choix de dossier, infos et récit,
génération avec avancement et arrêt), réglages du site et vérification
des outils.

Chaîne : jours imposés ([dates_traces] dans voyage.toml), arrêt propre de
la compression, suivi des étapes, état structuré (site.tableau_etat),
titre du voyage repris de voyage.toml sur l'accueil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
This commit is contained in:
FabriceandClaude Opus 5.5 committed 2026-10-09 14:07:47 +00:00
1 parent 608334b9d8
commit 06153a227e
37 files changed
+2618 -57

No files matched your search

+92 -28
View File
@@ -4,7 +4,7 @@ import os
from rtg import accueil, extraction, pages, photos, sources, video
from rtg.config import (SITE_TOML, VOYAGE_TOML, ecrire_toml, nom_lisible,
reglages_site, reglages_voyage)
from rtg.outils import dire, est_cache
from rtg.outils import dire, est_cache, verifier_arret
IGNORES = {"Scripts", "RoadTripsGenerator", "html", "assets", "videos", "photos", "photos_web",
"apercus", "rushs", "routes"}
@@ -46,19 +46,28 @@ def _sources_disponibles(voyage, liste):
return True
def generer_voyage(voyage, racine, site_cfg, compression=True, force=False):
def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suivi=None):
"""Toute la chaîne pour un voyage. `suivi(evenement)` (interface) reçoit
{"etape": …} au début de chaque étape et {"video": …} pendant la compression."""
cfg = reglages_voyage(voyage, site_cfg)
nom = os.path.relpath(voyage, racine)
dire("=" * 50)
dire(f"▶ {nom}")
dire("=" * 50)
def etape(libelle):
verifier_arret()
if suivi:
suivi({"etape": libelle, "voyage": nom})
etape("Sources")
a = sources.analyser(voyage, cfg)
for m in a.a_verifier:
dire(f"⚠️ {m}")
for r in sources.ranger(voyage, cfg, a):
dire(r)
etape("Positions")
extraction.extraire_photos(voyage, cfg, a)
if _sources_disponibles(voyage, cfg["sources"]["videos"]):
extraction.extraire_videos(voyage, a)
@@ -66,17 +75,24 @@ def generer_voyage(voyage, racine, site_cfg, compression=True, force=False):
dire("⏭️ Vidéos sources inaccessibles : positions reprises du dernier export.")
if compression and a.videos:
video.compresser(voyage, cfg, a, force=force)
etape("Vidéos")
progression = None
if suivi:
def progression(fichier, etat, pct, rang):
suivi({"video": fichier, "etat": etat, "pct": pct, "rang": rang})
video.compresser(voyage, cfg, a, force=force, progression=progression)
elif not compression:
dire("⏭️ Compression sautée.")
etape("Photos")
photos.preparer(voyage, cfg, force=force)
etape("Pages")
ok = pages.generer(voyage, racine, cfg["adresse_publique"])
dire()
return ok
def generer(racine, voyages=None, compression=True, force=False, mymaps_maj=False):
def generer(racine, voyages=None, compression=True, force=False, mymaps_maj=False, suivi=None):
racine = os.path.abspath(racine)
cfg = reglages_site(racine)
liste = [os.path.abspath(v) for v in voyages] if voyages else decouvrir(racine)
@@ -84,7 +100,10 @@ def generer(racine, voyages=None, compression=True, force=False, mymaps_maj=Fals
dire("❌ Aucun voyage trouvé (dossiers avec routes/, photos/, rushs/ ou voyage.toml).")
return False
dire(f"📁 {len(liste)} voyage(s) : {', '.join(os.path.relpath(v, racine) for v in liste)}\n")
echecs = [v for v in liste if not generer_voyage(v, racine, cfg, compression, force)]
echecs = [v for v in liste if not generer_voyage(v, racine, cfg, compression, force, suivi)]
verifier_arret()
if suivi:
suivi({"etape": "Accueil", "voyage": ""})
dire("=" * 50)
dire("▶ Accueil")
dire("=" * 50)
@@ -94,32 +113,58 @@ def generer(racine, voyages=None, compression=True, force=False, mymaps_maj=Fals
return not echecs
def etat(racine):
"""Tableau de l'avancement de chaque voyage."""
def rubrique_de(voyage, racine):
parent = os.path.dirname(os.path.abspath(voyage))
return "Voyages" if parent == os.path.abspath(racine) else nom_lisible(os.path.basename(parent))
def etat_voyage(v, racine, site_cfg):
"""Avancement d'un voyage (dictionnaire), sans rien lire de lourd."""
cfg = reglages_voyage(v, site_cfg)
a = sources.analyser(v, cfg, leger=True)
dossier = os.path.join(v, "videos")
faites_web = len([f for f in os.listdir(dossier) if f.endswith("_mobile.mp4")]) if os.path.isdir(dossier) else 0
if a.videos:
faites = sum(1 for s in a.videos if video.deja_faite(v, s, rapide=True))
vid = f"{faites}/{len(a.videos)}"
else:
faites = None
vid = f"{faites_web} (sources absentes)" if faites_web else "—"
ph = os.path.join(v, "photos")
nb_ph = len([f for f in os.listdir(ph) if f.lower().endswith((".jpg", ".jpeg"))]) if os.path.isdir(ph) else 0
pw = os.path.join(v, "photos_web")
nb_pw = len([f for f in os.listdir(pw) if not est_cache(f)]) if os.path.isdir(pw) else 0
index = os.path.join(v, "index.html")
genere = os.path.exists(index)
pages_ok = genere and all(
os.path.getmtime(index) >= os.path.getmtime(os.path.join(v, c))
for c in ("export_photos.csv", "export_videos.csv", "voyage.md")
if os.path.exists(os.path.join(v, c)))
jours = sorted(a.journees)
return {
"chemin": os.path.relpath(v, racine).replace(os.sep, "/"), "dossier": v,
"titre": cfg["titre"], "moto": cfg.get("moto", ""), "rubrique": rubrique_de(v, racine),
"debut": jours[0] if jours else None, "fin": jours[-1] if jours else None,
"jours": len(jours), "videos": vid, "videos_sources": len(a.videos),
"videos_faites": faites, "videos_web": faites_web,
"photos": nb_ph, "photos_web": nb_pw, "genere": genere, "pages_a_jour": pages_ok,
"a_verifier": a.a_verifier,
}
def tableau_etat(racine):
racine = os.path.abspath(racine)
site_cfg = reglages_site(racine)
return [etat_voyage(v, racine, site_cfg) for v in decouvrir(racine)]
def etat(racine):
"""Tableau de l'avancement de chaque voyage."""
lignes = [("Voyage", "Jours", "Vidéos web", "Photos web", "Pages", "À vérifier")]
for v in decouvrir(racine):
cfg = reglages_voyage(v, site_cfg)
a = sources.analyser(v, cfg, leger=True)
if a.videos:
faites = sum(1 for s in a.videos if video.deja_faite(v, s, rapide=True))
vid = f"{faites}/{len(a.videos)}"
else:
dossier = os.path.join(v, "videos")
n = len([f for f in os.listdir(dossier) if f.endswith("_mobile.mp4")]) if os.path.isdir(dossier) else 0
vid = f"{n} (sources absentes)" if n else "—"
ph = os.path.join(v, "photos")
nb_ph = len([f for f in os.listdir(ph) if f.lower().endswith((".jpg", ".jpeg"))]) if os.path.isdir(ph) else 0
pw = os.path.join(v, "photos_web")
nb_pw = len([f for f in os.listdir(pw) if not est_cache(f)]) if os.path.isdir(pw) else 0
index = os.path.join(v, "index.html")
pages_ok = "à jour" if os.path.exists(index) and all(
os.path.getmtime(index) >= os.path.getmtime(os.path.join(v, c))
for c in ("export_photos.csv", "export_videos.csv", "voyage.md")
if os.path.exists(os.path.join(v, c))) else "à refaire"
lignes.append((os.path.relpath(v, racine), str(len(a.journees)), vid,
f"{nb_pw}/{nb_ph}" if nb_ph else "—", pages_ok, str(len(a.a_verifier)) or "0"))
for e in tableau_etat(racine):
lignes.append((e["chemin"], str(e["jours"]), e["videos"],
f"{e['photos_web']}/{e['photos']}" if e["photos"] else "—",
"à jour" if e["pages_a_jour"] else "à refaire", str(len(e["a_verifier"]))))
largeurs = [max(len(l[i]) for l in lignes) for i in range(len(lignes[0]))]
for k, l in enumerate(lignes):
dire(" ".join(c.ljust(w) for c, w in zip(l, largeurs)))
@@ -143,6 +188,25 @@ def analyser(voyage, racine):
dire(f" ⚠️ {m}")
def creer_voyage(racine, dossier, titre, groupe="", moto=""):
"""Nouveau voyage : <racine>/[<groupe>/]<dossier>/voyage.toml, sans source.
Renvoie le chemin du dossier créé."""
for nom in (dossier, groupe):
if nom and (os.sep in nom or "/" in nom or nom.startswith((".", "_")) or nom in IGNORES):
raise ValueError(f"Nom de dossier refusé : {nom}")
chemin = os.path.join(racine, groupe, dossier) if groupe else os.path.join(racine, dossier)
if os.path.exists(chemin):
raise ValueError(f"Le dossier {os.path.relpath(chemin, racine)} existe déjà")
os.makedirs(chemin)
donnees = {"titre": titre or nom_lisible(dossier)}
if moto:
donnees["moto"] = moto
donnees["sources"] = {"traces": [], "videos": [], "photos": []}
ecrire_toml(os.path.join(chemin, VOYAGE_TOML), donnees,
entete="Voyage RoadTripsGenerator : sources, infos et réglages propres.")
return chemin
def migrer(racine):
"""Écrit site.toml et un voyage.toml par voyage à partir de l'existant
(mymaps.txt, moto.txt). Ne remplace jamais un fichier déjà présent."""