Upload files to "/"
This commit is contained in:
33
README.md
Normal file
33
README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Amazon PVD Mezzanine Encoder
|
||||
|
||||
**Automatisches Python-Script zur Erstellung von Amazon PVD-konformen Mezzanine-VOD-Dateien**
|
||||
(Blu-ray • ProRes • DVD • PAL • NTSC • Interlaced)
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Vollautomatische Erkennung** von SD/HD, PAL/NTSC und interlaced Material
|
||||
- **Bester Deinterlacer** (`bwdif mode=0`) → saubere 25 fps bei PAL-DVDs (kein Double-Framerate)
|
||||
- **Automatische Farbraum-Konvertierung** (Rec.601 PAL/NTSC ↔ Rec.709) inkl. `colorspace`-Filter
|
||||
- **Forced Subtitles** werden automatisch eingebrannt (wenn `_forced.srt` vorhanden)
|
||||
- **Intelligente Audio-Auswahl** (automatisch deutsche Spur oder manuelle Wahl)
|
||||
- **Amazon PVD optimierte Parameter** (Bitrate, Level, GOP, x264-Settings)
|
||||
- **Saubere Metadaten** + `faststart` für beste Streaming-Performance
|
||||
- Funktioniert mit Drag & Drop (`.bat` oder direkt auf Script ziehen)
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Windows (getestet)
|
||||
- FFmpeg + FFprobe (in `C:\Software\` oder Pfad in Script anpassen)
|
||||
- Python 3.8+
|
||||
|
||||
## Installation & Nutzung
|
||||
|
||||
1. Repo klonen oder herunterladen
|
||||
2. `ffmpeg.exe` und `ffprobe.exe` in `C:\Software\` ablegen (oder Pfade im Script ändern)
|
||||
3. Optional: `create_mezzanine.bat` anlegen:
|
||||
```bat
|
||||
@echo off
|
||||
python "%~dp0pvd_mezzanine.py" %*
|
||||
pause
|
||||
3
create_mezzanine.bat
Normal file
3
create_mezzanine.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
python "%~dp0pvd_mezzanine.py" %*
|
||||
pause
|
||||
269
pvd_mezzanine.py
Normal file
269
pvd_mezzanine.py
Normal file
@@ -0,0 +1,269 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user