# The Road Scene 내부용 기획서

작성일: 2026-03-13  
문서 버전: v0.1

## 1. 프로젝트 목표

영화/드라마 시나리오를 분석해 중요 씬 단위로 이미지를 생성하고, 해당 이미지를 정확한 씬 위치에 배치해 `PDF` 또는 `EPUB`로 출력하는 내부용 제작 시스템을 구축한다.

이 시스템은 단순 이미지 생성기가 아니라 다음을 만족해야 한다.

- 인물, 배경, 소품, 화풍의 일관성을 에피소드 단위와 시리즈 단위로 유지
- 감독 관점의 미장센 판단을 돕는 샷/카메라/조명/색감 추론
- 사람 편집자가 프롬프트와 비주얼 의도를 중간에 수정 가능한 Human-in-the-Loop 구조
- `LLM`, `T2I`, `I2I`, 후처리 모듈을 분리하여 교체 가능한 구조
- 잦은 모델/프롬프트/DB 변경을 견디는 자체 버전 관리와 마이그레이션 체계
- `LiteLLM`, `Opik` 기반의 추적, 평가, 비용 관리를 기본 제공
- 내부용 규모에 맞게 과도한 마이크로서비스 없이 쉽게 배포 가능
- 프로젝트 전체를 손쉽게 `export / import` 가능한 구조

## 2. 권장 제품 방향

### 핵심 방향

- 아키텍처는 `모듈형 모놀리스 + 비동기 워커`를 기본으로 한다.
- 저장 단위는 `프로젝트 폴더 + 개별 SQLite + asset 디렉터리`를 기본으로 한다.
- 시리즈/에피소드/씬/샷을 명시적 계층으로 두고, 일관성 데이터는 공용 `Visual Bible`로 관리한다.
- 생성은 한 번의 프롬프트 호출이 아니라 `분석 -> 계획 -> 초안 생성 -> 리파인 -> 검수 -> 배치`의 다단계 파이프라인으로 구성한다.
- 버전 관리는 Git에 기대지 않고 시스템 내부에서 `불변 revision + alias + migration registry` 방식으로 운영한다.

### 왜 이 방향이 맞는가

- 내부용이고 대규모 외부 트래픽이 아니므로 복잡한 분산 시스템보다 단순한 운영성이 중요하다.
- 반면 모델, 프롬프트, 스타일, 추적, 비용 구조는 자주 바뀌므로 모듈 경계와 버전 체계는 강해야 한다.
- 드라마 시리즈는 같은 인물/공간/소품이 반복 등장하므로, 단건 생성보다 `지속되는 기억 구조`가 더 중요하다.

## 3. 핵심 사용자 시나리오

### 시나리오 A. 새 프로젝트 시작

1. 사용자가 시나리오 파일을 업로드한다.
2. 시스템이 씬 단위로 자동 분할한다.
3. 중요 씬 후보와 추천 샷 수를 제안한다.
4. 사용자가 중요 씬을 확정한다.
5. 시리즈용 `Character / Location / Prop / Style Bible`을 생성 또는 수정한다.

### 시나리오 B. 씬별 이미지 생성

1. 시스템이 씬 맥락과 기존 바이블을 바탕으로 샷 플랜을 만든다.
2. LLM이 감독적 해석이 포함된 프롬프트 초안을 작성한다.
3. 사람이 카메라 앵글, 조명, 색감, 렌즈감, 연출 의도를 수정한다.
4. T2I 1차 생성 후, I2I/인페인팅/업스케일/스타일 고정 단계를 반복한다.
5. 시스템이 일관성 점수와 품질 점수를 제시한다.
6. 사용자가 최종 이미지를 채택한다.

### 시나리오 C. 문서 출력

1. 씬 본문과 이미지가 앵커 기준으로 자동 배치된다.
2. 사용자가 씬당 1장 또는 다장을 선택한다.
3. PDF/EPUB 미리보기를 확인하고 export 한다.

### 역할 모델

이 시스템은 내부용이지만 역할 경계는 분명해야 한다. 최소 역할은 `admin` 과 `creator` 두 개로 둔다.

| 역할 | 핵심 책임 | 허용 범위 |
|---|---|---|
| `admin` | 시스템 운영, 모델/키/전역 프롬프트/LoRA 자산 관리 | 서비스 전체 설정과 모든 프로젝트 관리 |
| `creator` | 프로젝트 생성, 시나리오 업로드, 이미지 생성/수정, PDF/EPUB 산출 | 자신이 접근 가능한 프로젝트 내부 작업 |

#### admin 권한

- LLM provider key, image provider key, webhook secret 관리
- LiteLLM 모델 라우팅, 기본 모델, fallback 정책 관리
- 전역 중요 프롬프트, system prompt, prompt fragment 기본값 관리
- LoRA Library 의 학습/가져오기/배포 정책 관리
- 전체 프로젝트 목록 조회, 프로젝트 생성/보관/비활성화
- 전역 cost dashboard, Opik 연동 설정, 배포 설정 관리

#### creator 권한

- 프로젝트 생성과 프로젝트 내부 설정 관리
- 시나리오 업로드, 씬 분석, 샷 플래닝, 이미지 생성 실행
- T2I/I2I 프롬프트 수동 수정
- 카메라 앵글, 렌즈, 조도, 빛 방향, LoRA stack 선택 및 조정
- 중간 승인, 재생성, PDF/EPUB export 수행

#### 권한 경계 원칙

- `creator` 는 전역 API key, LiteLLM routing, 전역 system prompt 를 변경할 수 없다
- `creator` 는 프로젝트 수준의 override 만 허용한다
- `admin` 은 전역 설정과 프로젝트 둘 다 접근 가능하다
- 프로젝트 이식성을 위해 프로젝트 데이터와 전역 설정/비밀값은 저장소를 분리한다

#### 저장 위치 원칙

- 프로젝트 관련 데이터:
  - 프로젝트 SQLite
- 전역 서비스 설정과 비밀값:
  - 서비스 레벨 catalog DB 또는 secret store

즉, `project.sqlite` 안에는 시나리오, 씬, 오브젝트, 생성 결과, 프로젝트별 override 만 넣고, API 키나 전역 모델 정책은 넣지 않는다.

#### 권장 서비스 레벨 테이블

전역 권한과 설정은 별도 catalog DB 에 둔다.

- `user_account`
  - 사용자 계정 기본 정보
- `user_role`
  - `admin`, `creator`
- `project_registry`
  - 전체 프로젝트 목록과 상태
- `project_member`
  - 프로젝트별 접근 권한
- `system_setting`
  - 전역 모델 기본값, prompt policy, 운영 플래그
- `provider_secret`
  - LLM/T2I/I2I provider key, webhook secret
- `lora_registry`
  - 공용 LoRA 자산 메타와 hosted URL

즉, `creator` 가 프로젝트를 생성하고 관리하더라도, 전역 키와 모델 정책은 이 service catalog 계층에서만 제어한다.

## 4. 권장 시스템 구조

### 아키텍처 원칙

- API, UI, 오케스트레이션은 하나의 애플리케이션으로 유지
- 무거운 작업만 워커로 분리
- 외부 모델 연동은 모두 provider adapter 뒤로 숨김
- 프로젝트 데이터는 자체적으로 이동 가능한 패키지 단위로 저장
- 전역 설정과 비밀값은 프로젝트 데이터와 분리
- 로그/트레이스/비용/평가는 생성 파이프라인과 같은 run id 로 묶음

```mermaid
flowchart LR
    AUSER[Admin User] --> UI[Web UI]
    CUSER[Creator User] --> UI
    UI --> API[Application API]
    API --> ORCH[Workflow Orchestrator]

    ORCH --> SI[Script Intelligence]
    ORCH --> CE[Continuity Engine]
    ORCH --> PC[Prompt Composer]
    ORCH --> GE[Generation Engine]
    ORCH --> LE[Layout Export Engine]
    ORCH --> RM[Revision Migration Manager]
    ORCH --> OBS[Observability Cost]

    GE --> LLM[LiteLLM Gateway]
    GE --> T2I[T2I Provider Adapters]
    GE --> I2I[I2I Refinement Adapters]

    LLM --> OPIK[Opik Server]
    OBS --> OPIK

    API --> DB[(Project SQLite)]
    API --> CDB[(Service Catalog DB)]
    API --> FS[Project Asset Store]
    RM --> DB
    RM --> FS
    LE --> FS
```

## 5. 추천 모듈 구성

