Files
VMAFOptimiser/AGENTS.md
2025-12-31 19:16:10 +04:00

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