# 03. Prompt I/O Contracts

이 문서는 각 prompt가 어떤 입력을 받고, 어떤 출력을 내야 하는지 현실적인 계약 형태로 정리한다. 여기서 제안하는 이름은 구현 클래스명이 아니라 contract name이다. 기존 step과 1:1 또는 n:1로 대응할 수 있다.

## 공통 원칙

모든 LLM 단계는 다음 중 하나의 역할만 가져야 한다.

| 역할 | 설명 | 예 |
|---|---|---|
| Extract | 원문에서 명시된 정보 추출 | scene segmentation, entity listing |
| Interpret | 원문 기반으로 시각적 의미 해석 | beat, character state |
| Select | 후보 중 제작 가치가 높은 것 선택 | shot selection |
| Plan | 다음 이미지 단계의 구조 계획 | background master plan |
| Compile | 이미 결정된 정보를 최종 prompt로 조립 | scene_detail |
| Validate | 앞 단계 출력의 의미 오류 검출 | shot_validator, t2i_review |

한 prompt가 이 역할을 3개 이상 동시에 수행하면 실패 확률이 급격히 올라간다.

## 공통 입력 envelope

프롬프트 user message는 항상 같은 외형을 갖는 것이 좋다.

```text
# TASK
<이번 호출이 소유하는 결정만 명시>

# NON-GOALS
<이번 호출이 결정하면 안 되는 것>

# SOURCE PACKET
<원문/씬/대사/action/evidence>

# CANON PACKET
<이미 확정된 인물/장소/소품/아웃룩/세계관>

# UPSTREAM CARDS
<beat/shot/continuity/background 등 이전 단계 결과>

# DECISION RULES
<이번 단계에서 적용할 우선순위>

# OUTPUT
Return strict JSON matching the schema.
```

## 공통 output fields

가능한 모든 단계에 아래 field를 넣자는 뜻은 아니다. 하지만 중요한 creative decision을 내는 단계는 최소 일부를 가져야 한다.

```json
{
  "schema_version": "creative_contract.v1",
  "project_id": "optional",
  "episode_id": "optional",
  "scene_index": 1,
  "source_facts": [],
  "visual_inferences": [],
  "creative_decisions": [],
  "uncertainties": [],
  "validation_notes": []
}
```

### EvidenceRef

```json
{
  "scene_index": 12,
  "source_type": "action",
  "span_id": "S12:A14-A18",
  "quote_short": "optional short quote under internal policy"
}
```

`quote_short`는 없어도 된다. 핵심은 원문 위치를 가리키는 `span_id`다.

## Contract 1. Scene Reading Card

대응 현재 step: `scene_summary`, 일부 `beat_extract` 전처리

목적: scene text를 "시각화 가능한 사건 단위"로 읽기 위한 기본 사실 카드.

### Input

```json
{
  "scene_index": 12,
  "heading": "S12. generic location - night",
  "scene_text": "...",
  "episode_summary": "...",
  "visual_world_rules": {
    "era": "...",
    "region": "...",
    "director_notes": []
  },
  "known_entities": {
    "characters": [],
    "locations": [],
    "props": []
  },
  "previous_scene_brief": {
    "scene_index": 11,
    "summary": "...",
    "open_states": []
  }
}
```

### Output

```json
{
  "scene_index": 12,
  "scene_function": "reveal | confrontation | transition | aftermath | setup | payoff | montage | dream | flashback",
  "time_space": {
    "time_of_day": "night",
    "reality_layer": "real | flashback | dream | hallucination | imagined | unknown",
    "location_hints": ["small interior room"]
  },
  "explicit_events": [
    {
      "event_id": "E12_01",
      "description": "A figure lies motionless on the floor.",
      "participants": ["C02"],
      "evidence_refs": [{"scene_index": 12, "source_type": "action", "span_id": "S12:A02"}]
    }
  ],
  "state_changes": [
    {
      "change_id": "SC12_01",
      "before": "observer does not know what is inside",
      "after": "observer sees the body",
      "visible_trigger": "doorway view reveals the figure",
      "evidence_refs": []
    }
  ],
  "visual_uncertainties": [
    {
      "question": "Exact body orientation is not fully specified.",
      "impact": "scene_consistency must choose and freeze a pose if multiple selected shots show the body."
    }
  ]
}
```

### Validation

- 모든 `participants`는 known entity이거나 `unresolved_person`으로 표시해야 한다.
- `scene_function`은 enum이어야 한다.
- 원문에 없는 신체 훼손, 무기, 피, 의상은 추가하지 않는다.

## Contract 2. Beat Card

대응 현재 step: `beat_extract`

