# Deep Analysis: Production Pipeline Integration & Generation Trigger Flows (Feature F12 & F11)

**Agent**: `teamwork_preview_explorer` (M4 Pipeline Integration & Generation Trigger Specialist 2)  
**Date**: 2026-10-08  
**Scope**: `05_Production_Pipeline/run_shot.py`, `05_Production_Pipeline/production_orchestrator.py`, `web_review/server.py`, and `tests/`  

---

## 1. Executive Summary

This investigation analyzes the integration contracts between the headless production pipeline scripts (`run_shot.py`, `production_orchestrator.py`) and the Web Review Studio backend (`web_review/server.py`), specifically addressing **Feature F11** (Web Review Studio Port 1515 Synchronization) and **Feature F12** (Automated Production Runner & Concat).

### Core Findings
1. **Disconnection in `/api/generate`**: `web_review/server.py` currently executes a legacy, inline agent-browser helper (`run_agent_browser_generation`) rather than invoking `05_Production_Pipeline/run_shot.py`. As a result, it does **not** upload Start Frames, does **not** extract OpenCV Tail Frames (`clean_frame_239.jpg`), does **not** perform visual continuity checks, and saves unversioned timestamps (`muse_video_<timestamp>.mp4`) instead of standard `<shot_id>_10s_v<N>.mp4` files.
2. **Missing CLI Alias in `run_shot.py`**: `run_shot.py` expects `--shot-id` and treats `--prompt` as required (`required=True`). However, `PROJECT.md` §Interface Contracts line 78 specifies `python 05_Production_Pipeline/run_shot.py --shot <shot_id>`. Furthermore, if `--prompt` or `--input-frame` are omitted, `run_shot.py` fails rather than resolving them from the prompt registry (`muse_ai_video_prompts.json`) and preceding shot tail frames.
3. **Completely Missing `/api/concat` Endpoint**: There is currently **zero** implementation of `/api/concat` in `web_review/server.py`. Neither the endpoint nor the background execution for `production_orchestrator.py --concat-scene <scene_id>` exists.
4. **Master Video Library Indexing Gap**: `production_orchestrator.py` outputs versioned master scene videos (`<scene_id>_master_v<N>.mp4`) to `04_Assets/videos/`. However, `build_library_data()` in `server.py` only scans `06_Exports/` for scene masters and categorizes non-shot files in `04_Assets/videos/` as generic `raw_asset`, causing generated scene masters to be omitted from the Web Studio's `scene_masters` collection.
5. **Test Suite Status**: All 155 existing tests across Tiers 1-4 pass. However, existing tests only verify CLI flags, data structures, and static regexes. Concrete end-to-end trigger tests for `/api/generate` and `/api/concat` are required for M4 validation.

---

## 2. Forensic Code Audit

### 2.1. `05_Production_Pipeline/run_shot.py`
- **Location**: `c:\Projects\KieuStory\05_Production_Pipeline\run_shot.py`
- **Current CLI Parser** (Lines 199–207):
  ```python
  parser = argparse.ArgumentParser()
  parser.add_argument("--shot-id", required=True)
  parser.add_argument("--input-frame", default=None)
  parser.add_argument("--prompt", required=True)
  parser.add_argument("--prev-video", default=None)
  parser.add_argument("--session", default="muse", help="Tên session agent-browser (mặc định: muse)")
  parser.add_argument("--download-dir", default=None, help="Thư mục download cô lập cho worker")
  ```
- **Issues Identified**:
  - `parser.add_argument("--shot-id", required=True)` rejects `--shot <shot_id>`, causing `unrecognized arguments: --shot` when invoked per `PROJECT.md` contract.
  - `--prompt` is mandatory. Running a shot from CLI or UI with just `--shot ep01_scene01_shot01` fails if prompt override is not provided.
  - `--input-frame` does not auto-resolve to the sweet-spot start frame or previous tail frame if omitted.
- **Video Versioning Logic** (Lines 101–113):
  - Uses regex `^{shot_id}_10s(?:_v(\d+))?\.mp4$` in `04_Assets/videos`.
  - Determines `next_v = max(highest_v + 1, ...)` and saves to `{shot_id}_10s_v{next_v}.mp4`.
  - **Verdict**: The internal versioning logic in `run_shot_pipeline()` is well-implemented, but cannot be reached via Web Studio because `server.py` bypasses `run_shot.py`.
