Refactored everything

This commit is contained in:
bnair
2026-01-03 11:57:26 +01:00
parent b4a82e0db5
commit f1e79ad01d
18 changed files with 2371 additions and 582 deletions

207
legacy/finalOptimiser.ps1 Normal file
View File

@@ -0,0 +1,207 @@
# SMART PRE-FLIGHT ENCODER - VERBOSE CONSOLE OUTPUT
# Usage: .\Encode-TVShows.ps1
param(
[string]$TvDir = "Z:\tv",
[string]$ContentDir = "Z:\content",
[int]$MaxJobs = 2,
[int]$Av1Q = 34,
[int]$HevcQ = 28,
[string]$EncoderAV1 = "av1_amf",
[string]$EncoderHEVC = "hevc_amf",
[switch]$SkipAV1 = $true
)
# --- CONFIGURATION ---
$Global:FFMPEG = "ffmpeg"
$Global:FFPROBE = "ffprobe"
$Global:TEMP_DIR = "C:\Users\bnair\Videos\encodes"
$Global:LockDir = "C:\Users\bnair\Videos\encodes\locks"
$LogFileTV = "C:\Users\bnair\Videos\encodes\encoding-log-tv.csv"
$LogFileContent = "C:\Users\bnair\Videos\encodes\encoding-log-content.csv"
if (-not (Test-Path $Global:TEMP_DIR)) { New-Item -ItemType Directory -Path $Global:TEMP_DIR -Force | Out-Null }
if (-not (Test-Path $Global:LockDir)) { New-Item -ItemType Directory -Path $Global:LockDir -Force | Out-Null }
function Init-LogFile {
param([string]$Path)
if (-not (Test-Path $Path)) { "Timestamp,File,InputSize,OutputSize,CodecUsed,Status,Savings" | Out-File -FilePath $Path -Encoding UTF8 }
}
Init-LogFile $LogFileTV
Init-LogFile $LogFileContent
function Test-Tools {
Write-Host "Setup: Checking required tools..." -ForegroundColor Cyan
if (-not (Get-Command $Global:FFMPEG -ErrorAction SilentlyContinue)) { Write-Host "ERROR: ffmpeg not found!" -ForegroundColor Red; exit 1 }
Write-Host "Setup: Tools found." -ForegroundColor Green
}
$SharedFunctions = {
function Get-LockId {
param([string]$FilePath)
try {
$pathBytes = [System.Text.Encoding]::UTF8.GetBytes($FilePath)
$hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($pathBytes)
return [BitConverter]::ToString($hash).Replace("-", "").Substring(0, 16)
} catch { return "unknown_lock" }
}
function Run-FFmpeg {
param($In, $Out, $Enc, $Q, $Seek=$null, $Duration=$null)
$argsList = "-hide_banner -loglevel error -y"
if ($Seek) { $argsList += " -ss $Seek" }
$argsList += " -i `"$In`""
if ($Duration) { $argsList += " -t $Duration" }
$argsList += " -c:v $Enc -usage transcoding -quality quality -rc cqp -qp_i $Q -qp_p $Q -qp_b $Q -c:a copy `"$Out`""
return Start-Process -FilePath "ffmpeg" -ArgumentList $argsList -NoNewWindow -Wait -PassThru
}
function Process-VideoFile {
param($InputFile, $CurrentLogFile, $LockDir, $TempDir, $Av1Q, $HevcQ, $EncAV1, $EncHEVC, $SkipAV1)
$pidStr = $PID.ToString()
$lockId = Get-LockId -FilePath $InputFile
$lockFile = Join-Path $LockDir "$lockId.lock"
try {
if (Test-Path $lockFile) { return }
$pidStr | Out-File -FilePath $lockFile -Force
$fileName = Split-Path $InputFile -Leaf
Write-Host "[$pidStr] Found: $fileName" -ForegroundColor White
# Skip Logic
$currentCodec = (& "ffprobe" -v error -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 "$InputFile" 2>&1)
if ($SkipAV1 -and ($currentCodec -match "av1" -or $currentCodec -match "hevc")) {
Write-Host "[$pidStr] SKIP: Already optimized ($currentCodec)" -ForegroundColor DarkGray
return
}
$inputSize = (Get-Item $InputFile).Length
# --- PHASE 1: PRE-FLIGHT SAMPLE ---
Write-Host "[$pidStr] Testing: Generating 60s sample..." -ForegroundColor Yellow
$tempSample = Join-Path $TempDir "$fileName.sample.mkv"
$procSample = Run-FFmpeg $InputFile $tempSample $EncAV1 $Av1Q "00:05:00" "60"
$doFullAV1 = $true
if ($procSample.ExitCode -eq 0 -and (Test-Path $tempSample)) {
$sampleSize = (Get-Item $tempSample).Length
# Threshold: 150MB for 60s is ~20Mbps (Likely bloat)
if ($sampleSize -gt 150MB) {
Write-Host "[$pidStr] Test Result: FAIL. Sample was $([math]::Round($sampleSize/1MB))MB. Too big for AV1." -ForegroundColor Red
$doFullAV1 = $false
} else {
Write-Host "[$pidStr] Test Result: PASS. Sample was $([math]::Round($sampleSize/1MB))MB. Proceeding with AV1." -ForegroundColor Green
}
Remove-Item $tempSample -Force
}
$finalStatus = "Failed"
$finalCodec = "None"
$finalSize = 0
$finalSavings = 0.00
# --- PHASE 2: FULL AV1 ---
if ($doFullAV1) {
Write-Host "[$pidStr] Action: Starting Full AV1 Encode..." -ForegroundColor Cyan
$tempAV1 = Join-Path $TempDir "$fileName.av1.mkv"
$procAV1 = Run-FFmpeg $InputFile $tempAV1 $EncAV1 $Av1Q
if ($procAV1.ExitCode -eq 0 -and (Test-Path $tempAV1)) {
$sizeAV1 = (Get-Item $tempAV1).Length
if ($sizeAV1 -lt $inputSize) {
$finalSavings = [math]::Round((1 - ($sizeAV1 / $inputSize)) * 100, 2)
Write-Host "[$pidStr] AV1 ACCEPTED: Saved ${finalSavings}%" -ForegroundColor Green
$finalOut = $InputFile -replace '\.mkv$', '_av1.mkv' -replace '\.mp4$', '_av1.mp4'
Move-Item $tempAV1 -Destination $finalOut -Force
Remove-Item $InputFile -Force
Rename-Item $finalOut -NewName $fileName -Force
"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'),`"$InputFile`",$inputSize,$sizeAV1,AV1,`"Replaced (AV1)`",$finalSavings%" | Out-File -FilePath $CurrentLogFile -Append -Encoding UTF8
return
} else {
Write-Host "[$pidStr] AV1 REJECTED: Larger than source ($([math]::Round($sizeAV1/1GB,2)) GB). Deleting..." -ForegroundColor Red
Remove-Item $tempAV1 -Force
}
}
}
# --- PHASE 3: HEVC FALLBACK ---
Write-Host "[$pidStr] Action: Trying HEVC Fallback..." -ForegroundColor Cyan
$tempHEVC = Join-Path $TempDir "$fileName.hevc.mkv"
$procHEVC = Run-FFmpeg $InputFile $tempHEVC $EncHEVC $HevcQ
if ($procHEVC.ExitCode -eq 0 -and (Test-Path $tempHEVC)) {
$sizeHEVC = (Get-Item $tempHEVC).Length
if ($sizeHEVC -lt $inputSize) {
$finalSavings = [math]::Round((1 - ($sizeHEVC / $inputSize)) * 100, 2)
Write-Host "[$pidStr] HEVC ACCEPTED: Saved ${finalSavings}%" -ForegroundColor Green
$finalOut = $InputFile -replace '\.mkv$', '_hevc.mkv' -replace '\.mp4$', '_hevc.mp4'
Move-Item $tempHEVC -Destination $finalOut -Force
Remove-Item $InputFile -Force
Rename-Item $finalOut -NewName $fileName -Force
$finalStatus = "Replaced (HEVC)"
$finalCodec = "HEVC"
$finalSize = $sizeHEVC
} else {
Write-Host "[$pidStr] HEVC REJECTED: Also larger. Keeping original." -ForegroundColor Red
Remove-Item $tempHEVC -Force
$finalStatus = "Rejected (Both Larger)"
$finalSize = $sizeHEVC
$finalCodec = "HEVC"
}
}
"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'),`"$InputFile`",$inputSize,$finalSize,$finalCodec,`"$finalStatus`",$finalSavings%" | Out-File -FilePath $CurrentLogFile -Append -Encoding UTF8
} finally {
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
}
}
}
function Process-Directory {
param($TargetDirectory, $TargetLogFile, $PhaseName)
if (-not (Test-Path $TargetDirectory)) { return }
Write-Host "`n=== PHASE: $PhaseName ===" -ForegroundColor Magenta
$files = Get-ChildItem -Path $TargetDirectory -Include *.mkv, *.mp4 -Recurse -File -ErrorAction SilentlyContinue
$videoFiles = $files | Where-Object { $_.Name -notmatch "_av1" -and $_.Name -notmatch "_hevc" }
Write-Host "Found $($videoFiles.Count) files." -ForegroundColor Cyan
$processed = 0
while ($processed -lt $videoFiles.Count) {
$batchSize = [math]::Min($MaxJobs, ($videoFiles.Count - $processed))
$currentBatch = $videoFiles[$processed..($processed + $batchSize - 1)]
$jobs = @()
foreach ($file in $currentBatch) {
$jobs += Start-Job -InitializationScript $SharedFunctions -ScriptBlock {
param($f, $log, $lock, $temp, $av1q, $hevcq, $e1, $e2, $skip)
Process-VideoFile $f $log $lock $temp $av1q $hevcq $e1 $e2 $skip
} -ArgumentList $file.FullName, $TargetLogFile, $Global:LockDir, $Global:TEMP_DIR, $Av1Q, $HevcQ, $EncoderAV1, $EncoderHEVC, $SkipAV1
}
while (($jobs | Where-Object { $_.State -eq 'Running' }).Count -gt 0) {
$jobs | Receive-Job
Start-Sleep -Seconds 2
}
$jobs | Receive-Job
$jobs | Remove-Job -Force
$processed += $batchSize
Write-Host "Progress Phase ${PhaseName}: $processed / $($videoFiles.Count)" -ForegroundColor Yellow
}
}
Test-Tools
Process-Directory $TvDir $LogFileTV "TV-Shows"
Process-Directory $ContentDir $LogFileContent "Content"
Write-Host "`nDone." -ForegroundColor Green

