- Mise en page sortie du Python : Scripts/templates/ (Jinja2 + CSS/JS communs copiés dans assets/ de chaque voyage) - Voyages rangés dans un sous-dossier (Balades/…) : rubriques sur l'accueil, 2 plus récents par rubrique, pages voyages.html et balades.html - prepare_web.py : photos allégées (photos_web/) et images d'aperçu des vidéos (apercus/) - Voyage d'une seule journée : la page du voyage est la carte du jour - Carte globale de l'accueil (OpenStreetMap) : voyages du site + cartes Google My Maps récupérées (mymaps.txt, _mymaps/), moto par voyage (moto.txt) - KML par voyage pour My Maps - compress.sh : réencode une sortie illisible (encodage interrompu) ; repli automatique sur l'autre qualité dans la page - Corrections : CSV vide (voyage sans photo), racine de genere_site.sh, fichiers ._ de macOS - README réécrit, commentaires allégés Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
275 lines
11 KiB
Bash
Executable File
275 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
||
# =============================================================================
|
||
# COMPRESSION DES RUSHS POUR LE WEB
|
||
#
|
||
# Pour chaque rush de <voyage>/rushs, deux fichiers dans <voyage>/videos :
|
||
# <base>.mp4 1080p HEVC (tag hvc1) — qualité, ~40 % plus léger que H.264
|
||
# <base>_mobile.mp4 720p H.264 (tag avc1) — chargé par défaut, lisible partout
|
||
#
|
||
# Un rush dont les deux sorties existent et sont lisibles est sauté
|
||
# (FORCE=1 pour refaire).
|
||
#
|
||
# Variables :
|
||
# JOBS=N rushs encodés en parallèle (défaut : cœurs performance / 4)
|
||
# THREADS=N threads par encodeur (défaut : cœurs / JOBS)
|
||
# ENCODER=cpu libx265 + libx264, meilleure qualité par octet (défaut)
|
||
# ENCODER=vt VideoToolbox, ~10× plus rapide sur Apple Silicon, un peu
|
||
# moins bon à taille égale ; JOBS=1 (un seul bloc matériel)
|
||
# PRESET=medium preset x264/x265
|
||
# CRF_HEVC=26 qualité du 1080p (échelle x265)
|
||
# CRF_H264=25 qualité du 720p (échelle x264, PAS comparable à x265)
|
||
# VTQ_HD=60 VTQ_SD=58 qualité VideoToolbox (1-100, plus haut = meilleur)
|
||
# FPS_MAX=60 plafond de fréquence d'images (un rush 120p est ramené à 60)
|
||
# FORCE=1 réencode même si les sorties existent
|
||
#
|
||
# Exemples (depuis le dossier du voyage) :
|
||
# ../../RoadTripsGenerator/Scripts/compress.sh
|
||
# JOBS=1 ../../RoadTripsGenerator/Scripts/compress.sh # avec barre de progression
|
||
# ENCODER=vt ../../RoadTripsGenerator/Scripts/compress.sh
|
||
# =============================================================================
|
||
|
||
set -uo pipefail
|
||
shopt -s nocaseglob nullglob
|
||
|
||
# --- Mode interne : rappelé par xargs pour traiter UN rush ---------------------
|
||
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
|
||
|
||
SOURCE_DIR="$BASE_DIR/rushs"
|
||
DEST_DIR="$BASE_DIR/videos"
|
||
|
||
# --- 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}"
|
||
|
||
# Sur Apple Silicon on ne compte que les cœurs performance : les cœurs
|
||
# d'efficience ralentiraient tout le pool de threads x265.
|
||
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)}"
|
||
|
||
JOBS_EXPLICIT=0
|
||
[ -n "${JOBS+x}" ] && JOBS_EXPLICIT=1
|
||
if [ "$ENCODER" = "vt" ] && [ "$JOBS_EXPLICIT" = "0" ]; then
|
||
JOBS=1
|
||
else
|
||
JOBS="${JOBS:-$(( CORES / 4 ))}"
|
||
fi
|
||
[ -z "$JOBS" ] && JOBS=1
|
||
[ "$JOBS" -lt 1 ] 2>/dev/null && JOBS=1
|
||
|
||
THREADS="${THREADS:-$(( CORES / JOBS ))}"
|
||
[ "$THREADS" -lt 2 ] 2>/dev/null && THREADS=2
|
||
|
||
# Le 720p H.264 est 3 à 5× plus rapide que le 1080p HEVC : moitié moins de threads.
|
||
THREADS_SD=$(( THREADS / 2 ))
|
||
[ "$THREADS_SD" -lt 2 ] && THREADS_SD=2
|
||
|
||
# --- Encodage d'un palier ----------------------------------------------------
|
||
# 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
|
||
# Progression seulement en séquentiel : à plusieurs jobs, les lignes s'entremêlent.
|
||
if [ "$JOBS" = "1" ]; then statopt=(-stats); else statopt=(); fi
|
||
|
||
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
|
||
# keyint=120 : une image-clé toutes les 2 s à 60 fps.
|
||
# pools=N borne le pool de threads x265 pour cohabiter avec les autres jobs.
|
||
# -tag:v hvc1 : sans lui ffmpeg écrit « hev1 », que Safari refuse.
|
||
vopts=(-c:v libx265 -preset "$PRESET" -profile:v main -crf "$crf"
|
||
-x265-params "keyint=120:min-keyint=24:aq-mode=3:pools=$THREADS"
|
||
-maxrate "$maxrate" -bufsize "$bufsize" -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
|
||
|
||
# ${a[@]+"${a[@]}"} : un tableau vide sous `set -u` fait planter le bash 3.2
|
||
# livré avec macOS.
|
||
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"; }
|
||
|
||
# Une sortie existante n'est gardée que si ffprobe sait la lire : un encodage
|
||
# interrompu laisse un gros fichier sans index (« moov atom not found »).
|
||
# Lecture rapide, l'index est en tête de fichier grâce à +faststart.
|
||
lisible() {
|
||
[ -s "$1" ] && ffprobe -v error -show_entries format=duration -of csv=p=0 "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
# --- Traitement d'un rush : les deux paliers en parallèle --------------------
|
||
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" ] && lisible "$desktop_out" && lisible "$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}
|
||
# 1080p natif conservé tel quel ; une source 2.7K/4K est ramenée à 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…"
|
||
|
||
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
|
||
}
|
||
|
||
if [ "$INTERNAL" = "1" ]; then
|
||
process_one "$TARGET"
|
||
exit $?
|
||
fi
|
||
|
||
# =============================================================================
|
||
# MODE PRINCIPAL
|
||
# =============================================================================
|
||
if [ ! -d "$SOURCE_DIR" ]; then
|
||
echo "❌ Erreur : Dossier source $SOURCE_DIR introuvable."
|
||
exit 1
|
||
fi
|
||
mkdir -p "$DEST_DIR"
|
||
|
||
# Liste stockée plutôt que `ffmpeg … | grep -q` : le SIGPIPE de grep -q,
|
||
# combiné à pipefail, donnerait un faux « encodeur absent ».
|
||
ENCODERS_LIST="$(ffmpeg -hide_banner -encoders 2>/dev/null)"
|
||
has_enc() { printf '%s' "$ENCODERS_LIST" | grep -q "$1"; }
|
||
|
||
if [ "$ENCODER" = "vt" ]; then
|
||
has_enc "hevc_videotoolbox" || { echo "❌ hevc_videotoolbox indisponible. Utilise ENCODER=cpu."; exit 1; }
|
||
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
|
||
|
||
files=()
|
||
for f in "$SOURCE_DIR"/*.mp4 "$SOURCE_DIR"/*.mov; do
|
||
[ -e "$f" ] || continue
|
||
# « ._nom » : métadonnées que macOS dépose sur les disques non-HFS (exFAT).
|
||
case "$(basename "$f")" in ._*) continue ;; esac
|
||
files+=("$f")
|
||
done
|
||
|
||
echo "--- COMPRESSION ROAD-TRIP (HYBRIDE HEVC / H.264) ---"
|
||
echo "📂 Source : $SOURCE_DIR"
|
||
echo "📤 Destination : $DEST_DIR"
|
||
if [ "$ENCODER" = "vt" ]; then
|
||
echo "⚙️ Encodeur : VideoToolbox (q=$VTQ_HD / $VTQ_SD)"
|
||
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 avec le bash 3.2 de macOS
|
||
# qui ne connaît pas `wait -n`.
|
||
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"
|
||
|
||
exit $rc
|