- **Tail Frame Extraction** (Lines 170–183):
  - Uses OpenCV (`cv2.VideoCapture`) to read frame count and capture the final frame (frame 239).
  - Writes to `04_Assets/keyframes/{shot_id}/clean_frame_239.jpg`.
  - **Verdict**: Correct and compliant with Head-Tail chaining standard.

### 2.2. `05_Production_Pipeline/production_orchestrator.py`
- **Location**: `c:\Projects\KieuStory\05_Production_Pipeline\production_orchestrator.py`
- **CLI Commands** (Lines 520–555):
  - `--status`: displays terminal table of shot and master status.
  - `--list-shots`: lists shot catalog for scene or episode.
  - `--render-shot <shot_id>`: calls `render_single_shot()`.
  - `--batch-scene <scene_id>`: batch renders entire scene sequentially, enforcing Head-Tail chaining.
  - `--concat-scene <scene_id>`: calls `concat_scene_shots(scene_id)`.
  - `--master-audio <scene_id>`: calls `master_scene_audio()`.
- **`concat_scene_shots` Analysis** (Lines 366–452):
  - Collects shots via `get_shots_for_scene(scene_id)` (sorted numerically).
  - Finds highest versioned video for each shot via `find_rendered_video(shot_id)`.
  - Resolves versioned master output: `resolve_versioned_path(VIDEOS_DIR, f"{scene_id}_master")` -> `<scene_id>_master_v<N>.mp4`.
  - **Audio Engine Integration**: Attempts `AudioContinuityEngine.stitch_with_audio_crossfade(video_paths_str, str(target_out), crossfade_dur=crossfade_dur, normalize_lufs=True)` to ensure equal-power crossfade and EBU R128 -14 LUFS loudness compliance.
  - **Fallback**: Uses FFmpeg `filter_complex` concat `[0:v:0][0:a:0]...concat=n=N:v=1:a=1[v][a]` preserving AAC 48kHz audio.
  - **Verdict**: `concat_scene_shots` is fully compliant with the audio preservation and versioning directives.

### 2.3. `web_review/server.py`
- **Location**: `c:\Projects\KieuStory\web_review\server.py`
- **Current `/api/generate` Implementation** (Lines 482–566):
  ```python
  class GenerateRequest(BaseModel):
      prompt: str
      scene_id: Optional[str] = None
      title: Optional[str] = None

  def run_agent_browser_generation(prompt: str, scene_title: str):
      # Hardcoded @e14, @e51, @e79, unversioned filenames!
      ...
  ```
- **Flaws**:
  1. No `shot_id` parameter in `GenerateRequest`.
  2. Bypasses `05_Production_Pipeline/run_shot.py`.
  3. No Start Frame upload.
  4. Generates filenames like `muse_video_1728372000.mp4` or `{title}_1728372000.mp4`, violating the mandatory `_v<N>` versioning directive (`ep01_sceneXX_shotYY_10s_v1.mp4`).
  5. Tail frame is never extracted.
- **Current `/api/concat` Implementation**:
  - **Nonexistent**. There is no route or function for concatenation.

---

## 3. Deep-Dive: Trigger Flows & Architecture Specification

```
                          ┌───────────────────────────┐
                          │   Web Review UI (1515)    │
                          └─────────────┬─────────────┘
                                        │
                 ┌──────────────────────┴──────────────────────┐
                 │ POST /api/generate                          │ POST /api/concat
                 ▼                                             ▼
  ┌─────────────────────────────┐               ┌─────────────────────────────┐
  │   web_review/server.py      │               │   web_review/server.py      │
  │   - Pydantic validation     │               │   - Pydantic validation     │
  │   - 409 Conflict check      │               │   - 409 Conflict check      │
  │   - Background task worker  │               │   - Background task worker  │
  └──────────────┬──────────────┘               └──────────────┬──────────────┘
                 │ Subprocess / Async                          │ Subprocess / Async
                 ▼                                             ▼
  ┌─────────────────────────────┐               ┌─────────────────────────────┐
  │   run_shot.py --shot <id>   │               │ production_orchestrator.py  │
  │   - Sweet-spot Start Frame  │               │   --concat-scene <scene_id> │
  │   - Audio guard in prompt   │               │   - AudioContinuityEngine   │
  │   - agent-browser session   │               │   - Crossfade & -14 LUFS    │
  │   - <shot_id>_10s_v<N>.mp4  │               │   - <scene>_master_v<N>.mp4 │
  │   - OpenCV Tail Frame       │               └──────────────┬──────────────┘
  └──────────────┬──────────────┘                              │
                 │                                             │
                 ▼                                             ▼
        04_Assets/videos/                             04_Assets/videos/
        04_Assets/keyframes/                          06_Exports/
                 │                                             │
                 └──────────────────────┬──────────────────────┘
                                        │
                                        ▼
                         ┌─────────────────────────────┐
                         │   GET /api/status           │
                         │   GET /api/library          │
                         └─────────────────────────────┘
```

