Refactored everything
This commit is contained in:
207
legacy/finalOptimiser.ps1
Normal file
207
legacy/finalOptimiser.ps1
Normal 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
931
legacy/optimize_library.py
Normal 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
100
legacy/run_optimisation.ps1
Normal 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
158
legacy/run_optimisation.sh
Normal 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
|
||||
30
legacy/run_smart_encoder.ps1
Normal file
30
legacy/run_smart_encoder.ps1
Normal 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..."
|
||||
Reference in New Issue
Block a user