feat(v5): 4K-UHD nach der Leistung — Grafikkarte mit 4K-Preset komprimiert, nur CPU bleibt verlustfrei

WAS: presetEmpfehlung liefert für UHD das 4K-Preset der gemessenen
Grafikkarten-Familie (VCN/NVENC/QSV) — sonst PRESET_KEINE (verlustfrei);
komprimierenFuer folgt der Empfehlung, wenn der Typ auf „automatisch"
steht. Eigene Wahl schlägt beides. Fenster: Kachel, Kompressions-Karte und
Erst-Einrichtung sagen „(8 Bit)" bzw. „verlustfrei — nur CPU-Encoder".
Gemessen am 12.09.2026 (HandBrake 1.11.2, RX 9070 XT, Akira-UHD-Abbild):
die 4K-Hardware-Presets sind 8-Bit (vce_h265, Profil main); der 10-Bit-
HEVC-Encoder vce_h265_10bit stürzt beim Start ab (Segfault, mit Profil
auto und main10); AV1 10-Bit („AV1 VCN 2160p 4K", vce_av1_10bit) läuft
— steht als eigene Wahl in der Liste, mit dem Hinweis auf AV1-fähige
Abspielgeräte.
WARUM: Durchsicht 12.09.2026, A4 — Commander-Entscheid: „Rippy analysiert
doch die Performance des PCs, könnten wir das nicht davon abhängig
machen?" Vorher rechnete der CPU-Default 4K auf 1080p klein, tagelang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-12 21:48:38 +02:00
co-authored by Claude Opus 5
parent 6b6e5c3436
commit 0fc87aa022
7 changed files with 96 additions and 21 deletions
@@ -7,7 +7,7 @@
// Werkzeuge mit Durchsuchen, Kompression mit Sprach-Auswahl, TMDb, // Werkzeuge mit Durchsuchen, Kompression mit Sprach-Auswahl, TMDb,
// Benachrichtigung, Automatik und Roh-Regel. Dazu der Poster-Hintergrund. // Benachrichtigung, Automatik und Roh-Regel. Dazu der Poster-Hintergrund.
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { DEFAULT_HB_PRESET, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung' import { DEFAULT_HB_PRESET, empfehlungText, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung'
import { Badge, FELD_KLASSE, Feld, KNOPF_GOLD, KNOPF_LEISE, PfadFeld, Schalter, gbKurz } from '../bausteine' import { Badge, FELD_KLASSE, Feld, KNOPF_GOLD, KNOPF_LEISE, PfadFeld, Schalter, gbKurz } from '../bausteine'
import { SprachAuswahl } from '../einstellungen/SprachAuswahl' import { SprachAuswahl } from '../einstellungen/SprachAuswahl'
import { benachrichtigungTesten, einstellungSetzen, posterLaden, speicherLaden, type Verbindung } from '../kernverbindung' import { benachrichtigungTesten, einstellungSetzen, posterLaden, speicherLaden, type Verbindung } from '../kernverbindung'
@@ -167,7 +167,7 @@ export function ErstEinrichtung({ verbindung }: { verbindung: Verbindung }) {
{EMPFEHLUNGEN.map(([name, typ]) => ( {EMPFEHLUNGEN.map(([name, typ]) => (
<div key={typ} className="rounded-lg border border-tinte/8 bg-buehne/60 p-2"> <div key={typ} className="rounded-lg border border-tinte/8 bg-buehne/60 p-2">
<p className="font-mono text-[10px] tracking-wider text-nebel">{name}</p> <p className="font-mono text-[10px] tracking-wider text-nebel">{name}</p>
<p className="mt-0.5 font-mono text-xs text-gold-hell">{presetEmpfehlung(typ, backends, presets) || DEFAULT_HB_PRESET}</p> <p className="mt-0.5 font-mono text-xs text-gold-hell">{empfehlungText(typ, presetEmpfehlung(typ, backends, presets) || DEFAULT_HB_PRESET)}</p>
</div> </div>
))} ))}
</div> </div>
@@ -4,7 +4,7 @@
// einstellungSetzen in die EINE Quelle (§ 6.7). // einstellungSetzen in die EINE Quelle (§ 6.7).
import { useState } from 'react' import { useState } from 'react'
import type { GeraeteInfo, HauptStatus, VorgangEintrag } from '../../gemeinsam/nachrichten' import type { GeraeteInfo, HauptStatus, VorgangEintrag } from '../../gemeinsam/nachrichten'
import { presetEmpfehlung } from '../../gemeinsam/preset-empfehlung' import { empfehlungText, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung'
import { import {
Badge, Badge,
FELD_KLASSE, FELD_KLASSE,
@@ -127,7 +127,7 @@ export function KompressionKarte({ verbindung }: { verbindung: Verbindung }) {
const werte = verbindung.einstellungen const werte = verbindung.einstellungen
const presets = verbindung.werkzeuge?.presets ?? [] const presets = verbindung.werkzeuge?.presets ?? []
const backends = verbindung.werkzeuge?.backends ?? [] const backends = verbindung.werkzeuge?.backends ?? []
const empfehlungFuer = (typ: string): string => presetEmpfehlung(typ, backends, presets) const empfehlungFuer = (typ: string): string => empfehlungText(typ, presetEmpfehlung(typ, backends, presets))
return ( return (
<SektionsKarte <SektionsKarte
id="kompression" id="kompression"
@@ -153,7 +153,8 @@ export function KompressionKarte({ verbindung }: { verbindung: Verbindung }) {
</div> </div>
<p className="text-[11px] leading-4 text-nebel"> <p className="text-[11px] leading-4 text-nebel">
„automatisch" nimmt die Empfehlung aus der gemessenen Liste deines HandBrake auf dieser Maschine die Grafikkarte, wo es sie gibt. Eine eigene Wahl „automatisch" nimmt die Empfehlung aus der gemessenen Liste deines HandBrake auf dieser Maschine die Grafikkarte, wo es sie gibt. Eine eigene Wahl
schlägt die Empfehlung immer. schlägt die Empfehlung immer. 4K-UHD wird nur mit Grafikkarten-Encoder komprimiert (8 Bit); ohne bleibt es verlustfrei. Wer HDR in 10 Bit will, wählt
AV1 VCN 2160p 4K" — die Abspielgeräte müssen dann AV1 können.
</p> </p>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<SprachAuswahl <SprachAuswahl
@@ -13,7 +13,7 @@ import type {
RipStatus, RipStatus,
VorgangEintrag, VorgangEintrag,
} from '../../gemeinsam/nachrichten' } from '../../gemeinsam/nachrichten'
import { DEFAULT_HB_PRESET, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung' import { DEFAULT_HB_PRESET, PRESET_KEINE, empfehlungText, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung'
import { import {
Badge, Badge,
IconDisc, IconDisc,
@@ -58,9 +58,11 @@ export function presetAnzeige(typ: GeraeteInfo['typ'], verbindung: Verbindung):
if (werte['transcodeEnabled'] === 'false') return 'ohne Kompression' if (werte['transcodeEnabled'] === 'false') return 'ohne Kompression'
const schluessel = typ === 'dvd' ? 'transcodePresetDvd' : typ === 'uhd' ? 'transcodePresetUhd' : 'transcodePresetBluray' const schluessel = typ === 'dvd' ? 'transcodePresetDvd' : typ === 'uhd' ? 'transcodePresetUhd' : 'transcodePresetBluray'
const eigene = werte[schluessel] ?? '' const eigene = werte[schluessel] ?? ''
if (eigene === PRESET_KEINE) return 'ohne Kompression'
if (eigene.length > 0) return eigene
const empfehlung = presetEmpfehlung(typ, verbindung.werkzeuge?.backends ?? [], verbindung.werkzeuge?.presets ?? []) const empfehlung = presetEmpfehlung(typ, verbindung.werkzeuge?.backends ?? [], verbindung.werkzeuge?.presets ?? [])
const gewaehlt = eigene.length > 0 ? eigene : empfehlung.length > 0 ? empfehlung : DEFAULT_HB_PRESET if (empfehlung === PRESET_KEINE) return typ === 'uhd' ? 'verlustfrei (nur CPU-Encoder)' : 'ohne Kompression'
return gewaehlt === 'keine' ? 'ohne Kompression' : gewaehlt return empfehlungText(typ, empfehlung.length > 0 ? empfehlung : DEFAULT_HB_PRESET)
} }
export function LaufwerkKachel({ export function LaufwerkKachel({
@@ -18,6 +18,11 @@
export const DEFAULT_HB_PRESET = 'H.265 MKV 1080p30' export const DEFAULT_HB_PRESET = 'H.265 MKV 1080p30'
// Reservierter Wert: „diesen Typ NICHT komprimieren". Kein echtes
// HandBrake-Preset heißt so (gegen alle 107 aus --preset-list geprüft).
// Lebt seit 5.7.0 hier, weil die Empfehlung ihn liefern kann.
export const PRESET_KEINE = 'keine'
const FAMILIEN: ReadonlyArray<readonly [backend: string, presetKennung: string]> = [ const FAMILIEN: ReadonlyArray<readonly [backend: string, presetKennung: string]> = [
['vce', 'VCN'], ['vce', 'VCN'],
['nvenc', 'NVENC'], ['nvenc', 'NVENC'],
@@ -25,7 +30,19 @@ const FAMILIEN: ReadonlyArray<readonly [backend: string, presetKennung: string]>
] ]
/** Das empfohlene Preset für diesen Disc-Typ — aus den GEMESSENEN /** Das empfohlene Preset für diesen Disc-Typ — aus den GEMESSENEN
* Backends und Preset-Namen. Fällt auf den CPU-Default zurück. */ * Backends und Preset-Namen. DVD und Blu-ray fallen auf den CPU-Default
* zurück.
*
* 4K-UHD seit 5.7.0 nach der Leistung (Commander-Entscheid 12.09.2026):
* Gibt es einen Grafikkarten-Encoder UND dessen 4K-Preset, wird damit
* komprimiert; gibt es nur CPU-Encoder, bleibt UHD verlustfrei
* (PRESET_KEINE) — ein 4K-Encode auf der CPU dauert Tage, und der alte
* CPU-Default „1080p30" rechnete 4K klein. Gemessen am 12.09.2026: Die
* 4K-Hardware-Presets („H.265 VCN 2160p 4K" usw.) sind 8-Bit; der
* 10-Bit-HEVC-Encoder der AMD-Karte (vce_h265_10bit) stürzt in HandBrake
* 1.11.2 beim Start ab, 10-Bit-AV1 („AV1 VCN 2160p 4K") läuft — wer HDR
* in 10 Bit will, wählt das AV1-Preset selbst (Abspielgeräte müssen AV1
* können). */
export function presetEmpfehlung( export function presetEmpfehlung(
discTyp: string, discTyp: string,
backends: readonly string[], backends: readonly string[],
@@ -36,9 +53,19 @@ export function presetEmpfehlung(
if (!backends.includes(backend)) continue if (!backends.includes(backend)) continue
const treffer = presets.find((p) => p.startsWith('H.265 ') && p.includes(kennung) && p.includes(gewuenscht)) const treffer = presets.find((p) => p.startsWith('H.265 ') && p.includes(kennung) && p.includes(gewuenscht))
if (treffer !== undefined) return treffer if (treffer !== undefined) return treffer
// 4K-Familie ohne 4K-Preset: das 1080p-Pendant ist besser als CPU. // Eine 4K-Disc ohne 4K-Preset der Familie: nicht klein rechnen —
// verlustfrei lassen (unten). Bei DVD/Blu-ray ist das 1080p-Pendant
// besser als CPU.
if (discTyp === 'uhd') continue
const kleiner = presets.find((p) => p.startsWith('H.265 ') && p.includes(kennung)) const kleiner = presets.find((p) => p.startsWith('H.265 ') && p.includes(kennung))
if (kleiner !== undefined) return kleiner if (kleiner !== undefined) return kleiner
} }
return DEFAULT_HB_PRESET return discTyp === 'uhd' ? PRESET_KEINE : DEFAULT_HB_PRESET
}
/** Klartext zur Empfehlung fürs Fenster (5.7.0). */
export function empfehlungText(discTyp: string, empfehlung: string): string {
if (empfehlung === PRESET_KEINE) return discTyp === 'uhd' ? 'verlustfrei lassen — nur CPU-Encoder gemessen' : 'verlustfrei lassen'
if (discTyp === 'uhd' && /2160p/.test(empfehlung)) return `${empfehlung} (8 Bit)`
return empfehlung
} }
+1 -1
View File
@@ -121,7 +121,7 @@ export async function komprimierenUndAblegen(
): Promise<AblageErgebnis> { ): Promise<AblageErgebnis> {
const einstellungen = umgebung.einstellungen() const einstellungen = umgebung.einstellungen()
const handbrake = umgebung.handbrake() const handbrake = umgebung.handbrake()
const soll = komprimierenFuer(auftrag.discTyp, einstellungen) const soll = komprimierenFuer(auftrag.discTyp, einstellungen, umgebung.presetEmpfehlung?.(auftrag.discTyp) ?? '')
const komprimiert = soll && handbrake.length > 0 const komprimiert = soll && handbrake.length > 0
const preset = const preset =
auftrag.preset.length > 0 auftrag.preset.length > 0
@@ -2,9 +2,9 @@
// mit den Befunden vom 25.07.2026 begründet: EIN Preset für alles rechnete // mit den Befunden vom 25.07.2026 begründet: EIN Preset für alles rechnete
// 4K auf 1080p herunter und DVDs hinauf. // 4K auf 1080p herunter und DVDs hinauf.
import { DEFAULT_HB_PRESET, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung' import { DEFAULT_HB_PRESET, PRESET_KEINE, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung'
export { DEFAULT_HB_PRESET, presetEmpfehlung } export { DEFAULT_HB_PRESET, PRESET_KEINE, presetEmpfehlung }
/** Einstellungs-Schlüssel je Disc-Typ. */ /** Einstellungs-Schlüssel je Disc-Typ. */
export const PRESET_SCHLUESSEL: Record<string, string> = { export const PRESET_SCHLUESSEL: Record<string, string> = {
@@ -13,15 +13,12 @@ export const PRESET_SCHLUESSEL: Record<string, string> = {
uhd: 'transcodePresetUhd', uhd: 'transcodePresetUhd',
} }
// Reservierter Wert: „diesen Typ NICHT komprimieren". Kein echtes
// HandBrake-Preset heißt so (gegen alle 90 aus --preset-list geprüft).
// Grund: 4K verlustfrei durchreichen, DVDs trotzdem schrumpfen.
export const PRESET_KEINE = 'keine'
export type Einstellungen = Record<string, string | boolean | undefined> export type Einstellungen = Record<string, string | boolean | undefined>
/** Soll dieser Disc-Typ nach dem Rip komprimiert werden? (pur) */ /** Soll dieser Disc-Typ nach dem Rip komprimiert werden? (pur)
export function komprimierenFuer(discTyp: string, einstellungen: Einstellungen): boolean { * 5.7.0: Steht der Typ auf „automatisch" und die gemessene Empfehlung
* sagt „keine" (4K ohne Grafikkarten-Encoder), bleibt es verlustfrei. */
export function komprimierenFuer(discTyp: string, einstellungen: Einstellungen, empfehlung = ''): boolean {
if (einstellungen['transcodeEnabled'] === false || einstellungen['transcodeEnabled'] === 'false') { if (einstellungen['transcodeEnabled'] === false || einstellungen['transcodeEnabled'] === 'false') {
return false return false
} }
@@ -29,6 +26,7 @@ export function komprimierenFuer(discTyp: string, einstellungen: Einstellungen):
if (schluessel.length > 0) { if (schluessel.length > 0) {
const wert = String(einstellungen[schluessel] ?? '').trim() const wert = String(einstellungen[schluessel] ?? '').trim()
if (wert === PRESET_KEINE) return false if (wert === PRESET_KEINE) return false
if (wert.length === 0 && empfehlung === PRESET_KEINE) return false
} }
return true return true
} }
+47
View File
@@ -0,0 +1,47 @@
// 5.7.0 — 4K nach der Leistung (Commander-Entscheid 12.09.2026): Grafik-
// karten-Encoder plus 4K-Preset → komprimieren; nur CPU → verlustfrei.
// Preset-Namen und Encoder am mitgelieferten HandBrake 1.11.2 gemessen
// (--preset-list, --help; vce_h265_10bit stürzt ab, AV1 10-Bit läuft).
import { describe, expect, it } from 'vitest'
import { DEFAULT_HB_PRESET, PRESET_KEINE, empfehlungText, presetEmpfehlung } from '../src/gemeinsam/preset-empfehlung'
import { komprimierenFuer, presetFuer } from '../src/kern/komprimieren/presets'
const PRESETS = ['H.265 MKV 1080p30', 'H.265 MKV 2160p60 4K', 'AV1 VCN 2160p 4K', 'H.265 VCN 2160p 4K', 'H.265 VCN 1080p', 'H.265 NVENC 1080p']
const NUR_CPU = ['cpu-x264', 'cpu-x265', 'cpu-av1']
const AMD = [...NUR_CPU, 'vce', 'vce-av1']
const NVIDIA_OHNE_4K = [...NUR_CPU, 'nvenc']
describe('presetEmpfehlung für 4K-UHD', () => {
it('Grafikkarte mit 4K-Preset → das 4K-Preset (8 Bit im Klartext)', () => {
expect(presetEmpfehlung('uhd', AMD, PRESETS)).toBe('H.265 VCN 2160p 4K')
expect(empfehlungText('uhd', 'H.265 VCN 2160p 4K')).toBe('H.265 VCN 2160p 4K (8 Bit)')
})
it('nur CPU → verlustfrei lassen, nie das 1080p-CPU-Preset', () => {
expect(presetEmpfehlung('uhd', NUR_CPU, PRESETS)).toBe(PRESET_KEINE)
expect(empfehlungText('uhd', PRESET_KEINE)).toContain('verlustfrei')
})
it('Grafikkarte ohne 4K-Preset → verlustfrei, nicht klein rechnen', () => {
expect(presetEmpfehlung('uhd', NVIDIA_OHNE_4K, PRESETS)).toBe(PRESET_KEINE)
})
it('DVD und Blu-ray wie bisher: Grafikkarte, sonst CPU-Default', () => {
expect(presetEmpfehlung('bluray', AMD, PRESETS)).toBe('H.265 VCN 1080p')
expect(presetEmpfehlung('dvd', NUR_CPU, PRESETS)).toBe(DEFAULT_HB_PRESET)
expect(presetEmpfehlung('bluray', NVIDIA_OHNE_4K, PRESETS)).toBe('H.265 NVENC 1080p')
})
})
describe('komprimierenFuer mit Empfehlung', () => {
it('automatisch + Empfehlung „keine" → nicht komprimieren', () => {
expect(komprimierenFuer('uhd', {}, PRESET_KEINE)).toBe(false)
expect(komprimierenFuer('uhd', { transcodePresetUhd: '' }, PRESET_KEINE)).toBe(false)
})
it('eigene Wahl schlägt die Empfehlung — in beide Richtungen', () => {
expect(komprimierenFuer('uhd', { transcodePresetUhd: 'AV1 VCN 2160p 4K' }, PRESET_KEINE)).toBe(true)
expect(komprimierenFuer('uhd', { transcodePresetUhd: PRESET_KEINE }, 'H.265 VCN 2160p 4K')).toBe(false)
expect(presetFuer('uhd', { transcodePresetUhd: 'AV1 VCN 2160p 4K' }, PRESET_KEINE)).toBe('AV1 VCN 2160p 4K')
})
it('Blu-ray bleibt unberührt', () => {
expect(komprimierenFuer('bluray', {}, 'H.265 VCN 1080p')).toBe(true)
expect(komprimierenFuer('bluray', {}, '')).toBe(true)
})
})