Buckets:
| import argparse, os, subprocess, sys, glob, random, shutil | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('--images-dir', required=True) | |
| parser.add_argument('--audio', default='') | |
| parser.add_argument('--output', required=True) | |
| parser.add_argument('--duration', type=float, default=3.0) | |
| parser.add_argument('--fps', type=int, default=24) | |
| args = parser.parse_args() | |
| image_exts = ('*.png', '*.jpg', '*.jpeg', '*.webp') | |
| images = [] | |
| for ext in image_exts: | |
| images.extend(sorted(glob.glob(os.path.join(args.images_dir, '**', ext), recursive=True))) | |
| if not images: | |
| print("ERROR: No images found"); sys.exit(1) | |
| print(f" Images: {len(images)} files") | |
| ffmpeg = "ffmpeg.exe" if os.name == 'nt' else "ffmpeg" | |
| out_dir = os.path.dirname(args.output) | |
| os.makedirs(out_dir, exist_ok=True) | |
| temp_dir = os.path.join(out_dir, '_segments') | |
| os.makedirs(temp_dir, exist_ok=True) | |
| frames = int(args.duration * args.fps) | |
| concat_path = os.path.join(temp_dir, 'list.txt') | |
| with open(concat_path, 'w') as f: | |
| for i, img in enumerate(images): | |
| seg_out = os.path.join(temp_dir, f"s{i:03d}.mp4") | |
| rng = random.Random(hash(img) % 10000) | |
| zoom = rng.uniform(1.02, 1.06) | |
| dx = rng.uniform(-30, 30) | |
| dy = rng.uniform(-30, 30) | |
| vf = ( | |
| f"scale=1920:1080:force_original_aspect_ratio=decrease," | |
| f"pad=1920:1080:(ow-iw)/2:(oh-ih)/2," | |
| f"zoompan=z='if(eq(on,1),1,min(zoom+({zoom}-1)/{frames},{zoom}))':" | |
| f"d={frames}:x='iw/2-(iw/zoom/2)+{dx}*(on/{frames})':" | |
| f"y='ih/2-(ih/zoom/2)+{dy}*(on/{frames})':s=1920x1080" | |
| ) | |
| subprocess.run( | |
| [ffmpeg, '-y', '-loop', '1', '-i', img, | |
| '-vf', vf, '-c:v', 'libx264', '-preset', 'fast', | |
| '-crf', '18', '-t', str(args.duration), '-an', seg_out], | |
| capture_output=True | |
| ) | |
| f.write(f"file '{seg_out}'\n") | |
| cmd = [ffmpeg, '-y', '-f', 'concat', '-safe', '0', '-i', concat_path, | |
| '-c:v', 'libx264', '-preset', 'slow', '-crf', '18', | |
| '-pix_fmt', 'yuv420p', '-movflags', '+faststart'] | |
| if args.audio and os.path.exists(args.audio): | |
| cmd.extend(['-i', args.audio, '-c:a', 'aac', '-b:a', '192k', '-shortest']) | |
| else: | |
| cmd.extend(['-an']) | |
| cmd.append(args.output) | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| if result.returncode == 0: | |
| size = os.path.getsize(args.output) | |
| print(f" -> Video: {args.output} ({size/1024/1024:.1f} MB)") | |
| else: | |
| print(f" ERROR: {result.stderr[-300:]}") | |
| sys.exit(1) | |
Xet Storage Details
- Size:
- 2.57 kB
- Xet hash:
- 7c6cf0ed90e39d4b29eaa51509284dbfa74ec489f370595440af340d8dbce4ac
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.