| 모듈 | 역할 | 교체 가능 포인트 |
|---|---|---|
| Script Intelligence | 시나리오 파싱, 씬 분할, 중요도 판정, 인물/배경/소품 추출 | LLM 모델, 규칙 기반 파서 |
| Scene Still Extractor | 에피소드 전체 full text 기준으로 최대한 많은 정지화면용 씬 후보를 추출 | GPT/Gemini 모델, heading parser, schema 버전 |
| Cross-Episode Analyzer | 이전 에피소드 스크립트와 승인 결과를 재분석해 재등장 후보를 찾음 | 검색 인덱스, LLM, 룰 엔진 |
| Identity Continuity Resolver | 인물의 동일 인물 판정, 나이 변화, 변장, 부상, 의상/헤어 상태를 variant 단위로 관리 | 얼굴/임베딩 모델, 규칙 엔진 |
| World Continuity Resolver | 배경, 장소, 날씨, 시간대, 세트 변형, 소품 상태 변화를 관리 | 장면 그래프, 룰셋 |
| Reference Retrieval Engine | 이전 승인 이미지와 variant 기준 이미지를 검색, 랭킹, bundle 화 | 벡터 검색, reranker, 해시 룰 |
| Cinematography Planner | 장면 의도 분석 후 구도, 카메라 높이, 렌즈, 빛 방향, 조명 패턴 후보를 산출 | 레시피 라이브러리, LLM |
| Look Recipe Library | composition / lens / lighting / palette / blocking 규칙을 버전된 자산으로 관리 | 레시피 버전, 템플릿 |
| Prompt Composer | 샷 스펙과 레시피를 결합해 프롬프트 템플릿 렌더링 | 템플릿 버전, LLM |
| Camera Angle Edit Adapter | 승인된 기준 이미지를 바탕으로 카메라 각도와 거리 변형을 I2I 로 수행 | `fal-ai/qwen-image-edit-2511-multiple-angles` 등 |
| Style LoRA Trainer | 스타일용 LoRA 를 paired image dataset 으로 학습 | `fal-ai/flux-kontext-trainer` |
| Style LoRA Apply Adapter | 학습된 LoRA 를 최종 구조 이미지에 후행 적용 | `fal-ai/flux-kontext-lora` |
| LoRA Registry Manager | 내부 학습 LoRA 와 외부 Flux-Kontext LoRA URL/파일을 통합 관리 | 자체 DB, object storage |
| LoRA Hosting Service | 학습 완료 자산과 업로드된 외부 LoRA 를 서비스 URL 로 보관 | 자체 URL, S3/MinIO, CDN |
| Generation Engine | T2I/I2I 다단계 실행, 시드/레퍼런스/리파인 관리 | 이미지 모델, 후처리 체인 |
| Review QA | 품질 점수, 일관성 검증, 인간 승인 워크플로 | 평가 모델, QA 규칙 |
| Layout Export Engine | 이미지와 시나리오 본문 매핑, PDF/EPUB 출력 | 템플릿, 렌더러 |
| Revision Migration Manager | DB/프롬프트/설정/결과물 버전 및 마이그레이션 | 마이그레이터 플러그인 |
| Observability Cost | Opik trace, 비용 합산, run lineage 저장 | 추적 백엔드 |
| Provider Registry | LiteLLM, T2I, I2I, 업스케일러 어댑터 등록 | 모델/벤더 추가 |

## 6. 데이터 모델 핵심

프로젝트는 시리즈 제작까지 고려해 다음 계층을 기본으로 한다.

- `Project`
- `Season` 또는 `SeriesGroup`
- `Episode`
- `Scene`
- `ShotPlan`
- `ImageCandidate`
- `ExportJob`

일관성 유지용 공용 개체는 별도로 둔다.

- `CharacterBible`
- `LocationBible`
- `PropBible`
- `StyleProfile`
- `ColorScript`
- `ShotGrammar`
- `CharacterVariant`
- `LocationVariant`
- `PropVariant`
- `SceneStateSnapshot`
- `SceneStillCandidate`
- `SceneStillVisibleEntity`
- `RelationFact`
- `RelationParticipant`
- `SceneRelationSnapshot`
- `ReappearanceCandidate`
- `ReferenceBundle`
- `AngleEditRecipe`
- `StyleTransformProfile`
- `StyleTrainingDataset`
- `StyleTrainingPair`
- `StyleTrainingJob`
- `StyleLoraAsset`
- `LoraRegistryItem`
- `LoraSource`
- `LoraStackPreset`
- `HostedAsset`
- `ImageDerivative`
- `ImageLineageEdge`
- `CompositionRecipe`
- `LensRecipe`
- `LightingRecipe`
- `BlockingRecipe`

```mermaid
erDiagram
    PROJECT ||--o{ EPISODE : contains
    EPISODE ||--o{ SCENE : contains
    SCENE ||--o{ SHOT_PLAN : contains
    SHOT_PLAN ||--o{ IMAGE_CANDIDATE : generates
    PROJECT ||--o{ CHARACTER_BIBLE : defines
    PROJECT ||--o{ LOCATION_BIBLE : defines
    PROJECT ||--o{ PROP_BIBLE : defines
    PROJECT ||--o{ STYLE_PROFILE : defines
    SCENE }o--o{ CHARACTER_BIBLE : uses
    SCENE }o--o{ LOCATION_BIBLE : uses
    SCENE }o--o{ PROP_BIBLE : uses
    SHOT_PLAN }o--|| STYLE_PROFILE : references
    IMAGE_CANDIDATE }o--|| REVISION : created_from
    PROJECT ||--o{ EXPORT_JOB : outputs
```

### 문서 배치용 엔티티

씬 텍스트와 이미지를 정확히 연결하려면 export 직전에 감으로 배치하면 안 되고, 배치용 엔티티를 별도로 둬야 한다.

| 엔티티 | 역할 |
|---|---|
| SceneAnchor | 원문 내 시작/끝 오프셋, 씬 헤딩 hash, 씬 순서를 고정 |
| RenderBlock | 텍스트 블록, 이미지 블록, 캡션 블록을 순서대로 구성 |
| ImagePlacementRule | 씬당 1장 또는 N장, 본문 앞/중간/뒤, 전체폭/그리드 여부 지정 |
| ExportPreset | PDF/EPUB 레이아웃, 여백, 이미지 캡션, 표지 규칙 관리 |

이 구조를 두면 다음이 가능해진다.

- 같은 씬을 PDF와 EPUB에 서로 다른 스타일로 출력
- 씬 수정 후에도 anchor 기준으로 이미지 위치 재계산
- 한 씬에 1장 또는 여러 장 이미지를 재현 가능하게 배치

### 변형 관리 엔티티

나이 변화, 변장, 부상, 시간 경과, 소품 손상은 `같은 개체의 다른 상태`이지 별개 개체가 아니다. 따라서 canon 과 variant 를 분리해야 한다.

| 엔티티 | 역할 |
|---|---|
| CharacterCanon | 인물의 변하지 않는 핵심 정체성 |
| CharacterVariant | 연령대, 변장, 부상, 헤어, 메이크업, 의상 상태 |
| LocationCanon | 장소의 기본 구조와 핵심 식별 포인트 |
| LocationVariant | 낮/밤, 계절, 날씨, 파손, 장식 변경, 군중 밀도 |
| PropCanon | 소품의 기본 형태와 상징성 |
| PropVariant | 새것/낡음/파손/오염/열림/닫힘/소지자 상태 |
| SceneStateSnapshot | 특정 씬 시점에서 각 variant 가 실제로 어떤 상태인지 기록 |
| ReappearanceCandidate | 이전 에피소드의 동일 개체 추정 결과와 confidence |
| ReferenceBundle | 이번 씬 생성에 실제 투입될 기준/승인 이미지 묶음 |

이 모델을 쓰면 "20대 청년 상태", "40대 중년 상태", "수염을 붙인 변장 상태"를 같은 인물 트리 아래 관리할 수 있다.

### 연속 에피소드 참조 엔티티

드라마 시리즈에서는 새 시나리오를 단독 분석하면 안 되고, 이전 에피소드와 연결해서 분석해야 한다.

| 엔티티 | 역할 |
|---|---|
| EpisodeEntityIndex | 과거 에피소드에서 추출된 인물/장소/소품 색인 |
| ReappearanceLink | 이번 에피소드 개체와 과거 개체의 연결 관계 |
| ApprovedImageAnchor | 과거 승인 이미지가 어떤 씬/샷/variant 를 대표하는지 표시 |
| RetrievalPolicy | 어떤 경우에 과거 이미지를 강제 참조할지 규칙화 |

이 구조가 있어야 "시즌 1 3화의 병원 복도"와 "시즌 1 9화의 같은 병원 복도"를 자동으로 연결할 수 있다.

### I2I 파생 엔티티

카메라 각도 변형과 화풍 변형은 둘 다 `기존 이미지 -> 파생 이미지` 구조이므로 lineage 를 분리 저장해야 한다.

