mise à jours HEVC pour 1080p
This commit is contained in:
Executable
+177
@@ -0,0 +1,177 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# CALIBRATION CRF — HEVC
|
||||||
|
#
|
||||||
|
# Encode un même extrait de rush à plusieurs CRF et mesure la qualité
|
||||||
|
# objective (VMAF si dispo, sinon SSIM) + la taille obtenue.
|
||||||
|
# Objectif : choisir CRF_HD / CRF_SD sur TES images plutôt qu'au doigt mouillé.
|
||||||
|
#
|
||||||
|
# Usage :
|
||||||
|
# ./bench_crf.sh rushs/GH011182.MP4 [start_sec] [duree_sec]
|
||||||
|
#
|
||||||
|
# Variables :
|
||||||
|
# CODEC=hevc palier 1080p HEVC (défaut)
|
||||||
|
# CODEC=h264 palier 720p H.264
|
||||||
|
# HEIGHT=1080 hauteur cible du test (1080 ou 720)
|
||||||
|
# CRFS="22 24 26 28 30 32"
|
||||||
|
# PRESET=medium
|
||||||
|
# ENCODER=cpu|vt (en mode vt, CRFS est interprété comme des valeurs -q:v)
|
||||||
|
# KEEP=1 conserve les fichiers encodés pour comparaison visuelle
|
||||||
|
#
|
||||||
|
# Les deux paliers du site se calibrent séparément — les échelles de CRF
|
||||||
|
# de x264 et x265 ne sont PAS comparables :
|
||||||
|
# ./bench_crf.sh rushs/GH011182.MP4 30 20 # 1080p HEVC
|
||||||
|
# CODEC=h264 HEIGHT=720 CRFS="20 22 24 26" ./bench_crf.sh rushs/GH011182.MP4 30 20
|
||||||
|
#
|
||||||
|
# Choisis un extrait REPRÉSENTATIF : du mouvement, de la végétation, du
|
||||||
|
# ciel dégradé. Un plan fixe sur un mur donnera un CRF trop optimiste.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SRC="${1:?Usage: ./bench_crf.sh <fichier_rush> [start_sec] [duree_sec]}"
|
||||||
|
START="${2:-30}"
|
||||||
|
DUR="${3:-20}"
|
||||||
|
|
||||||
|
CODEC="${CODEC:-hevc}"
|
||||||
|
HEIGHT="${HEIGHT:-1080}"
|
||||||
|
if [ "$CODEC" = "h264" ]; then
|
||||||
|
CRFS="${CRFS:-18 20 22 24 26 28}"
|
||||||
|
else
|
||||||
|
CRFS="${CRFS:-22 24 26 28 30 32}"
|
||||||
|
fi
|
||||||
|
PRESET="${PRESET:-medium}"
|
||||||
|
ENCODER="${ENCODER:-cpu}"
|
||||||
|
KEEP="${KEEP:-0}"
|
||||||
|
|
||||||
|
[ -f "$SRC" ] || { echo "❌ Fichier introuvable : $SRC"; exit 1; }
|
||||||
|
|
||||||
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/bench_crf.XXXXXX")"
|
||||||
|
cleanup() { [ "$KEEP" = "1" ] || rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# --- Détection de la métrique disponible -------------------------------------
|
||||||
|
FILTERS_LIST="$(ffmpeg -hide_banner -filters 2>/dev/null)"
|
||||||
|
if printf '%s' "$FILTERS_LIST" | grep -q "libvmaf"; then
|
||||||
|
METRIC="vmaf"
|
||||||
|
else
|
||||||
|
METRIC="ssim"
|
||||||
|
echo "ℹ️ libvmaf absent de ton ffmpeg → repli sur SSIM."
|
||||||
|
echo " (VMAF est bien plus parlant : brew install ffmpeg l'inclut normalement)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Référence : extrait copié sans réencodage -------------------------------
|
||||||
|
REF="$WORK/ref.mp4"
|
||||||
|
echo "✂️ Extraction de la référence (${DUR}s à partir de ${START}s)…"
|
||||||
|
ffmpeg -nostdin -hide_banner -loglevel error -y \
|
||||||
|
-ss "$START" -i "$SRC" -t "$DUR" \
|
||||||
|
-map 0:v:0 -c copy -tag:v hvc1 "$REF" 2>/dev/null
|
||||||
|
|
||||||
|
if [ ! -s "$REF" ]; then
|
||||||
|
echo "⚠️ Copie directe impossible, réencodage sans perte visible de la référence…"
|
||||||
|
ffmpeg -nostdin -hide_banner -loglevel error -y \
|
||||||
|
-ss "$START" -i "$SRC" -t "$DUR" \
|
||||||
|
-map 0:v:0 -c:v libx265 -preset ultrafast -x265-params lossless=1 \
|
||||||
|
-tag:v hvc1 "$REF"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Si on teste une hauteur inférieure à la source, la référence de comparaison
|
||||||
|
# doit être à la MÊME définition que le test : on fabrique une référence scalée.
|
||||||
|
SRC_H=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "$REF" | head -1)
|
||||||
|
REF_CMP="$REF"
|
||||||
|
if [ "$SRC_H" != "$HEIGHT" ]; then
|
||||||
|
REF_CMP="$WORK/ref_${HEIGHT}.mp4"
|
||||||
|
echo "📐 Référence de comparaison rescalée en ${HEIGHT}p…"
|
||||||
|
ffmpeg -nostdin -hide_banner -loglevel error -y -i "$REF" \
|
||||||
|
-vf "scale=-2:${HEIGHT}:flags=lanczos" \
|
||||||
|
-c:v libx265 -preset ultrafast -x265-params lossless=1 -tag:v hvc1 "$REF_CMP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REF_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$REF" | cut -d. -f1)
|
||||||
|
REF_DUR=${REF_DUR:-$DUR}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "🎯 Palier testé : ${HEIGHT}p en $(echo "$CODEC" | tr a-z A-Z)"
|
||||||
|
printf "%-6s %10s %12s %10s %10s\n" "CRF" "Taille" "Débit" "$(echo "$METRIC" | tr a-z A-Z)" "Encodage"
|
||||||
|
printf "%s\n" "--------------------------------------------------------"
|
||||||
|
|
||||||
|
BEST_LINE=""
|
||||||
|
for crf in $CRFS; do
|
||||||
|
OUT="$WORK/test_crf${crf}.mp4"
|
||||||
|
|
||||||
|
if [ "$CODEC" = "h264" ]; then
|
||||||
|
if [ "$ENCODER" = "vt" ]; then
|
||||||
|
VOPTS=(-c:v h264_videotoolbox -profile:v high -q:v "$crf" -allow_sw 1 -realtime 0)
|
||||||
|
else
|
||||||
|
VOPTS=(-c:v libx264 -preset "$PRESET" -profile:v high -level 4.0 -crf "$crf"
|
||||||
|
-g 120 -keyint_min 24)
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$ENCODER" = "vt" ]; then
|
||||||
|
VOPTS=(-c:v hevc_videotoolbox -profile:v main -q:v "$crf" -allow_sw 1 -realtime 0 -tag:v hvc1)
|
||||||
|
else
|
||||||
|
VOPTS=(-c:v libx265 -preset "$PRESET" -profile:v main -crf "$crf"
|
||||||
|
-x265-params "keyint=120:min-keyint=24:aq-mode=3" -tag:v hvc1)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
T0=$(date +%s)
|
||||||
|
ffmpeg -nostdin -hide_banner -loglevel error -y -i "$REF" \
|
||||||
|
-map 0:v:0 "${VOPTS[@]}" \
|
||||||
|
-vf "scale=-2:${HEIGHT}:flags=lanczos" \
|
||||||
|
-pix_fmt yuv420p \
|
||||||
|
-color_primaries bt709 -color_trc bt709 -colorspace bt709 \
|
||||||
|
-an "$OUT" 2>/dev/null
|
||||||
|
T1=$(date +%s)
|
||||||
|
ENC_TIME=$((T1 - T0))
|
||||||
|
|
||||||
|
[ -s "$OUT" ] || { printf "%-6s %10s\n" "$crf" "ÉCHEC"; continue; }
|
||||||
|
|
||||||
|
BYTES=$(stat -f%z "$OUT" 2>/dev/null || stat -c%s "$OUT")
|
||||||
|
MB=$(awk -v b="$BYTES" 'BEGIN{printf "%.1f", b/1048576}')
|
||||||
|
KBPS=$(awk -v b="$BYTES" -v d="$REF_DUR" 'BEGIN{printf "%.0f", b*8/1000/d}')
|
||||||
|
|
||||||
|
# --- Mesure de qualité ---
|
||||||
|
if [ "$METRIC" = "vmaf" ]; then
|
||||||
|
SCORE=$(ffmpeg -nostdin -hide_banner -loglevel info -i "$OUT" -i "$REF_CMP" \
|
||||||
|
-lavfi "[0:v]setpts=PTS-STARTPTS[d];[1:v]setpts=PTS-STARTPTS[r];[d][r]libvmaf" \
|
||||||
|
-f null - 2>&1 | grep -oE "VMAF score: [0-9.]+" | tail -1 | grep -oE "[0-9.]+")
|
||||||
|
SCORE=$(awk -v s="${SCORE:-0}" 'BEGIN{printf "%.2f", s}')
|
||||||
|
else
|
||||||
|
SCORE=$(ffmpeg -nostdin -hide_banner -loglevel info -i "$OUT" -i "$REF_CMP" \
|
||||||
|
-lavfi "[0:v]setpts=PTS-STARTPTS[d];[1:v]setpts=PTS-STARTPTS[r];[d][r]ssim" \
|
||||||
|
-f null - 2>&1 | grep -oE "All:[0-9.]+" | tail -1 | cut -d: -f2)
|
||||||
|
SCORE=$(awk -v s="${SCORE:-0}" 'BEGIN{printf "%.4f", s}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%-6s %9s M %9s k %10s %8s s\n" "$crf" "$MB" "$KBPS" "$SCORE" "$ENC_TIME"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "📖 Comment lire ce tableau"
|
||||||
|
if [ "$METRIC" = "vmaf" ]; then
|
||||||
|
cat <<'EOF'
|
||||||
|
VMAF ≥ 95 : indiscernable de la source à distance normale
|
||||||
|
VMAF 90-95 : excellent, cible raisonnable pour un site de voyage
|
||||||
|
VMAF 85-90 : bon, quelques artefacts visibles en plein écran
|
||||||
|
VMAF < 85 : dégradation nette sur les scènes chargées
|
||||||
|
→ Prends le CRF le plus ÉLEVÉ qui reste au-dessus de ton seuil.
|
||||||
|
Le gain de taille entre deux CRF consécutifs s'écrase vite ;
|
||||||
|
dès que la courbe de qualité plonge, tu es allé trop loin.
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
cat <<'EOF'
|
||||||
|
SSIM ≥ 0.98 : très bon
|
||||||
|
SSIM 0.96-0.98 : correct pour du web
|
||||||
|
SSIM < 0.95 : dégradation visible
|
||||||
|
→ Prends le CRF le plus ÉLEVÉ qui reste au-dessus de ton seuil.
|
||||||
|
⚠️ SSIM est moins fiable que VMAF sur des images très texturées
|
||||||
|
(feuillage, eau, gravier) : vérifie AUSSI à l'œil avec KEEP=1.
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$KEEP" = "1" ]; then
|
||||||
|
echo
|
||||||
|
echo "📁 Fichiers conservés dans : $WORK"
|
||||||
|
echo " Compare à l'œil, en plein écran, sur les plans les plus chargés."
|
||||||
|
fi
|
||||||
Regular → Executable
+297
-68
@@ -1,83 +1,312 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
shopt -s nocaseglob
|
# =============================================================================
|
||||||
|
# COMPRESSION ROAD-TRIP — STRATÉGIE HYBRIDE + ENCODAGE PARALLÈLE
|
||||||
|
# Source : GoPro Hero 9, rushs en HEVC 1080p
|
||||||
|
#
|
||||||
|
# Sorties (noms de fichiers INCHANGÉS par rapport à l'ancien script) :
|
||||||
|
# <base>.mp4 1080p natif, HEVC/H.265, tag hvc1
|
||||||
|
# → gros fichier, là où les 40 % d'économie comptent
|
||||||
|
# <base>_mobile.mp4 720p, H.264, tag avc1, profil High
|
||||||
|
# → fichier chargé par défaut, se lit sur TOUS
|
||||||
|
# les navigateurs sans exception
|
||||||
|
#
|
||||||
|
# PARALLÉLISME
|
||||||
|
# Deux niveaux, tous deux actifs par défaut :
|
||||||
|
# 1. Les deux paliers d'un même rush sont indépendants → lancés ensemble.
|
||||||
|
# 2. JOBS rushs traités simultanément (pool géré par xargs -P).
|
||||||
|
# Le nombre de threads par encodeur est borné pour ne pas surcharger :
|
||||||
|
# x265 sature vite (scaling sublinéaire au-delà de ~6-8 threads), donc
|
||||||
|
# plusieurs encodages courts valent mieux qu'un seul très parallélisé.
|
||||||
|
#
|
||||||
|
# Variables d'environnement :
|
||||||
|
# JOBS=N nombre de rushs en parallèle
|
||||||
|
# (défaut : cœurs performance / 4, minimum 1)
|
||||||
|
# THREADS=N threads par encodeur (défaut : cœurs / JOBS)
|
||||||
|
# ENCODER=cpu (défaut) libx265 + libx264 — meilleure qualité/octet
|
||||||
|
# ENCODER=vt VideoToolbox — ~10x plus rapide sur Apple Silicon
|
||||||
|
# (⚠️ le parallélisme n'y sert à rien : le bloc média
|
||||||
|
# de la puce est une ressource fixe → JOBS=1 forcé)
|
||||||
|
# PRESET=medium preset CPU : ultrafast…placebo
|
||||||
|
# CRF_HEVC=26 qualité du 1080p HEVC (échelle x265)
|
||||||
|
# CRF_H264=25 qualité du 720p H.264 (échelle x264 — PAS la même !)
|
||||||
|
# VTQ_HD=60 VTQ_SD=58 qualité VideoToolbox (1-100, plus haut = meilleur)
|
||||||
|
# FPS_MAX=60 plafonne le framerate (un rush 30p reste en 30p,
|
||||||
|
# un rush 120p/240p est ramené à 60)
|
||||||
|
# FORCE=1 réencode même si le fichier de sortie existe
|
||||||
|
#
|
||||||
|
# Exemples :
|
||||||
|
# ./compress.sh mon-voyage
|
||||||
|
# JOBS=4 ./compress.sh mon-voyage
|
||||||
|
# JOBS=1 ./compress.sh mon-voyage # séquentiel + barre de progression
|
||||||
|
# ENCODER=vt ./compress.sh mon-voyage
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
shopt -s nocaseglob nullglob
|
||||||
|
|
||||||
|
# --- 0. MODE INTERNE ---------------------------------------------------------
|
||||||
|
# Quand le script est rappelé par xargs pour traiter UN rush, il arrive ici.
|
||||||
|
if [ "${1:-}" = "--encode-one" ]; then
|
||||||
|
INTERNAL=1
|
||||||
|
BASE_DIR="${RT_BASE_DIR:-.}"
|
||||||
|
TARGET="${2:?--encode-one attend un fichier}"
|
||||||
|
else
|
||||||
|
INTERNAL=0
|
||||||
|
BASE_DIR="${1:-.}"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- 1. CONFIGURATION ---
|
|
||||||
# Utilise l'argument 1 comme dossier racine, ou le dossier courant par défaut
|
|
||||||
BASE_DIR="${1:-.}"
|
|
||||||
SOURCE_DIR="$BASE_DIR/rushs"
|
SOURCE_DIR="$BASE_DIR/rushs"
|
||||||
DEST_DIR="$BASE_DIR/videos"
|
DEST_DIR="$BASE_DIR/videos"
|
||||||
|
|
||||||
# Vérification du dossier source
|
# --- 1. CONFIGURATION --------------------------------------------------------
|
||||||
|
ENCODER="${ENCODER:-cpu}"
|
||||||
|
PRESET="${PRESET:-medium}"
|
||||||
|
CRF_HEVC="${CRF_HEVC:-26}"
|
||||||
|
CRF_H264="${CRF_H264:-25}"
|
||||||
|
VTQ_HD="${VTQ_HD:-60}"
|
||||||
|
VTQ_SD="${VTQ_SD:-58}"
|
||||||
|
FPS_MAX="${FPS_MAX:-60}"
|
||||||
|
FORCE="${FORCE:-0}"
|
||||||
|
|
||||||
|
# Détection des cœurs. Sur Apple Silicon on ne compte que les cœurs
|
||||||
|
# PERFORMANCE : les cœurs d'efficience sont si lents qu'ils deviennent des
|
||||||
|
# traînards dans un pool x265 et pénalisent l'encodage entier.
|
||||||
|
detect_cores() {
|
||||||
|
local c=""
|
||||||
|
c=$(sysctl -n hw.perflevel0.logicalcpu 2>/dev/null); [ -n "$c" ] && { echo "$c"; return; }
|
||||||
|
c=$(sysctl -n hw.logicalcpu 2>/dev/null); [ -n "$c" ] && { echo "$c"; return; }
|
||||||
|
c=$(nproc 2>/dev/null); [ -n "$c" ] && { echo "$c"; return; }
|
||||||
|
echo 4
|
||||||
|
}
|
||||||
|
CORES="${CORES:-$(detect_cores)}"
|
||||||
|
|
||||||
|
# En VideoToolbox, le parallélisme ne sert à rien (un seul moteur d'encodage
|
||||||
|
# matériel sur les puces M de base) — sauf si l'utilisateur force JOBS.
|
||||||
|
JOBS_EXPLICIT=0
|
||||||
|
[ -n "${JOBS+x}" ] && JOBS_EXPLICIT=1
|
||||||
|
if [ "$ENCODER" = "vt" ] && [ "$JOBS_EXPLICIT" = "0" ]; then
|
||||||
|
JOBS=1
|
||||||
|
else
|
||||||
|
JOBS="${JOBS:-$(( CORES / 4 ))}"
|
||||||
|
fi
|
||||||
|
[ "$JOBS" -lt 1 ] 2>/dev/null && JOBS=1
|
||||||
|
[ -z "$JOBS" ] && JOBS=1
|
||||||
|
|
||||||
|
THREADS="${THREADS:-$(( CORES / JOBS ))}"
|
||||||
|
[ "$THREADS" -lt 2 ] 2>/dev/null && THREADS=2
|
||||||
|
|
||||||
|
# Le palier 720p H.264 est 3 à 5× plus rapide que le 1080p HEVC : on lui donne
|
||||||
|
# moitié moins de threads. Il finit largement avant, et la fenêtre pendant
|
||||||
|
# laquelle les deux paliers tournent ensemble ne surcharge pas la machine.
|
||||||
|
THREADS_SD=$(( THREADS / 2 ))
|
||||||
|
[ "$THREADS_SD" -lt 2 ] && THREADS_SD=2
|
||||||
|
|
||||||
|
# --- 2. FONCTION D'ENCODAGE --------------------------------------------------
|
||||||
|
# encode <entrée> <sortie> <filtre_vf|none> <hevc|h264> <crf> <vtq> <maxrate> <bufsize> <audio>
|
||||||
|
encode() {
|
||||||
|
local in="$1" out="$2" vf="$3" codec="$4" crf="$5" vtq="$6" maxrate="$7" bufsize="$8" abr="$9"
|
||||||
|
|
||||||
|
local -a vopts vf_args statopt
|
||||||
|
if [ "$vf" = "none" ]; then vf_args=(); else vf_args=(-vf "$vf"); fi
|
||||||
|
|
||||||
|
# Barre de progression uniquement en séquentiel : à plusieurs jobs, les
|
||||||
|
# sorties de progression s'entremêlent et deviennent illisibles.
|
||||||
|
if [ "$JOBS" = "1" ]; then statopt=(-stats); else statopt=(); fi
|
||||||
|
|
||||||
|
# ⚠️ Les tableaux VIDES sont développés avec l'idiome ${a[@]+"${a[@]}"}.
|
||||||
|
# Le /bin/bash d'Apple est en 3.2, où `"${vide[@]}"` sous `set -u` provoque
|
||||||
|
# une erreur "unbound variable" (corrigé seulement en bash 4.4).
|
||||||
|
|
||||||
|
if [ "$codec" = "hevc" ]; then
|
||||||
|
if [ "$ENCODER" = "vt" ]; then
|
||||||
|
vopts=(-c:v hevc_videotoolbox -profile:v main -q:v "$vtq" -allow_sw 1 -realtime 0 -tag:v hvc1)
|
||||||
|
else
|
||||||
|
vopts=(
|
||||||
|
-c:v libx265 -preset "$PRESET" -profile:v main -crf "$crf"
|
||||||
|
# keyint=120 -> 1 image-clé toutes les 2 s à 60 fps.
|
||||||
|
# scenecut laissé ACTIF (défaut) : un GOP strictement fixe n'est
|
||||||
|
# utile qu'en streaming segmenté, pas pour du MP4 progressif.
|
||||||
|
# pools=N borne le pool WPP pour cohabiter avec les autres jobs.
|
||||||
|
-x265-params "keyint=120:min-keyint=24:aq-mode=3:pools=$THREADS"
|
||||||
|
-maxrate "$maxrate" -bufsize "$bufsize"
|
||||||
|
# -tag:v hvc1 est INDISPENSABLE : sans lui ffmpeg écrit 'hev1',
|
||||||
|
# que Safari et QuickTime refusent de lire.
|
||||||
|
-tag:v hvc1
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$ENCODER" = "vt" ]; then
|
||||||
|
vopts=(-c:v h264_videotoolbox -profile:v high -q:v "$vtq" -allow_sw 1 -realtime 0)
|
||||||
|
else
|
||||||
|
vopts=(
|
||||||
|
-c:v libx264 -preset "$PRESET" -profile:v high -level 4.0 -crf "$crf"
|
||||||
|
-g 120 -keyint_min 24
|
||||||
|
-threads "$THREADS_SD"
|
||||||
|
-maxrate "$maxrate" -bufsize "$bufsize"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ffmpeg -nostdin -hide_banner -loglevel error ${statopt[@]+"${statopt[@]}"} \
|
||||||
|
-i "$in" \
|
||||||
|
-map 0:v:0 -map 0:a:0\? \
|
||||||
|
"${vopts[@]}" \
|
||||||
|
${vf_args[@]+"${vf_args[@]}"} \
|
||||||
|
-pix_fmt yuv420p \
|
||||||
|
-fpsmax "$FPS_MAX" \
|
||||||
|
-color_primaries bt709 -color_trc bt709 -colorspace bt709 \
|
||||||
|
-c:a aac -b:a "$abr" -ac 1 -ar 48000 \
|
||||||
|
-map_metadata -1 \
|
||||||
|
-movflags +faststart \
|
||||||
|
-y "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
fsize() { stat -f%z "$1" 2>/dev/null || stat -c%s "$1"; }
|
||||||
|
|
||||||
|
# --- 3. TRAITEMENT D'UN RUSH -------------------------------------------------
|
||||||
|
process_one() {
|
||||||
|
local f="$1"
|
||||||
|
local filename base desktop_out mobile_out src_h vf_hd
|
||||||
|
filename=$(basename "$f")
|
||||||
|
base="${filename%.*}"
|
||||||
|
desktop_out="$DEST_DIR/${base}.mp4"
|
||||||
|
mobile_out="$DEST_DIR/${base}_mobile.mp4"
|
||||||
|
|
||||||
|
if [ "$FORCE" != "1" ] && [ -s "$desktop_out" ] && [ -s "$mobile_out" ]; then
|
||||||
|
echo "⏭️ $filename — déjà encodé (FORCE=1 pour refaire)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
src_h=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "$f" 2>/dev/null | head -1)
|
||||||
|
src_h=${src_h:-1080}
|
||||||
|
|
||||||
|
# Rushs déjà en 1080p : pas de rééchelonnage, on garde le pixel natif.
|
||||||
|
# Si un rush est plus grand (2.7K/4K), on redescend proprement à 1080p.
|
||||||
|
if [ "$src_h" -gt 1080 ]; then vf_hd="scale=-2:1080:flags=lanczos"; else vf_hd="none"; fi
|
||||||
|
|
||||||
|
echo "🎬 $filename (${src_h}p) — encodage des 2 paliers en parallèle…"
|
||||||
|
|
||||||
|
# Les deux paliers sont indépendants : même source, sorties distinctes.
|
||||||
|
# On les lance ensemble — le 720p H.264, bien plus léger, se glisse dans
|
||||||
|
# les trous laissés par le 1080p HEVC.
|
||||||
|
local pid_hd pid_sd rc_hd rc_sd
|
||||||
|
encode "$f" "$desktop_out" "$vf_hd" hevc "$CRF_HEVC" "$VTQ_HD" 8M 16M 96k &
|
||||||
|
pid_hd=$!
|
||||||
|
encode "$f" "$mobile_out" "scale=-2:720:flags=lanczos" h264 "$CRF_H264" "$VTQ_SD" 5M 10M 64k &
|
||||||
|
pid_sd=$!
|
||||||
|
wait "$pid_hd"; rc_hd=$?
|
||||||
|
wait "$pid_sd"; rc_sd=$?
|
||||||
|
|
||||||
|
if [ "$rc_hd" = "0" ] && [ "$rc_sd" = "0" ] && [ -s "$desktop_out" ] && [ -s "$mobile_out" ]; then
|
||||||
|
printf " ✅ %-24s %s → 1080p %s (HEVC) + 720p %s (H.264)\n" \
|
||||||
|
"$filename" "$(du -h "$f" | cut -f1)" \
|
||||||
|
"$(du -h "$desktop_out" | cut -f1)" "$(du -h "$mobile_out" | cut -f1)"
|
||||||
|
[ -n "${RT_STATS:-}" ] && \
|
||||||
|
echo "$(fsize "$f") $(( $(fsize "$desktop_out") + $(fsize "$mobile_out") ))" >> "$RT_STATS"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo " ❌ Échec sur $filename"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Point de sortie du mode interne (un seul rush, appelé par xargs)
|
||||||
|
if [ "$INTERNAL" = "1" ]; then
|
||||||
|
process_one "$TARGET"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MODE PRINCIPAL
|
||||||
|
# =============================================================================
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
echo "❌ Erreur : Dossier source $SOURCE_DIR introuvable."
|
echo "❌ Erreur : Dossier source $SOURCE_DIR introuvable."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Création du dossier de destination
|
|
||||||
mkdir -p "$DEST_DIR"
|
mkdir -p "$DEST_DIR"
|
||||||
|
|
||||||
echo "--- COMPRESSION ROAD-TRIP (CPU OPTIMISÉ) ---"
|
# Vérification des encodeurs disponibles.
|
||||||
echo "📂 Source : $SOURCE_DIR"
|
# (on stocke la liste : un `| grep -q` déclencherait un SIGPIPE que pipefail
|
||||||
echo "📤 Destination : $DEST_DIR"
|
# transformerait en faux négatif)
|
||||||
|
ENCODERS_LIST="$(ffmpeg -hide_banner -encoders 2>/dev/null)"
|
||||||
|
has_enc() { printf '%s' "$ENCODERS_LIST" | grep -q "$1"; }
|
||||||
|
|
||||||
# 2. BOUCLE DE TRAITEMENT
|
if [ "$ENCODER" = "vt" ]; then
|
||||||
for f in "$SOURCE_DIR"/*.{mp4,mov}; do
|
has_enc "hevc_videotoolbox" || { echo "❌ hevc_videotoolbox indisponible. Utilise ENCODER=cpu."; exit 1; }
|
||||||
[ -e "$f" ] || continue
|
has_enc "h264_videotoolbox" || { echo "❌ h264_videotoolbox indisponible. Utilise ENCODER=cpu."; exit 1; }
|
||||||
|
else
|
||||||
|
has_enc "libx265" || { echo "❌ libx265 absent de ton ffmpeg (brew install ffmpeg)."; exit 1; }
|
||||||
|
has_enc "libx264" || { echo "❌ libx264 absent de ton ffmpeg (brew install ffmpeg)."; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
# Extraction du nom de base (ex: GH011182.mp4 -> GH011182)
|
# Liste des rushs
|
||||||
filename=$(basename "$f")
|
files=()
|
||||||
base="${filename%.*}"
|
for f in "$SOURCE_DIR"/*.mp4 "$SOURCE_DIR"/*.mov; do
|
||||||
|
[ -e "$f" ] && files+=("$f")
|
||||||
# Conventions de nommage validées
|
|
||||||
desktop_out="$DEST_DIR/${base}.mp4"
|
|
||||||
mobile_out="$DEST_DIR/${base}_mobile.mp4"
|
|
||||||
|
|
||||||
echo "🎬 Traitement de : $filename ..."
|
|
||||||
|
|
||||||
# --- A. VERSION DESKTOP (1080p60 - Validé 233 Mo) ---
|
|
||||||
# Réglages : CRF 28, Maxrate 12M, GOP 120, FPS source exact
|
|
||||||
ffmpeg -i "$f" \
|
|
||||||
-c:v libx264 -preset slow -profile:v high \
|
|
||||||
-crf 28 \
|
|
||||||
-tune film \
|
|
||||||
-g 120 -keyint_min 120 -sc_threshold 0 \
|
|
||||||
-maxrate 15M -bufsize 30M \
|
|
||||||
-pix_fmt yuv420p \
|
|
||||||
-r 60000/1001 \
|
|
||||||
-vf "scale=1920:-2:flags=lanczos" \
|
|
||||||
-c:a aac -b:a 96k -ac 1 \
|
|
||||||
-map_metadata -1 \
|
|
||||||
-movflags +faststart \
|
|
||||||
-y -loglevel error \
|
|
||||||
"$desktop_out"
|
|
||||||
|
|
||||||
# --- B. VERSION MOBILE (720p60 - Optimisé 4G/Smartphone) ---
|
|
||||||
# Réglages : On baisse le maxrate à 6M pour la fluidité mobile
|
|
||||||
ffmpeg -i "$f" \
|
|
||||||
-c:v libx264 -preset slow -profile:v high \
|
|
||||||
-crf 28 \
|
|
||||||
-tune film \
|
|
||||||
-g 120 -keyint_min 120 -sc_threshold 0 \
|
|
||||||
-maxrate 6M -bufsize 12M \
|
|
||||||
-pix_fmt yuv420p \
|
|
||||||
-r 60000/1001 \
|
|
||||||
-vf "scale=1280:-2:flags=lanczos" \
|
|
||||||
-c:a aac -b:a 64k -ac 1 \
|
|
||||||
-map_metadata -1 \
|
|
||||||
-movflags +faststart \
|
|
||||||
-y -loglevel error \
|
|
||||||
"$mobile_out"
|
|
||||||
|
|
||||||
# Petit bilan de taille pour confirmer l'économie
|
|
||||||
if [ -f "$desktop_out" ]; then
|
|
||||||
d_size=$(du -h "$desktop_out" | cut -f1)
|
|
||||||
m_size=$(du -h "$mobile_out" | cut -f1)
|
|
||||||
echo " ✅ Desktop : $d_size (${base}.mp4)"
|
|
||||||
echo " ✅ Mobile : $m_size (${base}_mobile.mp4)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "---------------------------------------------------"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
shopt -u nocaseglob
|
echo "--- COMPRESSION ROAD-TRIP (HYBRIDE HEVC / H.264) ---"
|
||||||
echo "✅ TOUTES LES VIDÉOS SONT PRÊTES."
|
echo "📂 Source : $SOURCE_DIR"
|
||||||
|
echo "📤 Destination : $DEST_DIR"
|
||||||
|
if [ "$ENCODER" = "vt" ]; then
|
||||||
|
echo "⚙️ Encodeur : VideoToolbox (q=$VTQ_HD / $VTQ_SD)"
|
||||||
|
[ "$JOBS" = "1" ] && [ "$JOBS_EXPLICIT" = "0" ] && \
|
||||||
|
echo " ℹ️ JOBS=1 : le bloc média de la puce est une ressource fixe,"
|
||||||
|
[ "$JOBS" = "1" ] && [ "$JOBS_EXPLICIT" = "0" ] && \
|
||||||
|
echo " paralléliser ne ferait que mettre les jobs en file d'attente."
|
||||||
|
else
|
||||||
|
echo "⚙️ Encodeur : CPU preset=$PRESET (HEVC CRF $CRF_HEVC / H.264 CRF $CRF_H264)"
|
||||||
|
fi
|
||||||
|
echo "🧵 Parallélisme : $JOBS rush(s) à la fois × 2 paliers ($THREADS threads en 1080p, $THREADS_SD en 720p)"
|
||||||
|
echo " ($CORES cœur(s) performance détecté(s))"
|
||||||
|
echo " 1080p → HEVC (hvc1) | 720p → H.264 (avc1, lisible partout)"
|
||||||
|
echo "📼 ${#files[@]} rush(s) à traiter"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
|
echo "ℹ️ Rien à faire."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
RT_STATS="$(mktemp "${TMPDIR:-/tmp}/rtstats.XXXXXX")"
|
||||||
|
trap 'rm -f "$RT_STATS"' EXIT
|
||||||
|
|
||||||
|
SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
|
||||||
|
|
||||||
|
export RT_BASE_DIR="$BASE_DIR" RT_STATS
|
||||||
|
export ENCODER PRESET CRF_HEVC CRF_H264 VTQ_HD VTQ_SD FPS_MAX FORCE JOBS THREADS THREADS_SD CORES
|
||||||
|
|
||||||
|
T0=$(date +%s)
|
||||||
|
|
||||||
|
# xargs -P gère le pool de jobs : portable (y compris le bash 3.2 d'Apple,
|
||||||
|
# qui ne connaît pas `wait -n`) et il relance un job dès qu'un autre se termine.
|
||||||
|
printf '%s\0' "${files[@]}" | xargs -0 -P "$JOBS" -n1 "$SCRIPT_PATH" --encode-one
|
||||||
|
rc=$?
|
||||||
|
|
||||||
|
T1=$(date +%s)
|
||||||
|
ELAPSED=$((T1 - T0))
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ -s "$RT_STATS" ]; then
|
||||||
|
awk -v el="$ELAPSED" '
|
||||||
|
{ n++; s += $1; o += $2 }
|
||||||
|
END {
|
||||||
|
if (n > 0) {
|
||||||
|
printf "✅ %d vidéo(s) traitée(s) en %d min %02d s\n", n, el/60, el%60
|
||||||
|
printf "📊 Rushs : %d Mo → Web : %d Mo (%.0f%% du volume source)\n", \
|
||||||
|
s/1048576, o/1048576, o*100/s
|
||||||
|
}
|
||||||
|
}' "$RT_STATS"
|
||||||
|
else
|
||||||
|
printf "ℹ️ Aucun nouvel encodage (durée : %d min %02d s)\n" $((ELAPSED/60)) $((ELAPSED%60))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "🔍 Contrôle des tags (attendu : hvc1 pour les 1080p, avc1 pour les _mobile) :"
|
||||||
|
echo " ffprobe -v error -select_streams v:0 -show_entries stream=codec_tag_string \\"
|
||||||
|
echo " -of csv=p=0 $DEST_DIR/*.mp4"
|
||||||
|
echo
|
||||||
|
echo "💡 Pour trouver ton JOBS optimal, chronomètre 3-4 rushs à JOBS=1, 2 puis 4 :"
|
||||||
|
echo " le gain plafonne vite, et au-delà le cache thrashe."
|
||||||
|
|
||||||
|
exit $rc
|
||||||
|
|||||||
Regular → Executable
+224
-34
@@ -1,52 +1,242 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# EXTRACTION DES MÉTADONNÉES — photos EXIF + traces GPS embarquées GoPro
|
||||||
|
#
|
||||||
|
# Produit deux CSV sans en-tête, consommés par genere_carte.py :
|
||||||
|
# export_photos.csv : fichier, date, lat, lon, alt
|
||||||
|
# export_videos.csv : fichier, date GPS, lat, lon, alt, vitesse
|
||||||
|
#
|
||||||
|
# Usage : ./extraction_infos.sh [dossier_voyage]
|
||||||
|
#
|
||||||
|
# Variables :
|
||||||
|
# VIDEO_GPS=first (défaut) un seul point GPS par rush — RAPIDE.
|
||||||
|
# C'est tout ce que genere_carte.py utilise pour poser
|
||||||
|
# le marqueur : `folium.Marker(location=pts_v[0])`.
|
||||||
|
# VIDEO_GPS=full déroule toute la trace GPS de chaque rush.
|
||||||
|
# Lit l'intégralité de chaque fichier : compter plusieurs
|
||||||
|
# minutes par rush sur un disque externe.
|
||||||
|
# VARIANTE=D|A|B|C force le mode d'extraction vidéo (voir plus bas)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
shopt -s nocaseglob nullglob
|
||||||
|
|
||||||
# --- 1. CONFIGURATION DU RÉPERTOIRE ---
|
|
||||||
# On récupère l'argument 1 s'il existe, sinon on reste sur le répertoire courant
|
|
||||||
BASE_DIR="${1:-.}"
|
BASE_DIR="${1:-.}"
|
||||||
|
|
||||||
# Définition des dossiers et fichiers par rapport à la base
|
|
||||||
PHOTOS_DIR="$BASE_DIR/photos"
|
PHOTOS_DIR="$BASE_DIR/photos"
|
||||||
RUSHS_DIR="$BASE_DIR/rushs"
|
RUSHS_DIR="$BASE_DIR/rushs"
|
||||||
CSV_PHOTOS="$BASE_DIR/export_photos.csv"
|
CSV_PHOTOS="$BASE_DIR/export_photos.csv"
|
||||||
CSV_VIDEOS="$BASE_DIR/export_videos.csv"
|
CSV_VIDEOS="$BASE_DIR/export_videos.csv"
|
||||||
|
|
||||||
# On active l'insensibilité à la casse pour les jokers (*.mp4, etc.)
|
VIDEO_GPS="${VIDEO_GPS:-first}"
|
||||||
shopt -s nocaseglob
|
VARIANTE="${VARIANTE:-}"
|
||||||
|
|
||||||
echo "--- DÉBUT DE L'EXTRACTION BRUTE ---"
|
if ! command -v exiftool >/dev/null 2>&1; then
|
||||||
echo "📂 Répertoire racine : $BASE_DIR"
|
echo "❌ exiftool introuvable. Installe-le : brew install exiftool"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 2. RÉINITIALISATION DES FICHIERS CSV (Optionnel mais recommandé)
|
echo "--- EXTRACTION DES MÉTADONNÉES ---"
|
||||||
# Si tu veux cumuler sans effacer, commente les deux lignes suivantes :
|
echo "📂 Répertoire : $BASE_DIR"
|
||||||
> "$CSV_PHOTOS"
|
|
||||||
> "$CSV_VIDEOS"
|
|
||||||
|
|
||||||
# 3. PHOTOS (Fichiers .jpg, .jpeg)
|
TMPD="$(mktemp -d "${TMPDIR:-/tmp}/extr.XXXXXX")"
|
||||||
echo "📸 Traitement des photos dans : $PHOTOS_DIR"
|
ERRLOG="$TMPD/err.log"
|
||||||
|
: > "$ERRLOG"
|
||||||
|
trap 'rm -rf "$TMPD"' EXIT
|
||||||
|
|
||||||
|
nlines() { awk 'END{print NR+0}' "$1" 2>/dev/null || echo 0; }
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FORMATS
|
||||||
|
#
|
||||||
|
# `exiftool -p` supprime la LIGNE ENTIÈRE dès qu'UN SEUL tag du format est
|
||||||
|
# absent. Sur un rush GoPro, [Main] porte GPSLatitude/GPSLongitude mais PAS
|
||||||
|
# GPSDateTime, alors que [Doc1] porte tout. Un format à six tags peut donc ne
|
||||||
|
# rien produire sur un fichier parfaitement géolocalisé.
|
||||||
|
#
|
||||||
|
# ⚠️ NE JAMAIS ajouter -f pour « forcer » l'affichage. Vérifié en test :
|
||||||
|
# `-ee -p '$FileName,$SampleRate'` sur un fichier multi-documents donne
|
||||||
|
# 0 ligne sans -f, mais UNE SEULE ligne « fichier,- » AVEC -f — l'option fait
|
||||||
|
# perdre à -ee son itération par document. C'est pire que le mal.
|
||||||
|
#
|
||||||
|
# La parade : trois modes, essayés sur les premiers rushs ; on garde celui
|
||||||
|
# qui produit quelque chose.
|
||||||
|
# A = format complet (date, lat, lon, alt, vitesse)
|
||||||
|
# B = format réduit (date, lat, lon) — tolère l'absence d'alt/vitesse
|
||||||
|
# C = format réduit + -ee3, qui détaille davantage les sous-documents
|
||||||
|
# =============================================================================
|
||||||
|
FMT_PHOTOS='"$FileName",$DateTimeOriginal,$GPSLatitude,$GPSLongitude,$GPSAltitude'
|
||||||
|
FMT_PHOTOS_MIN='"$FileName",$DateTimeOriginal,$GPSLatitude,$GPSLongitude'
|
||||||
|
FMT_V_COMPLET='"$FileName",$GPSDateTime,$GPSLatitude,$GPSLongitude,$GPSAltitude,$GPSSpeed'
|
||||||
|
FMT_V_MINIMAL='"$FileName",$GPSDateTime,$GPSLatitude,$GPSLongitude'
|
||||||
|
|
||||||
|
PARSE_GPS='
|
||||||
|
# Reconstruit des lignes CSV depuis la sortie « Tag : valeur » de exiftool.
|
||||||
|
# Robuste au format long ("GPS Date Time") comme court ("GPSDateTime"), et au
|
||||||
|
# prefixe de groupe [Main] / [Doc1].
|
||||||
|
{
|
||||||
|
ligne = $0
|
||||||
|
sub(/^\[[^]]*\][ \t]*/, "", ligne)
|
||||||
|
if (index(ligne, ":") == 0) next
|
||||||
|
tag = ligne; sub(/[ \t]*:.*/, "", tag); gsub(/[ \t]/, "", tag)
|
||||||
|
val = ligne; sub(/^[^:]*:[ \t]*/, "", val); gsub(/[ \t]+$/, "", val)
|
||||||
|
|
||||||
|
if (tag == "GPSDateTime" && dt == "") dt = val
|
||||||
|
else if (tag == "CreateDate" && cd == "") cd = val
|
||||||
|
else if (tag == "GPSLatitude") lat = val
|
||||||
|
else if (tag == "GPSLongitude") {
|
||||||
|
if (lat != "") {
|
||||||
|
n++
|
||||||
|
if (mode == "full") { plat[n] = lat; plon[n] = val }
|
||||||
|
else if (flat == "") { flat = lat; flon = val }
|
||||||
|
lat = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (tag == "GPSAltitude") { if (mode == "full" && n > 0) palt[n] = val }
|
||||||
|
else if (tag == "GPSSpeed") { if (mode == "full" && n > 0) pspd[n] = val }
|
||||||
|
|
||||||
|
if (mode != "full" && flat != "" && dt != "") {
|
||||||
|
print f "," dt "," flat "," flon ",,"; emis = 1; exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
d = (dt != "" ? dt : cd)
|
||||||
|
if (d == "") exit
|
||||||
|
if (mode == "full") { for (i = 1; i <= n; i++) print f "," d "," plat[i] "," plon[i] "," palt[i] "," pspd[i] }
|
||||||
|
else if (!emis && flat != "") print f "," d "," flat "," flon ",,"
|
||||||
|
}
|
||||||
|
'
|
||||||
|
|
||||||
|
# extrait_video <mode> <fichier> -> lignes CSV sur stdout
|
||||||
|
#
|
||||||
|
# Mode D (par defaut) : on demande les tags un par un et on parse la sortie
|
||||||
|
# « Tag : valeur ». C est la seule methode qui atteint reellement [Doc1] :
|
||||||
|
# `-p` ne voit que [Main], lequel porte la position mais PAS GPSDateTime, si
|
||||||
|
# bien qu un format reclamant la date ne produit jamais rien. Verifie sur une
|
||||||
|
# sortie exiftool reelle de GoPro Hero 9 (GX011569.MP4).
|
||||||
|
# En mode rapide, awk sort des qu il a une date et une position : exiftool
|
||||||
|
# recoit SIGPIPE et cesse de lire le fichier.
|
||||||
|
extrait_video() {
|
||||||
|
local m="$1" f="$2" nom
|
||||||
|
nom=$(basename "$f")
|
||||||
|
case "$m" in
|
||||||
|
D) exiftool -ee -n -q -s -api largefilesupport=1 \
|
||||||
|
-GPSDateTime -GPSLatitude -GPSLongitude -GPSAltitude -GPSSpeed -CreateDate \
|
||||||
|
"$f" 2>>"$ERRLOG" \
|
||||||
|
| awk -v f="\"$nom\"" -v mode="$VIDEO_GPS" "$PARSE_GPS" ;;
|
||||||
|
A) exiftool -ee -n -q -api largefilesupport=1 -p "$FMT_V_COMPLET" "$f" 2>>"$ERRLOG" ;;
|
||||||
|
B) exiftool -ee -n -q -api largefilesupport=1 -p "$FMT_V_MINIMAL" "$f" 2>>"$ERRLOG" | awk '{print $0 ",,"}' ;;
|
||||||
|
C) exiftool -ee3 -n -q -api largefilesupport=1 -p "$FMT_V_MINIMAL" "$f" 2>>"$ERRLOG" | awk '{print $0 ",,"}' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1. PHOTOS ---------------------------------------------------------------
|
||||||
|
: > "$CSV_PHOTOS"
|
||||||
if [ -d "$PHOTOS_DIR" ]; then
|
if [ -d "$PHOTOS_DIR" ]; then
|
||||||
for f in "$PHOTOS_DIR"/*.jp*g; do
|
echo "📸 Photos : analyse en cours…"
|
||||||
[ -e "$f" ] || continue
|
# UN SEUL appel exiftool pour tout le dossier : chaque lancement du binaire
|
||||||
# Utilisation du chemin complet pour exiftool mais basename pour le CSV
|
# coûte 150-250 ms (Perl). Mesuré sur 150 photos : 29,3 s → 0,97 s.
|
||||||
exiftool -n -p "$(basename "$f")"',$DateTimeOriginal,$GPSLatitude,$GPSLongitude,$GPSAltitude' "$f" >> "$CSV_PHOTOS"
|
exiftool -n -q -ext jpg -ext jpeg -p "$FMT_PHOTOS" "$PHOTOS_DIR" \
|
||||||
done
|
2>>"$ERRLOG" > "$TMPD/p1.csv"
|
||||||
|
|
||||||
|
# Seconde passe, format réduit : rattrape les photos écartées par la
|
||||||
|
# première faute d'altitude. Deux appels batch restent sous la seconde.
|
||||||
|
exiftool -n -q -ext jpg -ext jpeg -p "$FMT_PHOTOS_MIN" "$PHOTOS_DIR" \
|
||||||
|
2>>"$ERRLOG" | awk '{print $0 ","}' > "$TMPD/p2.csv"
|
||||||
|
|
||||||
|
# Fusion : tout p1, plus les fichiers de p2 absents de p1.
|
||||||
|
cat "$TMPD/p1.csv" > "$CSV_PHOTOS"
|
||||||
|
awk -F, 'NR==FNR {vu[$1]=1; next} !($1 in vu)' "$TMPD/p1.csv" "$TMPD/p2.csv" >> "$CSV_PHOTOS"
|
||||||
|
|
||||||
|
n1=$(nlines "$TMPD/p1.csv"); nt=$(nlines "$CSV_PHOTOS")
|
||||||
|
printf "📸 Photos : %d ligne(s)" "$nt"
|
||||||
|
[ "$nt" -gt "$n1" ] && printf " (dont %d récupérée(s) sans altitude)" "$((nt - n1))"
|
||||||
|
echo
|
||||||
else
|
else
|
||||||
echo "⚠️ Dossier photos non trouvé, étape ignorée."
|
echo "⚠️ Dossier photos non trouvé, étape ignorée."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. VIDÉOS (Fichiers .mp4, .mov avec flux GoPro)
|
# --- 2. VIDÉOS ---------------------------------------------------------------
|
||||||
echo "🎬 Traitement des vidéos dans : $RUSHS_DIR"
|
: > "$CSV_VIDEOS"
|
||||||
if [ -d "$RUSHS_DIR" ]; then
|
if [ ! -d "$RUSHS_DIR" ]; then
|
||||||
for f in "$RUSHS_DIR"/*.{mp4,mov}; do
|
echo "⚠️ Dossier rushs non trouvé, étape ignorée."
|
||||||
[ -e "$f" ] || continue
|
|
||||||
# Ta commande exiftool exacte pour le flux GoPro
|
|
||||||
exiftool -ee -n -p "$(basename "$f")"',$GPSDateTime,$GPSLatitude,$GPSLongitude,$GPSAltitude,$GPSSpeed' "$f" >> "$CSV_VIDEOS"
|
|
||||||
done
|
|
||||||
else
|
else
|
||||||
echo "⚠️ Dossier rushs non trouvé, étape ignorée."
|
rushs=()
|
||||||
|
for f in "$RUSHS_DIR"/*.mp4 "$RUSHS_DIR"/*.mov; do
|
||||||
|
[ -e "$f" ] || continue
|
||||||
|
# macOS depose un fichier AppleDouble « ._nom » a cote de chaque
|
||||||
|
# fichier sur les disques non-HFS (exFAT). Ce ne sont pas des videos.
|
||||||
|
case "$(basename "$f")" in ._*) continue ;; esac
|
||||||
|
rushs+=("$f")
|
||||||
|
done
|
||||||
|
total="${#rushs[@]}"
|
||||||
|
|
||||||
|
if [ "$total" -eq 0 ]; then
|
||||||
|
echo "⚠️ Aucun rush trouvé dans $RUSHS_DIR"
|
||||||
|
else
|
||||||
|
if [ "$VIDEO_GPS" = "full" ]; then
|
||||||
|
echo "🎬 Vidéos : $total rush(s), trace GPS COMPLÈTE"
|
||||||
|
echo " ⏳ Chaque fichier est lu intégralement."
|
||||||
|
else
|
||||||
|
echo "🎬 Vidéos : $total rush(s), premier point GPS seulement"
|
||||||
|
echo " (VIDEO_GPS=full pour dérouler la trace entière)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Choix du mode d'extraction ---
|
||||||
|
# Testé sur 3 rushs au maximum : inutile de parcourir 552 fichiers
|
||||||
|
# pour découvrir qu'un mode ne marche pas.
|
||||||
|
if [ -z "$VARIANTE" ]; then
|
||||||
|
echo " 🔎 recherche du mode d'extraction…"
|
||||||
|
for essai in D A B C; do
|
||||||
|
k=0
|
||||||
|
for f in "${rushs[@]}"; do
|
||||||
|
k=$((k + 1)); [ "$k" -gt 3 ] && break
|
||||||
|
if [ -n "$(extrait_video "$essai" "$f" | head -n 1)" ]; then
|
||||||
|
VARIANTE="$essai"; break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ -n "$VARIANTE" ] && break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${VARIANTE:-}" in
|
||||||
|
D) echo " ✔ mode D : lecture directe des tags (atteint [Doc1])" ;;
|
||||||
|
A) echo " ✔ mode A : format complet (date, lat, lon, alt, vitesse)" ;;
|
||||||
|
B) echo " ✔ mode B : format réduit (date, lat, lon)" ;;
|
||||||
|
C) echo " ✔ mode C : format réduit + -ee3 (sous-documents détaillés)" ;;
|
||||||
|
*) echo " ⚠️ Aucun mode ne donne de résultat sur les 3 premiers rushs."
|
||||||
|
echo " Diagnostic : exiftool -ee -G3 -n <un_rush> | grep -i gps"
|
||||||
|
VARIANTE=A ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
i=0; avec=0; sans=0
|
||||||
|
T0=$(date +%s)
|
||||||
|
for f in "${rushs[@]}"; do
|
||||||
|
i=$((i + 1))
|
||||||
|
printf " [%3d/%3d] %-28s " "$i" "$total" "$(basename "$f")"
|
||||||
|
avant=$(nlines "$CSV_VIDEOS")
|
||||||
|
|
||||||
|
if [ "$VIDEO_GPS" = "full" ]; then
|
||||||
|
extrait_video "$VARIANTE" "$f" >> "$CSV_VIDEOS"
|
||||||
|
else
|
||||||
|
# `head -n 1` ferme le tuyau : exiftool reçoit SIGPIPE et
|
||||||
|
# s'arrête sans lire la fin du fichier.
|
||||||
|
extrait_video "$VARIANTE" "$f" | head -n 1 >> "$CSV_VIDEOS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ajoutes=$(( $(nlines "$CSV_VIDEOS") - avant ))
|
||||||
|
if [ "$ajoutes" -gt 0 ]; then
|
||||||
|
if [ "$VIDEO_GPS" = "full" ]; then printf "✓ %d points\n" "$ajoutes"; else echo "✓"; fi
|
||||||
|
avec=$((avec + 1))
|
||||||
|
else
|
||||||
|
echo "— pas de GPS"; sans=$((sans + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
T1=$(date +%s); EL=$((T1 - T0))
|
||||||
|
|
||||||
|
printf "🎬 Vidéos : %d point(s) GPS, %d rush(s) localisé(s)" "$(nlines "$CSV_VIDEOS")" "$avec"
|
||||||
|
[ "$sans" -gt 0 ] && printf ", %d sans GPS" "$sans"
|
||||||
|
printf " — %d min %02d s\n" $((EL / 60)) $((EL % 60))
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# On désactive l'option nocaseglob
|
echo "✅ Générés : $CSV_PHOTOS et $CSV_VIDEOS"
|
||||||
shopt -u nocaseglob
|
|
||||||
|
|
||||||
echo "--- TERMINÉ ---"
|
|
||||||
echo "✅ Fichiers générés : $CSV_PHOTOS et $CSV_VIDEOS"
|
|
||||||
|
|||||||
Regular → Executable
+132
-13
@@ -10,6 +10,7 @@ import sys
|
|||||||
import markdown2
|
import markdown2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import html
|
import html
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
# --- 1. CONFIGURATION ---
|
# --- 1. CONFIGURATION ---
|
||||||
base_dir = sys.argv[1] if len(sys.argv) > 1 else "./"
|
base_dir = sys.argv[1] if len(sys.argv) > 1 else "./"
|
||||||
@@ -27,6 +28,32 @@ if not os.path.exists(html_output_dir):
|
|||||||
COULEURS_JOURS = ['#FF5733', '#2ECC71', '#3498DB', '#9B59B6', '#F1C40F', '#E67E22', '#1ABC9C', '#34495E']
|
COULEURS_JOURS = ['#FF5733', '#2ECC71', '#3498DB', '#9B59B6', '#F1C40F', '#E67E22', '#1ABC9C', '#34495E']
|
||||||
noms_colonnes = ['Fichier', 'Date_Heure', 'Latitude', 'Longitude', 'Altitude', 'Vitesse']
|
noms_colonnes = ['Fichier', 'Date_Heure', 'Latitude', 'Longitude', 'Altitude', 'Vitesse']
|
||||||
|
|
||||||
|
# --- ÉCHAPPEMENT ---
|
||||||
|
# Sans ça, un fichier nommé « col & vue.jpg » produit un lien cassé et du HTML
|
||||||
|
# invalide. quote() encode l'espace et l'esperluette pour les URL, escape()
|
||||||
|
# protège le texte affiché.
|
||||||
|
def est_sidecar(nom):
|
||||||
|
"""Fichiers parasites macOS.
|
||||||
|
|
||||||
|
Sur un disque non-HFS (exFAT d'un disque externe, cle USB, partage reseau),
|
||||||
|
macOS depose un fichier AppleDouble « ._nom » a cote de chaque fichier pour
|
||||||
|
y stocker les metadonnees. « ._Ete_2026-08-01.gpx » n'est donc pas un GPX
|
||||||
|
mais un blob binaire, et gpxpy echoue dessus avec une erreur de decodage.
|
||||||
|
On ecarte aussi .DS_Store et les fichiers caches.
|
||||||
|
"""
|
||||||
|
return nom.startswith('._') or nom.startswith('.')
|
||||||
|
|
||||||
|
|
||||||
|
def url(chemin):
|
||||||
|
"""Chemin sûr pour un attribut src/href."""
|
||||||
|
return html.escape(quote(chemin))
|
||||||
|
|
||||||
|
|
||||||
|
def txt(valeur):
|
||||||
|
"""Texte sûr pour l'affichage."""
|
||||||
|
return html.escape(str(valeur))
|
||||||
|
|
||||||
|
|
||||||
# --- FONCTIONS UTILES ---
|
# --- FONCTIONS UTILES ---
|
||||||
def nettoyer_et_trier(df):
|
def nettoyer_et_trier(df):
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
@@ -52,11 +79,44 @@ def create_pin(color, icon_name):
|
|||||||
def inject_common_assets(m):
|
def inject_common_assets(m):
|
||||||
custom_js = """
|
custom_js = """
|
||||||
<script>
|
<script>
|
||||||
|
// --- DÉTECTION HEVC ---
|
||||||
|
// Stratégie hybride : le 720p (H.264) est chargé par défaut et se lit
|
||||||
|
// partout. Seul le 1080p est en HEVC. Si le navigateur ne sait pas décoder
|
||||||
|
// le HEVC (Firefox principalement), on neutralise le bouton 1080p —
|
||||||
|
// la vidéo reste lisible, seule l'option haute qualité disparaît.
|
||||||
|
var HEVC_OK = (function () {
|
||||||
|
try {
|
||||||
|
var v = document.createElement('video');
|
||||||
|
var t = v.canPlayType('video/mp4; codecs="hvc1.1.6.L120.B0"');
|
||||||
|
return t === 'probably' || t === 'maybe';
|
||||||
|
} catch (e) { return false; }
|
||||||
|
})();
|
||||||
|
|
||||||
|
function applyCodecSupport(popup) {
|
||||||
|
if (HEVC_OK) return;
|
||||||
|
var btns = popup.querySelectorAll('.quality-btn[data-hevc="1"]');
|
||||||
|
for (var i = 0; i < btns.length; i++) {
|
||||||
|
btns[i].classList.add('qbtn-disabled');
|
||||||
|
btns[i].title = "Le 1080p est encodé en HEVC, que ce navigateur ne sait pas décoder. Essaie Safari, Chrome ou Edge.";
|
||||||
|
}
|
||||||
|
var note = popup.querySelector('.hevc-note');
|
||||||
|
if (note) note.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
// --- GESTION DES BOUTONS 720p / 1080p ---
|
// --- GESTION DES BOUTONS 720p / 1080p ---
|
||||||
function changeQuality(videoId, newSrc, btnElement) {
|
function changeQuality(videoId, newSrc, btnElement) {
|
||||||
|
if (btnElement.classList.contains('qbtn-disabled')) return;
|
||||||
|
|
||||||
var video = document.getElementById(videoId);
|
var video = document.getElementById(videoId);
|
||||||
|
|
||||||
if (video.src.includes(newSrc)) return;
|
// On compare les NOMS DE FICHIER, pas les chemins : video.src est une
|
||||||
|
// URL absolue résolue par le navigateur, alors que newSrc est relatif
|
||||||
|
// ("../videos/X.mp4"). L'ancien test `video.src.includes(newSrc)` ne
|
||||||
|
// pouvait donc jamais être vrai, et recliquer sur la qualité déjà
|
||||||
|
// active rechargeait la vidéo en repartant du début.
|
||||||
|
var nomActuel = decodeURIComponent(video.src).split('/').pop();
|
||||||
|
var nomVoulu = decodeURIComponent(newSrc).split('/').pop();
|
||||||
|
if (nomActuel === nomVoulu) return;
|
||||||
|
|
||||||
var savedTime = video.currentTime;
|
var savedTime = video.currentTime;
|
||||||
var wasPlaying = !video.paused;
|
var wasPlaying = !video.paused;
|
||||||
@@ -99,8 +159,15 @@ def inject_common_assets(m):
|
|||||||
mutation.addedNodes.forEach(function(node) {
|
mutation.addedNodes.forEach(function(node) {
|
||||||
// Si l'élément ajouté est une popup Leaflet
|
// Si l'élément ajouté est une popup Leaflet
|
||||||
if (node.nodeType === 1 && node.classList && node.classList.contains('leaflet-popup')) {
|
if (node.nodeType === 1 && node.classList && node.classList.contains('leaflet-popup')) {
|
||||||
|
applyCodecSupport(node);
|
||||||
var video = node.querySelector('video');
|
var video = node.querySelector('video');
|
||||||
if (video) {
|
if (video) {
|
||||||
|
// Filet de sécurité : si le 1080p HEVC échoue malgré la
|
||||||
|
// détection, on retombe automatiquement sur le 720p H.264.
|
||||||
|
video.addEventListener('error', function () {
|
||||||
|
var fb = this.getAttribute('data-fallback');
|
||||||
|
if (fb && this.src.indexOf(fb) === -1) { this.src = fb; this.load(); }
|
||||||
|
});
|
||||||
console.log("🎬 Popup ouverte : Lancement de la vidéo...");
|
console.log("🎬 Popup ouverte : Lancement de la vidéo...");
|
||||||
var playPromise = video.play();
|
var playPromise = video.play();
|
||||||
if (playPromise !== undefined) {
|
if (playPromise !== undefined) {
|
||||||
@@ -170,6 +237,17 @@ def inject_common_assets(m):
|
|||||||
.active-720 { background-color: #2ECC71 !important; color: white; border-color: #27ae60 !important; opacity: 1 !important; }
|
.active-720 { background-color: #2ECC71 !important; color: white; border-color: #27ae60 !important; opacity: 1 !important; }
|
||||||
.active-1080 { background-color: #007AFF !important; color: white; border-color: #0056b3 !important; opacity: 1 !important; }
|
.active-1080 { background-color: #007AFF !important; color: white; border-color: #0056b3 !important; opacity: 1 !important; }
|
||||||
|
|
||||||
|
.qbtn-disabled {
|
||||||
|
opacity: 0.35 !important; cursor: not-allowed !important;
|
||||||
|
text-decoration: line-through; background: #eee !important;
|
||||||
|
}
|
||||||
|
.qbtn-disabled:hover { opacity: 0.35 !important; background: #eee !important; }
|
||||||
|
.hevc-note {
|
||||||
|
display: none; margin-top: 6px; font-family: sans-serif; font-size: 11px;
|
||||||
|
color: #8a6d3b; background: #fcf8e3; border-radius: 6px; padding: 6px 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.leaflet-top.leaflet-left { top: auto !important; bottom: 20px !important; left: 15px !important; }
|
.leaflet-top.leaflet-left { top: auto !important; bottom: 20px !important; left: 15px !important; }
|
||||||
.leaflet-popup-content-wrapper { border-radius: 12px; max-width: 95vw !important; }
|
.leaflet-popup-content-wrapper { border-radius: 12px; max-width: 95vw !important; }
|
||||||
#journal-panel {
|
#journal-panel {
|
||||||
@@ -211,6 +289,8 @@ try:
|
|||||||
jours_gpx = set()
|
jours_gpx = set()
|
||||||
if os.path.exists(routes_dir):
|
if os.path.exists(routes_dir):
|
||||||
for f in os.listdir(routes_dir):
|
for f in os.listdir(routes_dir):
|
||||||
|
if est_sidecar(f):
|
||||||
|
continue
|
||||||
match = re.search(r'(\d{4}-\d{2}-\d{2})', f)
|
match = re.search(r'(\d{4}-\d{2}-\d{2})', f)
|
||||||
if match: jours_gpx.add(pd.to_datetime(match.group(1)).date())
|
if match: jours_gpx.add(pd.to_datetime(match.group(1)).date())
|
||||||
tous_les_jours = sorted(list(set(df_v['Jour'].unique()) | set(df_p['Jour'].unique()) | jours_gpx))
|
tous_les_jours = sorted(list(set(df_v['Jour'].unique()) | set(df_p['Jour'].unique()) | jours_gpx))
|
||||||
@@ -240,12 +320,18 @@ for i, jour in enumerate(tous_les_jours):
|
|||||||
|
|
||||||
marker_cluster = MarkerCluster(name="Médias").add_to(m_day)
|
marker_cluster = MarkerCluster(name="Médias").add_to(m_day)
|
||||||
day_coords = []
|
day_coords = []
|
||||||
|
media_coords = [] # position des photos/vidéos, filet de cadrage
|
||||||
current_gpx_filename = None
|
current_gpx_filename = None
|
||||||
|
|
||||||
if os.path.exists(routes_dir):
|
if os.path.exists(routes_dir):
|
||||||
for gpx_file in os.listdir(routes_dir):
|
for gpx_file in os.listdir(routes_dir):
|
||||||
if gpx_file.startswith(day_str) and gpx_file.endswith('.gpx'):
|
# `day_str in gpx_file` et non `.startswith(day_str)` : la liste des
|
||||||
current_gpx_filename = gpx_file
|
# jours est bâtie avec re.search (date N'IMPORTE OÙ dans le nom),
|
||||||
|
# donc une trace « trace_2026-07-15_col.gpx » créait bien un jour
|
||||||
|
# mais n'était jamais retrouvée ici — jour vide, carte non cadrée.
|
||||||
|
if est_sidecar(gpx_file):
|
||||||
|
continue
|
||||||
|
if day_str in gpx_file and gpx_file.lower().endswith('.gpx'):
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(routes_dir, gpx_file), 'r') as f:
|
with open(os.path.join(routes_dir, gpx_file), 'r') as f:
|
||||||
gpx = gpxpy.parse(f)
|
gpx = gpxpy.parse(f)
|
||||||
@@ -259,12 +345,20 @@ for i, jour in enumerate(tous_les_jours):
|
|||||||
folium.PolyLine(pts, color=color_day, weight=7, opacity=0.8, popup=folium.Popup(popup_html, max_width=200)).add_to(m_global)
|
folium.PolyLine(pts, color=color_day, weight=7, opacity=0.8, popup=folium.Popup(popup_html, max_width=200)).add_to(m_global)
|
||||||
folium.PolyLine(pts, color=color_day, weight=3, opacity=0.8).add_to(m_mini)
|
folium.PolyLine(pts, color=color_day, weight=3, opacity=0.8).add_to(m_mini)
|
||||||
folium.PolyLine(pts, color="#FF0000", weight=4, opacity=0.8).add_to(m_day)
|
folium.PolyLine(pts, color="#FF0000", weight=4, opacity=0.8).add_to(m_day)
|
||||||
except: pass
|
# Le nom n'est retenu qu'APRES analyse reussie : sinon le
|
||||||
|
# bouton de telechargement pouvait pointer sur un fichier
|
||||||
|
# illisible traite apres la vraie trace.
|
||||||
|
current_gpx_filename = gpx_file
|
||||||
|
except Exception as e:
|
||||||
|
# `except: pass` avalait TOUT, y compris un Ctrl-C, et
|
||||||
|
# masquait les GPX corrompus : la trace disparaissait sans
|
||||||
|
# le moindre message.
|
||||||
|
print(f" ⚠️ GPX illisible ({gpx_file}) : {e}")
|
||||||
|
|
||||||
m_day.get_root().html.add_child(folium.Element('<div style="position: fixed; top: 15px; left: 15px; z-index: 10001;"><a href="../index.html" target="_top" style="text-decoration: none; background: #333; color: white; padding: 12px 20px; border-radius: 10px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 10px rgba(0,0,0,0.5); border: 2px solid white; display: block;">🏠 Accueil</a></div>' + get_journal_ui()))
|
m_day.get_root().html.add_child(folium.Element('<div style="position: fixed; top: 15px; left: 15px; z-index: 10001;"><a href="../index.html" target="_top" style="text-decoration: none; background: #333; color: white; padding: 12px 20px; border-radius: 10px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 10px rgba(0,0,0,0.5); border: 2px solid white; display: block;">🏠 Accueil</a></div>' + get_journal_ui()))
|
||||||
|
|
||||||
if current_gpx_filename:
|
if current_gpx_filename:
|
||||||
m_day.get_root().html.add_child(folium.Element(f'<div style="position: fixed; bottom: 85px; right: 15px; z-index: 10001;"><a href="../routes/{current_gpx_filename}" download style="text-decoration: none; background: #28a745; color: white; padding: 12px 20px; border-radius: 50px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.4); border: 2px solid white; display: block;">📥 GPX</a></div>'))
|
m_day.get_root().html.add_child(folium.Element(f'<div style="position: fixed; bottom: 85px; right: 15px; z-index: 10001;"><a href="../routes/{url(current_gpx_filename)}" download style="text-decoration: none; background: #28a745; color: white; padding: 12px 20px; border-radius: 50px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.4); border: 2px solid white; display: block;">📥 GPX</a></div>'))
|
||||||
|
|
||||||
# REGROUPEMENT PHOTOS
|
# REGROUPEMENT PHOTOS
|
||||||
day_p = df_p[df_p['Jour'] == jour].copy()
|
day_p = df_p[df_p['Jour'] == jour].copy()
|
||||||
@@ -276,8 +370,14 @@ for i, jour in enumerate(tous_les_jours):
|
|||||||
groupe = day_p[masque]
|
groupe = day_p[masque]
|
||||||
fichiers = groupe['Fichier'].tolist()
|
fichiers = groupe['Fichier'].tolist()
|
||||||
nb = len(fichiers)
|
nb = len(fichiers)
|
||||||
slides = "".join([f'<div class="slide" style="display:{"block" if idx == 0 else "none"};"><div class="file-header">{p.strip()}</div><a href="../photos/{p.strip()}" target="_blank"><img src="../photos/{p.strip()}" loading="lazy"></a></div>' for idx, p in enumerate(fichiers)])
|
slides = "".join([
|
||||||
|
f'<div class="slide" style="display:{"block" if idx == 0 else "none"};">'
|
||||||
|
f'<div class="file-header">{txt(p.strip())}</div>'
|
||||||
|
f'<a href="../photos/{url(p.strip())}" target="_blank">'
|
||||||
|
f'<img src="../photos/{url(p.strip())}" alt="{txt(p.strip())}" loading="lazy"></a></div>'
|
||||||
|
for idx, p in enumerate(fichiers)])
|
||||||
btns = f'<button class="nav-btn prev" onclick="moveSlide(this, -1)">❮</button><button class="nav-btn next" onclick="moveSlide(this, 1)">❯</button><div class="slide-counter">1/{nb}</div>' if nb > 1 else ""
|
btns = f'<button class="nav-btn prev" onclick="moveSlide(this, -1)">❮</button><button class="nav-btn next" onclick="moveSlide(this, 1)">❯</button><div class="slide-counter">1/{nb}</div>' if nb > 1 else ""
|
||||||
|
media_coords.append([ref['Latitude'], ref['Longitude']])
|
||||||
folium.Marker(location=[ref['Latitude'], ref['Longitude']], popup=folium.Popup(f'<div class="slider-container"><div class="slides">{slides}</div>{btns}</div>', max_width="100%"), icon=create_pin("#FF3B30", "camera" if nb==1 else "images")).add_to(marker_cluster)
|
folium.Marker(location=[ref['Latitude'], ref['Longitude']], popup=folium.Popup(f'<div class="slider-container"><div class="slides">{slides}</div>{btns}</div>', max_width="100%"), icon=create_pin("#FF3B30", "camera" if nb==1 else "images")).add_to(marker_cluster)
|
||||||
day_p = day_p[~masque]
|
day_p = day_p[~masque]
|
||||||
|
|
||||||
@@ -291,8 +391,9 @@ for i, jour in enumerate(tous_les_jours):
|
|||||||
clean_id = re.sub(r'[^a-zA-Z0-9]', '', base_name)
|
clean_id = re.sub(r'[^a-zA-Z0-9]', '', base_name)
|
||||||
video_id = f"vid_{clean_id}"
|
video_id = f"vid_{clean_id}"
|
||||||
|
|
||||||
desktop_file = f"../videos/{base_name}.mp4"
|
desktop_file = f"../videos/{url(base_name)}.mp4"
|
||||||
mobile_file = f"../videos/{base_name}_mobile.mp4"
|
mobile_file = f"../videos/{url(base_name)}_mobile.mp4"
|
||||||
|
media_coords.append(pts_v[0])
|
||||||
|
|
||||||
# Ajout 'autoplay muted' pour le lancement au clic
|
# Ajout 'autoplay muted' pour le lancement au clic
|
||||||
# Ajout 'preload=none' pour ne pas charger les autres
|
# Ajout 'preload=none' pour ne pas charger les autres
|
||||||
@@ -301,11 +402,13 @@ for i, jour in enumerate(tous_les_jours):
|
|||||||
f'<div style="width:calc(85vw - 40px); max-width:1000px; text-align:center;">'
|
f'<div style="width:calc(85vw - 40px); max-width:1000px; text-align:center;">'
|
||||||
|
|
||||||
f'<div class="header-row">'
|
f'<div class="header-row">'
|
||||||
f'<span class="file-name" title="{v_name.strip()}">{v_name.strip()}</span>'
|
f'<span class="file-name" title="{txt(v_name.strip())}">{txt(v_name.strip())}</span>'
|
||||||
f'<div class="quality-controls">'
|
f'<div class="quality-controls">'
|
||||||
# Le bouton 720p est actif par défaut (cohérent avec le fichier chargé)
|
# Le bouton 720p est actif par défaut (cohérent avec le fichier chargé)
|
||||||
f'<span class="quality-btn active-720" onclick="changeQuality(\'{video_id}\', \'{mobile_file}\', this)">720p</span>'
|
f'<span class="quality-btn active-720" onclick="changeQuality(\'{video_id}\', \'{mobile_file}\', this)">720p</span>'
|
||||||
f'<span class="quality-btn" onclick="changeQuality(\'{video_id}\', \'{desktop_file}\', this)">1080p</span>'
|
# data-hevc : ce palier est en H.265, il sera grisé si le
|
||||||
|
# navigateur ne sait pas le décoder.
|
||||||
|
f'<span class="quality-btn" data-hevc="1" onclick="changeQuality(\'{video_id}\', \'{desktop_file}\', this)">1080p</span>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
|
|
||||||
@@ -314,12 +417,28 @@ for i, jour in enumerate(tous_les_jours):
|
|||||||
# 3. Pas d'autoplay.
|
# 3. Pas d'autoplay.
|
||||||
f'<video id="{video_id}" style="width:100%; max-height:70vh; border-radius:8px; background:black;" '
|
f'<video id="{video_id}" style="width:100%; max-height:70vh; border-radius:8px; background:black;" '
|
||||||
f'controls playsinline webkit-playsinline preload="none" '
|
f'controls playsinline webkit-playsinline preload="none" '
|
||||||
f'src="{mobile_file}">' # <-- On pointe directement sur le fichier léger
|
f'src="{mobile_file}" data-fallback="{mobile_file}">' # <-- 720p H.264 : lisible partout
|
||||||
f'</video></div>'
|
f'</video>'
|
||||||
|
|
||||||
|
# Note discrète affichée uniquement si le 1080p est indisponible
|
||||||
|
f'<div class="hevc-note">'
|
||||||
|
f'Le 1080p est encodé en H.265/HEVC, que ce navigateur ne sait pas décoder. '
|
||||||
|
f'La lecture en 720p fonctionne normalement ; pour la haute définition, '
|
||||||
|
f'utilise Safari, Chrome ou Edge.'
|
||||||
|
f'</div>'
|
||||||
|
|
||||||
|
f'</div>'
|
||||||
)
|
)
|
||||||
folium.Marker(location=pts_v[0], popup=folium.Popup(v_pop, max_width="100%"), icon=create_pin("#007AFF", "play")).add_to(marker_cluster)
|
folium.Marker(location=pts_v[0], popup=folium.Popup(v_pop, max_width="100%"), icon=create_pin("#007AFF", "play")).add_to(marker_cluster)
|
||||||
|
|
||||||
if day_coords: m_day.fit_bounds(day_coords, padding=(50, 50))
|
# Cadrage : la trace GPX en priorité, sinon la position des médias.
|
||||||
|
# Sans ce repli, un jour sans GPX (mais avec photos) s'ouvrait sur une
|
||||||
|
# mappemonde centrée au large de l'Afrique.
|
||||||
|
cadrage = day_coords if day_coords else media_coords
|
||||||
|
if cadrage:
|
||||||
|
m_day.fit_bounds(cadrage, padding=(50, 50))
|
||||||
|
all_global_coords.extend(media_coords)
|
||||||
|
|
||||||
folium.LayerControl(position='topright').add_to(m_day)
|
folium.LayerControl(position='topright').add_to(m_day)
|
||||||
plugins.Fullscreen(position='topright').add_to(m_day)
|
plugins.Fullscreen(position='topright').add_to(m_day)
|
||||||
m_day.save(os.path.join(html_output_dir, file_name))
|
m_day.save(os.path.join(html_output_dir, file_name))
|
||||||
|
|||||||
Regular → Executable
+130
-40
@@ -1,48 +1,138 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
"""Page d'accueil listant tous les voyages du site.
|
||||||
|
|
||||||
EXCLURE = ['html', 'Scripts', '__pycache__', '.git']
|
Usage : ./genere_index_general.py [racine_du_site]
|
||||||
|
|
||||||
|
Un voyage est reconnu à la présence de son fichier mini.html (la carte
|
||||||
|
miniature produite par genere_carte.py).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import html
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
EXCLURE = {'html', 'Scripts', '__pycache__', '.git', 'node_modules', 'videos',
|
||||||
|
'photos', 'rushs', 'routes'}
|
||||||
OUTPUT_FILE = "index.html"
|
OUTPUT_FILE = "index.html"
|
||||||
|
|
||||||
def generer_index_general():
|
|
||||||
road_trips = []
|
|
||||||
for d in sorted(os.listdir('.')):
|
|
||||||
if os.path.isdir(d) and d not in EXCLURE:
|
|
||||||
# On cherche mini.html en priorité
|
|
||||||
if os.path.exists(os.path.join(d, 'mini.html')):
|
|
||||||
road_trips.append(d)
|
|
||||||
|
|
||||||
html_content = f"""
|
def cle_de_tri(nom):
|
||||||
<!DOCTYPE html>
|
"""Trie du plus récent au plus ancien quand un millésime est détecté
|
||||||
<html lang="fr">
|
dans le nom du dossier, alphabétiquement sinon."""
|
||||||
<head>
|
m = re.search(r'(19|20)\d{2}', nom)
|
||||||
<meta charset="UTF-8">
|
annee = int(m.group(0)) if m else 0
|
||||||
<title>Nos Road-Trips</title>
|
return (-annee, nom.lower())
|
||||||
<style>
|
|
||||||
body {{ font-family: 'Segoe UI', sans-serif; background: #f4f4f9; margin: 0; padding: 20px; }}
|
|
||||||
.grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 30px; max-width: 1200px; margin: 0 auto; }}
|
def lien(trip, suffixe=""):
|
||||||
.card {{ background: white; border-radius: 15px; overflow: hidden; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }}
|
"""URL sûre : les espaces et les & des noms de dossier sont encodés."""
|
||||||
.map-container {{ width: 100%; height: 300px; border: none; pointer-events: none; }}
|
return html.escape(quote(trip) + suffixe)
|
||||||
.card-content {{ padding: 20px; text-align: center; }}
|
|
||||||
.btn {{ display: inline-block; padding: 10px 25px; background: #3498db; color: white; text-decoration: none; border-radius: 25px; font-weight: bold; }}
|
|
||||||
</style>
|
def generer_index_general(racine="."):
|
||||||
</head>
|
road_trips = []
|
||||||
<body>
|
for d in sorted(os.listdir(racine)):
|
||||||
<h1 style="text-align:center;">🌍 Nos Road-Trips</h1>
|
if d in EXCLURE or d.startswith('.'):
|
||||||
<div class="grid">
|
continue
|
||||||
"""
|
chemin = os.path.join(racine, d)
|
||||||
|
if os.path.isdir(chemin) and os.path.exists(os.path.join(chemin, 'mini.html')):
|
||||||
|
road_trips.append(d)
|
||||||
|
|
||||||
|
road_trips.sort(key=cle_de_tri)
|
||||||
|
|
||||||
|
cartes = []
|
||||||
for trip in road_trips:
|
for trip in road_trips:
|
||||||
html_content += f"""
|
# Le nom est échappé : un dossier « rando & vercors » produisait
|
||||||
<div class="card">
|
# sinon du HTML invalide (& non échappé) et un lien cassé.
|
||||||
<iframe class="map-container" src="{trip}/mini.html"></iframe>
|
titre = html.escape(trip.replace('-', ' ').replace('_', ' '))
|
||||||
<div class="card-content">
|
cartes.append(f""" <article class="card">
|
||||||
<h2 style="text-transform: capitalize;">{trip.replace('-', ' ')}</h2>
|
<iframe class="map-container" src="{lien(trip, '/mini.html')}"
|
||||||
<a href="{trip}/index.html" class="btn">Voir le voyage</a>
|
loading="lazy" title="Carte de {titre}"></iframe>
|
||||||
</div>
|
<div class="card-content">
|
||||||
</div>
|
<h2>{titre}</h2>
|
||||||
"""
|
<a href="{lien(trip, '/index.html')}" class="btn">Voir le voyage</a>
|
||||||
html_content += "</div></body></html>"
|
</div>
|
||||||
with open(OUTPUT_FILE, "w", encoding="utf-8") as f: f.write(html_content)
|
</article>""")
|
||||||
|
|
||||||
|
if road_trips:
|
||||||
|
corps = '<div class="grid">\n' + "\n".join(cartes) + '\n </div>'
|
||||||
|
sous_titre = f"{len(road_trips)} voyage" + ("s" if len(road_trips) > 1 else "")
|
||||||
|
else:
|
||||||
|
corps = ('<p class="vide">Aucun voyage pour l\'instant.<br>'
|
||||||
|
'Lance <code>genere_site.sh</code> pour en générer.</p>')
|
||||||
|
sous_titre = ""
|
||||||
|
|
||||||
|
# Pas d'espace avant <!DOCTYPE html> : la version précédente commençait par
|
||||||
|
# un saut de ligne et de l'indentation, ce qui bascule certains navigateurs
|
||||||
|
# en mode « quirks » et fausse la mise en page.
|
||||||
|
html_content = f"""<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Nos Road-Trips</title>
|
||||||
|
<style>
|
||||||
|
:root {{ color-scheme: light dark; }}
|
||||||
|
* {{ box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
background: #f4f4f9; margin: 0; padding: 20px; color: #222;
|
||||||
|
}}
|
||||||
|
header {{ text-align: center; margin-bottom: 30px; }}
|
||||||
|
h1 {{ margin: 0 0 6px; font-size: clamp(1.6rem, 4vw, 2.4rem); }}
|
||||||
|
.sous-titre {{ color: #777; font-size: 0.9rem; }}
|
||||||
|
/* min() évite le débordement horizontal sur téléphone : l'ancienne règle
|
||||||
|
minmax(400px, 1fr) forçait 400 px de large sur un écran de 375 px. */
|
||||||
|
.grid {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(min(400px, 100%), 1fr));
|
||||||
|
gap: 30px; max-width: 1200px; margin: 0 auto;
|
||||||
|
}}
|
||||||
|
.card {{
|
||||||
|
background: #fff; border-radius: 15px; overflow: hidden;
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||||
|
transition: transform .2s, box-shadow .2s;
|
||||||
|
}}
|
||||||
|
.card:hover {{ transform: translateY(-4px); box-shadow: 0 14px 28px rgba(0,0,0,0.15); }}
|
||||||
|
/* loading="lazy" : sans ça, ouvrir la page chargeait une carte Leaflet
|
||||||
|
complète par voyage, d'un coup. */
|
||||||
|
.map-container {{ width: 100%; height: 300px; border: none; pointer-events: none; display: block; }}
|
||||||
|
.card-content {{ padding: 20px; text-align: center; }}
|
||||||
|
h2 {{ text-transform: capitalize; margin: 0 0 14px; font-size: 1.25rem; }}
|
||||||
|
.btn {{
|
||||||
|
display: inline-block; padding: 10px 25px; background: #3498db; color: #fff;
|
||||||
|
text-decoration: none; border-radius: 25px; font-weight: bold;
|
||||||
|
}}
|
||||||
|
.btn:hover {{ background: #2980b9; }}
|
||||||
|
.vide {{ text-align: center; color: #777; margin-top: 60px; line-height: 1.7; }}
|
||||||
|
code {{ background: #e8e8ef; padding: 2px 6px; border-radius: 4px; }}
|
||||||
|
@media (prefers-color-scheme: dark) {{
|
||||||
|
body {{ background: #16161a; color: #e8e8ea; }}
|
||||||
|
.card {{ background: #232329; box-shadow: 0 10px 20px rgba(0,0,0,0.4); }}
|
||||||
|
.sous-titre, .vide {{ color: #9a9aa2; }}
|
||||||
|
code {{ background: #2e2e36; }}
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>🌍 Nos Road-Trips</h1>
|
||||||
|
<div class="sous-titre">{sous_titre}</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{corps}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
sortie = os.path.join(racine, OUTPUT_FILE)
|
||||||
|
with open(sortie, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html_content)
|
||||||
|
print(f"🌍 Index général : {len(road_trips)} voyage(s) → {sortie}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
generer_index_general()
|
racine = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||||
|
generer_index_general(racine)
|
||||||
|
|||||||
+178
-11
@@ -1,16 +1,183 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# GÉNÉRATION DU SITE — orchestre la chaîne complète
|
||||||
|
#
|
||||||
|
# Pour chaque dossier de voyage :
|
||||||
|
# 1. extraction_infos.sh → CSV des métadonnées EXIF/GPS
|
||||||
|
# 2. compress.sh → encodage web des rushs
|
||||||
|
# 3. genere_carte.py → cartes du voyage
|
||||||
|
# Puis, UNE SEULE FOIS à la fin :
|
||||||
|
# 4. genere_index_general.py → page d'accueil listant tous les voyages
|
||||||
|
#
|
||||||
|
# Un dossier est considéré comme un voyage s'il contient AU MOINS un des
|
||||||
|
# sous-dossiers rushs/, routes/ ou photos/. Les manquants sont signalés et
|
||||||
|
# l'étape correspondante est simplement sautée (un voyage sans vidéo ou sans
|
||||||
|
# trace GPX reste parfaitement valable).
|
||||||
|
#
|
||||||
|
# Usage :
|
||||||
|
# ./genere_site.sh # tous les voyages
|
||||||
|
# ./genere_site.sh Ete-2026 # un seul voyage
|
||||||
|
# ./genere_site.sh Ete-2026 corse # plusieurs
|
||||||
|
#
|
||||||
|
# Variables : celles de compress.sh (JOBS, ENCODER, CRF_HEVC…) sont
|
||||||
|
# transmises telles quelles.
|
||||||
|
# SKIP_COMPRESS=1 régénère les cartes sans réencoder les vidéos
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
# Parcourir tous les éléments du répertoire courant
|
set -uo pipefail
|
||||||
for dir in */; do
|
shopt -s nullglob
|
||||||
# Supprimer le slash final pour l'affichage
|
|
||||||
dir=${dir%/}
|
|
||||||
|
|
||||||
# Vérifier si les trois sous-répertoires existent à l'intérieur
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
if [[ -d "$dir/rushs" && -d "$dir/routes" && -d "$dir/photos" ]]; then
|
# La racine du site est le dossier PARENT de Scripts/. On ne dépend donc pas
|
||||||
echo "✅ Dossier conforme trouvé : $dir"
|
# du répertoire courant : le script marche depuis n'importe où.
|
||||||
Scripts/extraction_infos.sh $dir
|
ROOT="${ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||||
Scripts/compress.sh $dir
|
SKIP_COMPRESS="${SKIP_COMPRESS:-0}"
|
||||||
Scripts/genere_carte.py $dir
|
|
||||||
|
cd "$ROOT" || { echo "❌ Racine introuvable : $ROOT"; exit 1; }
|
||||||
|
|
||||||
|
for s in extraction_infos.sh compress.sh genere_carte.py genere_index_general.py; do
|
||||||
|
if [ ! -f "$SCRIPT_DIR/$s" ]; then
|
||||||
|
echo "❌ Script manquant : $SCRIPT_DIR/$s"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
Scripts/genere_index_general.py
|
[ -x "$SCRIPT_DIR/$s" ] || chmod +x "$SCRIPT_DIR/$s" 2>/dev/null
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --- Diagnostic d'un sous-dossier -------------------------------------------
|
||||||
|
# `ls -l` affiche un lien symbolique CASSÉ exactement comme un lien valide :
|
||||||
|
# rien ne le distingue à l'œil. `test -d` renvoie faux dessus, d'où des
|
||||||
|
# « dossier manquant » incompréhensibles. On sépare donc les deux cas.
|
||||||
|
etat_dossier() {
|
||||||
|
if [ -d "$1" ]; then echo ok
|
||||||
|
elif [ -L "$1" ]; then echo lien_casse
|
||||||
|
else echo absent
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Résout la cible d'un lien en chemin absolu, pour pouvoir la montrer.
|
||||||
|
resoudre_lien() {
|
||||||
|
python3 -c 'import os,sys
|
||||||
|
p = sys.argv[1]
|
||||||
|
print(os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(p)), os.readlink(p))))' "$1" 2>/dev/null \
|
||||||
|
|| readlink "$1" 2>/dev/null \
|
||||||
|
|| echo "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
echo " GÉNÉRATION DU SITE"
|
||||||
|
echo " Racine : $ROOT"
|
||||||
|
echo "=================================================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Sélection des voyages ---------------------------------------------------
|
||||||
|
candidats=()
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
EXPLICITE=1
|
||||||
|
for d in "$@"; do candidats+=("${d%/}"); done
|
||||||
|
else
|
||||||
|
EXPLICITE=0
|
||||||
|
for d in */; do candidats+=("${d%/}"); done
|
||||||
|
fi
|
||||||
|
|
||||||
|
voyages=(); ignores=0
|
||||||
|
for dir in "${candidats[@]+"${candidats[@]}"}"; do
|
||||||
|
[ "$dir" = "Scripts" ] && continue
|
||||||
|
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
[ "$EXPLICITE" = "1" ] && echo "❌ $dir : ce dossier n'existe pas."
|
||||||
|
ignores=$((ignores + 1)); continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
presents=0; details=""
|
||||||
|
for sub in rushs routes photos; do
|
||||||
|
case "$(etat_dossier "$dir/$sub")" in
|
||||||
|
ok)
|
||||||
|
presents=$((presents + 1)) ;;
|
||||||
|
lien_casse)
|
||||||
|
# Toujours signalé, même en mode automatique : un lien cassé
|
||||||
|
# est une erreur à corriger, pas un dossier qu'on ignore.
|
||||||
|
echo "🔗 $dir/$sub : LIEN SYMBOLIQUE CASSÉ"
|
||||||
|
echo " pointe vers : $(resoudre_lien "$dir/$sub")"
|
||||||
|
echo " → cette cible n'existe pas. Le lien a probablement été"
|
||||||
|
echo " créé en relatif puis le dossier a été déplacé."
|
||||||
|
echo " Correction (avec un chemin absolu, plus robuste) :"
|
||||||
|
echo " rm '$dir/$sub' && ln -s /chemin/absolu/vers/$sub '$dir/$sub'"
|
||||||
|
details="$details $sub(lien cassé)" ;;
|
||||||
|
absent)
|
||||||
|
details="$details $sub(absent)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$presents" -gt 0 ]; then
|
||||||
|
voyages+=("$dir")
|
||||||
|
[ -n "$details" ] && echo "⚠️ $dir : sous-dossier(s) indisponible(s) :$details — étape(s) sautée(s)"
|
||||||
|
else
|
||||||
|
[ "$EXPLICITE" = "1" ] && echo "❌ $dir : aucun de rushs/ routes/ photos/ n'est utilisable :$details"
|
||||||
|
ignores=$((ignores + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#voyages[@]}" -eq 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "❌ Aucun voyage à traiter."
|
||||||
|
echo " Un voyage doit contenir au moins un de : rushs/ routes/ photos/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "📁 ${#voyages[@]} voyage(s) à traiter : ${voyages[*]}"
|
||||||
|
[ "$ignores" -gt 0 ] && [ "$EXPLICITE" = "0" ] && echo " ($ignores dossier(s) ignoré(s), ce ne sont pas des voyages)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Traitement --------------------------------------------------------------
|
||||||
|
ok=0; ko=0
|
||||||
|
T0=$(date +%s)
|
||||||
|
|
||||||
|
for dir in "${voyages[@]}"; do
|
||||||
|
echo "=================================================="
|
||||||
|
echo "▶ $dir"
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
if ! "$SCRIPT_DIR/extraction_infos.sh" "$dir"; then
|
||||||
|
echo "❌ Extraction échouée sur $dir — voyage ignoré."
|
||||||
|
ko=$((ko + 1)); echo; continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SKIP_COMPRESS" = "1" ]; then
|
||||||
|
echo "⏭️ Compression sautée (SKIP_COMPRESS=1)"
|
||||||
|
elif [ "$(etat_dossier "$dir/rushs")" != "ok" ]; then
|
||||||
|
echo "⏭️ Compression sautée : pas de dossier rushs/ utilisable."
|
||||||
|
elif ! "$SCRIPT_DIR/compress.sh" "$dir"; then
|
||||||
|
echo "⚠️ Compression incomplète sur $dir — on continue,"
|
||||||
|
echo " les cartes seront générées avec les vidéos disponibles."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! "$SCRIPT_DIR/genere_carte.py" "$dir"; then
|
||||||
|
echo "❌ Génération des cartes échouée sur $dir."
|
||||||
|
ko=$((ko + 1)); echo; continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok=$((ok + 1))
|
||||||
|
echo "✅ $dir terminé"
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Index général : UNE SEULE FOIS, après la boucle -------------------------
|
||||||
|
# (l'ancienne version l'appelait à chaque itération, y compris pour les
|
||||||
|
# dossiers non conformes — soit N fois le même travail.)
|
||||||
|
echo "=================================================="
|
||||||
|
echo "▶ Index général"
|
||||||
|
echo "=================================================="
|
||||||
|
"$SCRIPT_DIR/genere_index_general.py" "$ROOT"
|
||||||
|
|
||||||
|
T1=$(date +%s); EL=$((T1 - T0))
|
||||||
|
echo
|
||||||
|
echo "=================================================="
|
||||||
|
printf " ✅ %d voyage(s) généré(s)" "$ok"
|
||||||
|
[ "$ko" -gt 0 ] && printf ", ❌ %d en échec" "$ko"
|
||||||
|
printf " — %d min %02d s\n" $((EL / 60)) $((EL % 60))
|
||||||
|
echo " 🌍 Ouvre index.html à la racine du site."
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
[ "$ko" -gt 0 ] && exit 1
|
||||||
|
exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user