목적: 씬을 상태 변화 단위로 쪼갠다. 아직 이미지 shot을 고르지 않는다.

### Input

```json
{
  "scene_reading_card": {},
  "scene_text": "...",
  "visual_world_rules": {},
  "character_list": []
}
```

### Output

```json
{
  "scene_index": 12,
  "beats": [
    {
      "beat_index": 1,
      "beat_type": "arrival | discovery | reaction | conflict | decision | aftermath | transition",
      "description": "The observer reaches the threshold.",
      "before_state": "room contents unseen",
      "after_state": "observer has a partial view inside",
      "visible_evidence": "body posture, doorway, observer reaction",
      "source_event_ids": ["E12_01"],
      "evidence_refs": []
    }
  ]
}
```

### Validation

- beat는 상태 변화를 설명해야 한다.
- camera angle, final prompt, outfit은 넣지 않는다.
- beat가 추상 감정이면 `visible_evidence`로 외부화해야 한다.

## Contract 3. Shot Candidate Card

대응 현재 step: `shot_extract`, `shot_validator`

목적: beat를 "한 장의 still image로 가능한 순간 후보"로 분해한다.

### Input

```json
{
  "scene_index": 12,
  "scene_text": "...",
  "beats": [],
  "previous_shot_candidates": [],
  "allowed_character_names": []
}
```

### Output

```json
{
  "scene_index": 12,
  "shots": [
    {
      "shot_index": 1,
      "based_on_beat": 1,
      "moment_type": "before | during | after | reaction | pov | insert | establishing",
      "description": "The observer stands at the doorway, one hand on the frame, staring into the dark room.",
      "characters": ["observer_name"],
      "visible_objects": ["doorway"],
      "motion_freeze": {
        "has_motion": false,
        "motion_direction": ""
      },
      "visual_complexity": "low | medium | high",
      "evidence_refs": []
    }
  ]
}
```

### Validation

- 시간 연결어 금지.
- outfit 금지.
- entity ID 금지.
- 한 shot에 두 개 이상의 순간 금지.
- `characters`는 allowed list만 사용.

## Contract 4. Shot Selection Card

대응 현재 step: `shot_selection`

목적: 후보 shot 중 실제 웹북/이미지 생성 가치가 높은 것만 선택한다.

### Input

```json
{
  "scene_index": 12,
  "scene_reading_card": {},
  "beats": [],
  "shot_candidates": [],
  "episode_budget": {
    "max_per_scene": 3,
    "target_total": 100
  }
}
```

### Output

```json
{
  "scene_index": 12,
  "selected_shots": [
    {
      "shot_index": 4,
      "narrative_weight": "high",
      "visual_expressibility": "medium",
      "reason": "The discovery changes the observer's understanding and can be staged as a clear doorway reveal.",
      "story_function_to_preserve": "discovery of motionless body",
      "recommended_strategy": "simplify | partial_focus | reframe | direct"
    }
  ],
  "not_selected_high_risk": [
    {
      "shot_index": 5,
      "reason": "Narratively related but visually redundant with shot 4.",
      "fallback_if_user_requests": "reaction close framing"
    }
  ]
}
```

### Validation

- selected shot index는 후보에 존재해야 한다.
- scene cap, ratio cap을 지켜야 한다.
- reason 없는 선택은 reject.
- high narrative/low visual은 `recommended_strategy` 필수.

## Contract 5. Entity Canon Card

대응 현재 step: `entity_*`, `entity_merge`, `entity_relation`, `entity_detail`, `entity_t2i`

목적: 시나리오의 인물/장소/소품을 stable ID로 묶는다.

### Output

```json
{
  "characters": [
    {
      "character_id": "C01",
      "display_name": "generic internal name",
      "entity_type": "human | nonhuman | creature | crowd",
      "stable_visual_traits": ["age range", "body type", "hair shape"],
      "dynamic_traits": ["injury", "temporary dirt"],
      "variant_of": "",
      "visual_similarity_to_base": true,
      "t2i_identity_prompt": "..."
    }
  ],
  "locations": [
    {
      "location_id": "L01",
      "display_name": "generic room",
      "stable_layout_traits": [],
      "stateful_traits": []
    }
  ],
  "props": []
}
```

### Validation

- `stable_visual_traits`와 `dynamic_traits`를 섞지 않는다.
- variant가 얼굴 참조를 공유하는지 여부는 explicit field로 둔다.
- 장소는 physical space와 state를 분리한다.

## Contract 6. Continuity Card

대응 현재 step: `scene_consistency`

목적: 같은 씬의 여러 selected shot에서 변하지 않아야 하는 요소를 고정한다.

