#!/usr/bin/env bash set -euo pipefail usage() { cat < 0 (défaut : 12) -s SIZE taille max du côté long en pixels, > 0 (défaut : 1080) -y écraser le fichier de sortie sans demander -h afficher cette aide EOF } INPUT="images" OUTPUT="stop_motion.mp4" FPS="12" SIZE="1080" ASSUME_YES=0 while getopts ":i:o:f:s:yh" opt; do case $opt in i) INPUT=$OPTARG ;; o) OUTPUT=$OPTARG ;; f) FPS=$OPTARG ;; s) SIZE=$OPTARG ;; y) ASSUME_YES=1 ;; h) usage ; exit 0 ;; \?) echo "Erreur : option inconnue -$OPTARG" >&2 ; usage >&2 ; exit 1 ;; :) echo "Erreur : valeur manquante pour -$OPTARG" >&2 ; usage >&2 ; exit 1 ;; esac done if ! [[ $FPS =~ ^[0-9]+([.][0-9]+)?$ ]] || ! awk -v f="$FPS" 'BEGIN{exit !(f>0)}'; then echo "Erreur : -f doit être un nombre strictement positif (reçu : $FPS)" >&2 exit 1 fi if ! [[ $SIZE =~ ^[0-9]+$ ]] || ! awk -v s="$SIZE" 'BEGIN{exit !(s>0)}'; then echo "Erreur : -s doit être un entier strictement positif (reçu : $SIZE)" >&2 exit 1 fi if ! command -v ffmpeg >/dev/null 2>&1; then echo "Erreur : ffmpeg est introuvable. Installation : brew install ffmpeg" >&2 exit 1 fi if [ ! -d "$INPUT" ]; then echo "Erreur : le dossier d'images '$INPUT' n'existe pas." >&2 exit 1 fi IMAGES_ABS_DIR=$(cd "$INPUT" && pwd) IMAGES=$(LC_ALL=C find "$IMAGES_ABS_DIR" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.heic' \) | LC_ALL=C sort) if [ -z "$IMAGES" ]; then echo "Erreur : aucune image (jpg, jpeg, png, heic) trouvée dans '$INPUT'." >&2 exit 1 fi COUNT=$(printf '%s\n' "$IMAGES" | wc -l | tr -d ' ') if [ ! -d "$(dirname "$OUTPUT")" ]; then echo "Erreur : le dossier de sortie '$(dirname "$OUTPUT")' n'existe pas." >&2 exit 1 fi if [ -e "$OUTPUT" ] && [ "$ASSUME_YES" -ne 1 ]; then read -r -p "'$OUTPUT' existe déjà. L'écraser ? [o/N] " reponse case $reponse in o|O|y|Y|oui|Oui) ;; *) echo "Abandon." ; exit 0 ;; esac fi OUT_DIR=$(cd "$(dirname "$OUTPUT")" && pwd) OUT_NAME=$(basename "$OUTPUT") LIST_FILE=$(mktemp "$OUT_DIR/.stopmotion.XXXXXX") trap 'rm -f "$LIST_FILE"' EXIT quote_ffconcat() { local s=$1 s=${s//\\/\\\\} s=${s//\'/\'\\\'\'} printf "file '%s'\n" "$s" } DURATION=$(awk -v f="$FPS" 'BEGIN{printf "%.6f", 1/f}') { echo "ffconcat version 1.0" while IFS= read -r img; do quote_ffconcat "$img" printf 'duration %s\n' "$DURATION" done <<< "$IMAGES" } > "$LIST_FILE" if ! ffmpeg -y -hide_banner -loglevel error -stats \ -f concat -safe 0 -i "$LIST_FILE" \ -vf "scale=${SIZE}:${SIZE}:force_original_aspect_ratio=decrease:force_divisible_by=2" \ -r "$FPS" \ -c:v libx264 -pix_fmt yuv420p -movflags +faststart \ "$OUT_DIR/$OUT_NAME"; then rm -f "$OUT_DIR/$OUT_NAME" echo "Erreur : l'encodage ffmpeg a échoué." >&2 exit 1 fi DURATION_VIDEO=$(awk -v c="$COUNT" -v f="$FPS" 'BEGIN{printf "%.2f", c/f}') SIZE_HUMAN=$(du -h "$OUT_DIR/$OUT_NAME" | cut -f1 | tr -d ' ') echo "Vidéo créée : $OUT_NAME ($COUNT images, ${FPS} fps, ${DURATION_VIDEO} s, ${SIZE_HUMAN})"