| 엔티티 | 역할 |
|---|---|
| AngleEditRecipe | azimuth, elevation, zoom, 추가 지시문 등 카메라 변형 규칙 |
| StyleTransformProfile | 시리즈 공통 화풍 정책과 적용 강도, 기본 scale |
| StyleTrainingDataset | LoRA 학습용 데이터셋 메타 |
| StyleTrainingPair | start image / end image / caption 쌍 |
| StyleTrainingJob | 학습 파라미터, steps, learning rate, 상태 |
| StyleLoraAsset | 학습 완료 후 생성된 LoRA 파일과 config |
| LoraRegistryItem | 내부 학습 또는 외부 업로드 LoRA 의 공통 메타와 서비스 URL |
| LoraSource | `trained`, `uploaded`, `external_url`, `mirrored` 등 출처 구분 |
| LoraStackPreset | 여러 LoRA 를 순서와 scale 로 묶은 재사용 preset |
| HostedAsset | 자체 배포 환경에서 서빙되는 파일 URL 와 저장 위치 |
| ImageDerivative | 어떤 원본 이미지에서 어떤 I2I 단계로 파생되었는지 기록 |
| ImageLineageEdge | base -> angle edit -> repair -> style lora stack apply 같은 체인 연결 |

이 구조가 있어야 "같은 원본에서 3개 앵글 파생 후, 최종 구조 이미지에 여러 style LoRA 를 순서대로 적용"을 재현할 수 있다.

### 권장 저장 구조

각 프로젝트는 독립적인 폴더 단위로 보관한다.

```text
project-root/
  manifest.json
  project.sqlite
  assets/
    source-scripts/
    references/
    generated/
    refined/
    exports/
  revisions/
    prompts/
    configs/
    migrations/
  cache/
```

### 공용 LoRA 저장소

Flux-Kontext LoRA 는 프로젝트 내부 파일만으로 두지 않고 서비스 차원의 공용 저장소를 별도로 둔다.

```text
service-assets/
  loras/
    uploaded/
    trained/
    mirrored/
  manifests/
    lora-registry.sqlite
```

원칙:

- 프로젝트는 `LoraRegistryItem` id 만 참조
- 실제 LoRA 파일은 공용 저장소 또는 object storage 에 둠
- 배포 환경에서 접근 가능한 `hosted_url` 을 항상 확보
- export 시에는 LoRA 바이너리를 통째로 복사하지 않고 참조 방식 또는 선택적 bundle 포함 정책을 사용

### 왜 개별 SQLite가 적합한가

- 내부용 운영과 프로젝트 이동성이 좋다.
- 백업, 복제, zip export 가 단순하다.
- 에피소드 단위가 아니라 프로젝트 단위로 통째로 옮기기 쉽다.
- 추후 PostgreSQL로 옮기더라도 repository 계층만 교체하면 된다.

### SQLite 스키마 초안: 공통 Entity 모델

인물, 배경, 중요 물체를 서로 다른 구조로 관리하지 않고, `entity_canon -> entity_variant -> scene_entity_snapshot -> entity_reference_image` 로 통일하는 것이 좋다.  
즉, `character`, `location`, `prop` 은 `entity_type` 으로만 구분하고 변화 관리 방식은 동일하게 가져간다.

#### 핵심 원칙

- `entity_canon` 은 변하지 않는 핵심 정체성
- `entity_variant` 는 재사용 가능한 변화 상태
- `scene_entity_snapshot` 은 특정 씬 시점의 실제 상태
- `entity_reference_image` 는 canon/variant/snapshot 기준 대표 이미지 연결
- 이미지 파일 자체는 SQLite에 넣지 않고 `asset` 테이블과 외부 저장소를 사용

```mermaid
erDiagram
    ENTITY_CANON ||--o{ ENTITY_ALIAS : has
    ENTITY_CANON ||--o{ ENTITY_VARIANT : has
    ENTITY_CANON ||--o{ SCENE_ENTITY_SNAPSHOT : appears_as
    ENTITY_VARIANT ||--o{ ENTITY_VARIANT : derives
    ENTITY_VARIANT ||--o{ SCENE_ENTITY_SNAPSHOT : active_in
    ENTITY_CANON ||--o{ ENTITY_CHANGE_EVENT : changes
    ENTITY_VARIANT ||--o{ ENTITY_CHANGE_EVENT : from_variant
    ENTITY_VARIANT ||--o{ ENTITY_CHANGE_EVENT : to_variant
    SCENE_ENTITY_SNAPSHOT ||--o{ ENTITY_REFERENCE_IMAGE : anchors
    ENTITY_CANON ||--o{ ENTITY_REFERENCE_IMAGE : represented_by
    ENTITY_VARIANT ||--o{ ENTITY_REFERENCE_IMAGE : represented_by
    ASSET ||--o{ ENTITY_REFERENCE_IMAGE : stores
    EPISODE ||--o{ SCENE_ENTITY_SNAPSHOT : contains
    SCENE ||--o{ SCENE_ENTITY_SNAPSHOT : contains
    EPISODE ||--o{ ENTITY_CHANGE_EVENT : contains
    SCENE ||--o{ ENTITY_CHANGE_EVENT : contains
```

#### DDL 초안

```sql
PRAGMA foreign_keys = ON;

CREATE TABLE entity_canon (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL,
    entity_type TEXT NOT NULL CHECK (entity_type IN ('character', 'location', 'prop')),
    name TEXT NOT NULL,
    description TEXT,
    stable_traits_json TEXT NOT NULL DEFAULT '{}',
    continuity_notes TEXT,
    search_text TEXT NOT NULL DEFAULT '',
    status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deprecated')),
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE entity_alias (
    id TEXT PRIMARY KEY,
    canon_id TEXT NOT NULL REFERENCES entity_canon(id) ON DELETE CASCADE,
    alias TEXT NOT NULL,
    alias_type TEXT NOT NULL DEFAULT 'name',
    created_at TEXT NOT NULL
);

CREATE UNIQUE INDEX idx_entity_alias_unique
    ON entity_alias(canon_id, alias);

CREATE TABLE entity_variant (
    id TEXT PRIMARY KEY,
    canon_id TEXT NOT NULL REFERENCES entity_canon(id) ON DELETE CASCADE,
    parent_variant_id TEXT REFERENCES entity_variant(id) ON DELETE SET NULL,
    variant_type TEXT NOT NULL,
    name TEXT NOT NULL,
    delta_json TEXT NOT NULL DEFAULT '{}',
    rules_json TEXT NOT NULL DEFAULT '{}',
    reusable INTEGER NOT NULL DEFAULT 1 CHECK (reusable IN (0, 1)),
    status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'deprecated')),
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE INDEX idx_entity_variant_canon
    ON entity_variant(canon_id);

CREATE INDEX idx_entity_variant_parent
    ON entity_variant(parent_variant_id);

CREATE TABLE entity_change_event (
    id TEXT PRIMARY KEY,
    canon_id TEXT NOT NULL REFERENCES entity_canon(id) ON DELETE CASCADE,
    from_variant_id TEXT REFERENCES entity_variant(id) ON DELETE SET NULL,
    to_variant_id TEXT REFERENCES entity_variant(id) ON DELETE SET NULL,
    episode_id TEXT NOT NULL REFERENCES episode(id) ON DELETE CASCADE,
    scene_id TEXT NOT NULL REFERENCES scene(id) ON DELETE CASCADE,
    trigger_type TEXT NOT NULL,
    notes TEXT,
    created_at TEXT NOT NULL
);

CREATE INDEX idx_entity_change_event_canon_scene
    ON entity_change_event(canon_id, episode_id, scene_id);

CREATE TABLE scene_entity_snapshot (
    id TEXT PRIMARY KEY,
    episode_id TEXT NOT NULL REFERENCES episode(id) ON DELETE CASCADE,
    scene_id TEXT NOT NULL REFERENCES scene(id) ON DELETE CASCADE,
    canon_id TEXT NOT NULL REFERENCES entity_canon(id) ON DELETE CASCADE,
    variant_id TEXT REFERENCES entity_variant(id) ON DELETE SET NULL,
    state_json TEXT NOT NULL DEFAULT '{}',
    continuity_notes TEXT,
    confidence REAL NOT NULL DEFAULT 1.0,
    approved INTEGER NOT NULL DEFAULT 0 CHECK (approved IN (0, 1)),
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE INDEX idx_scene_entity_snapshot_scene
    ON scene_entity_snapshot(scene_id, canon_id);

CREATE INDEX idx_scene_entity_snapshot_episode
    ON scene_entity_snapshot(episode_id, canon_id);

CREATE TABLE asset (
    id TEXT PRIMARY KEY,
    project_id TEXT,
    storage_key TEXT NOT NULL,
    hosted_url TEXT,
    checksum TEXT,
    mime_type TEXT,
    width INTEGER,
    height INTEGER,
    asset_kind TEXT NOT NULL CHECK (
        asset_kind IN (
            'source',
            'generated',
            'reference',
            'derived',
            'lora',
            'thumbnail'
        )
    ),
    metadata_json TEXT NOT NULL DEFAULT '{}',
    created_at TEXT NOT NULL
);

CREATE UNIQUE INDEX idx_asset_storage_key
    ON asset(storage_key);

CREATE TABLE entity_reference_image (
    id TEXT PRIMARY KEY,
    canon_id TEXT NOT NULL REFERENCES entity_canon(id) ON DELETE CASCADE,
    variant_id TEXT REFERENCES entity_variant(id) ON DELETE SET NULL,
    snapshot_id TEXT REFERENCES scene_entity_snapshot(id) ON DELETE SET NULL,
    asset_id TEXT NOT NULL REFERENCES asset(id) ON DELETE CASCADE,
    role TEXT NOT NULL CHECK (
        role IN (
            'identity_anchor',
            'variant_anchor',
            'scene_anchor',
            'detail_anchor',
            'establishing_anchor'
        )
    ),
    is_primary INTEGER NOT NULL DEFAULT 0 CHECK (is_primary IN (0, 1)),
    priority INTEGER NOT NULL DEFAULT 100,
    quality_score REAL,
    approved INTEGER NOT NULL DEFAULT 1 CHECK (approved IN (0, 1)),
    notes TEXT,
    created_at TEXT NOT NULL
);

CREATE INDEX idx_entity_reference_image_canon
    ON entity_reference_image(canon_id, role, is_primary, priority);

CREATE INDEX idx_entity_reference_image_variant
    ON entity_reference_image(variant_id, role, is_primary, priority);

CREATE INDEX idx_entity_reference_image_snapshot
    ON entity_reference_image(snapshot_id, role, is_primary, priority);
```

