Page 1 of 1

Animated infinity loops instead of pictures

Posted: Mon Aug 11, 2025 9:38 am
by EstimPaul
Hi,

I wrote a little piece of software I present here. It takes a link to your favourite porn site and a start-time. Then it extracts a 30sec sniplet and replays it (needed for precision reasons). You can now read off the exact timing of a small sniplet (2-3 seconds suggested). It will render it smooth by transforming a frame sequence ABCDEF to ABCDEFEDCBA which allows smooth looping.
Then the output is compressed into a rel. small WEBP format. I also reduce size to (less equal) 720p for size reasons. A 2-second sniplet will result into a ~1MB animated "picture". So you can use some infinite-loop BJ video, or spanking, or whatever in your teases :-)

Here it is, as BASH script:

Code: Select all

#!/bin/bash
# Script to convert video to looping animated WebP looping infinitely
# in a smooth way. The main idea is to order frames : if the initial
# video has 6 frames, say ABCDEF then we construct a new video like
# ABCDEFEDCBA which then allows a smooth infinity-loop (the A-frame is
# displayed double in the looping, but that is fine, to avoid
# glitter. Careful: this DOUBLES video-length, roughly. So to keep
# filesize good, I suggest 2 second snips!

# Author: EstimPaul based on sniplets found in the web.

# ##############################

## youtube-downloader
YTDLP="python3 /home/user/yt-dlp/yt-dlp"

## mediaplayer
MPLAYER="mpv"


# ##############################

set -euo pipefail

# Configuration
FPS_INTERMEDIATE=24
FPS_OUTPUT=30
CRF=18
PRESET="veryfast"

# Function to show notifications
notify_user() {
    local message="$1"
    local urgency="${2:-normal}"
    
    if command -v notify-send &> /dev/null; then
        notify-send -u "$urgency" "Video to WebP" "$message"
    fi
    echo "$message"
}

# Function to cleanup temporary files
cleanup() {
    local temp_file="$1"
    if [[ -f "$temp_file" ]]; then
        rm -f "$temp_file"
    fi
}


# Function to get video duration
get_video_duration() {
    ffprobe -v quiet -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$input_file" 2>/dev/null || echo "0"
}

# Function to download a snip
dl_snip() {
    local temp_snip="${base_name}_tmp_snip.mp4"
    local output_file="${base_name}_loop.webp"

    # Ask for video link and start time
    read -p "Enter start time (e.g. 00:01:23 or 83). Hint: start 10 sec too early!: " start_time
    
    # Get direct video URL using yt-dlp
    echo "Retrieving direct video URL..."
    direct_url=$($YTDLP -g "$video_link" | head -n 1)
        
    if [ -z "$direct_url" ]; then
      echo "Failed to retrieve direct video URL."
      exit 1
    fi
    
    # Extract 30 seconds sniplet with video copy codec, no audio
    echo "Extracting 30 seconds sniplet..."
    
    if ! ffmpeg -ss "$start_time" -i "$direct_url" -t 30 -c:v copy -an -y "$temp_snip" 2>/dev/null < /dev/null; then
        echo "ffmpeg failed to extract sniplet." 
        exit 1
    fi
    

# Loop until user confirms final clip
while true; do
     # Play the sniplet once for initial review
     echo "Playing the 30-second sniplet (half speed, looped)..."
     $MPLAYER --loop --speed=0.5 --osd-playing-msg='Time: ${time-pos} sec' --osd-level=3 "$temp_snip" 2>/dev/null
    echo
    read -p "Enter precise start time of the clip within sniplet: " precise_start
    read -p "Enter duration of the clip: " duration

    echo "Extracting subclip from $precise_start for $duration ..."
    if ! ffmpeg -ss "$precise_start" -i "$temp_snip" -t "$duration" -c:v copy -an -y "$input_file" < /dev/null 2>/dev/null; then
        echo "Failed to extract subclip, please try again."
        continue
    fi

    echo "Playing extracted subclip..."
    $MPLAYER --osd-playing-msg='Time: ${time-pos} sec' --osd-level=3 "$input_file" 2>/dev/null

    # Ask user if the clip is acceptable
    read -p "Accept this clip? (y/N): " accept

    case "$accept" in
        [Yy]* )
            echo "Final clip saved for processing"
            break
            ;;
        * )
            echo "Let's try again."
            rm -f "$input_file"
            ;;
    esac
done

}


