Initial commit
This commit is contained in:
358
skills/media-processing/SKILL.md
Normal file
358
skills/media-processing/SKILL.md
Normal file
@@ -0,0 +1,358 @@
|
||||
---
|
||||
name: media-processing
|
||||
description: Process multimedia files with FFmpeg (video/audio encoding, conversion, streaming, filtering, hardware acceleration) and ImageMagick (image manipulation, format conversion, batch processing, effects, composition). Use when converting media formats, encoding videos with specific codecs (H.264, H.265, VP9), resizing/cropping images, extracting audio from video, applying filters and effects, optimizing file sizes, creating streaming manifests (HLS/DASH), generating thumbnails, batch processing images, creating composite images, or implementing media processing pipelines. Supports 100+ formats, hardware acceleration (NVENC, QSV), and complex filtergraphs.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Media Processing Skill
|
||||
|
||||
Process video, audio, and images using FFmpeg and ImageMagick command-line tools for conversion, optimization, streaming, and manipulation tasks.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use when:
|
||||
- Converting media formats (video, audio, images)
|
||||
- Encoding video with codecs (H.264, H.265, VP9, AV1)
|
||||
- Processing images (resize, crop, effects, watermarks)
|
||||
- Extracting audio from video
|
||||
- Creating streaming manifests (HLS/DASH)
|
||||
- Generating thumbnails and previews
|
||||
- Batch processing media files
|
||||
- Optimizing file sizes and quality
|
||||
- Applying filters and effects
|
||||
- Creating composite images or videos
|
||||
|
||||
## Tool Selection Guide
|
||||
|
||||
### FFmpeg: Video/Audio Processing
|
||||
Use FFmpeg for:
|
||||
- Video encoding, conversion, transcoding
|
||||
- Audio extraction, conversion, mixing
|
||||
- Live streaming (RTMP, HLS, DASH)
|
||||
- Video filters (scale, crop, rotate, overlay)
|
||||
- Hardware-accelerated encoding
|
||||
- Media file inspection (ffprobe)
|
||||
- Frame extraction, concatenation
|
||||
- Codec selection and optimization
|
||||
|
||||
### ImageMagick: Image Processing
|
||||
Use ImageMagick for:
|
||||
- Image format conversion (PNG, JPEG, WebP, GIF)
|
||||
- Resizing, cropping, transformations
|
||||
- Batch image processing (mogrify)
|
||||
- Visual effects (blur, sharpen, sepia)
|
||||
- Text overlays and watermarks
|
||||
- Image composition and montages
|
||||
- Color adjustments, filters
|
||||
- Thumbnail generation
|
||||
|
||||
### Decision Matrix
|
||||
|
||||
| Task | Tool | Why |
|
||||
|------|------|-----|
|
||||
| Video encoding | FFmpeg | Native video codec support |
|
||||
| Audio extraction | FFmpeg | Direct stream manipulation |
|
||||
| Image resize | ImageMagick | Optimized for still images |
|
||||
| Batch images | ImageMagick | mogrify for in-place edits |
|
||||
| Video thumbnails | FFmpeg | Frame extraction built-in |
|
||||
| GIF creation | FFmpeg or ImageMagick | FFmpeg for video source, ImageMagick for images |
|
||||
| Streaming | FFmpeg | Live streaming protocols |
|
||||
| Image effects | ImageMagick | Rich filter library |
|
||||
|
||||
## Installation
|
||||
|
||||
### macOS
|
||||
```bash
|
||||
brew install ffmpeg imagemagick
|
||||
```
|
||||
|
||||
### Ubuntu/Debian
|
||||
```bash
|
||||
sudo apt-get install ffmpeg imagemagick
|
||||
```
|
||||
|
||||
### Windows
|
||||
```bash
|
||||
# Using winget
|
||||
winget install ffmpeg
|
||||
winget install ImageMagick.ImageMagick
|
||||
|
||||
# Or download binaries
|
||||
# FFmpeg: https://ffmpeg.org/download.html
|
||||
# ImageMagick: https://imagemagick.org/script/download.php
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
```bash
|
||||
ffmpeg -version
|
||||
ffprobe -version
|
||||
magick -version
|
||||
# or
|
||||
convert -version
|
||||
```
|
||||
|
||||
## Quick Start Examples
|
||||
|
||||
### Video Conversion
|
||||
```bash
|
||||
# Convert format (copy streams, fast)
|
||||
ffmpeg -i input.mkv -c copy output.mp4
|
||||
|
||||
# Re-encode with H.264
|
||||
ffmpeg -i input.avi -c:v libx264 -crf 22 -c:a aac output.mp4
|
||||
|
||||
# Resize video to 720p
|
||||
ffmpeg -i input.mp4 -vf scale=-1:720 -c:a copy output.mp4
|
||||
```
|
||||
|
||||
### Audio Extraction
|
||||
```bash
|
||||
# Extract audio (no re-encoding)
|
||||
ffmpeg -i video.mp4 -vn -c:a copy audio.m4a
|
||||
|
||||
# Convert to MP3
|
||||
ffmpeg -i video.mp4 -vn -q:a 0 audio.mp3
|
||||
```
|
||||
|
||||
### Image Processing
|
||||
```bash
|
||||
# Convert format
|
||||
magick input.png output.jpg
|
||||
|
||||
# Resize maintaining aspect ratio
|
||||
magick input.jpg -resize 800x600 output.jpg
|
||||
|
||||
# Create square thumbnail
|
||||
magick input.jpg -resize 200x200^ -gravity center -extent 200x200 thumb.jpg
|
||||
```
|
||||
|
||||
### Batch Image Resize
|
||||
```bash
|
||||
# Resize all JPEGs to 800px width
|
||||
mogrify -resize 800x -quality 85 *.jpg
|
||||
|
||||
# Output to separate directory
|
||||
mogrify -path ./output -resize 800x600 *.jpg
|
||||
```
|
||||
|
||||
### Video Thumbnail
|
||||
```bash
|
||||
# Extract frame at 5 seconds
|
||||
ffmpeg -ss 00:00:05 -i video.mp4 -vframes 1 -vf scale=320:-1 thumb.jpg
|
||||
```
|
||||
|
||||
### HLS Streaming
|
||||
```bash
|
||||
# Generate HLS playlist
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -preset fast -crf 22 -g 48 \
|
||||
-c:a aac -b:a 128k \
|
||||
-f hls -hls_time 6 -hls_playlist_type vod \
|
||||
playlist.m3u8
|
||||
```
|
||||
|
||||
### Image Watermark
|
||||
```bash
|
||||
# Add watermark to corner
|
||||
magick input.jpg watermark.png -gravity southeast \
|
||||
-geometry +10+10 -composite output.jpg
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Optimize Video for Web
|
||||
```bash
|
||||
# H.264 with good compression
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -preset slow -crf 23 \
|
||||
-c:a aac -b:a 128k \
|
||||
-movflags +faststart \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
### Create Responsive Images
|
||||
```bash
|
||||
# Generate multiple sizes
|
||||
for size in 320 640 1024 1920; do
|
||||
magick input.jpg -resize ${size}x -quality 85 "output-${size}w.jpg"
|
||||
done
|
||||
```
|
||||
|
||||
### Extract Video Segment
|
||||
```bash
|
||||
# From 1:30 to 3:00 (re-encode for precision)
|
||||
ffmpeg -i input.mp4 -ss 00:01:30 -to 00:03:00 \
|
||||
-c:v libx264 -c:a aac output.mp4
|
||||
```
|
||||
|
||||
### Batch Image Optimization
|
||||
```bash
|
||||
# Convert PNG to optimized JPEG
|
||||
mogrify -path ./optimized -format jpg -quality 85 -strip *.png
|
||||
```
|
||||
|
||||
### Video GIF Creation
|
||||
```bash
|
||||
# High quality GIF with palette
|
||||
ffmpeg -i input.mp4 -vf "fps=15,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" output.gif
|
||||
```
|
||||
|
||||
### Image Blur Effect
|
||||
```bash
|
||||
# Gaussian blur
|
||||
magick input.jpg -gaussian-blur 0x8 output.jpg
|
||||
```
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Multi-Pass Video Encoding
|
||||
```bash
|
||||
# Pass 1 (analysis)
|
||||
ffmpeg -y -i input.mkv -c:v libx264 -b:v 2600k -pass 1 -an -f null /dev/null
|
||||
|
||||
# Pass 2 (encoding)
|
||||
ffmpeg -i input.mkv -c:v libx264 -b:v 2600k -pass 2 -c:a aac output.mp4
|
||||
```
|
||||
|
||||
### Hardware-Accelerated Encoding
|
||||
```bash
|
||||
# NVIDIA NVENC
|
||||
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset fast -crf 22 output.mp4
|
||||
|
||||
# Intel QuickSync
|
||||
ffmpeg -hwaccel qsv -c:v h264_qsv -i input.mp4 -c:v h264_qsv output.mp4
|
||||
```
|
||||
|
||||
### Complex Image Pipeline
|
||||
```bash
|
||||
# Resize, crop, border, adjust
|
||||
magick input.jpg \
|
||||
-resize 1000x1000^ \
|
||||
-gravity center \
|
||||
-crop 1000x1000+0+0 +repage \
|
||||
-bordercolor black -border 5x5 \
|
||||
-brightness-contrast 5x10 \
|
||||
-quality 90 \
|
||||
output.jpg
|
||||
```
|
||||
|
||||
### Video Filter Chains
|
||||
```bash
|
||||
# Scale, denoise, watermark
|
||||
ffmpeg -i video.mp4 -i logo.png \
|
||||
-filter_complex "[0:v]scale=1280:720,hqdn3d[v];[v][1:v]overlay=10:10" \
|
||||
-c:a copy output.mp4
|
||||
```
|
||||
|
||||
### Animated GIF from Images
|
||||
```bash
|
||||
# Create with delay
|
||||
magick -delay 100 -loop 0 frame*.png animated.gif
|
||||
|
||||
# Optimize size
|
||||
magick animated.gif -fuzz 5% -layers Optimize optimized.gif
|
||||
```
|
||||
|
||||
## Media Analysis
|
||||
|
||||
### Inspect Video Properties
|
||||
```bash
|
||||
# Detailed JSON output
|
||||
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4
|
||||
|
||||
# Get resolution
|
||||
ffprobe -v error -select_streams v:0 \
|
||||
-show_entries stream=width,height \
|
||||
-of csv=s=x:p=0 input.mp4
|
||||
```
|
||||
|
||||
### Image Information
|
||||
```bash
|
||||
# Basic info
|
||||
identify image.jpg
|
||||
|
||||
# Detailed format
|
||||
identify -verbose image.jpg
|
||||
|
||||
# Custom format
|
||||
identify -format "%f: %wx%h %b\n" image.jpg
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use CRF for quality control** - Better than bitrate for video
|
||||
2. **Copy streams when possible** - Avoid re-encoding with `-c copy`
|
||||
3. **Hardware acceleration** - GPU encoding 5-10x faster
|
||||
4. **Appropriate presets** - Balance speed vs compression
|
||||
5. **Batch with mogrify** - In-place image processing
|
||||
6. **Strip metadata** - Reduce file size with `-strip`
|
||||
7. **Progressive JPEG** - Better web loading with `-interlace Plane`
|
||||
8. **Limit memory** - Prevent crashes on large batches
|
||||
9. **Test on samples** - Verify settings before batch
|
||||
10. **Parallel processing** - Use GNU Parallel for multiple files
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
Detailed guides in `references/`:
|
||||
|
||||
- **ffmpeg-encoding.md** - Video/audio codecs, quality optimization, hardware acceleration
|
||||
- **ffmpeg-streaming.md** - HLS/DASH, live streaming, adaptive bitrate
|
||||
- **ffmpeg-filters.md** - Video/audio filters, complex filtergraphs
|
||||
- **imagemagick-editing.md** - Format conversion, effects, transformations
|
||||
- **imagemagick-batch.md** - Batch processing, mogrify, parallel operations
|
||||
- **format-compatibility.md** - Format support, codec recommendations
|
||||
|
||||
## Common Parameters
|
||||
|
||||
### FFmpeg Video
|
||||
- `-c:v` - Video codec (libx264, libx265, libvpx-vp9)
|
||||
- `-crf` - Quality (0-51, lower=better, 23=default)
|
||||
- `-preset` - Speed/compression (ultrafast to veryslow)
|
||||
- `-b:v` - Video bitrate (e.g., 2M, 2500k)
|
||||
- `-vf` - Video filters
|
||||
|
||||
### FFmpeg Audio
|
||||
- `-c:a` - Audio codec (aac, mp3, opus)
|
||||
- `-b:a` - Audio bitrate (e.g., 128k, 192k)
|
||||
- `-ar` - Sample rate (44100, 48000)
|
||||
|
||||
### ImageMagick Geometry
|
||||
- `800x600` - Fit within (maintains aspect)
|
||||
- `800x600!` - Force exact size
|
||||
- `800x600^` - Fill (may crop)
|
||||
- `800x` - Width only
|
||||
- `x600` - Height only
|
||||
- `50%` - Scale percentage
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**FFmpeg "Unknown encoder"**
|
||||
```bash
|
||||
# Check available encoders
|
||||
ffmpeg -encoders | grep h264
|
||||
|
||||
# Install codec libraries
|
||||
sudo apt-get install libx264-dev libx265-dev
|
||||
```
|
||||
|
||||
**ImageMagick "not authorized"**
|
||||
```bash
|
||||
# Edit policy file
|
||||
sudo nano /etc/ImageMagick-7/policy.xml
|
||||
# Change <policy domain="coder" rights="none" pattern="PDF" />
|
||||
# to <policy domain="coder" rights="read|write" pattern="PDF" />
|
||||
```
|
||||
|
||||
**Memory errors**
|
||||
```bash
|
||||
# Limit memory usage
|
||||
ffmpeg -threads 4 input.mp4 output.mp4
|
||||
magick -limit memory 2GB -limit map 4GB input.jpg output.jpg
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- FFmpeg: https://ffmpeg.org/documentation.html
|
||||
- FFmpeg Wiki: https://trac.ffmpeg.org/
|
||||
- ImageMagick: https://imagemagick.org/
|
||||
- ImageMagick Usage: https://imagemagick.org/Usage/
|
||||
358
skills/media-processing/references/ffmpeg-encoding.md
Normal file
358
skills/media-processing/references/ffmpeg-encoding.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# FFmpeg Video & Audio Encoding
|
||||
|
||||
Complete guide to codec selection, quality optimization, and hardware acceleration.
|
||||
|
||||
## Video Codecs
|
||||
|
||||
### H.264 (libx264)
|
||||
Most widely supported codec, excellent compression/quality balance.
|
||||
|
||||
**Best for:** Universal compatibility, streaming, web video
|
||||
|
||||
**Quality range:** CRF 17-28 (lower = better)
|
||||
|
||||
```bash
|
||||
# High quality
|
||||
ffmpeg -i input.mkv -c:v libx264 -preset slow -crf 18 -c:a copy output.mp4
|
||||
|
||||
# Standard quality (recommended)
|
||||
ffmpeg -i input.mkv -c:v libx264 -preset medium -crf 23 -c:a copy output.mp4
|
||||
|
||||
# Fast encoding
|
||||
ffmpeg -i input.mkv -c:v libx264 -preset fast -crf 23 -c:a copy output.mp4
|
||||
```
|
||||
|
||||
### H.265/HEVC (libx265)
|
||||
25-50% better compression than H.264, slower encoding.
|
||||
|
||||
**Best for:** 4K video, file size reduction, archival
|
||||
|
||||
```bash
|
||||
# High quality 4K
|
||||
ffmpeg -i input.mkv -c:v libx265 -preset medium -crf 24 -c:a copy output.mp4
|
||||
|
||||
# Balanced quality
|
||||
ffmpeg -i input.mkv -c:v libx265 -preset fast -crf 26 -c:a copy output.mp4
|
||||
```
|
||||
|
||||
### VP9 (libvpx-vp9)
|
||||
Royalty-free, WebM format, good for YouTube and open-source projects.
|
||||
|
||||
**Best for:** YouTube, Chrome/Firefox, open platforms
|
||||
|
||||
```bash
|
||||
# Quality-based (recommended)
|
||||
ffmpeg -i input.mkv -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus output.webm
|
||||
|
||||
# Two-pass for better quality
|
||||
ffmpeg -i input.mkv -c:v libvpx-vp9 -b:v 2M -pass 1 -an -f null /dev/null
|
||||
ffmpeg -i input.mkv -c:v libvpx-vp9 -b:v 2M -pass 2 -c:a libopus output.webm
|
||||
```
|
||||
|
||||
### AV1 (libaom-av1, libsvtav1)
|
||||
Next-generation codec, best compression, very slow encoding.
|
||||
|
||||
**Best for:** Future-proofing, maximum compression, low bandwidth
|
||||
|
||||
```bash
|
||||
# Using libaom (slow, highest quality)
|
||||
ffmpeg -i input.mkv -c:v libaom-av1 -crf 30 -b:v 0 -strict experimental output.mp4
|
||||
|
||||
# Using SVT-AV1 (faster)
|
||||
ffmpeg -i input.mkv -c:v libsvtav1 -crf 30 -preset 5 output.mp4
|
||||
```
|
||||
|
||||
## Audio Codecs
|
||||
|
||||
### AAC (Industry Standard)
|
||||
Best quality for streaming, universal support.
|
||||
|
||||
```bash
|
||||
# High quality
|
||||
ffmpeg -i input.mp4 -c:a aac -b:a 192k output.mp4
|
||||
|
||||
# Standard quality
|
||||
ffmpeg -i input.mp4 -c:a aac -b:a 128k output.mp4
|
||||
|
||||
# Low bitrate
|
||||
ffmpeg -i input.mp4 -c:a aac -b:a 96k output.mp4
|
||||
```
|
||||
|
||||
### MP3 (libmp3lame)
|
||||
Universal compatibility, good quality.
|
||||
|
||||
```bash
|
||||
# Variable bitrate (best quality)
|
||||
ffmpeg -i input.wav -c:a libmp3lame -q:a 0 output.mp3
|
||||
|
||||
# Constant bitrate
|
||||
ffmpeg -i input.wav -c:a libmp3lame -b:a 192k output.mp3
|
||||
```
|
||||
|
||||
### Opus (libopus)
|
||||
Best quality at low bitrates, ideal for voice and streaming.
|
||||
|
||||
```bash
|
||||
# Voice (mono)
|
||||
ffmpeg -i input.mp4 -c:a libopus -b:a 32k -ac 1 output.webm
|
||||
|
||||
# Music (stereo)
|
||||
ffmpeg -i input.mp4 -c:a libopus -b:a 128k output.webm
|
||||
```
|
||||
|
||||
### FLAC (Lossless)
|
||||
No quality loss, archival quality, larger files.
|
||||
|
||||
```bash
|
||||
# Lossless audio
|
||||
ffmpeg -i input.wav -c:a flac output.flac
|
||||
|
||||
# Extract audio losslessly
|
||||
ffmpeg -i video.mp4 -c:a flac audio.flac
|
||||
```
|
||||
|
||||
## Quality Optimization
|
||||
|
||||
### CRF (Constant Rate Factor)
|
||||
Best for quality-focused encoding. Single-pass, adjusts bitrate for complexity.
|
||||
|
||||
**CRF Scale:**
|
||||
- 0 = Lossless (huge files)
|
||||
- 17-18 = Visually lossless
|
||||
- 20-23 = High quality (recommended)
|
||||
- 24-28 = Medium quality
|
||||
- 30+ = Low quality
|
||||
- 51 = Worst quality
|
||||
|
||||
```bash
|
||||
# Visually lossless
|
||||
ffmpeg -i input.mp4 -c:v libx264 -crf 18 -preset slow output.mp4
|
||||
|
||||
# High quality (recommended)
|
||||
ffmpeg -i input.mp4 -c:v libx264 -crf 22 -preset medium output.mp4
|
||||
|
||||
# Balanced quality/size
|
||||
ffmpeg -i input.mp4 -c:v libx264 -crf 25 -preset fast output.mp4
|
||||
```
|
||||
|
||||
### Bitrate-Based Encoding
|
||||
Target specific file size or quality. Two-pass recommended.
|
||||
|
||||
```bash
|
||||
# Calculate target bitrate
|
||||
# bitrate = (target_size_MB * 8192) / duration_seconds - audio_bitrate
|
||||
|
||||
# Two-pass encoding (2600k video, 128k audio)
|
||||
ffmpeg -y -i input.mkv -c:v libx264 -b:v 2600k -pass 1 -an -f null /dev/null
|
||||
ffmpeg -i input.mkv -c:v libx264 -b:v 2600k -pass 2 -c:a aac -b:a 128k output.mp4
|
||||
```
|
||||
|
||||
### Presets (Speed vs Compression)
|
||||
Trade-off between encoding speed and file size.
|
||||
|
||||
**Available presets:**
|
||||
- `ultrafast` - Fastest, largest files
|
||||
- `superfast`
|
||||
- `veryfast`
|
||||
- `faster`
|
||||
- `fast`
|
||||
- `medium` - Default balance
|
||||
- `slow` - Better compression
|
||||
- `slower`
|
||||
- `veryslow` - Best compression
|
||||
- `placebo` - Not recommended (minimal gains)
|
||||
|
||||
```bash
|
||||
# Fast encoding (real-time)
|
||||
ffmpeg -i input.mp4 -c:v libx264 -preset ultrafast -crf 23 output.mp4
|
||||
|
||||
# Balanced
|
||||
ffmpeg -i input.mp4 -c:v libx264 -preset medium -crf 22 output.mp4
|
||||
|
||||
# Best compression (slow)
|
||||
ffmpeg -i input.mp4 -c:v libx264 -preset veryslow -crf 20 output.mp4
|
||||
```
|
||||
|
||||
## Hardware Acceleration
|
||||
|
||||
### NVIDIA NVENC
|
||||
5-10x faster encoding, slightly larger files than software encoding.
|
||||
|
||||
**Requirements:** NVIDIA GPU (GTX 10xx or newer)
|
||||
|
||||
```bash
|
||||
# H.264 with NVENC
|
||||
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset fast -crf 22 output.mp4
|
||||
|
||||
# H.265 with NVENC
|
||||
ffmpeg -hwaccel cuda -i input.mp4 -c:v hevc_nvenc -preset slow -crf 24 output.mp4
|
||||
|
||||
# Quality levels (instead of CRF)
|
||||
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset slow -rc vbr -cq 22 output.mp4
|
||||
```
|
||||
|
||||
**NVENC Presets:**
|
||||
- `default` - Balanced
|
||||
- `slow` - Better quality
|
||||
- `medium`
|
||||
- `fast`
|
||||
- `hp` - High performance
|
||||
- `hq` - High quality
|
||||
- `bd` - Bluray disk
|
||||
- `ll` - Low latency
|
||||
- `llhq` - Low latency high quality
|
||||
- `llhp` - Low latency high performance
|
||||
|
||||
### Intel QuickSync (QSV)
|
||||
Fast hardware encoding on Intel CPUs with integrated graphics.
|
||||
|
||||
**Requirements:** Intel CPU with Quick Sync Video support
|
||||
|
||||
```bash
|
||||
# H.264 with QSV
|
||||
ffmpeg -hwaccel qsv -c:v h264_qsv -i input.mp4 \
|
||||
-c:v h264_qsv -preset fast -global_quality 22 output.mp4
|
||||
|
||||
# H.265 with QSV
|
||||
ffmpeg -hwaccel qsv -c:v hevc_qsv -i input.mp4 \
|
||||
-c:v hevc_qsv -preset medium -global_quality 24 output.mp4
|
||||
|
||||
# Quality levels
|
||||
ffmpeg -hwaccel qsv -i input.mp4 -c:v h264_qsv -global_quality 20 output.mp4
|
||||
```
|
||||
|
||||
### AMD VCE/VCN
|
||||
Hardware encoding on AMD GPUs.
|
||||
|
||||
**Requirements:** AMD GPU with VCE/VCN support
|
||||
|
||||
```bash
|
||||
# H.264 with AMF
|
||||
ffmpeg -hwaccel auto -i input.mp4 \
|
||||
-c:v h264_amf -quality balanced -rc cqp -qp 22 output.mp4
|
||||
|
||||
# H.265 with AMF
|
||||
ffmpeg -hwaccel auto -i input.mp4 \
|
||||
-c:v hevc_amf -quality quality -rc cqp -qp 24 output.mp4
|
||||
```
|
||||
|
||||
### Apple VideoToolbox (macOS)
|
||||
Hardware encoding on macOS devices.
|
||||
|
||||
```bash
|
||||
# H.264 with VideoToolbox
|
||||
ffmpeg -i input.mp4 -c:v h264_videotoolbox -b:v 2M output.mp4
|
||||
|
||||
# H.265 with VideoToolbox
|
||||
ffmpeg -i input.mp4 -c:v hevc_videotoolbox -b:v 1.5M output.mp4
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Multi-Threading
|
||||
FFmpeg automatically uses multiple cores. Override if needed:
|
||||
|
||||
```bash
|
||||
# Limit threads
|
||||
ffmpeg -threads 4 -i input.mp4 -c:v libx264 output.mp4
|
||||
|
||||
# Auto (default)
|
||||
ffmpeg -threads 0 -i input.mp4 -c:v libx264 output.mp4
|
||||
```
|
||||
|
||||
### Tune Options
|
||||
Optimize encoder for specific content types:
|
||||
|
||||
```bash
|
||||
# Film content
|
||||
ffmpeg -i input.mp4 -c:v libx264 -tune film -crf 22 output.mp4
|
||||
|
||||
# Animation
|
||||
ffmpeg -i input.mp4 -c:v libx264 -tune animation -crf 22 output.mp4
|
||||
|
||||
# Grain (film with noise)
|
||||
ffmpeg -i input.mp4 -c:v libx264 -tune grain -crf 22 output.mp4
|
||||
|
||||
# Low latency streaming
|
||||
ffmpeg -i input.mp4 -c:v libx264 -tune zerolatency -crf 22 output.mp4
|
||||
|
||||
# Screen content (sharp edges)
|
||||
ffmpeg -i input.mp4 -c:v libx264 -tune stillimage -crf 22 output.mp4
|
||||
```
|
||||
|
||||
## Codec Selection Guide
|
||||
|
||||
### Use Cases
|
||||
|
||||
| Use Case | Codec | Settings |
|
||||
|----------|-------|----------|
|
||||
| Web video | H.264 | CRF 23, preset medium |
|
||||
| 4K streaming | H.265 | CRF 24, preset fast |
|
||||
| YouTube upload | VP9 or H.264 | CRF 23 |
|
||||
| Archive | H.265 or H.264 | CRF 18, preset slow |
|
||||
| Low bandwidth | AV1 or H.265 | CRF 30 |
|
||||
| Fast encoding | H.264 NVENC | preset fast |
|
||||
| Maximum compatibility | H.264 | profile main, level 4.0 |
|
||||
|
||||
### Platform Compatibility
|
||||
|
||||
| Platform | Recommended | Supported |
|
||||
|----------|------------|-----------|
|
||||
| Web browsers | H.264 | H.264, VP9, AV1 |
|
||||
| Mobile devices | H.264 | H.264, H.265 |
|
||||
| Smart TVs | H.264 | H.264, H.265 |
|
||||
| YouTube | VP9, H.264 | All |
|
||||
| Social media | H.264 | H.264 |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use CRF for most tasks** - Better than bitrate for variable content
|
||||
2. **Start with CRF 23** - Good balance, adjust based on results
|
||||
3. **Use slow preset** - For archival and final delivery
|
||||
4. **Use fast preset** - For previews and testing
|
||||
5. **Hardware acceleration** - When speed is critical
|
||||
6. **Two-pass encoding** - When file size is fixed
|
||||
7. **Match source frame rate** - Don't increase FPS
|
||||
8. **Don't upscale resolution** - Keep original or downscale
|
||||
9. **Test on short clips** - Verify settings before full encode
|
||||
10. **Keep source files** - Original quality for re-encoding
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Poor Quality Output
|
||||
```bash
|
||||
# Lower CRF value
|
||||
ffmpeg -i input.mp4 -c:v libx264 -crf 18 -preset slow output.mp4
|
||||
|
||||
# Use slower preset
|
||||
ffmpeg -i input.mp4 -c:v libx264 -crf 22 -preset veryslow output.mp4
|
||||
|
||||
# Increase bitrate (two-pass)
|
||||
ffmpeg -y -i input.mp4 -c:v libx264 -b:v 5M -pass 1 -an -f null /dev/null
|
||||
ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -pass 2 -c:a aac output.mp4
|
||||
```
|
||||
|
||||
### Slow Encoding
|
||||
```bash
|
||||
# Use faster preset
|
||||
ffmpeg -i input.mp4 -c:v libx264 -preset ultrafast output.mp4
|
||||
|
||||
# Use hardware acceleration
|
||||
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc output.mp4
|
||||
|
||||
# Reduce resolution
|
||||
ffmpeg -i input.mp4 -vf scale=1280:-1 -c:v libx264 output.mp4
|
||||
```
|
||||
|
||||
### Large File Size
|
||||
```bash
|
||||
# Increase CRF
|
||||
ffmpeg -i input.mp4 -c:v libx264 -crf 26 output.mp4
|
||||
|
||||
# Use better codec
|
||||
ffmpeg -i input.mp4 -c:v libx265 -crf 26 output.mp4
|
||||
|
||||
# Two-pass with target bitrate
|
||||
ffmpeg -y -i input.mp4 -c:v libx264 -b:v 1M -pass 1 -an -f null /dev/null
|
||||
ffmpeg -i input.mp4 -c:v libx264 -b:v 1M -pass 2 -c:a aac output.mp4
|
||||
```
|
||||
503
skills/media-processing/references/ffmpeg-filters.md
Normal file
503
skills/media-processing/references/ffmpeg-filters.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# FFmpeg Filters & Effects
|
||||
|
||||
Complete guide to video and audio filters, complex filtergraphs, and effect chains.
|
||||
|
||||
## Filter Basics
|
||||
|
||||
### Filter Syntax
|
||||
Filters are applied with `-vf` (video) or `-af` (audio).
|
||||
|
||||
```bash
|
||||
# Single filter
|
||||
ffmpeg -i input.mp4 -vf scale=1280:720 output.mp4
|
||||
|
||||
# Chain filters with comma
|
||||
ffmpeg -i input.mp4 -vf "scale=1280:720,hqdn3d" output.mp4
|
||||
|
||||
# Complex filtergraph with -filter_complex
|
||||
ffmpeg -i input.mp4 -i logo.png \
|
||||
-filter_complex "[0:v][1:v]overlay=10:10" \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
## Video Filters
|
||||
|
||||
### Scale (Resize)
|
||||
Change video dimensions.
|
||||
|
||||
```bash
|
||||
# Specific dimensions
|
||||
ffmpeg -i input.mp4 -vf scale=1280:720 output.mp4
|
||||
|
||||
# Maintain aspect ratio (auto height)
|
||||
ffmpeg -i input.mp4 -vf scale=1280:-1 output.mp4
|
||||
|
||||
# Maintain aspect ratio (auto width)
|
||||
ffmpeg -i input.mp4 -vf scale=-1:720 output.mp4
|
||||
|
||||
# Scale to half
|
||||
ffmpeg -i input.mp4 -vf scale=iw/2:ih/2 output.mp4
|
||||
|
||||
# Scale with algorithm
|
||||
ffmpeg -i input.mp4 -vf scale=1280:-1:flags=lanczos output.mp4
|
||||
```
|
||||
|
||||
**Scaling algorithms:**
|
||||
- `bilinear` - Fast, default
|
||||
- `bicubic` - Better quality
|
||||
- `lanczos` - Best quality, slower
|
||||
|
||||
### Crop
|
||||
Extract portion of video.
|
||||
|
||||
```bash
|
||||
# Crop width:height:x:y
|
||||
ffmpeg -i input.mp4 -vf crop=1280:720:0:0 output.mp4
|
||||
|
||||
# Crop from center
|
||||
ffmpeg -i input.mp4 -vf crop=1280:720:(iw-1280)/2:(ih-720)/2 output.mp4
|
||||
|
||||
# Auto-detect black borders
|
||||
ffmpeg -i input.mp4 -vf cropdetect -f null -
|
||||
|
||||
# Apply detected crop
|
||||
ffmpeg -i input.mp4 -vf crop=1920:800:0:140 output.mp4
|
||||
```
|
||||
|
||||
### Rotate & Flip
|
||||
Change video orientation.
|
||||
|
||||
```bash
|
||||
# Rotate 90° clockwise
|
||||
ffmpeg -i input.mp4 -vf transpose=1 output.mp4
|
||||
|
||||
# Rotate 90° counter-clockwise
|
||||
ffmpeg -i input.mp4 -vf transpose=2 output.mp4
|
||||
|
||||
# Rotate 180°
|
||||
ffmpeg -i input.mp4 -vf transpose=1,transpose=1 output.mp4
|
||||
|
||||
# Flip horizontal
|
||||
ffmpeg -i input.mp4 -vf hflip output.mp4
|
||||
|
||||
# Flip vertical
|
||||
ffmpeg -i input.mp4 -vf vflip output.mp4
|
||||
|
||||
# Rotate arbitrary angle
|
||||
ffmpeg -i input.mp4 -vf rotate=45*PI/180 output.mp4
|
||||
```
|
||||
|
||||
### Overlay (Watermark)
|
||||
Composite images over video.
|
||||
|
||||
```bash
|
||||
# Top-left corner
|
||||
ffmpeg -i video.mp4 -i logo.png \
|
||||
-filter_complex overlay=10:10 output.mp4
|
||||
|
||||
# Top-right corner
|
||||
ffmpeg -i video.mp4 -i logo.png \
|
||||
-filter_complex "overlay=W-w-10:10" output.mp4
|
||||
|
||||
# Bottom-right corner
|
||||
ffmpeg -i video.mp4 -i logo.png \
|
||||
-filter_complex "overlay=W-w-10:H-h-10" output.mp4
|
||||
|
||||
# Center
|
||||
ffmpeg -i video.mp4 -i logo.png \
|
||||
-filter_complex "overlay=(W-w)/2:(H-h)/2" output.mp4
|
||||
|
||||
# With transparency
|
||||
ffmpeg -i video.mp4 -i logo.png \
|
||||
-filter_complex "[1:v]format=rgba,colorchannelmixer=aa=0.5[logo];[0:v][logo]overlay=10:10" \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
### Denoise
|
||||
Reduce video noise.
|
||||
|
||||
```bash
|
||||
# High-quality denoise (hqdn3d)
|
||||
ffmpeg -i input.mp4 -vf hqdn3d output.mp4
|
||||
|
||||
# Stronger denoise
|
||||
ffmpeg -i input.mp4 -vf hqdn3d=4:3:6:4.5 output.mp4
|
||||
|
||||
# Temporal denoise (nlmeans - slow but best)
|
||||
ffmpeg -i input.mp4 -vf nlmeans output.mp4
|
||||
|
||||
# Fast denoise
|
||||
ffmpeg -i input.mp4 -vf dctdnoiz output.mp4
|
||||
```
|
||||
|
||||
### Deinterlace
|
||||
Remove interlacing artifacts.
|
||||
|
||||
```bash
|
||||
# YADIF (fast, good quality)
|
||||
ffmpeg -i input.mp4 -vf yadif output.mp4
|
||||
|
||||
# YADIF with frame doubling
|
||||
ffmpeg -i input.mp4 -vf yadif=1 output.mp4
|
||||
|
||||
# Bwdif (better quality)
|
||||
ffmpeg -i input.mp4 -vf bwdif output.mp4
|
||||
```
|
||||
|
||||
### Speed & Slow Motion
|
||||
Change playback speed.
|
||||
|
||||
```bash
|
||||
# 2x speed (video + audio)
|
||||
ffmpeg -i input.mp4 -vf setpts=0.5*PTS -af atempo=2.0 output.mp4
|
||||
|
||||
# 0.5x speed (slow motion)
|
||||
ffmpeg -i input.mp4 -vf setpts=2.0*PTS -af atempo=0.5 output.mp4
|
||||
|
||||
# 4x speed (chain atempo)
|
||||
ffmpeg -i input.mp4 -vf setpts=0.25*PTS -af atempo=2.0,atempo=2.0 output.mp4
|
||||
```
|
||||
|
||||
### Pad (Add Borders)
|
||||
Add borders or letterbox.
|
||||
|
||||
```bash
|
||||
# Add black borders to make 16:9
|
||||
ffmpeg -i input.mp4 -vf "pad=1920:1080:(ow-iw)/2:(oh-ih)/2" output.mp4
|
||||
|
||||
# Add colored borders
|
||||
ffmpeg -i input.mp4 -vf "pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=white" output.mp4
|
||||
|
||||
# Letterbox for Instagram (1:1)
|
||||
ffmpeg -i input.mp4 -vf "scale=1080:-1,pad=1080:1080:(ow-iw)/2:(oh-ih)/2:color=black" output.mp4
|
||||
```
|
||||
|
||||
### Sharpen & Blur
|
||||
Adjust image sharpness.
|
||||
|
||||
```bash
|
||||
# Sharpen (unsharp mask)
|
||||
ffmpeg -i input.mp4 -vf unsharp=5:5:1.0 output.mp4
|
||||
|
||||
# Stronger sharpen
|
||||
ffmpeg -i input.mp4 -vf unsharp=7:7:2.5 output.mp4
|
||||
|
||||
# Gaussian blur
|
||||
ffmpeg -i input.mp4 -vf gblur=sigma=8 output.mp4
|
||||
|
||||
# Box blur
|
||||
ffmpeg -i input.mp4 -vf boxblur=5:1 output.mp4
|
||||
```
|
||||
|
||||
### Color Adjustments
|
||||
Modify colors and exposure.
|
||||
|
||||
```bash
|
||||
# Brightness (+/- 1.0)
|
||||
ffmpeg -i input.mp4 -vf eq=brightness=0.1 output.mp4
|
||||
|
||||
# Contrast (+/- 2.0)
|
||||
ffmpeg -i input.mp4 -vf eq=contrast=1.2 output.mp4
|
||||
|
||||
# Saturation (0-3)
|
||||
ffmpeg -i input.mp4 -vf eq=saturation=1.5 output.mp4
|
||||
|
||||
# Gamma (0.1-10)
|
||||
ffmpeg -i input.mp4 -vf eq=gamma=1.2 output.mp4
|
||||
|
||||
# Combined adjustments
|
||||
ffmpeg -i input.mp4 -vf eq=brightness=0.05:contrast=1.1:saturation=1.2 output.mp4
|
||||
|
||||
# Curves (color grading)
|
||||
ffmpeg -i input.mp4 -vf curves=vintage output.mp4
|
||||
|
||||
# Hue shift
|
||||
ffmpeg -i input.mp4 -vf hue=h=90 output.mp4
|
||||
```
|
||||
|
||||
### Grayscale & Effects
|
||||
Convert to monochrome or apply effects.
|
||||
|
||||
```bash
|
||||
# Grayscale
|
||||
ffmpeg -i input.mp4 -vf hue=s=0 output.mp4
|
||||
|
||||
# Sepia tone
|
||||
ffmpeg -i input.mp4 -vf colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131 output.mp4
|
||||
|
||||
# Negative
|
||||
ffmpeg -i input.mp4 -vf negate output.mp4
|
||||
|
||||
# Edge detection
|
||||
ffmpeg -i input.mp4 -vf edgedetect output.mp4
|
||||
|
||||
# Vignette
|
||||
ffmpeg -i input.mp4 -vf vignette output.mp4
|
||||
```
|
||||
|
||||
### Fade In/Out
|
||||
Smooth transitions.
|
||||
|
||||
```bash
|
||||
# Fade in from black (2 seconds)
|
||||
ffmpeg -i input.mp4 -vf fade=in:0:60 output.mp4
|
||||
|
||||
# Fade out to black (last 2 seconds)
|
||||
ffmpeg -i input.mp4 -vf fade=out:st=28:d=2 output.mp4
|
||||
|
||||
# Both fade in and out
|
||||
ffmpeg -i input.mp4 -vf "fade=in:0:30,fade=out:st=28:d=2" output.mp4
|
||||
```
|
||||
|
||||
### Stabilization
|
||||
Reduce camera shake.
|
||||
|
||||
```bash
|
||||
# Two-pass stabilization
|
||||
# Pass 1: detect motion
|
||||
ffmpeg -i input.mp4 -vf vidstabdetect=shakiness=10:accuracy=15 -f null -
|
||||
|
||||
# Pass 2: stabilize
|
||||
ffmpeg -i input.mp4 -vf vidstabtransform=smoothing=30:input="transforms.trf" output.mp4
|
||||
```
|
||||
|
||||
### Text Overlay
|
||||
Add text to video.
|
||||
|
||||
```bash
|
||||
# Simple text
|
||||
ffmpeg -i input.mp4 -vf "drawtext=text='Hello World':fontsize=24:x=10:y=10" output.mp4
|
||||
|
||||
# With styling
|
||||
ffmpeg -i input.mp4 -vf "drawtext=text='Title':fontsize=48:fontcolor=white:x=(w-text_w)/2:y=50:box=1:boxcolor=black@0.5:boxborderw=5" output.mp4
|
||||
|
||||
# Timestamp
|
||||
ffmpeg -i input.mp4 -vf "drawtext=text='%{pts\:hms}':fontsize=20:x=10:y=10:fontcolor=white" output.mp4
|
||||
```
|
||||
|
||||
## Audio Filters
|
||||
|
||||
### Volume
|
||||
Adjust audio level.
|
||||
|
||||
```bash
|
||||
# Increase by 10dB
|
||||
ffmpeg -i input.mp4 -af volume=10dB output.mp4
|
||||
|
||||
# Decrease to 50%
|
||||
ffmpeg -i input.mp4 -af volume=0.5 output.mp4
|
||||
|
||||
# Double volume
|
||||
ffmpeg -i input.mp4 -af volume=2.0 output.mp4
|
||||
```
|
||||
|
||||
### Normalize
|
||||
Balance audio levels.
|
||||
|
||||
```bash
|
||||
# Loudness normalization (EBU R128)
|
||||
ffmpeg -i input.mp4 -af loudnorm output.mp4
|
||||
|
||||
# With specific target
|
||||
ffmpeg -i input.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11 output.mp4
|
||||
|
||||
# Two-pass normalization (better quality)
|
||||
# Pass 1: analyze
|
||||
ffmpeg -i input.mp4 -af loudnorm=print_format=json -f null -
|
||||
|
||||
# Pass 2: normalize with measured values
|
||||
ffmpeg -i input.mp4 -af loudnorm=measured_I=-23:measured_LRA=7:measured_TP=-2:measured_thresh=-33 output.mp4
|
||||
```
|
||||
|
||||
### Equalizer
|
||||
Adjust frequency bands.
|
||||
|
||||
```bash
|
||||
# Bass boost
|
||||
ffmpeg -i input.mp4 -af equalizer=f=100:width_type=h:width=200:g=10 output.mp4
|
||||
|
||||
# Treble boost
|
||||
ffmpeg -i input.mp4 -af equalizer=f=10000:width_type=h:width=2000:g=5 output.mp4
|
||||
|
||||
# Multiple bands
|
||||
ffmpeg -i input.mp4 -af "equalizer=f=100:g=5,equalizer=f=1000:g=-3" output.mp4
|
||||
```
|
||||
|
||||
### Compressor
|
||||
Dynamic range compression.
|
||||
|
||||
```bash
|
||||
# Basic compression
|
||||
ffmpeg -i input.mp4 -af acompressor output.mp4
|
||||
|
||||
# Custom settings
|
||||
ffmpeg -i input.mp4 -af acompressor=threshold=-20dB:ratio=4:attack=200:release=1000 output.mp4
|
||||
```
|
||||
|
||||
### Noise Reduction
|
||||
Remove background noise.
|
||||
|
||||
```bash
|
||||
# High-pass filter (remove low frequency noise)
|
||||
ffmpeg -i input.mp4 -af highpass=f=200 output.mp4
|
||||
|
||||
# Low-pass filter (remove high frequency noise)
|
||||
ffmpeg -i input.mp4 -af lowpass=f=3000 output.mp4
|
||||
|
||||
# Band-pass filter
|
||||
ffmpeg -i input.mp4 -af "highpass=f=200,lowpass=f=3000" output.mp4
|
||||
```
|
||||
|
||||
### Fade Audio
|
||||
Smooth audio transitions.
|
||||
|
||||
```bash
|
||||
# Fade in (2 seconds)
|
||||
ffmpeg -i input.mp4 -af afade=t=in:st=0:d=2 output.mp4
|
||||
|
||||
# Fade out (last 3 seconds)
|
||||
ffmpeg -i input.mp4 -af afade=t=out:st=27:d=3 output.mp4
|
||||
|
||||
# Both
|
||||
ffmpeg -i input.mp4 -af "afade=t=in:st=0:d=2,afade=t=out:st=27:d=3" output.mp4
|
||||
```
|
||||
|
||||
### Audio Mixing
|
||||
Combine multiple audio tracks.
|
||||
|
||||
```bash
|
||||
# Mix two audio files
|
||||
ffmpeg -i audio1.mp3 -i audio2.mp3 \
|
||||
-filter_complex amix=inputs=2:duration=longest output.mp3
|
||||
|
||||
# Mix with volume adjustment
|
||||
ffmpeg -i audio1.mp3 -i audio2.mp3 \
|
||||
-filter_complex "[0:a]volume=0.8[a1];[1:a]volume=0.5[a2];[a1][a2]amix=inputs=2" \
|
||||
output.mp3
|
||||
```
|
||||
|
||||
## Complex Filtergraphs
|
||||
|
||||
### Multiple Outputs
|
||||
Create multiple versions simultaneously.
|
||||
|
||||
```bash
|
||||
# Generate 3 resolutions at once
|
||||
ffmpeg -i input.mp4 \
|
||||
-filter_complex "[0:v]split=3[v1][v2][v3]; \
|
||||
[v1]scale=1920:1080[out1]; \
|
||||
[v2]scale=1280:720[out2]; \
|
||||
[v3]scale=640:360[out3]" \
|
||||
-map "[out1]" -c:v libx264 -crf 22 output_1080p.mp4 \
|
||||
-map "[out2]" -c:v libx264 -crf 23 output_720p.mp4 \
|
||||
-map "[out3]" -c:v libx264 -crf 24 output_360p.mp4 \
|
||||
-map 0:a -c:a copy
|
||||
```
|
||||
|
||||
### Picture-in-Picture
|
||||
Overlay small video on main video.
|
||||
|
||||
```bash
|
||||
ffmpeg -i main.mp4 -i small.mp4 \
|
||||
-filter_complex "[1:v]scale=320:180[pip]; \
|
||||
[0:v][pip]overlay=W-w-10:H-h-10" \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
### Side-by-Side Comparison
|
||||
Compare two videos.
|
||||
|
||||
```bash
|
||||
# Horizontal
|
||||
ffmpeg -i left.mp4 -i right.mp4 \
|
||||
-filter_complex "[0:v][1:v]hstack=inputs=2" \
|
||||
output.mp4
|
||||
|
||||
# Vertical
|
||||
ffmpeg -i top.mp4 -i bottom.mp4 \
|
||||
-filter_complex "[0:v][1:v]vstack=inputs=2" \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
### Crossfade Transition
|
||||
Smooth transition between videos.
|
||||
|
||||
```bash
|
||||
ffmpeg -i video1.mp4 -i video2.mp4 \
|
||||
-filter_complex "[0:v][1:v]xfade=transition=fade:duration=2:offset=8" \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
**Transition types:** fade, wipeleft, wiperight, wipeup, wipedown, slideleft, slideright, slideup, slidedown, circlecrop, rectcrop, distance, fadeblack, fadewhite, radial, smoothleft, smoothright, smoothup, smoothdown
|
||||
|
||||
### Color Correction Pipeline
|
||||
Professional color grading.
|
||||
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-filter_complex "[0:v]eq=contrast=1.1:brightness=0.05:saturation=1.2[v1]; \
|
||||
[v1]curves=vintage[v2]; \
|
||||
[v2]vignette[v3]; \
|
||||
[v3]unsharp=5:5:1.0[out]" \
|
||||
-map "[out]" -c:v libx264 -crf 18 output.mp4
|
||||
```
|
||||
|
||||
## Filter Performance
|
||||
|
||||
### GPU Acceleration
|
||||
Use hardware filters when available.
|
||||
|
||||
```bash
|
||||
# NVIDIA CUDA scale
|
||||
ffmpeg -hwaccel cuda -i input.mp4 \
|
||||
-vf scale_cuda=1280:720 \
|
||||
-c:v h264_nvenc output.mp4
|
||||
|
||||
# Multiple GPU filters
|
||||
ffmpeg -hwaccel cuda -i input.mp4 \
|
||||
-vf "scale_cuda=1280:720,hwdownload,format=nv12" \
|
||||
-c:v h264_nvenc output.mp4
|
||||
```
|
||||
|
||||
### Optimize Filter Order
|
||||
More efficient filter chains.
|
||||
|
||||
```bash
|
||||
# Bad: scale after complex operations
|
||||
ffmpeg -i input.mp4 -vf "hqdn3d,unsharp=5:5:1.0,scale=1280:720" output.mp4
|
||||
|
||||
# Good: scale first (fewer pixels to process)
|
||||
ffmpeg -i input.mp4 -vf "scale=1280:720,hqdn3d,unsharp=5:5:1.0" output.mp4
|
||||
```
|
||||
|
||||
## Common Filter Recipes
|
||||
|
||||
### YouTube Optimized
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" \
|
||||
-c:v libx264 -preset slow -crf 18 -c:a aac -b:a 192k \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
### Instagram Portrait
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-vf "scale=1080:1350:force_original_aspect_ratio=decrease,pad=1080:1350:(ow-iw)/2:(oh-ih)/2:color=white" \
|
||||
-c:v libx264 -preset fast -crf 23 -c:a aac \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
### Vintage Film Look
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-vf "curves=vintage,vignette=angle=PI/4,eq=saturation=0.8,noise=alls=10:allf=t" \
|
||||
-c:v libx264 -crf 20 output.mp4
|
||||
```
|
||||
|
||||
### Clean & Enhance
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-vf "hqdn3d=4:3:6:4.5,unsharp=5:5:1.0,eq=contrast=1.05:saturation=1.1" \
|
||||
-c:v libx264 -crf 20 output.mp4
|
||||
```
|
||||
403
skills/media-processing/references/ffmpeg-streaming.md
Normal file
403
skills/media-processing/references/ffmpeg-streaming.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# FFmpeg Streaming & Live Video
|
||||
|
||||
Complete guide to HLS/DASH streaming, live streaming platforms, and adaptive bitrate encoding.
|
||||
|
||||
## HLS (HTTP Live Streaming)
|
||||
|
||||
### Basic HLS Stream
|
||||
Generate playlist for on-demand streaming.
|
||||
|
||||
```bash
|
||||
# Simple HLS with default settings
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -c:a aac \
|
||||
-f hls -hls_time 6 -hls_playlist_type vod \
|
||||
-hls_segment_filename "segment_%03d.ts" \
|
||||
playlist.m3u8
|
||||
```
|
||||
|
||||
**Key parameters:**
|
||||
- `-hls_time` - Segment duration (seconds, default 2)
|
||||
- `-hls_playlist_type` - `vod` (on-demand) or `event` (live)
|
||||
- `-hls_segment_filename` - Naming pattern for segments
|
||||
|
||||
### Optimized HLS
|
||||
Better quality and compatibility.
|
||||
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -preset fast -crf 22 \
|
||||
-g 48 -sc_threshold 0 \
|
||||
-c:a aac -b:a 128k \
|
||||
-f hls -hls_time 6 -hls_playlist_type vod \
|
||||
-hls_segment_filename "segment_%03d.ts" \
|
||||
playlist.m3u8
|
||||
```
|
||||
|
||||
**Parameters explained:**
|
||||
- `-g 48` - Keyframe every 48 frames (2s @ 24fps)
|
||||
- `-sc_threshold 0` - Disable scene detection (consistent segments)
|
||||
|
||||
### Multi-Bitrate HLS (Adaptive)
|
||||
Create multiple quality levels for adaptive streaming.
|
||||
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-map 0:v -map 0:a -map 0:v -map 0:a -map 0:v -map 0:a \
|
||||
-c:v libx264 -crf 22 -c:a aac -b:a 128k \
|
||||
-b:v:0 800k -s:v:0 640x360 -maxrate:v:0 856k -bufsize:v:0 1200k \
|
||||
-b:v:1 1400k -s:v:1 842x480 -maxrate:v:1 1498k -bufsize:v:1 2100k \
|
||||
-b:v:2 2800k -s:v:2 1280x720 -maxrate:v:2 2996k -bufsize:v:2 4200k \
|
||||
-var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
|
||||
-master_pl_name master.m3u8 \
|
||||
-f hls -hls_time 6 -hls_list_size 0 \
|
||||
-hls_segment_filename "stream_%v/segment_%03d.ts" \
|
||||
stream_%v/playlist.m3u8
|
||||
```
|
||||
|
||||
**Creates:**
|
||||
- `master.m3u8` - Master playlist (entry point)
|
||||
- `stream_0/playlist.m3u8` - 360p stream
|
||||
- `stream_1/playlist.m3u8` - 480p stream
|
||||
- `stream_2/playlist.m3u8` - 720p stream
|
||||
|
||||
### HLS with Encryption
|
||||
Protect content with AES-128 encryption.
|
||||
|
||||
```bash
|
||||
# Generate encryption key
|
||||
openssl rand 16 > enc.key
|
||||
echo "enc.key" > enc.keyinfo
|
||||
echo "enc.key" >> enc.keyinfo
|
||||
openssl rand -hex 16 >> enc.keyinfo
|
||||
|
||||
# Encode with encryption
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -c:a aac \
|
||||
-f hls -hls_time 6 \
|
||||
-hls_key_info_file enc.keyinfo \
|
||||
-hls_segment_filename "segment_%03d.ts" \
|
||||
playlist.m3u8
|
||||
```
|
||||
|
||||
## DASH (Dynamic Adaptive Streaming)
|
||||
|
||||
### Basic DASH
|
||||
MPEG-DASH format for adaptive streaming.
|
||||
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -c:a aac \
|
||||
-f dash -seg_duration 6 \
|
||||
-use_template 1 -use_timeline 1 \
|
||||
manifest.mpd
|
||||
```
|
||||
|
||||
### Multi-Bitrate DASH
|
||||
Multiple quality levels.
|
||||
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-map 0:v -map 0:a -map 0:v -map 0:a \
|
||||
-c:v libx264 -c:a aac \
|
||||
-b:v:0 800k -s:v:0 640x360 \
|
||||
-b:v:1 1400k -s:v:1 1280x720 \
|
||||
-b:a:0 128k -b:a:1 128k \
|
||||
-f dash -seg_duration 6 \
|
||||
-use_template 1 -use_timeline 1 \
|
||||
manifest.mpd
|
||||
```
|
||||
|
||||
## RTMP Live Streaming
|
||||
|
||||
### Stream to Twitch
|
||||
```bash
|
||||
ffmpeg -re -i input.mp4 \
|
||||
-c:v libx264 -preset veryfast -maxrate 3000k -bufsize 6000k \
|
||||
-pix_fmt yuv420p -g 50 -c:a aac -b:a 128k -ar 44100 \
|
||||
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
|
||||
```
|
||||
|
||||
### Stream to YouTube
|
||||
```bash
|
||||
ffmpeg -re -i input.mp4 \
|
||||
-c:v libx264 -preset veryfast -maxrate 2500k -bufsize 5000k \
|
||||
-pix_fmt yuv420p -g 60 -c:a aac -b:a 128k \
|
||||
-f flv rtmp://a.rtmp.youtube.com/live2/STREAM_KEY
|
||||
```
|
||||
|
||||
### Stream to Facebook
|
||||
```bash
|
||||
ffmpeg -re -i input.mp4 \
|
||||
-c:v libx264 -preset veryfast -maxrate 4000k -bufsize 8000k \
|
||||
-pix_fmt yuv420p -g 60 -c:a aac -b:a 128k \
|
||||
-f flv rtmps://live-api-s.facebook.com:443/rtmp/STREAM_KEY
|
||||
```
|
||||
|
||||
### Custom RTMP Server
|
||||
```bash
|
||||
ffmpeg -re -i input.mp4 \
|
||||
-c:v libx264 -preset veryfast -tune zerolatency \
|
||||
-maxrate 2500k -bufsize 5000k \
|
||||
-pix_fmt yuv420p -g 50 \
|
||||
-c:a aac -b:a 128k -ar 44100 \
|
||||
-f flv rtmp://your-server.com/live/stream-key
|
||||
```
|
||||
|
||||
## Screen Capture + Stream
|
||||
|
||||
### Linux (X11)
|
||||
```bash
|
||||
ffmpeg -f x11grab -s 1920x1080 -framerate 30 -i :0.0 \
|
||||
-f pulse -ac 2 -i default \
|
||||
-c:v libx264 -preset veryfast -tune zerolatency \
|
||||
-maxrate 2500k -bufsize 5000k -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 128k -ar 44100 \
|
||||
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
|
||||
```
|
||||
|
||||
### macOS (AVFoundation)
|
||||
```bash
|
||||
# List devices
|
||||
ffmpeg -f avfoundation -list_devices true -i ""
|
||||
|
||||
# Capture and stream
|
||||
ffmpeg -f avfoundation -framerate 30 -i "1:0" \
|
||||
-c:v libx264 -preset veryfast -tune zerolatency \
|
||||
-maxrate 2500k -bufsize 5000k -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 128k \
|
||||
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
|
||||
```
|
||||
|
||||
### Windows (DirectShow)
|
||||
```bash
|
||||
ffmpeg -f dshow -i video="screen-capture-recorder":audio="Stereo Mix" \
|
||||
-c:v libx264 -preset ultrafast -tune zerolatency \
|
||||
-maxrate 750k -bufsize 3000k \
|
||||
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
|
||||
```
|
||||
|
||||
## Thumbnail Generation
|
||||
|
||||
### Single Thumbnail
|
||||
Extract frame at specific time.
|
||||
|
||||
```bash
|
||||
# At 5 seconds
|
||||
ffmpeg -ss 00:00:05 -i input.mp4 -vframes 1 -vf scale=320:-1 thumb.jpg
|
||||
|
||||
# At 10% duration
|
||||
ffmpeg -ss $(ffprobe -v error -show_entries format=duration \
|
||||
-of default=noprint_wrappers=1:nokey=1 input.mp4 | \
|
||||
awk '{print $1*0.1}') -i input.mp4 -vframes 1 thumb.jpg
|
||||
```
|
||||
|
||||
### Multiple Thumbnails
|
||||
Generate thumbnails at intervals.
|
||||
|
||||
```bash
|
||||
# One per minute
|
||||
ffmpeg -i input.mp4 -vf fps=1/60,scale=320:-1 thumb_%03d.jpg
|
||||
|
||||
# One per 10 seconds
|
||||
ffmpeg -i input.mp4 -vf fps=1/10,scale=320:-1 thumb_%03d.jpg
|
||||
|
||||
# First 10 frames
|
||||
ffmpeg -i input.mp4 -vframes 10 -vf scale=320:-1 thumb_%02d.jpg
|
||||
```
|
||||
|
||||
### Thumbnail Sprite Sheet
|
||||
Create single image with multiple thumbnails.
|
||||
|
||||
```bash
|
||||
# Generate frames
|
||||
ffmpeg -i input.mp4 -vf fps=1/10,scale=160:90 frames/thumb_%03d.jpg
|
||||
|
||||
# Combine into sprite (requires ImageMagick)
|
||||
montage frames/thumb_*.jpg -tile 5x -geometry +0+0 sprite.jpg
|
||||
```
|
||||
|
||||
## Preview Generation
|
||||
|
||||
### Video Preview (Trailer)
|
||||
Extract multiple short clips.
|
||||
|
||||
```bash
|
||||
# Extract 3 segments
|
||||
ffmpeg -i input.mp4 \
|
||||
-ss 00:00:30 -t 00:00:10 -c copy segment1.mp4
|
||||
ffmpeg -i input.mp4 \
|
||||
-ss 00:05:00 -t 00:00:10 -c copy segment2.mp4
|
||||
ffmpeg -i input.mp4 \
|
||||
-ss 00:10:00 -t 00:00:10 -c copy segment3.mp4
|
||||
|
||||
# Concatenate segments
|
||||
echo "file 'segment1.mp4'" > concat.txt
|
||||
echo "file 'segment2.mp4'" >> concat.txt
|
||||
echo "file 'segment3.mp4'" >> concat.txt
|
||||
ffmpeg -f concat -safe 0 -i concat.txt -c copy preview.mp4
|
||||
```
|
||||
|
||||
### Fast Preview (Low Quality)
|
||||
Quick preview for review.
|
||||
|
||||
```bash
|
||||
ffmpeg -i input.mp4 \
|
||||
-vf scale=640:-1 \
|
||||
-c:v libx264 -preset ultrafast -crf 28 \
|
||||
-c:a aac -b:a 64k \
|
||||
preview.mp4
|
||||
```
|
||||
|
||||
## Streaming Parameters
|
||||
|
||||
### Important RTMP Parameters
|
||||
|
||||
**Real-time reading:**
|
||||
- `-re` - Read input at native frame rate
|
||||
|
||||
**Low latency:**
|
||||
- `-tune zerolatency` - Optimize for minimal latency
|
||||
- `-preset ultrafast` or `veryfast` - Fast encoding
|
||||
|
||||
**Keyframes:**
|
||||
- `-g 50` - Keyframe interval (GOP size)
|
||||
- Recommended: 2 seconds (fps * 2)
|
||||
|
||||
**Rate control:**
|
||||
- `-maxrate` - Maximum bitrate (e.g., 3000k)
|
||||
- `-bufsize` - Buffer size (typically 2x maxrate)
|
||||
|
||||
**Compatibility:**
|
||||
- `-pix_fmt yuv420p` - Compatible pixel format
|
||||
|
||||
### Bitrate Recommendations
|
||||
|
||||
**1080p 60fps:**
|
||||
- 4500-6000 kbps video
|
||||
- 160 kbps audio
|
||||
|
||||
**1080p 30fps:**
|
||||
- 3000-4500 kbps video
|
||||
- 128 kbps audio
|
||||
|
||||
**720p 60fps:**
|
||||
- 2500-4000 kbps video
|
||||
- 128 kbps audio
|
||||
|
||||
**720p 30fps:**
|
||||
- 1500-2500 kbps video
|
||||
- 128 kbps audio
|
||||
|
||||
**480p:**
|
||||
- 500-1000 kbps video
|
||||
- 128 kbps audio
|
||||
|
||||
## UDP/RTP Streaming
|
||||
|
||||
### UDP Stream
|
||||
Simple network streaming.
|
||||
|
||||
```bash
|
||||
# Sender
|
||||
ffmpeg -re -i input.mp4 -c copy -f mpegts udp://192.168.1.100:1234
|
||||
|
||||
# Receiver
|
||||
ffplay udp://192.168.1.100:1234
|
||||
```
|
||||
|
||||
### RTP Stream
|
||||
Real-Time Protocol for low latency.
|
||||
|
||||
```bash
|
||||
# Audio only
|
||||
ffmpeg -re -i audio.mp3 -c:a libopus -f rtp rtp://192.168.1.100:5004
|
||||
|
||||
# Video + audio
|
||||
ffmpeg -re -i input.mp4 \
|
||||
-c:v libx264 -preset ultrafast \
|
||||
-c:a aac -f rtp rtp://192.168.1.100:5004
|
||||
```
|
||||
|
||||
### Multicast Stream
|
||||
Stream to multiple receivers.
|
||||
|
||||
```bash
|
||||
# Sender (multicast address)
|
||||
ffmpeg -re -i input.mp4 -c copy -f mpegts udp://239.255.0.1:1234
|
||||
|
||||
# Receiver
|
||||
ffplay udp://239.255.0.1:1234
|
||||
```
|
||||
|
||||
## Advanced Streaming
|
||||
|
||||
### Hardware-Accelerated Streaming
|
||||
Use GPU for faster encoding.
|
||||
|
||||
```bash
|
||||
# NVIDIA NVENC
|
||||
ffmpeg -re -i input.mp4 \
|
||||
-c:v h264_nvenc -preset fast -maxrate 3000k -bufsize 6000k \
|
||||
-c:a aac -b:a 128k \
|
||||
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
|
||||
|
||||
# Intel QSV
|
||||
ffmpeg -re -hwaccel qsv -i input.mp4 \
|
||||
-c:v h264_qsv -preset fast -maxrate 3000k -bufsize 6000k \
|
||||
-c:a aac -b:a 128k \
|
||||
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
|
||||
```
|
||||
|
||||
### Stream with Overlay
|
||||
Add graphics during stream.
|
||||
|
||||
```bash
|
||||
ffmpeg -re -i input.mp4 -i logo.png \
|
||||
-filter_complex "[0:v][1:v]overlay=10:10" \
|
||||
-c:v libx264 -preset veryfast -maxrate 3000k \
|
||||
-c:a copy \
|
||||
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
|
||||
```
|
||||
|
||||
### Loop Stream
|
||||
Continuously loop video for 24/7 stream.
|
||||
|
||||
```bash
|
||||
ffmpeg -stream_loop -1 -re -i input.mp4 \
|
||||
-c:v libx264 -preset veryfast -maxrate 2500k \
|
||||
-c:a aac -b:a 128k \
|
||||
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Buffering Issues
|
||||
```bash
|
||||
# Reduce buffer size
|
||||
ffmpeg -re -i input.mp4 -maxrate 2000k -bufsize 2000k -c:v libx264 -f flv rtmp://...
|
||||
|
||||
# Use faster preset
|
||||
ffmpeg -re -i input.mp4 -preset ultrafast -c:v libx264 -f flv rtmp://...
|
||||
```
|
||||
|
||||
### Audio/Video Desync
|
||||
```bash
|
||||
# Force constant frame rate
|
||||
ffmpeg -re -i input.mp4 -r 30 -c:v libx264 -f flv rtmp://...
|
||||
|
||||
# Use -vsync 1
|
||||
ffmpeg -re -i input.mp4 -vsync 1 -c:v libx264 -f flv rtmp://...
|
||||
```
|
||||
|
||||
### Connection Drops
|
||||
```bash
|
||||
# Increase timeout
|
||||
ffmpeg -timeout 5000000 -re -i input.mp4 -c:v libx264 -f flv rtmp://...
|
||||
|
||||
# Reconnect on failure (use wrapper script)
|
||||
while true; do
|
||||
ffmpeg -re -i input.mp4 -c:v libx264 -f flv rtmp://...
|
||||
sleep 5
|
||||
done
|
||||
```
|
||||
375
skills/media-processing/references/format-compatibility.md
Normal file
375
skills/media-processing/references/format-compatibility.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# Format Compatibility & Conversion Guide
|
||||
|
||||
Complete guide to media format support, codec recommendations, and conversion best practices.
|
||||
|
||||
## Image Format Support
|
||||
|
||||
### ImageMagick Formats
|
||||
|
||||
**Raster Formats (Full Support):**
|
||||
- JPEG (.jpg, .jpeg) - Lossy, universal
|
||||
- PNG (.png) - Lossless, transparency
|
||||
- WebP (.webp) - Modern, lossy/lossless
|
||||
- GIF (.gif) - Animation, limited colors
|
||||
- TIFF (.tif, .tiff) - Professional, lossless
|
||||
- BMP (.bmp) - Uncompressed, legacy
|
||||
- ICO (.ico) - Icons, multi-size
|
||||
|
||||
**Raw Formats (Read Support):**
|
||||
- CR2, NEF, ARW, DNG (Canon, Nikon, Sony, Adobe RAW)
|
||||
- Requires dcraw or ufraw-batch
|
||||
|
||||
**Vector Formats (Limited):**
|
||||
- SVG (.svg) - Read only, converts to raster
|
||||
- PDF (.pdf) - Read/write, may have policy restrictions
|
||||
|
||||
**Other Formats:**
|
||||
- HEIC (.heic) - Apple format, requires libheif
|
||||
- AVIF (.avif) - Next-gen, requires libavif
|
||||
- PSD (.psd) - Photoshop, basic support
|
||||
|
||||
### FFmpeg Image Support
|
||||
|
||||
**Input Formats:**
|
||||
- JPEG, PNG, BMP, TIFF, WebP, GIF
|
||||
- Image sequences (frame_%04d.png)
|
||||
|
||||
**Output Formats:**
|
||||
- JPEG, PNG, BMP, TIFF
|
||||
- Video from images
|
||||
|
||||
## Video Format Support
|
||||
|
||||
### Container Formats
|
||||
|
||||
**Universal Containers:**
|
||||
- MP4 (.mp4) - Most compatible, streaming
|
||||
- MKV (.mkv) - Feature-rich, flexible
|
||||
- WebM (.webm) - Web-optimized, open
|
||||
- AVI (.avi) - Legacy, broad support
|
||||
- MOV (.mov) - Apple, professional
|
||||
|
||||
**Streaming Containers:**
|
||||
- TS (.ts) - Transport stream, HLS segments
|
||||
- M3U8 (.m3u8) - HLS playlist
|
||||
- MPD (.mpd) - DASH manifest
|
||||
- FLV (.flv) - Flash (legacy)
|
||||
|
||||
**Professional Formats:**
|
||||
- ProRes (.mov) - Apple professional
|
||||
- DNxHD/DNxHR (.mxf, .mov) - Avid professional
|
||||
- MXF (.mxf) - Broadcast
|
||||
|
||||
### Video Codecs
|
||||
|
||||
**Modern Codecs:**
|
||||
- H.264/AVC (libx264) - Universal, excellent balance
|
||||
- H.265/HEVC (libx265) - Better compression, 4K
|
||||
- VP9 (libvpx-vp9) - Open, YouTube
|
||||
- AV1 (libaom-av1, libsvtav1) - Next-gen, best compression
|
||||
|
||||
**Legacy Codecs:**
|
||||
- MPEG-4 (mpeg4) - Older devices
|
||||
- MPEG-2 (mpeg2video) - DVD, broadcast
|
||||
- VP8 (libvpx) - WebM predecessor
|
||||
|
||||
**Professional Codecs:**
|
||||
- ProRes (prores) - Apple post-production
|
||||
- DNxHD (dnxhd) - Avid editing
|
||||
- Uncompressed (rawvideo) - Maximum quality
|
||||
|
||||
### Audio Codecs
|
||||
|
||||
**Modern Codecs:**
|
||||
- AAC (aac) - Universal, streaming
|
||||
- Opus (libopus) - Best low-bitrate
|
||||
- MP3 (libmp3lame) - Universal compatibility
|
||||
|
||||
**Lossless Codecs:**
|
||||
- FLAC (flac) - Open, archival
|
||||
- ALAC (alac) - Apple lossless
|
||||
- WAV (pcm_s16le) - Uncompressed
|
||||
|
||||
**Other Codecs:**
|
||||
- Vorbis (libvorbis) - Open, WebM
|
||||
- AC-3 (ac3) - Dolby Digital, surround
|
||||
- DTS (dts) - Cinema surround
|
||||
|
||||
## Format Recommendations
|
||||
|
||||
### Use Case Matrix
|
||||
|
||||
| Use Case | Image Format | Video Container | Video Codec | Audio Codec |
|
||||
|----------|--------------|-----------------|-------------|-------------|
|
||||
| Web general | JPEG 85% | MP4 | H.264 | AAC 128k |
|
||||
| Web transparency | PNG | - | - | - |
|
||||
| Web modern | WebP | WebM | VP9 | Opus |
|
||||
| Social media | JPEG 85% | MP4 | H.264 | AAC 128k |
|
||||
| 4K streaming | - | MP4 | H.265 | AAC 192k |
|
||||
| Archive | PNG/TIFF | MKV | H.265 CRF 18 | FLAC |
|
||||
| Email | JPEG 75% | - | - | - |
|
||||
| Print | TIFF/PNG | - | - | - |
|
||||
| YouTube | - | MP4/WebM | H.264/VP9 | AAC/Opus |
|
||||
| Live stream | - | FLV | H.264 | AAC |
|
||||
| Editing | - | MOV/MXF | ProRes/DNxHD | PCM |
|
||||
|
||||
### Platform Compatibility
|
||||
|
||||
**Web Browsers (2025):**
|
||||
- Images: JPEG, PNG, WebP, GIF, SVG
|
||||
- Video: MP4 (H.264), WebM (VP9), MP4 (AV1)
|
||||
- Audio: AAC, MP3, Opus, Vorbis
|
||||
|
||||
**Mobile Devices:**
|
||||
- iOS: JPEG, PNG, HEIC, MP4 (H.264/H.265), AAC
|
||||
- Android: JPEG, PNG, WebP, MP4 (H.264/H.265), AAC
|
||||
|
||||
**Smart TVs:**
|
||||
- Most: MP4 (H.264), AAC
|
||||
- Modern: MP4 (H.265), AC-3
|
||||
|
||||
**Social Media:**
|
||||
- All platforms: JPEG, MP4 (H.264), AAC
|
||||
|
||||
## Quality vs Size Trade-offs
|
||||
|
||||
### Image Quality Comparison
|
||||
|
||||
**JPEG Quality Levels:**
|
||||
- 95-100: ~5-10 MB (large image), minimal artifacts
|
||||
- 85-94: ~1-3 MB, imperceptible loss
|
||||
- 75-84: ~500 KB-1 MB, slight artifacts
|
||||
- 60-74: ~200-500 KB, visible artifacts
|
||||
- Below 60: <200 KB, poor quality
|
||||
|
||||
**Format Comparison (Same quality):**
|
||||
- WebP: 25-35% smaller than JPEG
|
||||
- HEIC: 40-50% smaller than JPEG
|
||||
- AVIF: 50-60% smaller than JPEG
|
||||
- PNG: 2-5x larger than JPEG (lossless)
|
||||
|
||||
### Video Quality Comparison
|
||||
|
||||
**H.264 CRF Values:**
|
||||
- CRF 18: Visually lossless, ~8-15 Mbps (1080p)
|
||||
- CRF 23: High quality, ~4-8 Mbps (1080p)
|
||||
- CRF 28: Medium quality, ~2-4 Mbps (1080p)
|
||||
|
||||
**Codec Comparison (Same quality):**
|
||||
- H.265: 40-50% smaller than H.264
|
||||
- VP9: 30-40% smaller than H.264
|
||||
- AV1: 50-60% smaller than H.264
|
||||
|
||||
### Audio Quality Comparison
|
||||
|
||||
**AAC Bitrates:**
|
||||
- 320 kbps: Transparent, archival
|
||||
- 192 kbps: High quality, music
|
||||
- 128 kbps: Good quality, streaming
|
||||
- 96 kbps: Acceptable, low bandwidth
|
||||
- 64 kbps: Poor, voice only
|
||||
|
||||
**Codec Efficiency (Same quality):**
|
||||
- Opus: Best at low bitrates (<128k)
|
||||
- AAC: Best overall balance
|
||||
- MP3: Less efficient but universal
|
||||
|
||||
## Conversion Best Practices
|
||||
|
||||
### Image Conversions
|
||||
|
||||
**PNG to JPEG:**
|
||||
```bash
|
||||
# Standard conversion
|
||||
magick input.png -quality 85 -strip output.jpg
|
||||
|
||||
# With transparency handling
|
||||
magick input.png -background white -flatten -quality 85 output.jpg
|
||||
```
|
||||
|
||||
**JPEG to WebP:**
|
||||
```bash
|
||||
# FFmpeg
|
||||
ffmpeg -i input.jpg -quality 80 output.webp
|
||||
|
||||
# ImageMagick
|
||||
magick input.jpg -quality 80 output.webp
|
||||
```
|
||||
|
||||
**RAW to JPEG:**
|
||||
```bash
|
||||
# Requires dcraw
|
||||
magick input.CR2 -quality 90 output.jpg
|
||||
```
|
||||
|
||||
**HEIC to JPEG:**
|
||||
```bash
|
||||
# Requires libheif
|
||||
magick input.heic -quality 85 output.jpg
|
||||
```
|
||||
|
||||
### Video Conversions
|
||||
|
||||
**MKV to MP4:**
|
||||
```bash
|
||||
# Copy streams (fast)
|
||||
ffmpeg -i input.mkv -c copy output.mp4
|
||||
|
||||
# Re-encode if needed
|
||||
ffmpeg -i input.mkv -c:v libx264 -crf 23 -c:a aac output.mp4
|
||||
```
|
||||
|
||||
**AVI to MP4:**
|
||||
```bash
|
||||
# Modern codecs
|
||||
ffmpeg -i input.avi -c:v libx264 -crf 23 -c:a aac output.mp4
|
||||
```
|
||||
|
||||
**MOV to MP4:**
|
||||
```bash
|
||||
# Copy if H.264 already
|
||||
ffmpeg -i input.mov -c copy output.mp4
|
||||
|
||||
# Convert ProRes to H.264
|
||||
ffmpeg -i input.mov -c:v libx264 -crf 18 -c:a aac output.mp4
|
||||
```
|
||||
|
||||
**Any to WebM:**
|
||||
```bash
|
||||
# VP9 encoding
|
||||
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus output.webm
|
||||
```
|
||||
|
||||
### Audio Conversions
|
||||
|
||||
**Extract Audio from Video:**
|
||||
```bash
|
||||
# Keep original codec
|
||||
ffmpeg -i video.mp4 -vn -c:a copy audio.m4a
|
||||
|
||||
# Convert to MP3
|
||||
ffmpeg -i video.mp4 -vn -q:a 0 audio.mp3
|
||||
|
||||
# Convert to FLAC (lossless)
|
||||
ffmpeg -i video.mp4 -vn -c:a flac audio.flac
|
||||
```
|
||||
|
||||
**Audio Format Conversion:**
|
||||
```bash
|
||||
# WAV to MP3
|
||||
ffmpeg -i input.wav -c:a libmp3lame -b:a 192k output.mp3
|
||||
|
||||
# MP3 to AAC
|
||||
ffmpeg -i input.mp3 -c:a aac -b:a 192k output.m4a
|
||||
|
||||
# Any to Opus
|
||||
ffmpeg -i input.wav -c:a libopus -b:a 128k output.opus
|
||||
```
|
||||
|
||||
## Codec Selection Guide
|
||||
|
||||
### Choose H.264 When:
|
||||
- Maximum compatibility needed
|
||||
- Targeting older devices
|
||||
- Streaming to unknown devices
|
||||
- Social media upload
|
||||
- Fast encoding required
|
||||
|
||||
### Choose H.265 When:
|
||||
- 4K video encoding
|
||||
- Storage space limited
|
||||
- Modern device targets
|
||||
- Archival quality needed
|
||||
- Bandwidth constrained
|
||||
|
||||
### Choose VP9 When:
|
||||
- YouTube upload
|
||||
- Open-source requirement
|
||||
- Chrome/Firefox primary
|
||||
- Royalty-free needed
|
||||
|
||||
### Choose AV1 When:
|
||||
- Future-proofing content
|
||||
- Maximum compression needed
|
||||
- Encoding time not critical
|
||||
- Modern platform targets
|
||||
|
||||
## Format Migration Strategies
|
||||
|
||||
### Archive to Web
|
||||
|
||||
```bash
|
||||
# High-res archive -> Web-optimized
|
||||
for img in archive/*.tif; do
|
||||
base=$(basename "$img" .tif)
|
||||
magick "$img" -resize 2000x2000\> -quality 85 -strip "web/${base}.jpg"
|
||||
magick "$img" -resize 2000x2000\> -quality 85 "web/${base}.webp"
|
||||
done
|
||||
```
|
||||
|
||||
### Legacy to Modern
|
||||
|
||||
```bash
|
||||
# Convert old formats to modern codecs
|
||||
for video in legacy/*.avi; do
|
||||
base=$(basename "$video" .avi)
|
||||
ffmpeg -i "$video" \
|
||||
-c:v libx264 -crf 23 -preset slow \
|
||||
-c:a aac -b:a 128k \
|
||||
"modern/${base}.mp4"
|
||||
done
|
||||
```
|
||||
|
||||
### Multi-Format Publishing
|
||||
|
||||
```bash
|
||||
# Create multiple formats for compatibility
|
||||
input="source.mp4"
|
||||
|
||||
# Modern browsers
|
||||
ffmpeg -i "$input" -c:v libx264 -crf 23 -c:a aac output.mp4
|
||||
ffmpeg -i "$input" -c:v libvpx-vp9 -crf 30 -c:a libopus output.webm
|
||||
|
||||
# Images
|
||||
ffmpeg -ss 5 -i "$input" -vframes 1 poster.jpg
|
||||
magick poster.jpg -quality 80 poster.webp
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Unsupported Format
|
||||
|
||||
```bash
|
||||
# Check FFmpeg formats
|
||||
ffmpeg -formats
|
||||
|
||||
# Check ImageMagick formats
|
||||
magick identify -list format
|
||||
|
||||
# Install missing codec support
|
||||
sudo apt-get install libx264-dev libx265-dev libvpx-dev
|
||||
```
|
||||
|
||||
### Compatibility Issues
|
||||
|
||||
```bash
|
||||
# Force compatible encoding
|
||||
ffmpeg -i input.mp4 \
|
||||
-c:v libx264 -profile:v high -level 4.0 \
|
||||
-pix_fmt yuv420p \
|
||||
-c:a aac -b:a 128k \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
### Quality Loss
|
||||
|
||||
```bash
|
||||
# Avoid multiple conversions
|
||||
# Bad: source -> edit -> web -> social
|
||||
# Good: source -> final (single conversion)
|
||||
|
||||
# Use lossless intermediate
|
||||
ffmpeg -i source.mp4 -c:v ffv1 intermediate.mkv
|
||||
# Edit intermediate
|
||||
ffmpeg -i intermediate.mkv -c:v libx264 final.mp4
|
||||
```
|
||||
612
skills/media-processing/references/imagemagick-batch.md
Normal file
612
skills/media-processing/references/imagemagick-batch.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# ImageMagick Batch Processing
|
||||
|
||||
Complete guide to batch operations, mogrify command, parallel processing, and automation.
|
||||
|
||||
## Mogrify Command
|
||||
|
||||
### Basic Mogrify
|
||||
Modify files in-place (overwrites originals).
|
||||
|
||||
```bash
|
||||
# Resize all JPEGs
|
||||
mogrify -resize 800x600 *.jpg
|
||||
|
||||
# Convert format (creates new files)
|
||||
mogrify -format png *.jpg
|
||||
|
||||
# Apply effect to all images
|
||||
mogrify -quality 85 -strip *.jpg
|
||||
```
|
||||
|
||||
**Warning:** mogrify modifies files in-place. Always backup originals or use `-path` to output to different directory.
|
||||
|
||||
### Output to Different Directory
|
||||
Preserve originals.
|
||||
|
||||
```bash
|
||||
# Create output directory first
|
||||
mkdir output
|
||||
|
||||
# Process to output directory
|
||||
mogrify -path ./output -resize 800x600 *.jpg
|
||||
|
||||
# With format conversion
|
||||
mogrify -path ./optimized -format webp -quality 80 *.png
|
||||
```
|
||||
|
||||
## Common Batch Operations
|
||||
|
||||
### Resize All Images
|
||||
|
||||
```bash
|
||||
# Resize to width 800
|
||||
mogrify -resize 800x *.jpg
|
||||
|
||||
# Resize to height 600
|
||||
mogrify -resize x600 *.jpg
|
||||
|
||||
# Fit within 800×600
|
||||
mogrify -resize 800x600 *.jpg
|
||||
|
||||
# Resize to exact dimensions
|
||||
mogrify -resize 800x600! *.jpg
|
||||
|
||||
# Only shrink, never enlarge
|
||||
mogrify -resize 800x600\> *.jpg
|
||||
```
|
||||
|
||||
### Format Conversion
|
||||
|
||||
```bash
|
||||
# PNG to JPEG
|
||||
mogrify -path ./jpg -format jpg -quality 85 *.png
|
||||
|
||||
# JPEG to WebP
|
||||
mogrify -path ./webp -format webp -quality 80 *.jpg
|
||||
|
||||
# Any format to PNG
|
||||
mogrify -path ./png -format png *.{jpg,gif,bmp}
|
||||
```
|
||||
|
||||
### Optimize Images
|
||||
|
||||
```bash
|
||||
# Strip metadata from all JPEGs
|
||||
mogrify -strip *.jpg
|
||||
|
||||
# Optimize JPEGs for web
|
||||
mogrify -quality 85 -strip -interlace Plane *.jpg
|
||||
|
||||
# Compress PNGs
|
||||
mogrify -quality 95 *.png
|
||||
|
||||
# Combined optimization
|
||||
mogrify -quality 85 -strip -interlace Plane -sampling-factor 4:2:0 *.jpg
|
||||
```
|
||||
|
||||
### Apply Effects
|
||||
|
||||
```bash
|
||||
# Add watermark to all images
|
||||
mogrify -gravity southeast -draw "image over 10,10 0,0 'watermark.png'" *.jpg
|
||||
|
||||
# Convert all to grayscale
|
||||
mogrify -colorspace Gray *.jpg
|
||||
|
||||
# Apply sepia tone
|
||||
mogrify -sepia-tone 80% *.jpg
|
||||
|
||||
# Sharpen all images
|
||||
mogrify -sharpen 0x1 *.jpg
|
||||
```
|
||||
|
||||
### Thumbnail Generation
|
||||
|
||||
```bash
|
||||
# Create square thumbnails
|
||||
mogrify -path ./thumbnails -resize 200x200^ -gravity center -extent 200x200 *.jpg
|
||||
|
||||
# Create thumbnails with max dimension
|
||||
mogrify -path ./thumbs -thumbnail 300x300 *.jpg
|
||||
|
||||
# Thumbnails with quality control
|
||||
mogrify -path ./thumbs -thumbnail 200x200 -quality 80 -strip *.jpg
|
||||
```
|
||||
|
||||
## Shell Loops
|
||||
|
||||
### Basic For Loop
|
||||
More control than mogrify.
|
||||
|
||||
```bash
|
||||
# Resize with custom naming
|
||||
for img in *.jpg; do
|
||||
magick "$img" -resize 800x600 "resized_$img"
|
||||
done
|
||||
|
||||
# Process to subdirectory
|
||||
mkdir processed
|
||||
for img in *.jpg; do
|
||||
magick "$img" -resize 1920x1080 "processed/$img"
|
||||
done
|
||||
```
|
||||
|
||||
### Multiple Operations
|
||||
|
||||
```bash
|
||||
# Complex processing pipeline
|
||||
for img in *.jpg; do
|
||||
magick "$img" \
|
||||
-resize 1920x1080^ \
|
||||
-gravity center \
|
||||
-crop 1920x1080+0+0 +repage \
|
||||
-unsharp 0x1 \
|
||||
-quality 85 -strip \
|
||||
"processed_$img"
|
||||
done
|
||||
```
|
||||
|
||||
### Format Conversion with Rename
|
||||
|
||||
```bash
|
||||
# Convert PNG to JPEG with new names
|
||||
for img in *.png; do
|
||||
magick "$img" -quality 90 "${img%.png}.jpg"
|
||||
done
|
||||
|
||||
# Add prefix during conversion
|
||||
for img in *.jpg; do
|
||||
magick "$img" -resize 800x "web_${img}"
|
||||
done
|
||||
```
|
||||
|
||||
### Conditional Processing
|
||||
|
||||
```bash
|
||||
# Only process large images
|
||||
for img in *.jpg; do
|
||||
width=$(identify -format "%w" "$img")
|
||||
if [ $width -gt 2000 ]; then
|
||||
magick "$img" -resize 2000x "resized_$img"
|
||||
fi
|
||||
done
|
||||
|
||||
# Skip existing output files
|
||||
for img in *.jpg; do
|
||||
output="output_$img"
|
||||
if [ ! -f "$output" ]; then
|
||||
magick "$img" -resize 800x "$output"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Parallel Processing
|
||||
|
||||
### GNU Parallel
|
||||
Process multiple images simultaneously.
|
||||
|
||||
```bash
|
||||
# Install GNU Parallel
|
||||
# Ubuntu/Debian: sudo apt-get install parallel
|
||||
# macOS: brew install parallel
|
||||
|
||||
# Basic parallel resize
|
||||
parallel magick {} -resize 800x600 resized_{} ::: *.jpg
|
||||
|
||||
# Parallel with function
|
||||
resize_image() {
|
||||
magick "$1" -resize 1920x1080 -quality 85 "processed_$1"
|
||||
}
|
||||
export -f resize_image
|
||||
parallel resize_image ::: *.jpg
|
||||
|
||||
# Limit concurrent jobs
|
||||
parallel -j 4 magick {} -resize 800x {} ::: *.jpg
|
||||
|
||||
# Progress indicator
|
||||
parallel --progress magick {} -resize 800x {} ::: *.jpg
|
||||
```
|
||||
|
||||
### Xargs Parallel
|
||||
|
||||
```bash
|
||||
# Using xargs for parallel processing
|
||||
ls *.jpg | xargs -I {} -P 4 magick {} -resize 800x processed_{}
|
||||
|
||||
# With find
|
||||
find . -name "*.jpg" -print0 | \
|
||||
xargs -0 -I {} -P 4 magick {} -resize 800x {}
|
||||
```
|
||||
|
||||
## Advanced Batch Patterns
|
||||
|
||||
### Recursive Processing
|
||||
|
||||
```bash
|
||||
# Process all JPEGs in subdirectories
|
||||
find . -name "*.jpg" -exec magick {} -resize 800x {} \;
|
||||
|
||||
# With output directory structure
|
||||
find . -name "*.jpg" -type f | while read img; do
|
||||
outdir="output/$(dirname "$img")"
|
||||
mkdir -p "$outdir"
|
||||
magick "$img" -resize 800x "$outdir/$(basename "$img")"
|
||||
done
|
||||
```
|
||||
|
||||
### Batch with Different Sizes
|
||||
|
||||
```bash
|
||||
# Generate multiple sizes
|
||||
for size in 320 640 1024 1920; do
|
||||
mkdir -p "output/${size}w"
|
||||
for img in *.jpg; do
|
||||
magick "$img" -resize ${size}x -quality 85 "output/${size}w/$img"
|
||||
done
|
||||
done
|
||||
|
||||
# Parallel version
|
||||
for size in 320 640 1024 1920; do
|
||||
mkdir -p "output/${size}w"
|
||||
parallel magick {} -resize ${size}x -quality 85 "output/${size}w/{}" ::: *.jpg
|
||||
done
|
||||
```
|
||||
|
||||
### Responsive Image Set
|
||||
|
||||
```bash
|
||||
# Create responsive image set with srcset
|
||||
mkdir -p responsive
|
||||
for img in *.jpg; do
|
||||
base="${img%.jpg}"
|
||||
for width in 320 640 1024 1920; do
|
||||
magick "$img" -resize ${width}x -quality 85 \
|
||||
"responsive/${base}-${width}w.jpg"
|
||||
done
|
||||
done
|
||||
```
|
||||
|
||||
### Watermark Batch
|
||||
|
||||
```bash
|
||||
# Add watermark to all images
|
||||
for img in *.jpg; do
|
||||
magick "$img" watermark.png \
|
||||
-gravity southeast -geometry +10+10 \
|
||||
-composite "watermarked_$img"
|
||||
done
|
||||
|
||||
# Different watermark positions for portrait vs landscape
|
||||
for img in *.jpg; do
|
||||
width=$(identify -format "%w" "$img")
|
||||
height=$(identify -format "%h" "$img")
|
||||
|
||||
if [ $width -gt $height ]; then
|
||||
# Landscape
|
||||
magick "$img" watermark.png -gravity southeast -composite "marked_$img"
|
||||
else
|
||||
# Portrait
|
||||
magick "$img" watermark.png -gravity south -composite "marked_$img"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Check Before Processing
|
||||
|
||||
```bash
|
||||
# Verify image before processing
|
||||
for img in *.jpg; do
|
||||
if identify "$img" > /dev/null 2>&1; then
|
||||
magick "$img" -resize 800x "processed_$img"
|
||||
else
|
||||
echo "Skipping corrupt image: $img"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Log Processing
|
||||
|
||||
```bash
|
||||
# Log successful and failed operations
|
||||
log_file="batch_process.log"
|
||||
error_log="errors.log"
|
||||
|
||||
for img in *.jpg; do
|
||||
if magick "$img" -resize 800x "output/$img" 2>> "$error_log"; then
|
||||
echo "$(date): Processed $img" >> "$log_file"
|
||||
else
|
||||
echo "$(date): Failed $img" >> "$error_log"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Dry Run Mode
|
||||
|
||||
```bash
|
||||
# Test without modifying files
|
||||
dry_run=true
|
||||
|
||||
for img in *.jpg; do
|
||||
cmd="magick $img -resize 800x processed_$img"
|
||||
if [ "$dry_run" = true ]; then
|
||||
echo "Would run: $cmd"
|
||||
else
|
||||
eval $cmd
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Optimization Workflows
|
||||
|
||||
### Web Publishing Pipeline
|
||||
|
||||
```bash
|
||||
# Complete web optimization workflow
|
||||
mkdir -p web/{original,optimized,thumbnails}
|
||||
|
||||
# Copy originals
|
||||
cp *.jpg web/original/
|
||||
|
||||
# Create optimized versions
|
||||
mogrify -path web/optimized \
|
||||
-resize 1920x1080\> \
|
||||
-quality 85 \
|
||||
-strip \
|
||||
-interlace Plane \
|
||||
web/original/*.jpg
|
||||
|
||||
# Create thumbnails
|
||||
mogrify -path web/thumbnails \
|
||||
-thumbnail 300x300 \
|
||||
-quality 80 \
|
||||
-strip \
|
||||
web/original/*.jpg
|
||||
```
|
||||
|
||||
### Archive to Web Conversion
|
||||
|
||||
```bash
|
||||
# Convert high-res archives to web formats
|
||||
for img in archives/*.jpg; do
|
||||
base=$(basename "$img" .jpg)
|
||||
|
||||
# Full size web version
|
||||
magick "$img" -resize 2048x2048\> -quality 90 -strip "web/${base}.jpg"
|
||||
|
||||
# Thumbnail
|
||||
magick "$img" -thumbnail 400x400 -quality 85 "web/${base}_thumb.jpg"
|
||||
|
||||
# WebP version
|
||||
magick "$img" -resize 2048x2048\> -quality 85 "web/${base}.webp"
|
||||
done
|
||||
```
|
||||
|
||||
### Print to Web Workflow
|
||||
|
||||
```bash
|
||||
# Convert print-ready images to web
|
||||
for img in print/*.tif; do
|
||||
base=$(basename "$img" .tif)
|
||||
|
||||
# Convert colorspace and optimize
|
||||
magick "$img" \
|
||||
-colorspace sRGB \
|
||||
-resize 2000x2000\> \
|
||||
-quality 90 \
|
||||
-strip \
|
||||
-interlace Plane \
|
||||
"web/${base}.jpg"
|
||||
done
|
||||
```
|
||||
|
||||
## Batch Reporting
|
||||
|
||||
### Generate Report
|
||||
|
||||
```bash
|
||||
# Create processing report
|
||||
report="batch_report.txt"
|
||||
echo "Batch Processing Report - $(date)" > "$report"
|
||||
echo "================================" >> "$report"
|
||||
|
||||
total=0
|
||||
success=0
|
||||
failed=0
|
||||
|
||||
for img in *.jpg; do
|
||||
((total++))
|
||||
if magick "$img" -resize 800x "output/$img" 2>/dev/null; then
|
||||
((success++))
|
||||
echo "✓ $img" >> "$report"
|
||||
else
|
||||
((failed++))
|
||||
echo "✗ $img" >> "$report"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> "$report"
|
||||
echo "Total: $total, Success: $success, Failed: $failed" >> "$report"
|
||||
```
|
||||
|
||||
### Image Inventory
|
||||
|
||||
```bash
|
||||
# Create inventory of images
|
||||
inventory="image_inventory.csv"
|
||||
echo "Filename,Width,Height,Format,Size,ColorSpace" > "$inventory"
|
||||
|
||||
for img in *.{jpg,png,gif}; do
|
||||
[ -f "$img" ] || continue
|
||||
info=$(identify -format "%f,%w,%h,%m,%b,%[colorspace]" "$img")
|
||||
echo "$info" >> "$inventory"
|
||||
done
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Optimize Loop Performance
|
||||
|
||||
```bash
|
||||
# Bad: Launch mogrify for each file
|
||||
for img in *.jpg; do
|
||||
mogrify -resize 800x "$img"
|
||||
done
|
||||
|
||||
# Good: Process all files in one mogrify call
|
||||
mogrify -resize 800x *.jpg
|
||||
|
||||
# Best: Use parallel processing for complex operations
|
||||
parallel magick {} -resize 800x -quality 85 processed_{} ::: *.jpg
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
```bash
|
||||
# Limit memory for batch processing
|
||||
for img in *.jpg; do
|
||||
magick -limit memory 2GB -limit map 4GB \
|
||||
"$img" -resize 50% "output/$img"
|
||||
done
|
||||
```
|
||||
|
||||
### Progress Tracking
|
||||
|
||||
```bash
|
||||
# Show progress for long batch operations
|
||||
total=$(ls *.jpg | wc -l)
|
||||
current=0
|
||||
|
||||
for img in *.jpg; do
|
||||
((current++))
|
||||
echo "Processing $current/$total: $img"
|
||||
magick "$img" -resize 800x "output/$img"
|
||||
done
|
||||
```
|
||||
|
||||
## Automation Scripts
|
||||
|
||||
### Complete Bash Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
INPUT_DIR="./input"
|
||||
OUTPUT_DIR="./output"
|
||||
QUALITY=85
|
||||
MAX_WIDTH=1920
|
||||
THUMBNAIL_SIZE=300
|
||||
|
||||
# Create output directories
|
||||
mkdir -p "$OUTPUT_DIR"/{full,thumbnails}
|
||||
|
||||
# Process images
|
||||
echo "Processing images..."
|
||||
for img in "$INPUT_DIR"/*.{jpg,jpeg,png}; do
|
||||
[ -f "$img" ] || continue
|
||||
|
||||
filename=$(basename "$img")
|
||||
base="${filename%.*}"
|
||||
|
||||
# Full size
|
||||
magick "$img" \
|
||||
-resize ${MAX_WIDTH}x\> \
|
||||
-quality $QUALITY \
|
||||
-strip \
|
||||
"$OUTPUT_DIR/full/${base}.jpg"
|
||||
|
||||
# Thumbnail
|
||||
magick "$img" \
|
||||
-thumbnail ${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE} \
|
||||
-quality 80 \
|
||||
-strip \
|
||||
"$OUTPUT_DIR/thumbnails/${base}_thumb.jpg"
|
||||
|
||||
echo "✓ $filename"
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
```
|
||||
|
||||
### Python Batch Script
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
INPUT_DIR = Path("./input")
|
||||
OUTPUT_DIR = Path("./output")
|
||||
SIZES = [320, 640, 1024, 1920]
|
||||
|
||||
# Create output directories
|
||||
for size in SIZES:
|
||||
(OUTPUT_DIR / f"{size}w").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Process images
|
||||
for img in INPUT_DIR.glob("*.jpg"):
|
||||
for size in SIZES:
|
||||
output = OUTPUT_DIR / f"{size}w" / img.name
|
||||
subprocess.run([
|
||||
"magick", str(img),
|
||||
"-resize", f"{size}x",
|
||||
"-quality", "85",
|
||||
"-strip",
|
||||
str(output)
|
||||
])
|
||||
print(f"✓ {img.name} -> {size}w")
|
||||
```
|
||||
|
||||
## Common Batch Recipes
|
||||
|
||||
### Social Media Sizes
|
||||
|
||||
```bash
|
||||
# Generate social media image sizes
|
||||
for img in *.jpg; do
|
||||
base="${img%.jpg}"
|
||||
|
||||
# Instagram square (1080×1080)
|
||||
magick "$img" -resize 1080x1080^ -gravity center -extent 1080x1080 "${base}_ig_square.jpg"
|
||||
|
||||
# Instagram portrait (1080×1350)
|
||||
magick "$img" -resize 1080x1350^ -gravity center -extent 1080x1350 "${base}_ig_portrait.jpg"
|
||||
|
||||
# Facebook post (1200×630)
|
||||
magick "$img" -resize 1200x630^ -gravity center -extent 1200x630 "${base}_fb_post.jpg"
|
||||
|
||||
# Twitter post (1200×675)
|
||||
magick "$img" -resize 1200x675^ -gravity center -extent 1200x675 "${base}_tw_post.jpg"
|
||||
done
|
||||
```
|
||||
|
||||
### Email Newsletter Images
|
||||
|
||||
```bash
|
||||
# Optimize images for email
|
||||
mogrify -path ./email \
|
||||
-resize 600x\> \
|
||||
-quality 75 \
|
||||
-strip \
|
||||
-interlace Plane \
|
||||
*.jpg
|
||||
```
|
||||
|
||||
### Backup and Archive
|
||||
|
||||
```bash
|
||||
# Create web versions and keep originals
|
||||
mkdir -p {originals,web}
|
||||
|
||||
# Move originals
|
||||
mv *.jpg originals/
|
||||
|
||||
# Create optimized copies
|
||||
for img in originals/*.jpg; do
|
||||
base=$(basename "$img")
|
||||
magick "$img" -resize 2000x2000\> -quality 85 -strip "web/$base"
|
||||
done
|
||||
```
|
||||
623
skills/media-processing/references/imagemagick-editing.md
Normal file
623
skills/media-processing/references/imagemagick-editing.md
Normal file
@@ -0,0 +1,623 @@
|
||||
# ImageMagick Image Editing
|
||||
|
||||
Complete guide to format conversion, resizing, effects, transformations, and composition.
|
||||
|
||||
## Format Conversion
|
||||
|
||||
### Basic Conversion
|
||||
Convert between image formats.
|
||||
|
||||
```bash
|
||||
# PNG to JPEG
|
||||
magick input.png output.jpg
|
||||
|
||||
# JPEG to WebP
|
||||
magick input.jpg output.webp
|
||||
|
||||
# Multiple outputs simultaneously
|
||||
magick input.png output.jpg output.webp output.gif
|
||||
|
||||
# Convert with quality setting
|
||||
magick input.png -quality 85 output.jpg
|
||||
```
|
||||
|
||||
### Quality Settings
|
||||
|
||||
**JPEG Quality (0-100):**
|
||||
- 95-100: Archival, minimal compression
|
||||
- 85-94: High quality, web publishing
|
||||
- 75-84: Medium quality, web optimized
|
||||
- 60-74: Lower quality, smaller files
|
||||
- Below 60: Visible artifacts
|
||||
|
||||
```bash
|
||||
# High quality
|
||||
magick input.png -quality 95 output.jpg
|
||||
|
||||
# Web optimized (recommended)
|
||||
magick input.png -quality 85 -strip output.jpg
|
||||
|
||||
# Smaller file size
|
||||
magick input.png -quality 75 -sampling-factor 4:2:0 -strip output.jpg
|
||||
```
|
||||
|
||||
**PNG Quality (0-9 = compression level):**
|
||||
```bash
|
||||
# Maximum compression (slower)
|
||||
magick input.jpg -quality 95 output.png
|
||||
|
||||
# Faster compression
|
||||
magick input.jpg -quality 75 output.png
|
||||
```
|
||||
|
||||
**WebP Quality:**
|
||||
```bash
|
||||
# Lossy with quality
|
||||
magick input.jpg -quality 80 output.webp
|
||||
|
||||
# Lossless
|
||||
magick input.png -define webp:lossless=true output.webp
|
||||
```
|
||||
|
||||
### Progressive & Optimization
|
||||
|
||||
```bash
|
||||
# Progressive JPEG (better web loading)
|
||||
magick input.png -quality 85 -interlace Plane output.jpg
|
||||
|
||||
# Strip metadata (reduce file size)
|
||||
magick input.jpg -strip output.jpg
|
||||
|
||||
# Combined optimization
|
||||
magick input.png -quality 85 -interlace Plane -strip output.jpg
|
||||
```
|
||||
|
||||
## Resizing Operations
|
||||
|
||||
### Basic Resize
|
||||
Maintain aspect ratio.
|
||||
|
||||
```bash
|
||||
# Fit within 800×600
|
||||
magick input.jpg -resize 800x600 output.jpg
|
||||
|
||||
# Resize to specific width (auto height)
|
||||
magick input.jpg -resize 800x output.jpg
|
||||
|
||||
# Resize to specific height (auto width)
|
||||
magick input.jpg -resize x600 output.jpg
|
||||
|
||||
# Scale by percentage
|
||||
magick input.jpg -resize 50% output.jpg
|
||||
```
|
||||
|
||||
### Advanced Resize
|
||||
|
||||
```bash
|
||||
# Resize only if larger (shrink only)
|
||||
magick input.jpg -resize 800x600\> output.jpg
|
||||
|
||||
# Resize only if smaller (enlarge only)
|
||||
magick input.jpg -resize 800x600\< output.jpg
|
||||
|
||||
# Force exact dimensions (ignore aspect ratio)
|
||||
magick input.jpg -resize 800x600! output.jpg
|
||||
|
||||
# Fill dimensions (may crop)
|
||||
magick input.jpg -resize 800x600^ output.jpg
|
||||
|
||||
# Minimum dimensions
|
||||
magick input.jpg -resize 800x600^ output.jpg
|
||||
```
|
||||
|
||||
### Resize Algorithms
|
||||
|
||||
```bash
|
||||
# High quality (Lanczos)
|
||||
magick input.jpg -filter Lanczos -resize 50% output.jpg
|
||||
|
||||
# Fast resize (Box)
|
||||
magick input.jpg -filter Box -resize 50% output.jpg
|
||||
|
||||
# Mitchel filter (good balance)
|
||||
magick input.jpg -filter Mitchell -resize 50% output.jpg
|
||||
```
|
||||
|
||||
**Filter comparison:**
|
||||
- `Lanczos` - Highest quality, slower
|
||||
- `Mitchell` - Good quality, fast
|
||||
- `Catrom` - Sharp, good for downscaling
|
||||
- `Box` - Fastest, acceptable quality
|
||||
- `Cubic` - Smooth results
|
||||
|
||||
## Cropping
|
||||
|
||||
### Basic Crop
|
||||
Extract region from image.
|
||||
|
||||
```bash
|
||||
# Crop width×height+x+y
|
||||
magick input.jpg -crop 400x400+100+100 output.jpg
|
||||
|
||||
# Remove virtual canvas after crop
|
||||
magick input.jpg -crop 400x400+100+100 +repage output.jpg
|
||||
|
||||
# Crop from center
|
||||
magick input.jpg -gravity center -crop 400x400+0+0 output.jpg
|
||||
|
||||
# Crop to aspect ratio
|
||||
magick input.jpg -gravity center -crop 16:9 +repage output.jpg
|
||||
```
|
||||
|
||||
### Smart Crop
|
||||
Content-aware cropping.
|
||||
|
||||
```bash
|
||||
# Trim transparent/same-color borders
|
||||
magick input.png -trim +repage output.png
|
||||
|
||||
# Trim with fuzz tolerance
|
||||
magick input.jpg -fuzz 10% -trim +repage output.jpg
|
||||
```
|
||||
|
||||
### Thumbnail Generation
|
||||
Create square thumbnails from any aspect ratio.
|
||||
|
||||
```bash
|
||||
# Resize and crop to square
|
||||
magick input.jpg -resize 200x200^ -gravity center -extent 200x200 thumb.jpg
|
||||
|
||||
# Alternative method
|
||||
magick input.jpg -thumbnail 200x200^ -gravity center -crop 200x200+0+0 +repage thumb.jpg
|
||||
|
||||
# With background (no crop)
|
||||
magick input.jpg -resize 200x200 -background white -gravity center -extent 200x200 thumb.jpg
|
||||
```
|
||||
|
||||
## Effects & Filters
|
||||
|
||||
### Blur Effects
|
||||
|
||||
```bash
|
||||
# Standard blur (radius 0 = auto)
|
||||
magick input.jpg -blur 0x8 output.jpg
|
||||
|
||||
# Gaussian blur (radius×sigma)
|
||||
magick input.jpg -gaussian-blur 5x3 output.jpg
|
||||
|
||||
# Motion blur (angle)
|
||||
magick input.jpg -motion-blur 0x20+45 output.jpg
|
||||
|
||||
# Radial blur
|
||||
magick input.jpg -radial-blur 10 output.jpg
|
||||
```
|
||||
|
||||
### Sharpen
|
||||
|
||||
```bash
|
||||
# Basic sharpen
|
||||
magick input.jpg -sharpen 0x1 output.jpg
|
||||
|
||||
# Stronger sharpen
|
||||
magick input.jpg -sharpen 0x3 output.jpg
|
||||
|
||||
# Unsharp mask (advanced)
|
||||
magick input.jpg -unsharp 0x1 output.jpg
|
||||
```
|
||||
|
||||
### Color Effects
|
||||
|
||||
```bash
|
||||
# Grayscale
|
||||
magick input.jpg -colorspace Gray output.jpg
|
||||
|
||||
# Sepia tone
|
||||
magick input.jpg -sepia-tone 80% output.jpg
|
||||
|
||||
# Negate (invert colors)
|
||||
magick input.jpg -negate output.jpg
|
||||
|
||||
# Posterize (reduce colors)
|
||||
magick input.jpg -posterize 8 output.jpg
|
||||
|
||||
# Solarize
|
||||
magick input.jpg -solarize 50% output.jpg
|
||||
```
|
||||
|
||||
### Artistic Effects
|
||||
|
||||
```bash
|
||||
# Edge detection
|
||||
magick input.jpg -edge 3 output.jpg
|
||||
|
||||
# Emboss
|
||||
magick input.jpg -emboss 2 output.jpg
|
||||
|
||||
# Oil painting
|
||||
magick input.jpg -paint 4 output.jpg
|
||||
|
||||
# Charcoal drawing
|
||||
magick input.jpg -charcoal 2 output.jpg
|
||||
|
||||
# Sketch
|
||||
magick input.jpg -sketch 0x20+120 output.jpg
|
||||
|
||||
# Swirl
|
||||
magick input.jpg -swirl 90 output.jpg
|
||||
```
|
||||
|
||||
## Adjustments
|
||||
|
||||
### Brightness & Contrast
|
||||
|
||||
```bash
|
||||
# Increase brightness
|
||||
magick input.jpg -brightness-contrast 10x0 output.jpg
|
||||
|
||||
# Increase contrast
|
||||
magick input.jpg -brightness-contrast 0x20 output.jpg
|
||||
|
||||
# Both
|
||||
magick input.jpg -brightness-contrast 10x20 output.jpg
|
||||
|
||||
# Negative values to decrease
|
||||
magick input.jpg -brightness-contrast -10x-10 output.jpg
|
||||
```
|
||||
|
||||
### Color Adjustments
|
||||
|
||||
```bash
|
||||
# Adjust saturation (HSL modulation)
|
||||
# Format: brightness,saturation,hue
|
||||
magick input.jpg -modulate 100,150,100 output.jpg
|
||||
|
||||
# Adjust hue
|
||||
magick input.jpg -modulate 100,100,120 output.jpg
|
||||
|
||||
# Combined adjustments
|
||||
magick input.jpg -modulate 105,120,100 output.jpg
|
||||
|
||||
# Adjust specific color channels
|
||||
magick input.jpg -channel Red -evaluate multiply 1.2 output.jpg
|
||||
```
|
||||
|
||||
### Auto Corrections
|
||||
|
||||
```bash
|
||||
# Auto level (normalize contrast)
|
||||
magick input.jpg -auto-level output.jpg
|
||||
|
||||
# Auto gamma correction
|
||||
magick input.jpg -auto-gamma output.jpg
|
||||
|
||||
# Normalize (stretch histogram)
|
||||
magick input.jpg -normalize output.jpg
|
||||
|
||||
# Enhance (digital enhancement)
|
||||
magick input.jpg -enhance output.jpg
|
||||
|
||||
# Equalize (histogram equalization)
|
||||
magick input.jpg -equalize output.jpg
|
||||
```
|
||||
|
||||
## Transformations
|
||||
|
||||
### Rotation
|
||||
|
||||
```bash
|
||||
# Rotate 90° clockwise
|
||||
magick input.jpg -rotate 90 output.jpg
|
||||
|
||||
# Rotate 180°
|
||||
magick input.jpg -rotate 180 output.jpg
|
||||
|
||||
# Rotate counter-clockwise
|
||||
magick input.jpg -rotate -90 output.jpg
|
||||
|
||||
# Rotate with background
|
||||
magick input.jpg -background white -rotate 45 output.jpg
|
||||
|
||||
# Auto-orient based on EXIF
|
||||
magick input.jpg -auto-orient output.jpg
|
||||
```
|
||||
|
||||
### Flip & Mirror
|
||||
|
||||
```bash
|
||||
# Flip vertically
|
||||
magick input.jpg -flip output.jpg
|
||||
|
||||
# Flip horizontally (mirror)
|
||||
magick input.jpg -flop output.jpg
|
||||
|
||||
# Both
|
||||
magick input.jpg -flip -flop output.jpg
|
||||
```
|
||||
|
||||
## Borders & Frames
|
||||
|
||||
### Simple Borders
|
||||
|
||||
```bash
|
||||
# Add 10px black border
|
||||
magick input.jpg -border 10x10 output.jpg
|
||||
|
||||
# Colored border
|
||||
magick input.jpg -bordercolor red -border 10x10 output.jpg
|
||||
|
||||
# Different width/height
|
||||
magick input.jpg -bordercolor blue -border 20x10 output.jpg
|
||||
```
|
||||
|
||||
### Advanced Frames
|
||||
|
||||
```bash
|
||||
# Raised frame
|
||||
magick input.jpg -mattecolor gray -frame 10x10+5+5 output.jpg
|
||||
|
||||
# Shadow effect
|
||||
magick input.jpg \
|
||||
\( +clone -background black -shadow 80x3+5+5 \) \
|
||||
+swap -background white -layers merge +repage \
|
||||
output.jpg
|
||||
|
||||
# Rounded corners
|
||||
magick input.jpg \
|
||||
\( +clone -threshold -1 -draw "fill black polygon 0,0 0,15 15,0 fill white circle 15,15 15,0" \
|
||||
\( +clone -flip \) -compose multiply -composite \
|
||||
\( +clone -flop \) -compose multiply -composite \
|
||||
\) -alpha off -compose copy_opacity -composite \
|
||||
output.png
|
||||
```
|
||||
|
||||
## Text & Annotations
|
||||
|
||||
### Basic Text
|
||||
|
||||
```bash
|
||||
# Simple text overlay
|
||||
magick input.jpg -pointsize 30 -fill white -annotate +10+30 "Hello" output.jpg
|
||||
|
||||
# Positioned text
|
||||
magick input.jpg -gravity south -pointsize 20 -fill white \
|
||||
-annotate +0+10 "Copyright 2025" output.jpg
|
||||
|
||||
# Text with background
|
||||
magick input.jpg -gravity center -pointsize 40 -fill white \
|
||||
-undercolor black -annotate +0+0 "Watermark" output.jpg
|
||||
```
|
||||
|
||||
### Advanced Text
|
||||
|
||||
```bash
|
||||
# Semi-transparent watermark
|
||||
magick input.jpg \
|
||||
\( -background none -fill "rgba(255,255,255,0.5)" \
|
||||
-pointsize 50 label:"DRAFT" \) \
|
||||
-gravity center -compose over -composite \
|
||||
output.jpg
|
||||
|
||||
# Text with stroke
|
||||
magick input.jpg -gravity center \
|
||||
-stroke black -strokewidth 2 -fill white \
|
||||
-pointsize 60 -annotate +0+0 "Title" \
|
||||
output.jpg
|
||||
|
||||
# Custom font
|
||||
magick input.jpg -font Arial-Bold -pointsize 40 \
|
||||
-gravity center -fill white -annotate +0+0 "Text" \
|
||||
output.jpg
|
||||
```
|
||||
|
||||
## Image Composition
|
||||
|
||||
### Overlay Images
|
||||
|
||||
```bash
|
||||
# Basic overlay (top-left)
|
||||
magick input.jpg overlay.png -composite output.jpg
|
||||
|
||||
# Position with gravity
|
||||
magick input.jpg watermark.png -gravity southeast -composite output.jpg
|
||||
|
||||
# Position with offset
|
||||
magick input.jpg watermark.png -gravity southeast \
|
||||
-geometry +10+10 -composite output.jpg
|
||||
|
||||
# Center overlay
|
||||
magick input.jpg logo.png -gravity center -composite output.jpg
|
||||
```
|
||||
|
||||
### Composite Modes
|
||||
|
||||
```bash
|
||||
# Over (default)
|
||||
magick input.jpg overlay.png -compose over -composite output.jpg
|
||||
|
||||
# Multiply
|
||||
magick input.jpg texture.png -compose multiply -composite output.jpg
|
||||
|
||||
# Screen
|
||||
magick input.jpg light.png -compose screen -composite output.jpg
|
||||
|
||||
# Overlay blend mode
|
||||
magick input.jpg pattern.png -compose overlay -composite output.jpg
|
||||
```
|
||||
|
||||
### Side-by-Side
|
||||
|
||||
```bash
|
||||
# Horizontal append
|
||||
magick image1.jpg image2.jpg +append output.jpg
|
||||
|
||||
# Vertical append
|
||||
magick image1.jpg image2.jpg -append output.jpg
|
||||
|
||||
# With spacing
|
||||
magick image1.jpg image2.jpg -gravity center \
|
||||
-background white -splice 10x0 +append output.jpg
|
||||
```
|
||||
|
||||
## Transparency
|
||||
|
||||
### Create Transparency
|
||||
|
||||
```bash
|
||||
# Make color transparent
|
||||
magick input.jpg -transparent white output.png
|
||||
|
||||
# Make similar colors transparent (with fuzz)
|
||||
magick input.jpg -fuzz 10% -transparent white output.png
|
||||
|
||||
# Alpha channel operations
|
||||
magick input.png -alpha set -channel A -evaluate multiply 0.5 +channel output.png
|
||||
```
|
||||
|
||||
### Remove Transparency
|
||||
|
||||
```bash
|
||||
# Flatten with white background
|
||||
magick input.png -background white -flatten output.jpg
|
||||
|
||||
# Flatten with custom color
|
||||
magick input.png -background "#ff0000" -flatten output.jpg
|
||||
```
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Vignette Effect
|
||||
|
||||
```bash
|
||||
# Default vignette
|
||||
magick input.jpg -vignette 0x20 output.jpg
|
||||
|
||||
# Custom vignette
|
||||
magick input.jpg -background black -vignette 0x25+10+10 output.jpg
|
||||
```
|
||||
|
||||
### Depth of Field Blur
|
||||
|
||||
```bash
|
||||
# Radial blur from center
|
||||
magick input.jpg \
|
||||
\( +clone -blur 0x8 \) \
|
||||
\( +clone -fill white -colorize 100 \
|
||||
-fill black -draw "circle %[fx:w/2],%[fx:h/2] %[fx:w/2],%[fx:h/4]" \
|
||||
-blur 0x20 \) \
|
||||
-composite output.jpg
|
||||
```
|
||||
|
||||
### HDR Effect
|
||||
|
||||
```bash
|
||||
magick input.jpg \
|
||||
\( +clone -colorspace gray \) \
|
||||
\( -clone 0 -auto-level -modulate 100,150,100 \) \
|
||||
-delete 0 -compose overlay -composite \
|
||||
output.jpg
|
||||
```
|
||||
|
||||
### Tilt-Shift Effect
|
||||
|
||||
```bash
|
||||
magick input.jpg \
|
||||
\( +clone -sparse-color Barycentric '0,%[fx:h*0.3] gray0 0,%[fx:h*0.5] white 0,%[fx:h*0.7] gray0' \) \
|
||||
\( +clone -blur 0x20 \) \
|
||||
-compose blend -define compose:args=100 -composite \
|
||||
output.jpg
|
||||
```
|
||||
|
||||
## Color Management
|
||||
|
||||
### Color Profiles
|
||||
|
||||
```bash
|
||||
# Strip color profile
|
||||
magick input.jpg -strip output.jpg
|
||||
|
||||
# Assign color profile
|
||||
magick input.jpg -profile sRGB.icc output.jpg
|
||||
|
||||
# Convert between profiles
|
||||
magick input.jpg -profile AdobeRGB.icc -profile sRGB.icc output.jpg
|
||||
```
|
||||
|
||||
### Color Space Conversion
|
||||
|
||||
```bash
|
||||
# Convert to sRGB
|
||||
magick input.jpg -colorspace sRGB output.jpg
|
||||
|
||||
# Convert to CMYK (print)
|
||||
magick input.jpg -colorspace CMYK output.tif
|
||||
|
||||
# Convert to LAB
|
||||
magick input.jpg -colorspace LAB output.jpg
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memory Management
|
||||
|
||||
```bash
|
||||
# Limit memory usage
|
||||
magick -limit memory 2GB -limit map 4GB input.jpg -resize 50% output.jpg
|
||||
|
||||
# Set thread count
|
||||
magick -limit thread 4 input.jpg -resize 50% output.jpg
|
||||
|
||||
# Streaming for large files
|
||||
magick -define stream:buffer-size=0 huge.jpg -resize 50% output.jpg
|
||||
```
|
||||
|
||||
### Quality vs Size
|
||||
|
||||
```bash
|
||||
# Maximum quality (large file)
|
||||
magick input.jpg -quality 95 output.jpg
|
||||
|
||||
# Balanced (recommended)
|
||||
magick input.jpg -quality 85 -strip output.jpg
|
||||
|
||||
# Smaller file (acceptable quality)
|
||||
magick input.jpg -quality 70 -sampling-factor 4:2:0 -strip output.jpg
|
||||
|
||||
# Progressive JPEG
|
||||
magick input.jpg -quality 85 -interlace Plane -strip output.jpg
|
||||
```
|
||||
|
||||
## Common Recipes
|
||||
|
||||
### Avatar/Profile Picture
|
||||
|
||||
```bash
|
||||
# Square thumbnail
|
||||
magick input.jpg -resize 200x200^ -gravity center -extent 200x200 avatar.jpg
|
||||
|
||||
# Circular avatar (PNG)
|
||||
magick input.jpg -resize 200x200^ -gravity center -extent 200x200 \
|
||||
\( +clone -threshold -1 -negate -fill white -draw "circle 100,100 100,0" \) \
|
||||
-alpha off -compose copy_opacity -composite avatar.png
|
||||
```
|
||||
|
||||
### Responsive Images
|
||||
|
||||
```bash
|
||||
# Generate multiple sizes
|
||||
for size in 320 640 1024 1920; do
|
||||
magick input.jpg -resize ${size}x -quality 85 -strip "output-${size}w.jpg"
|
||||
done
|
||||
```
|
||||
|
||||
### Photo Enhancement
|
||||
|
||||
```bash
|
||||
# Auto-enhance workflow
|
||||
magick input.jpg \
|
||||
-auto-level \
|
||||
-unsharp 0x1 \
|
||||
-brightness-contrast 5x10 \
|
||||
-modulate 100,110,100 \
|
||||
-quality 90 -strip \
|
||||
output.jpg
|
||||
```
|
||||
342
skills/media-processing/scripts/batch_resize.py
Normal file
342
skills/media-processing/scripts/batch_resize.py
Normal file
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Batch image resizing with multiple strategies.
|
||||
|
||||
Supports aspect ratio maintenance, smart cropping, thumbnail generation,
|
||||
watermarks, format conversion, and parallel processing.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
|
||||
class ImageResizer:
|
||||
"""Handle image resizing operations using ImageMagick."""
|
||||
|
||||
def __init__(self, verbose: bool = False, dry_run: bool = False):
|
||||
self.verbose = verbose
|
||||
self.dry_run = dry_run
|
||||
|
||||
def check_imagemagick(self) -> bool:
|
||||
"""Check if ImageMagick is available."""
|
||||
try:
|
||||
subprocess.run(
|
||||
['magick', '-version'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def build_resize_command(
|
||||
self,
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
width: Optional[int],
|
||||
height: Optional[int],
|
||||
strategy: str,
|
||||
quality: int,
|
||||
watermark: Optional[Path] = None
|
||||
) -> List[str]:
|
||||
"""Build ImageMagick resize command based on strategy."""
|
||||
cmd = ['magick', str(input_path)]
|
||||
|
||||
# Apply resize strategy
|
||||
if strategy == 'fit':
|
||||
# Fit within dimensions, maintain aspect ratio
|
||||
geometry = f"{width or ''}x{height or ''}"
|
||||
cmd.extend(['-resize', geometry])
|
||||
|
||||
elif strategy == 'fill':
|
||||
# Fill dimensions, crop excess
|
||||
if not width or not height:
|
||||
raise ValueError("Both width and height required for 'fill' strategy")
|
||||
cmd.extend([
|
||||
'-resize', f'{width}x{height}^',
|
||||
'-gravity', 'center',
|
||||
'-extent', f'{width}x{height}'
|
||||
])
|
||||
|
||||
elif strategy == 'cover':
|
||||
# Cover dimensions, may exceed
|
||||
if not width or not height:
|
||||
raise ValueError("Both width and height required for 'cover' strategy")
|
||||
cmd.extend(['-resize', f'{width}x{height}^'])
|
||||
|
||||
elif strategy == 'exact':
|
||||
# Force exact dimensions, ignore aspect ratio
|
||||
if not width or not height:
|
||||
raise ValueError("Both width and height required for 'exact' strategy")
|
||||
cmd.extend(['-resize', f'{width}x{height}!'])
|
||||
|
||||
elif strategy == 'thumbnail':
|
||||
# Create square thumbnail
|
||||
size = width or height or 200
|
||||
cmd.extend([
|
||||
'-resize', f'{size}x{size}^',
|
||||
'-gravity', 'center',
|
||||
'-extent', f'{size}x{size}'
|
||||
])
|
||||
|
||||
# Add watermark if specified
|
||||
if watermark:
|
||||
cmd.extend([
|
||||
str(watermark),
|
||||
'-gravity', 'southeast',
|
||||
'-geometry', '+10+10',
|
||||
'-composite'
|
||||
])
|
||||
|
||||
# Output settings
|
||||
cmd.extend([
|
||||
'-quality', str(quality),
|
||||
'-strip',
|
||||
str(output_path)
|
||||
])
|
||||
|
||||
return cmd
|
||||
|
||||
def resize_image(
|
||||
self,
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
width: Optional[int],
|
||||
height: Optional[int],
|
||||
strategy: str = 'fit',
|
||||
quality: int = 85,
|
||||
watermark: Optional[Path] = None
|
||||
) -> bool:
|
||||
"""Resize a single image."""
|
||||
try:
|
||||
# Ensure output directory exists
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cmd = self.build_resize_command(
|
||||
input_path, output_path, width, height,
|
||||
strategy, quality, watermark
|
||||
)
|
||||
|
||||
if self.verbose or self.dry_run:
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
if self.dry_run:
|
||||
return True
|
||||
|
||||
subprocess.run(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE if not self.verbose else None,
|
||||
stderr=subprocess.PIPE if not self.verbose else None,
|
||||
check=True
|
||||
)
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error resizing {input_path}: {e}", file=sys.stderr)
|
||||
if not self.verbose and e.stderr:
|
||||
print(e.stderr.decode(), file=sys.stderr)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error processing {input_path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def batch_resize(
|
||||
self,
|
||||
input_paths: List[Path],
|
||||
output_dir: Path,
|
||||
width: Optional[int],
|
||||
height: Optional[int],
|
||||
strategy: str = 'fit',
|
||||
quality: int = 85,
|
||||
format_ext: Optional[str] = None,
|
||||
watermark: Optional[Path] = None,
|
||||
parallel: int = 1
|
||||
) -> Tuple[int, int]:
|
||||
"""Resize multiple images."""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
def process_image(input_path: Path) -> Tuple[Path, bool]:
|
||||
"""Process single image for parallel execution."""
|
||||
if not input_path.exists() or not input_path.is_file():
|
||||
return input_path, False
|
||||
|
||||
# Determine output path
|
||||
output_name = input_path.stem
|
||||
if format_ext:
|
||||
output_path = output_dir / f"{output_name}.{format_ext.lstrip('.')}"
|
||||
else:
|
||||
output_path = output_dir / input_path.name
|
||||
|
||||
if not self.dry_run:
|
||||
print(f"Processing {input_path.name} -> {output_path.name}")
|
||||
|
||||
success = self.resize_image(
|
||||
input_path, output_path, width, height,
|
||||
strategy, quality, watermark
|
||||
)
|
||||
|
||||
return input_path, success
|
||||
|
||||
# Process images
|
||||
if parallel > 1:
|
||||
with ThreadPoolExecutor(max_workers=parallel) as executor:
|
||||
futures = [executor.submit(process_image, path) for path in input_paths]
|
||||
|
||||
for future in as_completed(futures):
|
||||
_, success = future.result()
|
||||
if success:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
else:
|
||||
for input_path in input_paths:
|
||||
_, success = process_image(input_path)
|
||||
if success:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
return success_count, fail_count
|
||||
|
||||
|
||||
def collect_images(paths: List[Path], recursive: bool = False) -> List[Path]:
|
||||
"""Collect image files from paths."""
|
||||
image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'}
|
||||
images = []
|
||||
|
||||
for path in paths:
|
||||
if path.is_file() and path.suffix.lower() in image_exts:
|
||||
images.append(path)
|
||||
elif path.is_dir():
|
||||
pattern = '**/*' if recursive else '*'
|
||||
for img_path in path.glob(pattern):
|
||||
if img_path.is_file() and img_path.suffix.lower() in image_exts:
|
||||
images.append(img_path)
|
||||
|
||||
return images
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Batch image resizing with multiple strategies.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'inputs',
|
||||
nargs='+',
|
||||
type=Path,
|
||||
help='Input image(s) or directory'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o', '--output',
|
||||
type=Path,
|
||||
required=True,
|
||||
help='Output directory'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-w', '--width',
|
||||
type=int,
|
||||
help='Target width in pixels'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-h', '--height',
|
||||
type=int,
|
||||
dest='img_height',
|
||||
help='Target height in pixels'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--strategy',
|
||||
choices=['fit', 'fill', 'cover', 'exact', 'thumbnail'],
|
||||
default='fit',
|
||||
help='Resize strategy (default: fit)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-q', '--quality',
|
||||
type=int,
|
||||
default=85,
|
||||
help='Output quality 0-100 (default: 85)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-f', '--format',
|
||||
help='Output format (e.g., jpg, png, webp)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-wm', '--watermark',
|
||||
type=Path,
|
||||
help='Watermark image to overlay'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-p', '--parallel',
|
||||
type=int,
|
||||
default=1,
|
||||
help='Number of parallel processes (default: 1)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r', '--recursive',
|
||||
action='store_true',
|
||||
help='Process directories recursively'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n', '--dry-run',
|
||||
action='store_true',
|
||||
help='Show commands without executing'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help='Verbose output'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate dimensions
|
||||
if not args.width and not args.img_height:
|
||||
print("Error: At least one of --width or --height required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize resizer
|
||||
resizer = ImageResizer(verbose=args.verbose, dry_run=args.dry_run)
|
||||
|
||||
# Check dependencies
|
||||
if not resizer.check_imagemagick():
|
||||
print("Error: ImageMagick not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Collect input images
|
||||
images = collect_images(args.inputs, args.recursive)
|
||||
|
||||
if not images:
|
||||
print("Error: No images found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Found {len(images)} image(s) to process")
|
||||
|
||||
# Create output directory
|
||||
if not args.dry_run:
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Process images
|
||||
success, fail = resizer.batch_resize(
|
||||
images,
|
||||
args.output,
|
||||
args.width,
|
||||
args.img_height,
|
||||
args.strategy,
|
||||
args.quality,
|
||||
args.format,
|
||||
args.watermark,
|
||||
args.parallel
|
||||
)
|
||||
|
||||
print(f"\nResults: {success} succeeded, {fail} failed")
|
||||
sys.exit(0 if fail == 0 else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
311
skills/media-processing/scripts/media_convert.py
Normal file
311
skills/media-processing/scripts/media_convert.py
Normal file
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unified media conversion tool for video, audio, and images.
|
||||
|
||||
Auto-detects format and applies appropriate tool (FFmpeg or ImageMagick).
|
||||
Supports quality presets, batch processing, and dry-run mode.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
|
||||
# Format mappings
|
||||
VIDEO_FORMATS = {'.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.m4v'}
|
||||
AUDIO_FORMATS = {'.mp3', '.aac', '.m4a', '.opus', '.flac', '.wav', '.ogg'}
|
||||
IMAGE_FORMATS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'}
|
||||
|
||||
# Quality presets
|
||||
QUALITY_PRESETS = {
|
||||
'web': {
|
||||
'video_crf': 23,
|
||||
'video_preset': 'medium',
|
||||
'audio_bitrate': '128k',
|
||||
'image_quality': 85
|
||||
},
|
||||
'archive': {
|
||||
'video_crf': 18,
|
||||
'video_preset': 'slow',
|
||||
'audio_bitrate': '192k',
|
||||
'image_quality': 95
|
||||
},
|
||||
'mobile': {
|
||||
'video_crf': 26,
|
||||
'video_preset': 'fast',
|
||||
'audio_bitrate': '96k',
|
||||
'image_quality': 80
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def check_dependencies() -> Tuple[bool, bool]:
|
||||
"""Check if ffmpeg and imagemagick are available."""
|
||||
ffmpeg_available = subprocess.run(
|
||||
['ffmpeg', '-version'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
).returncode == 0
|
||||
|
||||
magick_available = subprocess.run(
|
||||
['magick', '-version'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
).returncode == 0
|
||||
|
||||
return ffmpeg_available, magick_available
|
||||
|
||||
|
||||
def detect_media_type(file_path: Path) -> str:
|
||||
"""Detect media type from file extension."""
|
||||
ext = file_path.suffix.lower()
|
||||
|
||||
if ext in VIDEO_FORMATS:
|
||||
return 'video'
|
||||
elif ext in AUDIO_FORMATS:
|
||||
return 'audio'
|
||||
elif ext in IMAGE_FORMATS:
|
||||
return 'image'
|
||||
else:
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def build_video_command(
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
preset: str = 'web'
|
||||
) -> List[str]:
|
||||
"""Build FFmpeg command for video conversion."""
|
||||
quality = QUALITY_PRESETS[preset]
|
||||
|
||||
return [
|
||||
'ffmpeg', '-i', str(input_path),
|
||||
'-c:v', 'libx264',
|
||||
'-preset', quality['video_preset'],
|
||||
'-crf', str(quality['video_crf']),
|
||||
'-c:a', 'aac',
|
||||
'-b:a', quality['audio_bitrate'],
|
||||
'-movflags', '+faststart',
|
||||
'-y',
|
||||
str(output_path)
|
||||
]
|
||||
|
||||
|
||||
def build_audio_command(
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
preset: str = 'web'
|
||||
) -> List[str]:
|
||||
"""Build FFmpeg command for audio conversion."""
|
||||
quality = QUALITY_PRESETS[preset]
|
||||
output_ext = output_path.suffix.lower()
|
||||
|
||||
codec_map = {
|
||||
'.mp3': 'libmp3lame',
|
||||
'.aac': 'aac',
|
||||
'.m4a': 'aac',
|
||||
'.opus': 'libopus',
|
||||
'.flac': 'flac',
|
||||
'.wav': 'pcm_s16le',
|
||||
'.ogg': 'libvorbis'
|
||||
}
|
||||
|
||||
codec = codec_map.get(output_ext, 'aac')
|
||||
|
||||
cmd = ['ffmpeg', '-i', str(input_path), '-c:a', codec]
|
||||
|
||||
# Add bitrate for lossy codecs
|
||||
if codec not in ['flac', 'pcm_s16le']:
|
||||
cmd.extend(['-b:a', quality['audio_bitrate']])
|
||||
|
||||
cmd.extend(['-y', str(output_path)])
|
||||
return cmd
|
||||
|
||||
|
||||
def build_image_command(
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
preset: str = 'web'
|
||||
) -> List[str]:
|
||||
"""Build ImageMagick command for image conversion."""
|
||||
quality = QUALITY_PRESETS[preset]
|
||||
|
||||
return [
|
||||
'magick', str(input_path),
|
||||
'-quality', str(quality['image_quality']),
|
||||
'-strip',
|
||||
str(output_path)
|
||||
]
|
||||
|
||||
|
||||
def convert_file(
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
preset: str = 'web',
|
||||
dry_run: bool = False,
|
||||
verbose: bool = False
|
||||
) -> bool:
|
||||
"""Convert a single media file."""
|
||||
media_type = detect_media_type(input_path)
|
||||
|
||||
if media_type == 'unknown':
|
||||
print(f"Error: Unsupported format for {input_path}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Ensure output directory exists
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Build command based on media type
|
||||
if media_type == 'video':
|
||||
cmd = build_video_command(input_path, output_path, preset)
|
||||
elif media_type == 'audio':
|
||||
cmd = build_audio_command(input_path, output_path, preset)
|
||||
else: # image
|
||||
cmd = build_image_command(input_path, output_path, preset)
|
||||
|
||||
if verbose or dry_run:
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
if dry_run:
|
||||
return True
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE if not verbose else None,
|
||||
stderr=subprocess.PIPE if not verbose else None,
|
||||
check=True
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error converting {input_path}: {e}", file=sys.stderr)
|
||||
if not verbose and e.stderr:
|
||||
print(e.stderr.decode(), file=sys.stderr)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error converting {input_path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def batch_convert(
|
||||
input_paths: List[Path],
|
||||
output_dir: Optional[Path] = None,
|
||||
output_format: Optional[str] = None,
|
||||
preset: str = 'web',
|
||||
dry_run: bool = False,
|
||||
verbose: bool = False
|
||||
) -> Tuple[int, int]:
|
||||
"""Convert multiple files."""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
for input_path in input_paths:
|
||||
if not input_path.exists():
|
||||
print(f"Error: {input_path} not found", file=sys.stderr)
|
||||
fail_count += 1
|
||||
continue
|
||||
|
||||
# Determine output path
|
||||
if output_dir:
|
||||
output_name = input_path.stem
|
||||
if output_format:
|
||||
output_path = output_dir / f"{output_name}.{output_format.lstrip('.')}"
|
||||
else:
|
||||
output_path = output_dir / input_path.name
|
||||
else:
|
||||
if output_format:
|
||||
output_path = input_path.with_suffix(f".{output_format.lstrip('.')}")
|
||||
else:
|
||||
print(f"Error: No output format specified for {input_path}", file=sys.stderr)
|
||||
fail_count += 1
|
||||
continue
|
||||
|
||||
print(f"Converting {input_path.name} -> {output_path.name}")
|
||||
|
||||
if convert_file(input_path, output_path, preset, dry_run, verbose):
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
return success_count, fail_count
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Unified media conversion tool for video, audio, and images.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'inputs',
|
||||
nargs='+',
|
||||
type=Path,
|
||||
help='Input file(s) to convert'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o', '--output',
|
||||
type=Path,
|
||||
help='Output file or directory for batch conversion'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-f', '--format',
|
||||
help='Output format (e.g., mp4, jpg, mp3)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-p', '--preset',
|
||||
choices=['web', 'archive', 'mobile'],
|
||||
default='web',
|
||||
help='Quality preset (default: web)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n', '--dry-run',
|
||||
action='store_true',
|
||||
help='Show commands without executing'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help='Verbose output'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check dependencies
|
||||
ffmpeg_ok, magick_ok = check_dependencies()
|
||||
if not ffmpeg_ok and not magick_ok:
|
||||
print("Error: Neither ffmpeg nor imagemagick found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Handle single file vs batch conversion
|
||||
if len(args.inputs) == 1 and args.output and not args.output.is_dir():
|
||||
# Single file conversion
|
||||
success = convert_file(
|
||||
args.inputs[0],
|
||||
args.output,
|
||||
args.preset,
|
||||
args.dry_run,
|
||||
args.verbose
|
||||
)
|
||||
sys.exit(0 if success else 1)
|
||||
else:
|
||||
# Batch conversion
|
||||
output_dir = args.output if args.output else Path.cwd()
|
||||
if not args.output:
|
||||
output_dir = None # Will convert in place with new format
|
||||
|
||||
success, fail = batch_convert(
|
||||
args.inputs,
|
||||
output_dir,
|
||||
args.format,
|
||||
args.preset,
|
||||
args.dry_run,
|
||||
args.verbose
|
||||
)
|
||||
|
||||
print(f"\nResults: {success} succeeded, {fail} failed")
|
||||
sys.exit(0 if fail == 0 else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
24
skills/media-processing/scripts/requirements.txt
Normal file
24
skills/media-processing/scripts/requirements.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
# Media Processing Skill Dependencies
|
||||
# Python 3.10+ required
|
||||
|
||||
# No Python package dependencies - uses system binaries
|
||||
# Required system tools (install separately):
|
||||
# - FFmpeg (video/audio processing)
|
||||
# - ImageMagick (image processing)
|
||||
|
||||
# Testing dependencies (dev)
|
||||
pytest>=8.0.0
|
||||
pytest-cov>=4.1.0
|
||||
pytest-mock>=3.12.0
|
||||
|
||||
# Installation instructions:
|
||||
#
|
||||
# Ubuntu/Debian:
|
||||
# sudo apt-get install ffmpeg imagemagick
|
||||
#
|
||||
# macOS (Homebrew):
|
||||
# brew install ffmpeg imagemagick
|
||||
#
|
||||
# Windows:
|
||||
# choco install ffmpeg imagemagick
|
||||
# or download from official websites
|
||||
2
skills/media-processing/scripts/tests/requirements.txt
Normal file
2
skills/media-processing/scripts/tests/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pytest>=7.4.0
|
||||
pytest-cov>=4.1.0
|
||||
372
skills/media-processing/scripts/tests/test_batch_resize.py
Normal file
372
skills/media-processing/scripts/tests/test_batch_resize.py
Normal file
@@ -0,0 +1,372 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for batch_resize.py"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from batch_resize import ImageResizer, collect_images
|
||||
|
||||
|
||||
class TestImageResizer:
|
||||
"""Test ImageResizer class."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.resizer = ImageResizer(verbose=False, dry_run=False)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_imagemagick_available(self, mock_run):
|
||||
"""Test ImageMagick availability check."""
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
assert self.resizer.check_imagemagick() is True
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_imagemagick_unavailable(self, mock_run):
|
||||
"""Test when ImageMagick is not available."""
|
||||
mock_run.side_effect = FileNotFoundError()
|
||||
assert self.resizer.check_imagemagick() is False
|
||||
|
||||
def test_build_resize_command_fit_strategy(self):
|
||||
"""Test command building for 'fit' strategy."""
|
||||
cmd = self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=600,
|
||||
strategy="fit",
|
||||
quality=85
|
||||
)
|
||||
|
||||
assert "magick" in cmd
|
||||
assert str(Path("input.jpg")) in cmd
|
||||
assert "-resize" in cmd
|
||||
assert "800x600" in cmd
|
||||
assert "-quality" in cmd
|
||||
assert "85" in cmd
|
||||
assert "-strip" in cmd
|
||||
|
||||
def test_build_resize_command_fill_strategy(self):
|
||||
"""Test command building for 'fill' strategy."""
|
||||
cmd = self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=600,
|
||||
strategy="fill",
|
||||
quality=85
|
||||
)
|
||||
|
||||
assert "-resize" in cmd
|
||||
assert "800x600^" in cmd
|
||||
assert "-gravity" in cmd
|
||||
assert "center" in cmd
|
||||
assert "-extent" in cmd
|
||||
|
||||
def test_build_resize_command_thumbnail_strategy(self):
|
||||
"""Test command building for 'thumbnail' strategy."""
|
||||
cmd = self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=200,
|
||||
height=None,
|
||||
strategy="thumbnail",
|
||||
quality=85
|
||||
)
|
||||
|
||||
assert "200x200^" in cmd
|
||||
assert "-gravity" in cmd
|
||||
assert "center" in cmd
|
||||
|
||||
def test_build_resize_command_with_watermark(self):
|
||||
"""Test command building with watermark."""
|
||||
watermark = Path("watermark.png")
|
||||
cmd = self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=None,
|
||||
strategy="fit",
|
||||
quality=85,
|
||||
watermark=watermark
|
||||
)
|
||||
|
||||
assert str(watermark) in cmd
|
||||
assert "-gravity" in cmd
|
||||
assert "southeast" in cmd
|
||||
assert "-composite" in cmd
|
||||
|
||||
def test_build_resize_command_exact_strategy(self):
|
||||
"""Test command building for 'exact' strategy."""
|
||||
cmd = self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=600,
|
||||
strategy="exact",
|
||||
quality=85
|
||||
)
|
||||
|
||||
assert "800x600!" in cmd
|
||||
|
||||
def test_build_resize_command_fill_requires_dimensions(self):
|
||||
"""Test that 'fill' strategy requires both dimensions."""
|
||||
with pytest.raises(ValueError):
|
||||
self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=None,
|
||||
strategy="fill",
|
||||
quality=85
|
||||
)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_resize_image_success(self, mock_run):
|
||||
"""Test successful image resize."""
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
|
||||
result = self.resizer.resize_image(
|
||||
Path("input.jpg"),
|
||||
Path("output/output.jpg"),
|
||||
width=800,
|
||||
height=None,
|
||||
strategy="fit",
|
||||
quality=85
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_resize_image_dry_run(self, mock_run):
|
||||
"""Test resize in dry-run mode."""
|
||||
resizer = ImageResizer(dry_run=True)
|
||||
|
||||
result = resizer.resize_image(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=None
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_not_called()
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_resize_image_failure(self, mock_run):
|
||||
"""Test resize failure handling."""
|
||||
mock_run.side_effect = Exception("Resize failed")
|
||||
|
||||
result = self.resizer.resize_image(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=None
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestCollectImages:
|
||||
"""Test image collection functionality."""
|
||||
|
||||
def test_collect_images_from_file(self, tmp_path):
|
||||
"""Test collecting a single image file."""
|
||||
img_file = tmp_path / "test.jpg"
|
||||
img_file.touch()
|
||||
|
||||
images = collect_images([img_file])
|
||||
assert len(images) == 1
|
||||
assert images[0] == img_file
|
||||
|
||||
def test_collect_images_from_directory(self, tmp_path):
|
||||
"""Test collecting images from directory."""
|
||||
(tmp_path / "image1.jpg").touch()
|
||||
(tmp_path / "image2.png").touch()
|
||||
(tmp_path / "text.txt").touch()
|
||||
|
||||
images = collect_images([tmp_path])
|
||||
assert len(images) == 2
|
||||
assert all(img.suffix.lower() in {'.jpg', '.png'} for img in images)
|
||||
|
||||
def test_collect_images_recursive(self, tmp_path):
|
||||
"""Test recursive image collection."""
|
||||
subdir = tmp_path / "subdir"
|
||||
subdir.mkdir()
|
||||
(tmp_path / "image1.jpg").touch()
|
||||
(subdir / "image2.jpg").touch()
|
||||
|
||||
images = collect_images([tmp_path], recursive=True)
|
||||
assert len(images) == 2
|
||||
|
||||
images_non_recursive = collect_images([tmp_path], recursive=False)
|
||||
assert len(images_non_recursive) == 1
|
||||
|
||||
def test_collect_images_filters_extensions(self, tmp_path):
|
||||
"""Test that only image files are collected."""
|
||||
(tmp_path / "image.jpg").touch()
|
||||
(tmp_path / "doc.pdf").touch()
|
||||
(tmp_path / "text.txt").touch()
|
||||
|
||||
images = collect_images([tmp_path])
|
||||
assert len(images) == 1
|
||||
assert images[0].suffix.lower() == '.jpg'
|
||||
|
||||
def test_collect_images_multiple_paths(self, tmp_path):
|
||||
"""Test collecting from multiple paths."""
|
||||
dir1 = tmp_path / "dir1"
|
||||
dir2 = tmp_path / "dir2"
|
||||
dir1.mkdir()
|
||||
dir2.mkdir()
|
||||
|
||||
(dir1 / "image1.jpg").touch()
|
||||
(dir2 / "image2.png").touch()
|
||||
|
||||
images = collect_images([dir1, dir2])
|
||||
assert len(images) == 2
|
||||
|
||||
|
||||
class TestBatchResize:
|
||||
"""Test batch resize functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.resizer = ImageResizer(verbose=False, dry_run=False)
|
||||
|
||||
@patch.object(ImageResizer, "resize_image")
|
||||
def test_batch_resize_success(self, mock_resize, tmp_path):
|
||||
"""Test successful batch resize."""
|
||||
mock_resize.return_value = True
|
||||
|
||||
input_images = [
|
||||
tmp_path / "image1.jpg",
|
||||
tmp_path / "image2.jpg"
|
||||
]
|
||||
for img in input_images:
|
||||
img.touch()
|
||||
|
||||
output_dir = tmp_path / "output"
|
||||
|
||||
success, fail = self.resizer.batch_resize(
|
||||
input_images,
|
||||
output_dir,
|
||||
width=800,
|
||||
height=None,
|
||||
strategy="fit"
|
||||
)
|
||||
|
||||
assert success == 2
|
||||
assert fail == 0
|
||||
assert mock_resize.call_count == 2
|
||||
|
||||
@patch.object(ImageResizer, "resize_image")
|
||||
def test_batch_resize_with_failures(self, mock_resize, tmp_path):
|
||||
"""Test batch resize with some failures."""
|
||||
mock_resize.side_effect = [True, False, True]
|
||||
|
||||
input_images = [
|
||||
tmp_path / "image1.jpg",
|
||||
tmp_path / "image2.jpg",
|
||||
tmp_path / "image3.jpg"
|
||||
]
|
||||
for img in input_images:
|
||||
img.touch()
|
||||
|
||||
output_dir = tmp_path / "output"
|
||||
|
||||
success, fail = self.resizer.batch_resize(
|
||||
input_images,
|
||||
output_dir,
|
||||
width=800,
|
||||
height=None
|
||||
)
|
||||
|
||||
assert success == 2
|
||||
assert fail == 1
|
||||
|
||||
@patch.object(ImageResizer, "resize_image")
|
||||
def test_batch_resize_format_conversion(self, mock_resize, tmp_path):
|
||||
"""Test batch resize with format conversion."""
|
||||
mock_resize.return_value = True
|
||||
|
||||
input_images = [tmp_path / "image.png"]
|
||||
input_images[0].touch()
|
||||
|
||||
output_dir = tmp_path / "output"
|
||||
|
||||
self.resizer.batch_resize(
|
||||
input_images,
|
||||
output_dir,
|
||||
width=800,
|
||||
height=None,
|
||||
format_ext="jpg"
|
||||
)
|
||||
|
||||
# Check that resize_image was called with .jpg extension
|
||||
call_args = mock_resize.call_args[0]
|
||||
assert call_args[1].suffix == ".jpg"
|
||||
|
||||
|
||||
class TestResizeStrategies:
|
||||
"""Test different resize strategies."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.resizer = ImageResizer()
|
||||
|
||||
def test_fit_strategy_maintains_aspect(self):
|
||||
"""Test that 'fit' strategy maintains aspect ratio."""
|
||||
cmd = self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=600,
|
||||
strategy="fit",
|
||||
quality=85
|
||||
)
|
||||
|
||||
# Should have resize without ^ or !
|
||||
resize_idx = cmd.index("-resize")
|
||||
geometry = cmd[resize_idx + 1]
|
||||
assert "^" not in geometry
|
||||
assert "!" not in geometry
|
||||
|
||||
def test_cover_strategy_fills_dimensions(self):
|
||||
"""Test that 'cover' strategy fills dimensions."""
|
||||
cmd = self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=600,
|
||||
strategy="cover",
|
||||
quality=85
|
||||
)
|
||||
|
||||
resize_idx = cmd.index("-resize")
|
||||
geometry = cmd[resize_idx + 1]
|
||||
assert "^" in geometry
|
||||
|
||||
def test_exact_strategy_ignores_aspect(self):
|
||||
"""Test that 'exact' strategy ignores aspect ratio."""
|
||||
cmd = self.resizer.build_resize_command(
|
||||
Path("input.jpg"),
|
||||
Path("output.jpg"),
|
||||
width=800,
|
||||
height=600,
|
||||
strategy="exact",
|
||||
quality=85
|
||||
)
|
||||
|
||||
resize_idx = cmd.index("-resize")
|
||||
geometry = cmd[resize_idx + 1]
|
||||
assert "!" in geometry
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
259
skills/media-processing/scripts/tests/test_media_convert.py
Normal file
259
skills/media-processing/scripts/tests/test_media_convert.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for media_convert.py"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from media_convert import (
|
||||
build_audio_command,
|
||||
build_image_command,
|
||||
build_video_command,
|
||||
check_dependencies,
|
||||
convert_file,
|
||||
detect_media_type,
|
||||
)
|
||||
|
||||
|
||||
class TestMediaTypeDetection:
|
||||
"""Test media type detection."""
|
||||
|
||||
def test_detect_video_formats(self):
|
||||
"""Test video format detection."""
|
||||
assert detect_media_type(Path("test.mp4")) == "video"
|
||||
assert detect_media_type(Path("test.mkv")) == "video"
|
||||
assert detect_media_type(Path("test.avi")) == "video"
|
||||
assert detect_media_type(Path("test.mov")) == "video"
|
||||
|
||||
def test_detect_audio_formats(self):
|
||||
"""Test audio format detection."""
|
||||
assert detect_media_type(Path("test.mp3")) == "audio"
|
||||
assert detect_media_type(Path("test.aac")) == "audio"
|
||||
assert detect_media_type(Path("test.flac")) == "audio"
|
||||
assert detect_media_type(Path("test.wav")) == "audio"
|
||||
|
||||
def test_detect_image_formats(self):
|
||||
"""Test image format detection."""
|
||||
assert detect_media_type(Path("test.jpg")) == "image"
|
||||
assert detect_media_type(Path("test.png")) == "image"
|
||||
assert detect_media_type(Path("test.gif")) == "image"
|
||||
assert detect_media_type(Path("test.webp")) == "image"
|
||||
|
||||
def test_detect_unknown_format(self):
|
||||
"""Test unknown format detection."""
|
||||
assert detect_media_type(Path("test.txt")) == "unknown"
|
||||
assert detect_media_type(Path("test.doc")) == "unknown"
|
||||
|
||||
def test_case_insensitive(self):
|
||||
"""Test case-insensitive detection."""
|
||||
assert detect_media_type(Path("TEST.MP4")) == "video"
|
||||
assert detect_media_type(Path("TEST.JPG")) == "image"
|
||||
|
||||
|
||||
class TestCommandBuilding:
|
||||
"""Test command building functions."""
|
||||
|
||||
def test_build_video_command_web_preset(self):
|
||||
"""Test video command with web preset."""
|
||||
cmd = build_video_command(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
preset="web"
|
||||
)
|
||||
|
||||
assert "ffmpeg" in cmd
|
||||
assert "-i" in cmd
|
||||
assert str(Path("input.mp4")) in cmd
|
||||
assert "-c:v" in cmd
|
||||
assert "libx264" in cmd
|
||||
assert "-crf" in cmd
|
||||
assert "23" in cmd
|
||||
assert "-preset" in cmd
|
||||
assert "medium" in cmd
|
||||
assert str(Path("output.mp4")) in cmd
|
||||
|
||||
def test_build_video_command_archive_preset(self):
|
||||
"""Test video command with archive preset."""
|
||||
cmd = build_video_command(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
preset="archive"
|
||||
)
|
||||
|
||||
assert "18" in cmd # CRF for archive
|
||||
assert "slow" in cmd # Preset for archive
|
||||
|
||||
def test_build_audio_command_mp3(self):
|
||||
"""Test audio command for MP3 output."""
|
||||
cmd = build_audio_command(
|
||||
Path("input.wav"),
|
||||
Path("output.mp3"),
|
||||
preset="web"
|
||||
)
|
||||
|
||||
assert "ffmpeg" in cmd
|
||||
assert "-c:a" in cmd
|
||||
assert "libmp3lame" in cmd
|
||||
assert "-b:a" in cmd
|
||||
|
||||
def test_build_audio_command_flac(self):
|
||||
"""Test audio command for FLAC (lossless)."""
|
||||
cmd = build_audio_command(
|
||||
Path("input.wav"),
|
||||
Path("output.flac"),
|
||||
preset="web"
|
||||
)
|
||||
|
||||
assert "flac" in cmd
|
||||
assert "-b:a" not in cmd # No bitrate for lossless
|
||||
|
||||
def test_build_image_command(self):
|
||||
"""Test image command building."""
|
||||
cmd = build_image_command(
|
||||
Path("input.png"),
|
||||
Path("output.jpg"),
|
||||
preset="web"
|
||||
)
|
||||
|
||||
assert "magick" in cmd
|
||||
assert str(Path("input.png")) in cmd
|
||||
assert "-quality" in cmd
|
||||
assert "85" in cmd
|
||||
assert "-strip" in cmd
|
||||
assert str(Path("output.jpg")) in cmd
|
||||
|
||||
|
||||
class TestDependencyCheck:
|
||||
"""Test dependency checking."""
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_dependencies_both_available(self, mock_run):
|
||||
"""Test when both tools are available."""
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
ffmpeg_ok, magick_ok = check_dependencies()
|
||||
assert ffmpeg_ok is True
|
||||
assert magick_ok is True
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_dependencies_ffmpeg_only(self, mock_run):
|
||||
"""Test when only FFmpeg is available."""
|
||||
def side_effect(*args, **kwargs):
|
||||
if "ffmpeg" in args[0]:
|
||||
return MagicMock(returncode=0)
|
||||
return MagicMock(returncode=1)
|
||||
|
||||
mock_run.side_effect = side_effect
|
||||
ffmpeg_ok, magick_ok = check_dependencies()
|
||||
assert ffmpeg_ok is True
|
||||
assert magick_ok is False
|
||||
|
||||
|
||||
class TestFileConversion:
|
||||
"""Test file conversion functionality."""
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("media_convert.detect_media_type")
|
||||
def test_convert_video_file_dry_run(self, mock_detect, mock_run):
|
||||
"""Test video conversion in dry-run mode."""
|
||||
mock_detect.return_value = "video"
|
||||
|
||||
result = convert_file(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
preset="web",
|
||||
dry_run=True
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_not_called()
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("media_convert.detect_media_type")
|
||||
def test_convert_image_file_success(self, mock_detect, mock_run):
|
||||
"""Test successful image conversion."""
|
||||
mock_detect.return_value = "image"
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
|
||||
result = convert_file(
|
||||
Path("input.png"),
|
||||
Path("output.jpg"),
|
||||
preset="web"
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("media_convert.detect_media_type")
|
||||
def test_convert_file_error(self, mock_detect, mock_run):
|
||||
"""Test conversion error handling."""
|
||||
mock_detect.return_value = "video"
|
||||
mock_run.side_effect = Exception("Conversion failed")
|
||||
|
||||
result = convert_file(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4")
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
@patch("media_convert.detect_media_type")
|
||||
def test_convert_unknown_format(self, mock_detect):
|
||||
"""Test conversion with unknown format."""
|
||||
mock_detect.return_value = "unknown"
|
||||
|
||||
result = convert_file(
|
||||
Path("input.txt"),
|
||||
Path("output.txt")
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestQualityPresets:
|
||||
"""Test quality preset functionality."""
|
||||
|
||||
def test_web_preset_settings(self):
|
||||
"""Test web preset values."""
|
||||
cmd = build_video_command(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
preset="web"
|
||||
)
|
||||
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "23" in cmd_str # CRF
|
||||
assert "128k" in cmd_str # Audio bitrate
|
||||
|
||||
def test_archive_preset_settings(self):
|
||||
"""Test archive preset values."""
|
||||
cmd = build_video_command(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
preset="archive"
|
||||
)
|
||||
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "18" in cmd_str # Higher quality CRF
|
||||
assert "192k" in cmd_str # Higher audio bitrate
|
||||
|
||||
def test_mobile_preset_settings(self):
|
||||
"""Test mobile preset values."""
|
||||
cmd = build_video_command(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
preset="mobile"
|
||||
)
|
||||
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "26" in cmd_str # Lower quality CRF
|
||||
assert "96k" in cmd_str # Lower audio bitrate
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
397
skills/media-processing/scripts/tests/test_video_optimize.py
Normal file
397
skills/media-processing/scripts/tests/test_video_optimize.py
Normal file
@@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for video_optimize.py"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from video_optimize import VideoInfo, VideoOptimizer
|
||||
|
||||
|
||||
class TestVideoOptimizer:
|
||||
"""Test VideoOptimizer class."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.optimizer = VideoOptimizer(verbose=False, dry_run=False)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_ffmpeg_available(self, mock_run):
|
||||
"""Test FFmpeg availability check."""
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
assert self.optimizer.check_ffmpeg() is True
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_ffmpeg_unavailable(self, mock_run):
|
||||
"""Test when FFmpeg is not available."""
|
||||
mock_run.side_effect = FileNotFoundError()
|
||||
assert self.optimizer.check_ffmpeg() is False
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_get_video_info_success(self, mock_run):
|
||||
"""Test successful video info extraction."""
|
||||
mock_data = {
|
||||
"streams": [
|
||||
{
|
||||
"codec_type": "video",
|
||||
"codec_name": "h264",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"r_frame_rate": "30/1"
|
||||
},
|
||||
{
|
||||
"codec_type": "audio",
|
||||
"codec_name": "aac",
|
||||
"bit_rate": "128000"
|
||||
}
|
||||
],
|
||||
"format": {
|
||||
"duration": "120.5",
|
||||
"bit_rate": "5000000",
|
||||
"size": "75000000"
|
||||
}
|
||||
}
|
||||
|
||||
mock_run.return_value = MagicMock(
|
||||
stdout=json.dumps(mock_data).encode(),
|
||||
returncode=0
|
||||
)
|
||||
|
||||
info = self.optimizer.get_video_info(Path("test.mp4"))
|
||||
|
||||
assert info is not None
|
||||
assert info.width == 1920
|
||||
assert info.height == 1080
|
||||
assert info.fps == 30.0
|
||||
assert info.codec == "h264"
|
||||
assert info.audio_codec == "aac"
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_get_video_info_failure(self, mock_run):
|
||||
"""Test video info extraction failure."""
|
||||
mock_run.side_effect = Exception("ffprobe failed")
|
||||
|
||||
info = self.optimizer.get_video_info(Path("test.mp4"))
|
||||
assert info is None
|
||||
|
||||
def test_calculate_target_resolution_no_constraints(self):
|
||||
"""Test resolution calculation without constraints."""
|
||||
width, height = self.optimizer.calculate_target_resolution(
|
||||
1920, 1080, None, None
|
||||
)
|
||||
assert width == 1920
|
||||
assert height == 1080
|
||||
|
||||
def test_calculate_target_resolution_width_constraint(self):
|
||||
"""Test resolution calculation with width constraint."""
|
||||
width, height = self.optimizer.calculate_target_resolution(
|
||||
1920, 1080, 1280, None
|
||||
)
|
||||
assert width == 1280
|
||||
assert height == 720
|
||||
|
||||
def test_calculate_target_resolution_height_constraint(self):
|
||||
"""Test resolution calculation with height constraint."""
|
||||
width, height = self.optimizer.calculate_target_resolution(
|
||||
1920, 1080, None, 720
|
||||
)
|
||||
assert width == 1280
|
||||
assert height == 720
|
||||
|
||||
def test_calculate_target_resolution_both_constraints(self):
|
||||
"""Test resolution calculation with both constraints."""
|
||||
width, height = self.optimizer.calculate_target_resolution(
|
||||
1920, 1080, 1280, 720
|
||||
)
|
||||
assert width == 1280
|
||||
assert height == 720
|
||||
|
||||
def test_calculate_target_resolution_even_dimensions(self):
|
||||
"""Test that dimensions are always even."""
|
||||
width, height = self.optimizer.calculate_target_resolution(
|
||||
1920, 1080, 1279, None # Odd width
|
||||
)
|
||||
assert width % 2 == 0
|
||||
assert height % 2 == 0
|
||||
|
||||
def test_calculate_target_resolution_no_upscale(self):
|
||||
"""Test that small videos are not upscaled."""
|
||||
width, height = self.optimizer.calculate_target_resolution(
|
||||
640, 480, 1920, 1080
|
||||
)
|
||||
assert width == 640
|
||||
assert height == 480
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch.object(VideoOptimizer, "get_video_info")
|
||||
def test_optimize_video_dry_run(self, mock_get_info, mock_run):
|
||||
"""Test video optimization in dry-run mode."""
|
||||
mock_info = VideoInfo(
|
||||
path=Path("input.mp4"),
|
||||
duration=120.0,
|
||||
width=1920,
|
||||
height=1080,
|
||||
bitrate=5000000,
|
||||
fps=30.0,
|
||||
size=75000000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
mock_get_info.return_value = mock_info
|
||||
|
||||
optimizer = VideoOptimizer(dry_run=True)
|
||||
result = optimizer.optimize_video(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
max_width=1280
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_not_called()
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch.object(VideoOptimizer, "get_video_info")
|
||||
def test_optimize_video_resolution_reduction(self, mock_get_info, mock_run):
|
||||
"""Test video optimization with resolution reduction."""
|
||||
mock_info = VideoInfo(
|
||||
path=Path("input.mp4"),
|
||||
duration=120.0,
|
||||
width=1920,
|
||||
height=1080,
|
||||
bitrate=5000000,
|
||||
fps=30.0,
|
||||
size=75000000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
mock_get_info.return_value = mock_info
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
|
||||
result = self.optimizer.optimize_video(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
max_width=1280,
|
||||
max_height=720
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
# Check that scale filter is applied
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "-vf" in cmd
|
||||
filter_idx = cmd.index("-vf")
|
||||
assert "scale=1280:720" in cmd[filter_idx + 1]
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch.object(VideoOptimizer, "get_video_info")
|
||||
def test_optimize_video_fps_reduction(self, mock_get_info, mock_run):
|
||||
"""Test video optimization with FPS reduction."""
|
||||
mock_info = VideoInfo(
|
||||
path=Path("input.mp4"),
|
||||
duration=120.0,
|
||||
width=1920,
|
||||
height=1080,
|
||||
bitrate=5000000,
|
||||
fps=60.0,
|
||||
size=75000000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
mock_get_info.return_value = mock_info
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
|
||||
result = self.optimizer.optimize_video(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
target_fps=30.0
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
# Check that FPS filter is applied
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "-r" in cmd
|
||||
fps_idx = cmd.index("-r")
|
||||
assert "30.0" in cmd[fps_idx + 1]
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch.object(VideoOptimizer, "get_video_info")
|
||||
def test_optimize_video_two_pass(self, mock_get_info, mock_run):
|
||||
"""Test two-pass encoding."""
|
||||
mock_info = VideoInfo(
|
||||
path=Path("input.mp4"),
|
||||
duration=120.0,
|
||||
width=1920,
|
||||
height=1080,
|
||||
bitrate=5000000,
|
||||
fps=30.0,
|
||||
size=75000000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
mock_get_info.return_value = mock_info
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
|
||||
result = self.optimizer.optimize_video(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
two_pass=True
|
||||
)
|
||||
|
||||
assert result is True
|
||||
# Should be called twice (pass 1 and pass 2)
|
||||
assert mock_run.call_count == 2
|
||||
|
||||
# Check pass 1 command
|
||||
pass1_cmd = mock_run.call_args_list[0][0][0]
|
||||
assert "-pass" in pass1_cmd
|
||||
assert "1" in pass1_cmd
|
||||
|
||||
# Check pass 2 command
|
||||
pass2_cmd = mock_run.call_args_list[1][0][0]
|
||||
assert "-pass" in pass2_cmd
|
||||
assert "2" in pass2_cmd
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch.object(VideoOptimizer, "get_video_info")
|
||||
def test_optimize_video_crf_encoding(self, mock_get_info, mock_run):
|
||||
"""Test CRF-based encoding (single pass)."""
|
||||
mock_info = VideoInfo(
|
||||
path=Path("input.mp4"),
|
||||
duration=120.0,
|
||||
width=1920,
|
||||
height=1080,
|
||||
bitrate=5000000,
|
||||
fps=30.0,
|
||||
size=75000000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
mock_get_info.return_value = mock_info
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
|
||||
result = self.optimizer.optimize_video(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4"),
|
||||
crf=23,
|
||||
two_pass=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
# Check CRF parameter
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "-crf" in cmd
|
||||
crf_idx = cmd.index("-crf")
|
||||
assert "23" in cmd[crf_idx + 1]
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch.object(VideoOptimizer, "get_video_info")
|
||||
def test_optimize_video_failure(self, mock_get_info, mock_run):
|
||||
"""Test optimization failure handling."""
|
||||
mock_info = VideoInfo(
|
||||
path=Path("input.mp4"),
|
||||
duration=120.0,
|
||||
width=1920,
|
||||
height=1080,
|
||||
bitrate=5000000,
|
||||
fps=30.0,
|
||||
size=75000000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
mock_get_info.return_value = mock_info
|
||||
mock_run.side_effect = Exception("FFmpeg failed")
|
||||
|
||||
result = self.optimizer.optimize_video(
|
||||
Path("input.mp4"),
|
||||
Path("output.mp4")
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestVideoInfo:
|
||||
"""Test VideoInfo dataclass."""
|
||||
|
||||
def test_video_info_creation(self):
|
||||
"""Test creating VideoInfo object."""
|
||||
info = VideoInfo(
|
||||
path=Path("test.mp4"),
|
||||
duration=120.5,
|
||||
width=1920,
|
||||
height=1080,
|
||||
bitrate=5000000,
|
||||
fps=30.0,
|
||||
size=75000000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
|
||||
assert info.width == 1920
|
||||
assert info.height == 1080
|
||||
assert info.fps == 30.0
|
||||
assert info.codec == "h264"
|
||||
|
||||
|
||||
class TestCompareVideos:
|
||||
"""Test video comparison functionality."""
|
||||
|
||||
@patch.object(VideoOptimizer, "get_video_info")
|
||||
def test_compare_videos_success(self, mock_get_info, capsys):
|
||||
"""Test video comparison output."""
|
||||
orig_info = VideoInfo(
|
||||
path=Path("original.mp4"),
|
||||
duration=120.0,
|
||||
width=1920,
|
||||
height=1080,
|
||||
bitrate=5000000,
|
||||
fps=30.0,
|
||||
size=75000000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
|
||||
opt_info = VideoInfo(
|
||||
path=Path("optimized.mp4"),
|
||||
duration=120.0,
|
||||
width=1280,
|
||||
height=720,
|
||||
bitrate=2500000,
|
||||
fps=30.0,
|
||||
size=37500000,
|
||||
codec="h264",
|
||||
audio_codec="aac",
|
||||
audio_bitrate=128000
|
||||
)
|
||||
|
||||
mock_get_info.side_effect = [orig_info, opt_info]
|
||||
|
||||
optimizer = VideoOptimizer()
|
||||
optimizer.compare_videos(Path("original.mp4"), Path("optimized.mp4"))
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Resolution" in captured.out
|
||||
assert "1920x1080" in captured.out
|
||||
assert "1280x720" in captured.out
|
||||
assert "50.0%" in captured.out # Size reduction
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
414
skills/media-processing/scripts/video_optimize.py
Normal file
414
skills/media-processing/scripts/video_optimize.py
Normal file
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Video size optimization with quality/size balance.
|
||||
|
||||
Supports resolution reduction, frame rate adjustment, audio bitrate optimization,
|
||||
multi-pass encoding, and comparison metrics.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoInfo:
|
||||
"""Video file information."""
|
||||
path: Path
|
||||
duration: float
|
||||
width: int
|
||||
height: int
|
||||
bitrate: int
|
||||
fps: float
|
||||
size: int
|
||||
codec: str
|
||||
audio_codec: str
|
||||
audio_bitrate: int
|
||||
|
||||
|
||||
class VideoOptimizer:
|
||||
"""Handle video optimization operations using FFmpeg."""
|
||||
|
||||
def __init__(self, verbose: bool = False, dry_run: bool = False):
|
||||
self.verbose = verbose
|
||||
self.dry_run = dry_run
|
||||
|
||||
def check_ffmpeg(self) -> bool:
|
||||
"""Check if FFmpeg is available."""
|
||||
try:
|
||||
subprocess.run(
|
||||
['ffmpeg', '-version'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def get_video_info(self, input_path: Path) -> Optional[VideoInfo]:
|
||||
"""Extract video information using ffprobe."""
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe',
|
||||
'-v', 'quiet',
|
||||
'-print_format', 'json',
|
||||
'-show_format',
|
||||
'-show_streams',
|
||||
str(input_path)
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, check=True)
|
||||
data = json.loads(result.stdout)
|
||||
|
||||
# Find video and audio streams
|
||||
video_stream = None
|
||||
audio_stream = None
|
||||
|
||||
for stream in data['streams']:
|
||||
if stream['codec_type'] == 'video' and not video_stream:
|
||||
video_stream = stream
|
||||
elif stream['codec_type'] == 'audio' and not audio_stream:
|
||||
audio_stream = stream
|
||||
|
||||
if not video_stream:
|
||||
return None
|
||||
|
||||
# Parse frame rate
|
||||
fps_parts = video_stream.get('r_frame_rate', '0/1').split('/')
|
||||
fps = float(fps_parts[0]) / float(fps_parts[1]) if len(fps_parts) == 2 else 0
|
||||
|
||||
return VideoInfo(
|
||||
path=input_path,
|
||||
duration=float(data['format'].get('duration', 0)),
|
||||
width=int(video_stream.get('width', 0)),
|
||||
height=int(video_stream.get('height', 0)),
|
||||
bitrate=int(data['format'].get('bit_rate', 0)),
|
||||
fps=fps,
|
||||
size=int(data['format'].get('size', 0)),
|
||||
codec=video_stream.get('codec_name', 'unknown'),
|
||||
audio_codec=audio_stream.get('codec_name', 'none') if audio_stream else 'none',
|
||||
audio_bitrate=int(audio_stream.get('bit_rate', 0)) if audio_stream else 0
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting video info: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
def calculate_target_resolution(
|
||||
self,
|
||||
width: int,
|
||||
height: int,
|
||||
max_width: Optional[int],
|
||||
max_height: Optional[int]
|
||||
) -> Tuple[int, int]:
|
||||
"""Calculate target resolution maintaining aspect ratio."""
|
||||
if not max_width and not max_height:
|
||||
return width, height
|
||||
|
||||
aspect_ratio = width / height
|
||||
|
||||
if max_width and max_height:
|
||||
# Fit within both constraints
|
||||
if width > max_width or height > max_height:
|
||||
if width / max_width > height / max_height:
|
||||
new_width = max_width
|
||||
new_height = int(max_width / aspect_ratio)
|
||||
else:
|
||||
new_height = max_height
|
||||
new_width = int(max_height * aspect_ratio)
|
||||
else:
|
||||
new_width, new_height = width, height
|
||||
elif max_width:
|
||||
new_width = min(width, max_width)
|
||||
new_height = int(new_width / aspect_ratio)
|
||||
else:
|
||||
new_height = min(height, max_height)
|
||||
new_width = int(new_height * aspect_ratio)
|
||||
|
||||
# Ensure dimensions are even (required by some codecs)
|
||||
new_width = new_width - (new_width % 2)
|
||||
new_height = new_height - (new_height % 2)
|
||||
|
||||
return new_width, new_height
|
||||
|
||||
def optimize_video(
|
||||
self,
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
max_width: Optional[int] = None,
|
||||
max_height: Optional[int] = None,
|
||||
target_fps: Optional[float] = None,
|
||||
crf: int = 23,
|
||||
audio_bitrate: str = '128k',
|
||||
preset: str = 'medium',
|
||||
two_pass: bool = False
|
||||
) -> bool:
|
||||
"""Optimize a video file."""
|
||||
# Get input video info
|
||||
info = self.get_video_info(input_path)
|
||||
if not info:
|
||||
print(f"Error: Could not read video info for {input_path}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if self.verbose:
|
||||
print(f"\nInput video info:")
|
||||
print(f" Resolution: {info.width}x{info.height}")
|
||||
print(f" FPS: {info.fps:.2f}")
|
||||
print(f" Bitrate: {info.bitrate // 1000} kbps")
|
||||
print(f" Size: {info.size / (1024*1024):.2f} MB")
|
||||
|
||||
# Calculate target resolution
|
||||
target_width, target_height = self.calculate_target_resolution(
|
||||
info.width, info.height, max_width, max_height
|
||||
)
|
||||
|
||||
# Build FFmpeg command
|
||||
cmd = ['ffmpeg', '-i', str(input_path)]
|
||||
|
||||
# Video filters
|
||||
filters = []
|
||||
if target_width != info.width or target_height != info.height:
|
||||
filters.append(f'scale={target_width}:{target_height}')
|
||||
|
||||
if filters:
|
||||
cmd.extend(['-vf', ','.join(filters)])
|
||||
|
||||
# Frame rate adjustment
|
||||
if target_fps and target_fps < info.fps:
|
||||
cmd.extend(['-r', str(target_fps)])
|
||||
|
||||
# Video encoding
|
||||
if two_pass:
|
||||
# Two-pass encoding for better quality
|
||||
target_bitrate = int(info.bitrate * 0.7) # 30% reduction
|
||||
|
||||
# Pass 1
|
||||
pass1_cmd = cmd + [
|
||||
'-c:v', 'libx264',
|
||||
'-preset', preset,
|
||||
'-b:v', str(target_bitrate),
|
||||
'-pass', '1',
|
||||
'-an',
|
||||
'-f', 'null',
|
||||
'/dev/null' if sys.platform != 'win32' else 'NUL'
|
||||
]
|
||||
|
||||
if self.verbose or self.dry_run:
|
||||
print(f"Pass 1: {' '.join(pass1_cmd)}")
|
||||
|
||||
if not self.dry_run:
|
||||
try:
|
||||
subprocess.run(pass1_cmd, check=True, capture_output=not self.verbose)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error in pass 1: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Pass 2
|
||||
cmd.extend([
|
||||
'-c:v', 'libx264',
|
||||
'-preset', preset,
|
||||
'-b:v', str(target_bitrate),
|
||||
'-pass', '2'
|
||||
])
|
||||
else:
|
||||
# Single-pass CRF encoding
|
||||
cmd.extend([
|
||||
'-c:v', 'libx264',
|
||||
'-preset', preset,
|
||||
'-crf', str(crf)
|
||||
])
|
||||
|
||||
# Audio encoding
|
||||
cmd.extend([
|
||||
'-c:a', 'aac',
|
||||
'-b:a', audio_bitrate
|
||||
])
|
||||
|
||||
# Output
|
||||
cmd.extend(['-movflags', '+faststart', '-y', str(output_path)])
|
||||
|
||||
if self.verbose or self.dry_run:
|
||||
print(f"Command: {' '.join(cmd)}")
|
||||
|
||||
if self.dry_run:
|
||||
return True
|
||||
|
||||
# Execute
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=not self.verbose)
|
||||
|
||||
# Get output info
|
||||
output_info = self.get_video_info(output_path)
|
||||
if output_info and self.verbose:
|
||||
print(f"\nOutput video info:")
|
||||
print(f" Resolution: {output_info.width}x{output_info.height}")
|
||||
print(f" FPS: {output_info.fps:.2f}")
|
||||
print(f" Bitrate: {output_info.bitrate // 1000} kbps")
|
||||
print(f" Size: {output_info.size / (1024*1024):.2f} MB")
|
||||
reduction = (1 - output_info.size / info.size) * 100
|
||||
print(f" Size reduction: {reduction:.1f}%")
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error optimizing video: {e}", file=sys.stderr)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error optimizing video: {e}", file=sys.stderr)
|
||||
return False
|
||||
finally:
|
||||
# Clean up two-pass log files
|
||||
if two_pass and not self.dry_run:
|
||||
for log_file in Path('.').glob('ffmpeg2pass-*.log*'):
|
||||
log_file.unlink(missing_ok=True)
|
||||
|
||||
def compare_videos(self, original: Path, optimized: Path) -> None:
|
||||
"""Compare original and optimized videos."""
|
||||
orig_info = self.get_video_info(original)
|
||||
opt_info = self.get_video_info(optimized)
|
||||
|
||||
if not orig_info or not opt_info:
|
||||
print("Error: Could not compare videos", file=sys.stderr)
|
||||
return
|
||||
|
||||
print(f"\n{'Metric':<20} {'Original':<20} {'Optimized':<20} {'Change':<15}")
|
||||
print("-" * 75)
|
||||
|
||||
# Resolution
|
||||
orig_res = f"{orig_info.width}x{orig_info.height}"
|
||||
opt_res = f"{opt_info.width}x{opt_info.height}"
|
||||
print(f"{'Resolution':<20} {orig_res:<20} {opt_res:<20}")
|
||||
|
||||
# FPS
|
||||
fps_change = opt_info.fps - orig_info.fps
|
||||
print(f"{'FPS':<20} {orig_info.fps:<20.2f} {opt_info.fps:<20.2f} {fps_change:+.2f}")
|
||||
|
||||
# Bitrate
|
||||
orig_br = f"{orig_info.bitrate // 1000} kbps"
|
||||
opt_br = f"{opt_info.bitrate // 1000} kbps"
|
||||
br_change = ((opt_info.bitrate / orig_info.bitrate) - 1) * 100
|
||||
print(f"{'Bitrate':<20} {orig_br:<20} {opt_br:<20} {br_change:+.1f}%")
|
||||
|
||||
# Size
|
||||
orig_size = f"{orig_info.size / (1024*1024):.2f} MB"
|
||||
opt_size = f"{opt_info.size / (1024*1024):.2f} MB"
|
||||
size_reduction = (1 - opt_info.size / orig_info.size) * 100
|
||||
print(f"{'Size':<20} {orig_size:<20} {opt_size:<20} {-size_reduction:.1f}%")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Video size optimization with quality/size balance.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'input',
|
||||
type=Path,
|
||||
help='Input video file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o', '--output',
|
||||
type=Path,
|
||||
required=True,
|
||||
help='Output video file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-w', '--max-width',
|
||||
type=int,
|
||||
help='Maximum width in pixels'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-H', '--max-height',
|
||||
type=int,
|
||||
help='Maximum height in pixels'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--fps',
|
||||
type=float,
|
||||
help='Target frame rate'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--crf',
|
||||
type=int,
|
||||
default=23,
|
||||
help='CRF quality (18-28, lower=better, default: 23)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--audio-bitrate',
|
||||
default='128k',
|
||||
help='Audio bitrate (default: 128k)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--preset',
|
||||
choices=['ultrafast', 'superfast', 'veryfast', 'faster', 'fast',
|
||||
'medium', 'slow', 'slower', 'veryslow'],
|
||||
default='medium',
|
||||
help='Encoding preset (default: medium)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--two-pass',
|
||||
action='store_true',
|
||||
help='Use two-pass encoding (better quality)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--compare',
|
||||
action='store_true',
|
||||
help='Compare original and optimized videos'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n', '--dry-run',
|
||||
action='store_true',
|
||||
help='Show command without executing'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help='Verbose output'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate input
|
||||
if not args.input.exists():
|
||||
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize optimizer
|
||||
optimizer = VideoOptimizer(verbose=args.verbose, dry_run=args.dry_run)
|
||||
|
||||
# Check dependencies
|
||||
if not optimizer.check_ffmpeg():
|
||||
print("Error: FFmpeg not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Optimize video
|
||||
print(f"Optimizing {args.input.name}...")
|
||||
success = optimizer.optimize_video(
|
||||
args.input,
|
||||
args.output,
|
||||
args.max_width,
|
||||
args.max_height,
|
||||
args.fps,
|
||||
args.crf,
|
||||
args.audio_bitrate,
|
||||
args.preset,
|
||||
args.two_pass
|
||||
)
|
||||
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
||||
# Compare if requested
|
||||
if args.compare and not args.dry_run:
|
||||
optimizer.compare_videos(args.input, args.output)
|
||||
|
||||
print(f"\nOptimized video saved to: {args.output}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user