Files
RoadTripsGenerator/tests/test_chaine.py
T
FabriceandClaude Opus 5.5 ada3d76f50 Administration native sur le Mac, réencodage sélectif et aide intégrée
- admin.sh : RTG lancé directement sur le Mac (plus de conteneur), nginx
  joint l'interface par host.docker.internal:8765 ; variante conteneur
  conservée (admin-docker.sh, docker-compose-conteneur.yml)
- Compression : réglages lus dans les fichiers (CRF, codec), refaire
  seulement le 1080p ou le 720p, seulement si les réglages ont changé ;
  réglages vidéo propres à chaque voyage ; « Compression actuelle » affichée
- Plus d'arrêt automatique après 2 h d'inactivité (--arret-inactif, 0 par défaut) ;
  arrêt propre qui attend la fin des encodages en cours
- Aide contextuelle dans l'interface (rtg/interface/aide) ; README centré
  sur l'installation et la mise en place
- Cœurs attribués au conteneur détectés (cgroup) et signalés

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

149 lines
6.9 KiB
Python

"""Chaîne complète sur un petit voyage fabriqué à la volée.
Sources « mal rangées » exprès : une trace de deux jours au nom quelconque,
des photos dans un autre dossier (dont une HEIC et une sans GPS), une vidéo.
"""
import os
import shutil
import subprocess
from datetime import datetime, timedelta, timezone
import pytest
from rtg import config, site, sources
pytestmark = pytest.mark.skipif(not (shutil.which("exiftool") and shutil.which("ffmpeg")),
reason="exiftool et ffmpeg nécessaires")
UTC = timezone.utc
def _gpx(chemin):
"""Deux journées (Paris, UTC+2 en mai) ; un point à 00 h 30 locale doit
rester sur la veille (coupure à 4 h)."""
pts = []
t0 = datetime(2026, 5, 1, 6, 0, tzinfo=UTC) # 8 h locale
for i in range(60):
pts.append((48.80 + i * 0.002, 2.30 + i * 0.003, t0 + timedelta(minutes=2 * i)))
pts.append((48.95, 2.50, datetime(2026, 5, 1, 22, 30, tzinfo=UTC))) # 00 h 30 le 2 mai
t1 = datetime(2026, 5, 2, 7, 0, tzinfo=UTC)
for i in range(40):
pts.append((49.00 + i * 0.002, 2.60 + i * 0.002, t1 + timedelta(minutes=3 * i)))
seg = "\n".join(f'<trkpt lat="{la}" lon="{lo}"><time>{t.strftime("%Y-%m-%dT%H:%M:%SZ")}</time></trkpt>'
for la, lo, t in pts)
with open(chemin, "w") as f:
f.write(f'<?xml version="1.0"?><gpx version="1.1" creator="test" '
f'xmlns="http://www.topografix.com/GPX/1/1"><trk><name>test</name><trkseg>{seg}'
f'</trkseg></trk></gpx>')
def _photo(chemin, date_locale, gps=None):
from PIL import Image
im = Image.new("RGB", (3000, 2000), (90, 140, 200))
im.save(chemin, "JPEG")
args = ["exiftool", "-q", "-overwrite_original", f"-DateTimeOriginal={date_locale}"]
if gps:
args += [f"-GPSLatitude={gps[0]}", "-GPSLatitudeRef=N", f"-GPSLongitude={gps[1]}", "-GPSLongitudeRef=E"]
subprocess.run(args + [chemin], check=True)
def _heic(chemin, jpeg):
from PIL import Image
import pillow_heif
pillow_heif.register_heif_opener()
with Image.open(jpeg) as im:
im.save(chemin, format="HEIF", exif=im.info.get("exif"))
@pytest.fixture
def site_test(tmp_path):
racine = tmp_path / "site"
voyage = racine / "Balades" / "Test-05-2026"
sources_dir = tmp_path / "carte_sd"
for d in (voyage, sources_dir / "photos", sources_dir / "gopro"):
d.mkdir(parents=True)
_gpx(sources_dir / "export_68degres_quelconque.gpx")
_photo(str(sources_dir / "photos" / "IMG_0001.jpg"), "2026:05:01 09:30:00", gps=(48.85, 2.35))
# Sans GPS, prise à 9 h 00 locale le 1er mai = 7 h 00 UTC : 30e point de la trace
_photo(str(sources_dir / "photos" / "IMG_0002.jpg"), "2026:05:01 09:00:00")
_photo(str(tmp_path / "base.jpg"), "2026:05:02 10:00:00", gps=(49.01, 2.61))
_heic(str(sources_dir / "photos" / "IMG_0003.HEIC"), str(tmp_path / "base.jpg"))
subprocess.run(["ffmpeg", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=size=1280x720:rate=30",
"-f", "lavfi", "-i", "sine=frequency=440", "-t", "2", "-c:v", "libx264", "-c:a", "aac",
"-shortest", str(sources_dir / "gopro" / "GX010001.MP4")], check=True)
config.ecrire_toml(str(racine / "site.toml"), {"titre": "Site de test",
"video": {"preset": "ultrafast"}})
config.ecrire_toml(str(voyage / "voyage.toml"), {
"titre": "Test", "moto": "Moto d'essai",
"sources": {"traces": [str(sources_dir / "export_68degres_quelconque.gpx")],
"videos": [str(sources_dir / "gopro")],
"photos": [str(sources_dir / "photos")]}})
return racine, voyage
def test_decoupage_et_coupure(site_test):
racine, voyage = site_test
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
a = sources.analyser(str(voyage), cfg)
assert sorted(j.isoformat() for j in a.journees) == ["2026-05-01", "2026-05-02"]
# Le point de 00 h 30 (heure de Paris) appartient au 1er mai
derniers = [p for t in a.journees[min(a.journees)] for s in t.segments for p in s.points]
assert derniers[-1].latitude == 48.95
def test_chaine_complete(site_test):
racine, voyage = site_test
assert site.generer(str(racine))
# Traces rangées par jour
assert sorted(os.listdir(voyage / "routes")) == ["2026-05-01.gpx", "2026-05-02.gpx"]
# Photos copiées, HEIC convertie en JPEG
assert sorted(os.listdir(voyage / "photos")) == ["IMG_0001.jpg", "IMG_0002.jpg", "IMG_0003.jpeg"]
csv = (voyage / "export_photos.csv").read_text()
assert "IMG_0001.jpg" in csv and "IMG_0003.jpeg" in csv
ligne2 = [l for l in csv.splitlines() if "IMG_0002" in l][0]
lat = float(ligne2.split(",")[2])
assert abs(lat - (48.80 + 30 * 0.002)) < 0.003 # position déduite de la trace
# Vidéo compressée en deux paliers lisibles
assert (voyage / "videos" / "GX010001.mp4").exists()
assert (voyage / "videos" / "GX010001_mobile.mp4").exists()
# Pages
assert sorted(os.listdir(voyage / "html")) == ["carte_2026-05-01.html", "carte_2026-05-02.html"]
assert (voyage / "index.html").exists() and (voyage / "photos_web" / "IMG_0001.jpg").exists()
accueil = (racine / "index.html").read_text()
assert "Site de test" in accueil and "Balades/Test-05-2026/index.html" in accueil
def test_deuxieme_passage_ne_refait_rien(site_test, capsys):
racine, voyage = site_test
site.generer(str(racine))
capsys.readouterr()
site.generer(str(racine))
sortie = capsys.readouterr().out
assert "Aucun nouvel encodage" in sortie
assert "0 réduite(s)" in sortie
def test_reencodage_selon_reglages(site_test):
"""Les réglages relus dans les vidéos ; seule la version dont les réglages
ont changé est refaite, et rien quand ils n'ont pas changé."""
from rtg import video
racine, voyage = site_test
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
a = sources.analyser(str(voyage), cfg)
assert video.compresser(str(voyage), cfg, a)
hd, sd = video.sorties(str(voyage), a.videos[0])
assert video.reglages_fichier(hd) == ("HEVC", 26.0)
assert video.reglages_fichier(sd) == ("H.264", 25.0)
dates = (os.path.getmtime(hd), os.path.getmtime(sd))
config.modifier_toml(str(voyage / "voyage.toml"), {"video": {"crf_hevc": 30}})
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
assert video.compresser(str(voyage), cfg, a, refaire="differentes")
assert video.reglages_fichier(hd) == ("HEVC", 30.0)
assert os.path.getmtime(sd) == dates[1] # 720p inchangé
dates = (os.path.getmtime(hd), os.path.getmtime(sd))
assert video.compresser(str(voyage), cfg, a, refaire="differentes")
assert (os.path.getmtime(hd), os.path.getmtime(sd)) == dates # rien à refaire
bilan = video.bilan_compression(str(voyage))
assert bilan["1080p"]["reglages"] == {("HEVC", 30.0): 1}