fix: Ausgabedateinamen vereinheitlichen – Titel ohne Unterstriche
safe_output_stem und get_pvd_filename nutzten unterschiedliche Logik, sodass MP4 und MOV-Dateien inkonsistente Titel hatten. Neue gemeinsame Funktion extract_title stellt sicher, dass alle drei Ausgabedateien denselben Titel ohne Unterstriche verwenden. README und CLAUDE.md aktualisiert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What This Project Does
|
||||
|
||||
Amazon PVD Mezzanine Encoder — a Windows tool that creates Amazon-compatible mezzanine files from video sources (ProRes, Blu-ray, DVD, etc.). Produces three output files:
|
||||
1. `*_PVD.mp4` — H.264 with German stereo AAC audio
|
||||
2. `*_DEU_AUDIO_PCM.mov` — All German audio tracks as uncompressed PCM
|
||||
3. `*_OV_AUDIO_PCM.mov` — All non-German audio tracks as uncompressed PCM (if present)
|
||||
|
||||
## Running
|
||||
|
||||
```powershell
|
||||
# Drag-and-drop or batch launcher (recommended for end users)
|
||||
.\create_mezzanine.bat "D:\path\to\video.mov"
|
||||
|
||||
# Direct Python
|
||||
python .\pvd_mezzanine.py "D:\path\to\video.mov"
|
||||
|
||||
# CLI-only mode (no GUI)
|
||||
python .\pvd_mezzanine.py --cli "D:\path\to\video.mov"
|
||||
```
|
||||
|
||||
No build step. Pure Python 3.10+ with no third-party dependencies. Requires FFmpeg/FFprobe (auto-detected from PATH, `C:\Tools\FFMPEG`, `C:\Software`, or configured in `config.ini`).
|
||||
|
||||
## Architecture
|
||||
|
||||
All logic lives in `pvd_mezzanine.py`. The data flows in one direction:
|
||||
|
||||
```
|
||||
User Input → config.ini → FFprobe (probe_metadata) → Analysis (analyze_video / build_audio_roles) → JobPlan (build_plan) → FFmpeg commands → run_plan → Output files
|
||||
```
|
||||
|
||||
### Key Dataclasses
|
||||
|
||||
**`AudioRole`** — One audio track: stream index, language, channels, codec. `is_german` checks `{DEU, GER}`. `effective_channels` prefers filename-token over FFprobe-detected channels.
|
||||
|
||||
**`VideoProfile`** — Video analysis result: resolution, FPS, SD/HD, NTSC/PAL, interlacing, colorspace metadata, conversion flags.
|
||||
|
||||
**`JobPlan`** — Complete encoding spec: input/output paths, selected audio roles for MP4 and MOVs, video profile, generated FFmpeg command strings.
|
||||
|
||||
### Module Sections (by line range)
|
||||
|
||||
| Lines | Responsibility |
|
||||
|-------|----------------|
|
||||
| 27–180 | Config loading (`config.ini`), FFmpeg path detection, output directory resolution |
|
||||
| 301–431 | `probe_metadata()`, `analyze_video()`, `build_audio_roles()`, `parse_audio_tokens()` |
|
||||
| 449–625 | `build_plan()` orchestration, `build_pvd_mp4_command()`, `build_audio_mov_command()`, `build_video_filters()` |
|
||||
| 628–691 | `run_command()` (subprocess streaming), `run_plan()`, `run_cli()` |
|
||||
| 693–871 | `MezzanineApp` — Tkinter GUI with threaded encoding |
|
||||
| 873–935 | `main()`, `run_ui()`, `run_no_tk_fallback()`, `choose_input_with_powershell()` |
|
||||
|
||||
### Audio Detection Logic
|
||||
|
||||
Filename tokens take priority over FFprobe stream metadata. Format: 3-letter language code + channel spec — e.g., `Film_DEU51_ENG20.mov` → Stream 0 = German 5.1, Stream 1 = English 2.0. Token order maps to stream order.
|
||||
|
||||
MP4 audio selection: prefer German stereo → any German track (FFmpeg downmixes) → error if none found.
|
||||
|
||||
### Video Processing Rules
|
||||
|
||||
- SD (height ≤ 576): 8M bitrate, H.264 level 3.1
|
||||
- HD (height > 576): 30M bitrate, H.264 level 4.1
|
||||
- NTSC: height ≤ 480 and FPS ≥ 29
|
||||
- Colorspace filter only applied when source has complete metadata (primaries + transfer + space); otherwise skipped to prevent FFmpeg abort
|
||||
- Deinterlace (`bwdif`) applied if interlaced source detected
|
||||
- Forced subtitles burned if `_forced.srt` exists next to the input file
|
||||
|
||||
### UI Fallback Chain
|
||||
|
||||
1. Tkinter GUI (full interface with progress log)
|
||||
2. PowerShell native file dialog + console output (if Tkinter unavailable)
|
||||
3. CLI mode with `--cli` flag or file argument
|
||||
|
||||
### Output Naming
|
||||
|
||||
Title is derived from: Blu-ray folder name (4 levels above `BDMV`) → filename stem with audio tokens stripped → `UNKNOWN_TITLE`.
|
||||
|
||||
```
|
||||
Titel_DEU20_PVD.mp4
|
||||
Titel_DEU_AUDIO_PCM.mov
|
||||
Titel_OV_AUDIO_PCM.mov
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`config.ini` is auto-created on first run. Key settings:
|
||||
|
||||
```ini
|
||||
[ffmpeg]
|
||||
search_dirs = C:\Tools\FFMPEG, C:\Software
|
||||
|
||||
[output]
|
||||
preferred_dirs = F:\VOD, H:\VOD # first existing dir is used
|
||||
|
||||
[video]
|
||||
hd_bitrate = 30M
|
||||
sd_bitrate = 8M
|
||||
preset = slow
|
||||
tune = film
|
||||
|
||||
[audio]
|
||||
mp4_bitrate = 256k
|
||||
pcm_codec = pcm_s24le
|
||||
sample_rate = 48000
|
||||
```
|
||||
|
||||
## Language
|
||||
|
||||
Code comments, UI strings, and log messages are in German — maintain this convention when adding new messages or comments.
|
||||
@@ -198,12 +198,14 @@ Damit wird `F:\VOD` bevorzugt, wenn vorhanden. Sonst wird `H:\VOD` verwendet, we
|
||||
Dateinamen:
|
||||
|
||||
```text
|
||||
Titel_DEU20_PVD.mp4
|
||||
Titel_DEU_AUDIO_PCM.mov
|
||||
Titel_OV_AUDIO_PCM.mov
|
||||
TheDarkKnight_DEU20_PVD.mp4
|
||||
TheDarkKnight_DEU_AUDIO_PCM.mov
|
||||
TheDarkKnight_OV_AUDIO_PCM.mov
|
||||
```
|
||||
|
||||
Wenn nur deutsche Tonspuren vorhanden sind, wird keine `Titel_OV_AUDIO_PCM.mov` erzeugt.
|
||||
Der Titel-Teil (vor dem ersten Unterstrich) enthaelt keine Unterstriche – Woerter werden direkt zusammengefuegt. Unterstriche stehen nur im Audio-Suffix (`_DEU20_PVD`, `_DEU_AUDIO_PCM`, `_OV_AUDIO_PCM`).
|
||||
|
||||
Wenn nur deutsche Tonspuren vorhanden sind, wird keine `..._OV_AUDIO_PCM.mov` erzeugt.
|
||||
|
||||
## ProRes-Audio im Dateinamen
|
||||
|
||||
|
||||
+26
-28
@@ -255,31 +255,6 @@ class JobPlan:
|
||||
commands: list[tuple[str, list[str]]]
|
||||
|
||||
|
||||
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 = strip_audio_tokens(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 strip_audio_tokens(stem: str) -> str:
|
||||
cleaned = stem
|
||||
token_pattern = re.compile(r"(?i)(^|[_\-\s])([A-Z]{3}[1-8][0-9])(?=$|[_\-\s])")
|
||||
@@ -292,10 +267,33 @@ def strip_audio_tokens(stem: str) -> str:
|
||||
return cleaned or stem
|
||||
|
||||
|
||||
def extract_title(input_path: str) -> str:
|
||||
"""Extrahiert den Titel einheitlich für alle Ausgabedateien (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()]
|
||||
title = "".join(words)
|
||||
else:
|
||||
title = strip_audio_tokens(path.stem)
|
||||
|
||||
title = re.sub(r"[^A-Za-z0-9]", "", title)
|
||||
return title or "UNKNOWN_TITLE"
|
||||
|
||||
|
||||
def get_pvd_filename(input_path: str) -> str:
|
||||
return f"{extract_title(input_path)}_DEU20_PVD.mp4"
|
||||
|
||||
|
||||
def safe_output_stem(input_path: str) -> str:
|
||||
stem = strip_audio_tokens(Path(input_path).stem)
|
||||
stem = re.sub(r"[^A-Za-z0-9]+", "_", stem).strip("_")
|
||||
return stem or "UNKNOWN_TITLE"
|
||||
return extract_title(input_path)
|
||||
|
||||
|
||||
def probe_metadata(filepath: str) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user