···11+#!/usr/bin/env -S nix shell nixpkgs#parallel nixpkgs#ffmpeg nixpkgs#imagemagick --command bash
22+33+# Define temporary directory variables globally so trap can access them
44+FRAMES_IN=""
55+FRAMES_OUT=""
66+77+# Function to clean up and exit
88+cleanup_and_exit() {
99+ echo -e "\nInterrupted. Cleaning up temporary files..."
1010+ # Only remove if directories were actually created
1111+ [ -n "$FRAMES_IN" ] && [ -d "$FRAMES_IN" ] && rm -rf "$FRAMES_IN"
1212+ [ -n "$FRAMES_OUT" ] && [ -d "$FRAMES_OUT" ] && rm -rf "$FRAMES_OUT"
1313+ exit 1
1414+}
1515+1616+# Set up trap for Ctrl+C (SIGINT)
1717+trap cleanup_and_exit INT
1818+1919+# Check if input and output parameters are provided
2020+if [ $# -lt 2 ]; then
2121+ echo "Usage: $0 input_video output_video [num_threads]"
2222+ exit 1
2323+fi
2424+2525+INPUT_VIDEO=$1
2626+OUTPUT_VIDEO=$2
2727+# Default to number of CPU cores if threads not specified
2828+NUM_THREADS=${3:-$(nproc)}
2929+3030+# Determine and create temporary directories, preferring RAM disk
3131+TEMP_BASE=""
3232+if [ -d "/dev/shm" ] && [ -w "/dev/shm" ]; then
3333+ TEMP_BASE="/dev/shm"
3434+ echo "Using RAM disk (/dev/shm) for temporary files."
3535+elif [ -d "/tmp" ] && [ -w "/tmp" ] && mount | grep -q 'on /tmp type tmpfs'; then
3636+ TEMP_BASE="/tmp"
3737+ echo "Using RAM disk (/tmp) for temporary files."
3838+else
3939+ TEMP_BASE="."
4040+ echo "RAM disk not available or not writable, using current directory for temporary files."
4141+fi
4242+4343+# Create unique temporary directory names using process ID
4444+# Add a random suffix for extra safety in case PID wraps or script runs concurrently
4545+RAND_SUFFIX=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)
4646+FRAMES_IN="${TEMP_BASE}/inkify_frames_in_${$}_${RAND_SUFFIX}"
4747+FRAMES_OUT="${TEMP_BASE}/inkify_frames_out_${$}_${RAND_SUFFIX}"
4848+4949+# Check if magick command exists
5050+if ! command -v magick &>/dev/null; then
5151+ echo "Error: ImageMagick (magick command) not found. Please install it."
5252+ # Attempt cleanup before exiting
5353+ [ -d "$FRAMES_IN" ] && rm -rf "$FRAMES_IN"
5454+ [ -d "$FRAMES_OUT" ] && rm -rf "$FRAMES_OUT"
5555+ exit 1
5656+fi
5757+5858+# Check if eink-2color.png exists
5959+if [ ! -f "eink-2color.png" ]; then
6060+ echo "Error: Colormap file 'eink-2color.png' not found in the current directory."
6161+ # Attempt cleanup before exiting
6262+ [ -d "$FRAMES_IN" ] && rm -rf "$FRAMES_IN"
6363+ [ -d "$FRAMES_OUT" ] && rm -rf "$FRAMES_OUT"
6464+ exit 1
6565+fi
6666+6767+# Create temporary directories only after checks pass
6868+mkdir -p "$FRAMES_IN"
6969+if [ $? -ne 0 ]; then echo "Error: Could not create temporary directory $FRAMES_IN"; exit 1; fi
7070+mkdir -p "$FRAMES_OUT"
7171+if [ $? -ne 0 ]; then echo "Error: Could not create temporary directory $FRAMES_OUT"; rm -rf "$FRAMES_IN"; exit 1; fi
7272+7373+7474+# Extract frames from video and detect framerate
7575+FRAMERATE=$(ffprobe -v error -select_streams v -of default=noprint_wrappers=1:nokey=1 -show_entries stream=r_frame_rate "$INPUT_VIDEO" 2>/dev/null | bc -l | awk '{printf "%.2f", $0}')
7676+if [ -z "$FRAMERATE" ] || [ "$FRAMERATE" = "0.00" ]; then
7777+ echo "Error: Could not detect framerate for $INPUT_VIDEO."
7878+ cleanup_and_exit # Use cleanup function
7979+fi
8080+echo "Detected framerate: $FRAMERATE fps"
8181+8282+# Extract frames from video (use -threads for faster extraction)
8383+echo "Extracting frames to $FRAMES_IN..."
8484+if ! ffmpeg -threads "$NUM_THREADS" -i "$INPUT_VIDEO" "$FRAMES_IN/frame_%04d.png" >/dev/null 2>&1; then
8585+ echo "Error: ffmpeg failed to extract frames from $INPUT_VIDEO."
8686+ cleanup_and_exit # Use cleanup function
8787+fi
8888+8989+# Check if frames were extracted
9090+if ! ls "$FRAMES_IN"/frame_*.png > /dev/null 2>&1; then
9191+ echo "Error: No frames were extracted to $FRAMES_IN. Check input video and permissions."
9292+ cleanup_and_exit
9393+fi
9494+9595+9696+# Process frames in parallel using GNU Parallel
9797+if command -v parallel &>/dev/null; then
9898+ echo "Processing frames in parallel with $NUM_THREADS threads..."
9999+ # Need to export FRAMES_OUT for parallel to see it in subshells
100100+ export FRAMES_OUT
101101+ # Use parallel's built-in path manipulation {/} for basename
102102+ if ! find "$FRAMES_IN" -name "frame_*.png" | parallel --progress -j "$NUM_THREADS" \
103103+ "magick {} -dither FloydSteinberg -define dither:diffusion-amount=100% -remap eink-2color.png \"$FRAMES_OUT/{/}\""; then
104104+ echo "Error: Parallel processing with magick failed."
105105+ cleanup_and_exit
106106+ fi
107107+else
108108+ echo "GNU Parallel not found. Processing frames sequentially..."
109109+ # Process each frame with the dithering effect
110110+ processed_count=0
111111+ for frame in "$FRAMES_IN"/frame_*.png; do
112112+ # Check if the file exists before processing
113113+ if [ -f "$frame" ]; then
114114+ # Construct output path correctly using basename
115115+ output_frame="$FRAMES_OUT/$(basename "$frame")"
116116+ if ! magick "$frame" -dither FloydSteinberg -define dither:diffusion-amount=100% -remap eink-2color.png "$output_frame"; then
117117+ echo "Error: magick failed to process $frame."
118118+ # Decide if you want to stop on first error or continue
119119+ cleanup_and_exit # Stop on first error
120120+ # continue # Or uncomment to continue processing other frames
121121+ fi
122122+ # echo "Processed: $frame" # Can be too verbose
123123+ processed_count=$((processed_count + 1))
124124+ fi
125125+ done
126126+ echo "Processed $processed_count frames sequentially."
127127+ if [ $processed_count -eq 0 ]; then
128128+ echo "Error: No frames found to process sequentially in $FRAMES_IN"
129129+ cleanup_and_exit
130130+ fi
131131+fi
132132+133133+# Check if output frames exist before attempting recombination
134134+if ! ls "$FRAMES_OUT"/frame_*.png > /dev/null 2>&1; then
135135+ echo "Error: No processed frames found in $FRAMES_OUT. Image processing likely failed."
136136+ cleanup_and_exit
137137+fi
138138+139139+140140+# Recombine frames into video with detected framerate (use -threads for faster encoding)
141141+echo "Recombining frames from $FRAMES_OUT..."
142142+# Allow ffmpeg to prompt for overwrite by removing output redirection
143143+if ! ffmpeg -framerate "$FRAMERATE" -threads "$NUM_THREADS" -i "$FRAMES_OUT/frame_%04d.png" -c:v libx264 -preset faster -pix_fmt yuv420p "$OUTPUT_VIDEO"; then
144144+ echo "Error: ffmpeg failed to recombine frames into $OUTPUT_VIDEO (or user cancelled overwrite)."
145145+ cleanup_and_exit # Use cleanup function
146146+fi
147147+148148+# Use the cleanup function for consistency
149149+cleanup_and_exit() {
150150+ echo -e "\nCleaning up temporary files..."
151151+ # Only remove if directories were actually created
152152+ [ -n "$FRAMES_IN" ] && [ -d "$FRAMES_IN" ] && rm -rf "$FRAMES_IN"
153153+ [ -n "$FRAMES_OUT" ] && [ -d "$FRAMES_OUT" ] && rm -rf "$FRAMES_OUT"
154154+}
155155+# Call cleanup explicitly at the end (trap handles interruptions)
156156+cleanup_and_exit
157157+# Reset exit code to 0 for successful completion
158158+exit 0