#### 테이블 사용 방식

- 인물 대표 이미지:
  - `entity_type = 'character'`
  - canon 기준 `identity_anchor`
  - variant 기준 `variant_anchor`
- 배경 대표 이미지:
  - `entity_type = 'location'`
  - 공간 전체는 `establishing_anchor`
  - 디테일 컷은 `detail_anchor`
- 중요 물체 대표 이미지:
  - `entity_type = 'prop'`
  - 전체형은 `identity_anchor`
  - 손상/열림/오염 상태는 `variant_anchor`

### SQLite 스키마 초안: 관계 그래프 레이어

오브젝트 자체만 저장하면 continuity 가 절반만 해결된다.  
실제 시리즈 제작에서는 `누가 누구의 가족인지`, `누가 무엇을 소유하는지`, `누가 어디에 거주하거나 숨어 있는지`, `어떤 물체가 어떤 장소에 보관되는지`, `누가 누구를 조종하거나 추적하는지` 같은 관계가 반복적으로 재활용된다.

그래서 관계는 엔티티의 부속 텍스트로 묻어두지 말고, 별도 SQLite 레이어로 저장하는 편이 좋다.

#### 관계 레이어가 필요한 이유

- 인물, 장소, 소품 간 연결 구조를 시리즈 전반에서 재사용할 수 있다
- 동일 이름이 아닌데도 `관계 패턴` 덕분에 재등장 후보를 더 잘 찾을 수 있다
- 프롬프트 구성 시 `누가 어디서 무엇을 들고 있는가`를 구조적으로 주입할 수 있다
- 장면별 상태 변화와 관계 변화가 따로 추적된다
- 나중에 그래프 질의와 추천 reference bundle 구성에 쓰기 좋다

#### 권장 개념 분리

- `relation_fact`
  - 비교적 안정적인 관계 정의
  - 예: `동녘 - grandson_of - 서창`
  - 예: `캣츠아이 - sought_by - DR.NEX`
- `relation_participant`
  - 관계에 참여하는 엔티티와 역할
  - 예: `subject`, `target`, `owner`, `owned_item`, `container`, `resident`
- `scene_relation_snapshot`
  - 특정 씬에서 그 관계가 실제로 활성/변형된 상태
  - 예: 평소에는 `owned_by` 였지만 이번 씬에서는 `stolen_by` 로 바뀜

```mermaid
erDiagram
    ENTITY_CANON ||--o{ RELATION_PARTICIPANT : participates
    RELATION_FACT ||--o{ RELATION_PARTICIPANT : has
    RELATION_FACT ||--o{ SCENE_RELATION_SNAPSHOT : appears_in
    EPISODE ||--o{ SCENE_RELATION_SNAPSHOT : contains
    SCENE ||--o{ SCENE_RELATION_SNAPSHOT : contains
    SCENE_ENTITY_SNAPSHOT ||--o{ SCENE_RELATION_SNAPSHOT : grounds
```

#### DDL 초안

```sql
CREATE TABLE relation_fact (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL,
    relation_family TEXT NOT NULL CHECK (
        relation_family IN (
            'identity',
            'kinship',
            'social',
            'conflict',
            'collaboration',
            'possession',
            'containment',
            'location',
            'membership',
            'control',
            'goal',
            'event',
            'state',
            'transformation',
            'other'
        )
    ),
    relation_type TEXT NOT NULL,
    directionality TEXT NOT NULL CHECK (directionality IN ('directed', 'bidirectional', 'undirected')),
    temporal_scope TEXT NOT NULL CHECK (
        temporal_scope IN ('scene', 'episode', 'series', 'backstory', 'unknown')
    ),
    continuity_priority TEXT NOT NULL CHECK (
        continuity_priority IN ('critical', 'high', 'medium', 'low')
    ),
    continuity_reason TEXT NOT NULL,
    signature_hash TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE UNIQUE INDEX idx_relation_fact_signature
    ON relation_fact(project_id, signature_hash);

CREATE TABLE relation_participant (
    id TEXT PRIMARY KEY,
    relation_id TEXT NOT NULL REFERENCES relation_fact(id) ON DELETE CASCADE,
    canon_id TEXT NOT NULL REFERENCES entity_canon(id) ON DELETE CASCADE,
    participant_role TEXT NOT NULL,
    participant_order INTEGER NOT NULL DEFAULT 1,
    created_at TEXT NOT NULL
);

CREATE INDEX idx_relation_participant_relation
    ON relation_participant(relation_id, participant_order);

CREATE INDEX idx_relation_participant_canon
    ON relation_participant(canon_id, participant_role);

CREATE TABLE scene_relation_snapshot (
    id TEXT PRIMARY KEY,
    relation_id TEXT NOT NULL REFERENCES relation_fact(id) ON DELETE CASCADE,
    episode_id TEXT NOT NULL REFERENCES episode(id) ON DELETE CASCADE,
    scene_id TEXT NOT NULL REFERENCES scene(id) ON DELETE CASCADE,
    state_json TEXT NOT NULL DEFAULT '{}',
    evidence_json TEXT NOT NULL DEFAULT '[]',
    confidence REAL NOT NULL DEFAULT 1.0,
    approved INTEGER NOT NULL DEFAULT 0 CHECK (approved IN (0, 1)),
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE INDEX idx_scene_relation_snapshot_scene
    ON scene_relation_snapshot(scene_id, relation_id);

CREATE INDEX idx_scene_relation_snapshot_episode
    ON scene_relation_snapshot(episode_id, relation_id);
```

#### 관계 예시

- 인물-인물
  - `kinship / grandson_of`
  - `social / mentor_of`
  - `conflict / hunts`
- 인물-장소
  - `location / lives_in`
  - `location / hides_in`
  - `membership / works_at`
- 인물-물체
  - `possession / carries`
  - `control / activates`
  - `goal / seeks`
- 장소-물체
  - `containment / stores`
  - `state / houses`
- 인물-인물-물체-장소
  - 하나의 `relation_fact` 에 participant 4개를 넣어
  - 예: `동녘(subject) / 캣츠아이(target_item) / 화장터(storage_place) / 서창(owner)`
  - 처럼 n-ary 관계도 표현 가능

#### 권장 조회 패턴

다음 에피소드 분석 때는 아래 순서로 기억을 주입한다.

1. 새 씬에서 추출한 후보를 `entity_canon` 과 매칭
2. 최근 `scene_entity_snapshot` 으로 현재 상태 후보 확인
3. 맞는 `entity_variant` 와 `entity_change_event` 로 변화 이력 확인
4. `relation_fact` 와 `scene_relation_snapshot` 으로 현재 관계 그래프를 불러온다
5. `entity_reference_image` 에서 `is_primary = 1` 인 대표 이미지를 우선 선택
6. canon + variant + relation graph + recent scene anchor 조합으로 reference bundle 생성

이렇게 하면 인물뿐 아니라 배경과 중요 물체, 그리고 그 사이의 관계까지 동일한 방식으로 다음 에피소드 분석과 이미지 생성에 재사용할 수 있다.

## 7. 일관성 유지 전략

시리즈 일관성은 단일 프롬프트나 단일 reference 이미지로 해결되지 않는다. `정체성`, `상태`, `변형`, `씬 시점`, `촬영 해석`을 분리해야 한다.

### 0. Cross-Episode Multi-Phase Analysis

새 에피소드를 분석할 때는 현재 스크립트만 읽지 말고, 이전 에피소드의 스크립트와 승인 이미지까지 여러 phase 로 다시 읽어야 한다.

#### Phase A. 과거 에피소드 인덱싱

