Files
RoadTripsGenerator/rtg/config.py
T
FabriceandClaude Opus 5.5 f0e1c2dfce Phase 1 : moteur Python et commande rtg
- Paquet roadtripsgenerator (pyproject.toml), commande rtg :
  generer, etat, analyser, compresser, accueil, migrer
- Réglages site.toml / voyage.toml (anciens mymaps.txt et moto.txt
  encore lus) ; qualité vidéo en trois crans
- Sources laissées en place : traces découpées par journée d'après
  leurs horodatages (coupure à 4 h) ou la date du nom de fichier,
  photos copiées, HEIC converties en JPEG, position déduite de la
  trace pour les photos sans GPS
- extraction_infos.sh et compress.sh portés en Python (plus de bash) ;
  sorties vidéo écrites sous un nom temporaire ; positions des rushs
  inchangés reprises du dernier export ; export vidéo conservé quand
  le disque des rushs est débranché
- Pages, accueil et carte globale repris dans le paquet : sortie
  identique aux scripts (123 fichiers comparés sur 7 voyages)
- Tests pytest sur un voyage d'exemple fabriqué à la volée

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
2026-10-09 11:31:31 +00:00

148 lines
5.1 KiB
Python

"""Réglages du site (site.toml) et des voyages (voyage.toml).
Un réglage absent d'un voyage prend la valeur du site, puis la valeur par
défaut ci-dessous. Les anciens fichiers (mymaps.txt, moto.txt) sont encore lus
quand les nouveaux n'existent pas ; `rtg migrer` écrit les nouveaux.
"""
import copy
import os
import re
try:
import tomllib
except ModuleNotFoundError: # Python 3.10
import tomli as tomllib
SITE_TOML = "site.toml"
VOYAGE_TOML = "voyage.toml"
DEFAUTS = {
"titre": "Nos Road-Trips",
"adresse_publique": "https://roadtrip.ouaibe.fr/",
"accueil_nb_recents": 2,
"publication": {"mode": "sur_place", "destination": "", "adresse_videos": ""},
"video": {
"qualite": "equilibree", # econome | equilibree | haute
"encodeur": "processeur", # processeur | materiel
"preset": "medium",
"fps_max": 60,
"jobs": 0, # 0 = automatique
"threads": 0,
},
"photos": {"grand_cote": 1600, "qualite": 80, "positions_deduites": True},
"journees": {"fuseau": "Europe/Paris", "coupure_heure": 4},
"carte_globale": {"mymaps": []},
# Propres à un voyage
"rubrique": "",
"moto": "",
"sources": {"traces": ["routes"], "videos": ["rushs"], "photos": ["photos"]},
"exclusions": {"fichiers": []},
}
# Qualité « humaine » → paramètres d'encodeur. « equilibree » = réglages
# historiques de compress.sh (CRF 26 HEVC / 25 H.264).
QUALITES = {
"econome": {"crf_hevc": 28, "crf_h264": 27, "vtq_hd": 55, "vtq_sd": 53},
"equilibree": {"crf_hevc": 26, "crf_h264": 25, "vtq_hd": 60, "vtq_sd": 58},
"haute": {"crf_hevc": 23, "crf_h264": 22, "vtq_hd": 68, "vtq_sd": 65},
}
def fusion(base, ajout):
"""Fusion récursive : les valeurs de `ajout` l'emportent."""
r = copy.deepcopy(base)
for k, v in (ajout or {}).items():
if isinstance(v, dict) and isinstance(r.get(k), dict):
r[k] = fusion(r[k], v)
else:
r[k] = copy.deepcopy(v)
return r
def lire_toml(chemin):
if not os.path.exists(chemin):
return {}
with open(chemin, "rb") as f:
return tomllib.load(f)
def trouver_racine(depart="."):
"""Premier dossier, en remontant depuis `depart`, qui contient site.toml ;
à défaut `depart` lui-même."""
d = os.path.abspath(depart)
while True:
if os.path.exists(os.path.join(d, SITE_TOML)):
return d
parent = os.path.dirname(d)
if parent == d:
return os.path.abspath(depart)
d = parent
def _mymaps_ancien(racine):
f = os.path.join(racine, "mymaps.txt")
if not os.path.exists(f):
return []
with open(f, encoding="utf-8") as fh:
return [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")]
def reglages_site(racine):
brut = lire_toml(os.path.join(racine, SITE_TOML))
cfg = fusion(DEFAUTS, brut)
if not brut.get("carte_globale", {}).get("mymaps"):
cfg["carte_globale"]["mymaps"] = _mymaps_ancien(racine)
return cfg
def reglages_voyage(dossier, site=None):
"""Réglages effectifs d'un voyage : défauts ← site ← voyage.toml (← moto.txt)."""
brut = lire_toml(os.path.join(dossier, VOYAGE_TOML))
cfg = fusion(site if site is not None else DEFAUTS, brut)
if not brut.get("moto"):
f = os.path.join(dossier, "moto.txt")
if os.path.exists(f):
with open(f, encoding="utf-8") as fh:
cfg["moto"] = fh.readline().strip()
if not cfg.get("titre") or "titre" not in brut:
cfg["titre"] = os.path.basename(dossier.rstrip("/")).replace("-", " ").replace("_", " ")
return cfg
def parametres_video(cfg):
"""Paramètres d'encodage effectifs (dictionnaire plat)."""
v = cfg["video"]
q = QUALITES.get(v.get("qualite", "equilibree"), QUALITES["equilibree"])
return dict(q, encodeur="vt" if v.get("encodeur") == "materiel" else "cpu",
preset=v.get("preset", "medium"), fps_max=int(v.get("fps_max", 60)),
jobs=int(v.get("jobs", 0)), threads=int(v.get("threads", 0)))
# --- Écriture (rtg migrer, interface) -----------------------------------------
def _toml_valeur(v):
if isinstance(v, bool):
return "true" if v else "false"
if isinstance(v, (int, float)):
return str(v)
if isinstance(v, list):
return "[" + ", ".join(_toml_valeur(x) for x in v) + "]"
s = str(v).replace("\\", "\\\\").replace('"', '\\"')
return f'"{s}"'
def ecrire_toml(chemin, donnees, entete=""):
"""Écrit un TOML simple (valeurs, listes, une profondeur de tables)."""
lignes = [f"# {l}" for l in entete.splitlines()] + ([""] if entete else [])
for k, v in donnees.items():
if not isinstance(v, dict):
lignes.append(f"{k} = {_toml_valeur(v)}")
for k, v in donnees.items():
if isinstance(v, dict):
lignes += ["", f"[{k}]"] + [f"{kk} = {_toml_valeur(vv)}" for kk, vv in v.items()]
with open(chemin, "w", encoding="utf-8") as f:
f.write("\n".join(lignes) + "\n")
def nom_lisible(nom):
return re.sub(r"[-_]+", " ", nom).strip()