changemaker.lite/media-manager/scripts/generate_gifs_from_video.sh

295 lines
9.3 KiB
Bash
Executable File

#!/bin/bash
# Script to generate GIF segments from a source video
# Cuts video into segments of target length, auto-detects orientation
# Outputs to local/gifs/<video-name>/horizontal/ or /vertical/
# Uses NVIDIA NVENC GPU acceleration when available
# Enable strict error handling
set -o pipefail # Catch failures in piped commands
# ============================================================
# Environment Variables
# ============================================================
SOURCE_VIDEO="${SOURCE_VIDEO:-}"
SEGMENT_DURATION="${SEGMENT_DURATION:-59}"
GIFS_OUTPUT_DIR="${GIFS_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/gifs}"
# ============================================================
# Validate Inputs
# ============================================================
if [[ -z "$SOURCE_VIDEO" ]]; then
echo "Error: SOURCE_VIDEO environment variable is required"
exit 1
fi
if [[ ! -f "$SOURCE_VIDEO" ]]; then
echo "Error: Source video not found: $SOURCE_VIDEO"
exit 1
fi
# ============================================================
# Check Dependencies
# ============================================================
if ! command -v ffmpeg &> /dev/null; then
echo "Error: ffmpeg is not installed"
exit 1
fi
if ! command -v ffprobe &> /dev/null; then
echo "Error: ffprobe is not installed"
exit 1
fi
# ============================================================
# Detect NVENC Availability
# ============================================================
detect_encoder() {
# Check if CPU mode is forced
if [[ "$FORCE_CPU" == "true" ]]; then
echo "Using CPU encoder (libx265) [--cpu flag]"
ENCODER="libx265"
ENCODER_OPTS="-preset medium -crf 23 -x265-params log-level=error"
return
fi
if ffmpeg -hide_banner -encoders 2>/dev/null | grep -q hevc_nvenc; then
# Test NVENC with a real encode
local test_output="/tmp/nvenc_detect_test_$$.mp4"
local test_result
test_result=$(ffmpeg -y -f lavfi -i "color=black:s=256x256:d=0.1" \
-c:v hevc_nvenc -preset p4 -gpu 0 \
"$test_output" 2>&1)
local test_exit=$?
rm -f "$test_output"
if [[ $test_exit -ne 0 ]]; then
echo "WARNING: NVENC test failed, falling back to CPU encoder"
ENCODER="libx265"
ENCODER_OPTS="-preset medium -crf 23 -x265-params log-level=error"
return
fi
echo "Using NVENC GPU encoder (hevc_nvenc)"
ENCODER="hevc_nvenc"
ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -tag:v hvc1"
else
echo "NVENC not available, using CPU encoder (libx265)"
ENCODER="libx265"
ENCODER_OPTS="-preset medium -crf 23 -x265-params log-level=error"
fi
}
# ============================================================
# Get Video Metadata via ffprobe
# ============================================================
get_video_duration() {
local file="$1"
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null | cut -d. -f1
}
get_video_dimensions() {
local file="$1"
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x "$file" 2>/dev/null
}
get_video_quality() {
local file="$1"
local dimensions=$(get_video_dimensions "$file")
local height=$(echo "$dimensions" | cut -d'x' -f2)
if [[ "$height" -ge 2160 ]]; then
echo "4K"
elif [[ "$height" -ge 1440 ]]; then
echo "1440p"
elif [[ "$height" -ge 1080 ]]; then
echo "1080p"
elif [[ "$height" -ge 720 ]]; then
echo "720p"
elif [[ "$height" -ge 480 ]]; then
echo "480p"
else
echo "SD"
fi
}
detect_orientation() {
local file="$1"
local dimensions=$(get_video_dimensions "$file")
local width=$(echo "$dimensions" | cut -d'x' -f1)
local height=$(echo "$dimensions" | cut -d'x' -f2)
if [[ "$width" -gt "$height" ]]; then
echo "H"
else
echo "V"
fi
}
# ============================================================
# Main Script
# ============================================================
echo ""
echo "========================================="
echo " GIF Segment Generator"
echo "========================================="
echo " Source: $SOURCE_VIDEO"
echo " Segment Duration: ${SEGMENT_DURATION}s"
echo "========================================="
echo ""
# Detect encoder
detect_encoder
echo ""
# Get video metadata
TOTAL_DURATION=$(get_video_duration "$SOURCE_VIDEO")
QUALITY=$(get_video_quality "$SOURCE_VIDEO")
ORIENTATION=$(detect_orientation "$SOURCE_VIDEO")
if [[ -z "$TOTAL_DURATION" ]] || [[ "$TOTAL_DURATION" -eq 0 ]]; then
echo "Error: Could not determine video duration"
exit 1
fi
echo "Video Information:"
echo " Duration: ${TOTAL_DURATION}s"
echo " Quality: $QUALITY"
echo " Orientation: $ORIENTATION ($([ "$ORIENTATION" == "H" ] && echo "Horizontal" || echo "Vertical"))"
echo ""
# Calculate number of segments
NUM_SEGMENTS=$((TOTAL_DURATION / SEGMENT_DURATION))
REMAINDER=$((TOTAL_DURATION % SEGMENT_DURATION))
# Include partial last segment if it's at least 10 seconds
if [[ "$REMAINDER" -ge 10 ]]; then
NUM_SEGMENTS=$((NUM_SEGMENTS + 1))
fi
if [[ "$NUM_SEGMENTS" -eq 0 ]]; then
echo "Error: Video is too short to create segments (${TOTAL_DURATION}s < ${SEGMENT_DURATION}s)"
exit 1
fi
echo "Will create $NUM_SEGMENTS segments"
echo ""
# Extract base name from source video (without extension and metadata tags)
SOURCE_BASENAME=$(basename "$SOURCE_VIDEO" .mp4)
# Remove existing metadata tags like [H:MM] [Quality] [E] [H/V]
CLEAN_BASENAME=$(echo "$SOURCE_BASENAME" | sed -E 's/ *\[[^][]*\]//g' | sed 's/ *$//')
# Create output directory with -gifs suffix
OUTPUT_DIR="${GIFS_OUTPUT_DIR}/${CLEAN_BASENAME}-gifs"
echo "Output directory: $OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# Track progress
CURRENT_SEGMENT=0
SUCCESSFUL=0
FAILED=0
start_time=$(date +%s)
# Generate segments
for ((i=0; i<NUM_SEGMENTS; i++)); do
CURRENT_SEGMENT=$((i + 1))
START_TIME=$((i * SEGMENT_DURATION))
# Determine actual segment duration (might be shorter for last segment)
if [[ $i -eq $((NUM_SEGMENTS - 1)) ]] && [[ "$REMAINDER" -gt 0 ]] && [[ "$REMAINDER" -ge 10 ]]; then
ACTUAL_DURATION=$REMAINDER
else
ACTUAL_DURATION=$SEGMENT_DURATION
fi
# Format segment number with leading zeros (3 digits)
SEGMENT_NUM=$(printf "%03d" $CURRENT_SEGMENT)
# Create output filename
# Format: <video-base-name> - gif<NNN> - [<duration>s] [<quality>] [E] [H/V].mp4
OUTPUT_FILENAME="${CLEAN_BASENAME} - gif${SEGMENT_NUM} - [${ACTUAL_DURATION}s] [${QUALITY}] [E] [${ORIENTATION}].mp4"
OUTPUT_PATH="${OUTPUT_DIR}/${OUTPUT_FILENAME}"
echo ""
echo "[$CURRENT_SEGMENT/$NUM_SEGMENTS] Creating segment..."
echo " Start: ${START_TIME}s, Duration: ${ACTUAL_DURATION}s"
echo " Output: $(basename "$OUTPUT_PATH")"
# Skip if already exists
if [[ -f "$OUTPUT_PATH" ]]; then
echo " Skipped: Already exists"
SUCCESSFUL=$((SUCCESSFUL + 1))
continue
fi
# Encode segment with GPU (with CPU fallback)
TEMP_OUTPUT="${OUTPUT_PATH}.temp.mp4"
encode_output=$(ffmpeg -nostdin -hide_banner \
-ss "$START_TIME" -i "$SOURCE_VIDEO" \
-t "$ACTUAL_DURATION" \
-c:v $ENCODER $ENCODER_OPTS \
-c:a aac -b:a 128k \
-movflags +faststart \
-y "$TEMP_OUTPUT" 2>&1)
encode_exit=$?
# Check for NVENC failure and retry with CPU
if [[ $encode_exit -ne 0 ]] && [[ "$ENCODER" == "hevc_nvenc" ]]; then
echo " NVENC failed, retrying with CPU encoder..."
rm -f "$TEMP_OUTPUT"
encode_output=$(ffmpeg -nostdin -hide_banner \
-ss "$START_TIME" -i "$SOURCE_VIDEO" \
-t "$ACTUAL_DURATION" \
-c:v libx265 -preset medium -crf 23 -x265-params log-level=error \
-c:a aac -b:a 128k \
-movflags +faststart \
-y "$TEMP_OUTPUT" 2>&1)
encode_exit=$?
fi
# Verify and finalize
if [[ $encode_exit -eq 0 ]] && [[ -f "$TEMP_OUTPUT" ]]; then
file_size=$(stat -c%s "$TEMP_OUTPUT" 2>/dev/null)
if [[ -n "$file_size" ]] && [[ "$file_size" -gt 10000 ]]; then
mv "$TEMP_OUTPUT" "$OUTPUT_PATH"
file_size_human=$(du -h "$OUTPUT_PATH" | cut -f1)
echo " Success! (${file_size_human})"
SUCCESSFUL=$((SUCCESSFUL + 1))
else
echo " Error: Output file too small (likely corrupted)"
rm -f "$TEMP_OUTPUT"
FAILED=$((FAILED + 1))
fi
else
echo " Error: Encoding failed (exit code: $encode_exit)"
rm -f "$TEMP_OUTPUT"
FAILED=$((FAILED + 1))
fi
# Small delay between segments to prevent GPU overload
sleep 0.2
done
# Calculate elapsed time
end_time=$(date +%s)
elapsed=$((end_time - start_time))
hours=$((elapsed / 3600))
minutes=$(((elapsed % 3600) / 60))
seconds=$((elapsed % 60))
echo ""
echo "========================================="
echo " GIF Generation Complete!"
echo "========================================="
echo " Source: $(basename "$SOURCE_VIDEO")"
echo " Output: $OUTPUT_DIR"
echo " Segments: $SUCCESSFUL successful, $FAILED failed"
echo " Time elapsed: ${hours}h ${minutes}m ${seconds}s"
echo "========================================="
echo ""