- 이전 에피소드들의 시나리오에서 인물, 장소, 소품, 사건 키워드, 관계를 추출
- 승인된 이미지에서 얼굴, 의상, 장소 구조, 소품 특징, 색감 정보를 요약
- 이 결과를 `EpisodeEntityIndex` 와 `ApprovedImageAnchor` 로 저장

#### Phase B. 재등장 후보 탐지

- 새 에피소드의 추출 결과와 과거 인덱스를 매칭
- 이름 동일, 별칭 동일, 설명 유사, 관계 유사, 장소 구조 유사, 소품 특성 유사 여부를 함께 본다
- 결과는 `ReappearanceCandidate` 로 저장하고 confidence 와 근거를 남긴다

#### Phase C. 동일 개체 판정

- 사람이 후보를 승인하거나 수정
- 승인되면 `ReappearanceLink` 를 생성해 canon 또는 variant 에 연결
- 애매한 경우는 `same canon, new variant` 와 `new canon` 중 하나로 정리

#### Phase D. Reference Bundle 구성

- 연결된 과거 개체의 승인 이미지 중 대표성이 높은 이미지를 선택
- canon 이미지, variant 이미지, 최근 승인 이미지, 유사 구도 이미지를 묶어 `ReferenceBundle` 생성
- 이 bundle 을 이후 shot generation 의 기본 입력으로 사용

#### Phase E. 생성 후 역반영

- 이번 에피소드에서 새로 승인된 이미지는 다시 인덱스에 반영
- 이후 다음 에피소드에서 또 reference 로 재활용

```mermaid
flowchart LR
    PastScripts[Past Episode Scripts] --> Index[Episode Entity Index]
    PastImages[Approved Images] --> Index
    NewScript[New Episode Script] --> Detect[Reappearance Detection]
    Index --> Detect
    Detect --> Review[Human Link Review]
    Review --> Link[Canon or Variant Link]
    Link --> Bundle[Reference Bundle Build]
    Bundle --> Gen[Generation]
    Gen --> Approved[Approved Images]
    Approved --> Index
```

### 1. Canon 과 Variant 분리

- 인물/장소/소품은 먼저 `Canon` 으로 등록한다.
- 나이 변화, 변장, 부상, 의상 교체, 파손, 날씨 변화는 모두 `Variant` 로 파생한다.
- Variant 는 부모-자식 관계와 적용 조건을 가진다.

예시:

- CharacterCanon: 형사 김도윤
- CharacterVariant A: 20대 초반 회상 장면
- CharacterVariant B: 40대 현재 시점
- CharacterVariant C: 중년 + 수염 분장 + 검은 모자 변장

### 2. 상태 전이 규칙

- Variant 는 임의 선택이 아니라 `scene precondition` 과 `transition rule` 에 의해 활성화된다.
- 예를 들어 회상 전환, 시간 점프, 변장 준비 장면, 전투 후 부상, 비를 맞은 직후 같은 조건이 명시되어야 한다.
- 불가능한 조합은 hard constraint 로 막는다.

예시 hard constraint:

- 고등학생 버전과 중년 수염 버전 동시 활성화 금지
- 파손된 소품과 "새 제품" 상태 동시 사용 금지
- 낮 장면인데 야간 네온 조명 variant 사용 금지

### 3. Scene State Snapshot

- 각 씬은 "현재 무엇이 보이는가"를 snapshot 으로 가진다.
- snapshot 은 인물의 나이/변장/의상/감정, 장소의 시간대/날씨/파손 상태, 소품의 상태를 함께 묶는다.
- 연속 에피소드에서는 이전 씬 snapshot 을 상속해 다음 씬 기본값으로 쓴다.

### 4. Reference Bundle 계층

- canon 기준 이미지
- variant 기준 이미지
- scene-specific 승인 이미지
- shot-specific pose/layout reference

이 네 층을 분리해야 "같은 사람인데 나이가 다르다" 또는 "같은 방인데 아침과 밤이 다르다"를 안정적으로 다룰 수 있다.

### 4-1. 과거 에피소드 이미지 자동 참조 규칙

- 동일 인물로 판정되면 최근 승인 이미지와 대표 variant 이미지를 자동 포함
- 동일 장소로 판정되면 넓은 establishing 이미지와 디테일 이미지 둘 다 포함
- 동일 소품으로 판정되면 형태 보존용 close-up 기준 이미지를 포함
- 신규 variant 로 판정되면 부모 canon 이미지와 가장 가까운 변형 이미지를 함께 포함
- confidence 가 낮으면 자동 강제 적용하지 않고 추천만 한다

### 5. Prompt Locking

- 공통 스타일 토큰, camera grammar, 금지 토큰을 씬별 프롬프트에 자동 삽입
- canon/variant 별 필수 속성은 프롬프트에서 누락되지 않도록 잠금
- 프로젝트 승인 전에는 스타일 버전이 임의로 바뀌지 않도록 잠금

### 6. Reference-Aware Generation

- 이전 승인 이미지, variant 기준 이미지, 장소 variant 기준 이미지를 조합해 사용
- 과거 에피소드에서 연결된 승인 이미지는 `ReferenceBundle` 을 통해 자동 주입
- 필요 시 `LoRA`, `IP-Adapter`, `ControlNet`, 얼굴 고정, 포즈 고정, 인페인팅 모듈을 선택적으로 적용
- variant 간 공통점과 차이점은 delta prompt 로 분리해 넣는다

### 6-1. Camera Angle Edit as I2I

카메라 각도 변형은 새 이미지를 처음부터 다시 뽑는 문제라기보다, `승인된 기준 이미지`를 바탕으로 시점을 바꾸는 문제로 다룬다.

- 기준은 먼저 하나의 `base keyframe` 을 승인받는다
- 이후 앵글 변형은 별도 I2I adapter 로 수행한다
- 기본 후보 adapter 는 `fal-ai/qwen-image-edit-2511-multiple-angles`
- 이 adapter 에는 `horizontal_angle`, `vertical_angle`, `zoom`, `additional_prompt` 같은 구조화 파라미터를 전달한다
- adapter 가 반환하는 `constructed prompt` 도 함께 저장해 Opik trace 와 결과 lineage 에 남긴다
- 즉, Cinematography Planner 의 결과는 텍스트 프롬프트뿐 아니라 `angle edit recipe` 로도 내려간다

권장 원칙:

- 주인공/배경/소품이 이미 잘 잡힌 컷을 먼저 기준 이미지로 확정
- 같은 장면의 다른 시점은 angle edit 로 파생
- angle edit 결과가 구조를 크게 무너뜨리면 base image 를 바꾸지 않고 repair 단계로 복구

### 6-2. LoRA Asset Lifecycle and Multi-LoRA Apply

화풍은 생성 플로우 안에서 항상 학습하는 것이 아니라, `독립적인 LoRA 자산 관리 메뉴`에서 준비해 두고 생성 시 선택해서 쓰는 구조가 맞다.

#### A. 내부 학습 LoRA 생성

- 학습 adapter 는 `fal-ai/flux-kontext-trainer`
- 입력은 `before image / after image` 쌍과 선택적 caption 으로 구성된 dataset
- job 파라미터로 `steps`, `learning_rate`, `output_lora_format` 등을 관리하고 기본 정책은 `output_lora_format = fal` 로 둔다
- trainer 페이지 기준 최소 학습 step 은 `500` 이므로 UI 기본값도 그 이상으로 둔다
- 반환되는 LoRA 산출물과 config 파일을 `StyleLoraAsset` 으로 저장하고, 다시 `LoraRegistryItem` 으로 등록한다

#### B. 외부 LoRA 업로드 또는 URL 등록

- 외부 Flux-Kontext editing LoRA 는 파일 업로드 또는 외부 URL 등록으로 추가한다
- 서비스 운영 안정성을 위해 가능하면 외부 URL 을 그대로 쓰지 않고 자체 스토리지로 mirror 한 뒤 `HostedAsset` URL 을 발급한다
- 출처는 `LoraSource` 로 기록해 `trained`, `uploaded`, `external_url`, `mirrored` 를 구분한다

#### C. 서비스 배포 위치에 저장

- 학습 완료 자산과 외부 업로드 자산은 `LoRA Hosting Service` 를 통해 서비스 URL 을 가진다
- 저장 위치는 배포 환경의 object storage 또는 정적 asset 서버를 사용한다
- 생성 파이프라인은 DB 안의 바이너리를 직접 읽지 않고, registry 가 관리하는 URL 을 참조한다

#### D. 생성 시 다중 LoRA 적용

- 적용 adapter 는 `fal-ai/flux-kontext-lora`
- 입력은 `카메라 앵글, 빛, 조도, 구조 보정까지 끝난 최종 구조 이미지`
- 여기에 `loras[]` 배열을 사용해 여러 LoRA 를 순서대로 넣고, 각 항목에 `path/url` 과 `scale` 을 부여한다
- 즉, style 은 구도와 광량을 결정하는 단계가 아니라, 최종 구조 컷에 입히는 마지막 룩 레이어다

권장 원칙:

- 화풍용 LoRA 는 project 또는 season 단위 자산으로 관리
- 내부 학습 LoRA 와 외부 LoRA 를 같은 registry 에서 관리하되 출처 메타는 분리
- 여러 LoRA 를 쓰더라도 기본은 `LoraStackPreset` 으로 묶어 재사용
- 화풍 적용은 angle/light/edit 이전이 아니라 이후에 둔다
- 하나의 LoRA 만 고르는 UI가 아니라 ordered stack UI 로 둔다

### 7. Continuity QA

- 얼굴 동일성뿐 아니라 `연령 적합성`, `변장 적합성`, `의상 상태`, `소품 상태`, `배경 상태`를 각각 점수화
- 사람이 승인한 기준 이미지를 기준점으로 삼아 편차를 탐지
- 점수가 낮으면 어느 축에서 실패했는지 표시한 뒤 I2I 리파인 큐로 보낸다

```mermaid
flowchart LR
    Canon[Canon Entity] --> Variant[Variant Tree]
    Past[Past Episodes] --> Detect[Reappearance Detection]
    Detect --> Variant
    Variant --> Snapshot[Scene State Snapshot]
    Snapshot --> Ref[Reference Bundle]
    Snapshot --> Prompt[Prompt Constraints]
    Ref --> Base[Base Generation]
    Prompt --> Base
    Base --> Angle[Angle Edit I2I]
    Angle --> Repair[Repair and Light I2I]
    Repair --> Style[Style LoRA Stack Apply I2I]
    Style --> QA[Continuity QA]
```

## 8. 생성 파이프라인 설계

핵심은 `LLM 1회 + T2I 1회` 구조가 아니라, `과거 에피소드 재분석`, `재등장 매칭`, `상태 해석`, `시네마토그래피 판단`, `구조 생성`, `후행 style LoRA stack 적용`으로 분리된 파이프라인이다.

### 8-0. Fulltext-Only Scene Still Extraction 원칙

씬 추출 단계는 이제 `chunked` 가 아니라 `에피소드 PDF 전체를 텍스트로 추출한 full text 1회 입력`을 기본으로 둔다.

- `heading parser` 로 감지한 scene heading catalog 를 함께 넣어 coverage checklist 로 사용
- LLM 은 각 heading 당 최소 1개, 필요하면 추가 beat 까지 `SceneStillCandidate` 를 생성
- 각 candidate 는 `still_frame_prompt_raw`, `camera`, `lighting`, `visible_entities` 를 분리해서 반환
- `visible_entities` 는 실제 화면에 보이는 인물/배경/물체만 허용하고, 보이지 않는 영혼/내면/추상 개념은 제외
- 후처리에서 `entity_canon` / prior series memory 와 매칭해 `[CHAR_0001]`, `[LOC_0007]`, `[PROP_0012]` 같은 stable id 를 붙인다
- 최종 prompt 는 `still_frame_prompt + [ID] anchor tags` 구조로 재조립한다

즉, scene still 추출은 단순 요약이 아니라 `T2I 에 바로 넘길 수 있는 정지 화면 후보 + 카메라/조명 directive + continuity link` 를 만드는 단계다.

```mermaid
flowchart TD
    A[Script Import] --> B[Scene Segmentation]
    B --> B2[Fulltext Scene Still Extraction]
    A --> A2[Past Episode Index Load]
    B2 --> C[Entity Extraction]
    A2 --> C2[Cross-Episode Reappearance Match]
    C --> C2
    C2 --> D[Continuity State Resolve]
    D --> D2[Reference Bundle Build]
    D --> E[Cinematography Analysis]
    E --> F[Recipe Match: Composition Lens Light]
    D2 --> G[Shot Spec Build]
    F --> G
    G --> H[Prompt Draft by LLM]
    H --> I[Human Edit and Override]
    I --> J[Base T2I or Base I2I Pass]
    J --> J2[Angle Edit I2I]
    J2 --> J3[Repair and Light I2I]
    J3 --> J4[Style LoRA Stack Apply I2I]
    J4 --> K[Auto Ranking]
    K --> L{Quality OK?}
    L -- No --> M[I2I Refine / Inpaint / Upscale]
    M --> N[Continuity QA]
    N --> O{Approved?}
    O -- No --> I
    O -- Yes --> P[Scene Layout Placement]
    P --> Q[PDF / EPUB Export]
    O -- Yes --> R[Backfill Episode Index]
    L -- Yes --> N
```

### 단계별 상세

| 단계 | 목적 | 입력 | 출력 |
|---|---|---|---|
| Scene Segmentation | 시나리오 구조화 | 원본 스크립트 | 씬, 대사, 행동 단위 |
| Past Episode Index Load | 이전 에피소드 지식 불러오기 | 시리즈 데이터 | 검색 가능한 과거 인덱스 |
| Entity Extraction | 인물/배경/소품 추출 | 씬 텍스트 | 바이블 초안 |
| Cross-Episode Reappearance Match | 과거와 현재 개체를 연결 | 현재 추출 결과 + 과거 인덱스 | 재등장 후보와 연결 |
| Continuity State Resolve | canon/variant/snapshot 확정 | 씬 + 바이블 | 씬 상태 모델 |
| Reference Bundle Build | 과거 승인 이미지와 현재 기준 이미지를 묶음 | 연결 결과 + 자산 저장소 | reference bundle |
| Cinematography Analysis | 장면 의도와 감정 구조 해석 | 씬 상태 + 서사 정보 | 미장센 요구사항 |
| Recipe Match | 구도/렌즈/빛/조명 레시피 선택 | 미장센 요구사항 | shot spec |
| Prompt Draft | 레시피가 반영된 프롬프트 초안 | shot spec | 편집 가능한 프롬프트 |
| Base T2I or Base I2I Pass | 기준 이미지 생성 또는 선정 | 프롬프트 + 레퍼런스 | base keyframe |
| Angle Edit I2I | 기준 이미지를 다른 카메라 시점으로 변형 | base keyframe + angle edit recipe | angle variants |
| Repair and Light I2I | 구조 보정, 광량/조도, 디테일 수선 | angle variant + repair recipe | structural finals |
| Style LoRA Stack Apply I2I | 여러 Flux-Kontext LoRA 를 순서대로 적용 | structural final + ordered loras[] | styled finals |
| I2I Refine | 품질/일관성 향상 | 선택 후보 | 최종 후보 |
| Layout Placement | 씬과 이미지 배치 | 본문 + 최종 이미지 | 출력 레이아웃 |

### 시네마토그래피 판단 레이어

카메라 각도, 구도, 렌즈, 빛 방향은 프롬프트 한 줄로 뭉개면 안 된다. `판단` 과 `적용` 을 분리해야 한다.

#### 1. Cinematography Analysis

이 단계는 장면을 읽고 다음을 해석한다.

- 힘의 관계: 누가 우위인지, 누가 눌리는지
- 감정 거리: 친밀, 고립, 긴장, 위협
- 정보 공개 방식: 숨길지, 드러낼지, 점진적으로 reveal 할지
- 공간 사용: 좁게 느껴야 하는지, 확장감이 필요한지
- 움직임 성격: 정적, 불안정, 추적, 관조

#### 2. Prepared Recipe Library

판단 결과를 바로 프롬프트로 쓰지 않고 미리 준비된 레시피 세트에 매핑한다.

| 레시피 종류 | 예시 필드 |
|---|---|
| CompositionRecipe | subject placement, negative space, symmetry, depth layers |
| CameraAngleRecipe | eye-level, low-angle, high-angle, dutch, over-shoulder |
| LensRecipe | focal length band, perspective compression, depth of field |
| LightingRecipe | key direction, fill ratio, rim light, source motivation |
| ColorRecipe | palette, contrast, saturation, temperature |
| BlockingRecipe | 인물 배치, 시선 방향, 거리 관계 |

#### 3. Recipe Application

적용은 다음 순서로 한다.

1. 장면 해석 결과를 recipe query 로 변환
2. candidate recipe 3~5개를 랭킹
3. 사람 편집자가 recipe 를 선택 또는 수정
4. 선택된 recipe 를 shot spec 과 `angle edit recipe` 로 고정
5. prompt fragment 와 model conditioning 으로 분해해 생성 엔진에 전달
6. 후행 style LoRA 적용용 `LoraStackPreset` 또는 ordered `LoraRegistryItem[]` 을 attach

즉, `심미적 판단 -> 레시피 선택 -> shot spec / angle recipe / lora stack -> 프롬프트/컨디셔닝` 으로 내려가야 한다.

#### 4. 구조화 필드 예시

- 카메라 거리: ECU, CU, MCU, MS, LS
- 카메라 각도: eye-level, low-angle, high-angle, over-shoulder, top-shot
- 렌즈: 24mm wide, 35mm natural, 50mm intimate, 85mm compressed
- 빛 방향: front, side, back, top, motivated practical
- 광질: hard, soft, diffused, volumetric
- 색감: warm dusk, cold sodium vapor, muted realism, saturated noir
- 구도 의도: isolation, confrontation, dominance, vulnerability, revelation