### 3.1. `/api/generate` Trigger Flow
1. **Request Schema**:
   ```python
   class GenerateRequest(BaseModel):
       prompt: Optional[str] = None
       shot_id: Optional[str] = None
       scene_id: Optional[str] = None
       title: Optional[str] = None
       input_frame: Optional[str] = None
       session_name: Optional[str] = "muse"
       force: Optional[bool] = False
   ```
   - Validation Rule: Must have either `prompt` or `shot_id` (or both). Empty `{}` returns HTTP 422.
2. **Concurrency Guard**:
   - If `gen_state["status"] in ("generating", "concatenating")`: return HTTP 409 Conflict with `{"status": "busy", "message": "..."}`.
3. **Background Execution**:
   - Set `gen_state["status"] = "generating"`.
   - Update `gen_state["current_prompt"]` and `gen_state["progress_text"]`.
   - Construct CLI invocation:
     ```python
     cmd = [
         sys.executable,
         str(BASE_DIR / "05_Production_Pipeline" / "run_shot.py"),
         "--shot", resolved_shot_id,
     ]
     if prompt:
         cmd.extend(["--prompt", prompt])
     if input_frame:
         cmd.extend(["--input-frame", input_frame])
     if session_name:
         cmd.extend(["--session", session_name])
     ```
   - Stream `stdout` line by line via `subprocess.Popen`, writing to `gen_state["progress_text"]` and `log_msg(line)`.
   - On exit code 0:
     - Set `gen_state["status"] = "completed"`.
     - Find newly produced versioned video (`<shot_id>_10s_v<N>.mp4`) and set `gen_state["last_video"] = file.name`.
     - Invalidate `_library_cache = {}`.
   - On error:
     - Set `gen_state["status"] = "error"`.
     - Set `gen_state["progress_text"] = "Render failed"`.

### 3.2. `/api/concat` Trigger Flow
1. **Request Schema**:
   ```python
   class ConcatRequest(BaseModel):
       scene_id: str
       crossfade_dur: Optional[float] = 1.0
       output_path: Optional[str] = None
       master_audio: Optional[bool] = False
       bgm_path: Optional[str] = None
       ambience_path: Optional[str] = None
   ```
   - Also allow query parameter fallback: `scene_id: Optional[str] = Query(None)`.
2. **Concurrency Guard**:
   - If `gen_state["status"] in ("generating", "concatenating")`: return HTTP 409 Conflict.
3. **Background Execution**:
   - Set `gen_state["status"] = "concatenating"`.
   - Set `gen_state["progress_text"] = f"Đang ghép nối master cho cảnh {scene_id}..."`.
   - Invoke `production_orchestrator.py`:
     ```python
     cmd = [
         sys.executable,
         str(BASE_DIR / "05_Production_Pipeline" / "production_orchestrator.py"),
         "--concat-scene", scene_id,
         "--crossfade-dur", str(crossfade_dur)
     ]
     if output_path:
         cmd.extend(["--output", output_path])
     ```
   - Stream `stdout` line by line into `gen_state["progress_text"]` and `gen_state["logs"]`.
   - If `master_audio` requested, chain invocation of `--master-audio <scene_id>`.
   - On success:
     - Set `gen_state["status"] = "completed"`.
     - Find the generated `<scene_id>_master_v<N>.mp4` file and set `gen_state["last_video"] = file.name`.
     - Invalidate `_library_cache = {}`.
   - On error:
     - Set `gen_state["status"] = "error"`.