# Function to convert video to WebP
convert_video_to_webp() {
    local temp_cfr="${base_name}_tmp_cfr.mp4"
    local temp_rev="${base_name}_tmp_rev.mp4"
    local temp_abba="${base_name}_tmp_abba.mp4"

    
    # Validate input file
    if [[ ! -f "$input_file" ]]; then
        notify_user "Error: Input file '$input_file' not found" "critical"
        return 1
    fi
    
    
    # Get video duration
    local duration
    duration=$(get_video_duration "$input_file")
    
    if (( $(echo "$duration < 1" | bc -l) )); then
        notify_user "Error: Unable to determine video duration or video too short" "critical"
        return 1
    fi
    
    notify_user "Starting conversion of '$input_file' to WebP..."
    
    # Step 1: Convert to constant frame rate
    notify_user "Step 1/3: Converting to constant frame rate..."
    if ! ffmpeg -y -i "$input_file" \
        -r "$FPS_INTERMEDIATE" \
        -c:v libx264 \
        -crf "$CRF" \
        -preset "$PRESET" \
        -an \
        "$temp_cfr" 2>/dev/null; then
        notify_user "Error: Failed to convert to CFR" "critical"
        cleanup "$temp_cfr"
        return 1
    fi

    # Step 2:
    notify_user "Step 2/3: reverting video to smooth one using: AB -> ABBA ..."
      
     # get total frame number
     if ! frame_number=$(ffmpeg -i "$temp_cfr" -vcodec copy -acodec copy -f null /dev/null 2>&1 | grep 'frame=' | sed -nE 's/.*frame=[[:space:]]*([0-9]+)[[:space:]]*fps=.*/\1/p'); then
         notify_user "Error: Failed to get total frame number" "critical"
         cleanup "$temp_cfr"
         return 1
     fi
      
     # Get fps as decimal number
     if ! fps=$(ffprobe -v 0 -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 "$temp_cfr" 2>/dev/null); then
         notify_user "Error: Failed to get fps" "critical"
         cleanup "$temp_cfr"
         return 1
     fi
      
     fps_decimal=$(echo "scale=6; $fps" | bc -l)
      
     # Calculate the segment duration (seconds) for frames to reverse
     duration_to_reverse=$(echo "scale=6; $frame_number / $fps_decimal" | bc -l)
      
     # Get total duration of input video in seconds
     if ! total_duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$temp_cfr" 2>/dev/null); then
         notify_user "Error: Failed to get total duration" "critical"
         cleanup "$temp_cfr"
         return 1
     fi
      
     # Calculate start time for the segment to extract
     start_time=$(echo "scale=6; $total_duration - $duration_to_reverse" | bc -l)
     start_time=$(printf "%.6f" "$start_time")
      
     # Run ffmpeg to extract last N frames segment and reverse it
     if ! ffmpeg -ss "$start_time" -i "$temp_cfr" -t "$duration_to_reverse" -r "$fps_decimal" -vf reverse -af areverse -c:v libx264 -crf 18 -preset veryfast -c:a aac -b:a 128k "$temp_rev" 2>/dev/null; then
         notify_user "Error: Failed to reverse video segment" "critical"
         cleanup "$temp_cfr"
         cleanup "$temp_rev"
         return 1
     fi
      
      # Concat!
     if ! ffmpeg -i "$temp_cfr" -i "$temp_rev" -filter_complex "[0:v:0][1:v:0]concat=n=2:v=1:a=0[outv]" -map "[outv]" -c:v libx264 -crf 18 -preset veryfast "$temp_abba" 2>/dev/null; then
         notify_user "Error: Failed to concatenate videos" "critical"
         cleanup "$temp_cfr"
         cleanup "$temp_rev"
         return 1
     fi
      
   
    # Step 3: Convert to WebP
    notify_user "Step 3/3: Converting to animated WebP..."

    if ! ffmpeg -y -i "$temp_abba" \
         -vf "fps=30,scale='if(gt(iw,720),720,iw)':-2:flags=lanczos" \
         -loop 0 \
         -quality 80 \
         -method 6 \
         -lossless 0 \
         "$output_file" 2>/dev/null; then
         notify_user "Error: Failed to convert to WebP" "critical"
         cleanup "$temp_abba"
         return 1
    fi
    
    # Cleanup temporary files
    cleanup "$temp_cfr"
    cleanup "$temp_rev"
    cleanup "$temp_abba"
    cleanup "$temp_snip"
    cleanup "$input_file"
    
    # Get file sizes for comparison
    local input_size output_size
    input_size=$(du -h "$input_file" | cut -f1)
    output_size=$(du -h "$output_file" | cut -f1)
    
    notify_user "āœ… Conversion completed successfully!
Input: $input_size → Output: $output_size
Saved as: $(basename "$output_file")"
    
}

