# REMEDIATION PLAN — MILESTONE M4 ITERATION 2

**Author**: Explorer 1 (`explorer_m4_remediation_1`)  
**Target Milestone**: Milestone M4 Iteration 2 (Gate Closure)  
**Execution Agent**: Worker 2  
**Verification Agents**: Forensic Auditor, Challenger 1, Reviewer 2  

---

## 1. Executive Summary

Milestone M4 was rejected at the Gate due to two distinct blocking defects:
1. **Defect 1 (System Invariant Violation)**: `tests/test_m2_hygiene.py::test_character_portraits_unharmed` failed on line 149 (`assert total_char_files == 62`). Physical files in `04_Assets/characters/` expanded to 117 files due to 55 untracked files (54 turnaround sheet PNGs + 1 cropped view artifact) deposited into character subdirectories during recent model sheet development.
2. **Defect 2 (String-Replacement Bug in Fallback Concat)**: In `05_Production_Pipeline/production_orchestrator.py:1170`, `ffmpeg_exe.replace("ffmpeg", "ffprobe")` mutated the directory name `ffmpeg-9.0.2-full_build` into non-existent `ffprobe-9.0.2-full_build`. The resulting `FileNotFoundError` was caught silently, causing audio-less clips (`-an`) to be diagnosed as having audio (`has_a = True`), which crashed FFmpeg with `Stream specifier ':a' ... matches no streams`.

This document provides the verified, step-by-step remediation plan and exact code diffs for Worker 2 to close both defects cleanly without circumventing invariants or tests.

---

## 2. Root Cause & Investigation Evidence

### 2.1 Defect 1: Character Assets Invariant Violation
- **Tracked Assets Integrity**:
  * `git ls-files 04_Assets/characters`: Exactly 62 files tracked in Git.
  * `CORE_EP01_CHARACTERS` (14 core portraits): 100% verified intact, valid PNG format, size > 500kB, and exact dimensions ((1280, 720) for 720p and (1376, 768) for master).
- **Untracked Pollution Breakdown (55 files total)**:
  * 54 turnaround sheets across 7 folders:
    - `01_Main_Protagonists/`: 18 files (9 characters/variants x 2: master + 720p)
    - `02_Vuong_Family_And_Fate/`: 16 files (8 characters x 2)
    - `03_Thuc_Clan_And_Hoan_Mansion/`: 4 files (2 characters x 2)
    - `04_Brokers_And_Brothels/`: 6 files (3 characters x 2)
    - `05_Imperial_Court_And_Officials/`: 6 files (3 characters x 2)
    - `06_Buddhist_And_Spiritual/`: 2 files (1 character x 2)
    - `07_Warlord_Camp_And_Rebels/`: 2 files (1 character x 2)
  * 1 view crop artifact:
    - `01_Main_Protagonists/views/thuy_kieu_maiden_portrait.png`
- **Root Cause & Active Process Status**:
  * `tools/update_banana_turnaround_prompts.py` (lines 425–428) hardcoded `master_path` and `p720_path` to `04_Assets/characters/{folder}/...`.
  * `tools/manage_turnaround_sheets.py` (line 17) hardcoded `CHARACTERS_DIR = BASE_DIR / "04_Assets" / "characters"`, and line 97 exported cropped views to `target_sheet["path"].parent / "views"`.
  * Process status: Confirmed **NO ACTIVE PROCESS** is running. The file count is stable at 117.

### 2.2 Defect 2: String-Replacement Bug in Fallback Concat
- **Bug Location**: `05_Production_Pipeline/production_orchestrator.py:1170`.
- **Corrupted Path Analysis**:
  * `get_ffmpeg()` returns:
    `C:\Users\Admin\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-9.0.2-full_build\bin\ffmpeg.exe`.
  * Naive `ffmpeg_exe.replace("ffmpeg", "ffprobe")` produces:
    `C:\Users\Admin\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffprobe-9.0.2-full_build\bin\ffprobe.exe` (`os.path.exists == False`).
  * `subprocess.run(probe_cmd)` raises `FileNotFoundError`, caught silently by line 1180 (`except Exception: has_a = True`).
  * On silent/audio-less inputs (`-an`), FFmpeg tries to map non-existent stream specifier `[{i}:a]aresample=...` and crashes.