---

## 4. Test Suite Inspection (Feature F12 Across Tiers 1–4)

| Test File | Test Case | Target Feature | Assertion / Verification Method |
|---|---|---|---|
| `test_tier1_features.py` | `test_f12_01_production_orchestrator_cli_flags` | F12.1 | `--help` output contains `--status`, `--list-shots`, `--render-shot`, `--concat-scene`, `--master-audio` |
| `test_tier1_features.py` | `test_f12_02_run_shot_pipeline_callable` | F12.2 | `run_shot.py` exports `run_shot_pipeline` |
| `test_tier1_features.py` | `test_f12_03_shot_resolution_logic` | F12.3 | `resolve_start_frame("ep01_scene02_shot02", {})` executes without exception |
| `test_tier1_features.py` | `test_f12_04_get_shots_for_scene_ordering` | F12.4 | `get_shots_for_scene("ep01_scene01")` returns shots in strictly ascending numerical order |
| `test_tier1_features.py` | `test_f12_05_tail_frame_discovery` | F12.5 | `find_tail_frame("nonexistent_shot")` returns `None` |
| `test_tier2_boundaries.py` | `test_f12_b01_invalid_shot_id_handling` | F12.B1 | `find_rendered_video("shot_nonexistent_999999")` returns `None` |
| `test_tier2_boundaries.py` | `test_f12_b02_missing_input_frame_graceful_fallback` | F12.B2 | `resolve_start_frame` does not crash when `explicit_frame` is nonexistent |
| `test_tier2_boundaries.py` | `test_f12_b03_first_shot_of_scene_tail_frame_lookup` | F12.B3 | Shot 01 of scene does not attempt to lookup Shot 00 |
| `test_tier2_boundaries.py` | `test_f12_b04_empty_scene_id_query` | F12.B4 | `get_shots_for_scene("")` returns empty list |
| `test_tier2_boundaries.py` | `test_f12_b05_unsupported_cli_command_error_code` | F12.B5 | CLI exits with code 2 on unknown argument |
| `test_tier3_interactions.py`| `test_f7_f12_prompt_registry_consumable_by_runner` | F7+F12 | Production orchestrator parses prompt registry |
| `test_tier3_interactions.py`| `test_f11_f12_web_studio_generate_triggers_production_runner` | F11+F12 | `GenerateRequest` supports `scene_id` and `gen_state` tracks status |
| `test_tier3_interactions.py`| `test_f12_f13_orchestrator_concat_produces_verified_master` | F12+F13 | Inspects `concat_scene_shots` source for `filter_complex`, `concat=n=`, `libx264`, `aac` |
| `test_tier3_interactions.py`| `test_f5_f12_head_tail_chaining_resolves_via_orchestrator` | F5+F12 | `resolve_start_frame` is callable |
| `test_tier4_workloads.py` | `test_workload_08_production_orchestrator_scene_batch_simulation` | Workload 8 | Simulates shot listing and frame resolution across EP01 Scene 01 |

### Gaps to Bridge for M4 Acceptance
To guarantee complete adversarial and regression protection during M4:
1. Verify `/api/concat` endpoint registration and HTTP schema validation.
2. Verify `/api/concat` 409 conflict when another task is active.
3. Verify that `GenerateRequest(shot_id="ep01_scene01_shot01")` is valid without explicit `prompt`.
4. Verify that `run_shot.py --shot <id>` CLI flag works identically to `--shot-id <id>`.

---

## 5. Formulated Code Modifications for Worker M4

### Modification 1: `05_Production_Pipeline/run_shot.py`
**Objective**: Add `--shot` CLI alias and auto-resolve prompt & start frame when omitted.

