361 lines
8.2 KiB
Markdown
361 lines
8.2 KiB
Markdown
# VMAF Optimiser - Agent Guidelines
|
|
|
|
## Quick Reference
|
|
|
|
**Purpose:** Video library optimization pipeline using VMAF quality targets with AV1 encoding.
|
|
|
|
**Core Files:**
|
|
- `optimize_library.py` - Main Python script (342 lines)
|
|
- `run_optimisation.sh` - Linux/macOS wrapper
|
|
- `run_optimisation.ps1` - Windows wrapper
|
|
|
|
---
|
|
|
|
## Build/Lint/Test Commands
|
|
|
|
### Development Setup
|
|
|
|
```bash
|
|
# Install dependencies (if not already)
|
|
cargo install ab-av1 # v0.10.3+
|
|
brew install ffmpeg # macOS
|
|
# OR: apt install ffmpeg # Linux/WSL
|
|
# OR: winget install ffmpeg # Windows
|
|
```
|
|
|
|
### Linting
|
|
|
|
```bash
|
|
# Ruff is the linter (indicated by .ruff_cache/)
|
|
ruff check optimize_library.py
|
|
|
|
# Format with ruff
|
|
ruff format optimize_library.py
|
|
|
|
# Check specific issues
|
|
ruff check optimize_library.py --select E,F,W
|
|
```
|
|
|
|
### Running the Application
|
|
|
|
```bash
|
|
# Linux/macOS
|
|
./run_optimisation.sh --directory /media --vmaf 95 --workers 1
|
|
|
|
# Windows
|
|
.\run_optimisation.ps1 -directory "D:\Movies" -vmaf 95 -workers 1
|
|
|
|
# Direct Python execution
|
|
python3 optimize_library.py /media --vmaf 95 --preset 6 --workers 1
|
|
```
|
|
|
|
### Testing
|
|
|
|
**No formal test suite exists currently.** Test manually by:
|
|
|
|
```bash
|
|
# Test with single video file
|
|
python3 optimize_library.py /media/sample.mkv --vmaf 95 --workers 1
|
|
|
|
# Dry run (validate logic without encoding)
|
|
python3 optimize_library.py /media --vmaf 95 --thorough
|
|
|
|
# Check dependencies
|
|
python3 optimize_library.py 2>&1 | grep -E "(ffmpeg|ab-av1)"
|
|
```
|
|
|
|
---
|
|
|
|
## Code Style Guidelines
|
|
|
|
### Python Style (PEP 8 Compliant)
|
|
|
|
**Imports:**
|
|
```python
|
|
# Standard library first, grouped logically
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import json
|
|
import shutil
|
|
import platform
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from pathlib import Path
|
|
```
|
|
|
|
**Naming Conventions:**
|
|
```python
|
|
# Constants: UPPER_SNAKE_CASE
|
|
DEFAULT_VMAF = 95.0
|
|
DEFAULT_PRESET = 6
|
|
EXTENSIONS = {".mkv", ".mp4", ".mov", ".avi", ".ts"}
|
|
|
|
# Functions: snake_case
|
|
def get_video_info(filepath):
|
|
def build_ab_av1_command(input_path, output_path, args):
|
|
|
|
# Variables: snake_case
|
|
input_path = Path(filepath)
|
|
output_path = input_path.with_stem(input_path.stem + "_av1")
|
|
|
|
# Module-level cache: _PREFIX (private)
|
|
_AB_AV1_HELP_CACHE = {}
|
|
```
|
|
|
|
**Formatting:**
|
|
- 4-space indentation
|
|
- Line length: ~88-100 characters (ruff default: 88)
|
|
- No trailing whitespace
|
|
- One blank line between functions
|
|
- Two blank lines before class definitions (if any)
|
|
|
|
**Function Structure:**
|
|
```python
|
|
def function_name(param1, param2, optional_param=None):
|
|
"""Brief description if needed."""
|
|
try:
|
|
# Implementation
|
|
return result
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
return None # or handle gracefully
|
|
```
|
|
|
|
**Subprocess Calls:**
|
|
```python
|
|
# Use subprocess.run for all external commands
|
|
cmd = ["ffmpeg", "-i", input_file, output_file]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
|
|
# Check return codes explicitly
|
|
if result.returncode != 0:
|
|
print(f"Command failed: {result.stderr}")
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
```python
|
|
# Always wrap external tool calls in try-except
|
|
try:
|
|
info = get_video_info(filepath)
|
|
if not info:
|
|
return # Early return on None
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"FFmpeg failed: {e}")
|
|
return
|
|
|
|
# Use specific exception types when possible
|
|
except FileNotFoundError:
|
|
print("File not found")
|
|
except json.JSONDecodeError:
|
|
print("Invalid JSON")
|
|
```
|
|
|
|
### Platform Detection
|
|
|
|
```python
|
|
# Use platform module for OS detection
|
|
def is_wsl():
|
|
if os.environ.get("WSL_DISTRO_NAME"):
|
|
return True
|
|
try:
|
|
with open("/proc/sys/kernel/osrelease", "r") as f:
|
|
return "microsoft" in f.read().lower()
|
|
except FileNotFoundError:
|
|
return False
|
|
|
|
def platform_label():
|
|
system = platform.system()
|
|
if system == "Linux" and is_wsl():
|
|
return "Linux (WSL)"
|
|
return system
|
|
```
|
|
|
|
### Argument Parsing
|
|
|
|
```python
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Description")
|
|
parser.add_argument("directory", help="Root directory")
|
|
parser.add_argument("--vmaf", type=float, default=95.0, help="Target VMAF")
|
|
args = parser.parse_args()
|
|
```
|
|
|
|
---
|
|
|
|
## Shell Script Guidelines (run_optimisation.sh)
|
|
|
|
**Shebang & Error Handling:**
|
|
```bash
|
|
#!/bin/bash
|
|
set -e # Exit on error
|
|
```
|
|
|
|
**Color Output:**
|
|
```bash
|
|
COLOR_RED='\033[0;31m'
|
|
COLOR_GREEN='\033[0;32m'
|
|
COLOR_CYAN='\033[0;36m'
|
|
COLOR_RESET='\033[0m'
|
|
|
|
log_info() {
|
|
echo -e "${COLOR_CYAN}$*${COLOR_RESET}"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${COLOR_RED}ERROR: $*${COLOR_RESET}" >&2
|
|
}
|
|
```
|
|
|
|
**Argument Parsing:**
|
|
```bash
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--vmaf)
|
|
VMAF="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
DIRECTORY="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
```
|
|
|
|
---
|
|
|
|
## PowerShell Guidelines (run_optimisation.ps1)
|
|
|
|
**Parameter Declaration:**
|
|
```powershell
|
|
param(
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$Directory = ".",
|
|
[float]$Vmaf = 95.0,
|
|
[switch]$Thorough
|
|
)
|
|
```
|
|
|
|
**Error Handling:**
|
|
```powershell
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function Write-ColorOutput {
|
|
param([string]$Message, [string]$Color = "White")
|
|
Write-Host $Message -ForegroundColor $Color
|
|
}
|
|
```
|
|
|
|
**Process Management:**
|
|
```powershell
|
|
$process = Start-Process -FilePath $pythonCmd.Path -ArgumentList $arguments `
|
|
-NoNewWindow -PassThru
|
|
$process.WaitForExit()
|
|
$exitCode = $process.ExitCode
|
|
```
|
|
|
|
---
|
|
|
|
## Key Constraints & Best Practices
|
|
|
|
### When Modifying `optimize_library.py`
|
|
|
|
1. **Maintain platform compatibility:** Always test on Linux, Windows, and macOS
|
|
2. **Preserve subprocess patterns:** Use `subprocess.run` with `check=True`
|
|
3. **Handle missing dependencies:** Check `shutil.which()` before running tools
|
|
4. **Thread safety:** The script uses `ThreadPoolExecutor` - avoid global state
|
|
5. **Path handling:** Always use `Path` objects from `pathlib`
|
|
|
|
### When Modifying Wrapper Scripts
|
|
|
|
1. **Keep interfaces consistent:** Both scripts should accept the same parameters
|
|
2. **Preserve color output:** Users expect colored status messages
|
|
3. **Validate Python path:** Handle `python3` vs `python` vs `py`
|
|
4. **Check script existence:** Verify `optimize_library.py` exists before running
|
|
|
|
### File Organization
|
|
|
|
- Keep functions under 50 lines
|
|
- Use descriptive names (no abbreviations like `proc_file`, use `process_file`)
|
|
- Cache external command help text (see `_AB_AV1_HELP_CACHE`)
|
|
- Use constants for magic numbers and strings
|
|
|
|
### Hardware Acceleration
|
|
|
|
- Auto-detect via `normalize_hwaccel()` function
|
|
- Respect `--hwaccel` flag
|
|
- Check ab-av1 support with `ab_av1_supports()` before using flags
|
|
- Default: `auto` (d3d11va on Windows, videotoolbox on macOS, vaapi on Linux)
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Checking Tool Availability
|
|
```python
|
|
def check_dependencies():
|
|
missing = []
|
|
for tool in ["ffmpeg", "ffprobe", "ab-av1"]:
|
|
if not shutil.which(tool):
|
|
missing.append(tool)
|
|
if missing:
|
|
print(f"Error: Missing tools: {', '.join(missing)}")
|
|
sys.exit(1)
|
|
```
|
|
|
|
### Building Commands Conditionally
|
|
```python
|
|
cmd = ["ab-av1", "auto-encode", "-i", input_path]
|
|
|
|
if args.encoder:
|
|
if ab_av1_supports("auto-encode", "--encoder"):
|
|
cmd.extend(["--encoder", args.encoder])
|
|
else:
|
|
print("Warning: Encoder not supported")
|
|
```
|
|
|
|
### File Path Operations
|
|
```python
|
|
# Use pathlib for cross-platform paths
|
|
input_path = Path(filepath)
|
|
output_path = input_path.with_stem(input_path.stem + "_av1")
|
|
|
|
# Safe existence check
|
|
if output_path.exists():
|
|
print(f"Skipping: {input_path.name}")
|
|
return
|
|
```
|
|
|
|
---
|
|
|
|
## Version Control
|
|
|
|
```bash
|
|
# Check for changes
|
|
git status
|
|
|
|
# Format before committing
|
|
ruff format optimize_library.py
|
|
ruff check optimize_library.py
|
|
|
|
# Commit with conventional commits
|
|
git commit -m "feat: add hardware acceleration support"
|
|
git commit -m "fix: handle missing ffprobe gracefully"
|
|
git commit -m "docs: update setup instructions"
|
|
```
|
|
|
|
---
|
|
|
|
## Important Notes
|
|
|
|
1. **No type hints:** Current codebase doesn't use Python typing
|
|
2. **No formal tests:** Test manually with sample videos
|
|
3. **No package.json:** This is a standalone script, not a Python package
|
|
4. **Lock files:** `.lock/` directory created at runtime for multi-machine coordination
|
|
5. **Logs:** JSONL format in `logs/` directory for structured data
|
|
|
|
---
|
|
|
|
**Last Updated:** December 31, 2025
|