# Interactive Audio Stream Selection - Architecture Diagram ## System Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ main.py │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ ArgumentParser │ │ │ │ --filter-audio (enables audio filtering) │ │ │ │ --interactive (enables interactive mode) ← NEW │ │ │ │ --cq, --r, --m, --language, --test (existing) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ normalize_input_path() → folder path │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ process_folder( │ │ │ │ filter_audio=True/False, │ │ │ │ interactive_audio=True/False ← NEW │ │ │ │ ) │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ core/process_manager.py │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ process_folder(folder, ..., filter_audio, interactive) │ │ │ │ ↑ NEW param │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ For each video file: │ │ │ │ 1. Get source resolution & target resolution │ │ │ │ 2. Create audio_filter_config dict: │ │ │ │ { │ │ │ │ "enabled": filter_audio, │ │ │ │ "interactive": interactive_audio ← NEW FIELD │ │ │ │ } │ │ │ │ 3. Call run_ffmpeg() with audio_filter_config │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ core/encode_engine.py │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ run_ffmpeg( │ │ │ │ input_file, output_file, ..., │ │ │ │ audio_filter_config={enabled, interactive} │ │ │ │ ) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 1. streams = get_audio_streams(input_file) │ │ │ │ └─ Returns: [(index, ch, br, lang, meta), ...] │ │ │ │ │ │ │ │ 2. if audio_filter_config.get("enabled"): │ │ │ │ ├─ if audio_filter_config.get("interactive"): │ │ │ │ │ └─ Call: prompt_user_audio_selection(streams) ← ◆ │ │ │ │ │ [SHOWS PROMPT TO USER] │ │ │ │ │ └─ Returns: filtered_streams │ │ │ │ │ │ │ │ │ └─ else: │ │ │ │ └─ Call: filter_audio_streams(input_file, streams) │ │ │ │ (Automatic: keep best English + Commentary) │ │ │ │ └─ Returns: filtered_streams │ │ │ │ │ │ │ │ 3. For each stream in filtered_streams: │ │ │ │ ├─ choose_audio_bitrate() (codec selection) │ │ │ │ └─ Build FFmpeg codec params (-c:a, -b:a, etc.) │ │ │ │ │ │ │ │ 4. subprocess.run(ffmpeg_cmd) │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ core/audio_handler.py │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ def prompt_user_audio_selection(streams) ← NEW FUNCTION │ │ │ │ ◆ Interactive User Prompt ◆ │ │ │ │ │ │ │ │ Display: │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ │ │ 🎵 AUDIO STREAM SELECTION │ │ │ │ │ │ │ │ │ │ │ │ Stream #0: 2ch | Lang: eng | Bitrate: 128kbps │ │ │ │ │ Stream #1: 6ch | Lang: eng | Bitrate: 448kbps │ │ │ │ │ Stream #2: 2ch | Lang: spa | Bitrate: 128kbps │ │ │ │ │ Stream #3: 2ch | Lang: comment | Bitrate: 64kbps │ │ │ │ │ │ │ │ │ │ │ Keep streams: 1,3 │ │ │ │ │ │ │ │ │ │ │ │ ✅ Keeping 2 stream(s), removing 2 stream(s) │ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ Process: │ │ │ │ 1. Check if streams empty/single → return as-is │ │ │ │ 2. Display all streams with formatting │ │ │ │ 3. Prompt user for comma-separated indices │ │ │ │ 4. Parse and validate input │ │ │ │ 5. Filter streams to selected only │ │ │ │ 6. Log selections & removed streams │ │ │ │ 7. Return filtered_streams │ │ │ │ │ │ │ │ Error Handling: │ │ │ │ • Invalid input → Keep all (log warning) │ │ │ │ • No selections → Keep all (log warning) │ │ │ │ • Empty input → Keep all (user confirmed) │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Data Flow Example ### User Command ```bash python main.py "C:\Videos" --filter-audio --interactive ``` ### Data Transformation ``` Step 1: ArgumentParser ───────────────────── Input Args: folder = "C:\Videos" filter_audio = True interactive_audio = True Output: args object ──────────────────────────────────────────────────────── Step 2: main() → process_folder() ─────────────────────────────────── Input: folder, filter_audio=True, interactive_audio=True Output: Called with both flags ──────────────────────────────────────────────────────── Step 3: process_folder() → Builds audio_filter_config ────────────────────────────────────────────────────── Input: filter_audio=True interactive_audio=True Logic: if filter_audio is not None: audio_filter_config = { "enabled": True, "interactive": True ← NEW } Output: audio_filter_config dict ──────────────────────────────────────────────────────── Step 4: process_folder() → run_ffmpeg() ───────────────────────────────────────── Input: input_file = "movie.mkv" audio_filter_config = {"enabled": True, "interactive": True} Output: Called with config ──────────────────────────────────────────────────────── Step 5: run_ffmpeg() → Audio Stream Detection ────────────────────────────────────────────── Input: input_file = "movie.mkv" Output: streams = [ (0, 2, 128, "eng", 0), # Stream #0: 2ch English 128kbps (1, 6, 448, "eng", 0), # Stream #1: 6ch English 448kbps (2, 2, 128, "spa", 0), # Stream #2: 2ch Spanish 128kbps (3, 2, 64, "und", 0) # Stream #3: 2ch Undefined 64kbps ] ──────────────────────────────────────────────────────── Step 6: Audio Filtering Decision ──────────────────────────────── Input: audio_filter_config = {"enabled": True, "interactive": True} streams = [4 streams above] Logic: if audio_filter_config.get("enabled"): ✓ True if audio_filter_config.get("interactive"): ✓ True → Call prompt_user_audio_selection() ← INTERACTIVE PATH Output: User prompt shown to console ──────────────────────────────────────────────────────── Step 7: prompt_user_audio_selection() → User Input ────────────────────────────────────────────────────── Input: streams = [4 streams] Display: 🎵 AUDIO STREAM SELECTION ════════════════════════════════════════════════════ Stream #0: 2ch | Lang: eng | Bitrate: 128kbps Stream #1: 6ch | Lang: eng | Bitrate: 448kbps Stream #2: 2ch | Lang: spa | Bitrate: 128kbps Stream #3: 2ch | Lang: undefined | Bitrate: 64kbps Keep streams: ← WAIT FOR USER INPUT User Input: "1,3" Parse: selected_indices = {1, 3} Filter: filtered = [ (1, 6, 448, "eng", 0), ✓ Keep (3, 2, 64, "und", 0) ✓ Keep ] Output: ✅ Keeping 2 stream(s), removing 2 stream(s) Return: filtered streams ──────────────────────────────────────────────────────── Step 8: Back to run_ffmpeg() → Codec Selection ────────────────────────────────────────────── Input: streams = [ (1, 6, 448, "eng", 0), (3, 2, 64, "und", 0) ] Process each stream: Stream 1: 6ch → choose_audio_bitrate() → ("eac3", 384000) Stream 3: 2ch → choose_audio_bitrate() → ("aac", 160000) Output: FFmpeg codec params: -c:a:1 eac3 -b:a:1 384k -ac:1 6 -channel_layout:1 5.1 -c:a:3 aac -b:a:3 160k -ac:3 2 -channel_layout:3 stereo ──────────────────────────────────────────────────────── Step 9: FFmpeg Encoding ─────────────────────── Input: ffmpeg -i movie.mkv \ -vf scale=... \ -c:v av1_nvenc \ -c:a:1 eac3 -b:a:1 384k ... \ -c:a:3 aac -b:a:3 160k ... \ output.mkv Process: FFmpeg encodes video and audio streams Only streams 1 and 3 included (streams 0 and 2 excluded) Output: output.mkv (with only selected audio tracks) ``` ## State Diagram ``` ┌─────────────────────────────────┐ │ User Runs Script │ │ --filter-audio --interactive │ └──────────────┬──────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ Parse Arguments │ │ interactive_audio = True │ └──────────────┬──────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ process_folder() │ │ Build audio_filter_config │ │ {enabled: T, interactive: T} │ └──────────────┬──────────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ For each file Detect audio streams ┌──────────────┐ get_audio_streams() │ run_ffmpeg() │ └─ Returns 4 streams └──────┬───────┘ │ ▼ ┌──────────────────────────┐ │ Check filter enabled? │ │ audio_filter_config │ └──────┬─────────────┬─────┘ │ No │ Yes │ ▼ │ ┌─────────────────────┐ │ │ Check interactive? │ │ └────┬────────────┬───┘ │ │ No │ Yes │ │ ▼ │ │ ┌───────────────────┐ │ │ │ INTERACTIVE PROMPT│ │ │ │ Show streams │ │ │ │ Get user input │ │ │ │ Filter streams │ │ │ └─────────┬─────────┘ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ Automatic Filter │ │ │ │ (Best English + │ │ │ │ Commentary) │ │ │ └─────────┬────────┘ │ │ │ │ └────────────────┴───────────┘ │ ▼ ┌────────────────────────────────┐ │ Apply Codec Selection │ │ (for selected streams only) │ │ choose_audio_bitrate() │ └────────────┬───────────────────┘ │ ▼ ┌────────────────────────────────┐ │ Build FFmpeg Command │ │ (with selected audio streams) │ └────────────┬───────────────────┘ │ ▼ ┌────────────────────────────────┐ │ Run FFmpeg Encoding │ │ subprocess.run(cmd) │ └────────────┬───────────────────┘ │ ▼ ┌────────────────────────────────┐ │ Success/Failure Handling │ │ Log Results │ └────────────┬───────────────────┘ │ ┌────────────┴─────────┐ │ │ Next file? Process Complete ``` ## Component Interaction ``` ┌─────────────┐ │ main.py │ └──────┬──────┘ │ calls with (filter_audio, interactive_audio) │ ▼ ┌──────────────────────┐ │ process_manager.py │ ├──────────────────────┤ │ • Build config │ ◄─── Set "interactive" field │ • For each file: │ in audio_filter_config │ └─ run_ffmpeg() │ └──────┬───────────────┘ │ passes audio_filter_config │ ▼ ┌──────────────────────┐ │ encode_engine.py │ ├──────────────────────┤ │ • Check "enabled" │ ◄─── Decide which │ • Check "interactive"│ filtering method │ • Route to: │ to use │ ├─ interactive path│ │ └─ automatic path │ └──────┬───────────────┘ │ passes streams │ ▼ ┌──────────────────────┐ │ audio_handler.py │ ├──────────────────────┤ │ • Interactive: │ │ prompt_user_...() │◄──── NEW FUNCTION │ └─ Show & filter │ Shows prompt │ │ Gets user input │ • Automatic: │ Returns filtered │ filter_audio_...() │ │ └─ Logic filter │ └──────────────────────┘ │ returns filtered streams │ ▼ ┌──────────────────────┐ │ encode_engine.py │ ├──────────────────────┤ │ • Codec selection │ │ • Build FFmpeg cmd │ │ • Run encoding │ └──────────────────────┘ ``` --- This architecture ensures clean separation of concerns: - **main.py**: CLI interface - **process_manager.py**: Orchestration & config building - **encode_engine.py**: FFmpeg command building & execution - **audio_handler.py**: Audio detection & stream filtering The interactive prompt is cleanly isolated in `audio_handler.py` and only called when needed.