```python
# In 05_Production_Pipeline/run_shot.py:
# Line 200:
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="THẬP NGŨ NIÊN - Pipeline Runner cho Shot đơn lẻ")
    parser.add_argument("--shot", "--shot-id", dest="shot_id", required=True, help="Mã định danh shot (vd: ep01_scene01_shot01)")
    parser.add_argument("--input-frame", default=None, help="Đường dẫn Start Frame (tự động phân giải nếu để trống)")
    parser.add_argument("--prompt", default=None, help="Prompt chuyển động video (tự động nạp từ registry nếu để trống)")
    parser.add_argument("--prev-video", default=None, help="Đường dẫn video shot trước để kiểm tra continuity")
    parser.add_argument("--session", default="muse", help="Tên session agent-browser (mặc định: muse)")
    parser.add_argument("--download-dir", default=None, help="Thư mục download cô lập cho worker")
    args = parser.parse_args()
    
    # Auto-resolve prompt if not supplied
    shot_prompt = args.prompt
    if not shot_prompt:
        prompts_file = BASE_DIR / "02_AI_Prompts" / "muse_ai_video_prompts.json"
        if prompts_file.exists():
            try:
                import json
                with open(prompts_file, "r", encoding="utf-8") as pf:
                    pdata = json.load(pf)
                    shot_entry = pdata.get("motion_prompts", {}).get(args.shot_id, {})
                    shot_prompt = shot_entry.get("motion_prompt") if isinstance(shot_entry, dict) else shot_entry
            except Exception as e:
                print(f"[!] Không thể đọc prompt registry: {e}")
        if not shot_prompt:
            print(f"[!] Lỗi: Không tìm thấy prompt cho shot '{args.shot_id}'. Vui lòng chỉ định --prompt.")
            sys.exit(1)

    # Auto-resolve start frame if not supplied
    shot_input_frame = args.input_frame
    if not shot_input_frame:
        try:
            from production_orchestrator import resolve_start_frame, get_all_shots
            all_s = get_all_shots()
            shot_data = all_s.get(args.shot_id, {})
            shot_input_frame = resolve_start_frame(args.shot_id, shot_data)
        except Exception:
            pass

    # Auto-resolve prev_video if not supplied
    shot_prev_video = args.prev_video
    if not shot_prev_video:
        try:
            from production_orchestrator import find_rendered_video
            m = re.search(r"^(ep\d+_scene\d+)_shot(\d+)$", args.shot_id, re.IGNORECASE)
            if m:
                s_prefix = m.group(1)
                s_num = int(m.group(2))
                if s_num > 1:
                    prev_id = f"{s_prefix}_shot{s_num - 1:02d}"
                    pv = find_rendered_video(prev_id)
                    if pv:
                        shot_prev_video = str(pv.resolve())
        except Exception:
            pass

    success, _ = run_shot_pipeline(
        args.shot_id, 
        shot_input_frame, 
        shot_prompt, 
        shot_prev_video,
        session_name=args.session,
        download_dir=Path(args.download_dir) if args.download_dir else None
    )
    sys.exit(0 if success else 1)
```

---

### Modification 2: `web_review/server.py`
**Objective**:
1. Update `GenerateRequest` model to accept `shot_id`, `input_frame`, `force`, `session_name`.
2. Add `ConcatRequest` model.
3. Route `/api/generate` to background worker executing `run_shot.py --shot <shot_id>`.
4. Implement `/api/concat` endpoint executing `production_orchestrator.py --concat-scene <scene_id>`.
5. Update `build_library_data()` to index scene masters in `04_Assets/videos/` as well as `06_Exports/`.
6. Enforce port 1515 and host `0.0.0.0`.

