diff --git a/Scripts/bench_crf.sh b/Scripts/bench_crf.sh new file mode 100755 index 0000000..33f4dfb --- /dev/null +++ b/Scripts/bench_crf.sh @@ -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 [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 diff --git a/Scripts/compress.sh b/Scripts/compress.sh old mode 100644 new mode 100755 index 6581972..047984f --- a/Scripts/compress.sh +++ b/Scripts/compress.sh @@ -1,83 +1,312 @@ #!/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) : +# .mp4 1080p natif, HEVC/H.265, tag hvc1 +# → gros fichier, là où les 40 % d'économie comptent +# _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" 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