# TECHNICAL BLUEPRINT — MILESTONE M4: SCENE CONCAT, AUDIO MASTERING & EBU R128 ARCHITECTURE

> **Target Features**: F9 (Audio-Preserving FFmpeg Concat), F10 (EBU R128 Audio Normalization), F11 (Ep01 Master Assembly & E2E Validation)  
> **Target Scope**: Re-production Campaign for Ep01 Scenes 01 to 10 (140 shots = 23m20s)  
> **Source Directives**: `ORIGINAL_REQUEST.md §R3`, `PROJECT.md`, `AGENTS.md §4-§6`, `00_Project_Bible/CINEMATIC_AUDIO_PIPELINE.md`  
> **Investigator**: Explorer 1 (`explorer_m4_1`)  
> **Implementer**: Worker M4 (`worker_m4_1`)  

---

## 1. EXECUTIVE SUMMARY & ARCHITECTURAL FOUNDATION

Milestone M4 establishes the final finishing pipeline for the 10-Scene Re-production Campaign of Episode 01 ("Thập Ngũ Niên"):
1. **Feature F9 (Scene Concat)**: Stitch individual 10s AI video shots into seamless Scene Masters (`<scene_id>_master_v<N>.mp4`) using FFmpeg `filter_complex concat`, retaining 100% AAC 48kHz audio streams with **zero OpenCV** video-only concatenation.
2. **Feature F10 (Audio Mastering & Normalization)**: Master individual scene audio to global broadcast standards (YouTube Green Dollar: Integrated Loudness `-14.0 LUFS ± 0.5`, True Peak `-1.0 dBTP`, Loudness Range `9-11 LU`, 48kHz stereo AAC) using Two-Pass Linear Normalization, 4-stem soundscape layering, and dynamic sidechain ducking (-14dB).
3. **Feature F11 (Grand Feature Assembly)**: Assemble all 10 completed Scene Masters (Scenes 01 to 10, total 140 shots = 23m20s) into the unified Episode 01 Feature Master: `06_Exports/ep01_full_feature_master_v1.mp4`.

### The Five Invariants of Milestone M4
| Invariant | Rule | Enforcement Mechanism |
| :--- | :--- | :--- |
| **I1. Zero OpenCV Concat** | OpenCV `cv2.VideoWriter` only processes video pixel matrices, silently destroying 100% of Muse.ai AAC audio. | Strictly prohibited by `AGENTS.md §4`. Bounded to FFmpeg `filter_complex concat` with synchronous `[v][a]` routing. |
| **I2. Sample-Accurate Timeline (Mode A)** | Audio crossfade must NOT shrink the timeline across multi-shot scenes. | Primary engine uses Mode A (`boundary_smoothing` / 30ms micro-fade with `curve=qsin`) ensuring zero timeline drift across scenes with up to 27 shots. |
| **I3. EBU R128 Compliance** | Final masters must pass YouTube Green Dollar loudness gates without codec clipping or volume penalties. | Two-Pass Linear Loudnorm (`linear=true`, I=-14 LUFS, TP=-1.0 dBTP, LRA=9-11 LU, 48kHz stereo AAC) with silence guard (`-inf` handling). |
| **I4. 4-Stem Dynamic Ducking** | BGM must not clash with character dialogue or poetry recitation. | 800Hz-3500Hz notch filter (`equalizer=f=2150:width_type=h:width=2700:g=-3.5`) and sidechain compression (-14dB, attack=50ms, release=300ms). |
| **I5. Deterministic Versioning** | Output files must never overwrite existing renders or tests. | `resolve_versioned_path` applies strict `_v<N>.mp4` version incrementing across `04_Assets/videos/` and `06_Exports/`. |

---

## 2. DEEP-DIVE CODEBASE INSPECTION & GAP ANALYSIS

### 2.1 Inspection of `concat_scene_shots` in `production_orchestrator.py`

#### Current Implementation (Lines 1000–1096)
```python
def concat_scene_shots(
    scene_id: str,
    output_path: Optional[str] = None,
    crossfade_dur: float = 1.0,
    enable_critic: bool = True
) -> Optional[Path]:
```
1. **Shot Discovery**: Gathers shots via `get_shots_for_scene(scene_id)` and finds videos via `find_rendered_video(shot_id)` (prioritizing highest version `_v3 > _v2 > _v1`).
2. **Quality Gate Integration**: If `enable_critic`, evaluates `evaluate_scene_gate(scene_id, video_paths_str)`. Only proceeds if approved (score >= 0.8).
3. **Primary Engine**:
   ```python
   engine = AudioContinuityEngine()
   success = engine.stitch_with_audio_crossfade(
       video_paths_str,
       str(target_out),
       crossfade_dur=crossfade_dur,
       normalize_lufs=True
   )
   ```