### Input

```json
{
  "scene_index": 12,
  "selected_shots": [],
  "shot_staging": [],
  "shot_director": [],
  "scene_text": "...",
  "entity_canon": {}
}
```

### Output

```json
{
  "scene_index": 12,
  "fixed_elements": [
    {
      "element_id": "body_full_pose",
      "element_type": "character_state",
      "bound_entity_id": "C02",
      "description_for_prompt": "an East Asian woman lying motionless on her left side, left arm bent near the face, eyes closed",
      "applies_to_shots": [1, 3, 4],
      "framing_class": "full | upper_body | body_part_detail | environment",
      "must_not_duplicate_with": ["body_wrist_detail"],
      "evidence_refs": [],
      "confidence": "high"
    }
  ]
}
```

### Validation

- `applies_to_shots`는 selected shot subset이어야 한다.
- 같은 entity의 full/body-part fixed element가 같은 shot에 동시에 들어가면 reject.
- 원문에 없는 훼손/피/상처 추가 금지.
- `minItems` 정책이 있으면 schema와 prompt가 일치해야 한다.

## Contract 7. Background Strategy Card

대응 현재 step: `background_classify`

목적: 어떤 location group이 dedicated background를 가져야 하는지 결정한다.

### Output

```json
{
  "building_groups": [
    {
      "group_id": "bg_rooftop_unit",
      "kind": "chain_bg",
      "anchor_loc": "L01",
      "members": [
        {"loc_id": "L01", "label": "small room", "shot_count": 8, "is_indoor": true}
      ],
      "total_shots": 8,
      "rationale": "Indoor recurring location with enough selected shots."
    }
  ]
}
```

### Validation

- 모든 location이 정확히 한 group에 속해야 한다.
- `group_id`는 ASCII snake_case.
- `chain_bg` 기준은 deterministic하게 재계산 가능해야 한다.

## Contract 8. Background Master Plan Card

대응 현재 step: `background_master_plan`

목적: floor plan과 background state chain을 설계한다.

### Output

```json
{
  "group_id": "bg_rooftop_unit",
  "floor_plans": [
    {
      "fp_id": "fp_main_room",
      "sub_location": "main_room",
      "scope": "single small interior room",
      "depends_on_fp": []
    }
  ],
  "backgrounds": [
    {
      "bg_id": "cb_main_room_night_normal",
      "loc_id": "L01",
      "sub_location": "main_room",
      "state_label": "night_normal",
      "depends_on_fp": ["fp_main_room"],
      "depends_on_bg": [],
      "applies_to_shots": ["S12_Shot1", "S12_Shot3"]
    }
  ],
  "gen_order": ["fp_main_room", "cb_main_room_night_normal"]
}
```

### Validation

- `applies_to_shots`는 selected shot universe에 있어야 한다.
- `depends_on_bg`는 같은 sub_location의 이전 bg만 참조.
- `state_label`은 DB 저장 가능 길이 또는 Text field 정책을 따라야 한다.
- `gen_order`는 topological sort로 검증한다.

## Contract 9. Floor Plan Prompt Card

대응 현재 step: `floor_plan_prompt`

목적: background render의 layout reference로 쓸 단순 2D floor plan prompt를 만든다.

### Output

```json
{
  "fp_id": "fp_main_room",
  "t2i_prompt": "Flat top-down schematic 2D floor plan...",
  "key_elements": ["door", "window", "table"],
  "numbered_elements": [
    {
      "number": 1,
      "label": "entrance door",
      "category": "opening",
      "position_hint": "south wall"
    }
  ],
  "camera_recommendations": [
    {
      "bg_id": "cb_main_room_night_normal",
      "sub_location": "main_room",
      "camera_position": "near number 1, facing number 2",
      "camera_height": "eye-level standing",
      "lens_hint": "35mm wide angle",
      "framing_notes": ""
    }
  ]
}
```

### Validation

- Prompt가 floor plan인지 background photo인지 혼동하면 reject.
- numbered elements는 unique.
- 모든 bg_id에 camera recommendation이 있어야 한다.

## Contract 10. Background Prompt Card

대응 현재 step: `background_prompt`

목적: 사람이 없는 empty background image prompt를 만든다.

### Output

```json
{
  "bg_id": "cb_main_room_night_normal",
  "source_language": "ko",
  "t2i_prompt": "실제 카메라로 촬영한 다큐멘터리풍 빈 실내 사진...",
  "ref_guide": "Use as empty room layout and material reference.",
  "shot_guides": [
    {
      "shot_id": "S12_Shot1",
      "guide_text": "Use this as the room reference; the doorway side should remain consistent."
    }
  ]
}
```