931
legacy/optimize_library.py Normal file
View File

@@ -0,0 +1,931 @@
import os
import sys
import subprocess
import argparse
import json
import shutil
import platform
import time
import signal
from pathlib import Path
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed, ProcessPoolExecutor
from threading import Lock, get_ident
# --- Configuration ---
DEFAULT_VMAF = 95.0
DEFAULT_PRESET = 6
DEFAULT_WORKERS = 1
DEFAULT_SAMPLES = 4
EXTENSIONS = {".mkv", ".mp4", ".mov", ".avi", ".ts"}
TARGETS = [94.0, 93.0, 92.0, 90.0]
MIN_SAVINGS_PERCENT = 12.0
TARGET_SAVINGS_FOR_ESTIMATE = 15.0
# Hardware encoder quality compensation (GPU needs higher VMAF target to match CPU quality)
HW_ENCODER_VMAF_OFFSET = 2.0
HW_ENCODERS = {"av1_amf", "av1_nvenc", "av1_qsv", "av1_vaapi"}
# Global state for resume capability
_processed_files = set()
_lock = Lock()
_shutdown_requested = False
_AB_AV1_HELP_CACHE = {}
_hw_worker_id = None # Thread ID of the designated hardware worker
def claim_hardware_worker():
"""Attempt to claim hardware worker status for this thread. Returns True if claimed."""
global _hw_worker_id
with _lock:
thread_id = get_ident()
if _hw_worker_id is None:
_hw_worker_id = thread_id
return True
return _hw_worker_id == thread_id
def signal_handler(signum, frame):
"""Handle graceful shutdown"""
global _shutdown_requested
_shutdown_requested = True
print("\n\n⚠️ Shutdown requested. Finishing current tasks...")
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
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 required tools: {', '.join(missing)}")
print(
"Please install FFmpeg and 'ab-av1' (via cargo install ab-av1) before running."
)
sys.exit(1)
def is_wsl():
if os.environ.get("WSL_DISTRO_NAME"):
return True
try:
with open("/proc/sys/kernel/osrelease", "r", encoding="utf-8") 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
def _ab_av1_help(subcommand):
cached = _AB_AV1_HELP_CACHE.get(subcommand)
if cached is not None:
return cached
try:
result = subprocess.run(
["ab-av1", subcommand, "--help"],
capture_output=True,
text=True,
check=False,
)
help_text = (result.stdout or "") + "\n" + (result.stderr or "")
except Exception:
help_text = ""
_AB_AV1_HELP_CACHE[subcommand] = help_text
return help_text
def ab_av1_supports(subcommand, flag):
return flag in _ab_av1_help(subcommand)
def normalize_hwaccel(value):
if value is None:
return None
v = value.strip()
if not v:
return None
v_lower = v.lower()
if v_lower in {"none", "off", "false", "0"}:
return None
if v_lower != "auto":
return v
system = platform.system()
if system == "Windows":
return "d3d11va"
if system == "Darwin":
return "videotoolbox"
return "vaapi"
def get_probe_data(filepath):
"""Get comprehensive video data using ffprobe"""
try:
cmd = [
"ffprobe",
"-v",
"quiet",
"-print_format",
"json",
"-show_streams",
"-show_format",
str(filepath),
]
# FIX: Added encoding="utf-8" and errors="ignore"
res = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
encoding="utf-8",
errors="ignore"
)
return json.loads(res.stdout)
except Exception as e:
print(f"Error probing {filepath}: {e}")
return None
def get_video_stats(data):
"""Extract video statistics from ffprobe output"""
if not data or "streams" not in data or "format" not in data:
return None
v_stream = next((s for s in data["streams"] if s["codec_type"] == "video"), None)
if not v_stream:
return None
size = int(data["format"].get("size", 0))
duration = float(data["format"].get("duration", 0))
# Calculate bitrate from file size and duration (more reliable than ffprobe's bitrate)
if size > 0 and duration > 0:
bitrate = int((size * 8) / duration / 1000)
else:
bitrate = int(data["format"].get("bitrate", 0)) // 1000
return {
"codec": v_stream.get("codec_name"),
"width": v_stream.get("width"),
"height": v_stream.get("height"),
"bitrate": bitrate,
"size": size,
"duration": duration,
}
def log_result(log_dir, log_name, data):
"""Log result to JSONL file"""
os.makedirs(log_dir, exist_ok=True)
log_file = Path(log_dir) / f"{log_name}.jsonl"
data["timestamp"] = datetime.now().isoformat()
with _lock:
with open(log_file, "a") as f:
f.write(json.dumps(data) + "\n")
def run_command_streaming(cmd, description=""):
"""Run command and stream output in real-time"""
print(f" [Running {description}]")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1,
)
if process.stdout:
for line in process.stdout:
if _shutdown_requested:
process.terminate()
break
print(f" {line.rstrip()}")
process.wait()
return process.returncode
def run_crf_search(
filepath,
target_vmaf,
preset,
temp_dir,
encoder="svt-av1",
use_hw=False,
hwaccel=None,
):
"""Run CRF search for a specific VMAF target"""
is_hw_encoder = encoder in HW_ENCODERS
# Apply VMAF offset for hardware encoders to match software quality
effective_vmaf = (
target_vmaf + HW_ENCODER_VMAF_OFFSET if is_hw_encoder else target_vmaf
)
# --- Build the Command (Used for both running and parsing) ---
cmd = [
"ab-av1",
"crf-search",
"-i",
str(filepath),
"--min-vmaf",
str(effective_vmaf),
"--preset",
str(preset),
"--max-encoded-percent",
"100",
"--temp-dir",
temp_dir,
"--samples",
"4",
]
# FORCE use of the specified encoder (Fixes the CPU default crash)
if ab_av1_supports("crf-search", "--encoder"):
cmd.extend(["--encoder", encoder])
# Hardware decode acceleration
if use_hw and hwaccel:
if ab_av1_supports("crf-search", "--enc-input"):
cmd.extend(["--enc-input", f"hwaccel={hwaccel}"])
if hwaccel == "vaapi":
cmd.extend(["--enc-input", "hwaccel_output_format=vaapi"])
vmaf_label = f"VMAF {effective_vmaf}" if is_hw_encoder else f"VMAF {target_vmaf}"
print(f" - Searching for CRF to hit {vmaf_label}...")
# 1. First Run: Stream output to console
returncode = run_command_streaming(cmd, f"crf-search {vmaf_label}")
if returncode == 0:
# 2. Second Run: Capture output to parse the CRF
# FIX: Reuse 'cmd' and add encoding parameters to prevent charmap crashes
res = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="ignore"
)
lines = res.stdout.strip().split("\n")
for line in reversed(lines):
if "crf" in line.lower():
try:
parts = line.split()
crf_val = float(parts[1])
percent = 100.0
for p in parts:
if "%" in p:
percent = float(p.strip("()%"))
break
return {
"crf": crf_val,
"predicted_percent": percent,
"vmaf": target_vmaf,
}
except Exception as e:
print(f" ! Failed to parse crf-search output: {e}")
return None
def find_target_savings_params(
filepath,
start_vmaf,
preset,
temp_dir,
encoder="svt-av1",
use_hw=False,
hwaccel=None,
):
"""Find VMAF target that achieves minimum savings"""
print(f"\n --- Finding VMAF for {TARGET_SAVINGS_FOR_ESTIMATE}% savings ---")
test_targets = [t for t in TARGETS if t <= start_vmaf]
for i, target in enumerate(test_targets):
if _shutdown_requested:
return None
print(
f" Testing VMAF {target} for {TARGET_SAVINGS_FOR_ESTIMATE}% target... (test {i + 1}/{len(test_targets)})"
)
result = run_crf_search(
filepath, target, preset, temp_dir, encoder, use_hw, hwaccel
)
if result:
predicted_savings = 100.0 - result["predicted_percent"]
quality_drop = start_vmaf - target
print(
f" ✓ VMAF {target}: CRF {result['crf']}, Savings {predicted_savings:.1f}%, Drop -{quality_drop:.0f}"
)
if predicted_savings >= TARGET_SAVINGS_FOR_ESTIMATE:
print(f"\n ✅ FOUND {TARGET_SAVINGS_FOR_ESTIMATE}%+ SAVINGS:")
print(f" Target VMAF: {target} (quality drop: -{quality_drop:.0f})")
print(f" CRF: {result['crf']}")
print(f" Predicted savings: {predicted_savings:.1f}%")
return {
"target_vmaf": target,
"crf": result["crf"],
"savings": predicted_savings,
"quality_drop": quality_drop,
"found": True,
}
else:
print(f" ✗ Could not achieve VMAF {target}")
print(f"\n 📝 COULD NOT ACHIEVE {TARGET_SAVINGS_FOR_ESTIMATE}% SAVINGS")
print(f" Tried VMAF targets: {test_targets}")
return None
def run_encode(
filepath, output_path, crf, preset, encoder="svt-av1", use_hw=False, hwaccel=None
):
"""Run full encoding with real-time output streaming"""
cmd = [
"ab-av1",
"encode",
"--input",
str(filepath),
"--output",
str(output_path),
"--crf",
str(crf),
"--preset",
str(preset),
"--acodec",
"copy",
]
if encoder != "svt-av1":
if ab_av1_supports("encode", "--encoder"):
cmd.extend(["--encoder", encoder])
if use_hw and hwaccel:
if ab_av1_supports("encode", "--enc-input"):
cmd.extend(["--enc-input", f"hwaccel={hwaccel}"])
if hwaccel == "vaapi":
cmd.extend(["--enc-input", "hwaccel_output_format=vaapi"])
return run_command_streaming(cmd, f"encoding (CRF {crf})")
def provide_recommendations(
stats_before, hit_vmaf, predicted_savings, target_result=None
):
"""Provide recommendations based on analysis results"""
print(f"\n --- Recommendations for {stats_before['codec']} content ---")
if target_result and target_result["found"]:
print(f" 📊 TO HIT {TARGET_SAVINGS_FOR_ESTIMATE}% SAVINGS:")
print(
f" → Target VMAF: {target_result['target_vmaf']} (drop: -{target_result['quality_drop']:.0f})"
)
print(f" → CRF: {target_result['crf']}")
print(f" → Predicted: {target_result['savings']:.1f}% savings")
print(f" → Trade-off: Quality reduction for space savings")
print()
if stats_before["bitrate"] < 2000:
print(f" → Source bitrate is low ({stats_before['bitrate']}k)")
print(f" → AV1 gains minimal on highly compressed sources")
print(f" → Recommendation: SKIP - Source already optimized")
return
if stats_before["codec"] in ["hevc", "h265", "vp9"]:
print(f" → Source already uses modern codec ({stats_before['codec']})")
print(f" → AV1 gains minimal on already-compressed content")
print(f" → Recommendation: SKIP - Codec already efficient")
return
if target_result and not target_result["found"]:
print(
f" → Could not achieve {TARGET_SAVINGS_FOR_ESTIMATE}% even at lowest VMAF"
)
print(f" → Content may not compress well with AV1")
print(f" → Recommendation: SKIP - Review manually")
def refresh_plex(plex_url, plex_token):
"""Refresh Plex library after encoding completion"""
if not plex_url or not plex_token:
return
try:
print("\n📡 Refreshing Plex library...")
cmd = [
"curl",
"-X",
"GET",
f"{plex_url}/library/sections/1/refresh",
"-H",
f"X-Plex-Token: {plex_token}",
]
subprocess.run(cmd, capture_output=True, check=False)
print(" ✓ Plex refresh triggered")
except Exception as e:
print(f" ⚠️ Failed to refresh Plex: {e}")
def process_file(
filepath,
log_dir,
log_name,
preset,
hw_encoder="svt-av1",
use_hw_mode=False,
hwaccel=None,
):
"""Process a single video file with intelligent VMAF targeting"""
global _shutdown_requested
# --- LOGIC FIX START ---
# If "use_hw_mode" (Hybrid) is FALSE, use the requested encoder for EVERYONE.
if not use_hw_mode:
encoder = hw_encoder
use_hw = (encoder in HW_ENCODERS)
else:
# Hybrid Mode: Only 1 worker gets HW, rest get CPU
use_hw = False
if hwaccel and hw_encoder in HW_ENCODERS:
use_hw = claim_hardware_worker()
encoder = hw_encoder if use_hw else "svt-av1"
# --- LOGIC FIX END ---
filepath = Path(filepath)
lock_file = Path(log_dir).parent / ".lock" / f"{filepath.name}.lock"
# Check lock file (multi-machine coordination)
if lock_file.exists():
print(f"Skipping (Locked): {filepath.name}")
return True
# Create lock
lock_file.parent.mkdir(parents=True, exist_ok=True)
lock_file.touch()
try:
probe_data = get_probe_data(filepath)
if not probe_data:
print(f"Skipping (Invalid probe data): {filepath.name}")
return True
stats_before = get_video_stats(probe_data)
if not stats_before or stats_before["codec"] == "av1":
print(f"Skipping (Already AV1 or invalid): {filepath.name}")
return True
# Mark as processed
file_key = str(filepath)
with _lock:
if file_key in _processed_files:
return True
_processed_files.add(file_key)
print(f"\n--- Processing: {filepath.name} ---")
# Added 'errors="ignore"' to print to avoid Unicode crashes on console
try:
print(
f" Source: {stats_before['codec']} @ {stats_before['bitrate']}k, {stats_before['size'] / (1024**3):.2f} GB"
)
except UnicodeEncodeError:
print(f" Source: {stats_before['codec']} @ {stats_before['bitrate']}k")
if _shutdown_requested:
return False
temp_dir = Path(log_dir).parent / "tmp"
temp_dir.mkdir(exist_ok=True)
# Step 1: Try VMAF 94
print(f"\n [Step 1] Testing VMAF 94...")
search_result_94 = run_crf_search(
filepath, 94.0, preset, str(temp_dir), encoder, use_hw, hwaccel
)
if not search_result_94:
print(f" !! Could not hit VMAF 94")
search_result_94 = run_crf_search(
filepath, 93.0, preset, str(temp_dir), encoder, use_hw, hwaccel
)
if not search_result_94:
search_result_94 = run_crf_search(
filepath, 92.0, preset, str(temp_dir), encoder, use_hw, hwaccel
)
if not search_result_94:
search_result_94 = run_crf_search(
filepath, 90.0, preset, str(temp_dir), encoder, use_hw, hwaccel
)
if not search_result_94:
print(f" !! Failed all VMAF targets ({TARGETS}) for {filepath.name}")
log_result(
log_dir,
"failed_searches",
{
"file": str(filepath),
"status": "all_targets_failed",
"targets": TARGETS,
},
)
provide_recommendations(stats_before, None, 0)
return False
crf_94 = search_result_94["crf"]
predicted_savings_94 = 100.0 - search_result_94["predicted_percent"]
if predicted_savings_94 >= MIN_SAVINGS_PERCENT:
print(
f"\n ✅ VMAF 94 gives {predicted_savings_94:.1f}% savings (≥{MIN_SAVINGS_PERCENT}%)"
)
print(f" → Proceeding with VMAF 94, CRF {crf_94}")
encode_params = {
"crf": crf_94,
"vmaf": 94.0,
"predicted_percent": search_result_94["predicted_percent"],
}
else:
print(
f"\n ⚠️ VMAF 94 gives {predicted_savings_94:.1f}% savings (<{MIN_SAVINGS_PERCENT}%)"
)
search_result_93 = run_crf_search(
filepath, 93.0, preset, str(temp_dir), encoder, use_hw, hwaccel
)
if search_result_93:
predicted_savings_93 = 100.0 - search_result_93["predicted_percent"]
if predicted_savings_93 >= MIN_SAVINGS_PERCENT:
print(
f" ✅ VMAF 93 gives {predicted_savings_93:.1f}% savings (≥{MIN_SAVINGS_PERCENT}%)"
)
print(
f" → Proceeding with VMAF 93, CRF {search_result_93['crf']}"
)
encode_params = {
"crf": search_result_93["crf"],
"vmaf": 93.0,
"predicted_percent": search_result_93["predicted_percent"],
}
else:
print(
f" ⚠️ VMAF 93 gives {predicted_savings_93:.1f}% savings (also <{MIN_SAVINGS_PERCENT}%)"
)
print(
f" → Finding VMAF for {TARGET_SAVINGS_FOR_ESTIMATE}% savings..."
)
target_result = find_target_savings_params(
filepath, 93.0, preset, str(temp_dir), encoder, use_hw, hwaccel
)
provide_recommendations(
stats_before, 93.0, predicted_savings_93, target_result
)
log_result(
log_dir,
"low_savings_skips",
{
"file": str(filepath),
"vmaf_94": 94.0,
"savings_94": predicted_savings_94,
"vmaf_93": 93.0,
"savings_93": predicted_savings_93,
"target_for_15_percent": target_result,
"recommendations": "logged_for_review",
},
)
return True
else:
print(f" !! Could not achieve VMAF 93")
log_result(
log_dir,
"failed_searches",
{"file": str(filepath), "status": "vmaf_93_failed"},
)
return False
temp_output = temp_dir / f"{filepath.stem}.av1_temp.mkv"
if temp_output.exists():
temp_output.unlink()
start_time = time.time()
res = run_encode(
filepath,
temp_output,
encode_params["crf"],
preset,
encoder,
use_hw,
hwaccel,
)
if res != 0:
print(f"\n !! Encode failed with code {res}")
if temp_output.exists():
temp_output.unlink()
log_result(
log_dir,
"failed_encodes",
{"file": str(filepath), "status": "encode_failed", "returncode": res},
)
return False
encode_duration = time.time() - start_time
print(f" ✓ Encode completed in {encode_duration:.1f}s")
probe_after = get_probe_data(temp_output)
stats_after = get_video_stats(probe_after)
if not stats_after:
print(f" !! Failed to probe encoded file")
if temp_output.exists():
temp_output.unlink()
return False
actual_savings = (1 - (stats_after["size"] / stats_before["size"])) * 100
print(f"\n Results:")
print(
f" - Before: {stats_before['size'] / (1024**3):.2f} GB @ {stats_before['bitrate']}k"
)
print(
f" - After: {stats_after['size'] / (1024**3):.2f} GB @ {stats_after['bitrate']}k"
)
print(f" - Savings: {actual_savings:.2f}%")
final_path = filepath
if filepath.suffix.lower() == ".mp4":
final_path = filepath.with_suffix(".mkv")
if final_path.exists():
final_path.unlink()
shutil.move(str(filepath), str(final_path))
shutil.move(str(temp_output), str(final_path))
print(f" ✓ Successfully optimized: {final_path.name}")
log_result(
log_dir,
log_name,
{
"file": str(final_path),
"status": "success",
"vmaf": encode_params["vmaf"],
"crf": encode_params["crf"],
"before": stats_before,
"after": stats_after,
"duration": encode_duration,
"savings": actual_savings,
},
)
return True
finally:
# Remove lock file
if lock_file.exists():
lock_file.unlink()
def scan_library(root, exclude_dirs=None):
"""Scan library for video files, excluding certain directories"""
exclude_dirs = exclude_dirs or []
files = []
for dirpath, dirnames, filenames in os.walk(root):
# Skip excluded directories
dirnames[:] = [d for d in dirnames if d not in exclude_dirs]
for filename in filenames:
if Path(filename).suffix.lower() not in EXTENSIONS:
continue
full_path = Path(dirpath) / filename
if "_av1" in full_path.stem:
continue
files.append(full_path)
return files
def main():
parser = argparse.ArgumentParser(
description="Optimize video library to AV1 using VMAF targeting."
)
parser.add_argument("directory", help="Root directory to scan")
parser.add_argument(
"--vmaf",
type=float,
default=DEFAULT_VMAF,
help=f"Target VMAF score (default: {DEFAULT_VMAF})",
)
parser.add_argument(
"--preset",
type=int,
default=DEFAULT_PRESET,
help=f"SVT-AV1 Preset (default: {DEFAULT_PRESET})",
)
parser.add_argument(
"--workers",
type=int,
default=DEFAULT_WORKERS,
help=f"Concurrent files to process (default: {DEFAULT_WORKERS})",
)
parser.add_argument(
"--samples",
type=int,
default=DEFAULT_SAMPLES,
help=f"Samples to use for CRF search if supported (default: {DEFAULT_SAMPLES})",
)
parser.add_argument(
"--hwaccel",
default=None,
help=(
"Hardware acceleration for decode. "
"Examples: auto, vaapi, d3d11va, videotoolbox. Use 'none' to disable."
),
)
parser.add_argument(
"--encoder",
default="svt-av1",
help=(
"Video encoder to use. Default: svt-av1 (CPU). "
"Hardware encoders: av1_amf (AMD), av1_nvenc (NVIDIA), av1_qsv (Intel)."
),
)
parser.add_argument(
"--use-hardware-worker",
action="store_true",
help="Use 1 hardware encoding worker + rest CPU workers (requires --encoder with HW encoder)",
)
parser.add_argument(
"--plex-url",
default=None,
help="Plex server URL (e.g., http://localhost:32400)",
)
parser.add_argument(
"--plex-token",
default=None,
help="Plex auth token",
)
parser.add_argument(
"--log-dir",
default="/opt/Optmiser/logs",
help="Log directory (default: /opt/Optmiser/logs)",
)
args = parser.parse_args()
if args.workers < 1:
print("Error: --workers must be >= 1")
sys.exit(2)
check_dependencies()
root = Path(args.directory)
if not root.exists():
print(f"Directory not found: {root}")
sys.exit(1)
hwaccel = normalize_hwaccel(args.hwaccel)
print(f"Platform: {platform_label()}")
print(f"Scanning library: {root}")
print(f"VMAF targets: {TARGETS}")
print(f"Minimum savings: {MIN_SAVINGS_PERCENT}%")
print(f"Estimate target: {TARGET_SAVINGS_FOR_ESTIMATE}%")
print(f"Encoder Preset: {args.preset}")
print(f"Workers: {args.workers}")
if hwaccel:
print(f"HWAccel: {hwaccel}")
if args.use_hardware_worker:
print(f"Hardware worker: 1 HW + {args.workers - 1} CPU")
if args.plex_url:
print(f"Plex refresh: Enabled")
print("-" * 60)
# Determine log name based on directory
root_parts = str(root).lower().split("/")
if "content" in root_parts:
log_name = "content"
exclude_dirs = []
else:
log_name = "tv_movies"
exclude_dirs = ["content"]
print(f"Log file: {log_name}.jsonl")
files = scan_library(root, exclude_dirs)
if not files:
print("No media files found.")
return
print(f"Found {len(files)} files to process")
print("-" * 60)
processed_count = 0
success_count = 0
fail_count = 0
# Hardware worker configuration
# HW worker uses specified encoder; CPU workers use svt-av1
hw_encoder = args.encoder if args.encoder in HW_ENCODERS else None
use_hw_primary = args.use_hardware_worker and hw_encoder is not None
if args.workers == 1:
# Single thread - just process files
for file_path in files:
if _shutdown_requested:
break
processed_count += 1
result = process_file(
file_path,
args.log_dir,
log_name,
args.preset,
args.encoder,
use_hw_primary,
hwaccel,
)
if result:
success_count += 1
else:
fail_count += 1
else:
# Multi-threaded processing
with ThreadPoolExecutor(max_workers=args.workers) as executor:
futures = []
for file_path in files:
if _shutdown_requested:
break
# All workers try to claim HW; only the first thread succeeds
# and will use HW for ALL its tasks
future = executor.submit(
process_file,
file_path,
args.log_dir,
log_name,
args.preset,
args.encoder,
use_hw_primary,
hwaccel,
)
futures.append(future)
for future in as_completed(futures):
if _shutdown_requested:
break
processed_count += 1
try:
result = future.result()
if result:
success_count += 1
else:
fail_count += 1
except Exception as e:
print(f" !! Unexpected error: {e}")
import traceback
traceback.print_exc()
fail_count += 1
print("\n" + "=" * 60)
print(f"SUMMARY: {root}")
print(f" Processed: {processed_count} files")
print(f" Success/Skip: {success_count}")
print(f" Failed: {fail_count}")
print("=" * 60)
# Refresh Plex on completion
if success_count > 0:
refresh_plex(args.plex_url, args.plex_token)
if __name__ == "__main__":
main()