# Main execution
main() {
    if [[ -z "${1:-}" ]]; then
        echo "Usage: $0 video_link"
        exit 1
    fi
    video_link="$1"
    
    # Check if ffmpeg is available
    if ! command -v ffmpeg &> /dev/null; then
        notify_user "Error: ffmpeg is not installed. Install it with: sudo dnf install ffmpeg" "critical"
        return 1
    fi
    # Check if YTDLP is available
    if ! command -v $YTDLP &> /dev/null; then
        notify_user "Error: YOUTUBE-DOWNLOADER is not installed/wrong path. Install it or change path in the head of file." "critical"
        return 1
    fi
    # Check if MPLAYER is available
    if ! command -v $MPLAYER &> /dev/null; then
        notify_user "Error: YOUTUBE-DOWNLOADER is not installed/wrong path. Install it or change path in the head of file." "critical"
        return 1
    fi


    ###### define filenames
    input_file=$(mktemp /tmp/tmp.XXXXXX.mp4)
    base_name="${input_file%.*}"
    output_file=$(yt-dlp --get-filename -o "%(title).100s.%(id)s.webp" --restrict-filenames $video_link)
    echo "DEBUG: output_file='$output_file'"
    # Download sniplet
    dl_snip
    # Convert to absolute path
    input_file=$(realpath "$input_file")
    # Start conversion to looping webp
    convert_video_to_webp "$input_file"
}

# Trap cleanup on exit
trap 'cleanup "${base_name}_tmp_cfr.mp4" 2>/dev/null || true; cleanup "${base_name}_tmp_abba.mp4" 2>/dev/null || true; cleanup "${base_name}_tmp_rev.mp4" 2>/dev/null || true' EXIT

# Run main function
main "$@"

and here is the same as PYTHON code (produced from the bash one by a good LLM)

Code: Select all

"""
YouTube to Looping WebP Converter

This script converts YouTube videos into smoothly looping WebP animations
using the "ping-pong" technique (ABBA instead of just AB) for seamless loops.

Usage:
    python youtube_to_webp.py "https://youtube.com/watch?v=..."

Requirements:
    - Python 3.6+
    - yt-dlp (install with: pip install yt-dlp)
    - ffmpeg (must be in system PATH)
    - ffprobe (must be in system PATH)
    Optional: mpv (for preview functionality)
"""

import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Optional, Tuple, List

# Configuration
FPS_INTERMEDIATE = 24
FPS_OUTPUT = 30
CRF = 18
PRESET = "veryfast"
MAX_SNIP_SECONDS = 30
SNIP_EXTRA_SECONDS = 10
WEBP_SCALE_MAX_WIDTH = 720
WEBP_QUALITY = 80
WEBP_METHOD = 6
WEBP_LOSSLESS = 0

def check_dependencies() -> Tuple[str, str, str, Optional[str]]:
    """Check if required dependencies are installed and return their paths."""
    # Check yt-dlp
    yt_dlp_path = shutil.which("yt-dlp") or shutil.which("youtube-dl")
    if not yt_dlp_path:
        print("Error: yt-dlp is not installed. Install it with: pip install yt-dlp", file=sys.stderr)
        sys.exit(1)
    
    # Check ffmpeg
    ffmpeg_path = shutil.which("ffmpeg")
    if not ffmpeg_path:
        print("Error: ffmpeg is not installed. Please install ffmpeg.", file=sys.stderr)
        sys.exit(1)
    
    # Check ffprobe
    ffprobe_path = shutil.which("ffprobe")
    if not ffprobe_path:
        print("Error: ffprobe is not installed. Please install ffmpeg (which includes ffprobe).", file=sys.stderr)
        sys.exit(1)
    
    # Check mpv (optional)
    mpv_path = shutil.which("mpv")
    if not mpv_path:
        print("Warning: mpv not found. Preview functionality will be limited.")
    
    return yt_dlp_path, ffmpeg_path, ffprobe_path, mpv_path