4. **Fallback Concat**:
   ```python
   inputs = []
   filter_parts = []
   for i, v in enumerate(video_files):
       inputs.extend(["-i", str(v)])
       filter_parts.append(f"[{i}:v:0][{i}:a:0]")
   n = len(video_files)
   concat_filter = f"{''.join(filter_parts)}concat=n={n}:v=1:a=1[v][a]"
   ```

#### Identified Critical Gap 1: Audio Duration Shrinkage in Mode B vs Mode A
- In `stitch_with_audio_crossfade` (`audio_continuity_engine.py` line 181), parameter `mode` defaults to `"acrossfade"` (Mode B).
- In Mode B, FFmpeg chains `acrossfade=d={crossfade_dur}:c1=qsin:c2=qsin`. For $N$ shots, total audio duration shrinks by $(N-1) \times \text{crossfade\_dur}$.
- **Impact in Ep01**:
  - `ep01_scene05` contains **27 shots** (270s video). In Mode B with 1.0s crossfade, audio shrinks by **26 seconds**, leaving the final 26 seconds as silent `apad` padding and drifting dialogue out of sync by up to 26s!
  - In contrast, Mode A (`boundary_smoothing` / `micro_crossfade`) applies a 30ms micro-fade (`curve=qsin`) at shot boundaries and concatenates without overlap:
    $$\text{afade=t=in:st=0:d=0.03} \quad + \quad \text{afade=t=out:st=cdur-0.03:d=0.03} \quad + \quad \text{concat=n=N:v=0:a=1}$$
  - Mode A eliminates click/pop at the boundaries while preserving **100.0% sample-accurate duration** (zero timeline loss).
- **Remedy**: `concat_scene_shots` MUST explicitly pass `mode="boundary_smoothing"` to `stitch_with_audio_crossfade` by default, and expose `--mode` on the CLI.

#### Identified Gap 2: Missing Audio Stream Guard in Fallback Concat
- In the fallback path, `filter_parts.append(f"[{i}:v:0][{i}:a:0]")` assumes all input video files have stream `0:a:0`.
- If an AI video was rendered without audio or is missing an audio track, FFmpeg terminates with: `Stream specifier ':a:0' in filtergraph description matches no streams.`
- In contrast, `AudioContinuityEngine` handles mute videos by generating silence (`aevalsrc=0:d={clip_dur}:s=48000:c=stereo`).
- **Remedy**: The fallback concat should also detect missing audio tracks and supply silence or rely on `AudioContinuityEngine`.

---

### 2.2 Inspection of `audio_continuity_engine.py` & `master_scene_audio`

#### Audio Normalization Pipeline
1. **Pass 1 Measurement (`measure_loudness`)**:
   Runs `loudnorm=I=-14:TP=-1.0:LRA=9:print_format=json -f null -`. Extracts `input_i`, `input_tp`, `input_lra`, `input_thresh`, `target_offset`.
   Includes `-inf` silence guard: if `input_i == "-inf"` or `< -90.0`, sets `is_silent=True` to prevent AAC encoder crashes.
2. **Pass 2 Linear Normalization (`normalize_loudness`)**:
   Constructs:
   `loudnorm=I=-14:TP=-1.0:LRA=9:measured_I={mi}:measured_TP={mt}:measured_LRA={ml}:measured_thresh={mth}:offset={off}:linear=true`
   Using `linear=true` prevents dynamic range compression and pumping artifacts.
   Uses `-c:v copy` (stream copy) for zero visual re-encoding loss, with fallback to `-c:v libx264 -crf 18`.

#### 4-Stem Sound Architecture (`mix_four_stems`)
- **Stem 1 (BGM)**:
  - Looped continuously: `-stream_loop -1 -i <stem1_bgm>`.
  - Notch EQ: `equalizer=f=2150:width_type=h:width=2700:g=-3.5` (suppresses 800Hz - 3500Hz by -3.5dB to carve room for voice).
  - Sidechain ducking: `sidechaincompress=threshold=0.08:ratio=4:attack=50:release=300` (lowers BGM by -14dB when vocal appears).
- **Stem 2 (Ambience)**:
  - Looped continuously: `-stream_loop -1 -i <stem2_ambience>`.
  - Ducked when dialogue is present.
- **Stem 3 (Foley)**:
  - Spot SFX mixed at `foley_vol=0.80`.
- **Stem 4 (Dialogue)**:
  - Dialogue track (or native video dialogue) mixed at `dialogue_vol=1.0`, triggers sidechain compression on Stems 1 and 2.
