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
This commit is contained in:
1 parent
97b936fd3e
commit
f0e1c2dfce
23 files changed
+3083
-1
No files matched your search
@@ -0,0 +1,123 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user