즉, 프롬프트는 자유 텍스트만 두지 않고 구조화 필드와 recipe object 를 병행해야 한다.

## 9. Human-in-the-Loop UI 제안

### 화면 0. Admin Console

- 서비스 전역 설정 관리
- LLM/API key, LiteLLM 모델 라우팅, 기본 provider 정책 설정
- 전역 prompt template, system prompt, 중요한 prompt fragment 관리
- LoRA Library 운영 정책, hosted asset 상태, 전체 비용/Opik 모니터링
- 전체 프로젝트 목록 조회, 생성, 보관, 권한 관리

### 화면 1. Project Dashboard

- 프로젝트 생성, 시나리오 업로드, 에피소드 목록
- 전체 진행률, 비용, 실패 작업, export 상태 표시
- `creator` 는 자신이 접근 가능한 프로젝트만 봄
- `admin` 은 전체 프로젝트와 전역 상태를 함께 볼 수 있음

### 화면 2. Script Workspace

- 좌측: 시나리오 원문
- 우측: 씬 분할 결과, 중요도, 추출된 개체
- 기능: 씬 병합/분할, 중요 씬 지정, 앵커 조정
- 기능: 이전 에피소드 재등장 후보와 매칭 근거 확인

### 화면 3. Continuity Bible Studio

- 캐릭터/장소/소품 카드 뷰
- 각 개체별 canon 과 variant 트리, 기준 이미지, 설명, 금지 요소, 허용 변형 규칙 관리
- 별도 `Relation Graph` 패널에서 인물-장소-소품 관계 fact 와 participant role 관리
- 관계 타입별 필터: 가족, 적대, 소유, 은신, 보관, 조종, 추적, 소속
- 같은 엔티티가 여러 관계에 걸쳐 연결되는 그래프 시각화와 수동 수정
- 시리즈/에피소드 상속 관계 확인
- 이전 에피소드 승인 이미지에서 자동 수집된 reference 후보 검토

### 독립 메뉴. LoRA Library

이 메뉴는 생성 흐름과 분리된 독립 메뉴다. 학습은 항상 수행되는 것이 아니므로 Shot Planner 안에 두지 않는다.

#### 탭 A. Train

- 학습용 이미지 pair 업로드: `before image` 와 `after image`
- pair 별 caption 입력과 기본 caption 자동 채우기
- dataset 유효성 검사: 해상도, pair 누락, caption 누락 여부
- 학습 파라미터 입력: steps, learning rate, output format
- 기본 preset: `steps >= 500`, `output_lora_format = fal`
- 학습 job 상태: queued, running, completed, failed

#### 탭 B. Import

- 외부 Flux-Kontext LoRA 파일 업로드
- 외부 LoRA URL 등록
- 필요 시 자체 스토리지로 mirror 하여 서비스 URL 발급

#### 탭 C. Registry

- 내부 학습 LoRA 와 외부 LoRA 를 한 리스트로 관리
- source, provider, created_at, hosted_url, checksum, notes 표시
- 활성/비활성, deprecated, alias 관리
- `admin` 은 전체 registry 관리
- `creator` 는 사용 권한이 있는 LoRA 만 조회 및 선택

#### 탭 D. Stack Presets

- 여러 LoRA 를 순서대로 추가
- 각 LoRA 별 scale 조정
- preset 이름 저장
- 특정 프로젝트/시즌의 기본 preset 지정

### 화면 4. Shot Planner & Cinematography Lab

- 상단: 씬 요약, 감정선, 연출 의도
- 중앙: 샷 목록 타임라인
- 우측: recipe 패널과 구조화 프롬프트 편집기
- 기능: 카메라 각도, 구도, 렌즈, 빛 방향, 조명 패턴, 색감, 화풍 잠금, seed group 수정
- 기능: recipe 추천안 비교, 선택, override, custom recipe 저장
- 기능: angle edit slider, 적용할 `LoraStackPreset` 또는 ordered LoRA 목록 선택, base image 기준 파생 체인 확인

### 화면 5. Image Review Board

- 후보 이미지 그리드
- 품질 점수, 일관성 점수, 연령/변장 적합성, 배경/소품 상태 적합성, 비용, 생성 체인 lineage 표시
- one-click 로 `재생성`, `I2I`, `인페인팅`, `업스케일`, `승인`
- base -> angle -> repair/light -> style lora stack 순서의 lineage 비교 보기

### 화면 6. Export Studio

- 씬 본문과 이미지 위치 미리보기
- 씬당 이미지 수 선택
- 캡션, 여백, 표지, 목차, 메타데이터 설정
- PDF / EPUB 동시 출력

```mermaid
flowchart LR
    AC[Admin Console] --> LL[LoRA Library]
    AC --> D[Dashboard]
    D --> S[Script Workspace]
    S --> B[Bible Studio]
    B --> P[Shot Planner and Cinematography Lab]
    LL --> P
    P --> R[Image Review Board]
    R --> E[Export Studio]
    R --> P
    E --> D
```

## 10. 프롬프트 버전 관리 전략

이 시스템의 핵심은 프롬프트가 자주 바뀐다는 점이다. 따라서 프롬프트는 코드 문자열이 아니라 `버전된 자산`으로 관리해야 한다.

### 관리 단위

- `Prompt Template`
- `Prompt Fragment`
- `Shot Grammar`
- `Style Pack`
- `Provider Config`
- `Eval Profile`

### 권장 원칙

- 모든 프롬프트 렌더 결과를 저장한다.
- 실행 시점의 입력 변수와 렌더된 최종 프롬프트를 함께 보관한다.
- 템플릿 수정은 `draft -> approved -> deprecated` lifecycle 을 가진다.
- 씬 생성 결과는 어떤 프롬프트 버전으로 나왔는지 lineage 를 가진다.
- 과거 결과 재현을 위해 모델명, 파라미터, 템플릿 버전, 레퍼런스 이미지 해시를 함께 보관한다.

### 예시 엔티티

| 엔티티 | 예시 필드 |
|---|---|
| prompt_template | id, name, semver, schema_version, status |
| prompt_revision | template_id, revision_no, body, variables_schema, created_by |
| prompt_render_log | run_id, revision_id, inputs_json, rendered_prompt |
| provider_config_revision | provider_key, model_name, params_json, active_from |
| eval_profile | metrics, thresholds, scorer_version |

## 11. DB / 결과물 / 설정 버전 체계

Git 외 별도 시스템 버전 관리는 최소 4축으로 나눠야 한다.

1. `Project Schema Version`
2. `Prompt Schema Version`
3. `Provider Config Version`
4. `Artifact Metadata Version`

여기에 모든 변경은 `immutable revision` 으로 남기고, 현재 사용 버전은 alias 로 가리킨다.

```mermaid
flowchart TD
    A[Draft Revision] --> B[Validation]
    B --> C[Approved Revision]
    C --> D[Alias: current]
    C --> E[Run Execution]
    E --> F[Result Lineage Stored]
    C --> G[Deprecated Revision]
```

### revision 운영 원칙

- revision 은 수정하지 않고 새로 만든다.
- 승인된 revision 만 production workflow 에서 사용한다.
- 기존 결과물은 생성 당시 revision 과 강하게 연결한다.
- alias 변경은 빠르지만, 과거 결과의 의미를 바꾸지 않는다.

### 실행 재현성 필드

시스템 자체 업그레이드에 대비하려면 결과물마다 아래 provenance 를 함께 저장해야 한다.

- `app_build_version`
- `pipeline_version`
- `worker_image_digest`
- `adapter_version`
- `prompt_revision_id`
- `provider_config_revision_id`
- `export_preset_revision_id`

즉, 같은 씬 이미지를 나중에 다시 열어도 "어떤 코드/설정/프롬프트로 만들어졌는가"가 남아 있어야 한다.

## 12. 마이그레이션 방법론

### 목표

- DB 스키마 변경
- 프롬프트 입력 스키마 변경
- 생성 메타데이터 필드 변경
- 출력 레이아웃 규칙 변경

를 모두 안전하게 흡수해야 한다.

### 권장 정책

- 모든 프로젝트는 `manifest.json` 에 현재 버전과 최소 호환 버전을 기록
- 프로젝트 open/import 시 호환성 검사 수행
- 마이그레이션은 `forward-only + dry-run + backup` 기본 정책
- DB와 프롬프트 마이그레이션을 분리하되 하나의 실행 보고서로 묶음
- 각 마이그레이션은 idempotent 해야 함

### 마이그레이션 레이어

| 레이어 | 예시 |
|---|---|
| DB Migration | 테이블 추가, 컬럼 분리, 인덱스 변경 |
| Content Migration | prompt json 필드명 변경, layout anchor 재계산 |
| Asset Migration | 이미지 메타 재추출, 썸네일 재생성 |
| Config Migration | provider 옵션 키 변경, 기본값 갱신 |