### Validation

- 사람/얼굴/시신 직접 묘사 금지.
- floor plan을 top-down으로 복제하라는 표현 금지.
- source language 준수.
- background object 중복을 유도하는 문장 주의.

## Contract 11. Render Prompt Card

대응 현재 step: `scene_detail`

목적: selected shot 1개를 최종 image prompt로 컴파일한다.

### Input

```json
{
  "scene_index": 12,
  "shot_index": 4,
  "shot_candidate": {},
  "shot_selection": {},
  "entity_canon": {},
  "outlook_assignments": {},
  "shot_director": {},
  "shot_staging": {},
  "continuity_card": {},
  "background_binding": {
    "bg_id": "cb_main_room_night_normal",
    "asset_id": "optional",
    "camera_meta": {}
  },
  "provider_rules": {
    "max_people": 3,
    "close_framing_skips_background_ref": true
  }
}
```

### Output

```json
{
  "scene_index": 12,
  "shot_index": 4,
  "scene_type": "normal",
  "representative_moment": "The observer freezes in the doorway.",
  "render_strategy": "direct | simplify | partial_focus | reframe",
  "t2i_variations": [
    {
      "variant_label": "var_1",
      "camera_effect": "eye-level doorway composition",
      "t2i_prompt": "Photorealistic cinematic still. C01O02 in a dark jacket, an East Asian woman, stands at the doorway...",
      "visible_entities": ["C01", "L01"],
      "outfit_assignments": [
        {"character_id": "C01", "outlook_id": "O02"}
      ],
      "background_binding": {
        "bg_id": "cb_main_room_night_normal",
        "reference_usage": "exact_background | atmosphere_reference | skipped_close_framing"
      },
      "continuity_elements_used": ["body_full_pose"]
    }
  ],
  "validation_hints": [
    "C02 body pose must remain unchanged.",
    "Do not create a second door; use the background reference doorway if attached."
  ]
}
```

### Validation

- 시스템 prompt와 schema가 C## vs C##O## 사용 정책을 동일하게 가져야 한다.
- `visible_entities`는 shot_director selected visible subset이어야 한다.
- close framing이면 background ref 관련 문장 금지.
- body-part focus이면 C##/C##O## 금지.
- background object를 새로 생성하는 문장 금지.
- prompt 안 ID와 `outfit_assignments`가 일치해야 한다.

## Contract 12. Asset Readiness Card

대응 현재 구현상 deterministic preflight로 추가 권장

목적: image generation 전에 참조 이미지가 실제로 존재하고 DB와 path가 맞는지 검증한다.

### Input

```json
{
  "render_prompt_card": {},
  "required_refs": [
    {"kind": "character_outlook", "id": "C01O02"},
    {"kind": "background", "id": "cb_main_room_night_normal"},
    {"kind": "prop", "id": "P03"}
  ]
}
```

### Output

```json
{
  "shot_key": "S12_Shot4_var_1",
  "ready": false,
  "resolved_refs": [
    {
      "kind": "character_outlook",
      "id": "C01O02",
      "asset_id": "uuid",
      "file_path": "projects/.../C01O02.png",
      "exists_on_disk": true
    }
  ],
  "missing_refs": [
    {
      "kind": "background",
      "id": "cb_main_room_night_normal",
      "reason": "ImageAsset row missing"
    }
  ],
  "action": "block | partial_without_ref | regenerate_ref"
}
```

### Validation

- 이 단계는 LLM이 아니다.
- path는 project root 기준으로 normalize한다.
- DB row와 disk file 둘 다 있어야 ready.
- 실패를 swallow하지 않는다.

## Contract 간 연결

```mermaid
flowchart TD
    A[Scene Reading Card] --> B[Beat Card]
    B --> C[Shot Candidate Card]
    C --> D[Shot Selection Card]
    C --> E[Entity Canon Card]
    D --> F[Continuity Card]
    D --> G[Background Strategy Card]
    G --> H[Background Master Plan Card]
    H --> I[Floor Plan Prompt Card]
    I --> J[Background Prompt Card]
    F --> K[Render Prompt Card]
    J --> K
    E --> K
    K --> L[Asset Readiness Card]
    L --> M[Image Generation]
```

## 중요한 현실적 선택

Card를 지금 당장 DB 테이블로 만들 필요는 없다. 먼저 checkpoint manifest에 `schema_version`과 `contract_name`을 넣어 저장하고, deterministic validator가 읽도록 만드는 것이 안전하다. 사람이 approve한 결과나 cross-episode canon만 DB로 승격하면 된다.