- **Final Master Output**:
  - Automatically runs Two-Pass Linear Normalization on the mixed master output to guarantee `-14.0 LUFS ± 0.5`.

#### Audio Asset Audit
Our filesystem search confirmed the presence of:
1. `04_Assets/audio/scene01_bgm.m4a`:
   - Duration: 178.88s (~3 minutes)
   - Codec: Opus in m4a container
   - Sample rate: 48000 Hz, 2 channels (Stereo)
   - Peak: -0.0 dB, Mean: -18.0 dB
2. `04_Assets/audio_sfx/ep01_scene04_festival_crowd_ambience_120s.wav`:
   - Duration: 120.0s (2 minutes)
   - Codec: PCM 16-bit
   - Sample rate: 48000 Hz, 2 channels (Stereo)
   - Peak: -10.4 dB, Mean: -28.6 dB
3. `04_Assets/audio/festival_crowd_ambience_bed.wav`:
   - Duration: 60.0s
   - Codec: PCM 16-bit, 48000 Hz, Stereo

#### Identified Gap 3: Audio Asset Path Discrepancy
- Some runbook documentation references `04_Assets/audio_sfx/scene01_bgm.m4a`, while the file resides in `04_Assets/audio/scene01_bgm.m4a`.
- **Remedy**: Create a unified `resolve_audio_asset(path_or_name: str) -> Optional[Path]` helper that resolves files across `04_Assets/audio/`, `04_Assets/audio_sfx/`, and repo paths.

---

### 2.3 Inspection of Multi-Scene Feature Assembly

#### Existing Assembly Scripts
- `assemble_ep04_feature.py`, `assemble_ep05_feature.py`, `assemble_ep06_feature.py` implement scene-to-feature concatenation for Episodes 04–06.
- `assemble_ep01_feature.py` currently targets 15 scenes (`ep01_scene01` to `ep01_scene15`, plus legacy intro files).
- However, the **Re-production Campaign** specifically targets **Scenes 01 to 10** (140 shots = 23m20s) as specified in `ORIGINAL_REQUEST.md §R3` and `PROJECT.md`:
  > "10 Scenes (140 shots = 23m20s) mapped to 188 prompt library (`02_AI_Prompts/gemini_banana_prompts.json`) and Character Bible masters."
- Furthermore, `assemble_episode_master("ep01")` is not yet available as a callable function within `production_orchestrator.py`.

#### Ep01 Re-Production Scenes Inventory
| Scene ID | Shot Count | Duration | Title / Dramatic Arc |
| :--- | :---: | :---: | :--- |
| `ep01_scene01` | 21 | 210s (3m30s) | Mở đầu: Khung truyện 198x & Bầu trời xuân Gia Tĩnh |
| `ep01_scene02` | 15 | 150s (2m30s) | Chân dung Thúy Kiều & Thúy Vân ("Mai cốt cách tuyết tinh thần") |
| `ep01_scene03` | 15 | 150s (2m30s) | Hội Đạp Thanh & Tiết Thanh Minh ven suối ngọc |
| `ep01_scene04` | 12 | 120s (2m00s) | Nấm mồ đạm bạc của Đạm Tiên bên ghềnh cỏ nội |
| `ep01_scene05` | 27 | 270s (4m30s) | Cuộc kỳ ngộ định mệnh cùng chàng Kim Trọng |
| `ep01_scene06` | 14 | 140s (2m20s) | Thơ văn xướng họa & Tiếng sét ái tình |
| `ep01_scene07` | 8 | 80s (1m20s) | Đêm trăng tương tư & Thao thức bên song cửa |
| `ep01_scene08` | 8 | 80s (1m20s) | Kim Trọng bắc thang sang vườn Thúy |
| `ep01_scene09` | 6 | 60s (1m00s) | Đêm hoa chúc thề nguyền dưới vầng trăng vằng vặc |
| `ep01_scene10` | 14 | 140s (2m20s) | Khúc tỳ bà đoàn viên & Dự cảm giông tố |
| **TOTAL** | **140 shots** | **1,400s (23m20s)** | **Ep01 Feature Master: 10 Cảnh Trọn Vẹn** |

---

## 3. SPECIFICATION & DESIGN: `assemble_episode_master`

### 3.1 Function Signature & Architecture
```python
def assemble_episode_master(
    episode_id: str = "ep01",
    scene_ids: Optional[List[str]] = None,
    output_path: Optional[str] = None,
    dry_run: bool = False,
    check_only: bool = False,
    normalize_final: bool = True
) -> Optional[Path]:
    """
    Ghép nối toàn bộ Scene Masters của một Episode thành Grand Feature Master hoàn chỉnh.
    Bảo toàn 100% âm thanh AAC 48kHz qua FFmpeg filter_complex concat ([v][a])
    và chuẩn hóa âm thanh điện ảnh chuẩn EBU R128 (-14 LUFS, TP -1.0 dBTP).
    
    Tham số:
        episode_id: Mã tập phim (mặc định: 'ep01').
        scene_ids: Danh sách scene_id tùy chọn. Nếu None, tự động nạp 10 scenes chuẩn (Scenes 01-10).
        output_path: Đường dẫn file xuất tùy chỉnh. Nếu None, dùng 06_Exports/ep01_full_feature_master_v1.mp4.
        dry_run: Chỉ mô phỏng và in lệnh FFmpeg mà không chạy render.
        check_only: Kiểm tra tính sẵn sàng của các file scene masters.
        normalize_final: Thực hiện Pass 2 EBU R128 Loudnorm cho video thành phẩm.
    """
```

### 3.2 Candidate Resolution Algorithm for Scene Masters
For each `scene_id` (e.g. `ep01_scene01` to `ep01_scene10`), candidate files are resolved by priority:
1. `06_Exports/{scene_id}_cinematic_master_v*.mp4` (highest version `_v3 > _v2 > _v1`)
2. `06_Exports/{scene_id}_cinematic_master.mp4`
3. `06_Exports/{scene_id}_master_v*.mp4` (highest version)
4. `04_Assets/videos/{scene_id}_master_v*.mp4` (highest version)
5. `04_Assets/videos/{scene_id}_master.mp4`

```python
def resolve_scene_master(scene_id: str) -> Optional[Path]:
    """Tìm master video cho scene theo thứ tự ưu tiên chất lượng và phiên bản."""
    candidates = []
    
    # 1. 06_Exports: cinematic master (versioned & unversioned)
    for p in EXPORTS_DIR.glob(f"{scene_id}_cinematic_master*.mp4"):
        if p.is_file() and p.stat().st_size > 1000:
            candidates.append(p)
            
    # 2. 06_Exports: regular master
    for p in EXPORTS_DIR.glob(f"{scene_id}_master*.mp4"):
        if p.is_file() and p.stat().st_size > 1000:
            candidates.append(p)
            
    # 3. 04_Assets/videos: scene master
    for p in VIDEOS_DIR.glob(f"{scene_id}*master*.mp4"):
        if p.is_file() and p.stat().st_size > 1000:
            candidates.append(p)
            
    if not candidates:
        return None
        
    def sort_key(p: Path):
        m = re.search(r"_v(\d+)\.mp4$", p.name, re.IGNORECASE)
        v_num = int(m.group(1)) if m else 0
        is_cinematic = 1 if "cinematic" in p.name.lower() else 0
        return (is_cinematic, v_num, p.stat().st_mtime)
        
    candidates.sort(key=sort_key, reverse=True)
    return candidates[0]
```

### 3.3 Output Resolution & Versioning
- Target filename: `ep01_full_feature_master_v<N>.mp4`.
- Output directory: `06_Exports/`.
- Resolved via `resolve_versioned_path(EXPORTS_DIR, f"{episode_id}_full_feature_master")`.

### 3.4 Concat Filter Construction
For $N$ scene master files ($N = 10$):
```bash
ffmpeg -y \
  -i 06_Exports/ep01_scene01_cinematic_master_v1.mp4 \
  -i 06_Exports/ep01_scene02_cinematic_master_v1.mp4 \
  ... \
  -i 06_Exports/ep01_scene10_cinematic_master_v1.mp4 \
  -filter_complex "[0:v:0][0:a:0][1:v:0][1:a:0]...[9:v:0][9:a:0]concat=n=10:v=1:a=1[v][a]" \
  -map "[v]" \
  -map "[a]" \
  -c:v libx264 -crf 18 -preset slow \
  -c:a aac -b:a 192k -ar 48000 \
  06_Exports/ep01_full_feature_master_raw_v1.mp4
```

### 3.5 Final EBU R128 Normalization
```python
if normalize_final and AudioContinuityEngine is not None:
    engine = AudioContinuityEngine()
    engine.normalize_loudness(
        str(raw_output),
        str(target_output),
        target_lufs=-14.0,
        target_tp=-1.0,
        target_lra=9.0,
        two_pass=True
    )
    if raw_output.exists():
        raw_output.unlink()
```

---

## 4. CONCRETE IMPLEMENTATION CODE PROPOSALS FOR WORKER M4

Worker M4 should apply these specific changes to `05_Production_Pipeline/production_orchestrator.py` and `assemble_ep01_feature.py`.

### 4.1 Addition: Audio Asset Resolver Helper
Place in `05_Production_Pipeline/production_orchestrator.py`:
```python
def resolve_audio_asset(asset_name_or_path: Optional[str]) -> Optional[Path]:
    """
    Tìm kiếm và định vị đường dẫn file âm thanh (BGM, Ambience, Foley)
    xuyên suốt các thư mục tài nguyên chuẩn:
    1. Đường dẫn tuyệt đối hoặc tương đối trực tiếp
    2. 04_Assets/audio/
    3. 04_Assets/audio_sfx/
    4. 04_Assets/audio_voice/
    """
    if not asset_name_or_path:
        return None
    p = Path(asset_name_or_path)
    if p.exists() and p.is_file():
        return p.resolve()
        
    name = p.name
    search_dirs = [
        ASSETS_DIR / "audio",
        ASSETS_DIR / "audio_sfx",
        ASSETS_DIR / "audio_voice",
        ASSETS_DIR / "archive" / "audio",
    ]
    for d in search_dirs:
        candidate = d / name
        if candidate.exists() and candidate.is_file():
            return candidate.resolve()
        # Thử tìm không phân biệt hoa thường
        for f in d.glob("*"):
            if f.name.lower() == name.lower() and f.is_file():
                return f.resolve()
    return None
```

### 4.2 Modification: `concat_scene_shots` with Mode A Default
In `production_orchestrator.py`, update `concat_scene_shots`:
```python
def concat_scene_shots(
    scene_id: str,
    output_path: Optional[str] = None,
    crossfade_dur: float = 1.0,
    enable_critic: bool = True,
    mode: str = "boundary_smoothing"  # MẶC ĐỊNH MODE A: Zero duration shrinkage
) -> Optional[Path]:
    """
    Ghép nối tất cả các shot của Scene thành video Master liền mạch
    sử dụng AudioContinuityEngine.stitch_with_audio_crossfade (Mode A: boundary_smoothing)
    để bảo toàn 100% âm thanh AAC, loại bỏ giật cụt, và chuẩn hóa -14 LUFS.
    """
    scene_shots = get_shots_for_scene(scene_id)
    if not scene_shots:
        print(f"[!] Không tìm thấy shot nào cho: {scene_id}")
        return None

    video_files = []
    for shot_id, _ in scene_shots:
        v = find_rendered_video(shot_id)
        if not v:
            print(f"[!] Thiếu video cho shot {shot_id}! Chưa thể ghép master.")
            return None
        video_files.append(v)

    # Thẩm định Scene Gate trước khi ghép nối Master
    if enable_critic and evaluate_scene_gate is not None:
        video_paths_str = [str(v) for v in video_files]
        scene_verdict = evaluate_scene_gate(scene_id, video_paths_str)
        print(f"\n🏛️ Antigravity Scene Gate ({scene_id}): Score = {scene_verdict.overall_score:.2f} | Action = {scene_verdict.suggested_action}")
        if not scene_verdict.approved:
            print(f"[!] Scene Gate từ chối ghép Master cho {scene_id}: {scene_verdict.critique_notes}")
            return None

    if output_path:
        target_out = Path(output_path)
    else:
        target_out = resolve_versioned_path(VIDEOS_DIR, f"{scene_id}_master")

    # Ưu tiên sử dụng AudioContinuityEngine (Mode A: boundary_smoothing)
    if AudioContinuityEngine is not None:
        try:
            engine = AudioContinuityEngine()
            video_paths_str = [str(v) for v in video_files]
            print(f"\n🎬 Đang ghép nối {len(video_files)} shot bằng AudioContinuityEngine (mode={mode}) -> {target_out.name}...")
            success = engine.stitch_with_audio_crossfade(
                video_paths_str,
                str(target_out),
                crossfade_dur=crossfade_dur,
                normalize_lufs=True,
                mode=mode
            )
            if success and target_out.exists():
                print(f"[✓] GHÉP MASTER THÀNH CÔNG: {target_out}")
                print(f"    Dung lượng: {target_out.stat().st_size / (1024*1024):.2f} MB")
                return target_out
            else:
                print("[!] AudioContinuityEngine thất bại, kích hoạt fallback FFmpeg filter_complex concat...")
        except Exception as e:
            print(f"[!] Lỗi AudioContinuityEngine ({e}), chuyển sang fallback FFmpeg filter_complex concat...")

    # Fallback: Hard-cut FFmpeg filter_complex concat bảo toàn âm thanh
    ffmpeg_exe = get_ffmpeg()
    inputs = []
    filter_parts = []
    for i, v in enumerate(video_files):
        inputs.extend(["-i", str(v)])
        filter_parts.append(f"[{i}:v:0][{i}:a:0]")
    
    n = len(video_files)
    concat_filter = f"{''.join(filter_parts)}concat=n={n}:v=1:a=1[v][a]"

    cmd = [
        ffmpeg_exe, "-y",
        *inputs,
        "-filter_complex", concat_filter,
        "-map", "[v]",
        "-map", "[a]",
        "-c:v", "libx264",
        "-crf", "18",
        "-preset", "slow",
        "-c:a", "aac",
        "-b:a", "192k",
        "-ar", "48000",
        str(target_out)
    ]

    print(f"\n🎬 Đang ghép nối {n} shot thành Master ({target_out.name}) qua Fallback Concat...")
    res = subprocess.run(cmd, capture_output=True, text=True)
    if res.returncode == 0 and target_out.exists():
        print(f"[✓] GHÉP MASTER THÀNH CÔNG: {target_out}")
        print(f"    Dung lượng: {target_out.stat().st_size / (1024*1024):.2f} MB")
        return target_out
    else:
        print(f"[!] Lỗi khi ghép video bằng FFmpeg:\n{res.stderr}")
        return None
```

### 4.3 Modification: `master_scene_audio` with `resolve_audio_asset`
In `production_orchestrator.py`:
```python
def master_scene_audio(
    scene_id: str,
    bgm_path: Optional[str] = None,
    ambience_path: Optional[str] = None,
    output_path: Optional[str] = None
) -> bool:
    """Master âm thanh toàn diện cho Scene video theo tiêu chuẩn YouTube Green Dollar (-14 LUFS)."""
    master_candidates = sorted(
        list(VIDEOS_DIR.glob(f"{scene_id}*master*.mp4")),
        key=lambda p: p.stat().st_mtime,
        reverse=True
    )
    if master_candidates:
        master_video = master_candidates[0]
    else:
        print(f"[!] Không tìm thấy master video cho: {scene_id}")
        return False

    if output_path:
        final_out = Path(output_path)
    else:
        final_out = resolve_versioned_path(EXPORTS_DIR, f"{scene_id}_cinematic_master")

    EXPORTS_DIR.mkdir(parents=True, exist_ok=True)

    if AudioContinuityEngine is None:
        print("[!] Lỗi: AudioContinuityEngine không khả dụng.")
        return False

    resolved_bgm = resolve_audio_asset(bgm_path)
    resolved_amb = resolve_audio_asset(ambience_path)

    print(f"\n🎵 BẮT ĐẦU MASTER ÂM THANH CHO: {scene_id} (Nguồn: {master_video.name})")
    engine = AudioContinuityEngine()

    if resolved_bgm or resolved_amb:
        print(f"   Áp dụng 4-Stem Mixing: BGM={resolved_bgm.name if resolved_bgm else 'None'}, Ambience={resolved_amb.name if resolved_amb else 'None'}...")
        success = engine.mix_four_stems(
            video_path=str(master_video),
            output_path=str(final_out),
            stem1_bgm=str(resolved_bgm) if resolved_bgm else None,
            stem2_ambience=str(resolved_amb) if resolved_amb else None,
            target_lufs=-14.0,
            two_pass=True
        )
    else:
        success = engine.normalize_loudness(
            str(master_video),
            str(final_out),
            target_lufs=-14.0,
            two_pass=True
        )

    if success and final_out.exists():
        print(f"[✓] HOÀN TẤT MASTER ÂM THANH! Xuất bản phẩm tại: {final_out}")
        return True
    else:
        print(f"[!] Master âm thanh thất bại cho {scene_id}.")
        return False
```

### 4.4 Addition: `assemble_episode_master` Implementation
Add directly to `05_Production_Pipeline/production_orchestrator.py`:
```python
# Danh mục 10 scenes chuẩn của Ep01 Re-production Campaign (140 shots = 23m20s)
EP01_CANONICAL_SCENES = [
    f"ep01_scene{i:02d}" for i in range(1, 11)
]

def resolve_scene_master(scene_id: str) -> Optional[Path]:
    """Tìm master video cho scene theo thứ tự ưu tiên chất lượng và phiên bản."""
    candidates = []
    # 1. 06_Exports: cinematic master
    for p in EXPORTS_DIR.glob(f"{scene_id}_cinematic_master*.mp4"):
        if p.is_file() and p.stat().st_size > 1000:
            candidates.append(p)
    # 2. 06_Exports: regular master
    for p in EXPORTS_DIR.glob(f"{scene_id}_master*.mp4"):
        if p.is_file() and p.stat().st_size > 1000:
            candidates.append(p)
    # 3. 04_Assets/videos: scene master
    for p in VIDEOS_DIR.glob(f"{scene_id}*master*.mp4"):
        if p.is_file() and p.stat().st_size > 1000:
            candidates.append(p)

    if not candidates:
        return None

    def sort_key(p: Path):
        m = re.search(r"_v(\d+)\.mp4$", p.name, re.IGNORECASE)
        v_num = int(m.group(1)) if m else 0
        is_cinematic = 1 if "cinematic" in p.name.lower() else 0
        return (is_cinematic, v_num, p.stat().st_mtime)

    candidates.sort(key=sort_key, reverse=True)
    return candidates[0]


def assemble_episode_master(
    episode_id: str = "ep01",
    scene_ids: Optional[List[str]] = None,
    output_path: Optional[str] = None,
    dry_run: bool = False,
    check_only: bool = False,
    normalize_final: bool = True
) -> Optional[Path]:
    """
    Ghép nối tất cả Scene Masters thành Grand Feature Master hoàn chỉnh.
    Bảo toàn 100% âm thanh AAC qua FFmpeg filter_complex concat ([v][a])
    và chuẩn hóa EBU R128 (-14 LUFS).
    """
    if scene_ids is None:
        if episode_id.lower() == "ep01":
            scene_ids = EP01_CANONICAL_SCENES
        else:
            scene_ids = [f"{episode_id}_scene{i:02d}" for i in range(1, 11)]

    EXPORTS_DIR.mkdir(parents=True, exist_ok=True)

    if output_path:
        target_out = Path(output_path)
    else:
        target_out = resolve_versioned_path(EXPORTS_DIR, f"{episode_id.lower()}_full_feature_master")

    print("=" * 80)
    print(f"🎬 KIEU STORY AI CINEMA — ASSEMBLE EPISODE MASTER ({episode_id.upper()})")
    print(f"   Quy mô phân cảnh: {len(scene_ids)} Scenes")
    print(f"   Đích xuất: {target_out}")
    print("=" * 80)

    available_scenes = []
    missing_scenes = []

    for sc in scene_ids:
        m_file = resolve_scene_master(sc)
        if m_file and m_file.exists():
            available_scenes.append((sc, m_file))
            print(f"  [✓] {sc:<18} -> {m_file.name} ({m_file.stat().st_size / (1024*1024):.1f} MB)")
        else:
            missing_scenes.append(sc)
            print(f"  [✗] {sc:<18} -> CHƯA CÓ MASTER (Thiếu file)")

    print("-" * 80)
    print(f"Tổng cảnh: {len(scene_ids)} | Sẵn sàng: {len(available_scenes)} | Thiếu: {len(missing_scenes)}")

    if check_only:
        print("[✓] Chế độ kiểm tra (--check): Báo cáo trạng thái hoàn tất.")
        return target_out if not missing_scenes else None

    if dry_run:
        print("\n[*] Chế độ mô phỏng (--dry-run):")
        inputs_str = " ".join([f"-i {p.name}" for _, p in available_scenes[:3]]) + " ..."
        print(f"    Lệnh FFmpeg dự kiến: ffmpeg -y {inputs_str} -filter_complex ... {target_out.name}")
        return target_out

    if missing_scenes:
        print(f"\n[!] Không thể ghép nối toàn vẹn Episode Master khi thiếu {len(missing_scenes)} cảnh:")
        for sc in missing_scenes:
            print(f"    - {sc}")
        return None

    # Thực hiện ghép nối FFmpeg filter_complex concat
    ffmpeg_exe = get_ffmpeg()
    inputs = []
    filter_parts = []
    for i, (_, p) in enumerate(available_scenes):
        inputs.extend(["-i", str(p)])
        filter_parts.append(f"[{i}:v:0][{i}:a:0]")

    n = len(available_scenes)
    concat_filter = f"{''.join(filter_parts)}concat=n={n}:v=1:a=1[v][a]"
    raw_target = target_out.parent / f"{target_out.stem}_raw{target_out.suffix}"

    cmd = [
        ffmpeg_exe, "-y",
        *inputs,
        "-filter_complex", concat_filter,
        "-map", "[v]",
        "-map", "[a]",
        "-c:v", "libx264",
        "-crf", "18",
        "-preset", "slow",
        "-c:a", "aac",
        "-b:a", "192k",
        "-ar", "48000",
        str(raw_target)
    ]

    print(f"\n🎬 Đang ghép nối {n} Scene Masters thành Feature Master...")
    res = subprocess.run(cmd, capture_output=True, text=True)
    if res.returncode != 0 or not raw_target.exists():
        print(f"[!] Lỗi khi ghép nối FFmpeg:\n{res.stderr}")
        return None

    print(f"[✓] Ghép thô thành công ({raw_target.name}).")

    # Chuẩn hóa âm thanh toàn diện EBU R128 Two-Pass Linear
    if normalize_final and AudioContinuityEngine is not None:
        print(f"[*] Đang chuẩn hóa âm lượng EBU R128 (-14 LUFS Two-Pass) cho Feature Master hoàn thiện...")
        engine = AudioContinuityEngine()
        norm_ok = engine.normalize_loudness(
            str(raw_target),
            str(target_out),
            target_lufs=-14.0,
            target_tp=-1.0,
            target_lra=9.0,
            two_pass=True
        )
        if norm_ok and target_out.exists():
            if raw_target.exists():
                raw_target.unlink()
            print(f"\n🎉 HOÀN TẤT EPISODE GRAND FEATURE MASTER: {target_out}")
            print(f"   Dung lượng: {target_out.stat().st_size / (1024*1024):.2f} MB")
            return target_out
        else:
            print("[!] Chuẩn hóa âm lượng thất bại, giữ lại bản raw.")
            return raw_target
    else:
        if raw_target.exists():
            shutil.move(str(raw_target), str(target_out))
        return target_out
```

