# TheRoad Scene Lab -- Architecture Document

> Generated: 2026-03-21
> Codebase snapshot: commit `2ddd5aa` (main branch)

---

## Table of Contents

1. [System Overview](#1-system-overview)
2. [LLM Infrastructure](#2-llm-infrastructure)
3. [Pipeline Steps (Detailed)](#3-pipeline-steps-detailed)
   - 3.1 [Entity Extraction (4-Turn Multi-turn)](#31-entity-extraction)
   - 3.2 [Scene Segmentation (Gemini Lite 2-Stage)](#32-scene-segmentation)
   - 3.3 [Scene Dependency Extraction](#33-scene-dependency-extraction)
   - 3.4 [Outlook Extraction](#34-outlook-extraction)
   - 3.5 [Scene Detail Analysis (Parallel)](#35-scene-detail-analysis)
   - 3.6 [World Guide Generation](#36-world-guide-generation)
   - 3.7 [Reference Image Generation](#37-reference-image-generation)
   - 3.8 [Scene Image Generation](#38-scene-image-generation)
   - 3.9 [Prompt Sanitizer](#39-prompt-sanitizer)
4. [Service Orchestration](#4-service-orchestration)
5. [LLM Router -- Per-Step Model Configuration](#5-llm-router)
6. [Configuration Reference](#6-configuration-reference)
7. [Prompt Version Registry](#7-prompt-version-registry)
8. [Data Flow Diagram](#8-data-flow-diagram)

---

## 1. System Overview

TheRoad Scene Lab is a screenplay-to-webbook pipeline. It takes a full screenplay/scenario text and produces:
- Extracted entities (characters, locations, props) with visual descriptions
- Scene-by-scene breakdowns with T2I prompts
- Reference images for each entity (character face, outfit, locations, props)
- Scene images (cinematic still frames) for each scene beat

The pipeline uses two LLM providers:
- **Gemini** -- text analysis + scene segmentation + image generation (T2I)
- **OpenAI GPT** -- LVM (vision) judgments: validation, comparison selection, detail extraction

No chunking is applied to screenplay text; the full text is always sent in one call.

---

## 2. LLM Infrastructure

### 2.1 Gemini Text Client

**File:** `backend/app/modules/llm/gemini_text_client.py` (lines 1-222)

- REST API client using `urllib.request` (no SDK dependency)
- API endpoint: `https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent`
- Supports multi-turn conversation (history maintained in `_history` list)
- Structured output via `responseJsonSchema` in generationConfig
- Auto-logs every call to DB via `llm_logger.log_llm_call()`
- Retry logic: retries on HTTP 429/500/502/503/504, with exponential backoff (`2 * attempt` seconds)
- On 429, rotates to next API key from the key pool (if not using fixed key)
- Temperature default: 0.2
- Max output tokens: configured via `settings.llm_max_output_tokens` (default 65536)
- Timeout: `settings.llm_timeout_text` (default 600 seconds)

Key methods:
- `send()` -- multi-turn message, returns dict (structured) or str (free text)
- `send_structured()` -- convenience wrapper that enforces dict return
- `reset_history()` -- clears multi-turn conversation state

### 2.2 OpenAI Client

**File:** `backend/app/modules/llm/openai_client.py` (lines 1-170)

- REST API client using `urllib.request` (no SDK dependency)
- API endpoint: `https://api.openai.com/v1/responses` (Responses API)
- Structured output via `json_schema` format with `strict: true`
- Auto-logs every call to DB
- Same retry logic as Gemini (429/500/502/503/504, exponential backoff)
- Default model: `gpt-5.5` (configurable via `settings.openai_model`)

Key method:
- `generate_structured()` -- single-turn structured output call

### 2.3 Gemini Key Pool

**File:** `backend/app/modules/llm/gemini_key_pool.py` (lines 1-91)

- Thread-safe round-robin API key rotation
- Loads keys from environment variables: `GEMINI_API_KEY` (base) + `GEMINI_API_KEY1`, `GEMINI_API_KEY2`, etc.
- Also reads from `.env` file in `backend/` directory
- Uses `threading.Lock` for counter synchronization
- `get_next_key()` returns the next key in round-robin order
- Purpose: distribute rate-limit load across multiple API keys

### 2.4 Gemini Image Client

**File:** `backend/app/modules/llm/gemini_image_client.py` (lines 1-243)

- REST API client for Gemini image generation
- Same URL template as text client, uses `generateContent` endpoint
- Default model: `settings.gemini_image_model` (`gemini-3.1-flash-image-preview`)
- Supports labeled reference images: text label + inline base64 PNG
- Response modalities: `["TEXT", "IMAGE"]`
- Image config: `aspectRatio`, `imageSize: "2K"`
- `ModerationError` exception for safety/content blocks (detected via `promptFeedback.blockReason` or candidate `finishReason: SAFETY/IMAGE_SAFETY`)
- Uses key pool round-robin, same retry logic as text client
- Timeout: `settings.llm_timeout_image_gen` (default 180 seconds)
- Returns: `(image_bytes, elapsed_ms)` tuple

### 2.5 LLM Logger

**File:** `backend/app/modules/llm/llm_logger.py` (lines 1-72)

- Logs every LLM call (success or error) to `LLMCallLog` DB table
- Truncates prompts/output at 10,000 characters
- Non-fatal: catches all DB exceptions and logs a warning
- Records: model_name, system_prompt, user_prompt, output_text, duration_ms, input_tokens, output_tokens, status, error_message, project_id, episode_id, operation_type, step_name, reference_image_ids

### 2.6 LLM Routing (LiteLLM Router)

**Files:**
- `backend/app/modules/llm/llm_client.py` — LiteLLM Router 통합 진입점 (`call_structured` / `call_text` / `call_multiturn` + `_resolve_model`)
- `backend/app/core/step_manifest.py` — single source of truth (`default_model` / `provider` 필드)

- **Single source**: `STEP_MANIFEST.<step>.default_model` 가 truth (alias 형식, e.g. ``"gpt-mini"``). `llm_client.PIPELINE_STEPS` 는 manifest + extension table 의 derived view (problems.md #5).
- Project-level overrides via `project_llm_config` dict stored in `ProjectSettings.llm_config_json`.
- `call_structured()` — LiteLLM Router 가 provider 별 변환 자동 처리, `response_format=json_schema strict=True` + local jsonschema validation (problems.md #13).
- 이전 `llm_router.py` (이 dead code) 는 2026-05-02 삭제.
- `call_text()` -- same routing for free-text responses

---

## 3. Pipeline Steps (Detailed)

### 3.1 Entity Extraction

**Function:** `extract_entities_multiturn()`
**File:** `backend/app/modules/pipeline/entity_extractor_v2.py` (line 234)

**LLM Models Used:**
| Sub-step | Provider | Model | Notes |
|---|---|---|---|
| Turn 0 (style) | Gemini | `gemini-3.1-pro-preview` | via `GeminiTextClient()` default |
| Turn 1 (names) | Gemini | `gemini-3.1-pro-preview` | same session (multi-turn) |
| Turn 1.5 (review) | OpenAI | `gpt-5.5` | GPT LVM review of entity list |
| Turn 1.7 (batch detail) | OpenAI | `gpt-5.5` | via `OpenAIClient()` |
| Turn 2+ (T2I prompts) | Gemini | `gemini-3.1-pro-preview` | parallel, independent clients |

**Input:**
- `fulltext` -- full screenplay text (no chunking)
- `prior_entities` -- entity list from previous episodes (optional, for name consistency)
- `checkpoint_dir` -- directory for resume checkpoints

**Output:**
```python
{
    "characters": [{"name", "description", "visual_traits", "t2i_prompt"}],
    "locations":  [{"name", "description", "visual_traits", "t2i_prompt"}],
    "props":      [{"name", "description", "visual_traits", "t2i_prompt"}],
    "brief":      {Turn 1 result},
    "style":      {Turn 0 result -- era, region, genre, episode_summary},
}
```

**Prompt Files:**
- Directory: `prompts/_base/entity_extractor_v2/`
- Latest version: `1.202603171237/`
- Files used:
  - `system.md` -- system instruction for all turns
  - `turn0_style.md` -- style/era/genre analysis prompt
  - `turn0_style_schema.json` -- JSON schema for Turn 0 output
  - `turn1.md` -- entity names extraction prompt
  - `turn1_review_schema.json` -- JSON schema for GPT Turn 1.5 review
  - `turn1_7_detail_batch.md` -- batch detail extraction prompt
  - `turn1_7_detail_batch_schema.json` -- JSON schema for Turn 1.7
  - `turn_entity_detail.md` -- per-entity T2I prompt generation

**JSON Schemas:**
- `TURN1_SCHEMA` (inline, line 44): `{characters: [{name, appearances}], locations: [...], props: [...]}`
- `ENTITY_DETAIL_SCHEMA` (inline, line 68): `{name, entity_type, description, visual_traits, t2i_prompt}`
- `turn0_style_schema.json` (external): style analysis output
- `turn1_review_schema.json` (external): GPT review with importance/validity
- `turn1_7_detail_batch_schema.json` (external): batch entity details

**Threading/Parallelism:**
- Turns 0, 1, 1.5, 1.7: **sequential** (multi-turn conversation + GPT review)
- Turn 2+ (T2I per entity): **parallel** via `ThreadPoolExecutor`
  - `max_workers = min(10, total_entities)`
  - 2-second stagger between submissions (RPM limit avoidance)
  - Each entity uses an independent `GeminiTextClient()` (no shared history)
  - 3 retries per entity with `2 * attempt` second backoff

**Checkpoint/Resume:**
- Checkpoint file: `{checkpoint_dir}/entity_extraction_{md5(fulltext)[:8]}.json`
- Saves after each completed turn: `completed_turn` field tracks progress
- Turn completion order: `turn0` -> `turn1` -> `turn1.5` -> `turn1.7` -> (parallel, no per-entity checkpoint)
- Checkpoint deleted on successful completion

**Dependencies:** None (first step in pipeline)

---

### 3.2 Scene Segmentation

**Function:** `_segment_scenes()`
**File:** `backend/app/modules/pipeline/scene_extractor_v2.py` (line 132)

**LLM Models Used:**
| Sub-step | Provider | Model | Notes |
|---|---|---|---|
| Stage 1: Primary segmentation | Gemini | `gemini-3.1-flash-lite-preview` | `_segment_by_llm()` (line 272) |
| Stage 2: Large scene re-split | Gemini | `gemini-3-flash-preview` | `_split_large_scene_by_llm()` (line 167) |

**Input:**
- `fulltext` -- full screenplay text
- `split_threshold` -- max characters per scene segment (default 600, configurable per project via `ProjectSettings.scene_split_threshold`)

**Output:**
```python
[{
    "scene_index": int,      # 1-based
    "heading": str,          # scene title (Korean)
    "start_char": int,       # character offset in fulltext
    "end_char": int,
    "length": int,           # end_char - start_char
}]
```

**Prompt Files:** None external -- prompts are inline in the code.

**JSON Schemas (inline):**
- Segmentation schema (line 297): `{scenes: [{scene_index, heading, start_line_text}]}`
- Re-split schema (line 186): same structure

**Segmentation Fallback Chain:**
1. `_segment_by_llm()` -- Gemini Lite AI segmentation (primary)
2. `_segment_by_heading()` -- regex `INT./EXT.` heading detection (fallback if LLM returns empty)
3. `_single_scene_fallback()` -- entire text as one scene (ultimate fallback)

**Threading/Parallelism:** Sequential (single call per stage)

**Checkpoint/Resume:**
- Segments saved to `{checkpoint_dir}/segments.json` by `analysis_service._run_scene_phase()`
- If file exists on resume, segmentation is skipped entirely

**Dependencies:** Entity Extraction must complete first (style result used for context)

---

### 3.3 Scene Dependency Extraction

**Function:** `extract_scene_dependencies()`
**File:** `backend/app/modules/pipeline/scene_dependency_extractor.py` (line 19)

**LLM Model:** Gemini `gemini-3.1-pro-preview` (default via `GeminiTextClient()`)
**Router step:** `scene_dependency` (default: `gemini-3.1-pro-preview`)

**Input:**
- `segments` -- list of scene segments from Step 3.2
- `fulltext` -- full screenplay text
- Each scene: `scene_index`, `heading`, `text_preview` (first 600 chars)

**Output:**
```python
{
    scene_index: {
        "prev_ref": int,    # visually related previous scene index (-1 if none)
        "next_ref": int,    # visually related next scene index (-1 if none)
        "reason": str,      # reason for the connection
    }
}
```

**Prompt Files:**
- Directory: `prompts/_base/scene_dependency/`
- Latest version: `1.202603190100/`
- Files:
  - `extract_prompt.md` -- extraction prompt template
  - `extract_schema.json` -- JSON schema for output

**JSON Schema:** Loaded from `extract_schema.json` (external)

**Threading/Parallelism:** Sequential (single LLM call for all scenes)
- 3 retries with `5 * (retry + 1)` second backoff

**Checkpoint/Resume:**
- Saved to `{checkpoint_dir}/scene_dependencies.json` by `analysis_service._run_scene_phase()`
- Skipped on resume if file exists

**Dependencies:** Scene Segmentation (Step 3.2) must complete first

---

### 3.4 Outlook Extraction

**Function:** `extract_all_outlooks()`
**File:** `backend/app/modules/pipeline/outlook_extractor.py` (line 35)

**LLM Model:** Routed via `llm_client.call_structured(step="outlook_extraction")`
**Default:** Gemini `gemini-3.1-pro-preview`

**Input:**
- `segments` -- scene segment list (with full scene text extracted from fulltext)
- `fulltext` -- full screenplay text
- `characters` -- list of confirmed character names (from entity extraction)

**Output:**
```python
{
    "outlooks": [{
        "name": str,           # e.g., "formal_suit"
        "description": str,    # visual description of the outfit
        "is_shared": bool,     # whether shared across characters
    }],
    "scene_assignments": [{
        "scene_index": int,
        "characters": [{
            "character_name": str,
            "outlook_name": str,
        }],
    }],
}
```

**Prompt Files:**
- Directory: `prompts/_base/outlook_extractor/`
- Latest version: `3.202603202200/`
- Version history: `1.202603181600/` -> `2.202603201900/` -> `3.202603202200/`
- Files:
  - `extract_prompt.md` -- extraction prompt template
  - `extract_schema.json` -- JSON schema for output

**JSON Schema:** Loaded from `extract_schema.json` (external)

**Threading/Parallelism:** Sequential (single LLM call with all scenes + characters)
- 3 retries with `5 * (retry + 1)` second backoff

**Checkpoint/Resume:**
- Saved to `{checkpoint_dir}/outlook_extraction.json` by `analysis_service._run_scene_phase()`
- Skipped on resume if file exists

**Dependencies:**
- Scene Segmentation (Step 3.2) -- needs segments
- Entity Extraction (Step 3.1) -- needs character names

---

### 3.5 Scene Detail Analysis

**Function:** `extract_scenes_multiturn()`
**File:** `backend/app/modules/pipeline/scene_extractor_v2.py` (line 545)

**LLM Model:** Routed via `llm_client.call_structured(step="scene_detail")`
**Default:** OpenAI `gpt-5.5` (configurable per project)

**Input (per scene worker):**
- `scene_text` -- extracted text for this scene segment
- `entity_names_block` -- formatted entity reference list with `[[name]+[outlook]]` markers
- `scene_outlook_mapping` -- character-outlook assignments for this scene
- `prev_ref_text` / `next_ref_text` -- headings of visually related scenes (from dependency extraction)
- `variation_count` -- number of T2I variations to generate (default 2, from `settings.scene_variation_count`)

**Output (per scene):**
```python
{
    "scene_index": int,
    "heading": str,
    "beat_title": str,
    "scene_type": str,  # "normal"|"montage"|"flashback"|"dream"|"voiceover"|"transition"
    "representative_moment": str,
    "t2i_prompt": str,
    "t2i_variations": [{"t2i_prompt": str, ...}],
    "visible_entities": [{"entity_name": str, "entity_type": str}],
    "dependent_scene_index": int,
    "dependency_reason": str,
}
```

**Aggregate output:**
```python
{
    "scene_list": [{scene_index, heading, summary, start_char, end_char}],
    "scenes": [scene detail dicts],
    "total_scenes": int,
}
```

**Prompt Files:**
- Directory: `prompts/_base/scene_extractor_v2/`
- Latest version: `6.202603202200/`
- Version history: 6 versions (`1.202603171237` through `6.202603202200`)
- Files used:
  - `system.md` -- system instruction
  - `turn_scene_detail.md` -- per-scene analysis prompt template
  - `turn1_split_long.md` -- long scene split prompt (used in Step 3.2 fallback)
  - `scene_detail_schema.json` -- JSON schema for scene detail output

**JSON Schemas:**
- `scene_detail_schema.json` (external, from latest version dir)
- Inline fallback schema at line 96 if external not found

**Threading/Parallelism:**
- **Always parallel mode** (line 630: `if True:`)
- `ThreadPoolExecutor` with `max_workers = min(settings.max_concurrent_entity_detail, queue_size)`
- Default `max_concurrent_entity_detail = 10`
- 2-second stagger between task submissions
- Each scene uses an independent LLM call via `llm_client.call_structured()` (모든 tier — local jsonschema validation 포함)
- 3 retries per scene with `5 * (retry + 1)` second backoff

**Checkpoint/Resume:**
- Checkpoint file: `{checkpoint_dir}/scene_extraction_{md5(fulltext)[:8]}.json`
- Saves after each completed scene: `completed_scenes`, `scene_list_brief`, `all_outlooks`, `outlook_names_so_far`
- Completed scene indices tracked to skip on resume
- Checkpoint deleted on successful completion

**Dependencies:**
- Entity Extraction (Step 3.1)
- Scene Segmentation (Step 3.2)
- Scene Dependency Extraction (Step 3.3)
- Outlook Extraction (Step 3.4)

---

### 3.6 World Guide Generation

**Function:** `WorldGuideGenerator.generate()`
**File:** `backend/app/modules/world_guide_generator.py` (line 106)

**LLM Model:** OpenAI `gpt-5.5` (via `OpenAIClient`)
**Router step:** `world_guide` (default: OpenAI `gpt-5.5`)

**Input:**
- `fulltext` -- full screenplay text
- `language` -- "ko" or "en"
- `source_file` -- source filename
- `entities` -- compact entity list (name, entity_type, description, stable_traits)
- `stills` -- compact scene still list (still_index, heading, beat_title, still_frame_prompt)

**Output:**
```python
{
    "world_time_period": str,
    "world_setting_summary": str,
    "technology_level": str,
    "era_guardrails": [str],         # 3-8 items
    "costume_guardrails": [str],     # 3-8 items
    "location_guardrails": [str],    # 2-6 items
    "prop_guardrails": [str],        # 2-6 items
    "prohibited_visual_misreads": [str],  # 4-12 items
    "continuity_guardrails": [str],  # 3-8 items
    "image_generation_notes": [str], # 2-6 items
    "style_rules": {
        "must_maintain": [str],      # 3-5 items
        "must_avoid": [str],         # 3-5 items
    },
}
```

**Prompt Files:**
- Directory: `prompts/_base/prototype_prompts/v5/`
- Files:
  - `world_guide_system.md` -- system prompt
  - `world_guide_user.md` -- user prompt template

**JSON Schema:** `WORLD_GUIDE_SCHEMA` (inline, line 17) -- strict schema with `additionalProperties: false`

**Threading/Parallelism:** Sequential (single call)

**Checkpoint/Resume:**
- World guide stored in `WorldGuide` DB table with `source_hash`
- Hash computed from `fulltext[:500] + entity_count + still_count`
- If hash matches existing record, generation is skipped

**Dependencies:**
- Entity Extraction (Step 3.1) -- needs entities
- Scene Detail Analysis (Step 3.5) -- needs stills

---

### 3.7 Reference Image Generation

**Function:** `ImageService.generate_reference_images_only()`
**File:** `backend/app/services/image_service.py` (line 441)

**LLM Models Used:**
| Sub-step | Provider | Model | Notes |
|---|---|---|---|
| T2I generation | Gemini | `gemini-3.1-flash-image-preview` | `GeminiImageClient` |
| GPT LVM validation | OpenAI | `gpt-5.5` | via `ref_image_pipeline` |
| GPT comparison selection | OpenAI | `gpt-5.5` | via `ref_image_pipeline` |

**Input:**
- Entity list from DB (id, name, entity_type, description, stable_traits)
- Visual dependency graph (base entity -> variant entity)
- T2I prompt from entity extraction or entity description

**Output:**
- PNG image files saved to `{projects_dir}/{project_id}/images/{episode_id}/reference/`
- `ImageAsset` records in DB with `asset_type="reference"`, `is_primary=1`
- Outlook composite images: character face + outfit reference

**Prompt Files:**
- Reference image prompts: `prompts/_base/ref_image_prompts/1.202603181600/`
  - `character_ref.md`, `character_outlook_ref.md`, `location_ref.md`, `prop_ref.md`

**Threading/Parallelism:**
- Entities processed in topological order (dependency graph batches)
- Within each batch: **parallel** via `ThreadPoolExecutor`
  - `max_workers = min(settings.max_concurrent_image_gen, batch_size)`
  - Default `max_concurrent_image_gen = 15`
- Outlook composites: separate parallel pass after base references complete

**Checkpoint/Resume:**
- `ImageCheckpointManager` at `{projects_dir}/{project_id}/checkpoints/images/{episode_id}/`
- Two modes: `resume` (skip already-done entities) or `full` (delete all, regenerate)
- Per-entity completion tracked

**Dependencies:**
- Analysis must be complete (`episode.status == "analyzed"`)
- Entity extraction must have produced entities

---

### 3.8 Scene Image Generation

**Function:** `ImageService.generate_images()`
**File:** `backend/app/services/image_service.py` (line 846)

**LLM Models Used:**
| Sub-step | Provider | Model | Notes |
|---|---|---|---|
| T2I prompt translation | Gemini | `gemini-3.1-pro-preview` | `_build_final_scene_prompt()` (line 62) |
| Image generation | Gemini | `gemini-3.1-flash-image-preview` | `SceneImageGenerator` |
| Angle recommendation | OpenAI | `gpt-5.5` | GPT Vision, `_select_and_recommend_angle()` |
| Best image selection | OpenAI | `gpt-5.5` | GPT Vision, `_select_final_best()` |
| Angle adjustment | fal.ai | `qwen-image-edit-2511-multiple-angles` | External API |
| Moderation retry | OpenAI | `gpt-5.5` | `PromptSanitizer` |

**Scene Prompt Construction:**
- `SceneImageGenerator._build_scene_prompt()` at line 38 in `scene_image_generator.py`
- Structured sections: SCENE DESCRIPTION -> WORLD CONTEXT -> SCENE META -> REFERENCES -> CAMERA -> LIGHTING -> SCENE RULES

**Reference Image Assembly:**
- `_build_labeled_references()` at line 243 in `scene_image_generator.py`
- Order: character references -> prop references -> previous scene (continuity)
- Location references intentionally excluded (continuity via previous scene instead)

**Prompt Translation:**
- `_build_final_scene_prompt()` at line 62 in `image_service.py`
- Converts Korean T2I prompt to English via Gemini Flash
- Removes proper nouns, maps `[[char]+[outlook]]` markers to reference image numbers
- Prompt template: `prompts/_base/scene_image/1.202603181600/translate_prompt.md`

**Output:**
- PNG image files saved to `{projects_dir}/{project_id}/images/{episode_id}/scene/`
- `ImageAsset` records in DB with `asset_type="scene"`
- Per-scene variation images (variant_a, variant_b)

**Prompt Files:**
- Scene image prompt template: `prompts/_base/scene_image/1.202603181600/translate_prompt.md`
- Scene rules: `prompts/_base/scene_generator/v1/scene_rules_ko.md`, `scene_rules_en.md`
- Prototype scene prompts: `prompts/_base/prototype_prompts/v5/scene_image_ko.md`, `scene_image_en.md`

**Threading/Parallelism:**
- Scene image generation: **parallel** via `ThreadPoolExecutor`
  - `max_workers = min(settings.max_concurrent_image_gen, stills_to_gen)`
  - Default `max_concurrent_image_gen = 15`
- Scene dependency ordering: topological sort of scene dependency graph
- Within each batch: parallel generation

**Checkpoint/Resume:**
- `ImageCheckpointManager` at `{projects_dir}/{project_id}/checkpoints/images/{episode_id}/`
- Two modes: `resume` (skip already-done stills) or `full` (delete all, regenerate)
- Per-still completion tracked

**Dependencies:**
- Analysis complete (`episode.status == "analyzed"`)
- Scene count > 0
- Reference images must exist (at least some primary references)

---

### 3.9 Prompt Sanitizer

**Class:** `PromptSanitizer`
**File:** `backend/app/modules/prompt_sanitizer.py` (line 69)

**LLM Model:** OpenAI GPT (via `BaseLLMClient` -- typically `gpt-5.5`)
**Router step:** `prompt_sanitize` (default: OpenAI `gpt-5.5`)

**Purpose:** When Gemini blocks a T2I prompt due to safety/moderation, this module rewrites the prompt using GPT to bypass the block while preserving the scene's intent.

**Input:**
- `original_prompt` -- the blocked T2I prompt
- `block_reason` -- reason for block (e.g., "SAFETY", "HARM")
- `block_categories` -- list of safety categories that triggered the block
- `attempt` -- attempt number (1-3), determines strategy

**Output:**
```python
{
    "sanitized_prompt": str,   # rewritten prompt
    "changes": str,            # description of what was changed
    "strategy": str,           # strategy key used
}
```

**Sanitization Strategies (escalating):**
| Attempt | Strategy | Description |
|---|---|---|
| 1 | `film_previs` | "Pre-visualization concept art for film production" framing |
| 2 | `movie_poster` | "Movie poster key visual" framing, emotion/mood focus |
| 3 | `aftermath` | "Moment after the action" framing, contemplative/static |

**Prompt Files:**
- Directory: `prompts/_base/prompt_sanitizer/v1/`
- Files:
  - `sanitize_system.md` -- system prompt
  - `sanitize_user.md` -- user prompt template

**JSON Schema:** `SANITIZE_SCHEMA` (inline, line 16): `{sanitized_prompt, changes, strategy}`

**Threading/Parallelism:** Sequential (called inline during image generation retry loop)

**Checkpoint/Resume:** None (stateless, called per-blocked prompt)

**Dependencies:** Called by `SceneImageGenerator.generate_for_still_with_retry()` on `ModerationError`

---

## 4. Service Orchestration

### 4.1 AnalysisService

**File:** `backend/app/services/analysis_service.py`

**`run_analysis()` (line 873)** -- Full analysis pipeline:

```
Phase 1: _run_entity_phase() (line 601)
   |-- extract_entities_multiturn()          [Entity Extraction, Step 3.1]
   |-- Save style to ProjectSettings
   |-- Save episode summary
   |-- Regenerate project summary (multi-episode)
   |-- _save_entities_v2()                   [DB: EntityCanon + RelationFact]
   |
Phase 2: _run_scene_phase() (line 704)
   |-- _segment_scenes()                     [Scene Segmentation, Step 3.2]
   |-- extract_scene_dependencies()          [Scene Dependencies, Step 3.3]
   |-- extract_all_outlooks()                [Outlook Extraction, Step 3.4]
   |-- Save outlooks to DB (EntityCanon, CharacterOutlook)
   |-- extract_scenes_multiturn()            [Scene Detail, Step 3.5]
   |-- _save_scenes_v2()                     [DB: SceneStill]
```

**`reanalyze_scenes()` (line 1004)** -- Scene-only re-analysis:
- Keeps existing entities
- Deletes scene-phase checkpoints (segments, dependencies, outlooks, per-scene)
- Runs Phase 2 only

### 4.2 ImageService

**File:** `backend/app/services/image_service.py`

**`generate_reference_images_only()` (line 441):**
```
1. Validate: analysis complete, entities exist
2. Build visual dependency graph (topological sort)
3. Get or create WorldGuide
4. For each entity batch (dependency order):
   |-- generate_and_validate_reference() [parallel, ThreadPoolExecutor]
   |-- Save ImageAsset to DB (immediate commit per entity)
5. Generate outlook composite images [parallel]
```

**`generate_images()` (line 846):**
```
1. Validate: analysis complete, scenes exist, reference images exist
2. Get or create WorldGuide
3. Load reference images from DB
4. For each scene (dependency order):
   |-- Build scene prompt (_build_scene_prompt)
   |-- Translate prompt (_build_final_scene_prompt)
   |-- Generate image (GeminiImageClient)
   |-- If ModerationError: sanitize + retry (up to 3x)
   |-- GPT Vision: angle recommendation + best selection
   |-- fal.ai: angle adjustment
   |-- Save ImageAsset to DB
```

---

## 5. LLM Routing -- Per-Step Model Configuration

**Files:**
- `backend/app/core/step_manifest.py` (single source — `default_model` / `provider`)
- `backend/app/modules/llm/llm_client.py` (`PIPELINE_STEPS` derived view + extension table)

이 표는 _step_manifest.generated.md 가 자동 생성하는 manifest snapshot 의 일부 발췌. 권위 있는 최신 표는 `docs/architecture/_step_manifest.generated.md` 참조 (재생성: `backend/.venv/bin/python backend/scripts/dump_step_manifest.py`).

| Step Key | Label | Default Provider | Default Model | Category |
|---|---|---|---|---|
| `entity_style` | 요소 추출 -- 스타일 분석 | gemini | gemini-3.1-pro-preview | analysis |
| `entity_names` | 요소 추출 -- 이름 목록 | gemini | gemini-3.1-pro-preview | analysis |
| `entity_review` | 요소 추출 -- GPT 리뷰 | openai | gpt-5.5 | analysis |
| `entity_detail` | 요소 추출 -- 상세 정보 | gemini | gemini-3.1-pro-preview | analysis |
| `scene_segmentation` | 씬 세그먼테이션 | gemini | gemini-3.1-flash-lite-preview | analysis |
| `scene_split` | 큰 씬 분할 | gemini | gemini-3-flash-preview | analysis |
| `scene_dependency` | 씬 연관 분석 | gemini | gemini-3.1-pro-preview | analysis |
| `outlook_extraction` | 아웃룩 추출 | gemini | gemini-3.1-pro-preview | analysis |
| `scene_detail` | 씬 상세 분석 | openai | gpt-5.5 | analysis |
| `world_guide` | 월드 가이드 생성 | openai | gpt-5.5 | image |
| `prompt_translation` | T2I 프롬프트 번역 | gemini | gemini-3-flash-preview | image |
| `prompt_sanitize` | 프롬프트 안전화 | openai | gpt-5.5 | image |
| `project_summary` | 프로젝트 요약 | gemini | gemini-3.1-pro-preview | analysis |

**Available Models:**

OpenAI:
- gpt-5.5 (frontier, 1M context)
- gpt-5.5-mini (mid, 400K context)
- gpt-5.5-nano (lite, 400K context)
- gpt-4.1 (frontier, 1M context)
- gpt-4.1-mini (mid, 1M context)
- gpt-4.1-nano (lite, 1M context)
- o3 (reasoning, 200K context)
- o4-mini (reasoning, 200K context)

Gemini:
- gemini-3.1-pro-preview (frontier, 1M context)
- gemini-3-flash-preview (mid, 1M context)
- gemini-3.1-flash-lite-preview (lite, 1M context)
- gemini-2.5-pro (frontier, 1M context)
- gemini-2.5-flash (mid, 1M context)
- gemini-2.5-flash-lite (lite, 1M context)

---

## 6. Configuration Reference

**File:** `backend/app/core/config.py` (lines 1-47)

| Setting | Default | Description |
|---|---|---|
| `openai_model` | `gpt-5.5` | Default OpenAI model |
| `gemini_text_model` | `gemini-3.1-pro-preview` | Default Gemini text analysis model |
| `gemini_flash_model` | `gemini-3-flash-preview` | Gemini Flash model (scene split, prompt translation) |
| `gemini_lite_model` | `gemini-3.1-flash-lite-preview` | Gemini Lite model (scene segmentation) |
| `gemini_image_model` | `gemini-3.1-flash-image-preview` | Gemini image generation model |
| `max_concurrent_image_gen` | 15 | Max parallel image generation workers |
| `max_concurrent_variation` | 10 | Max parallel variation workers |
| `max_concurrent_entity_detail` | 10 | Max parallel entity detail/scene detail workers |
| `scene_variation_count` | 2 | T2I variations per scene |
| `scene_detail_llm` | `gpt` | Scene detail analysis provider ("gemini" or "gpt") |
| `scene_segment_context_chars` | 100 | Context chars around scene segments |
| `llm_max_output_tokens` | 65536 | Max output tokens (all LLMs) |
| `llm_timeout_text` | 600 | Text analysis timeout (seconds) |
| `llm_timeout_image_gen` | 180 | Image generation timeout (seconds) |
| `llm_timeout_validation` | 120 | Image validation timeout (seconds) |
| `llm_max_retries` | 3 | Max retries on timeout/server error |
| `database_url` | PostgreSQL | Database connection string |
| `projects_dir` | `{PROJECT_ROOT}/projects` | Project data storage directory |

---

## 7. Prompt Version Registry

All prompts are stored under `prompts/_base/` with versioned subdirectories. Newer versions sort last alphabetically (descending sort = latest first).

### Active Pipeline Prompts

| Module | Prompt Directory | Latest Version | Files |
|---|---|---|---|
| Entity Extractor v2 | `entity_extractor_v2/` | `1.202603171237` | `system.md`, `turn0_style.md`, `turn0_style_schema.json`, `turn1.md`, `turn1_review_schema.json`, `turn1_7_detail_batch.md`, `turn1_7_detail_batch_schema.json`, `turn_entity_detail.md` |
| Scene Extractor v2 | `scene_extractor_v2/` | `6.202603202200` | `system.md`, `turn_scene_detail.md`, `turn0_context.md`, `turn1_split_long.md`, `scene_detail_schema.json` |
| Outlook Extractor | `outlook_extractor/` | `3.202603202200` | `extract_prompt.md`, `extract_schema.json` |
| Outlook Merger | `outlook_merger/` | `1.202603181600` | `merge_prompt.md`, `merge_schema.json` |
| Scene Dependency | `scene_dependency/` | `1.202603190100` | `extract_prompt.md`, `extract_schema.json` |
| Scene Image Translation | `scene_image/` | `1.202603181600` | `translate_prompt.md` |
| Reference Image | `ref_image_prompts/` | `1.202603181600` | `character_ref.md`, `character_outlook_ref.md`, `location_ref.md`, `prop_ref.md` |
| Prompt Sanitizer | `prompt_sanitizer/` | `v1` | `sanitize_system.md`, `sanitize_user.md` |
| World Guide | `prototype_prompts/` | `v5` | `world_guide_system.md`, `world_guide_user.md` |

### LVM (Vision) Prompts

| Module | Prompt Directory | Latest Version | Files |
|---|---|---|---|
| LVM Prompts | `lvm_prompts/` | `2.202603181600` | `entity_list_review.md`, `ref_comparison.md`, `ref_validation.md`, `scene_improvement.md`, `scene_validation.md`, `style_rules_generator.md`, `combined_variation_recommend.md`, `representative_selection.md`, `select_best_from_n.md` |

### Legacy/Supporting Prompts

| Module | Directory | Latest Version |
|---|---|---|
| Entity Extraction (v1) | `entity_extraction/` | `v7` |
| Scene Stills (v1) | `scene_stills/` | `v2` |
| Reference Image (v1) | `reference_image/` | `v2` |
| T2I Composer | `t2i_composer/` | `v1` |
| T2I Visual Converter | `t2i_visual_converter/` | `v3` |
| Variation Recommender | `variation_recommender/` | `v2` |
| I2I Editor | `i2i_editor/` | `v1` |
| Image Validation | `image_validation/` | `v1` |
| Scene Generator | `scene_generator/` | `v1` |
| PDF Validation | `pdf_validation/` | `v1` |

---

## 8. Data Flow Diagram

```
                    +-----------------+
                    |   Screenplay    |
                    |   (fulltext)    |
                    +--------+--------+
                             |
                    Phase 1: ANALYSIS
                             |
              +--------------v--------------+
              |  Entity Extraction (4-turn) |
              |  Gemini Pro + GPT 5.4       |
              +--+---------+----------+-----+
                 |         |          |
           characters  locations    props
                 |         |          |
                 +----+----+----------+
                      |
                      v
         +------------+-------------+
         |    Style / Summary       |
         |    (era, region, genre)  |
         +------------+-------------+
                      |
                    Phase 2: SCENE ANALYSIS
                      |
         +------------v-------------+
         |  Scene Segmentation      |
         |  Gemini Lite (2-stage)   |
         +------------+-------------+
                      |
         +------------v-------------+      +------------------+
         |  Scene Dependencies      |      | Outlook Extraction|
         |  Gemini Pro              |      | Gemini Pro       |
         +------------+-------------+      +--------+---------+
                      |                             |
                      +-------------+---------------+
                                    |
                      +-------------v--------------+
                      |  Scene Detail (parallel)   |
                      |  GPT 5.4 (per scene)       |
                      +-------------+--------------+
                                    |
                    DB: SceneStill + EntityCanon + CharacterOutlook
                                    |
                    Phase 3: IMAGE GENERATION
                                    |
              +---------------------v-------------------+
              |        World Guide Generation           |
              |        GPT 5.4                          |
              +---------------------+-------------------+
                                    |
              +---------------------v-------------------+
              |   Reference Image Generation            |
              |   Gemini Image + GPT Vision validation  |
              |   (parallel, topological order)          |
              +---------------------+-------------------+
                                    |
              +---------------------v-------------------+
              |   Scene Image Generation                |
              |   Prompt translation (Gemini Flash)     |
              |   T2I (Gemini Image)                    |
              |   Angle adjust (fal.ai)                 |
              |   Best selection (GPT Vision)           |
              |   Sanitize on block (GPT)               |
              |   (parallel, dependency order)           |
              +---------------------+-------------------+
                                    |
                              Output: PNG files
                              + ImageAsset DB records
```

---

## Appendix: File Index

| File | Purpose |
|---|---|
| `backend/app/core/config.py` | Settings (models, timeouts, concurrency) |
| `backend/app/modules/llm/gemini_text_client.py` | Gemini text REST client |
| `backend/app/modules/llm/gemini_image_client.py` | Gemini image REST client |
| `backend/app/modules/llm/openai_client.py` | OpenAI Responses API client |
| `backend/app/modules/llm/gemini_key_pool.py` | API key round-robin pool |
| `backend/app/modules/llm/llm_logger.py` | LLM call DB logger |
| `backend/app/modules/llm/llm_client.py` | LiteLLM Router 통합 entry + `_resolve_model` |
| `backend/app/core/step_manifest.py` | Single source — per-step model routing (`default_model` / `provider`) |
| `backend/app/modules/pipeline/entity_extractor_v2.py` | Entity extraction (4-turn) |
| `backend/app/modules/pipeline/scene_extractor_v2.py` | Scene segmentation + detail |
| `backend/app/modules/pipeline/outlook_extractor.py` | Outfit/outlook extraction |
| `backend/app/modules/pipeline/scene_dependency_extractor.py` | Scene visual dependency |
| `backend/app/modules/scene_image_generator.py` | Scene image prompt builder |
| `backend/app/modules/world_guide_generator.py` | World guide generation |
| `backend/app/modules/prompt_sanitizer.py` | Blocked prompt rewriting |
| `backend/app/services/analysis_service.py` | Analysis orchestration |
| `backend/app/services/image_service.py` | Image generation orchestration |