- **Canonical Solution**:
  * `05_Production_Pipeline/audio_continuity_engine.py:59` already provides `get_ffprobe() -> str`, which correctly inspects `DEFAULT_FFPROBE_PATHS` and locates the real executable.

---

## 3. Concrete Step-by-Step Remediation Instructions for Worker 2

### Step 1: Relocate Untracked Turnaround Sheets and Views
Worker 2 will run a Python relocation script to move all untracked files out of `04_Assets/characters/` into `04_Assets/turnaround_sheets/`:

```python
# Script: relocate_turnaround_assets.py
import shutil
import subprocess
from pathlib import Path

char_dir = Path("04_Assets/characters")
dest_dir = Path("04_Assets/turnaround_sheets")
dest_dir.mkdir(parents=True, exist_ok=True)

res = subprocess.run(["git", "ls-files", "04_Assets/characters"], capture_output=True, text=True)
git_files = set(Path(p).resolve() for p in res.stdout.strip().splitlines() if p)

untracked_files = [f for f in char_dir.rglob("*") if f.is_file() and f.resolve() not in git_files]

print(f"Moving {len(untracked_files)} untracked files to {dest_dir}...")
for src_file in untracked_files:
    rel = src_file.relative_to(char_dir)
    target = dest_dir / rel
    target.parent.mkdir(parents=True, exist_ok=True)
    shutil.move(str(src_file), str(target))
    print(f"  [✓] Moved {rel} -> {target}")

# Clean up empty views directory if left behind
views_dir = char_dir / "01_Main_Protagonists" / "views"
if views_dir.exists() and not any(views_dir.iterdir()):
    views_dir.rmdir()
    print("  [✓] Cleaned up empty views directory.")
```

**Verification Gate for Step 1**:
- Run `python -c "import os; print('Total character files:', sum(len(f) for _, _, f in os.walk('04_Assets/characters')))"` -> Must output `62`.
- Run `git status -s 04_Assets/characters` -> Must be completely empty.

---

### Step 2: Update Pipeline Tools & Prompts to Prevent Future Re-pollution
To ensure no future tool run re-deposits turnaround files into `04_Assets/characters/`:

1. **In `tools/update_banana_turnaround_prompts.py`**:
   Update lines 426–427 to use `04_Assets/turnaround_sheets/`:
   ```python
   <<<<
                   master_path = f"04_Assets/characters/{folder}/{char_id}_turnaround_sheet_master.png"
                   p720_path = f"04_Assets/characters/{folder}/{char_id}_turnaround_sheet_720p.png"
   ====
                   master_path = f"04_Assets/turnaround_sheets/{folder}/{char_id}_turnaround_sheet_master.png"
                   p720_path = f"04_Assets/turnaround_sheets/{folder}/{char_id}_turnaround_sheet_720p.png"
   >>>>
   ```
   Execute `python tools/update_banana_turnaround_prompts.py` to synchronize all turnaround asset paths in `02_AI_Prompts/gemini_banana_prompts.json`.

2. **In `tools/manage_turnaround_sheets.py`**:
   Update line 17 to point to `04_Assets/turnaround_sheets`:
   ```python
   <<<<
   CHARACTERS_DIR = BASE_DIR / "04_Assets" / "characters"
   ====
   TURNAROUND_DIR = BASE_DIR / "04_Assets" / "turnaround_sheets"
   CHARACTERS_DIR = TURNAROUND_DIR
   >>>>
   ```

---

### Step 3: Fix Fallback Concat Stream Specifier Probe in `production_orchestrator.py`
Worker 2 will apply the following exact modifications to `05_Production_Pipeline/production_orchestrator.py`:

