diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7004d01 --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md index 065d2e0..3840ab7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/pvd_mezzanine.py b/pvd_mezzanine.py index 3d1b888..3379bf4 100644 --- a/pvd_mezzanine.py +++ b/pvd_mezzanine.py @@ -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: