Files
pvd/pvd_mezzanine.py
2026-04-13 17:01:31 +00:00

269 lines
9.2 KiB
Python

import os
import sys
import json
import subprocess
import math
import re
from pathlib import Path
# =============================================================================
# 1. KONFIGURATION UND PFADE
# =============================================================================
FFMPEG_EXE = r"C:\Software\ffmpeg.exe"
FFPROBE_EXE = r"C:\Software\ffprobe.exe"
OUTPUT_BASE_DIR = r"H:\VOD"
def get_pvd_filename(input_path: str) -> str:
"""TITEL-EXTRAKTION (Blu-ray / ProRes / DVD)"""
path = Path(input_path)
path_parts = [p.upper() for p in path.parts]
if "BDMV" in path_parts:
bdmv_index = path_parts.index("BDMV")
try:
project_folder = path_parts[bdmv_index - 4]
except IndexError:
project_folder = path_parts[bdmv_index - 1] if bdmv_index > 0 else "UNKNOWN"
clean_name = re.sub(r'^BEST_', '', project_folder, flags=re.IGNORECASE)
words = [w.capitalize() for w in clean_name.split('_') if w.strip()]
extracted_title = "".join(words)
else:
extracted_title = path.stem
extracted_title = re.sub(r'[^A-Za-z0-9]', '', extracted_title)
if not extracted_title:
extracted_title = "UNKNOWN_TITLE"
return f"{extracted_title}_DEU20_PVD.mp4"
def probe_metadata(filepath: str) -> dict:
cmd = [
FFPROBE_EXE, "-v", "quiet", "-print_format", "json",
"-show_streams", "-show_format", filepath
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
if result.returncode != 0:
print(f"CRITICAL ERROR: ffprobe konnte die Datei nicht lesen.\n{result.stderr}")
sys.exit(1)
return json.loads(result.stdout)
def main():
if len(sys.argv) < 2:
print("ERROR: Ziehe eine Datei auf dieses Script oder die verknüpfte .bat")
os.system("pause")
sys.exit(1)
input_file = sys.argv[1]
if not os.path.isfile(input_file):
print(f"ERROR: Eingabedatei nicht gefunden: {input_file}")
os.system("pause")
sys.exit(1)
os.makedirs(OUTPUT_BASE_DIR, exist_ok=True)
# --- SCHRITT 1: ANALYSE ---
print(f"\n[1/5] Analysiere Quelle: {os.path.basename(input_file)}")
metadata = probe_metadata(input_file)
v_stream = next((s for s in metadata['streams'] if s['codec_type'] == 'video'), None)
a_streams = [s for s in metadata['streams'] if s['codec_type'] == 'audio']
if not v_stream:
print("Fehler: Kein Videostream gefunden!")
sys.exit(1)
if not a_streams:
print("Fehler: Keine Audio-Tracks gefunden!")
sys.exit(1)
# === AUFLÖSUNG + PAL/NTSC + INTERLACED-ERKENNUNG + FARBRAUM ===
width = int(v_stream.get('width', 0))
height = int(v_stream.get('height', 0))
fps_raw = v_stream.get('r_frame_rate', '24/1')
num, den = map(int, fps_raw.split('/'))
fps = num / den
keyint = math.ceil(fps)
# SD / PAL / NTSC Erkennung
if height <= 576:
is_ntsc = (height <= 480 and fps >= 29.0)
print(f" -> Auflösung: {width}x{height} @ {fps:.3f} fps → {'NTSC' if is_ntsc else 'PAL'} SD-Material")
is_sd = True
else:
is_ntsc = False
is_sd = False
print(f" -> Auflösung: {width}x{height} @ {fps:.3f} fps → HD-Material")
print(f" -> Keyint (1 Sek. GOP): {keyint}")
# === INTERLACED-ERKENNUNG FÜR DVD ===
field_order = v_stream.get('field_order', 'unknown')
scan_type = v_stream.get('scan_type', 'unknown')
is_interlaced = (field_order != 'progressive' and field_order != 'unknown') or scan_type == 'interlaced'
if is_interlaced:
print(f" → **Interlaced erkannt** (field_order={field_order}, scan_type={scan_type}) → Deinterlacing mit bwdif (beste Qualität, 25 fps) aktiviert")
else:
print(" → Progressive Quelle → kein Deinterlacing nötig")
# Ziel-Farbraum (Rec.601 PAL/NTSC vs Rec.709)
if is_sd:
if is_ntsc:
target_prim = "smpte170m"
target_trc = "smpte170m"
target_space = "smpte170m"
target_name = "Rec.601 NTSC (smpte170m)"
else:
target_prim = "bt470bg"
target_trc = "bt470bg"
target_space = "bt470bg"
target_name = "Rec.601 PAL (bt470bg)"
else:
target_prim = "bt709"
target_trc = "bt709"
target_space = "bt709"
target_name = "Rec.709 HD"
# Quell-Farbraum prüfen + echte Konvertierung
source_prim = v_stream.get('color_primaries', 'unknown')
source_trc = v_stream.get('color_transfer', 'unknown')
source_space = v_stream.get('color_space', 'unknown')
needs_conversion = (
source_prim == 'unknown' or
source_prim != target_prim or
source_trc != target_trc or
source_space != target_space
)
if needs_conversion:
print(f" → Farbkonvertierung AKTIVIERT → Ziel: {target_name}")
else:
print(f" → Quell-Farbraum passt perfekt → keine Konvertierung")
# --- SCHRITT 2: AUDIO-WAHL ---
print("\n[2/5] Vorhandene Audio-Tracks:")
for i, s in enumerate(a_streams):
lang = s.get('tags', {}).get('language', 'und').upper()
ch = s.get('channels', 2)
codec = s.get('codec_name', 'pcm')
print(f" [{i}] Index {s['index']}: {lang} ({ch} Kanäle, {codec})")
selected_audio_list_idx = next(
(i for i, s in enumerate(a_streams) if s.get('tags', {}).get('language', 'und').lower() in ('ger', 'deu')),
None
)
if selected_audio_list_idx is None:
choice = input("\nKeine deutsche Spur automatisch erkannt.\nWelche Spur ist die deutsche Master-Spur? [Standard: 0]: ") or "0"
try:
selected_audio_list_idx = int(choice)
if not 0 <= selected_audio_list_idx < len(a_streams):
raise ValueError
except Exception:
print("Ungültige Eingabe → verwende Spur 0.")
selected_audio_list_idx = 0
selected_audio_idx = a_streams[selected_audio_list_idx]['index']
print(f" → Gewählte Audio-Spur: Index {selected_audio_idx}")
# --- SCHRITT 3: VF-FILTER (Deinterlace + Farbe + Subs) ---
output_name = get_pvd_filename(input_file)
output_path = os.path.join(OUTPUT_BASE_DIR, output_name)
forced_srt = os.path.splitext(input_file)[0] + "_forced.srt"
vf_filters = []
# 1. Deinterlacing mit bwdif (beste Qualität, behält originale Framerate = 25 fps bei PAL)
if is_interlaced:
vf_filters.append("bwdif=mode=0:parity=auto")
# 2. Farbkonvertierung (falls nötig)
if needs_conversion:
conv = (f"colorspace=primaries={target_prim}:trc={target_trc}:matrix={target_space}:range=tv")
vf_filters.append(conv)
# 3. Forced SRT Burn-In
if os.path.exists(forced_srt):
print(f"\n[3/5] Forced SRT gefunden → wird eingebrannt: {os.path.basename(forced_srt)}")
clean_srt_path = forced_srt.replace("\\", "/").replace(":", "\\:")
vf_filters.append(f"subtitles='{clean_srt_path}':force_style='Fontsize=14,MarginV=38'")
else:
print("\n[3/5] Keine Forced SRT gefunden → Burn-In übersprungen.")
# --- SCHRITT 4: ENCODING ---
print(f"\n[4/5] Starte Encoding → {output_name}")
print(f" → Ziel-Farbraum: {target_name}")
if is_sd:
bv = "8M"
maxr = "10M"
bufs = "15M"
lvl = "3.1"
print(" → SD-Parameter (Bitrate + Level)")
else:
bv = "28M"
maxr = "35M"
bufs = "50M"
lvl = "4.1"
print(" → HD-Parameter (Bitrate + Level)")
ffmpeg_cmd = [
FFMPEG_EXE, "-hide_banner", "-y", "-i", input_file,
"-map", f"0:{v_stream['index']}",
"-map", f"0:{selected_audio_idx}",
]
if vf_filters:
vf_str = ",".join(vf_filters)
ffmpeg_cmd.extend(["-vf", vf_str])
ffmpeg_cmd.extend([
"-c:v", "libx264",
"-profile:v", "high",
"-level", lvl,
"-pix_fmt", "yuv420p",
"-b:v", bv,
"-maxrate", maxr,
"-bufsize", bufs,
"-preset", "slow",
"-tune", "film",
"-x264-params", f"keyint={keyint}:min-keyint=2:scenecut=40:bframes=3:aq-mode=2",
"-color_primaries", target_prim,
"-color_trc", target_trc,
"-colorspace", target_space,
])
ffmpeg_cmd.extend([
"-c:a", "aac",
"-b:a", "256k",
"-ac", "2",
"-ar", "48000",
"-f", "mp4",
"-movflags", "+faststart",
"-metadata:s:a:0", "language=ger",
"-metadata:s:v:0", "language=ger",
"-map_chapters", "-1",
"-map_metadata", "-1",
"-avoid_negative_ts", "make_zero",
output_path
])
print("\n--- FFmpeg Kommando ---")
print(" ".join(ffmpeg_cmd))
print("------------------------\n")
result = subprocess.run(ffmpeg_cmd, check=False)
if result.returncode == 0:
print(f"\n[5/5] ✅ ERFOLGREICH abgeschlossen!\nDatei: {output_path}")
else:
print(f"\n[5/5] ❌ FFmpeg-Fehler (Code {result.returncode})")
os.system("pause")
if __name__ == "__main__":
main()