conversion_project/ARCHITECTURE.md
2026-01-08 18:52:06 -05:00

24 KiB

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

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.