quicktools.videotools
Video utilities: transcription, audio extraction, metadata, frame capture, and speaker diarization — powered by PyAV, faster-whisper, and Pyannote.
Requires the optional 'av' package: pip install av (This is already installed automatically as a dependency of faster-whisper.)
Supports common video containers: MP4, MOV, MKV, AVI, WEBM, and more — since PyAV uses FFmpeg's decoding engine internally, the same one that powers audiotools.
1"""Video utilities: transcription, audio extraction, metadata, frame capture, 2and speaker diarization — powered by PyAV, faster-whisper, and Pyannote. 3 4Requires the optional 'av' package: pip install av 5(This is already installed automatically as a dependency of faster-whisper.) 6 7Supports common video containers: MP4, MOV, MKV, AVI, WEBM, and more — since PyAV 8uses FFmpeg's decoding engine internally, the same one that powers audiotools. 9""" 10import os 11import shutil 12import tempfile 13 14 15def get_video_info(path: str) -> dict: 16 """Return basic metadata about a video file: duration (seconds), width, height, and frame rate.""" 17 import av 18 container = av.open(path) 19 stream = container.streams.video[0] 20 duration = float(container.duration / av.time_base) if container.duration else None 21 info = { 22 "duration_seconds": duration, 23 "width": stream.width, 24 "height": stream.height, 25 "fps": float(stream.average_rate) if stream.average_rate else None, 26 } 27 container.close() 28 return info 29 30 31def get_video_duration(path: str) -> float: 32 """Return the duration of a video file in seconds.""" 33 return get_video_info(path)["duration_seconds"] 34 35 36def extract_audio_from_video(video_path: str, output_audio_path: str) -> None: 37 """Extract the audio track from a video file and save it as a standalone audio file 38 (format is inferred from output_audio_path's extension, e.g. .mp3, .wav, .m4a).""" 39 import av 40 41 input_container = av.open(video_path) 42 audio_stream = input_container.streams.audio[0] 43 44 output_container = av.open(output_audio_path, mode="w") 45 output_stream = output_container.add_stream("aac" if output_audio_path.endswith((".m4a", ".mp4")) else "mp3") 46 47 for frame in input_container.decode(audio_stream): 48 for packet in output_stream.encode(frame): 49 output_container.mux(packet) 50 51 for packet in output_stream.encode(None): 52 output_container.mux(packet) 53 54 output_container.close() 55 input_container.close() 56 57 58def extract_video_frame(video_path: str, timestamp_seconds: float, output_image_path: str) -> None: 59 """Extract a single frame from a video at the given timestamp (seconds) and save it as an image.""" 60 import av 61 62 container = av.open(video_path) 63 stream = container.streams.video[0] 64 65 target_pts = int(timestamp_seconds / stream.time_base) 66 container.seek(target_pts, stream=stream) 67 68 for frame in container.decode(stream): 69 if frame.time >= timestamp_seconds: 70 frame.to_image().save(output_image_path) 71 break 72 73 container.close() 74 75 76def transcribe_video(path: str, model_size: str = "base", language: str | None = None) -> str: 77 """Transcribe the spoken audio in a video file to plain text. Works directly on 78 video containers (MP4, MOV, MKV, etc.) — the audio track is extracted automatically.""" 79 from quicktools.audiotools import transcribe_audio 80 return transcribe_audio(path, model_size=model_size, language=language) 81 82 83def transcribe_video_with_timestamps(path: str, model_size: str = "base") -> list[dict]: 84 """Transcribe a video's audio into timestamped segments, each with 'start', 'end', and 'text'.""" 85 from quicktools.audiotools import transcribe_audio_with_timestamps 86 return transcribe_audio_with_timestamps(path, model_size=model_size) 87 88 89def transcribe_video_word_level(path: str, model_size: str = "base") -> list[dict]: 90 """Transcribe a video's audio into word-by-word timestamps, each with 'word', 'start', and 'end'. 91 Useful for generating captions synced precisely to speech.""" 92 from quicktools.audiotools import transcribe_audio_word_level 93 return transcribe_audio_word_level(path, model_size=model_size) 94 95 96# --- NEW DIARIZATION AND STYLING FEATURES --- 97 98from quicktools import audiotools 99 100def transcribe_video_with_speakers(path_or_url: str, hf_token: str, model_size: str = "base", device: str = "auto") -> list[dict]: 101 """ 102 Extracts audio from a local video file or web URL (YouTube, TikTok, IG, X), 103 transcribes it, and maps the text to specific speakers. 104 """ 105 print(f"Processing video source: {path_or_url}") 106 return audiotools.transcribe_with_speakers(path_or_url, hf_token, model_size, device) 107 108 109def save_video_script_to_docx(transcript_data: list[dict], output_path: str, title_text: str = "Video Script & Transcript") -> None: 110 """ 111 Formats speaker-mapped video transcript data as a professional script in Word (.docx). 112 """ 113 try: 114 from docx import Document 115 from docx.shared import Pt, RGBColor 116 except ImportError: 117 raise ImportError("Saving to Word requires python-docx. Run: pip install python-docx") 118 119 doc = Document() 120 title = doc.add_heading(title_text, level=1) 121 title.alignment = 1 # Center align 122 123 last_speaker = None 124 125 for entry in transcript_data: 126 p = doc.add_paragraph() 127 128 # Format timestamp as [MM:SS] 129 start_m, start_s = divmod(int(entry["start"]), 60) 130 time_str = f"[{start_m:02d}:{start_s:02d}]" 131 132 # Speaker header line if speaker changed 133 if entry["speaker"] != last_speaker: 134 speaker_run = p.add_run(f"{time_str} {entry['speaker']}:\n") 135 speaker_run.bold = True 136 speaker_run.font.color.rgb = RGBColor(112, 48, 160) # Distinct Purple highlight for video speakers 137 last_speaker = entry["speaker"] 138 139 p.add_run(entry["text"]) 140 p.paragraph_format.space_after = Pt(8) 141 142 doc.save(output_path) 143 144 145import os 146import shutil 147import random 148 149def download_video(url: str, output_dir: str = ".", filename: str | None = None, resolution: str = "best", cookiefile: str | None = None) -> str: 150 """ 151 Downloads a video file from supported web URLs (YouTube, TikTok, Instagram, X/Twitter, etc.). 152 153 :param url: The web video URL to download. 154 :param output_dir: Destination folder (defaults to current directory). 155 :param filename: Optional custom filename (without extension). Defaults to video title. 156 :param resolution: Quality target ('best' or 'worst'). 157 :param cookiefile: Path to a cookies.txt file to bypass login restrictions. 158 :return: Absolute file path of the downloaded video. 159 """ 160 try: 161 import yt_dlp 162 except ImportError: 163 raise ImportError( 164 "Downloading videos requires 'yt-dlp'. Install it with: pip install yt-dlp" 165 ) 166 167 os.makedirs(output_dir, exist_ok=True) 168 out_template = f"{filename}.%(ext)s" if filename else "%(title)s.%(ext)s" 169 target_path = os.path.join(output_dir, out_template) 170 171 # --- AUTO-DETECT SERVER COOKIES --- 172 if not cookiefile and os.path.exists('/app/cookies.txt'): 173 cookiefile = '/app/cookies.txt' 174 # ---------------------------------- 175 176 # --- THE SMART FFMPEG FALLBACK --- 177 has_ffmpeg = shutil.which("ffmpeg") is not None 178 179 if resolution == "best": 180 if has_ffmpeg: 181 # Grab max quality separate streams and merge them 182 fmt = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' 183 else: 184 # Fall back to the best PRE-MERGED format (usually 720p max) to prevent a crash 185 print("⚠️ [quicktools] FFmpeg not found. Falling back to pre-merged 720p format.") 186 print("💡 Tip: Install FFmpeg ('winget install ffmpeg') to enable 1080p+ downloads.") 187 fmt = 'best[ext=mp4]/best' 188 else: 189 fmt = 'worst' 190 # --------------------------------- 191 192 ydl_opts = { 193 'format': fmt, 194 'outtmpl': target_path, 195 'quiet': False, 196 'no_warnings': True, 197 'merge_output_format': 'mp4' if has_ffmpeg else None, 198 'extractor_args': {'youtube': ['player_client=android']}, 199 } 200 201 # --- CRITICAL APPLE iOS FIXES --- 202 if has_ffmpeg: 203 # Force FFmpeg to move the moov atom to the top of the file (Fast Start) 204 ydl_opts['postprocessor_args'] = ['-movflags', '+faststart'] 205 # -------------------------------- 206 207 # --- ATTACH COOKIE IF AVAILABLE --- 208 if cookiefile and os.path.exists(cookiefile): 209 print(f"🔑 Using cookie authentication file: {cookiefile}") 210 ydl_opts['cookiefile'] = cookiefile 211 # ---------------------------------- 212 213 # --- HELPER FUNCTION FOR EXECUTION AND VALIDATION --- 214 def attempt_download(opts: dict) -> str: 215 with yt_dlp.YoutubeDL(opts) as ydl: 216 info = ydl.extract_info(url, download=True) 217 filepath = ydl.prepare_filename(info) 218 219 # Check if output was merged into an mp4 container 220 base, _ = os.path.splitext(filepath) 221 if os.path.exists(f"{base}.mp4"): 222 filepath = f"{base}.mp4" 223 224 # CRITICAL: verify the file actually exists AND has real content. 225 # A silent merge failure can leave a 0-byte or missing file that would 226 # otherwise look "successful" to the caller. 227 if not os.path.exists(filepath): 228 raise RuntimeError(f"Download appeared to succeed but no file was found at {filepath}") 229 file_size = os.path.getsize(filepath) 230 if file_size == 0: 231 raise RuntimeError(f"Downloaded file is empty (0 bytes): {filepath}") 232 233 print(f"✅ Video saved to: {filepath} ({file_size / 1024 / 1024:.1f} MB)") 234 return os.path.abspath(filepath) 235 236 # === ATTEMPT 1: IPv6 Workaround === 237 print(f"📥 [Attempt 1] Downloading video from {url} via IPv6...") 238 ydl_opts_ipv6 = ydl_opts.copy() 239 ydl_opts_ipv6['source_address'] = '0::0' 240 241 try: 242 return attempt_download(ydl_opts_ipv6) 243 except Exception as e_ipv6: 244 print(f"⚠️ IPv6 Attempt Failed/Blocked: {str(e_ipv6)}") 245 246 # === ATTEMPT 2: Rotating Webshare Proxy Fallback === 247 proxy_env = os.getenv("RESIDENTIAL_PROXY") 248 if proxy_env: 249 # Parse the comma-separated string of 10 proxies you added to .env 250 proxy_list = [p.strip() for p in proxy_env.split(",") if p.strip()] 251 chosen_proxy = random.choice(proxy_list) 252 253 # Print masked proxy IP for terminal debugging (hides your username/password) 254 masked_ip = chosen_proxy.split('@')[-1] if '@' in chosen_proxy else chosen_proxy 255 print(f"🛡️ [Attempt 2] Routing download through random Webshare proxy ({masked_ip})...") 256 257 ydl_opts_proxy = ydl_opts.copy() 258 ydl_opts_proxy['proxy'] = chosen_proxy 259 260 try: 261 return attempt_download(ydl_opts_proxy) 262 except Exception as e_proxy: 263 print(f"❌ Proxy Attempt Failed: {str(e_proxy)}") 264 raise RuntimeError(f"Both IPv6 and Proxy downloads failed. Last error: {str(e_proxy)}") 265 else: 266 print("❌ No RESIDENTIAL_PROXY found in environment variables.") 267 raise RuntimeError(f"IPv6 download failed ({str(e_ipv6)}), and no Webshare Proxy was configured.") 268 269 270import subprocess 271def convert_video_to_animated(input_path: str, output_path: str, target_format: str = "gif", fps: int = 15, width: int = 512) -> None: 272 """Converts a video to an animated GIF or WebP sticker using FFmpeg.""" 273 if not shutil.which("ffmpeg"): 274 raise RuntimeError("FFmpeg is required for video-to-animation conversion. Please install it.") 275 276 # WhatsApp/Telegram stickers prefer a 512px boundary. We scale proportionally. 277 scale_filter = f"fps={fps},scale={width}:-1:flags=lanczos" 278 279 if target_format.lower() == "gif": 280 # High-quality GIF generation using a 2-pass color palette 281 vf_cmd = f"{scale_filter},split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" 282 cmd = ["ffmpeg", "-y", "-i", input_path, "-vf", vf_cmd, "-loop", "0", output_path] 283 elif target_format.lower() == "webp": 284 # Animated WebP optimized for social media stickers 285 cmd = [ 286 "ffmpeg", "-y", "-i", input_path, 287 "-vcodec", "libwebp", 288 "-vf", scale_filter, 289 "-lossless", "0", 290 "-compression_level", "4", 291 "-q:v", "50", 292 "-loop", "0", 293 "-preset", "picture", 294 "-an", "-vsync", "0", 295 output_path 296 ] 297 else: 298 raise ValueError("Target format must be 'gif' or 'webp'") 299 300 # Execute FFmpeg silently 301 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 302 if result.returncode != 0: 303 raise RuntimeError(f"FFmpeg conversion failed: {result.stderr}")
16def get_video_info(path: str) -> dict: 17 """Return basic metadata about a video file: duration (seconds), width, height, and frame rate.""" 18 import av 19 container = av.open(path) 20 stream = container.streams.video[0] 21 duration = float(container.duration / av.time_base) if container.duration else None 22 info = { 23 "duration_seconds": duration, 24 "width": stream.width, 25 "height": stream.height, 26 "fps": float(stream.average_rate) if stream.average_rate else None, 27 } 28 container.close() 29 return info
Return basic metadata about a video file: duration (seconds), width, height, and frame rate.
32def get_video_duration(path: str) -> float: 33 """Return the duration of a video file in seconds.""" 34 return get_video_info(path)["duration_seconds"]
Return the duration of a video file in seconds.
37def extract_audio_from_video(video_path: str, output_audio_path: str) -> None: 38 """Extract the audio track from a video file and save it as a standalone audio file 39 (format is inferred from output_audio_path's extension, e.g. .mp3, .wav, .m4a).""" 40 import av 41 42 input_container = av.open(video_path) 43 audio_stream = input_container.streams.audio[0] 44 45 output_container = av.open(output_audio_path, mode="w") 46 output_stream = output_container.add_stream("aac" if output_audio_path.endswith((".m4a", ".mp4")) else "mp3") 47 48 for frame in input_container.decode(audio_stream): 49 for packet in output_stream.encode(frame): 50 output_container.mux(packet) 51 52 for packet in output_stream.encode(None): 53 output_container.mux(packet) 54 55 output_container.close() 56 input_container.close()
Extract the audio track from a video file and save it as a standalone audio file (format is inferred from output_audio_path's extension, e.g. .mp3, .wav, .m4a).
59def extract_video_frame(video_path: str, timestamp_seconds: float, output_image_path: str) -> None: 60 """Extract a single frame from a video at the given timestamp (seconds) and save it as an image.""" 61 import av 62 63 container = av.open(video_path) 64 stream = container.streams.video[0] 65 66 target_pts = int(timestamp_seconds / stream.time_base) 67 container.seek(target_pts, stream=stream) 68 69 for frame in container.decode(stream): 70 if frame.time >= timestamp_seconds: 71 frame.to_image().save(output_image_path) 72 break 73 74 container.close()
Extract a single frame from a video at the given timestamp (seconds) and save it as an image.
77def transcribe_video(path: str, model_size: str = "base", language: str | None = None) -> str: 78 """Transcribe the spoken audio in a video file to plain text. Works directly on 79 video containers (MP4, MOV, MKV, etc.) — the audio track is extracted automatically.""" 80 from quicktools.audiotools import transcribe_audio 81 return transcribe_audio(path, model_size=model_size, language=language)
Transcribe the spoken audio in a video file to plain text. Works directly on video containers (MP4, MOV, MKV, etc.) — the audio track is extracted automatically.
84def transcribe_video_with_timestamps(path: str, model_size: str = "base") -> list[dict]: 85 """Transcribe a video's audio into timestamped segments, each with 'start', 'end', and 'text'.""" 86 from quicktools.audiotools import transcribe_audio_with_timestamps 87 return transcribe_audio_with_timestamps(path, model_size=model_size)
Transcribe a video's audio into timestamped segments, each with 'start', 'end', and 'text'.
90def transcribe_video_word_level(path: str, model_size: str = "base") -> list[dict]: 91 """Transcribe a video's audio into word-by-word timestamps, each with 'word', 'start', and 'end'. 92 Useful for generating captions synced precisely to speech.""" 93 from quicktools.audiotools import transcribe_audio_word_level 94 return transcribe_audio_word_level(path, model_size=model_size)
Transcribe a video's audio into word-by-word timestamps, each with 'word', 'start', and 'end'. Useful for generating captions synced precisely to speech.
101def transcribe_video_with_speakers(path_or_url: str, hf_token: str, model_size: str = "base", device: str = "auto") -> list[dict]: 102 """ 103 Extracts audio from a local video file or web URL (YouTube, TikTok, IG, X), 104 transcribes it, and maps the text to specific speakers. 105 """ 106 print(f"Processing video source: {path_or_url}") 107 return audiotools.transcribe_with_speakers(path_or_url, hf_token, model_size, device)
Extracts audio from a local video file or web URL (YouTube, TikTok, IG, X), transcribes it, and maps the text to specific speakers.
110def save_video_script_to_docx(transcript_data: list[dict], output_path: str, title_text: str = "Video Script & Transcript") -> None: 111 """ 112 Formats speaker-mapped video transcript data as a professional script in Word (.docx). 113 """ 114 try: 115 from docx import Document 116 from docx.shared import Pt, RGBColor 117 except ImportError: 118 raise ImportError("Saving to Word requires python-docx. Run: pip install python-docx") 119 120 doc = Document() 121 title = doc.add_heading(title_text, level=1) 122 title.alignment = 1 # Center align 123 124 last_speaker = None 125 126 for entry in transcript_data: 127 p = doc.add_paragraph() 128 129 # Format timestamp as [MM:SS] 130 start_m, start_s = divmod(int(entry["start"]), 60) 131 time_str = f"[{start_m:02d}:{start_s:02d}]" 132 133 # Speaker header line if speaker changed 134 if entry["speaker"] != last_speaker: 135 speaker_run = p.add_run(f"{time_str} {entry['speaker']}:\n") 136 speaker_run.bold = True 137 speaker_run.font.color.rgb = RGBColor(112, 48, 160) # Distinct Purple highlight for video speakers 138 last_speaker = entry["speaker"] 139 140 p.add_run(entry["text"]) 141 p.paragraph_format.space_after = Pt(8) 142 143 doc.save(output_path)
Formats speaker-mapped video transcript data as a professional script in Word (.docx).
150def download_video(url: str, output_dir: str = ".", filename: str | None = None, resolution: str = "best", cookiefile: str | None = None) -> str: 151 """ 152 Downloads a video file from supported web URLs (YouTube, TikTok, Instagram, X/Twitter, etc.). 153 154 :param url: The web video URL to download. 155 :param output_dir: Destination folder (defaults to current directory). 156 :param filename: Optional custom filename (without extension). Defaults to video title. 157 :param resolution: Quality target ('best' or 'worst'). 158 :param cookiefile: Path to a cookies.txt file to bypass login restrictions. 159 :return: Absolute file path of the downloaded video. 160 """ 161 try: 162 import yt_dlp 163 except ImportError: 164 raise ImportError( 165 "Downloading videos requires 'yt-dlp'. Install it with: pip install yt-dlp" 166 ) 167 168 os.makedirs(output_dir, exist_ok=True) 169 out_template = f"{filename}.%(ext)s" if filename else "%(title)s.%(ext)s" 170 target_path = os.path.join(output_dir, out_template) 171 172 # --- AUTO-DETECT SERVER COOKIES --- 173 if not cookiefile and os.path.exists('/app/cookies.txt'): 174 cookiefile = '/app/cookies.txt' 175 # ---------------------------------- 176 177 # --- THE SMART FFMPEG FALLBACK --- 178 has_ffmpeg = shutil.which("ffmpeg") is not None 179 180 if resolution == "best": 181 if has_ffmpeg: 182 # Grab max quality separate streams and merge them 183 fmt = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' 184 else: 185 # Fall back to the best PRE-MERGED format (usually 720p max) to prevent a crash 186 print("⚠️ [quicktools] FFmpeg not found. Falling back to pre-merged 720p format.") 187 print("💡 Tip: Install FFmpeg ('winget install ffmpeg') to enable 1080p+ downloads.") 188 fmt = 'best[ext=mp4]/best' 189 else: 190 fmt = 'worst' 191 # --------------------------------- 192 193 ydl_opts = { 194 'format': fmt, 195 'outtmpl': target_path, 196 'quiet': False, 197 'no_warnings': True, 198 'merge_output_format': 'mp4' if has_ffmpeg else None, 199 'extractor_args': {'youtube': ['player_client=android']}, 200 } 201 202 # --- CRITICAL APPLE iOS FIXES --- 203 if has_ffmpeg: 204 # Force FFmpeg to move the moov atom to the top of the file (Fast Start) 205 ydl_opts['postprocessor_args'] = ['-movflags', '+faststart'] 206 # -------------------------------- 207 208 # --- ATTACH COOKIE IF AVAILABLE --- 209 if cookiefile and os.path.exists(cookiefile): 210 print(f"🔑 Using cookie authentication file: {cookiefile}") 211 ydl_opts['cookiefile'] = cookiefile 212 # ---------------------------------- 213 214 # --- HELPER FUNCTION FOR EXECUTION AND VALIDATION --- 215 def attempt_download(opts: dict) -> str: 216 with yt_dlp.YoutubeDL(opts) as ydl: 217 info = ydl.extract_info(url, download=True) 218 filepath = ydl.prepare_filename(info) 219 220 # Check if output was merged into an mp4 container 221 base, _ = os.path.splitext(filepath) 222 if os.path.exists(f"{base}.mp4"): 223 filepath = f"{base}.mp4" 224 225 # CRITICAL: verify the file actually exists AND has real content. 226 # A silent merge failure can leave a 0-byte or missing file that would 227 # otherwise look "successful" to the caller. 228 if not os.path.exists(filepath): 229 raise RuntimeError(f"Download appeared to succeed but no file was found at {filepath}") 230 file_size = os.path.getsize(filepath) 231 if file_size == 0: 232 raise RuntimeError(f"Downloaded file is empty (0 bytes): {filepath}") 233 234 print(f"✅ Video saved to: {filepath} ({file_size / 1024 / 1024:.1f} MB)") 235 return os.path.abspath(filepath) 236 237 # === ATTEMPT 1: IPv6 Workaround === 238 print(f"📥 [Attempt 1] Downloading video from {url} via IPv6...") 239 ydl_opts_ipv6 = ydl_opts.copy() 240 ydl_opts_ipv6['source_address'] = '0::0' 241 242 try: 243 return attempt_download(ydl_opts_ipv6) 244 except Exception as e_ipv6: 245 print(f"⚠️ IPv6 Attempt Failed/Blocked: {str(e_ipv6)}") 246 247 # === ATTEMPT 2: Rotating Webshare Proxy Fallback === 248 proxy_env = os.getenv("RESIDENTIAL_PROXY") 249 if proxy_env: 250 # Parse the comma-separated string of 10 proxies you added to .env 251 proxy_list = [p.strip() for p in proxy_env.split(",") if p.strip()] 252 chosen_proxy = random.choice(proxy_list) 253 254 # Print masked proxy IP for terminal debugging (hides your username/password) 255 masked_ip = chosen_proxy.split('@')[-1] if '@' in chosen_proxy else chosen_proxy 256 print(f"🛡️ [Attempt 2] Routing download through random Webshare proxy ({masked_ip})...") 257 258 ydl_opts_proxy = ydl_opts.copy() 259 ydl_opts_proxy['proxy'] = chosen_proxy 260 261 try: 262 return attempt_download(ydl_opts_proxy) 263 except Exception as e_proxy: 264 print(f"❌ Proxy Attempt Failed: {str(e_proxy)}") 265 raise RuntimeError(f"Both IPv6 and Proxy downloads failed. Last error: {str(e_proxy)}") 266 else: 267 print("❌ No RESIDENTIAL_PROXY found in environment variables.") 268 raise RuntimeError(f"IPv6 download failed ({str(e_ipv6)}), and no Webshare Proxy was configured.")
Downloads a video file from supported web URLs (YouTube, TikTok, Instagram, X/Twitter, etc.).
:param url: The web video URL to download. :param output_dir: Destination folder (defaults to current directory). :param filename: Optional custom filename (without extension). Defaults to video title. :param resolution: Quality target ('best' or 'worst'). :param cookiefile: Path to a cookies.txt file to bypass login restrictions. :return: Absolute file path of the downloaded video.
272def convert_video_to_animated(input_path: str, output_path: str, target_format: str = "gif", fps: int = 15, width: int = 512) -> None: 273 """Converts a video to an animated GIF or WebP sticker using FFmpeg.""" 274 if not shutil.which("ffmpeg"): 275 raise RuntimeError("FFmpeg is required for video-to-animation conversion. Please install it.") 276 277 # WhatsApp/Telegram stickers prefer a 512px boundary. We scale proportionally. 278 scale_filter = f"fps={fps},scale={width}:-1:flags=lanczos" 279 280 if target_format.lower() == "gif": 281 # High-quality GIF generation using a 2-pass color palette 282 vf_cmd = f"{scale_filter},split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" 283 cmd = ["ffmpeg", "-y", "-i", input_path, "-vf", vf_cmd, "-loop", "0", output_path] 284 elif target_format.lower() == "webp": 285 # Animated WebP optimized for social media stickers 286 cmd = [ 287 "ffmpeg", "-y", "-i", input_path, 288 "-vcodec", "libwebp", 289 "-vf", scale_filter, 290 "-lossless", "0", 291 "-compression_level", "4", 292 "-q:v", "50", 293 "-loop", "0", 294 "-preset", "picture", 295 "-an", "-vsync", "0", 296 output_path 297 ] 298 else: 299 raise ValueError("Target format must be 'gif' or 'webp'") 300 301 # Execute FFmpeg silently 302 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 303 if result.returncode != 0: 304 raise RuntimeError(f"FFmpeg conversion failed: {result.stderr}")
Converts a video to an animated GIF or WebP sticker using FFmpeg.