# Infrastructure — StepRunner, LLM Client, Config

> 2026-04-15 보정: v10codex 기준. Step API 주 경로 + checkpoint 저장 실패 승격 + partial 게이트 semantic 반영.

## 1. StepRunner 베이스 클래스

**파일**: `backend/app/core/step_runner.py`

모든 파이프라인 단계의 실행 프레임워크. 서브클래스는 `_execute(mode)` 만 구현.

### 라이프사이클
```python
run(mode: "resume" | "force"):
    1. check_gate()          # 선행 의존 completed/not_applicable/partial 허용
    2. check_applicability() # disabled/on_demand/if_* 체크
    3. resume 확인           # completed이면 스킵
    4. force 처리            # 하위 체크포인트 삭제 + stale 처리
    5. _execute(mode)        # 서브클래스 실행 (실제 로직)
    6. save_checkpoint()     # 원자적 JSON 저장 (실패 시 RuntimeError)
    7. invalidate_downstream() # 하위 step stale
    8. update_step_run()     # DB status 업데이트
```

### 게이트 semantic (중요)
- `check_gate()` 는 선행 step 상태로 `completed`, `not_applicable`, `partial` 모두 허용
- API `get_all_steps()` 의 `can_run` 판정도 동일 semantic 사용 (partial upstream도 runnable)
- 프런트 버튼 활성 조건도 이 semantic 유지

### 체크포인트 저장 (원자적)
```python
save_checkpoint(data):
    1. 기존 manifest.json → manifest_{timestamp}_{uuid}.json 아카이빙
    2. manifest_{run_id}.tmp 파일 생성
    3. os.replace() → manifest.json (원자적 교체)
    4. 실패 시: logger.error + RuntimeError raise (step failure로 승격)
```

### 체크포인트 소유 규칙
- **각 step은 자기 이름의 manifest.json만 쓴다**
- 다른 step의 checkpoint 파일을 `write_text()` / `os.replace()` 로 덮어쓰는 것 금지
- alias 또는 파생 step이 필요하면 공통 checkpoint writer를 통해 원자적으로 갱신
- 소비자가 여러 source를 읽어야 하는 경우: 우선 체크포인트 + fallback 체크포인트 패턴 사용

### _DetailStepMixin
scene_detail, scene_consistency 등이 사용하는 헬퍼:
```python
def _load_prev_checkpoint(self, step_id: str) -> Optional[Dict]:
    # {projects_dir}/{project_id}/checkpoints/episodes/{episode_id}/{step_id}/manifest.json 로드
```

### 반환값 규격
```python
{
    "completed_count": 51,
    "applicable_count": 51,
    "failed_count": 0,
    "data": { ... }  # step별 출력 데이터
}
```

## 2. LLM Client

**파일**: `backend/app/modules/llm/llm_client.py`

### LiteLLM Router 설정
- 전략: simple-shuffle
- 재시도: llm_max_retries (기본 3)
- 타임아웃: 텍스트 600s, 이미지 180s, 검증 120s

### PIPELINE_STEPS (모델 별칭 → 실제 모델)
```python
PIPELINE_STEPS = {
    "text_cleanup":       {"default": "gemini-lite"},
    "scene_segmentation": {"default": "gemini-flash"},
    "episode_summary":    {"default": "gpt-mini"},
    "visual_world_rules": {"default": "gpt"},
    "beat_extract":       {"default": "gemini-pro"},
    "shot_extract":       {"default": "gemini-pro"},
    "entity_all_character": {"default": "gemini-pro"},
    "scene_director":     {"default": "gemini-pro"},
    "shot_staging":       {"default": "gpt"},
    "scene_consistency":  {"default": "gemini-pro"},
    "scene_detail":       {"default": "gpt"},
    "shot_dependency_t2i": {"default": "gpt-mini"},
    "t2i_review":         {"default": "gemini-flash"},
    # ... 기타 동일 패턴
}
```

### 모델 해석 경로
```python
_resolve_model(step, project_config):
    1. project_config[step]["model"] (프로젝트별 오버라이드)
    2. PIPELINE_STEPS[step]["default"] (기본값)
    3. fallback: "gemini-pro"
```

**주의**: `step_manifest.py`의 `default_model`은 UI 표시용. 실제 라우팅은 `PIPELINE_STEPS` 결정.

### 핵심 함수
```python
call_structured(step, system_prompt, user_prompt, response_schema, ...)
    # → JSON parsed dict
    # response_format: json_schema (모든 provider 자동 변환)
    # max_tokens: 65536

call_text(step, system_prompt, user_prompt, ...)
    # → raw text string

call_multiturn(step, messages, response_schema, ...)
    # → multi-turn 대화
```

### Gemini Key Pool
- `.env`에서 `GEMINI_API_KEY`, `GEMINI_API_KEY_2`, ... 로드
- 모든 Gemini 모델이 키 풀 공유 (round-robin)
- Opik: LiteLLM callback 자동 추적

## 3. Prompt Loader

**파일**: `backend/app/modules/prompt_loader.py`

```python
load_prompt(module, name, db=None, **format_kwargs)
    # DB 우선 → 파일 fallback
    # 버전 정렬: _version_sort_key (숫자식)
    # format_kwargs로 {변수} 치환

load_schema(module, name, db=None)
    # JSON schema dict 반환
    # structured output 용

_version_sort_key(version_dir_name)
    # "2.202603171200" → (2, "202603171200")
    # 숫자식 정렬 (사전식 금지!)
```