```python
# In web_review/server.py:

DEFAULT_PORT = 1515
DEFAULT_HOST = "0.0.0.0"
PIPELINE_DIR = BASE_DIR / "05_Production_Pipeline"
RUN_SHOT_SCRIPT = PIPELINE_DIR / "run_shot.py"
ORCHESTRATOR_SCRIPT = PIPELINE_DIR / "production_orchestrator.py"

from pydantic import BaseModel, root_validator

class GenerateRequest(BaseModel):
    prompt: Optional[str] = None
    shot_id: Optional[str] = None
    scene_id: Optional[str] = None
    title: Optional[str] = None
    input_frame: Optional[str] = None
    session_name: Optional[str] = "muse"
    force: Optional[bool] = False

    @root_validator(pre=True)
    def validate_prompt_or_shot(cls, values):
        p = values.get("prompt")
        s = values.get("shot_id")
        if not p and not s:
            raise ValueError("Phải cung cấp ít nhất 'prompt' hoặc 'shot_id'")
        return values

class ConcatRequest(BaseModel):
    scene_id: str
    crossfade_dur: Optional[float] = 1.0
    output_path: Optional[str] = None
    master_audio: Optional[bool] = False
    bgm_path: Optional[str] = None
    ambience_path: Optional[str] = None


def execute_shot_generation_subprocess(shot_id: str, prompt: Optional[str], input_frame: Optional[str], session_name: str):
    global gen_state
    try:
        gen_state["status"] = "generating"
        gen_state["current_prompt"] = prompt or f"[Auto from registry for {shot_id}]"
        gen_state["progress_text"] = f"Khởi động chu trình sản xuất cho shot: {shot_id}..."
        log_msg(f"Bắt đầu chu trình run_shot cho {shot_id}")

        cmd = [sys.executable, str(RUN_SHOT_SCRIPT), "--shot", shot_id]
        if prompt:
            cmd.extend(["--prompt", prompt])
        if input_frame:
            cmd.extend(["--input-frame", input_frame])
        if session_name:
            cmd.extend(["--session", session_name])

        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            encoding="utf-8",
            errors="replace",
            cwd=str(BASE_DIR)
        )

        for line in iter(proc.stdout.readline, ''):
            clean_l = line.strip()
            if clean_l:
                gen_state["progress_text"] = clean_l
                log_msg(clean_l)
        proc.wait()

        if proc.returncode == 0:
            # Locate newest video
            v_matches = sorted(list(VIDEOS_DIR.glob(f"{shot_id}*.mp4")), key=lambda p: p.stat().st_mtime, reverse=True)
            newest_vid = v_matches[0].name if v_matches else f"{shot_id}_10s_v1.mp4"
            gen_state["status"] = "completed"
            gen_state["last_video"] = newest_vid
            gen_state["progress_text"] = f"Render thành công shot: {shot_id} -> {newest_vid}"
            log_msg(f"Hoàn thành shot {shot_id}: {newest_vid}")
        else:
            gen_state["status"] = "error"
            gen_state["progress_text"] = f"Lỗi render shot {shot_id} (Exit code: {proc.returncode})"
            log_msg(f"Thất bại shot {shot_id}: code {proc.returncode}")

    except Exception as e:
        gen_state["status"] = "error"
        gen_state["progress_text"] = f"Ngoại lệ: {str(e)}"
        log_msg(f"Exception: {str(e)}")
    finally:
        global _library_cache
        _library_cache = {}


def execute_scene_concat_subprocess(scene_id: str, crossfade_dur: float, output_path: Optional[str], master_audio: bool):
    global gen_state
    try:
        gen_state["status"] = "concatenating"
        gen_state["progress_text"] = f"Đang ghép nối Master Scene: {scene_id} (crossfade {crossfade_dur}s)..."
        log_msg(f"Bắt đầu ghép Master cho {scene_id}")

        cmd = [
            sys.executable, str(ORCHESTRATOR_SCRIPT),
            "--concat-scene", scene_id,
            "--crossfade-dur", str(crossfade_dur)
        ]
        if output_path:
            cmd.extend(["--output", output_path])

        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            encoding="utf-8",
            errors="replace",
            cwd=str(BASE_DIR)
        )

        for line in iter(proc.stdout.readline, ''):
            clean_l = line.strip()
            if clean_l:
                gen_state["progress_text"] = clean_l
                log_msg(clean_l)
        proc.wait()

        if proc.returncode == 0:
            if master_audio:
                # Optionally master audio
                cmd_ma = [sys.executable, str(ORCHESTRATOR_SCRIPT), "--master-audio", scene_id]
                subprocess.run(cmd_ma, cwd=str(BASE_DIR))

            m_matches = sorted(list(VIDEOS_DIR.glob(f"{scene_id}*master*.mp4")) + list(EXPORTS_DIR.glob(f"{scene_id}*master*.mp4")), key=lambda p: p.stat().st_mtime, reverse=True)
            newest_m = m_matches[0].name if m_matches else f"{scene_id}_master_v1.mp4"
            gen_state["status"] = "completed"
            gen_state["last_video"] = newest_m
            gen_state["progress_text"] = f"Ghép nối Master thành công: {scene_id} -> {newest_m}"
            log_msg(f"Hoàn thành Concat {scene_id}: {newest_m}")
        else:
            gen_state["status"] = "error"
            gen_state["progress_text"] = f"Lỗi ghép Master cho {scene_id} (code {proc.returncode})"
            log_msg(f"Thất bại Concat {scene_id}: code {proc.returncode}")

    except Exception as e:
        gen_state["status"] = "error"
        gen_state["progress_text"] = f"Ngoại lệ: {str(e)}"
        log_msg(f"Exception: {str(e)}")
    finally:
        global _library_cache
        _library_cache = {}


@app.post("/api/generate")
async def trigger_generate(req: GenerateRequest, bg_tasks: BackgroundTasks):
    global gen_state
    if gen_state["status"] in ("generating", "concatenating"):
        return JSONResponse({"status": "busy", "message": "Một tác vụ sinh/ghép video đang chạy"}, status_code=409)

    shot_id = req.shot_id
    if not shot_id:
        if req.title and re.search(r"ep\d+_scene\d+_shot\d+", req.title, re.IGNORECASE):
            shot_id = re.search(r"ep\d+_scene\d+_shot\d+", req.title, re.IGNORECASE).group(0).lower()
        else:
            shot_id = f"custom_shot_{int(time.time())}"

    bg_tasks.add_task(
        execute_shot_generation_subprocess,
        shot_id=shot_id,
        prompt=req.prompt,
        input_frame=req.input_frame,
        session_name=req.session_name or "muse"
    )
    return {
        "status": "accepted",
        "message": f"Đã bắt đầu tác vụ sinh video cho shot: {shot_id}",
        "shot_id": shot_id
    }


@app.post("/api/concat")
async def trigger_concat(
    req: Optional[ConcatRequest] = None,
    scene_id: Optional[str] = Query(None),
    crossfade_dur: Optional[float] = Query(1.0),
    bg_tasks: BackgroundTasks = None
):
    global gen_state
    sc_id = (req.scene_id if req else None) or scene_id
    if not sc_id:
        raise HTTPException(status_code=422, detail="Thiếu tham số 'scene_id'")

    if gen_state["status"] in ("generating", "concatenating"):
        return JSONResponse({"status": "busy", "message": "Một tác vụ đang chạy trong hệ thống"}, status_code=409)

    xfade = (req.crossfade_dur if req else None) or crossfade_dur or 1.0
    out_path = req.output_path if req else None
    master_audio = req.master_audio if req else False

    bg_tasks.add_task(
        execute_scene_concat_subprocess,
        scene_id=sc_id,
        crossfade_dur=xfade,
        output_path=out_path,
        master_audio=master_audio
    )
    return {
        "status": "accepted",
        "message": f"Đã bắt đầu tác vụ ghép nối Master Scene cho {sc_id}",
        "scene_id": sc_id
    }
```

