Animated infinity loops instead of pictures
Posted: Mon Aug 11, 2025 9:38 am
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:
and here is the same as PYTHON code (produced from the bash one by a good LLM)
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()