### 4.5 CLI Arguments Update for `production_orchestrator.py`
Add in `main()` of `production_orchestrator.py`:
```python
parser.add_argument("--assemble-episode", type=str, help="Ghép nối tất cả Scene Masters của 1 Episode thành Feature Master (vd: ep01)")
parser.add_argument("--scenes", nargs="+", default=None, help="Danh sách cảnh tùy chỉnh cho assemble-episode")
parser.add_argument("--check", action="store_true", help="Kiểm tra tính sẵn sàng các file master mà không thực thi")
parser.add_argument("--dry-run", action="store_true", help="Mô phỏng quy trình ghép nối và in pipeline")
parser.add_argument("--mode", type=str, default="boundary_smoothing", choices=["boundary_smoothing", "acrossfade", "micro_crossfade"], help="Chế độ crossfade âm thanh")
```
And in command dispatch:
```python
elif args.assemble_episode:
    assemble_episode_master(
        episode_id=args.assemble_episode,
        scene_ids=args.scenes,
        output_path=args.output,
        dry_run=args.dry_run,
        check_only=args.check
    )
elif args.concat_scene:
    concat_scene_shots(args.concat_scene, args.output, crossfade_dur=args.crossfade_dur, mode=args.mode)
```

---

## 5. COMPREHENSIVE VERIFICATION TEST SUITE DESIGN

Worker M4 will implement the test suite `tests/test_m4_concat_audio_mastering.py` covering all features of M4:

```
tests/test_m4_concat_audio_mastering.py
├── TestFFmpegConcatArchitecture
│   ├── test_01_filter_complex_syntax_generation
│   ├── test_02_mode_a_boundary_smoothing_zero_drift
│   └── test_03_zero_opencv_audio_preservation
├── TestEbuR128Mastering
│   ├── test_04_two_pass_loudnorm_compliance (-14 LUFS ± 0.5, TP <= -1.0)
│   ├── test_05_notch_filter_and_dynamic_ducking_attenuation (-14dB)
│   └── test_06_audio_asset_resolution
└── TestEpisodeMasterAssembly
    ├── test_07_resolve_scene_master_priority
    ├── test_08_assemble_episode_master_check_mode
    ├── test_09_assemble_episode_master_dry_run
    └── test_10_full_synthetic_episode_assembly_and_loudnorm
```

---

## 6. IMPLEMENTATION ROADMAP & STEP-BY-STEP CHECKLIST FOR WORKER M4

1. **Step 1: Helper Integration (`production_orchestrator.py`)**:
   - Add `resolve_audio_asset()` to locate BGM/ambience files across `04_Assets/audio/`, `04_Assets/audio_sfx/`, etc.
   - Add `resolve_scene_master()` to prioritize versioned cinematic masters.
2. **Step 2: Upgrade `concat_scene_shots`**:
   - Add `mode="boundary_smoothing"` parameter default to prevent audio duration loss in multi-shot scenes.
3. **Step 3: Upgrade `master_scene_audio`**:
   - Integrate `resolve_audio_asset()` for both `bgm_path` and `ambience_path`.
4. **Step 4: Implement `assemble_episode_master`**:
   - Implement `assemble_episode_master()` supporting 10 canonical scenes of Ep01, `--check`, `--dry-run`, and final EBU R128 loudness normalization.
5. **Step 5: Align `assemble_ep01_feature.py`**:
   - Import and delegate to `assemble_episode_master("ep01")` in `assemble_ep01_feature.py`.
6. **Step 6: Execute M4 Test Suite**:
   - Run `pytest tests/test_m4_concat_audio_mastering.py -v`.
   - Ensure 100% test pass rate with zero regression.