#### Modification 3A (Import Header, line 105–110):
```python
<<<<
try:
    from audio_continuity_engine import AudioContinuityEngine, get_ffmpeg
except ImportError:
    AudioContinuityEngine = None
    get_ffmpeg = lambda: "ffmpeg"
====
try:
    from audio_continuity_engine import AudioContinuityEngine, get_ffmpeg, get_ffprobe
except ImportError:
    AudioContinuityEngine = None
    get_ffmpeg = lambda: "ffmpeg"
    get_ffprobe = lambda: "ffprobe"
>>>>
```

#### Modification 3B (Fallback Concat Audio Probe, line 1170):
```python
<<<<
    # Kiểm tra audio track cho từng clip để tránh crash khi gặp clip câm (-an)
    has_audio_list = []
    for v in video_files:
        has_a = True
        try:
            ffprobe_exe = ffmpeg_exe.replace("ffmpeg", "ffprobe")
            probe_cmd = [
                ffprobe_exe, "-v", "error",
                "-show_entries", "stream=codec_type",
                "-of", "json", str(v)
            ]
====
    # Kiểm tra audio track cho từng clip để tránh crash khi gặp clip câm (-an)
    has_audio_list = []
    for v in video_files:
        has_a = True
        try:
            ffprobe_exe = get_ffprobe()
            probe_cmd = [
                ffprobe_exe, "-v", "error",
                "-show_entries", "stream=codec_type",
                "-of", "json", str(v)
            ]
>>>>
```

---

### Step 4: Add Regression Test for Fallback Concat with Silent Clips
To guarantee this bug never recurs undetected, Worker 2 will add a dedicated unit test in `tests/test_production_pipeline_m4.py`:
`test_05_fallback_concat_with_silent_clip_without_crash`:
- Stitches a normal audio clip and a silent (`-an`) clip.
- Patches `AudioContinuityEngine.stitch_with_audio_crossfade` to return `False` (forcing invocation of the fallback branch).
- Asserts that `po.concat_scene_shots` succeeds without raising exceptions or returning `None`, and produces a valid output file containing AAC stereo audio.

---

## 4. Verification & Validation Gate Suite

Worker 2 must execute and verify 100% pass across all the following suites before handing off:

1. **Hygiene & Character Assets Invariant Suite**:
   ```bash
   python -m pytest tests/test_m2_hygiene.py -v
   ```
   *Expected Result*: **6/6 PASSED** (including `test_character_portraits_unharmed`).

2. **Adversarial Audio Concat Probe Suite**:
   ```bash
   python -m pytest tests/test_adversarial_m4_audio_concat_probe.py -v
   ```
   *Expected Result*: **5/5 PASSED**.

3. **M4 Production Pipeline Suite**:
   ```bash
   python -m pytest tests/test_production_pipeline_m4.py -v
   ```
   *Expected Result*: **16/16 PASSED** (with new fallback concat test).

4. **Antigravity Critic Quality Gate Suite**:
   ```bash
   python -m pytest tests/test_critic_gate.py -v
   ```
   *Expected Result*: **30/30 PASSED**.

5. **Combined Regression Suite**:
   ```bash
   python -m pytest tests/test_production_pipeline_m3.py tests/test_adversarial_m3_audio_engine.py tests/test_tier5_pipeline_hardening.py -q
   ```
   *Expected Result*: **42/42 PASSED**.

---

## 5. Risk Assessment & Invariant Guard

| Invariant | Protection Strategy | Verification Check |
| :--- | :--- | :--- |
| **Character Bible Preservation** | Relocate ONLY files not tracked by Git (`untracked_files`). Never touch the 62 tracked files. | `git diff HEAD -- 04_Assets/characters` must be 100% empty. |
| **No Creative Asset Deletion** | Move files to `04_Assets/turnaround_sheets/` preserving all directory structure; zero files deleted. | File count in `04_Assets/turnaround_sheets/` must be exactly 55. |
| **Timeline Synchronization** | Mode A `boundary_smoothing` remains primary; fallback concat silence injection uses `aevalsrc=0` without trimming. | Probe 1 & Probe 2 zero duration drift invariant maintained. |
| **Silent Clip Resilience** | `get_ffprobe()` guarantees valid executable path on all platforms. | Fallback test passes with exit code 0. |