## 4. Config (pydantic-settings)

**파일**: `backend/app/core/config.py`

### 주요 설정
```python
# 파이프라인
shot_variation_count: int = 1        # 샷별 T2I 변형 수
episode_max_shots: int = 0           # 0=무제한, N=비율 분배
shot_selection_enabled: bool = False # LLM 선택 활성화

# 기능 플래그
set_design_enabled: bool = False     # 배경 참조 이미지 생성
fal_ai_enabled: bool = False         # fal.ai 앵글 적용

# 병렬화
max_concurrent_image_gen: int = 15
max_concurrent_variation: int = 10
max_concurrent_entity_detail: int = 10

# LLM
llm_max_output_tokens: int = 65536
llm_timeout_text: int = 600
llm_max_retries: int = 3

# 프로젝트 경로
projects_dir: str = "./projects"     # 체크포인트 저장 위치
```

## 5. Step 등록

### step_manifest.py (SSOT)
- 모든 step의 메타데이터 정의 — **단일 진실 소스**
- order, depends_on, default_model, fan_out, applicability

### steps/__init__.py
- import + STEP_CLASSES dict에 등록
```python
from app.core.steps.scene_consistency_step import SceneConsistencyStep
STEP_CLASSES["scene_consistency"] = SceneConsistencyStep
```

### llm_client.py PIPELINE_STEPS
- 실제 런타임 모델 해석용
- manifest와 정합성 검증 필수 — 신규 step 추가 시 양쪽 모두 등록
- 장기 목표: manifest에서 자동 파생 또는 startup에서 parity 검증 후 서버 기동

## 6. API 엔드포인트

### Step API (주 경로)
**파일**: `backend/app/api/v1/steps.py`

```
GET  /api/v1/projects/{pid}/episodes/{eid}/steps
     → 전체 step 상태 목록 (partial upstream도 can_run=true)

POST /api/v1/projects/{pid}/episodes/{eid}/steps/{sid}
     → resume 실행

POST /api/v1/projects/{pid}/episodes/{eid}/steps/{sid}?mode=force
     → force 재실행 (하위 invalidate)

POST /api/v1/projects/{pid}/episodes/{eid}/steps/run-all
     → 전체 파이프라인 순차 실행

GET  /api/v1/projects/{pid}/episodes/{eid}/steps/{sid}/result
     → 체크포인트 데이터 조회
```

### Legacy Episode API (호환용)
```
POST /api/v1/episodes/{eid}/analyze
POST /api/v1/episodes/{eid}/reanalyze-scenes
```
- 장기적으로 step orchestration wrapper로 축소 예정
- 신규 기능은 반드시 Step API에 먼저 붙인다

### 스냅샷 API
```
POST .../steps/snapshots?label=before-test
     → 전체 체크포인트 스냅샷 저장

GET  .../steps/snapshots
     → 스냅샷 목록

POST .../steps/snapshots/restore?version=YYYYMMDD_HHMMSS
     → 스냅샷 복원
```

## 7. 파일 구조 (핵심)

```
backend/
├── app/
│   ├── api/v1/steps.py          # API 엔드포인트
│   ├── core/
│   │   ├── config.py            # 설정
│   │   ├── step_manifest.py     # 33단계 정의 (SSOT)
│   │   ├── step_runner.py       # 베이스 클래스
│   │   └── steps/
│   │       ├── __init__.py      # 클래스 등록
│   │       ├── detail_steps.py  # SceneDetailStep (핵심)
│   │       ├── scene_consistency_step.py
│   │       ├── shot_staging_step.py
│   │       ├── shot_dependency_t2i_step.py
│   │       ├── image_steps.py   # 이미지 4단계
│   │       └── ...
│   ├── modules/
│   │   ├── llm/llm_client.py   # LLM 라우팅
│   │   ├── prompt_loader.py    # 프롬프트 로딩
│   │   └── pipeline/           # step 로직 모듈
│   │       ├── shot_staging.py
│   │       ├── t2i_review.py
│   │       └── set_design.py
│   ├── services/
│   │   └── image_service.py    # 이미지 생성 서비스 (4000+ lines)
│   └── models/
│       └── project.py          # DB 모델
├── prompts/
│   └── _base/                  # 프롬프트 파일
│       ├── scene_consistency/2.202604141200/
│       ├── scene_extractor_v2/17.202604101200/
│       ├── shot_dependency_t2i/4.202604141200/
│       ├── shot_staging/5.202604101200/
│       └── ...
└── projects/                   # 프로젝트 데이터 + 체크포인트
    └── {project_id}/
        └── checkpoints/episodes/{episode_id}/{step_id}/manifest.json
```

## 8. 개발 사이클

```
설계 → 개발 → 리뷰(codex+claude) → 테스팅 → 수정 → 리뷰 → E2E 테스팅(UI) → 분석 → 수정 → 리뷰 → 디플로이
```

- **코드리뷰 반드시 Codex + Claude 병행** (둘 다 실행 후 수정)
- **테스트 전 반드시 스냅샷 저장** (force 실행은 하위 체크포인트 삭제)
- **서버 재시작 후 파이프라인 실행** (코드 수정 반영)
- **해당 단계만 force 실행** (전체 run-all 금지)
