From 883c1c290b024359e0898376812bdfd102af97c2 Mon Sep 17 00:00:00 2001
From: Hitonabi
Date: Sat, 25 Jul 2026 21:04:42 +0200
Subject: [PATCH] fix(caps,ui): Encoder werden gemessen statt behauptet
Der Modulkopf von caps.py verspricht "ehrlich erkannt, nicht behauptet" -
erkenne_encoder() tat aber drei Mal das Gegenteil:
1. cpu-x264 und cpu-x265 standen fest verdrahtet in der Liste ("immer
dabei"). Ein Rip-Worker OHNE HandBrake behauptete damit, komprimieren zu
koennen; jeder Transcode dort endete sofort mit "nicht installiert".
2. 'vaapi' wurde allein wegen /dev/dri gemeldet, ohne zu pruefen, ob
HandBrake das ueberhaupt kann. Auf der VM gemessen: das Worker-Image
kennt svt_av1/x264/x265/mpeg4/mpeg2/VP8/VP9/theora und KEINEN einzigen
Hardware-Encoder. Rippy haette VAAPI versprochen und dann versagt.
3. Ueber die Rechenleistung sagte es gar nichts. Genau deshalb war nicht zu
sehen, dass ein 4K-Encode auf dieser Maschine Tage braucht: CPU-Modell
ist das generische "QEMU Virtual CPU version 2.5+", grep -c avx2
/proc/cpuinfo = 0, nur bis sse4_2. x265 lebt von AVX2. Gemessen an der
Leseposition des Quellstroms: 28-55 h fuer einen Film, 3,84 von 4 Kernen
gesaettigt. Abhilfe waere der CPU-Typ 'host' in Proxmox.
Jetzt wird die Encoder-Liste aus `HandBrakeCLI --help` geparst (Abschnitt
-e/--encoder, Formatstrings im Worker-Image gemessen - AGENTS Regel D), und
Hardware zaehlt nur, wenn Geraet UND HandBrake-Unterstuetzung da sind. Neu
gemeldet: cpu-av1 (svt_av1 ist wirklich verfuegbar) sowie CPU-Modell,
Kernzahl und Vektorbefehlsstufe je Worker.
UI: Kerne/Vektorbefehle stehen bei jedem Worker (Worker-Tab und
Einstellungen -> System), dazu die ungefilterte HandBrake-Auskunft und eine
sichtbare Warnung, wenn AVX2 fehlt - inklusive des Hinweises auf den
VM-CPU-Typ. Der Warntext liegt in lib/encoder.ts, damit er nicht doppelt
im Code steht.
Mit im gleichen Zug (dieselbe Datei): der Schalter "Alle Tracks rippen" ist
entfallen. Er war ein Placebo - abcde bekommt keine Track-Auswahl und es gab
auch keine Oberflaeche, um eine Teilmenge zu waehlen. Eine Audio-CD wurde
also immer vollstaendig gerippt, egal wie der Schalter stand. An seiner
Stelle steht jetzt die Wahrheit.
7 Tests, Testdaten sind echte HandBrake-Ausgaben von der VM. Der Parser ist
zusaetzlich gegen die vollen 697 Zeilen der echten --help gegengeprueft.
Co-Authored-By: Claude Opus 5
---
docker/ui/src/components/FirstRunWizard.tsx | 1 +
docker/ui/src/components/RipTargetModal.tsx | 3 +-
docker/ui/src/components/WorkerVerwaltung.tsx | 19 +++
docker/ui/src/lib/design.ts | 1 +
docker/ui/src/lib/encoder.ts | 24 +++
docker/ui/src/pages/Settings.tsx | 49 +++++--
docker/worker/caps.py | 137 ++++++++++++++++--
docker/worker/test_caps.py | 115 +++++++++++++++
8 files changed, 328 insertions(+), 21 deletions(-)
create mode 100644 docker/ui/src/lib/encoder.ts
create mode 100644 docker/worker/test_caps.py
diff --git a/docker/ui/src/components/FirstRunWizard.tsx b/docker/ui/src/components/FirstRunWizard.tsx
index 6686381..6cb2be5 100644
--- a/docker/ui/src/components/FirstRunWizard.tsx
+++ b/docker/ui/src/components/FirstRunWizard.tsx
@@ -14,6 +14,7 @@ interface WorkerInfo {
const ENCODER_LABELS: Record = {
'cpu-x264': 'CPU · H.264 (x264)',
'cpu-x265': 'CPU · H.265 (x265)',
+ 'cpu-av1': 'CPU · AV1 (SVT-AV1)',
'vaapi': 'Hardware · VAAPI (AMD/Intel)',
'nvenc': 'Hardware · NVENC (NVIDIA)',
}
diff --git a/docker/ui/src/components/RipTargetModal.tsx b/docker/ui/src/components/RipTargetModal.tsx
index 470c3d1..be8a591 100644
--- a/docker/ui/src/components/RipTargetModal.tsx
+++ b/docker/ui/src/components/RipTargetModal.tsx
@@ -38,7 +38,8 @@ interface WorkerWahl {
}
const ENCODER_KURZ: Record = {
- 'cpu-x264': 'H.264', 'cpu-x265': 'H.265', 'vaapi': 'VAAPI⚡', 'nvenc': 'NVENC⚡',
+ 'cpu-x264': 'H.264', 'cpu-x265': 'H.265', 'cpu-av1': 'AV1',
+ 'vaapi': 'VAAPI⚡', 'nvenc': 'NVENC⚡',
}
interface TitelInfo {
diff --git a/docker/ui/src/components/WorkerVerwaltung.tsx b/docker/ui/src/components/WorkerVerwaltung.tsx
index 2cf85bc..8f31c99 100644
--- a/docker/ui/src/components/WorkerVerwaltung.tsx
+++ b/docker/ui/src/components/WorkerVerwaltung.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { Server, RefreshCw, Copy, CheckCircle, Trash2, Download } from 'lucide-react'
import { api } from '../lib/api'
+import { simdWarnung } from '../lib/encoder'
import { useToast } from '../context/ToastContext'
import { Card, CardHeader, CardTitle, CardContent } from './ui/Card'
import { Button } from './ui/Button'
@@ -16,16 +17,22 @@ interface WorkerInfo {
ip?: string
makemkv?: string
handbrake?: string
+ cpu_modell?: string
+ cpu_kerne?: string
+ cpu_simd?: string
+ handbrake_encoder?: string
}
}
const ENCODER_LABELS: Record = {
'cpu-x264': 'CPU · H.264',
'cpu-x265': 'CPU · H.265',
+ 'cpu-av1': 'CPU · AV1 (SVT)',
'vaapi': 'GPU · VAAPI (AMD/Intel)',
'nvenc': 'GPU · NVENC (NVIDIA)',
}
+
function relativeZeit(iso?: string): string {
if (!iso) return 'nie'
const sekunden = Math.floor((Date.now() - new Date(iso).getTime()) / 1000)
@@ -134,6 +141,18 @@ export default function WorkerVerwaltung() {
{w.info?.ip ? ` · IP ${w.info.ip}` : ''}
{w.info?.hostname && w.info.hostname !== w.name ? ` · ID ${w.info.hostname}` : ''}
+ {(w.info?.cpu_kerne || w.info?.cpu_simd) && (
+
+ {w.info?.cpu_kerne ? `${w.info.cpu_kerne} Kerne` : ''}
+ {w.info?.cpu_kerne && w.info?.cpu_simd ? ' · ' : ''}
+ {w.info?.cpu_simd ? `Vektorbefehle ${w.info.cpu_simd}` : ''}
+
+ )}
+ {simdWarnung(w.info?.cpu_simd) && (
+
+ {simdWarnung(w.info?.cpu_simd)}
+
+ )}
diff --git a/docker/ui/src/lib/design.ts b/docker/ui/src/lib/design.ts
index ee62395..fe84198 100644
--- a/docker/ui/src/lib/design.ts
+++ b/docker/ui/src/lib/design.ts
@@ -80,6 +80,7 @@ export const LOG_LEVEL_STYLES = {
export const ENCODER_BADGES: Record
= {
'cpu-x264': { label: 'H.264 (CPU)', highlight: false },
'cpu-x265': { label: 'H.265 (CPU)', highlight: false },
+ 'cpu-av1': { label: 'AV1 (CPU)', highlight: false },
'vaapi': { label: 'VAAPI ⚡', highlight: true },
'nvenc': { label: 'NVENC ⚡', highlight: true },
}
diff --git a/docker/ui/src/lib/encoder.ts b/docker/ui/src/lib/encoder.ts
new file mode 100644
index 0000000..4aebd19
--- /dev/null
+++ b/docker/ui/src/lib/encoder.ts
@@ -0,0 +1,24 @@
+// Encoder-Fähigkeiten eines Workers in Klartext übersetzen.
+//
+// Befund 25.07.2026: Die Rippy-VM lief auf dem generischen QEMU-CPU-Modell
+// („QEMU Virtual CPU version 2.5+") und hatte deshalb kein AVX2, nur sse4_2.
+// x265 lebt von diesen Vektorbefehlen — ein 4K-Encode brauchte dort gemessene
+// 28-55 Stunden, und nirgends im UI war das zu sehen. Der Worker meldet die
+// Angaben seit dieser Runde selbst (worker/caps.py), hier werden sie gedeutet.
+
+// Stufen, mit denen Software-Encoding brauchbar schnell ist.
+export const SIMD_SCHNELL = ['avx512f', 'avx2']
+
+/**
+ * Warnt, wenn die CPU dieses Workers zu schwach für Software-Encoding ist.
+ * Gibt null zurück, wenn alles in Ordnung ist ODER die Stufe unbekannt ist —
+ * lieber nichts sagen als etwas Falsches behaupten.
+ */
+export function simdWarnung(simd?: string): string | null {
+ if (!simd || simd === 'unbekannt') return null
+ if (SIMD_SCHNELL.includes(simd)) return null
+ return `Diese CPU kann nur ${simd} (kein AVX2) — Software-Encoding ist hier `
+ + 'sehr langsam. Bei einer virtuellen Maschine hilft es meist, den '
+ + 'CPU-Typ auf "host" zu stellen; sonst besser einen Worker mit '
+ + 'Hardware-Encoder wählen.'
+}
diff --git a/docker/ui/src/pages/Settings.tsx b/docker/ui/src/pages/Settings.tsx
index 5df4d5b..6e74f22 100644
--- a/docker/ui/src/pages/Settings.tsx
+++ b/docker/ui/src/pages/Settings.tsx
@@ -10,6 +10,7 @@ import { Button } from '../components/ui/Button'
import { Input, Select, Toggle } from '../components/ui/Input'
import { MEDIA_SERVER_OPTIONEN } from '../lib/mediaServer'
import { ENCODER_BADGES } from '../lib/design'
+import { simdWarnung } from '../lib/encoder'
interface SettingsState {
tmdbApiKey: string
@@ -19,7 +20,6 @@ interface SettingsState {
movieDir: string
seriesDir: string
musicDir: string
- ripAllTracks: boolean
mainFeatureOnly: boolean
autoEject: boolean
autoRipStart: boolean
@@ -48,7 +48,6 @@ const defaultSettings: SettingsState = {
movieDir: 'movies',
seriesDir: 'series',
musicDir: 'music',
- ripAllTracks: true,
mainFeatureOnly: false,
autoEject: true,
autoRipStart: false,
@@ -74,7 +73,15 @@ interface WorkerInfo {
// keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg
// in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich
// rippt, zaehlt, deshalb steht die Angabe pro Worker und nicht global.
- info?: { makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string }
+ // cpu_kerne/cpu_simd: Rechenleistung fuers Software-Encodieren. Ohne diese
+ // Angaben war am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf der
+ // Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne).
+ // handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst.
+ info?: {
+ makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string,
+ cpu_modell?: string, cpu_kerne?: string, cpu_simd?: string,
+ handbrake_encoder?: string,
+ }
}
// Antwort von GET/POST/DELETE /system/keydb (Feldnamen exakt wie die API sie liefert).
@@ -427,14 +434,16 @@ export default function SettingsPage() {
🎵 Audio-CD
-
- handleChange('ripAllTracks', v)}
- />
-
+ {/* Der Schalter „Alle Tracks rippen" ist am 25.07.2026 entfallen.
+ Er war ein Placebo: abcde bekommt keine Track-Auswahl (siehe
+ build_abcde_cmd) und es gibt auch keine Oberflaeche, um eine
+ Teilmenge zu waehlen — eine Audio-CD wurde also immer
+ vollstaendig gerippt, egal wie der Schalter stand. Statt einer
+ Wahl, die nichts bewirkt, steht hier jetzt die Wahrheit. */}
+
+ 💿 Eine Audio-CD wird immer vollständig gerippt — alle Tracks als
+ FLAC, mit MusicBrainz-Tags.
+
Allgemein
@@ -555,6 +564,24 @@ export default function SettingsPage() {
{ENCODER_BADGES[e]?.label || e}
))}
+ {(w.info?.cpu_kerne || w.info?.cpu_simd) && (
+
+ {w.info?.cpu_modell ? `${w.info.cpu_modell} · ` : ''}
+ {w.info?.cpu_kerne ? `${w.info.cpu_kerne} Kerne` : ''}
+ {w.info?.cpu_kerne && w.info?.cpu_simd ? ' · ' : ''}
+ {w.info?.cpu_simd ? `Vektorbefehle ${w.info.cpu_simd}` : ''}
+
+ )}
+ {simdWarnung(w.info?.cpu_simd) && (
+
+ {simdWarnung(w.info?.cpu_simd)}
+
+ )}
+ {w.info?.handbrake_encoder && (
+
+ HandBrake meldet: {w.info.handbrake_encoder}
+
+ )}
))
)}
diff --git a/docker/worker/caps.py b/docker/worker/caps.py
index 791198e..f01d2bf 100644
--- a/docker/worker/caps.py
+++ b/docker/worker/caps.py
@@ -6,26 +6,133 @@ Remote-GPU-Worker meldet sich hier genauso wie der eingebaute CPU-Worker.
"""
import os
+import platform
import re
import shutil
import subprocess
-def erkenne_encoder() -> list:
- """Liste der verfügbaren Encoder-Backends auf dieser Maschine."""
- gefunden = ["cpu-x264", "cpu-x265"] # HandBrake-Software-Encoder, immer dabei
+HB_ENCODER_KOPF = re.compile(r"^-e,\s*--encoder\b")
- # VAAPI: AMD (VCN) und Intel (QuickSync) melden sich über /dev/dri
- if os.path.exists("/dev/dri/renderD128"):
+# Reihenfolge = Anzeige-Reihenfolge im UI.
+SIMD_STUFEN = ("avx512f", "avx2", "avx", "sse4_2")
+
+
+def parse_encoder_liste(hilfe_text: str) -> list:
+ """Video-Encoder, die DIESES HandBrake wirklich anbietet.
+
+ Quelle: `HandBrakeCLI --help`, Abschnitt `-e, --encoder `
+ (AGENTS Regel D — im Worker-Image am 25.07.2026 gemessen, nicht geraten):
+
+ -e, --encoder Select video encoder:
+ svt_av1
+ x264
+ x265
+ ...
+ --encoder-preset
+
+ Der Block endet an der nächsten Option — also an der ersten Zeile, deren
+ erstes nicht-leeres Zeichen ein Bindestrich ist.
+ """
+ encoder = []
+ im_block = False
+ for zeile in (hilfe_text or "").splitlines():
+ inhalt = zeile.strip()
+ if not im_block:
+ if HB_ENCODER_KOPF.match(inhalt):
+ im_block = True
+ continue
+ if not inhalt:
+ continue
+ if inhalt.startswith("-"):
+ break
+ encoder.append(inhalt.split()[0])
+ return encoder
+
+
+def leite_backends_ab(hb_encoder, dri_da: bool, nvidia_da: bool) -> list:
+ """Übersetzt die HandBrake-Encoder-Namen in die Backends, die das UI zeigt.
+
+ Zwei Ehrlichkeits-Regeln, die vorher fehlten (Befund 25.07.2026):
+ * Ohne HandBrake gibt es KEINE Encoder. Vorher standen `cpu-x264` und
+ `cpu-x265` fest verdrahtet in der Liste („immer dabei") — ein reiner
+ Rip-Worker ohne HandBrake behauptete damit, komprimieren zu können.
+ * Hardware zählt nur, wenn das Gerät da ist UND HandBrake den Encoder
+ überhaupt kennt. Vorher genügte `/dev/dri` — das Worker-Image kann
+ laut eigener Auskunft aber gar keinen Hardware-Encoder.
+ """
+ namen = {e.lower() for e in (hb_encoder or [])}
+ gefunden = []
+ if any(n.startswith("x264") for n in namen):
+ gefunden.append("cpu-x264")
+ if any(n.startswith("x265") for n in namen):
+ gefunden.append("cpu-x265")
+ if any(n.startswith("svt_av1") for n in namen):
+ gefunden.append("cpu-av1")
+ if dri_da and any(("qsv" in n) or ("vce" in n) or ("vaapi" in n) for n in namen):
gefunden.append("vaapi")
-
- # NVENC: NVIDIA-Treiber im Container sichtbar
- if shutil.which("nvidia-smi") or os.path.exists("/usr/lib/x86_64-linux-gnu/libnvidia-encode.so.1"):
+ if nvidia_da and any("nvenc" in n for n in namen):
gefunden.append("nvenc")
-
return gefunden
+def beste_simd(flags_text: str) -> str:
+ """Höchste Vektor-Befehlsstufe der CPU — entscheidend fürs Software-Encodieren.
+
+ Befund 25.07.2026: Die Rippy-VM läuft auf dem generischen QEMU-CPU-Modell
+ („QEMU Virtual CPU version 2.5+") und hat deshalb KEIN AVX2, nur sse4_2.
+ x265 lebt von diesen Befehlen; ein 4K-Encode brauchte dort gemessene
+ 28-55 Stunden. Sichtbar war das nirgends — genau diese Lücke schließt die
+ Anzeige. Abhilfe bei Proxmox/KVM: CPU-Typ der VM auf `host` stellen.
+ """
+ vorhanden = set((flags_text or "").split())
+ for stufe in SIMD_STUFEN:
+ if stufe in vorhanden:
+ return stufe
+ return "unbekannt"
+
+
+def _lies_cpuinfo() -> tuple:
+ """(Modellname, Flags-Text, Kernzahl) aus /proc/cpuinfo — leer auf Windows."""
+ modell, flags, kerne = "", "", 0
+ try:
+ with open("/proc/cpuinfo", encoding="utf-8", errors="replace") as f:
+ for zeile in f:
+ if zeile.startswith("processor"):
+ kerne += 1
+ elif not modell and zeile.startswith("model name"):
+ modell = zeile.split(":", 1)[-1].strip()
+ elif not flags and zeile.startswith("flags"):
+ flags = zeile.split(":", 1)[-1].strip()
+ except OSError:
+ pass
+ return modell, flags, kerne
+
+
+def hole_handbrake_hilfe() -> str:
+ """`HandBrakeCLI --help` einmal abrufen (leer, wenn nicht installiert)."""
+ if not shutil.which("HandBrakeCLI"):
+ return ""
+ try:
+ aus = subprocess.run(
+ ["HandBrakeCLI", "--help"], capture_output=True, text=True, timeout=30
+ )
+ return (aus.stdout or "") + (aus.stderr or "")
+ except (OSError, subprocess.TimeoutExpired):
+ return ""
+
+
+def erkenne_encoder() -> list:
+ """Encoder-Backends, die auf DIESER Maschine wirklich nutzbar sind."""
+ hb_encoder = parse_encoder_liste(hole_handbrake_hilfe())
+ dri_da = os.path.exists("/dev/dri/renderD128")
+ nvidia_da = bool(
+ shutil.which("nvidia-smi")
+ or os.path.exists("/usr/lib/x86_64-linux-gnu/libnvidia-encode.so.1")
+ )
+ return leite_backends_ab(hb_encoder, dri_da, nvidia_da)
+
+
def erkenne_ip() -> str:
"""Beste erratbare eigene IP (UDP-Route-Trick, KEIN echter Traffic).
@@ -56,11 +163,23 @@ def werkzeug_versionen() -> dict:
"""
import socket
+ modell, flags, kerne = _lies_cpuinfo()
info = {
# Zuordnung im UI: hostname (für den Celery-Online-Abgleich) + IP
"hostname": socket.gethostname(),
"ip": erkenne_ip(),
+ # Rechenleistung fürs Software-Encodieren. Ohne diese drei Angaben war
+ # am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf dieser Maschine
+ # Tage statt Stunden braucht (kein AVX2, 4 Kerne).
+ "cpu_modell": modell or (platform.processor() or "unbekannt"),
+ "cpu_kerne": str(kerne or os.cpu_count() or 0),
+ "cpu_simd": beste_simd(flags),
}
+ hb_encoder = parse_encoder_liste(hole_handbrake_hilfe())
+ if hb_encoder:
+ # Die ungefilterte Auskunft von HandBrake — damit im UI nachprüfbar ist,
+ # woher die Backend-Liste kommt.
+ info["handbrake_encoder"] = ",".join(hb_encoder)
if shutil.which("makemkvcon"):
info["makemkv"] = os.getenv("MAKEMKV_VERSION") or "installiert"
if shutil.which("HandBrakeCLI"):
diff --git a/docker/worker/test_caps.py b/docker/worker/test_caps.py
new file mode 100644
index 0000000..5f60748
--- /dev/null
+++ b/docker/worker/test_caps.py
@@ -0,0 +1,115 @@
+"""Tests der Fähigkeiten-Erkennung — reine Funktionen, keine Infrastruktur.
+
+Die Testdaten sind ECHTE Ausgaben, am 25.07.2026 im Worker-Image der Rippy-VM
+abgenommen (AGENTS Regel D). Erfundene Formate hätten hier keinen Wert: genau
+daran ist die Erkennung vorher gescheitert.
+"""
+
+import caps
+
+# Wörtlich aus `docker exec rippy-worker-1 HandBrakeCLI --help` (HandBrake im
+# Worker-Image, 25.07.2026). Bemerkenswert: KEIN Hardware-Encoder dabei.
+HB_HILFE_ECHT = """
+ -Z, --preset Select preset by name
+ -e, --encoder Select video encoder:
+ svt_av1
+ svt_av1_10bit
+ x264
+ x264_10bit
+ x265
+ x265_10bit
+ x265_12bit
+ mpeg4
+ mpeg2
+ VP8
+ VP9
+ VP9_10bit
+ theora
+ --encoder-preset
+ Adjust video encoding settings
+"""
+
+# Wie dieselbe Stelle auf einer Maschine MIT Hardware-Encodern aussieht
+# (HandBrake-Windows-Build; Struktur identisch, nur mehr Namen).
+HB_HILFE_MIT_HARDWARE = """
+ -e, --encoder Select video encoder:
+ x264
+ x265
+ nvenc_h264
+ nvenc_h265
+ qsv_h264
+ qsv_h265
+ --encoder-preset
+"""
+
+
+def test_encoder_liste_wird_aus_echter_hilfe_gelesen():
+ encoder = caps.parse_encoder_liste(HB_HILFE_ECHT)
+ assert encoder == [
+ "svt_av1", "svt_av1_10bit",
+ "x264", "x264_10bit",
+ "x265", "x265_10bit", "x265_12bit",
+ "mpeg4", "mpeg2", "VP8", "VP9", "VP9_10bit", "theora",
+ ]
+ # Der Block endet an der nächsten Option — sie darf nicht mitgelesen werden
+ assert not any("encoder-preset" in e for e in encoder)
+ assert not any(e.startswith("-") for e in encoder)
+
+
+def test_encoder_liste_ohne_handbrake_ist_leer():
+ assert caps.parse_encoder_liste("") == []
+ assert caps.parse_encoder_liste(None) == []
+ assert caps.parse_encoder_liste("irgendein Text ohne Encoder-Abschnitt") == []
+
+
+def test_ohne_handbrake_keine_encoder():
+ """Der wichtigste Fall: vorher standen cpu-x264/cpu-x265 fest verdrahtet in
+ der Liste. Ein Rip-Worker ohne HandBrake behauptete damit, komprimieren zu
+ können — und jeder Transcode dort endete sofort mit 'nicht installiert'."""
+ assert caps.leite_backends_ab([], dri_da=True, nvidia_da=True) == []
+
+
+def test_backends_aus_echter_liste():
+ encoder = caps.parse_encoder_liste(HB_HILFE_ECHT)
+ # Kein /dev/dri, kein NVIDIA — und HandBrake kann hier ohnehin keine Hardware
+ assert caps.leite_backends_ab(encoder, dri_da=False, nvidia_da=False) == [
+ "cpu-x264", "cpu-x265", "cpu-av1",
+ ]
+
+
+def test_hardware_nur_wenn_geraet_UND_handbrake_es_kann():
+ """Befund 25.07.2026: Vorher genügte /dev/dri, um 'vaapi' zu melden. Das
+ Worker-Image kann laut eigener Auskunft aber gar keinen Hardware-Encoder —
+ Rippy hätte VAAPI versprochen und wäre beim Encodieren gescheitert."""
+ ohne_hw = caps.parse_encoder_liste(HB_HILFE_ECHT)
+ assert "vaapi" not in caps.leite_backends_ab(ohne_hw, dri_da=True, nvidia_da=True)
+ assert "nvenc" not in caps.leite_backends_ab(ohne_hw, dri_da=True, nvidia_da=True)
+
+ mit_hw = caps.parse_encoder_liste(HB_HILFE_MIT_HARDWARE)
+ # Gerät fehlt → trotz Können kein Hardware-Backend
+ assert caps.leite_backends_ab(mit_hw, dri_da=False, nvidia_da=False) == [
+ "cpu-x264", "cpu-x265",
+ ]
+ # Beides da → beides gemeldet
+ beides = caps.leite_backends_ab(mit_hw, dri_da=True, nvidia_da=True)
+ assert "vaapi" in beides # qsv_* vorhanden
+ assert "nvenc" in beides
+
+
+def test_simd_stufe_erkennt_die_hoechste():
+ assert caps.beste_simd("fpu vme de sse4_2 avx avx2") == "avx2"
+ assert caps.beste_simd("sse4_2 avx avx2 avx512f") == "avx512f"
+ assert caps.beste_simd("fpu vme de pse tsc sse4_2") == "sse4_2"
+ assert caps.beste_simd("") == "unbekannt"
+ assert caps.beste_simd(None) == "unbekannt"
+
+
+def test_simd_stufe_der_rippy_vm_ist_sse4_2():
+ """Echt gemessen auf der Rippy-VM (25.07.2026): generisches QEMU-CPU-Modell,
+ `grep -c avx2 /proc/cpuinfo` = 0. Genau das macht Software-4K dort so lahm."""
+ flags_vm = (
+ "fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 "
+ "clflush mmx fxsr sse sse2 syscall nx lm rep_good nopl cpuid "
+ "pni cx16 sse4_1 sse4_2 x2apic popcnt aes hypervisor lahf_lm"
+ )
+ assert caps.beste_simd(flags_vm) == "sse4_2"