### 실행 흐름

```mermaid
sequenceDiagram
    participant U as User
    participant A as App
    participant M as Migration Manager
    participant B as Backup
    participant P as Project DB/Assets

    U->>A: Open or Import Project
    A->>M: Check versions
    M->>B: Create backup snapshot
    M->>P: Run dry-run
    M->>P: Apply migrations
    M->>A: Emit migration report
    A->>U: Open upgraded project
```

### 중요한 운영 규칙

- 마이그레이션 실패 시 자동 롤백 가능해야 한다.
- 결과물 재현성이 필요한 run 은 이전 revision 으로도 다시 열람 가능해야 한다.
- prompt migration 후에는 golden scene 세트로 회귀 평가를 자동 수행한다.

## 13. LiteLLM + Opik 설계

### LiteLLM 사용 이유

- LLM provider 교체 비용 축소
- 모델 버전 업그레이드 시 adapter 변경 최소화
- fallback / routing / usage aggregation 가능
- 비용 추적을 중앙화하기 좋음

### fal 기반 I2I 운영 원칙

- `fal` 호출은 브라우저가 아니라 서버사이드 proxy 에서 수행
- 장시간 작업은 queue + webhook 기반으로 수집
- 각 요청에 `fal_model_id`, `fal_request_id`, `source_image_id`, `recipe_revision_id` 를 남긴다
- `flux-kontext-lora` 호출 시 사용한 ordered `loras[]`, 각 scale, hosted_url 을 함께 기록한다
- 비용 집계 시 LLM 비용과 분리해 `image_edit_cost` 로 별도 집계한다
- trainer job 은 inference 와 분리해 `lora_training_cost` 와 `training_steps` 로 집계한다

### Opik 사용 포인트

- 씬 분석, 샷 플래닝, 프롬프트 생성, 평가 단계를 trace 로 연결
- prompt revision 별 성능 비교
- golden scene 기준 실험 관리
- 품질/비용/지연 시간 트레이드오프 관측

### 권장 연동 방식

- 모든 파이프라인 실행에 `run_id`, `project_id`, `episode_id`, `scene_id`, `shot_id` 부여
- LiteLLM 호출 wrapper 에 usage/cost capture 삽입
- Opik trace/span metadata 에 revision 정보 기록
- 내부 DB에는 rollup 테이블을 두어 프로젝트/에피소드/씬 단위 집계 제공

### Opik 최적화 방법론

Opik 은 단순 로그 저장보다 `prompt optimization loop` 중심으로 써야 한다.

1. `golden scene set` 을 만든다.
2. prompt revision 후보를 여러 개 실행한다.
3. 품질 점수, 일관성 점수, 비용, 지연 시간을 함께 비교한다.
4. pairwise 비교와 사람 평가를 함께 저장한다.
5. 승인된 revision 만 alias `current` 로 승격한다.

권장 평가지표는 다음과 같다.

- 씬 의미 충실도
- 캐릭터 일관성
- 배경/소품 일관성
- 스타일 고정도
- 사람 수정 횟수
- 최종 승인까지의 총 비용
- 승인까지 걸린 총 시간

### 비용 추적 최소 지표

- LLM input/output token
- LLM 추정 비용
- T2I/I2I 호출 횟수와 모델별 비용
- `fal` angle edit 호출 수와 이미지 크기 기준 비용
- `fal` flux-kontext-lora 호출 수와 이미지 기준 비용
- `fal` flux-kontext-trainer steps 와 학습 비용
- 리파인 횟수
- 씬당 승인까지 걸린 총 비용
- 에피소드당 평균 비용
- export 기준 최종 산출물 1페이지당 비용

## 14. 배포 구조

내부용이므로 첫 버전은 다음 정도가 적당하다.

- `web + api` 하나의 앱 컨테이너
- `worker` 하나의 비동기 작업 컨테이너
- `LiteLLM` 프록시 컨테이너
- `Opik` 서버
- 로컬 디스크 또는 `S3/MinIO` 호환 스토리지

```mermaid
flowchart LR
    Browser --> WebAPI[Web + API]
    WebAPI --> Worker
    WebAPI --> LiteLLM
    WebAPI --> Opik
    WebAPI --> Storage[(Local Disk / MinIO)]
    WebAPI --> Catalog[(Optional System DB)]
    Worker --> LiteLLM
    Worker --> Storage
    Worker --> Opik
```

### 배포 권장안

- 개발: `docker compose`
- 사내 서버: 단일 VM 또는 NAS 연동 서버
- 백업: 프로젝트 폴더 단위 스냅샷
- 복구: 프로젝트 폴더 import 후 catalog 재등록
- 외부 image edit provider 연동은 API key 보호를 위해 반드시 서버 경유

## 15. Export / Import 설계

### Export 단위

- 프로젝트 전체
- 특정 에피소드만
- 특정 씬 묶음만

### 권장 포맷

- 내부 보관용: `project_bundle.zip`
- 로컬 이동용: 압축 해제 가능한 프로젝트 폴더
- 문서 산출물: `PDF`, `EPUB`

### bundle 내용

- `manifest.json`
- `project.sqlite`
- 생성/레퍼런스 asset
- prompt/config revision snapshot
- export preset

### 장점

- 환경 의존성을 줄임
- 다른 머신으로 옮기기 쉬움
- 특정 시점의 상태를 통째로 아카이브 가능

## 16. 추천 기술 스택

### 백엔드

- `Python + FastAPI`
- `SQLAlchemy + Alembic` 또는 유사 migration layer
- `SQLite` 기본, 추후 PostgreSQL 전환 가능
- 큐는 `RQ`, `Dramatiq`, `Celery` 중 단순한 것 우선

### 프론트엔드

- `Next.js` 또는 `React + Vite`
- 문서 미리보기와 이미지 검토에 강한 컴포넌트 구조
- JSON schema 기반 prompt editor 일부 도입 가능

### 생성 관련

- LLM: `LiteLLM` 뒤에 OpenAI / Anthropic / Gemini 등 라우팅
- T2I/I2I: provider adapter 구조
- 평가: CLIP 유사도, 얼굴/객체 consistency scorer, 인간 승인

### 출력

- HTML 템플릿 기반 PDF 렌더링
- EPUB 패키징 엔진 분리

## 17. 초기 개발 우선순위

### Phase 1. 코어 제작 흐름

- 프로젝트/에피소드/씬 구조
- 시나리오 import
- fulltext scene still extraction
- scene heading catalog + visible entity link
- prompt workbench
- T2I 1차 생성
- 승인 이미지 수동 배치
- PDF export

### Phase 2. 일관성 엔진

- Visual Bible
- scene memory
- continuity score
- I2I refine loop

### Phase 3. 버전/운영 고도화

- revision manager
- migration registry
- Opik 평가 대시보드
- 비용 집계
- EPUB export

## 18. 핵심 리스크와 대응

| 리스크 | 설명 | 대응 |
|---|---|---|
| 일관성 붕괴 | 에피소드가 길어질수록 외형/공간 변형 누적 | 바이블 + 기준 이미지 + QA score + 수동 승인 |
| 프롬프트 난잡화 | 사람이 계속 수정하며 템플릿이 오염 | fragment/grammar 분리, 승인 상태 관리 |
| 모델 업그레이드 충격 | LLM/T2I 교체 시 결과 품질 변화 | LiteLLM + adapter + golden scene 회귀 평가 |
| 비용 폭증 | 반복 리파인으로 비용 상승 | scene cost budget, auto-stop, 승인 기준 |
| 마이그레이션 실패 | 오래된 프로젝트 reopen 실패 | snapshot backup, dry-run, forward-only policy |

## 19. 최종 제안

이 프로젝트의 적정한 첫 구조는 `작지만 강한 내부 제작 시스템`이다.  
핵심은 다음 세 가지다.

1. `모듈형 모놀리스 + 워커`로 단순하게 시작한다.
2. `Visual Bible + Scene Memory + Revision System`으로 시리즈 일관성을 확보한다.
3. `LiteLLM + Opik + 자체 migration`으로 잦은 업그레이드를 흡수한다.

즉, 이 시스템의 경쟁력은 이미지 모델 하나가 아니라 다음 조합에서 나온다.

- 구조화된 씬 해석
- 감독적 샷 플래닝
- 사람 개입 가능한 프롬프트 워크벤치
- 다단계 T2I/I2I 리파인
- 강한 revision / migration / cost tracking

---

## 부록 A. MVP 범위 한 줄 정의

`드라마/영화 시나리오를 씬 단위로 분석하고, 사람의 수정이 가능한 프롬프트 워크플로를 거쳐, 일관성 있는 이미지와 PDF/EPUB 결과물을 생성하는 내부 제작 시스템`

## 부록 B. 다음 문서로 이어질 항목

- API 명세 초안
- SQLite 조회 패턴과 repository 설계
- prompt template schema 초안
- image generation adapter interface 초안
- export layout schema 초안
