Media Processing Skill Process video, audio, and images using FFmpeg, ImageMagick, and RMBG CLI tools. Tool Selection | Task | Tool | Reason | |------|------|--------| | Video encoding/conversion | FFmpeg | Native codec support, streaming | | Audio extraction/conversion | FFmpeg | Direct stream manipulation | | Image resize/effects | ImageMagick | Optimized for still images | | Background removal | RMBG | AI-powered, local processing | | Batch images | ImageMagick | mogrify for in-place edits | | Video thumbnails | FFmpeg | Frame extraction built-in | | GIF creation | FFmpeg/ImageMagick | FFm…

| wc -l | tr -d ' ')\n\nif [ \"$TOTAL_FILES\" -eq 0 ]; then\n echo -e \"${YELLOW}Warning: No image files found in '$INPUT_DIR'${NC}\"\n exit 0\nfi\n\n# Display configuration\necho -e \"${GREEN}Batch Background Removal Configuration:${NC}\"\necho \" Input Dir: $INPUT_DIR\"\necho \" Output Dir: $OUTPUT_DIR\"\necho \" Model: $MODEL\"\necho \" Resolution: $MAX_RESOLUTION\"\necho \" Total Files: $TOTAL_FILES\"\necho \"\"\n\n# Process each image\nSUCCESS_COUNT=0\nFAIL_COUNT=0\nCURRENT=0\n\nwhile IFS= read -r file; do\n [ -z \"$file\" ] && continue\n\n CURRENT=$((CURRENT + 1))\n BASENAME=$(basename \"$file\")\n OUTPUT_FILE=\"$OUTPUT_DIR/${BASENAME%.*}.png\"\n\n echo -e \"${BLUE}[$CURRENT/$TOTAL_FILES]${NC} Processing: $BASENAME\"\n\n if rmbg \"$file\" -m \"$MODEL\" -o \"$OUTPUT_FILE\" -r \"$MAX_RESOLUTION\" 2>/dev/null; then\n SUCCESS_COUNT=$((SUCCESS_COUNT + 1))\n echo -e \" ${GREEN}✓ Success${NC}\"\n else\n FAIL_COUNT=$((FAIL_COUNT + 1))\n echo -e \" ${RED}✗ Failed${NC}\"\n fi\ndone \u003c\u003c\u003c \"$IMAGE_FILES\"\n\n# Display summary\necho \"\"\necho -e \"${GREEN}Batch Processing Complete${NC}\"\necho \" Total: $TOTAL_FILES files\"\necho \" Success: $SUCCESS_COUNT files\"\necho \" Failed: $FAIL_COUNT files\"\necho \" Output: $OUTPUT_DIR\"\n\nif [ \"$FAIL_COUNT\" -gt 0 ]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3275,"content_sha256":"56c990116ccef526957766def641090242ad0b92609580b24b162e8ee7ae3cca"},{"filename":"scripts/media_convert.py","content":"#!/usr/bin/env python3\n\"\"\"\nUnified media conversion tool for video, audio, and images.\n\nAuto-detects format and applies appropriate tool (FFmpeg or ImageMagick).\nSupports quality presets, batch processing, and dry-run mode.\n\"\"\"\n\nimport argparse\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import List, Optional, Tuple\n\n\n# Format mappings\nVIDEO_FORMATS = {'.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.m4v'}\nAUDIO_FORMATS = {'.mp3', '.aac', '.m4a', '.opus', '.flac', '.wav', '.ogg'}\nIMAGE_FORMATS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'}\n\n# Quality presets\nQUALITY_PRESETS = {\n 'web': {\n 'video_crf': 23,\n 'video_preset': 'medium',\n 'audio_bitrate': '128k',\n 'image_quality': 85\n },\n 'archive': {\n 'video_crf': 18,\n 'video_preset': 'slow',\n 'audio_bitrate': '192k',\n 'image_quality': 95\n },\n 'mobile': {\n 'video_crf': 26,\n 'video_preset': 'fast',\n 'audio_bitrate': '96k',\n 'image_quality': 80\n }\n}\n\n\ndef check_dependencies() -> Tuple[bool, bool]:\n \"\"\"Check if ffmpeg and imagemagick are available.\"\"\"\n ffmpeg_available = subprocess.run(\n ['ffmpeg', '-version'],\n stdout=subprocess.DEVNULL,\n stderr=subprocess.DEVNULL\n ).returncode == 0\n\n magick_available = subprocess.run(\n ['magick', '-version'],\n stdout=subprocess.DEVNULL,\n stderr=subprocess.DEVNULL\n ).returncode == 0\n\n return ffmpeg_available, magick_available\n\n\ndef detect_media_type(file_path: Path) -> str:\n \"\"\"Detect media type from file extension.\"\"\"\n ext = file_path.suffix.lower()\n\n if ext in VIDEO_FORMATS:\n return 'video'\n elif ext in AUDIO_FORMATS:\n return 'audio'\n elif ext in IMAGE_FORMATS:\n return 'image'\n else:\n return 'unknown'\n\n\ndef build_video_command(\n input_path: Path,\n output_path: Path,\n preset: str = 'web'\n) -> List[str]:\n \"\"\"Build FFmpeg command for video conversion.\"\"\"\n quality = QUALITY_PRESETS[preset]\n\n return [\n 'ffmpeg', '-i', str(input_path),\n '-c:v', 'libx264',\n '-preset', quality['video_preset'],\n '-crf', str(quality['video_crf']),\n '-c:a', 'aac',\n '-b:a', quality['audio_bitrate'],\n '-movflags', '+faststart',\n '-y',\n str(output_path)\n ]\n\n\ndef build_audio_command(\n input_path: Path,\n output_path: Path,\n preset: str = 'web'\n) -> List[str]:\n \"\"\"Build FFmpeg command for audio conversion.\"\"\"\n quality = QUALITY_PRESETS[preset]\n output_ext = output_path.suffix.lower()\n\n codec_map = {\n '.mp3': 'libmp3lame',\n '.aac': 'aac',\n '.m4a': 'aac',\n '.opus': 'libopus',\n '.flac': 'flac',\n '.wav': 'pcm_s16le',\n '.ogg': 'libvorbis'\n }\n\n codec = codec_map.get(output_ext, 'aac')\n\n cmd = ['ffmpeg', '-i', str(input_path), '-c:a', codec]\n\n # Add bitrate for lossy codecs\n if codec not in ['flac', 'pcm_s16le']:\n cmd.extend(['-b:a', quality['audio_bitrate']])\n\n cmd.extend(['-y', str(output_path)])\n return cmd\n\n\ndef build_image_command(\n input_path: Path,\n output_path: Path,\n preset: str = 'web'\n) -> List[str]:\n \"\"\"Build ImageMagick command for image conversion.\"\"\"\n quality = QUALITY_PRESETS[preset]\n\n return [\n 'magick', str(input_path),\n '-quality', str(quality['image_quality']),\n '-strip',\n str(output_path)\n ]\n\n\ndef convert_file(\n input_path: Path,\n output_path: Path,\n preset: str = 'web',\n dry_run: bool = False,\n verbose: bool = False\n) -> bool:\n \"\"\"Convert a single media file.\"\"\"\n media_type = detect_media_type(input_path)\n\n if media_type == 'unknown':\n print(f\"Error: Unsupported format for {input_path}\", file=sys.stderr)\n return False\n\n # Ensure output directory exists\n output_path.parent.mkdir(parents=True, exist_ok=True)\n\n # Build command based on media type\n if media_type == 'video':\n cmd = build_video_command(input_path, output_path, preset)\n elif media_type == 'audio':\n cmd = build_audio_command(input_path, output_path, preset)\n else: # image\n cmd = build_image_command(input_path, output_path, preset)\n\n if verbose or dry_run:\n print(f\"Command: {' '.join(cmd)}\")\n\n if dry_run:\n return True\n\n try:\n result = subprocess.run(\n cmd,\n stdout=subprocess.PIPE if not verbose else None,\n stderr=subprocess.PIPE if not verbose else None,\n check=True\n )\n return True\n except subprocess.CalledProcessError as e:\n print(f\"Error converting {input_path}: {e}\", file=sys.stderr)\n if not verbose and e.stderr:\n print(e.stderr.decode(), file=sys.stderr)\n return False\n except Exception as e:\n print(f\"Error converting {input_path}: {e}\", file=sys.stderr)\n return False\n\n\ndef batch_convert(\n input_paths: List[Path],\n output_dir: Optional[Path] = None,\n output_format: Optional[str] = None,\n preset: str = 'web',\n dry_run: bool = False,\n verbose: bool = False\n) -> Tuple[int, int]:\n \"\"\"Convert multiple files.\"\"\"\n success_count = 0\n fail_count = 0\n\n for input_path in input_paths:\n if not input_path.exists():\n print(f\"Error: {input_path} not found\", file=sys.stderr)\n fail_count += 1\n continue\n\n # Determine output path\n if output_dir:\n output_name = input_path.stem\n if output_format:\n output_path = output_dir / f\"{output_name}.{output_format.lstrip('.')}\"\n else:\n output_path = output_dir / input_path.name\n else:\n if output_format:\n output_path = input_path.with_suffix(f\".{output_format.lstrip('.')}\")\n else:\n print(f\"Error: No output format specified for {input_path}\", file=sys.stderr)\n fail_count += 1\n continue\n\n print(f\"Converting {input_path.name} -> {output_path.name}\")\n\n if convert_file(input_path, output_path, preset, dry_run, verbose):\n success_count += 1\n else:\n fail_count += 1\n\n return success_count, fail_count\n\n\ndef main():\n \"\"\"Main entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description='Unified media conversion tool for video, audio, and images.'\n )\n parser.add_argument(\n 'inputs',\n nargs='+',\n type=Path,\n help='Input file(s) to convert'\n )\n parser.add_argument(\n '-o', '--output',\n type=Path,\n help='Output file or directory for batch conversion'\n )\n parser.add_argument(\n '-f', '--format',\n help='Output format (e.g., mp4, jpg, mp3)'\n )\n parser.add_argument(\n '-p', '--preset',\n choices=['web', 'archive', 'mobile'],\n default='web',\n help='Quality preset (default: web)'\n )\n parser.add_argument(\n '-n', '--dry-run',\n action='store_true',\n help='Show commands without executing'\n )\n parser.add_argument(\n '-v', '--verbose',\n action='store_true',\n help='Verbose output'\n )\n\n args = parser.parse_args()\n\n # Check dependencies\n ffmpeg_ok, magick_ok = check_dependencies()\n if not ffmpeg_ok and not magick_ok:\n print(\"Error: Neither ffmpeg nor imagemagick found\", file=sys.stderr)\n sys.exit(1)\n\n # Handle single file vs batch conversion\n if len(args.inputs) == 1 and args.output and not args.output.is_dir():\n # Single file conversion\n success = convert_file(\n args.inputs[0],\n args.output,\n args.preset,\n args.dry_run,\n args.verbose\n )\n sys.exit(0 if success else 1)\n else:\n # Batch conversion\n output_dir = args.output if args.output else Path.cwd()\n if not args.output:\n output_dir = None # Will convert in place with new format\n\n success, fail = batch_convert(\n args.inputs,\n output_dir,\n args.format,\n args.preset,\n args.dry_run,\n args.verbose\n )\n\n print(f\"\\nResults: {success} succeeded, {fail} failed\")\n sys.exit(0 if fail == 0 else 1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8439,"content_sha256":"896961d11de1f7040243e388a82b30295e3224ec7b0afb4d023f4579c832e709"},{"filename":"scripts/README.md","content":"# Media Processing Scripts\n\nHelper scripts for common media processing tasks.\n\n## Background Removal Scripts\n\n### remove-background.sh\nRemove background from a single image using RMBG CLI.\n\n```bash\n# Basic usage\n./remove-background.sh photo.jpg\n\n# With specific model\n./remove-background.sh photo.jpg briaai\n\n# With custom output and resolution\n./remove-background.sh photo.jpg briaai output.png 4096\n```\n\n**Arguments:**\n- `input` - Input image file (required)\n- `model` - Model name: u2netp, modnet, briaai, isnet-anime, silueta, u2net-cloth (default: modnet)\n- `output` - Output file path (default: auto-generated)\n- `resolution` - Max resolution in pixels (default: 2048)\n\n### batch-remove-background.sh\nRemove backgrounds from all images in a directory.\n\n```bash\n# Basic usage\n./batch-remove-background.sh ./photos\n\n# With custom output directory\n./batch-remove-background.sh ./photos ./output\n\n# With specific model and resolution\n./batch-remove-background.sh ./photos ./output briaai 4096\n```\n\n**Arguments:**\n- `input_dir` - Input directory with images (required)\n- `output_dir` - Output directory (default: input_dir/no-bg)\n- `model` - Model name (default: modnet)\n- `resolution` - Max resolution in pixels (default: 2048)\n\n### remove-bg-node.js\nNode.js script for background removal with progress tracking.\n\n```bash\n# Basic usage\nnode remove-bg-node.js photo.jpg\n\n# With options\nnode remove-bg-node.js photo.jpg -m briaai -o output.png -r 4096 -p\n```\n\n**Options:**\n- `-o, --output \u003cpath>` - Output file path\n- `-m, --model \u003cname>` - Model: briaai, modnet, u2netp\n- `-r, --resolution \u003cn>` - Max resolution\n- `-p, --progress` - Show progress\n\n## Image Processing Scripts\n\n### batch_resize.py\nBatch resize images with various options.\n\n```bash\npython batch_resize.py -i ./input -o ./output -w 800 -h 600\n```\n\n## Video Processing Scripts\n\n### video_optimize.py\nOptimize videos for web with quality and size optimization.\n\n```bash\npython video_optimize.py -i input.mp4 -o output.mp4 --preset slow --crf 23\n```\n\n### media_convert.py\nConvert media files between different formats.\n\n```bash\npython media_convert.py -i input.mkv -o output.mp4 --codec h264\n```\n\n## Requirements\n\n### Shell Scripts\n- Bash (macOS, Linux)\n- rmbg-cli: `npm install -g rmbg-cli`\n- FFmpeg: `brew install ffmpeg` or `apt-get install ffmpeg`\n- ImageMagick: `brew install imagemagick` or `apt-get install imagemagick`\n\n### Node.js Scripts\n- Node.js 14+\n- Dependencies: `npm install rmbg`\n\n### Python Scripts\n- Python 3.7+\n- Dependencies: `pip install -r requirements.txt`\n\n## Testing\n\nRun tests:\n```bash\ncd tests\nbash test_all.sh\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2606,"content_sha256":"8876aca16d9d78f900fb8cf370f09cc9369134a671a2f8b50788fb72ebd46ff4"},{"filename":"scripts/remove-background.sh","content":"#!/bin/bash\n# Background removal script using RMBG CLI\n# Usage: ./remove-background.sh \u003cinput> [model] [output] [resolution]\n\nset -e\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m' # No Color\n\n# Default values\nINPUT=\"\"\nMODEL=\"modnet\"\nOUTPUT=\"\"\nMAX_RESOLUTION=\"2048\"\n\n# Parse arguments\nINPUT=\"$1\"\nif [ -n \"$2\" ]; then\n MODEL=\"$2\"\nfi\nif [ -n \"$3\" ]; then\n OUTPUT=\"$3\"\nfi\nif [ -n \"$4\" ]; then\n MAX_RESOLUTION=\"$4\"\nfi\n\n# Validate input\nif [ -z \"$INPUT\" ]; then\n echo -e \"${RED}Error: Input file is required${NC}\"\n echo \"\"\n echo \"Usage: $0 \u003cinput> [model] [output] [resolution]\"\n echo \"\"\n echo \"Arguments:\"\n echo \" input Input image file (required)\"\n echo \" model Model name: u2netp, modnet, briaai, isnet-anime, silueta, u2net-cloth (default: modnet)\"\n echo \" output Output file path (default: auto-generated)\"\n echo \" resolution Max resolution in pixels (default: 2048)\"\n echo \"\"\n echo \"Examples:\"\n echo \" $0 photo.jpg\"\n echo \" $0 photo.jpg briaai\"\n echo \" $0 photo.jpg briaai output.png\"\n echo \" $0 photo.jpg briaai output.png 4096\"\n exit 1\nfi\n\nif [ ! -f \"$INPUT\" ]; then\n echo -e \"${RED}Error: Input file '$INPUT' not found${NC}\"\n exit 1\nfi\n\n# Check if rmbg-cli is installed\nif ! command -v rmbg &> /dev/null; then\n echo -e \"${YELLOW}Warning: rmbg-cli not found${NC}\"\n echo \"Installing rmbg-cli globally...\"\n npm install -g rmbg-cli\n echo -e \"${GREEN}✓ rmbg-cli installed${NC}\"\nfi\n\n# Generate output filename if not provided\nif [ -z \"$OUTPUT\" ]; then\n BASENAME=$(basename \"$INPUT\" | sed 's/\\.[^.]*$//')\n OUTPUT=\"${BASENAME}-no-bg.png\"\nfi\n\n# Display configuration\necho -e \"${GREEN}Background Removal Configuration:${NC}\"\necho \" Input: $INPUT\"\necho \" Model: $MODEL\"\necho \" Output: $OUTPUT\"\necho \" Resolution: $MAX_RESOLUTION\"\necho \"\"\n\n# Remove background\necho \"Processing...\"\nrmbg \"$INPUT\" -m \"$MODEL\" -o \"$OUTPUT\" -r \"$MAX_RESOLUTION\"\n\nif [ $? -eq 0 ]; then\n echo -e \"${GREEN}✓ Background removed successfully${NC}\"\n echo \" Output: $OUTPUT\"\n\n # Display file sizes\n INPUT_SIZE=$(du -h \"$INPUT\" | cut -f1)\n OUTPUT_SIZE=$(du -h \"$OUTPUT\" | cut -f1)\n echo \"\"\n echo \"File sizes:\"\n echo \" Input: $INPUT_SIZE\"\n echo \" Output: $OUTPUT_SIZE\"\nelse\n echo -e \"${RED}✗ Background removal failed${NC}\"\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2407,"content_sha256":"fe8caea66795c3d6b85c478de3e361f4983da486efa3813fda8bd1bcb792089e"},{"filename":"scripts/requirements.txt","content":"# Media Processing Skill Dependencies\n# Python 3.10+ required\n\n# No Python package dependencies - uses system binaries\n# Required system tools (install separately):\n# - FFmpeg (video/audio processing)\n# - ImageMagick (image processing)\n\n# Testing dependencies (dev)\npytest>=8.0.0\npytest-cov>=4.1.0\npytest-mock>=3.12.0\n\n# Installation instructions:\n#\n# Ubuntu/Debian:\n# sudo apt-get install ffmpeg imagemagick\n#\n# macOS (Homebrew):\n# brew install ffmpeg imagemagick\n#\n# Windows:\n# choco install ffmpeg imagemagick\n# or download from official websites\n","content_type":"text/plain; charset=utf-8","language":null,"size":558,"content_sha256":"656461e5a959cc78eda21807a73b5c20e78e6bd116fa89c7606c82eeacaab221"},{"filename":"scripts/tests/requirements.txt","content":"pytest>=7.4.0\npytest-cov>=4.1.0\n","content_type":"text/plain; charset=utf-8","language":null,"size":32,"content_sha256":"7f336e73b484fac1a0807a6cfba48eefe79c12f3c348d988a708dda2d6df6d14"},{"filename":"scripts/tests/test_batch_resize.py","content":"#!/usr/bin/env python3\n\"\"\"Tests for batch_resize.py\"\"\"\n\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, call, patch\n\nimport pytest\n\n# Add parent directory to path\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\nfrom batch_resize import ImageResizer, collect_images\n\n\nclass TestImageResizer:\n \"\"\"Test ImageResizer class.\"\"\"\n\n def setup_method(self):\n \"\"\"Set up test fixtures.\"\"\"\n self.resizer = ImageResizer(verbose=False, dry_run=False)\n\n @patch(\"subprocess.run\")\n def test_check_imagemagick_available(self, mock_run):\n \"\"\"Test ImageMagick availability check.\"\"\"\n mock_run.return_value = MagicMock(returncode=0)\n assert self.resizer.check_imagemagick() is True\n\n @patch(\"subprocess.run\")\n def test_check_imagemagick_unavailable(self, mock_run):\n \"\"\"Test when ImageMagick is not available.\"\"\"\n mock_run.side_effect = FileNotFoundError()\n assert self.resizer.check_imagemagick() is False\n\n def test_build_resize_command_fit_strategy(self):\n \"\"\"Test command building for 'fit' strategy.\"\"\"\n cmd = self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=600,\n strategy=\"fit\",\n quality=85\n )\n\n assert \"magick\" in cmd\n assert str(Path(\"input.jpg\")) in cmd\n assert \"-resize\" in cmd\n assert \"800x600\" in cmd\n assert \"-quality\" in cmd\n assert \"85\" in cmd\n assert \"-strip\" in cmd\n\n def test_build_resize_command_fill_strategy(self):\n \"\"\"Test command building for 'fill' strategy.\"\"\"\n cmd = self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=600,\n strategy=\"fill\",\n quality=85\n )\n\n assert \"-resize\" in cmd\n assert \"800x600^\" in cmd\n assert \"-gravity\" in cmd\n assert \"center\" in cmd\n assert \"-extent\" in cmd\n\n def test_build_resize_command_thumbnail_strategy(self):\n \"\"\"Test command building for 'thumbnail' strategy.\"\"\"\n cmd = self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=200,\n height=None,\n strategy=\"thumbnail\",\n quality=85\n )\n\n assert \"200x200^\" in cmd\n assert \"-gravity\" in cmd\n assert \"center\" in cmd\n\n def test_build_resize_command_with_watermark(self):\n \"\"\"Test command building with watermark.\"\"\"\n watermark = Path(\"watermark.png\")\n cmd = self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=None,\n strategy=\"fit\",\n quality=85,\n watermark=watermark\n )\n\n assert str(watermark) in cmd\n assert \"-gravity\" in cmd\n assert \"southeast\" in cmd\n assert \"-composite\" in cmd\n\n def test_build_resize_command_exact_strategy(self):\n \"\"\"Test command building for 'exact' strategy.\"\"\"\n cmd = self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=600,\n strategy=\"exact\",\n quality=85\n )\n\n assert \"800x600!\" in cmd\n\n def test_build_resize_command_fill_requires_dimensions(self):\n \"\"\"Test that 'fill' strategy requires both dimensions.\"\"\"\n with pytest.raises(ValueError):\n self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=None,\n strategy=\"fill\",\n quality=85\n )\n\n @patch(\"subprocess.run\")\n def test_resize_image_success(self, mock_run):\n \"\"\"Test successful image resize.\"\"\"\n mock_run.return_value = MagicMock(returncode=0)\n\n result = self.resizer.resize_image(\n Path(\"input.jpg\"),\n Path(\"output/output.jpg\"),\n width=800,\n height=None,\n strategy=\"fit\",\n quality=85\n )\n\n assert result is True\n mock_run.assert_called_once()\n\n @patch(\"subprocess.run\")\n def test_resize_image_dry_run(self, mock_run):\n \"\"\"Test resize in dry-run mode.\"\"\"\n resizer = ImageResizer(dry_run=True)\n\n result = resizer.resize_image(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=None\n )\n\n assert result is True\n mock_run.assert_not_called()\n\n @patch(\"subprocess.run\")\n def test_resize_image_failure(self, mock_run):\n \"\"\"Test resize failure handling.\"\"\"\n mock_run.side_effect = Exception(\"Resize failed\")\n\n result = self.resizer.resize_image(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=None\n )\n\n assert result is False\n\n\nclass TestCollectImages:\n \"\"\"Test image collection functionality.\"\"\"\n\n def test_collect_images_from_file(self, tmp_path):\n \"\"\"Test collecting a single image file.\"\"\"\n img_file = tmp_path / \"test.jpg\"\n img_file.touch()\n\n images = collect_images([img_file])\n assert len(images) == 1\n assert images[0] == img_file\n\n def test_collect_images_from_directory(self, tmp_path):\n \"\"\"Test collecting images from directory.\"\"\"\n (tmp_path / \"image1.jpg\").touch()\n (tmp_path / \"image2.png\").touch()\n (tmp_path / \"text.txt\").touch()\n\n images = collect_images([tmp_path])\n assert len(images) == 2\n assert all(img.suffix.lower() in {'.jpg', '.png'} for img in images)\n\n def test_collect_images_recursive(self, tmp_path):\n \"\"\"Test recursive image collection.\"\"\"\n subdir = tmp_path / \"subdir\"\n subdir.mkdir()\n (tmp_path / \"image1.jpg\").touch()\n (subdir / \"image2.jpg\").touch()\n\n images = collect_images([tmp_path], recursive=True)\n assert len(images) == 2\n\n images_non_recursive = collect_images([tmp_path], recursive=False)\n assert len(images_non_recursive) == 1\n\n def test_collect_images_filters_extensions(self, tmp_path):\n \"\"\"Test that only image files are collected.\"\"\"\n (tmp_path / \"image.jpg\").touch()\n (tmp_path / \"doc.pdf\").touch()\n (tmp_path / \"text.txt\").touch()\n\n images = collect_images([tmp_path])\n assert len(images) == 1\n assert images[0].suffix.lower() == '.jpg'\n\n def test_collect_images_multiple_paths(self, tmp_path):\n \"\"\"Test collecting from multiple paths.\"\"\"\n dir1 = tmp_path / \"dir1\"\n dir2 = tmp_path / \"dir2\"\n dir1.mkdir()\n dir2.mkdir()\n\n (dir1 / \"image1.jpg\").touch()\n (dir2 / \"image2.png\").touch()\n\n images = collect_images([dir1, dir2])\n assert len(images) == 2\n\n\nclass TestBatchResize:\n \"\"\"Test batch resize functionality.\"\"\"\n\n def setup_method(self):\n \"\"\"Set up test fixtures.\"\"\"\n self.resizer = ImageResizer(verbose=False, dry_run=False)\n\n @patch.object(ImageResizer, \"resize_image\")\n def test_batch_resize_success(self, mock_resize, tmp_path):\n \"\"\"Test successful batch resize.\"\"\"\n mock_resize.return_value = True\n\n input_images = [\n tmp_path / \"image1.jpg\",\n tmp_path / \"image2.jpg\"\n ]\n for img in input_images:\n img.touch()\n\n output_dir = tmp_path / \"output\"\n\n success, fail = self.resizer.batch_resize(\n input_images,\n output_dir,\n width=800,\n height=None,\n strategy=\"fit\"\n )\n\n assert success == 2\n assert fail == 0\n assert mock_resize.call_count == 2\n\n @patch.object(ImageResizer, \"resize_image\")\n def test_batch_resize_with_failures(self, mock_resize, tmp_path):\n \"\"\"Test batch resize with some failures.\"\"\"\n mock_resize.side_effect = [True, False, True]\n\n input_images = [\n tmp_path / \"image1.jpg\",\n tmp_path / \"image2.jpg\",\n tmp_path / \"image3.jpg\"\n ]\n for img in input_images:\n img.touch()\n\n output_dir = tmp_path / \"output\"\n\n success, fail = self.resizer.batch_resize(\n input_images,\n output_dir,\n width=800,\n height=None\n )\n\n assert success == 2\n assert fail == 1\n\n @patch.object(ImageResizer, \"resize_image\")\n def test_batch_resize_format_conversion(self, mock_resize, tmp_path):\n \"\"\"Test batch resize with format conversion.\"\"\"\n mock_resize.return_value = True\n\n input_images = [tmp_path / \"image.png\"]\n input_images[0].touch()\n\n output_dir = tmp_path / \"output\"\n\n self.resizer.batch_resize(\n input_images,\n output_dir,\n width=800,\n height=None,\n format_ext=\"jpg\"\n )\n\n # Check that resize_image was called with .jpg extension\n call_args = mock_resize.call_args[0]\n assert call_args[1].suffix == \".jpg\"\n\n\nclass TestResizeStrategies:\n \"\"\"Test different resize strategies.\"\"\"\n\n def setup_method(self):\n \"\"\"Set up test fixtures.\"\"\"\n self.resizer = ImageResizer()\n\n def test_fit_strategy_maintains_aspect(self):\n \"\"\"Test that 'fit' strategy maintains aspect ratio.\"\"\"\n cmd = self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=600,\n strategy=\"fit\",\n quality=85\n )\n\n # Should have resize without ^ or !\n resize_idx = cmd.index(\"-resize\")\n geometry = cmd[resize_idx + 1]\n assert \"^\" not in geometry\n assert \"!\" not in geometry\n\n def test_cover_strategy_fills_dimensions(self):\n \"\"\"Test that 'cover' strategy fills dimensions.\"\"\"\n cmd = self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=600,\n strategy=\"cover\",\n quality=85\n )\n\n resize_idx = cmd.index(\"-resize\")\n geometry = cmd[resize_idx + 1]\n assert \"^\" in geometry\n\n def test_exact_strategy_ignores_aspect(self):\n \"\"\"Test that 'exact' strategy ignores aspect ratio.\"\"\"\n cmd = self.resizer.build_resize_command(\n Path(\"input.jpg\"),\n Path(\"output.jpg\"),\n width=800,\n height=600,\n strategy=\"exact\",\n quality=85\n )\n\n resize_idx = cmd.index(\"-resize\")\n geometry = cmd[resize_idx + 1]\n assert \"!\" in geometry\n\n\nif __name__ == \"__main__\":\n pytest.main([__file__, \"-v\"])\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10952,"content_sha256":"ce898f6a8f10596399a4dfaadb1abc9e2a133e18e375e1b6a5b5e56faf6d0033"},{"filename":"scripts/tests/test_media_convert.py","content":"#!/usr/bin/env python3\n\"\"\"Tests for media_convert.py\"\"\"\n\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\n# Add parent directory to path\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\nfrom media_convert import (\n build_audio_command,\n build_image_command,\n build_video_command,\n check_dependencies,\n convert_file,\n detect_media_type,\n)\n\n\nclass TestMediaTypeDetection:\n \"\"\"Test media type detection.\"\"\"\n\n def test_detect_video_formats(self):\n \"\"\"Test video format detection.\"\"\"\n assert detect_media_type(Path(\"test.mp4\")) == \"video\"\n assert detect_media_type(Path(\"test.mkv\")) == \"video\"\n assert detect_media_type(Path(\"test.avi\")) == \"video\"\n assert detect_media_type(Path(\"test.mov\")) == \"video\"\n\n def test_detect_audio_formats(self):\n \"\"\"Test audio format detection.\"\"\"\n assert detect_media_type(Path(\"test.mp3\")) == \"audio\"\n assert detect_media_type(Path(\"test.aac\")) == \"audio\"\n assert detect_media_type(Path(\"test.flac\")) == \"audio\"\n assert detect_media_type(Path(\"test.wav\")) == \"audio\"\n\n def test_detect_image_formats(self):\n \"\"\"Test image format detection.\"\"\"\n assert detect_media_type(Path(\"test.jpg\")) == \"image\"\n assert detect_media_type(Path(\"test.png\")) == \"image\"\n assert detect_media_type(Path(\"test.gif\")) == \"image\"\n assert detect_media_type(Path(\"test.webp\")) == \"image\"\n\n def test_detect_unknown_format(self):\n \"\"\"Test unknown format detection.\"\"\"\n assert detect_media_type(Path(\"test.txt\")) == \"unknown\"\n assert detect_media_type(Path(\"test.doc\")) == \"unknown\"\n\n def test_case_insensitive(self):\n \"\"\"Test case-insensitive detection.\"\"\"\n assert detect_media_type(Path(\"TEST.MP4\")) == \"video\"\n assert detect_media_type(Path(\"TEST.JPG\")) == \"image\"\n\n\nclass TestCommandBuilding:\n \"\"\"Test command building functions.\"\"\"\n\n def test_build_video_command_web_preset(self):\n \"\"\"Test video command with web preset.\"\"\"\n cmd = build_video_command(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n preset=\"web\"\n )\n\n assert \"ffmpeg\" in cmd\n assert \"-i\" in cmd\n assert str(Path(\"input.mp4\")) in cmd\n assert \"-c:v\" in cmd\n assert \"libx264\" in cmd\n assert \"-crf\" in cmd\n assert \"23\" in cmd\n assert \"-preset\" in cmd\n assert \"medium\" in cmd\n assert str(Path(\"output.mp4\")) in cmd\n\n def test_build_video_command_archive_preset(self):\n \"\"\"Test video command with archive preset.\"\"\"\n cmd = build_video_command(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n preset=\"archive\"\n )\n\n assert \"18\" in cmd # CRF for archive\n assert \"slow\" in cmd # Preset for archive\n\n def test_build_audio_command_mp3(self):\n \"\"\"Test audio command for MP3 output.\"\"\"\n cmd = build_audio_command(\n Path(\"input.wav\"),\n Path(\"output.mp3\"),\n preset=\"web\"\n )\n\n assert \"ffmpeg\" in cmd\n assert \"-c:a\" in cmd\n assert \"libmp3lame\" in cmd\n assert \"-b:a\" in cmd\n\n def test_build_audio_command_flac(self):\n \"\"\"Test audio command for FLAC (lossless).\"\"\"\n cmd = build_audio_command(\n Path(\"input.wav\"),\n Path(\"output.flac\"),\n preset=\"web\"\n )\n\n assert \"flac\" in cmd\n assert \"-b:a\" not in cmd # No bitrate for lossless\n\n def test_build_image_command(self):\n \"\"\"Test image command building.\"\"\"\n cmd = build_image_command(\n Path(\"input.png\"),\n Path(\"output.jpg\"),\n preset=\"web\"\n )\n\n assert \"magick\" in cmd\n assert str(Path(\"input.png\")) in cmd\n assert \"-quality\" in cmd\n assert \"85\" in cmd\n assert \"-strip\" in cmd\n assert str(Path(\"output.jpg\")) in cmd\n\n\nclass TestDependencyCheck:\n \"\"\"Test dependency checking.\"\"\"\n\n @patch(\"subprocess.run\")\n def test_check_dependencies_both_available(self, mock_run):\n \"\"\"Test when both tools are available.\"\"\"\n mock_run.return_value = MagicMock(returncode=0)\n ffmpeg_ok, magick_ok = check_dependencies()\n assert ffmpeg_ok is True\n assert magick_ok is True\n\n @patch(\"subprocess.run\")\n def test_check_dependencies_ffmpeg_only(self, mock_run):\n \"\"\"Test when only FFmpeg is available.\"\"\"\n def side_effect(*args, **kwargs):\n if \"ffmpeg\" in args[0]:\n return MagicMock(returncode=0)\n return MagicMock(returncode=1)\n\n mock_run.side_effect = side_effect\n ffmpeg_ok, magick_ok = check_dependencies()\n assert ffmpeg_ok is True\n assert magick_ok is False\n\n\nclass TestFileConversion:\n \"\"\"Test file conversion functionality.\"\"\"\n\n @patch(\"subprocess.run\")\n @patch(\"media_convert.detect_media_type\")\n def test_convert_video_file_dry_run(self, mock_detect, mock_run):\n \"\"\"Test video conversion in dry-run mode.\"\"\"\n mock_detect.return_value = \"video\"\n\n result = convert_file(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n preset=\"web\",\n dry_run=True\n )\n\n assert result is True\n mock_run.assert_not_called()\n\n @patch(\"subprocess.run\")\n @patch(\"media_convert.detect_media_type\")\n def test_convert_image_file_success(self, mock_detect, mock_run):\n \"\"\"Test successful image conversion.\"\"\"\n mock_detect.return_value = \"image\"\n mock_run.return_value = MagicMock(returncode=0)\n\n result = convert_file(\n Path(\"input.png\"),\n Path(\"output.jpg\"),\n preset=\"web\"\n )\n\n assert result is True\n mock_run.assert_called_once()\n\n @patch(\"subprocess.run\")\n @patch(\"media_convert.detect_media_type\")\n def test_convert_file_error(self, mock_detect, mock_run):\n \"\"\"Test conversion error handling.\"\"\"\n mock_detect.return_value = \"video\"\n mock_run.side_effect = Exception(\"Conversion failed\")\n\n result = convert_file(\n Path(\"input.mp4\"),\n Path(\"output.mp4\")\n )\n\n assert result is False\n\n @patch(\"media_convert.detect_media_type\")\n def test_convert_unknown_format(self, mock_detect):\n \"\"\"Test conversion with unknown format.\"\"\"\n mock_detect.return_value = \"unknown\"\n\n result = convert_file(\n Path(\"input.txt\"),\n Path(\"output.txt\")\n )\n\n assert result is False\n\n\nclass TestQualityPresets:\n \"\"\"Test quality preset functionality.\"\"\"\n\n def test_web_preset_settings(self):\n \"\"\"Test web preset values.\"\"\"\n cmd = build_video_command(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n preset=\"web\"\n )\n\n cmd_str = \" \".join(cmd)\n assert \"23\" in cmd_str # CRF\n assert \"128k\" in cmd_str # Audio bitrate\n\n def test_archive_preset_settings(self):\n \"\"\"Test archive preset values.\"\"\"\n cmd = build_video_command(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n preset=\"archive\"\n )\n\n cmd_str = \" \".join(cmd)\n assert \"18\" in cmd_str # Higher quality CRF\n assert \"192k\" in cmd_str # Higher audio bitrate\n\n def test_mobile_preset_settings(self):\n \"\"\"Test mobile preset values.\"\"\"\n cmd = build_video_command(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n preset=\"mobile\"\n )\n\n cmd_str = \" \".join(cmd)\n assert \"26\" in cmd_str # Lower quality CRF\n assert \"96k\" in cmd_str # Lower audio bitrate\n\n\nif __name__ == \"__main__\":\n pytest.main([__file__, \"-v\"])\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7836,"content_sha256":"c067e325304b7cb84701a1481e2aa077ce8a29a04799a1b31a7768342d4c1f20"},{"filename":"scripts/tests/test_video_optimize.py","content":"#!/usr/bin/env python3\n\"\"\"Tests for video_optimize.py\"\"\"\n\nimport json\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\n# Add parent directory to path\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\nfrom video_optimize import VideoInfo, VideoOptimizer\n\n\nclass TestVideoOptimizer:\n \"\"\"Test VideoOptimizer class.\"\"\"\n\n def setup_method(self):\n \"\"\"Set up test fixtures.\"\"\"\n self.optimizer = VideoOptimizer(verbose=False, dry_run=False)\n\n @patch(\"subprocess.run\")\n def test_check_ffmpeg_available(self, mock_run):\n \"\"\"Test FFmpeg availability check.\"\"\"\n mock_run.return_value = MagicMock(returncode=0)\n assert self.optimizer.check_ffmpeg() is True\n\n @patch(\"subprocess.run\")\n def test_check_ffmpeg_unavailable(self, mock_run):\n \"\"\"Test when FFmpeg is not available.\"\"\"\n mock_run.side_effect = FileNotFoundError()\n assert self.optimizer.check_ffmpeg() is False\n\n @patch(\"subprocess.run\")\n def test_get_video_info_success(self, mock_run):\n \"\"\"Test successful video info extraction.\"\"\"\n mock_data = {\n \"streams\": [\n {\n \"codec_type\": \"video\",\n \"codec_name\": \"h264\",\n \"width\": 1920,\n \"height\": 1080,\n \"r_frame_rate\": \"30/1\"\n },\n {\n \"codec_type\": \"audio\",\n \"codec_name\": \"aac\",\n \"bit_rate\": \"128000\"\n }\n ],\n \"format\": {\n \"duration\": \"120.5\",\n \"bit_rate\": \"5000000\",\n \"size\": \"75000000\"\n }\n }\n\n mock_run.return_value = MagicMock(\n stdout=json.dumps(mock_data).encode(),\n returncode=0\n )\n\n info = self.optimizer.get_video_info(Path(\"test.mp4\"))\n\n assert info is not None\n assert info.width == 1920\n assert info.height == 1080\n assert info.fps == 30.0\n assert info.codec == \"h264\"\n assert info.audio_codec == \"aac\"\n\n @patch(\"subprocess.run\")\n def test_get_video_info_failure(self, mock_run):\n \"\"\"Test video info extraction failure.\"\"\"\n mock_run.side_effect = Exception(\"ffprobe failed\")\n\n info = self.optimizer.get_video_info(Path(\"test.mp4\"))\n assert info is None\n\n def test_calculate_target_resolution_no_constraints(self):\n \"\"\"Test resolution calculation without constraints.\"\"\"\n width, height = self.optimizer.calculate_target_resolution(\n 1920, 1080, None, None\n )\n assert width == 1920\n assert height == 1080\n\n def test_calculate_target_resolution_width_constraint(self):\n \"\"\"Test resolution calculation with width constraint.\"\"\"\n width, height = self.optimizer.calculate_target_resolution(\n 1920, 1080, 1280, None\n )\n assert width == 1280\n assert height == 720\n\n def test_calculate_target_resolution_height_constraint(self):\n \"\"\"Test resolution calculation with height constraint.\"\"\"\n width, height = self.optimizer.calculate_target_resolution(\n 1920, 1080, None, 720\n )\n assert width == 1280\n assert height == 720\n\n def test_calculate_target_resolution_both_constraints(self):\n \"\"\"Test resolution calculation with both constraints.\"\"\"\n width, height = self.optimizer.calculate_target_resolution(\n 1920, 1080, 1280, 720\n )\n assert width == 1280\n assert height == 720\n\n def test_calculate_target_resolution_even_dimensions(self):\n \"\"\"Test that dimensions are always even.\"\"\"\n width, height = self.optimizer.calculate_target_resolution(\n 1920, 1080, 1279, None # Odd width\n )\n assert width % 2 == 0\n assert height % 2 == 0\n\n def test_calculate_target_resolution_no_upscale(self):\n \"\"\"Test that small videos are not upscaled.\"\"\"\n width, height = self.optimizer.calculate_target_resolution(\n 640, 480, 1920, 1080\n )\n assert width == 640\n assert height == 480\n\n @patch(\"subprocess.run\")\n @patch.object(VideoOptimizer, \"get_video_info\")\n def test_optimize_video_dry_run(self, mock_get_info, mock_run):\n \"\"\"Test video optimization in dry-run mode.\"\"\"\n mock_info = VideoInfo(\n path=Path(\"input.mp4\"),\n duration=120.0,\n width=1920,\n height=1080,\n bitrate=5000000,\n fps=30.0,\n size=75000000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n mock_get_info.return_value = mock_info\n\n optimizer = VideoOptimizer(dry_run=True)\n result = optimizer.optimize_video(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n max_width=1280\n )\n\n assert result is True\n mock_run.assert_not_called()\n\n @patch(\"subprocess.run\")\n @patch.object(VideoOptimizer, \"get_video_info\")\n def test_optimize_video_resolution_reduction(self, mock_get_info, mock_run):\n \"\"\"Test video optimization with resolution reduction.\"\"\"\n mock_info = VideoInfo(\n path=Path(\"input.mp4\"),\n duration=120.0,\n width=1920,\n height=1080,\n bitrate=5000000,\n fps=30.0,\n size=75000000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n mock_get_info.return_value = mock_info\n mock_run.return_value = MagicMock(returncode=0)\n\n result = self.optimizer.optimize_video(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n max_width=1280,\n max_height=720\n )\n\n assert result is True\n mock_run.assert_called_once()\n\n # Check that scale filter is applied\n cmd = mock_run.call_args[0][0]\n assert \"-vf\" in cmd\n filter_idx = cmd.index(\"-vf\")\n assert \"scale=1280:720\" in cmd[filter_idx + 1]\n\n @patch(\"subprocess.run\")\n @patch.object(VideoOptimizer, \"get_video_info\")\n def test_optimize_video_fps_reduction(self, mock_get_info, mock_run):\n \"\"\"Test video optimization with FPS reduction.\"\"\"\n mock_info = VideoInfo(\n path=Path(\"input.mp4\"),\n duration=120.0,\n width=1920,\n height=1080,\n bitrate=5000000,\n fps=60.0,\n size=75000000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n mock_get_info.return_value = mock_info\n mock_run.return_value = MagicMock(returncode=0)\n\n result = self.optimizer.optimize_video(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n target_fps=30.0\n )\n\n assert result is True\n\n # Check that FPS filter is applied\n cmd = mock_run.call_args[0][0]\n assert \"-r\" in cmd\n fps_idx = cmd.index(\"-r\")\n assert \"30.0\" in cmd[fps_idx + 1]\n\n @patch(\"subprocess.run\")\n @patch.object(VideoOptimizer, \"get_video_info\")\n def test_optimize_video_two_pass(self, mock_get_info, mock_run):\n \"\"\"Test two-pass encoding.\"\"\"\n mock_info = VideoInfo(\n path=Path(\"input.mp4\"),\n duration=120.0,\n width=1920,\n height=1080,\n bitrate=5000000,\n fps=30.0,\n size=75000000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n mock_get_info.return_value = mock_info\n mock_run.return_value = MagicMock(returncode=0)\n\n result = self.optimizer.optimize_video(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n two_pass=True\n )\n\n assert result is True\n # Should be called twice (pass 1 and pass 2)\n assert mock_run.call_count == 2\n\n # Check pass 1 command\n pass1_cmd = mock_run.call_args_list[0][0][0]\n assert \"-pass\" in pass1_cmd\n assert \"1\" in pass1_cmd\n\n # Check pass 2 command\n pass2_cmd = mock_run.call_args_list[1][0][0]\n assert \"-pass\" in pass2_cmd\n assert \"2\" in pass2_cmd\n\n @patch(\"subprocess.run\")\n @patch.object(VideoOptimizer, \"get_video_info\")\n def test_optimize_video_crf_encoding(self, mock_get_info, mock_run):\n \"\"\"Test CRF-based encoding (single pass).\"\"\"\n mock_info = VideoInfo(\n path=Path(\"input.mp4\"),\n duration=120.0,\n width=1920,\n height=1080,\n bitrate=5000000,\n fps=30.0,\n size=75000000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n mock_get_info.return_value = mock_info\n mock_run.return_value = MagicMock(returncode=0)\n\n result = self.optimizer.optimize_video(\n Path(\"input.mp4\"),\n Path(\"output.mp4\"),\n crf=23,\n two_pass=False\n )\n\n assert result is True\n mock_run.assert_called_once()\n\n # Check CRF parameter\n cmd = mock_run.call_args[0][0]\n assert \"-crf\" in cmd\n crf_idx = cmd.index(\"-crf\")\n assert \"23\" in cmd[crf_idx + 1]\n\n @patch(\"subprocess.run\")\n @patch.object(VideoOptimizer, \"get_video_info\")\n def test_optimize_video_failure(self, mock_get_info, mock_run):\n \"\"\"Test optimization failure handling.\"\"\"\n mock_info = VideoInfo(\n path=Path(\"input.mp4\"),\n duration=120.0,\n width=1920,\n height=1080,\n bitrate=5000000,\n fps=30.0,\n size=75000000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n mock_get_info.return_value = mock_info\n mock_run.side_effect = Exception(\"FFmpeg failed\")\n\n result = self.optimizer.optimize_video(\n Path(\"input.mp4\"),\n Path(\"output.mp4\")\n )\n\n assert result is False\n\n\nclass TestVideoInfo:\n \"\"\"Test VideoInfo dataclass.\"\"\"\n\n def test_video_info_creation(self):\n \"\"\"Test creating VideoInfo object.\"\"\"\n info = VideoInfo(\n path=Path(\"test.mp4\"),\n duration=120.5,\n width=1920,\n height=1080,\n bitrate=5000000,\n fps=30.0,\n size=75000000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n\n assert info.width == 1920\n assert info.height == 1080\n assert info.fps == 30.0\n assert info.codec == \"h264\"\n\n\nclass TestCompareVideos:\n \"\"\"Test video comparison functionality.\"\"\"\n\n @patch.object(VideoOptimizer, \"get_video_info\")\n def test_compare_videos_success(self, mock_get_info, capsys):\n \"\"\"Test video comparison output.\"\"\"\n orig_info = VideoInfo(\n path=Path(\"original.mp4\"),\n duration=120.0,\n width=1920,\n height=1080,\n bitrate=5000000,\n fps=30.0,\n size=75000000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n\n opt_info = VideoInfo(\n path=Path(\"optimized.mp4\"),\n duration=120.0,\n width=1280,\n height=720,\n bitrate=2500000,\n fps=30.0,\n size=37500000,\n codec=\"h264\",\n audio_codec=\"aac\",\n audio_bitrate=128000\n )\n\n mock_get_info.side_effect = [orig_info, opt_info]\n\n optimizer = VideoOptimizer()\n optimizer.compare_videos(Path(\"original.mp4\"), Path(\"optimized.mp4\"))\n\n captured = capsys.readouterr()\n assert \"Resolution\" in captured.out\n assert \"1920x1080\" in captured.out\n assert \"1280x720\" in captured.out\n assert \"50.0%\" in captured.out # Size reduction\n\n\nif __name__ == \"__main__\":\n pytest.main([__file__, \"-v\"])\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12131,"content_sha256":"483f996459f251f9f15affaca183e9bee558432a7b49903a87fff9d7da244273"},{"filename":"scripts/video_optimize.py","content":"#!/usr/bin/env python3\n\"\"\"\nVideo size optimization with quality/size balance.\n\nSupports resolution reduction, frame rate adjustment, audio bitrate optimization,\nmulti-pass encoding, and comparison metrics.\n\"\"\"\n\nimport argparse\nimport json\nimport subprocess\nimport sys\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Optional, Tuple\n\n\n@dataclass\nclass VideoInfo:\n \"\"\"Video file information.\"\"\"\n path: Path\n duration: float\n width: int\n height: int\n bitrate: int\n fps: float\n size: int\n codec: str\n audio_codec: str\n audio_bitrate: int\n\n\nclass VideoOptimizer:\n \"\"\"Handle video optimization operations using FFmpeg.\"\"\"\n\n def __init__(self, verbose: bool = False, dry_run: bool = False):\n self.verbose = verbose\n self.dry_run = dry_run\n\n def check_ffmpeg(self) -> bool:\n \"\"\"Check if FFmpeg is available.\"\"\"\n try:\n subprocess.run(\n ['ffmpeg', '-version'],\n stdout=subprocess.DEVNULL,\n stderr=subprocess.DEVNULL,\n check=True\n )\n return True\n except (subprocess.CalledProcessError, FileNotFoundError):\n return False\n\n def get_video_info(self, input_path: Path) -> Optional[VideoInfo]:\n \"\"\"Extract video information using ffprobe.\"\"\"\n try:\n cmd = [\n 'ffprobe',\n '-v', 'quiet',\n '-print_format', 'json',\n '-show_format',\n '-show_streams',\n str(input_path)\n ]\n\n result = subprocess.run(cmd, capture_output=True, check=True)\n data = json.loads(result.stdout)\n\n # Find video and audio streams\n video_stream = None\n audio_stream = None\n\n for stream in data['streams']:\n if stream['codec_type'] == 'video' and not video_stream:\n video_stream = stream\n elif stream['codec_type'] == 'audio' and not audio_stream:\n audio_stream = stream\n\n if not video_stream:\n return None\n\n # Parse frame rate\n fps_parts = video_stream.get('r_frame_rate', '0/1').split('/')\n fps = float(fps_parts[0]) / float(fps_parts[1]) if len(fps_parts) == 2 else 0\n\n return VideoInfo(\n path=input_path,\n duration=float(data['format'].get('duration', 0)),\n width=int(video_stream.get('width', 0)),\n height=int(video_stream.get('height', 0)),\n bitrate=int(data['format'].get('bit_rate', 0)),\n fps=fps,\n size=int(data['format'].get('size', 0)),\n codec=video_stream.get('codec_name', 'unknown'),\n audio_codec=audio_stream.get('codec_name', 'none') if audio_stream else 'none',\n audio_bitrate=int(audio_stream.get('bit_rate', 0)) if audio_stream else 0\n )\n\n except Exception as e:\n print(f\"Error getting video info: {e}\", file=sys.stderr)\n return None\n\n def calculate_target_resolution(\n self,\n width: int,\n height: int,\n max_width: Optional[int],\n max_height: Optional[int]\n ) -> Tuple[int, int]:\n \"\"\"Calculate target resolution maintaining aspect ratio.\"\"\"\n if not max_width and not max_height:\n return width, height\n\n aspect_ratio = width / height\n\n if max_width and max_height:\n # Fit within both constraints\n if width > max_width or height > max_height:\n if width / max_width > height / max_height:\n new_width = max_width\n new_height = int(max_width / aspect_ratio)\n else:\n new_height = max_height\n new_width = int(max_height * aspect_ratio)\n else:\n new_width, new_height = width, height\n elif max_width:\n new_width = min(width, max_width)\n new_height = int(new_width / aspect_ratio)\n else:\n new_height = min(height, max_height)\n new_width = int(new_height * aspect_ratio)\n\n # Ensure dimensions are even (required by some codecs)\n new_width = new_width - (new_width % 2)\n new_height = new_height - (new_height % 2)\n\n return new_width, new_height\n\n def optimize_video(\n self,\n input_path: Path,\n output_path: Path,\n max_width: Optional[int] = None,\n max_height: Optional[int] = None,\n target_fps: Optional[float] = None,\n crf: int = 23,\n audio_bitrate: str = '128k',\n preset: str = 'medium',\n two_pass: bool = False\n ) -> bool:\n \"\"\"Optimize a video file.\"\"\"\n # Get input video info\n info = self.get_video_info(input_path)\n if not info:\n print(f\"Error: Could not read video info for {input_path}\", file=sys.stderr)\n return False\n\n if self.verbose:\n print(f\"\\nInput video info:\")\n print(f\" Resolution: {info.width}x{info.height}\")\n print(f\" FPS: {info.fps:.2f}\")\n print(f\" Bitrate: {info.bitrate // 1000} kbps\")\n print(f\" Size: {info.size / (1024*1024):.2f} MB\")\n\n # Calculate target resolution\n target_width, target_height = self.calculate_target_resolution(\n info.width, info.height, max_width, max_height\n )\n\n # Build FFmpeg command\n cmd = ['ffmpeg', '-i', str(input_path)]\n\n # Video filters\n filters = []\n if target_width != info.width or target_height != info.height:\n filters.append(f'scale={target_width}:{target_height}')\n\n if filters:\n cmd.extend(['-vf', ','.join(filters)])\n\n # Frame rate adjustment\n if target_fps and target_fps \u003c info.fps:\n cmd.extend(['-r', str(target_fps)])\n\n # Video encoding\n if two_pass:\n # Two-pass encoding for better quality\n target_bitrate = int(info.bitrate * 0.7) # 30% reduction\n\n # Pass 1\n pass1_cmd = cmd + [\n '-c:v', 'libx264',\n '-preset', preset,\n '-b:v', str(target_bitrate),\n '-pass', '1',\n '-an',\n '-f', 'null',\n '/dev/null' if sys.platform != 'win32' else 'NUL'\n ]\n\n if self.verbose or self.dry_run:\n print(f\"Pass 1: {' '.join(pass1_cmd)}\")\n\n if not self.dry_run:\n try:\n subprocess.run(pass1_cmd, check=True, capture_output=not self.verbose)\n except subprocess.CalledProcessError as e:\n print(f\"Error in pass 1: {e}\", file=sys.stderr)\n return False\n\n # Pass 2\n cmd.extend([\n '-c:v', 'libx264',\n '-preset', preset,\n '-b:v', str(target_bitrate),\n '-pass', '2'\n ])\n else:\n # Single-pass CRF encoding\n cmd.extend([\n '-c:v', 'libx264',\n '-preset', preset,\n '-crf', str(crf)\n ])\n\n # Audio encoding\n cmd.extend([\n '-c:a', 'aac',\n '-b:a', audio_bitrate\n ])\n\n # Output\n cmd.extend(['-movflags', '+faststart', '-y', str(output_path)])\n\n if self.verbose or self.dry_run:\n print(f\"Command: {' '.join(cmd)}\")\n\n if self.dry_run:\n return True\n\n # Execute\n try:\n subprocess.run(cmd, check=True, capture_output=not self.verbose)\n\n # Get output info\n output_info = self.get_video_info(output_path)\n if output_info and self.verbose:\n print(f\"\\nOutput video info:\")\n print(f\" Resolution: {output_info.width}x{output_info.height}\")\n print(f\" FPS: {output_info.fps:.2f}\")\n print(f\" Bitrate: {output_info.bitrate // 1000} kbps\")\n print(f\" Size: {output_info.size / (1024*1024):.2f} MB\")\n reduction = (1 - output_info.size / info.size) * 100\n print(f\" Size reduction: {reduction:.1f}%\")\n\n return True\n\n except subprocess.CalledProcessError as e:\n print(f\"Error optimizing video: {e}\", file=sys.stderr)\n return False\n except Exception as e:\n print(f\"Error optimizing video: {e}\", file=sys.stderr)\n return False\n finally:\n # Clean up two-pass log files\n if two_pass and not self.dry_run:\n for log_file in Path('.').glob('ffmpeg2pass-*.log*'):\n log_file.unlink(missing_ok=True)\n\n def compare_videos(self, original: Path, optimized: Path) -> None:\n \"\"\"Compare original and optimized videos.\"\"\"\n orig_info = self.get_video_info(original)\n opt_info = self.get_video_info(optimized)\n\n if not orig_info or not opt_info:\n print(\"Error: Could not compare videos\", file=sys.stderr)\n return\n\n print(f\"\\n{'Metric':\u003c20} {'Original':\u003c20} {'Optimized':\u003c20} {'Change':\u003c15}\")\n print(\"-\" * 75)\n\n # Resolution\n orig_res = f\"{orig_info.width}x{orig_info.height}\"\n opt_res = f\"{opt_info.width}x{opt_info.height}\"\n print(f\"{'Resolution':\u003c20} {orig_res:\u003c20} {opt_res:\u003c20}\")\n\n # FPS\n fps_change = opt_info.fps - orig_info.fps\n print(f\"{'FPS':\u003c20} {orig_info.fps:\u003c20.2f} {opt_info.fps:\u003c20.2f} {fps_change:+.2f}\")\n\n # Bitrate\n orig_br = f\"{orig_info.bitrate // 1000} kbps\"\n opt_br = f\"{opt_info.bitrate // 1000} kbps\"\n br_change = ((opt_info.bitrate / orig_info.bitrate) - 1) * 100\n print(f\"{'Bitrate':\u003c20} {orig_br:\u003c20} {opt_br:\u003c20} {br_change:+.1f}%\")\n\n # Size\n orig_size = f\"{orig_info.size / (1024*1024):.2f} MB\"\n opt_size = f\"{opt_info.size / (1024*1024):.2f} MB\"\n size_reduction = (1 - opt_info.size / orig_info.size) * 100\n print(f\"{'Size':\u003c20} {orig_size:\u003c20} {opt_size:\u003c20} {-size_reduction:.1f}%\")\n\n\ndef main():\n \"\"\"Main entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description='Video size optimization with quality/size balance.'\n )\n parser.add_argument(\n 'input',\n type=Path,\n help='Input video file'\n )\n parser.add_argument(\n '-o', '--output',\n type=Path,\n required=True,\n help='Output video file'\n )\n parser.add_argument(\n '-w', '--max-width',\n type=int,\n help='Maximum width in pixels'\n )\n parser.add_argument(\n '-H', '--max-height',\n type=int,\n help='Maximum height in pixels'\n )\n parser.add_argument(\n '--fps',\n type=float,\n help='Target frame rate'\n )\n parser.add_argument(\n '--crf',\n type=int,\n default=23,\n help='CRF quality (18-28, lower=better, default: 23)'\n )\n parser.add_argument(\n '--audio-bitrate',\n default='128k',\n help='Audio bitrate (default: 128k)'\n )\n parser.add_argument(\n '--preset',\n choices=['ultrafast', 'superfast', 'veryfast', 'faster', 'fast',\n 'medium', 'slow', 'slower', 'veryslow'],\n default='medium',\n help='Encoding preset (default: medium)'\n )\n parser.add_argument(\n '--two-pass',\n action='store_true',\n help='Use two-pass encoding (better quality)'\n )\n parser.add_argument(\n '--compare',\n action='store_true',\n help='Compare original and optimized videos'\n )\n parser.add_argument(\n '-n', '--dry-run',\n action='store_true',\n help='Show command without executing'\n )\n parser.add_argument(\n '-v', '--verbose',\n action='store_true',\n help='Verbose output'\n )\n\n args = parser.parse_args()\n\n # Validate input\n if not args.input.exists():\n print(f\"Error: Input file not found: {args.input}\", file=sys.stderr)\n sys.exit(1)\n\n # Initialize optimizer\n optimizer = VideoOptimizer(verbose=args.verbose, dry_run=args.dry_run)\n\n # Check dependencies\n if not optimizer.check_ffmpeg():\n print(\"Error: FFmpeg not found\", file=sys.stderr)\n sys.exit(1)\n\n # Optimize video\n print(f\"Optimizing {args.input.name}...\")\n success = optimizer.optimize_video(\n args.input,\n args.output,\n args.max_width,\n args.max_height,\n args.fps,\n args.crf,\n args.audio_bitrate,\n args.preset,\n args.two_pass\n )\n\n if not success:\n sys.exit(1)\n\n # Compare if requested\n if args.compare and not args.dry_run:\n optimizer.compare_videos(args.input, args.output)\n\n print(f\"\\nOptimized video saved to: {args.output}\")\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13064,"content_sha256":"c2df1f79fc8314b94bade5e7ceee7b4d54459402a7f91bf12ebe71b640746b68"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Media Processing Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"Process video, audio, and images using FFmpeg, ImageMagick, and RMBG CLI tools.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Tool Selection","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Task","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reason","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Video encoding/conversion","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FFmpeg","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native codec support, streaming","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Audio extraction/conversion","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FFmpeg","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct stream manipulation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Image resize/effects","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ImageMagick","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Optimized for still images","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Background removal","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RMBG","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AI-powered, local processing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Batch images","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ImageMagick","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mogrify for in-place edits","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Video thumbnails","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FFmpeg","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Frame extraction built-in","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GIF creation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FFmpeg/ImageMagick","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FFmpeg for video, ImageMagick for images","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Installation","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# macOS\nbrew install ffmpeg imagemagick\nnpm install -g rmbg-cli\n\n# Ubuntu/Debian\nsudo apt-get install ffmpeg imagemagick\nnpm install -g rmbg-cli\n\n# Verify\nffmpeg -version && magick -version && rmbg --version","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Essential Commands","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Video: Convert/re-encode\nffmpeg -i input.mkv -c copy output.mp4\nffmpeg -i input.avi -c:v libx264 -crf 22 -c:a aac output.mp4\n\n# Video: Extract audio\nffmpeg -i video.mp4 -vn -c:a copy audio.m4a\n\n# Image: Convert/resize\nmagick input.png output.jpg\nmagick input.jpg -resize 800x600 output.jpg\n\n# Image: Batch resize\nmogrify -resize 800x -quality 85 *.jpg\n\n# Background removal\nrmbg input.jpg # Basic (modnet)\nrmbg input.jpg -m briaai -o output.png # High quality\nrmbg input.jpg -m u2netp -o output.png # Fast","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Parameters","type":"text"}]},{"type":"paragraph","content":[{"text":"FFmpeg:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-c:v libx264","type":"text","marks":[{"type":"code_inline"}]},{"text":" - H.264 codec","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-crf 22","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Quality (0-51, lower=better)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-preset slow","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Speed/compression balance","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-c:a aac","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Audio codec","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"ImageMagick:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"800x600","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Fit within (maintains aspect)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"800x600^","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Fill (may crop)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-quality 85","type":"text","marks":[{"type":"code_inline"}]},{"text":" - JPEG quality","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-strip","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Remove metadata","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"RMBG:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-m briaai","type":"text","marks":[{"type":"code_inline"}]},{"text":" - High quality model","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-m u2netp","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Fast model","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-r 4096","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Max resolution","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"paragraph","content":[{"text":"Detailed guides in ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ffmpeg-encoding.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Codecs, quality, hardware acceleration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ffmpeg-streaming.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - HLS/DASH, live streaming","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ffmpeg-filters.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Filters, complex filtergraphs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"imagemagick-editing.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Effects, transformations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"imagemagick-batch.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Batch processing, parallel ops","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rmbg-background-removal.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - AI models, CLI usage","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"common-workflows.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Video optimization, responsive images, GIF creation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"troubleshooting.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Error fixes, performance tips","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"format-compatibility.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Format support, codec recommendations","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"media-processing","author":"@skillopedia","source":{"stars":0,"repo_name":"theone-training-skills","origin_url":"https://github.com/the1studio/theone-training-skills/blob/HEAD/.claude/skills/media-processing/SKILL.md","repo_owner":"the1studio","body_sha256":"e34c91df8cab7abf41adf807e32219b4a8538161d37eb0720bc80d8a7338da69","cluster_key":"e3f186bbb7bc749202761fa1a6aa70c26c33f997b91125a30703f477110066c1","clean_bundle":{"format":"clean-skill-bundle-v1","source":"the1studio/theone-training-skills/.claude/skills/media-processing/SKILL.md","attachments":[{"id":"db157b78-862a-5ff1-a3f7-c511c8f9d879","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/db157b78-862a-5ff1-a3f7-c511c8f9d879/attachment.md","path":"references/common-workflows.md","size":2865,"sha256":"4ebf9159c7e6955a49b7323c1a1e367a9e63e7722ed7a71157b3267f67c77f5a","contentType":"text/markdown; charset=utf-8"},{"id":"a96e7c92-a2e1-524e-a585-de0ed738e2d4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a96e7c92-a2e1-524e-a585-de0ed738e2d4/attachment.md","path":"references/ffmpeg-encoding.md","size":9463,"sha256":"903682c3375524ce29a12f52c72e947ab114cee1187d88652360911d06a5759f","contentType":"text/markdown; charset=utf-8"},{"id":"5693eb3e-2661-5bf5-9551-ba1dce3baf14","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5693eb3e-2661-5bf5-9551-ba1dce3baf14/attachment.md","path":"references/ffmpeg-filters.md","size":11800,"sha256":"347447ae305410e5bb72a4934ad4fadeb91fdc12435551b038a7f895a49a72a0","contentType":"text/markdown; charset=utf-8"},{"id":"eeeb92b8-690d-5a6d-96fe-f422beb8bacb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eeeb92b8-690d-5a6d-96fe-f422beb8bacb/attachment.md","path":"references/ffmpeg-streaming.md","size":9562,"sha256":"087eab32cf7336e7815158635a4487db9640d038a22bb01a269277393c5b510f","contentType":"text/markdown; charset=utf-8"},{"id":"76056a30-efb9-54a3-a82d-445c590ac1ca","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/76056a30-efb9-54a3-a82d-445c590ac1ca/attachment.md","path":"references/format-compatibility.md","size":8667,"sha256":"900ac7216f595d94ae597096c2bf12cd62bfe0cf1841b143e4ac8564cd12e02f","contentType":"text/markdown; charset=utf-8"},{"id":"c3d2f1a0-50ab-5b37-8fc6-be25b30e7e51","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c3d2f1a0-50ab-5b37-8fc6-be25b30e7e51/attachment.md","path":"references/imagemagick-batch.md","size":12189,"sha256":"e9aefed340939dc2241df5a45f82a49516bd348b6340d2acb42b4a3cfdf30537","contentType":"text/markdown; charset=utf-8"},{"id":"000ca706-c1b0-5c28-b6c1-e4336612095a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/000ca706-c1b0-5c28-b6c1-e4336612095a/attachment.md","path":"references/imagemagick-editing.md","size":13182,"sha256":"61f9e9f4d7266db2ed6bd7774f4790c4146fab07c127413a1037b53af8096c0e","contentType":"text/markdown; charset=utf-8"},{"id":"ec10da89-6285-57d6-8a32-cab3862d51b0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ec10da89-6285-57d6-8a32-cab3862d51b0/attachment.md","path":"references/rmbg-background-removal.md","size":1685,"sha256":"2311024465a5e1e3c379e58406fb18f5bb6c7e2c861134c8ff3f9e5364aaaada","contentType":"text/markdown; charset=utf-8"},{"id":"873a19a4-96eb-5d61-aae4-00efeb381cbd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/873a19a4-96eb-5d61-aae4-00efeb381cbd/attachment.md","path":"references/troubleshooting.md","size":2729,"sha256":"064e02fa485a5493e84f362736b13f1d30862e461be3d5074d11504ee5438997","contentType":"text/markdown; charset=utf-8"},{"id":"0af4c5cc-ef74-59d1-a333-2a00eca1278c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0af4c5cc-ef74-59d1-a333-2a00eca1278c/attachment.md","path":"scripts/README.md","size":2606,"sha256":"8876aca16d9d78f900fb8cf370f09cc9369134a671a2f8b50788fb72ebd46ff4","contentType":"text/markdown; charset=utf-8"},{"id":"a3c8c243-bf54-5937-83bd-6d3139eeea9b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a3c8c243-bf54-5937-83bd-6d3139eeea9b/attachment.sh","path":"scripts/batch-remove-background.sh","size":3275,"sha256":"56c990116ccef526957766def641090242ad0b92609580b24b162e8ee7ae3cca","contentType":"application/x-sh; charset=utf-8"},{"id":"a17b050e-6414-5a6b-80d9-ffbe4eca8cc7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a17b050e-6414-5a6b-80d9-ffbe4eca8cc7/attachment.py","path":"scripts/batch_resize.py","size":10196,"sha256":"ce8e55a5546a0c49b84c8e97021a875db7be83f2f8f1a53ee1285e64d4e0b60e","contentType":"text/x-python; charset=utf-8"},{"id":"1e60c3e0-f7ca-57e2-bcd6-b8785c717a02","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1e60c3e0-f7ca-57e2-bcd6-b8785c717a02/attachment.py","path":"scripts/media_convert.py","size":8439,"sha256":"896961d11de1f7040243e388a82b30295e3224ec7b0afb4d023f4579c832e709","contentType":"text/x-python; charset=utf-8"},{"id":"f6393e63-48bc-51a5-b8ec-cff22310b39f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f6393e63-48bc-51a5-b8ec-cff22310b39f/attachment.sh","path":"scripts/remove-background.sh","size":2407,"sha256":"fe8caea66795c3d6b85c478de3e361f4983da486efa3813fda8bd1bcb792089e","contentType":"application/x-sh; charset=utf-8"},{"id":"4e66e471-dc05-5cc0-a77b-3096f4e6faac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4e66e471-dc05-5cc0-a77b-3096f4e6faac/attachment.js","path":"scripts/remove-bg-node.js","size":4187,"sha256":"1897902d7e6c9ed8837c9277021faaf54fb61c4d4360a09f5f4ea6bad730d7dd","contentType":"application/javascript; charset=utf-8"},{"id":"19ddbd11-121f-5cba-817b-f7007bff55a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/19ddbd11-121f-5cba-817b-f7007bff55a9/attachment.txt","path":"scripts/requirements.txt","size":558,"sha256":"656461e5a959cc78eda21807a73b5c20e78e6bd116fa89c7606c82eeacaab221","contentType":"text/plain; charset=utf-8"},{"id":"a0795141-efab-5d5e-b665-ca9f03076848","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a0795141-efab-5d5e-b665-ca9f03076848/attachment","path":"scripts/tests/.coverage","size":53248,"sha256":"86e832012e03a2fbebbed563c84e3fbfae6f6fa379db99696301593fa40790bd","contentType":"application/octet-stream"},{"id":"860990e4-b9a0-5299-94ac-0d8449c07a09","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/860990e4-b9a0-5299-94ac-0d8449c07a09/attachment.txt","path":"scripts/tests/requirements.txt","size":32,"sha256":"7f336e73b484fac1a0807a6cfba48eefe79c12f3c348d988a708dda2d6df6d14","contentType":"text/plain; charset=utf-8"},{"id":"331f6b78-2ccb-530a-845e-3bce23f559ea","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/331f6b78-2ccb-530a-845e-3bce23f559ea/attachment.py","path":"scripts/tests/test_batch_resize.py","size":10952,"sha256":"ce898f6a8f10596399a4dfaadb1abc9e2a133e18e375e1b6a5b5e56faf6d0033","contentType":"text/x-python; charset=utf-8"},{"id":"9c320310-1e4b-5fe2-8b47-5fa7d315185c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9c320310-1e4b-5fe2-8b47-5fa7d315185c/attachment.py","path":"scripts/tests/test_media_convert.py","size":7836,"sha256":"c067e325304b7cb84701a1481e2aa077ce8a29a04799a1b31a7768342d4c1f20","contentType":"text/x-python; charset=utf-8"},{"id":"3d423f2f-2c9f-5b52-b4bd-951f4b668453","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3d423f2f-2c9f-5b52-b4bd-951f4b668453/attachment.py","path":"scripts/tests/test_video_optimize.py","size":12131,"sha256":"483f996459f251f9f15affaca183e9bee558432a7b49903a87fff9d7da244273","contentType":"text/x-python; charset=utf-8"},{"id":"7a6f5ed4-1684-5ad7-9219-5f748165919e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7a6f5ed4-1684-5ad7-9219-5f748165919e/attachment.py","path":"scripts/video_optimize.py","size":13064,"sha256":"c2df1f79fc8314b94bade5e7ceee7b4d54459402a7f91bf12ebe71b640746b68","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"3b4e1af588f94a089999f6307fac34863aa4e99c7494f5cbad965a0056c6d811","attachment_count":22,"text_attachments":21,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":1,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":".claude/skills/media-processing/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"media-content","category_label":"Media"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"media-content","import_tag":"clean-skills-v1","description":"Process multimedia files with FFmpeg (video/audio encoding, conversion, streaming, filtering, hardware acceleration), ImageMagick (image manipulation, format conversion, batch processing, effects, composition), and RMBG (AI-powered background removal). Use when converting media formats, encoding videos with specific codecs (H.264, H.265, VP9), resizing/cropping images, removing backgrounds from 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."}},"renderedAt":1782979400937}

Media Processing Skill Process video, audio, and images using FFmpeg, ImageMagick, and RMBG CLI tools. Tool Selection | Task | Tool | Reason | |------|------|--------| | Video encoding/conversion | FFmpeg | Native codec support, streaming | | Audio extraction/conversion | FFmpeg | Direct stream manipulation | | Image resize/effects | ImageMagick | Optimized for still images | | Background removal | RMBG | AI-powered, local processing | | Batch images | ImageMagick | mogrify for in-place edits | | Video thumbnails | FFmpeg | Frame extraction built-in | | GIF creation | FFmpeg/ImageMagick | FFm…