#!/usr/bin/bash # Define custom exit codes EXIT_SUCCESS=0 EXIT_MISSING_ARGS=1 EXIT_FFPROBE_FAILED=2 EXIT_ZERO_DURATION=3 EXIT_FFMPEG_FAILED=4 EXIT_TIME_PARSING_FAILED=5 EXIT_COMMAND_NOT_FOUND=6 EXIT_OVERWRITE_PREVENTED=7 EXIT_NAMED_PIPE_FAILED=8 # --- Script Configuration (read from environment or use defaults) --- # Target audio bitrate in kbps. AUDIO_BITRATE_KBPS=${AUDIO_BITRATE_KBPS:-96} DEFAULT_TARGET_SIZE_MIB=100 # Default target output size if not specified # Set optional trimming variables from the environment START_TIME=${START_TIME:-""} END_TIME=${END_TIME:-""} # Set optional FPS variable from the environment TARGET_FPS=${TARGET_FPS:-""} # Set optional video speed variable from the environment TARGET_SPEED=${TARGET_SPEED:-1.0} # Set optional audio muting variable from the environment MUTE_AUDIO=${MUTE_AUDIO:-""} # --- Time Logging Variables --- SCRIPT_START_TIME=$(date +%s) SCRIPT_START_TIME_HUMAN=$(date +"%Y-%m-%d %H:%M:%S") # --- Check for required commands --- # `bc` is used for floating-point arithmetic. # `ffprobe`, `ffmpeg`, and `realpath` are the core tools. for cmd in bc ffprobe ffmpeg realpath; do if ! command -v "$cmd" &> /dev/null; then echo "Error: Required command '$cmd' not found. Please install it." >&2 exit "$EXIT_COMMAND_NOT_FOUND" fi done # --- Function to parse a time string (e.g., HH:MM:SS.mmm or SS) to seconds --- # This is a helper function to correctly handle the time calculations. parse_time_to_seconds() { local time_str="$1" # Regex to check for HH:MM:SS format if [[ "$time_str" =~ ^([0-9]+):([0-9]{2}):([0-9]{2}(\.[0-9]+)?)$ ]]; then local hours=${BASH_REMATCH[1]} local minutes=${BASH_REMATCH[2]} local seconds=${BASH_REMATCH[3]} # Use awk to handle floating point math awk -v h="$hours" -v m="$minutes" -v s="$seconds" 'BEGIN { printf "%.3f", h*3600 + m*60 + s }' # Regex to check for MM:SS format elif [[ "$time_str" =~ ^([0-9]+):([0-9]{2}(\.[0-9]+)?)$ ]]; then local minutes=${BASH_REMATCH[1]} local seconds=${BASH_REMATCH[2]} awk -v m="$minutes" -v s="$seconds" 'BEGIN { printf "%.3f", m*60 + s }' # Check for simple seconds format elif [[ "$time_str" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then # It's already in seconds, no need to convert echo "$time_str" else echo "" # Return empty string for invalid format fi } # --- Function to monitor and display FFmpeg progress --- # Reads progress data from a named pipe (FIFO) monitor_progress() { local pass_name="$1" local fifo_path="$2" local total_duration="$3" while read -r line; do if [[ "$line" =~ ^out_time_ms ]]; then # Extract milliseconds from the line current_time_ms=$(echo "$line" | cut -d'=' -f2) current_time_seconds=$(echo "scale=3; $current_time_ms / 1000000" | bc -l) # Calculate percentage using `bc` for robust floating-point math local percentage if [ "$(echo "$total_duration == 0" | bc -l)" -eq 1 ]; then percentage=0.0 else percentage=$(echo "scale=1; ($current_time_seconds / $total_duration) * 100" | bc -l) # Clamp the percentage to be within 0 and 100 if [ "$(echo "$percentage > 100" | bc -l)" -eq 1 ]; then percentage=100.0 fi if [ "$(echo "$percentage < 0" | bc -l)" -eq 1 ]; then percentage=0.0 fi fi # Print the progress bar printf "\r--- %s: %.1f%% complete ---" "$pass_name" "$percentage" fi done < "$fifo_path" # Print a newline to clean up the progress bar echo } # --- Argument Parsing --- # Initialize positional arguments with empty values OUTPUT_FILE="" TARGET_SIZE_MIB="${DEFAULT_TARGET_SIZE_MIB}" TARGET_SMALLEST_DIMENSION="" INPUT_FILE="$1" # The first argument is always the input file # --- Usage Instructions -- if [ -z "$INPUT_FILE" ]; then echo "About this script:" echo "------------------" echo "This script converts video files to the WebM format while targeting a specific output file size." echo "It is an ideal tool for sharing videos on platforms with strict file size limitations, such as" echo "Discord's 100 MB limit for non-Nitro users. The script specifically uses the WebM format because" echo "Discord automatically downscales MP4 videos larger than 720p, whereas WebM videos often" echo "bypass this limitation, allowing you to maintain better quality and resolution." echo "" echo "This script was developed with the assistance of Gemini AI." echo "" echo "Usage: $0 [output_video.webm] [target_size_mib] [target_smallest_dimension]" echo "" echo " : Path to the input video file (e.g., .mp4, .mov, .mkv). (REQUIRED)" echo " [output_video.webm] : (Optional) Desired path for the output WebM video file." echo " If omitted, saves as original input video filename with .webm extension in current working directory." echo " [target_size_mib] : (Optional) Desired output file size in MiB. Defaults to ${DEFAULT_TARGET_SIZE_MIB} MiB." echo " [target_smallest_dimension] : (Optional) The target size for the video's smallest dimension (e.g., '720' for 720p equivalent)." echo " The other dimension will be calculated to maintain aspect ratio." echo " If omitted, no scaling is applied." echo "" echo "Optional environment variables:" echo " START_TIME : The start time for trimming (e.g., '00:01:30' or '90')." echo " END_TIME : The end time for trimming (e.g., '00:03:00' or '180')." echo " (You can use START_TIME only, END_TIME only, or both.)" echo "" echo " AUDIO_BITRATE_KBPS: Target audio bitrate in kbps (e.g., '128'). Defaults to 96 kbps. Set to 0 to mute audio." echo " TARGET_FPS: Target frames per second (e.g., '24' or '30'). If omitted, the original FPS is used." echo " TARGET_SPEED: Playback speed factor (e.g., '0.5' for half speed, '1.5' for 1.5x speed). Defaults to 1.0." echo " MUTE_AUDIO: Set to any non-empty value (e.g., 'true', '1') to mute the audio track." echo "" echo "Examples:" echo " $0 my_video.mp4" # auto-output, default size, no scale, no trim echo " TARGET_FPS=24 $0 my_video.mp4 80" # auto-output, 80 MiB, 24 FPS, no scale, no trim echo " MUTE_AUDIO=true $0 my_video.mp4" # Mute audio using MUTE_AUDIO variable echo " AUDIO_BITRATE_KBPS=0 $0 my_video.mp4" # Mute audio by setting bitrate to 0 echo " TARGET_SPEED=0.5 START_TIME=\"10\" $0 my_video.mp4" # half speed from 10s to end echo " START_TIME=\"10\" END_TIME=\"20\" $0 my_video.mp4 80 720" # trim, scale, and resize echo "" exit "$EXIT_MISSING_ARGS" fi # Shift off INPUT_FILE ($1) so subsequent arguments can be processed as $1, $2, etc. shift # Process remaining arguments ($1, $2, $3 now refer to original $2, $3, $4) # Check if the current $1 (original $2) is a number. If so, it means OUTPUT_FILE was skipped. if [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]; then # This argument is TARGET_SIZE_MIB (OUTPUT_FILE was omitted) TARGET_SIZE_MIB="$1" shift # Consume this argument # Check if the next argument ($1, original $3) is TARGET_SMALLEST_DIMENSION if [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]; then TARGET_SMALLEST_DIMENSION="$1" shift # Consume this argument fi else # This argument is not a number, so it must be OUTPUT_FILE (or no further args) if [ -n "$1" ]; then OUTPUT_FILE="$1" shift # Consume this argument # Check if the next argument ($1, original $3) is TARGET_SIZE_MIB if [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]; then TARGET_SIZE_MIB="$1" shift # Consume this argument # Check if the next argument ($1, original $4) is TARGET_SMALLEST_DIMENSION if [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]; then TARGET_SMALLEST_DIMENSION="$1" shift # Consume this argument fi fi fi fi # If OUTPUT_FILE is still empty after parsing (meaning it was never explicitly provided), # derive it from the input filename. if [ -z "$OUTPUT_FILE" ]; then FILENAME_NO_EXT=$(basename "${INPUT_FILE%.*}") OUTPUT_FILE="${FILENAME_NO_EXT}.webm" fi # Warn if any unexpected arguments remain if [ -n "$1" ]; then echo "Warning: Unrecognized arguments after parsing: $*" >&2 # Output warning to stderr fi # --- Check for overwrite before starting --- INPUT_REALPATH=$(realpath -s "$INPUT_FILE") OUTPUT_REALPATH=$(realpath -s "$OUTPUT_FILE") if [ "$INPUT_REALPATH" = "$OUTPUT_REALPATH" ]; then echo "Error: The output file path is the same as the input file path." >&2 echo "Please specify a different output filename or path to avoid overwriting the original file." >&2 exit "$EXIT_OVERWRITE_PREVENTED" fi # Save pass log file. Using basename to strip the last extension (.webm) PASSLOGFILE="$(basename "$OUTPUT_FILE" .webm)_passlog" # Create a temporary named pipe for progress reporting PROGRESS_FIFO="$(mktemp -u --suffix=.fifo)" mkfifo "$PROGRESS_FIFO" if [ $? -ne 0 ]; then echo "Error: Failed to create a named pipe for progress reporting." >&2 exit "$EXIT_NAMED_PIPE_FAILED" fi # Clean up the named pipe on exit trap "rm -f \"$PROGRESS_FIFO\"" EXIT # --- Main Logic --- # Check if audio should be muted IS_AUDIO_ENABLED="true" if [[ -n "$MUTE_AUDIO" ]] || [[ "$AUDIO_BITRATE_KBPS" -eq 0 ]]; then IS_AUDIO_ENABLED="false" echo "Audio is being muted." fi echo "--- WebM Conversion Script ---" echo "Script started at: ${SCRIPT_START_TIME_HUMAN}" echo "Input File: ${INPUT_FILE}" echo "Output File: ${OUTPUT_FILE}" echo "Target Size: ${TARGET_SIZE_MIB} MiB (Default if not specified: ${DEFAULT_TARGET_SIZE_MIB} MiB)" echo "Pass Log File: ${PASSLOGFILE}" if [ -n "${TARGET_FPS}" ]; then echo "Target FPS: ${TARGET_FPS}" fi if [ "$(echo "$TARGET_SPEED > 1.0" | bc -l)" -eq 1 ]; then echo "Target Speed: ${TARGET_SPEED}x (faster)" elif [ "$(echo "$TARGET_SPEED < 1.0" | bc -l)" -eq 1 ]; then echo "Target Speed: ${TARGET_SPEED}x (slower)" else echo "Target Speed: ${TARGET_SPEED}x (normal)" fi echo "------------------------------" # --- Determine Cropping Arguments --- FFMPEG_CROP_ARGS="" FFMPEG_POST_CROP_ARGS="" # Check for cropping conditions (either START_TIME or END_TIME is set) if [ -n "$START_TIME" ] || [ -n "$END_TIME" ]; then if [ -n "$START_TIME" ]; then FFMPEG_CROP_ARGS="-ss ${START_TIME}" echo "Trimming video from start time: ${START_TIME}" fi if [ -n "$END_TIME" ]; then # When using -to with -ss, it must come after the input file for fast seeking. # So we store it in a separate variable to be placed later. FFMPEG_POST_CROP_ARGS="-to ${END_TIME}" echo "Trimming video to end time: ${END_TIME}" fi fi # --- Get video duration and dimensions using ffprobe --- # Get original video duration ORIGINAL_DURATION_SECONDS=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE") if [ -z "$ORIGINAL_DURATION_SECONDS" ]; then echo "Error: Could not get duration from '$INPUT_FILE'." >&2 exit "$EXIT_FFPROBE_FAILED" fi # Get original video dimensions ORIGINAL_WIDTH=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE") ORIGINAL_HEIGHT=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE") if [ -z "$ORIGINAL_WIDTH" ] || [ -z "$ORIGINAL_HEIGHT" ]; then echo "Error: Could not get video dimensions from '$INPUT_FILE'." >&2 exit "$EXIT_FFMPEG_FAILED" fi # Now calculate the effective duration based on crop args DURATION_SECONDS=0.0 if [ -n "$START_TIME" ] || [ -n "$END_TIME" ]; then if [ -n "$START_TIME" ] && [ -n "$END_TIME" ]; then # Both start and end time are set, calculate the difference START_SECONDS=$(parse_time_to_seconds "$START_TIME") END_SECONDS=$(parse_time_to_seconds "$END_TIME") if [ -z "$START_SECONDS" ] || [ -z "$END_SECONDS" ]; then echo "Error: Could not parse START_TIME or END_TIME. Please check the format." >&2 exit "$EXIT_TIME_PARSING_FAILED" fi DURATION_SECONDS=$(awk -v start="$START_SECONDS" -v end="$END_SECONDS" 'BEGIN { printf "%.0f", end - start }') elif [ -n "$START_TIME" ]; then # Only start time is set, subtract from original duration START_SECONDS=$(parse_time_to_seconds "$START_TIME") if [ -z "$START_SECONDS" ]; then echo "Error: Could not parse START_TIME: '$START_TIME'." >&2 exit "$EXIT_TIME_PARSING_FAILED" fi DURATION_SECONDS=$(awk -v start="$START_SECONDS" -v original="$ORIGINAL_DURATION_SECONDS" 'BEGIN { printf "%.0f", original - start }') elif [ -n "$END_TIME" ]; then # Only end time is set, use that as the duration END_SECONDS=$(parse_time_to_seconds "$END_TIME") if [ -z "$END_SECONDS" ]; then echo "Error: Could not parse END_TIME: '$END_SECONDS'." >&2 exit "$EXIT_TIME_PARSING_FAILED" fi DURATION_SECONDS=$(printf "%.0f" "$END_SECONDS") fi else # No trimming, use the original duration DURATION_SECONDS=$(printf "%.0f" "$ORIGINAL_DURATION_SECONDS") fi # Final check for duration after calculations if [ "$DURATION_SECONDS" -le 0 ]; then echo "Error: Calculated video duration is 0 or less. Please check your start and end times." >&2 exit "$EXIT_ZERO_DURATION" fi # Apply speed factor to the duration if [ "$(echo "$TARGET_SPEED != 1.0" | bc -l)" -eq 1 ]; then NEW_DURATION_SECONDS=$(awk -v dur="$DURATION_SECONDS" -v speed="$TARGET_SPEED" 'BEGIN { printf "%.0f", dur / speed }') echo "Adjusting duration for speed factor: ${DURATION_SECONDS} seconds -> ${NEW_DURATION_SECONDS} seconds" DURATION_SECONDS="$NEW_DURATION_SECONDS" fi echo "Video duration to be processed: ${DURATION_SECONDS} seconds" echo "Original video dimensions: ${ORIGINAL_WIDTH}x${ORIGINAL_HEIGHT}" # --- Determine Filters and build filter graphs --- # Initialize the filter arrays declare -a VIDEO_FILTERS=() declare -a AUDIO_FILTERS=() # The CORRECT order of operations for best quality: # 1. Add FPS filter if specified (drops frames from the source) if [[ -n "$TARGET_FPS" && "$TARGET_FPS" =~ ^[0-9]+$ ]]; then VIDEO_FILTERS+=("fps=fps=${TARGET_FPS}") echo "Applying target FPS: ${TARGET_FPS}" else echo "No target FPS applied." fi # 2. Add speed filter if not normal speed (setpts for video) if [ "$(echo "$TARGET_SPEED != 1.0" | bc -l)" -eq 1 ]; then # Add setpts filter for video VIDEO_FILTERS+=("setpts=PTS/${TARGET_SPEED}") # Add atempo filter for audio (may require chaining) if [ "$IS_AUDIO_ENABLED" = "true" ]; then current_speed=$(echo "$TARGET_SPEED" | bc -l) while [ "$(echo "$current_speed > 2.0" | bc -l)" -eq 1 ]; do AUDIO_FILTERS+=("atempo=2.0") current_speed=$(echo "$current_speed / 2.0" | bc -l) done while [ "$(echo "$current_speed < 0.5" | bc -l)" -eq 1 ]; do AUDIO_FILTERS+=("atempo=0.5") current_speed=$(echo "$current_speed / 0.5" | bc -l) done if [ "$(echo "$current_speed != 1.0" | bc -l)" -eq 1 ]; then AUDIO_FILTERS+=("atempo=${current_speed}") fi fi echo "Applying speed filter: ${TARGET_SPEED}x" else echo "No speed filter applied." fi # 3. Add scaling filter if specified (applied to the already-filtered frames) if [[ -n "$TARGET_SMALLEST_DIMENSION" && "$TARGET_SMALLEST_DIMENSION" =~ ^[0-9]+$ ]]; then if [ "$ORIGINAL_WIDTH" -gt "$ORIGINAL_HEIGHT" ]; then VIDEO_FILTERS+=("scale=-2:${TARGET_SMALLEST_DIMENSION}") echo "Scaling landscape video: Smallest dimension (height) set to ${TARGET_SMALLEST_DIMENSION} pixels. Width will be calculated to maintain aspect ratio." elif [ "$ORIGINAL_HEIGHT" -gt "$ORIGINAL_WIDTH" ]; then VIDEO_FILTERS+=("scale=${TARGET_SMALLEST_DIMENSION}:-2") echo "Scaling portrait video: Smallest dimension (width) set to ${TARGET_SMALLEST_DIMENSION} pixels. Height will be calculated to maintain aspect ratio." else VIDEO_FILTERS+=("scale=${TARGET_SMALLEST_DIMENSION}:${TARGET_SMALLEST_DIMENSION}") echo "Scaling square video: Both dimensions set to ${TARGET_SMALLEST_DIMENSION} pixels." fi else echo "No video scaling applied (or invalid target_smallest_dimension)." fi # Join filters into a single string for FFmpeg VIDEO_FILTER_GRAPH="" if [ ${#VIDEO_FILTERS[@]} -gt 0 ]; then IFS=, VIDEO_FILTER_GRAPH="-vf $(echo "${VIDEO_FILTERS[*]}")" unset IFS fi AUDIO_FILTER_GRAPH="" if [ ${#AUDIO_FILTERS[@]} -gt 0 ]; then IFS=, AUDIO_FILTER_GRAPH="-af $(echo "${AUDIO_FILTERS[*]}")" unset IFS fi # --- 2. Calculate target total bitrate --- # Convert target size from MiB to kilobits (1 MiB = 8192 kbits) TARGET_SIZE_KBITS=$((TARGET_SIZE_MIB * 8 * 1024)) # Check for division by zero if duration is 0 if [ "$DURATION_SECONDS" -eq 0 ]; then echo "Error: Video duration is 0 seconds. Cannot calculate bitrate. Input video may be corrupt or too short." >&2 exit "$EXIT_ZERO_DURATION" fi # Calculate audio bitrate if audio is enabled, otherwise set it to 0 if [ "$IS_AUDIO_ENABLED" = "true" ]; then echo "Audio Bitrate: ${AUDIO_BITRATE_KBPS} kbps" AUDIO_BITRATE_TO_SUBTRACT="${AUDIO_BITRATE_KBPS}" else echo "Audio Bitrate: (Muted)" AUDIO_BITRATE_TO_SUBTRACT=0 fi # Calculate total target bitrate, with a small buffer for overhead TARGET_TOTAL_BITRATE_KBPS=$(( (TARGET_SIZE_KBITS / DURATION_SECONDS) * 95 / 100 )) # --- 3. Calculate target video bitrate --- TARGET_VIDEO_BITRATE_KBPS=$((TARGET_TOTAL_BITRATE_KBPS - AUDIO_BITRATE_TO_SUBTRACT)) # Ensure video bitrate is not negative or too low (e.g., minimum 50 kbps for video) if [ "$TARGET_VIDEO_BITRATE_KBPS" -le 50 ]; then echo "Warning: Calculated video bitrate (${TARGET_VIDEO_BITRATE_KBPS}kbps) is very low. This might severely impact quality. Setting minimum to 50kbps." >&2 TARGET_VIDEO_BITRATE_KBPS=50 fi echo "Calculated total target bitrate: ${TARGET_TOTAL_BITRATE_KBPS} kbps" echo "Calculated video bitrate: ${TARGET_VIDEO_BITRATE_KBPS} kbps" echo "Starting FFmpeg two-pass conversion..." # --- FFmpeg Pass 1 --- echo "--- Starting FFmpeg Pass 1 ---" PASS1_START_TIME=$(date +%s) # Start progress monitor in the background monitor_progress "Pass 1" "$PROGRESS_FIFO" "$DURATION_SECONDS" & MONITOR_PID=$! ffmpeg -hide_banner -v warning ${FFMPEG_CROP_ARGS} -i "$INPUT_FILE" ${FFMPEG_POST_CROP_ARGS} \ -g 240 -c:v libvpx-vp9 -quality best -b:v "${TARGET_VIDEO_BITRATE_KBPS}k" \ ${VIDEO_FILTER_GRAPH} \ -pass 1 -passlogfile "$PASSLOGFILE" -an -f webm -y /dev/null \ -progress pipe:1 > "$PROGRESS_FIFO" FFMPEG_PASS1_EXIT_CODE=$? wait "$MONITOR_PID" PASS1_END_TIME=$(date +%s) PASS1_DURATION=$((PASS1_END_TIME - PASS1_START_TIME)) echo "--- FFmpeg Pass 1 completed in ${PASS1_DURATION} seconds ---" if [ "$FFMPEG_PASS1_EXIT_CODE" -ne "$EXIT_SUCCESS" ]; then echo "Error: FFmpeg first pass failed with exit code $FFMPEG_PASS1_EXIT_CODE." >&2 rm -f "${PASSLOGFILE}-0.log" "${PASSLOGFILE}-0.log.temp" exit "$EXIT_FFMPEG_FAILED" fi # --- FFmpeg Pass 2 --- echo "--- Starting FFmpeg Pass 2 ---" PASS2_START_TIME=$(date +%s) # Build audio options for the second pass if [ "$IS_AUDIO_ENABLED" = "true" ]; then # The fix is here: removing the quotes around the entire `-b:a ...` part # so that the shell correctly interprets the variable and the 'k' suffix. FFMPEG_AUDIO_OPTIONS="${AUDIO_FILTER_GRAPH} -c:a libopus -b:a ${AUDIO_BITRATE_KBPS}k" else FFMPEG_AUDIO_OPTIONS="-an" fi # Start progress monitor for pass 2 monitor_progress "Pass 2" "$PROGRESS_FIFO" "$DURATION_SECONDS" & MONITOR_PID=$! ffmpeg -hide_banner -v warning ${FFMPEG_CROP_ARGS} -i "$INPUT_FILE" ${FFMPEG_POST_CROP_ARGS} \ -g 240 -c:v libvpx-vp9 -quality best -b:v "${TARGET_VIDEO_BITRATE_KBPS}k" \ ${VIDEO_FILTER_GRAPH} \ -pass 2 -passlogfile "$PASSLOGFILE" \ ${FFMPEG_AUDIO_OPTIONS} \ -y "$OUTPUT_FILE" \ -progress pipe:1 > "$PROGRESS_FIFO" FFMPEG_PASS2_EXIT_CODE=$? wait "$MONITOR_PID" PASS2_END_TIME=$(date +%s) PASS2_DURATION=$((PASS2_END_TIME - PASS2_START_TIME)) echo "--- FFmpeg Pass 2 completed in ${PASS2_DURATION} seconds ---" # Clean up pass log files rm -f "${PASSLOGFILE}-0.log" "${PASSLOGFILE}-0.log.temp" if [ "$FFMPEG_PASS2_EXIT_CODE" -ne "$EXIT_SUCCESS" ]; then echo "Error: FFmpeg second pass failed with exit code $FFMPEG_PASS2_EXIT_CODE." >&2 exit "$EXIT_FFMPEG_FAILED" fi echo "Conversion complete! Output file: $OUTPUT_FILE" echo "Final size ( check with 'ls -lh \"$OUTPUT_FILE\"' ): " ls -lh "$OUTPUT_FILE" SCRIPT_END_TIME=$(date +%s) TOTAL_DURATION=$((SCRIPT_END_TIME - SCRIPT_START_TIME)) echo "--- Total script execution time: ${TOTAL_DURATION} seconds ---" exit "$EXIT_SUCCESS"