---

## 6. Recommendations for Worker M4

1. **Apply Edits to `run_shot.py`**:
   - Update argument parser line 201 to accept both `--shot` and `--shot-id`.
   - Make `--prompt` optional and implement fallback lookup into `muse_ai_video_prompts.json`.
   - Make `--input-frame` auto-resolve using `production_orchestrator.resolve_start_frame`.
2. **Apply Edits to `web_review/server.py`**:
   - Replace `run_agent_browser_generation` with `execute_shot_generation_subprocess`.
   - Update `GenerateRequest` with `root_validator` and additional fields (`shot_id`, `input_frame`, etc.).
   - Add `ConcatRequest` and implement `@app.post("/api/concat")`.
   - Enhance `build_library_data()` to inspect `VIDEOS_DIR` for `*master*.mp4` files and group them into `scene_masters`.
   - Ensure default port is 1515 and host is `0.0.0.0`.
3. **Verify All Tiers Pass**:
   - Run `pytest tests/test_tier1_features.py tests/test_tier2_boundaries.py tests/test_tier3_interactions.py tests/test_tier4_workloads.py`.
   - Ensure 100% pass with zero regressions.
4. **Coordinate with Explorer 3 for Frontend UI**:
   - Confirm that buttons in `web_review/templates/index.html` send `{ shot_id: id }` to `/api/generate` and `{ scene_id: sc_key }` to `/api/concat`.