100
legacy/run_optimisation.ps1 Normal file
View File

@@ -0,0 +1,100 @@
param(
[Parameter(Mandatory=$false)]
[string]$Directory = ".",
[float]$Vmaf = 95.0,
[int]$Preset = 6,
[int]$Workers = 1,
[int]$Samples = 4,
[string]$Hwaccel = "",
[string]$Encoder = "svt-av1",
[switch]$UseHardwareWorker,
[string]$PlexUrl = "",
[string]$PlexToken = "",
[string]$LogDir = "/opt/Optmiser/logs"
)
$ErrorActionPreference = "Stop"
function Write-ColorOutput {
param([string]$Message, [string]$Color = "White")
Write-Host $Message -ForegroundColor $Color
}
function Invoke-OptimizeLibrary {
$scriptPath = Join-Path $PSScriptRoot "optimize_library.py"
if (-not (Test-Path $scriptPath)) {
Write-ColorOutput -Message "ERROR: optimize_library.py not found in current directory" -Color "Red"
exit 1
}
$pythonCmd = Get-Command "python"
if (-not $pythonCmd) {
Write-ColorOutput -Message "ERROR: Python 3 not found. Please install Python 3." -Color "Red"
exit 1
}
$arguments = @(
$scriptPath,
$Directory,
"--vmaf", $Vmaf.ToString("F1"),
"--preset", $Preset.ToString(),
"--workers", $Workers.ToString(),
"--samples", $Samples.ToString(),
"--encoder", $Encoder,
"--log-dir", $LogDir
)
if ($Hwaccel) {
$arguments += "--hwaccel", $Hwaccel
}
if ($UseHardwareWorker) {
$arguments += "--use-hardware-worker"
}
if ($PlexUrl) {
$arguments += "--plex-url", $PlexUrl
}
if ($PlexToken) {
$arguments += "--plex-token", $PlexToken
}
Write-ColorOutput -Message "Running optimize_library.py..." -Color "Cyan"
Write-ColorOutput -Message " Directory: $Directory" -Color "White"
Write-ColorOutput -Message " Target VMAF: $Vmaf" -Color "White"
Write-ColorOutput -Message " Preset: $Preset" -Color "White"
Write-ColorOutput -Message " Workers: $Workers" -Color "White"
Write-ColorOutput -Message " Samples: $Samples" -Color "White"
Write-ColorOutput -Message " Encoder: $Encoder" -Color "White"
if ($Hwaccel) {
Write-ColorOutput -Message " HW Accel: $Hwaccel" -Color "White"
}
if ($UseHardwareWorker) {
Write-ColorOutput -Message " Hardware worker: Enabled (1 HW + $($Workers - 1) CPU)" -Color "White"
}
if ($PlexUrl -and $PlexToken) {
Write-ColorOutput -Message " Plex refresh: Enabled" -Color "White"
}
Write-Host ""
$process = Start-Process -FilePath $pythonCmd.Path -ArgumentList $arguments -NoNewWindow -PassThru
$process.WaitForExit()
$exitCode = $process.ExitCode
if ($exitCode -eq 0) {
Write-ColorOutput -Message "SUCCESS: Library optimization completed" -Color "Green"
} else {
Write-ColorOutput -Message "ERROR: optimize_library.py exited with code $exitCode" -Color "Red"
}
exit $exitCode
}
Write-ColorOutput -Message "========================================" -Color "Cyan"
Write-ColorOutput -Message "VMAF Library Optimiser (Windows)" -Color "Yellow"
Write-ColorOutput -Message "========================================" -Color "Cyan"
Write-Host ""
Invoke-OptimizeLibrary

