423 lines
24 KiB
Markdown
423 lines
24 KiB
Markdown
# 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.
|