def run_command(cmd: List[str], capture_output: bool = False, check: bool = True) -> str:
    """Run a command and handle errors."""
    try:
        result = subprocess.run(
            cmd,
            capture_output=capture_output,
            text=True,
            check=check,
            stderr=subprocess.PIPE
        )
        if capture_output:
            return result.stdout.strip()
        return ""
    except subprocess.CalledProcessError as e:
        error_msg = e.stderr.strip() if e.stderr else str(e)
        print(f"Command failed: {' '.join(cmd)}\nError: {error_msg}", file=sys.stderr)
        if not capture_output:
            sys.exit(1)
        return ""

def time_to_seconds(time_str: str) -> float:
    """Convert time string (HH:MM:SS, MM:SS, or SS) to seconds."""
    parts = list(map(float, re.split(r'[:.,]', time_str.strip())))
    
    if len(parts) == 1:
        return parts  # Seconds
    elif len(parts) == 2:
        return parts * 60 + parts[1]  # Minutes:Seconds
    elif len(parts) == 3:
        return parts * 3600 + parts[1] * 60 + parts[2]  # Hours:Minutes:Seconds
    else:
        raise ValueError(f"Invalid time format: {time_str}")

def seconds_to_time_str(seconds: float) -> str:
    """Convert seconds to HH:MM:SS.mmm format."""
    hours = int(seconds // 3600)
    seconds %= 3600
    minutes = int(seconds // 60)
    seconds %= 60
    return f"{hours:02d}:{minutes:02d}:{seconds:06.3f}"

def get_video_duration(ffprobe: str, file_path: Path) -> float:
    """Get video duration in seconds."""
    cmd = [
        ffprobe,
        "-v", "quiet",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        str(file_path)
    ]
    try:
        output = run_command(cmd, capture_output=True)
        return float(output) if output else 0.0
    except (ValueError, subprocess.CalledProcessError):
        return 0.0

def get_video_fps(ffprobe: str, file_path: Path) -> float:
    """Get video FPS as a decimal value."""
    cmd = [
        ffprobe,
        "-v", "error",
        "-select_streams", "v:0",
        "-show_entries", "stream=r_frame_rate",
        "-of", "csv=p=0",
        str(file_path)
    ]
    try:
        fps_str = run_command(cmd, capture_output=True)
        if '/' in fps_str:
            num, den = map(float, fps_str.split('/'))
            return num / den
        return float(fps_str)
    except (ValueError, subprocess.CalledProcessError, TypeError):
        return FPS_INTERMEDIATE  # Fallback to configured FPS

def get_output_filename(yt_dlp: str, url: str) -> str:
    """Generate output filename using yt-dlp's template."""
    cmd = [
        yt_dlp,
        "--get-filename",
        "-o", "%(title).100s.%(id)s.webp",
        "--restrict-filenames",
        url
    ]
    try:
        filename = run_command(cmd, capture_output=True)
        # Clean up any invalid characters for cross-platform compatibility
        filename = re.sub(r'[\\/*?:"<>|]', "_", filename)
        if not filename.endswith(".webp"):
            filename += ".webp"
        return filename
    except Exception:
        # Fallback if yt-dlp command fails
        video_id = re.search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", url)
        id_str = video_id.group(1) if video_id else "video"
        return f"youtube_{id_str}.webp"

def get_direct_url(yt_dlp: str, url: str) -> str:
    """Get direct video URL using yt-dlp."""
    cmd = [yt_dlp, "-g", url]
    output = run_command(cmd, capture_output=True)
    
    # Return the first HTTP/HTTPS URL
    for line in output.splitlines():
        if line.startswith(("http://", "https://")):
            return line.strip()
    
    raise RuntimeError("Failed to retrieve direct video URL")

def play_video(mpv: Optional[str], file_path: Path, speed: float = 1.0, loop: bool = False) -> None:
    """Play video using mpv if available."""
    if not mpv:
        print(f"\nPreview not available (mpv not found). Open this file to review: {file_path}")
        return
    
    args = [mpv, str(file_path)]
    if loop:
        args.insert(1, "--loop")
    if speed != 1.0:
        args.insert(1, f"--speed={speed}")
    args.insert(1, "--osd-level=3")
    args.insert(1, "--osd-playing-msg=Time: ${time-pos} sec")
    
    try:
        subprocess.run(args, check=True)
    except subprocess.CalledProcessError:
        print(f"Warning: Failed to play {file_path}", file=sys.stderr)

def extract_snip(
    ffmpeg: str,
    yt_dlp: str,
    url: str,
    start_time: str,
    temp_dir: str
) -> Path:
    """Extract a 30-second snippet from the video."""
    print("Retrieving direct video URL...")
    direct_url = get_direct_url(yt_dlp, url)
    
    print("Extracting 30 seconds sniplet...")
    temp_snip = Path(temp_dir) / "temp_snip.mp4"
    
    cmd = [
        ffmpeg,
        "-ss", start_time,
        "-i", direct_url,
        "-t", str(MAX_SNIP_SECONDS),
        "-c:v", "copy",
        "-an",
        "-y",
        str(temp_snip)
    ]
    run_command(cmd)
    
    return temp_snip

def select_subclip(
    ffmpeg: str,
    mpv: Optional[str],
    snip_path: Path
) -> Tuple[Path, float, float]:
    """Interactive process to select a subclip from the snip."""
    temp_dir = snip_path.parent
    input_file = Path(temp_dir) / "input_video.mp4"
    
    while True:
        # Play snippet for initial review
        print("\nPlaying the 30-second sniplet (half speed, looped)...")
        play_video(mpv, snip_path, speed=0.5, loop=True)
        
        # Get user input
        try:
            precise_start = input("\nEnter precise start time of the clip within sniplet (e.g., 00:01:23 or 83): ").strip()
            duration = input("Enter duration of the clip (e.g., 2.5): ").strip()
            
            # Convert to seconds for validation
            start_sec = time_to_seconds(precise_start)
            duration_sec = time_to_seconds(duration)
            
            # Validate inputs
            snip_duration = get_video_duration(ffmpeg, snip_path)
            if start_sec < 0 or start_sec >= snip_duration:
                print(f"Error: Start time must be between 0 and {snip_duration:.2f} seconds")
                continue
                
            if duration_sec <= 0 or (start_sec + duration_sec) > snip_duration:
                print(f"Error: Duration must be positive and end before {snip_duration:.2f} seconds")
                continue
                
        except ValueError as e:
            print(f"Error: {e}")
            continue
            
        print(f"\nExtracting subclip from {precise_start} for {duration}...")
        cmd = [
            ffmpeg,
            "-ss", precise_start,
            "-i", str(snip_path),
            "-t", duration,
            "-c:v", "copy",
            "-an",
            "-y",
            str(input_file)
        ]
        try:
            run_command(cmd)
        except subprocess.CalledProcessError:
            print("Failed to extract subclip, please try again.")
            continue
            
        # Preview the extracted clip
        print("\nPlaying extracted subclip...")
        play_video(mpv, input_file)
        
        # Ask for confirmation
        accept = input("\nAccept this clip? (y/N): ").strip().lower()
        if accept.startswith('y'):
            print("Final clip saved for processing")
            return input_file, start_sec, duration_sec
            
        print("Let's try again.")
        if input_file.exists():
            input_file.unlink()

def create_abba_loop(
    ffmpeg: str,
    ffprobe: str,
    input_file: Path,
    temp_dir: str
) -> Path:
    """Create the ABBA (ping-pong) loop pattern for smooth looping."""
    print("\nStep 1/3: Converting to constant frame rate...")
    temp_cfr = Path(temp_dir) / "temp_cfr.mp4"
    
    cmd = [
        ffmpeg,
        "-y", "-i", str(input_file),
        "-r", str(FPS_INTERMEDIATE),
        "-c:v", "libx264",
        "-crf", str(CRF),
        "-preset", PRESET,
        "-an",
        str(temp_cfr)
    ]
    run_command(cmd)
    
    print("Step 2/3: Creating smooth loop using ABBA pattern...")
    # Get video properties
    duration = get_video_duration(ffprobe, temp_cfr)
    fps = get_video_fps(ffprobe, temp_cfr)
    
    if duration <= 0 or fps <= 0:
        raise RuntimeError("Could not determine video duration or FPS")
    
    # Create reversed segment
    temp_rev = Path(temp_dir) / "temp_rev.mp4"
    cmd = [
        ffmpeg,
        "-y", "-i", str(temp_cfr),
        "-vf", "reverse",
        "-an",
        "-c:v", "libx264",
        "-crf", str(CRF),
        "-preset", PRESET,
        str(temp_rev)
    ]
    run_command(cmd)
    
    # Concatenate original and reversed
    temp_abba = Path(temp_dir) / "temp_abba.mp4"
    cmd = [
        ffmpeg,
        "-y",
        "-i", str(temp_cfr),
        "-i", str(temp_rev),
        "-filter_complex", "[0:v:0][1:v:0]concat=n=2:v=1:a=0[outv]",
        "-map", "[outv]",
        "-c:v", "libx264",
        "-crf", str(CRF),
        "-preset", PRESET,
        str(temp_abba)
    ]
    run_command(cmd)
    
    return temp_abba

def convert_to_webp(
    ffmpeg: str,
    abba_file: Path,
    output_file: Path
) -> None:
    """Convert the ABBA file to looping WebP."""
    print("Step 3/3: Converting to animated WebP...")
    
    cmd = [
        ffmpeg,
        "-y", "-i", str(abba_file),
        "-vf", f"fps={FPS_OUTPUT},scale='if(gt(iw,{WEBP_SCALE_MAX_WIDTH}),{WEBP_SCALE_MAX_WIDTH},iw)':-2:flags=lanczos",
        "-loop", "0",
        "-quality", str(WEBP_QUALITY),
        "-method", str(WEBP_METHOD),
        "-lossless", str(WEBP_LOSSLESS),
        str(output_file)
    ]
    run_command(cmd)

def get_file_size(file_path: Path) -> str:
    """Get human-readable file size."""
    size_bytes = file_path.stat().st_size
    if size_bytes < 1024:
        return f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.1f} KB"
    elif size_bytes < 1024 * 1024 * 1024:
        return f"{size_bytes / (1024 * 1024):.1f} MB"
    else:
        return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"

def main():
    """Main function."""
    parser = argparse.ArgumentParser(description="Convert YouTube videos to looping WebP animations")
    parser.add_argument("url", help="YouTube video URL")
    args = parser.parse_args()
    
    # Check dependencies
    yt_dlp, ffmpeg, ffprobe, mpv = check_dependencies()
    
    try:
        # Create temporary directory
        with tempfile.TemporaryDirectory() as temp_dir:
            print(f"\nWorking in temporary directory: {temp_dir}")
            
            # Get output filename
            output_file = Path(get_output_filename(yt_dlp, args.url))
            print(f"Output will be saved as: {output_file}")
            
            # Get start time from user
            start_time = input(
                f"Enter start time (e.g., 00:01:23 or 83). "
                f"Hint: start {SNIP_EXTRA_SECONDS} sec too early!: "
            ).strip()
            
            # Extract snip
            temp_snip = extract_snip(ffmpeg, yt_dlp, args.url, start_time, temp_dir)
            
            # Select subclip
            input_file, _, _ = select_subclip(ffmpeg, mpv, temp_snip)
            
            # Create ABBA loop
            abba_file = create_abba_loop(ffmpeg, ffprobe, input_file, temp_dir)
            
            # Convert to WebP
            convert_to_webp(ffmpeg, abba_file, output_file)
            
            # Show completion message
            input_size = get_file_size(input_file)
            output_size = get_file_size(output_file)
            print(f"\nāœ… Conversion completed successfully!")
            print(f"Input: {input_size} → Output: {output_size}")
            print(f"Saved as: {output_file}")
            
    except KeyboardInterrupt:
        print("\nOperation cancelled by user.")
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()



Re: Animated infinity loops instead of pictures

Posted: Mon Aug 11, 2025 11:39 am
by EstimPaul
Here are examples. They take < 1min to be produced each.
https://postimg.cc/w72H91BR and https://postimg.cc/Ln1CV7c9 or https://postimg.cc/nXvQTYV9

Re: Animated infinity loops instead of pictures

Posted: Mon Aug 11, 2025 7:34 pm
by EstimPaul
Oh! Maybe EOS should receive an update :-)

And GIF's ? They are 4-6x larger, but if that is the only way to go ...

Re: Animated infinity loops instead of pictures

Posted: Tue Aug 12, 2025 7:36 am
by EstimPaul
for GIF's you would replace "step 3" in to

Code: Select all

ffmpeg -i $temp_abba -vf "fps=10,scale='if(gt(iw,ih),720,-1)':'if(gt(ih,iw),720,-1)',palettegen" -y palette.png
ffmpeg -i $temp_abba -i palette.png -filter_complex "fps=10,scale='if(gt(iw,ih),720,-1)':'if(gt(ih,iw),720,-1)',paletteuse" "$output_file".gif
cleanup palette.png
in the script & you'll get animated GIF's.

Additionally I asked the "tech support" about webp :-D