158
legacy/run_optimisation.sh Normal file
View File

@@ -0,0 +1,158 @@
#!/bin/bash
set -e
COLOR_RED='\033[0;31m'
COLOR_GREEN='\033[0;32m'
COLOR_CYAN='\033[0;36m'
COLOR_YELLOW='\033[1;33m'
COLOR_WHITE='\033[0;37m'
COLOR_RESET='\033[0m'
log_info() {
echo -e "${COLOR_CYAN}$*${COLOR_RESET}"
}
log_error() {
echo -e "${COLOR_RED}ERROR: $*${COLOR_RESET}" >&2
}
log_success() {
echo -e "${COLOR_GREEN}$*${COLOR_RESET}"
}
DIRECTORY="."
VMAF="95.0"
PRESET="6"
WORKERS="1"
SAMPLES="4"
HWACCEL=""
USE_HW_WORKER=""
PLEX_URL=""
PLEX_TOKEN=""
LOG_DIR="/opt/Optmiser/logs"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--directory)
DIRECTORY="$2"
shift 2
;;
--vmaf)
VMAF="$2"
shift 2
;;
--preset)
PRESET="$2"
shift 2
;;
--workers)
WORKERS="$2"
shift 2
;;
--samples)
SAMPLES="$2"
shift 2
;;
--hwaccel)
HWACCEL="$2"
shift 2
;;
--use-hardware-worker)
USE_HARDWARE_WORKER="true"
shift
;;
--plex-url)
PLEX_URL="$2"
shift 2
;;
--plex-token)
PLEX_TOKEN="$2"
shift 2
;;
--log-dir)
LOG_DIR="$2"
shift 2
;;
*)
DIRECTORY="$1"
shift
;;
esac
done
# Check if python3 is available
if ! command -v python3 &> /dev/null; then
if ! command -v python &> /dev/null; then
log_error "Python 3 not found. Please install Python 3."
exit 1
else
PYTHON_CMD="python"
fi
else
PYTHON_CMD="python3"
fi
# Check if optimize_library.py exists
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_PATH="$SCRIPT_DIR/optimize_library.py"
if [[ ! -f "$SCRIPT_PATH" ]]; then
log_error "optimize_library.py not found in: $SCRIPT_DIR"
exit 1
fi
# Build command arguments
ARGS=(
"$PYTHON_CMD" "$SCRIPT_PATH"
"$DIRECTORY"
--vmaf "$VMAF"
--preset "$PRESET"
--workers "$WORKERS"
--samples "$SAMPLES"
--log-dir "$LOG_DIR"
)
if [[ -n "$THOROUGH" ]]; then
ARGS+=(--thorough)
fi
if [[ -n "$HWACCEL" ]]; then
ARGS+=(--hwaccel "$HWACCEL")
fi
# Print configuration
log_info "========================================"
log_info "VMAF Library Optimiser (Linux/Server)"
log_info "========================================"
echo ""
log_info "Directory: $DIRECTORY"
log_info "Target VMAF: $VMAF"
log_info "Preset: $PRESET"
log_info "Workers: $WORKERS"
log_info "Samples: $SAMPLES"
log_info "Encoder: $ENCODER"
if [[ -n "$THOROUGH" ]]; then
log_info "Thorough: Yes"
fi
if [[ -n "$HWACCEL" ]]; then
log_info "HW Accel: $HWACCEL"
fi
echo ""
log_info "Running optimize_library.py..."
echo ""
# Run the optimisation
"${ARGS[@]}"
EXIT_CODE=$?
# Handle exit code
if [ $EXIT_CODE -eq 0 ]; then
log_success "SUCCESS: Library optimisation completed"
else
log_error "optimize_library.py exited with code $EXIT_CODE"
fi
exit $EXIT_CODE

View File

@@ -0,0 +1,30 @@
# Run Smart GPU Encoder
# Wrapper to ensure correct environment
$ScriptPath = "$PSScriptRoot\smart_gpu_encoder.py"
$PythonPath = "python" # Or specific path like "C:\Python39\python.exe"
# Directories (Change these to your actual paths)
$TvDir = "Z:\tv"
$ContentDir = "Z:\content"
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " SMART GPU ENCODER (AMD AMF + VMAF)" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host "Script: $ScriptPath"
Write-Host "TV Dir: $TvDir"
Write-Host "Content Dir: $ContentDir"
Write-Host ""
# Check if ab-av1 exists in bin
if (-not (Test-Path "$PSScriptRoot\bin\ab-av1.exe")) {
Write-Host "WARNING: ab-av1.exe not found in bin folder!" -ForegroundColor Red
Write-Host "Please download it and place it in $PSScriptRoot\bin\"
exit 1
}
# Run the python script
& $PythonPath $ScriptPath --tv-dir $TvDir --content-dir $ContentDir
Write-Host "`nDone." -ForegroundColor Green
Read-Host "Press Enter to exit..."