# BGFIRST2 레시피 프로덕션 이식 구현 플랜

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (inline). Steps use checkbox (`- [ ]`) syntax.

**Goal:** 2026-07-20 사용자 확정 3건 — ①배경 렌더 무인(NO PEOPLE) 계약 강제 ②콘티 사용 샷을 BGFIRST2 2단 체인(GPT 원근 콘티 → Step1 GPT 재투영 빈 배경 → Step2 nb2 인물 삽입)으로 교체 ③롤 구조=콘티 체인 1장 vs 무콘티 1장 → VLM 2택1(기존 4택1 대체) — 을 flag 게이트로 production 파이프라인에 이식한다.

**Architecture:** 실증 정본 = `exp_bgfirst_v2_gpt.py` + `exp_chainA_true_bgfirst.py` + `exp_conti_persp.py`(0adc4df1… scratchpad). 이식 위치 = `shot_conti_light`(콘티 저작 계약 v2·GPT 엔진·참조 확장) + `still_recipe_service`(Step1 GPT 재투영 + 2택1 롤 구조) + `background_render`(무인 계약). 판정·조립 기계는 기존 `multiroll_select`(roll_prompts/roll_refs/judge_flip) 재사용.

**Tech:** gpt-image-2(콘티 저작·Step1 재투영), nb2(Step2 인물 삽입·무콘티 후보), Gemini(판정·결함검사).

## Global Constraints (전 Task 공통)

- **flag default OFF = 기존 경로 byte-identical** (프롬프트·지문·config_hash 전부 무변경, opt-in stamping 관례).
- 프롬프트 = **새 버전 디렉토리** (`N.YYYYMMDDHHmm`), 기존 팩 덮어쓰기 금지.
- 시나리오 의존 코딩 금지·글자/substring 의미 판단 금지 (의미 판정=LLM/VLM).
- LLM/VLM 입력 원문 절대 자르지 않기.
- 모델 분업: 선화·재투영(스케치-사진 정렬)=gpt-image-2 / 최종 실사=nb2 / 판정=gemini.
- pipeline/ 기존 모듈 삭제 금지. scene_still sync UPSERT 유지.
- 개발 사이클: 설계(본 문서) → 구현 → Codex 리뷰 → 테스팅 → 수정 → 커밋·푸시(자율).

## 신규 flag (app/core/config.py, 전부 default False)

| flag | 소비처 | 의미 |
|---|---|---|
| `background_no_people_enabled` | background_render.render_one_background | 배경 렌더 프롬프트에 무인 계약 절 강제 주입 |
| `background_no_people_gate_enabled` | 동일 (전자 ON 전제) | 렌더 후 Gemini VLM 무인 판정 → 위반 시 1회 재렌더 + 감사 기록 |
| `still_bgfirst_enabled` | shot_conti_light_step + still_recipe_service | ②콘티 v2(원근 가이드·GPT) + ③2단 체인·2택1 |

`still_bgfirst_enabled` 하나로 콘티 저작 v2와 스틸 체인을 함께 게이트한다(두 스텝 계약이 한 레시피의 상·하류라 분리 flag 는 드리프트 위험). 콘티 스텝 config_hash·스틸 extra_fingerprint 양쪽에 ON 시만 스탬프.

---

### Task 1: 배경 렌더 무인 계약 (①)

**Files:**
- Create: `prompts/_base/background_unmanned/1.YYYYMMDDHHmm/{no_people_clause.md, no_people_retry.md, gate_judge_sys.md}`
- Create: `backend/app/modules/pipeline/background_unmanned.py`
- Modify: `backend/app/modules/pipeline/background_render.py` (render_one_background 서두)
- Modify: `backend/app/core/steps/background_render_step.py` (_config_hash opt-in 스탬프)
- Test: `backend/tests/unit/test_background_unmanned.py`

**설계:**
- L09B01 실측: LLM 저작 배경 프롬프트가 인물 서술을 포함하면 플레이트에 인물이 구워짐(무인 계약 위반, 하류 스틸 전체 오염). 근본 강제 지점=렌더 경로 단일 seam.
- `render_one_background` 함수 서두(= plate_multiroll 위임 **이전**)에서 flag ON 시 `prompt = prompt + "\n\n" + no_people_clause` — 단롤 gpt 경로와 nb2 multiroll 경로가 같은 계약을 상속. 절 내용(영문, 팩 스템): 배경 플레이트=무인 계약 — no people, no figures, no faces, no body parts, no silhouettes/reflections/shadows of people; 인물 서술이 프롬프트에 있어도 "그 인물이 없는 상태의 장소"만 그린다.
- **무인 VLM 게이트**(별도 flag, 검토 결과=구현하되 기본 OFF): 렌더 성공 후 `judge_unmanned(png)` — Gemini structured vision 판정 `{people_visible: bool, evidence_ko}` (gate_judge_sys.md, 하드코딩·substring 없음). 위반 시 `no_people_retry` 절(에스컬레이션 문구)을 덧붙여 **1회 재렌더** → 재판정. 그래도 위반 = 렌더는 ok 유지 + `info["people_detected"]=true` 감사 기록·warning (플레이트 실패가 에피소드 전체를 막지 않도록 fail-open+audit — Codex 리뷰 논점으로 명시).
- 지문·hash: flag ON 시 `background_render_step._config_hash` 에 `background_no_people_pack`(+gate 시 judge 모델) 스탬프. plate_multiroll 지문은 prompt 자체가 바뀌므로 자동 기여.

**Steps:**
- [ ] 팩 스템 3종 작성 (영문 정본, 범용 — 장소·인물 고유명 0)
- [ ] `background_unmanned.py`: `resolve_prompt_version`, `build_no_people_clause()`, `build_retry_clause()`, `judge_unmanned(png_path, project_config) -> dict` (call_structured vision)
- [ ] `render_one_background` 배선 (+`info` 키 3종: `no_people_clause_attached`, `unmanned_gate`, `people_detected`)
- [ ] step config_hash opt-in 스탬프
- [ ] 유닛: OFF byte-identical(프롬프트 불변)·ON 절 부착·게이트 위반→재렌더 1회→감사 기록 (fake judge/gen)

### Task 2: 콘티 저작 v2 — 원근 가이드 GPT 콘티 (②전반)

**Files:**
- Create: `prompts/_base/shot_conti_light/2.YYYYMMDDHHmm/` — v1 스템 5종 사본(무변경) + `perspective_guides.md`(정본 PERSPECTIVE_GUIDES) + `ref_header_plate.md` + `ref_header_plate_char.md`(GPT ref 서두 설명)
- Modify: `backend/app/modules/pipeline/shot_conti_light.py`
- Modify: `backend/app/core/steps/shot_conti_light_step.py`
- Test: `backend/tests/unit/test_shot_conti_light_persp.py`

**설계 (정본 = exp_conti_persp.py + exp_chainA_multi.py 콘티부):**
- `PROMPT_VERSION_MAP += {"2": "2.YYYYMMDDHHmm"}`, 스텝 selector: flag ON→"2", OFF→"1".
- `build_conti_prompt` v2 조립 = v1 순서 그대로 + 끝에 `camera_frame_en`(build_camera_frame_clause 렌더 완문, staging 부재=생략) + `perspective_guides` 스템. 신규 kwargs `camera_frame_en: str = ""` — v1 에선 항상 "" 전달(byte-identical).
- **THE LOCATION = 샷별 서브공간**: `run_shot_conti_light` 에 `shot_place_by_tag: Dict[str,str]` 추가 — 스텝이 classify v3 `shots[tag].place_en → scenes[si].place_en → location_by_scene` 우선순위로 구성해 전달. flag OFF = 기존 location_by_scene 그대로.
- **참조 확장**: `ContiGenFn` 을 `(tag, prompt, labeled_refs: List[(label, Path)], out_path)` 로 리팩토링(OFF 경로는 `[("plate", plate)]` 전달 → 동일 GPT 호출·산출 불변). flag ON: `[plate] + 캐릭터 기본 참조 ≤2장`(비례 참조 — 실측: 엔티티 참조 시 선화 계약 안정). GPT 프롬프트 서두에 ref_header 스템(플레이트만/플레이트+캐릭터 2종 분기 — 조건은 참조 유무, 의미 판단 아님).
- 캐릭터 참조 소스: 스텝에서 scene_still rows 의 visible_entities_json → character 엔티티 → primary reference ImageAsset 경로 (기본 모습=outlook v14 보편 계약). state/composite 해석은 콘티에 불필요(비례 참조 목적) — base primary 로 충분.
- 스텝: `shot_staging` CP 로드 → `build_camera_frame_clause`(still_recipe 재사용, 결정론). 지문: labeled_refs 확장이 compute_input_fingerprint 에 자동 기여 + config_hash 에 `still_bgfirst_enabled/conti 팩 v2/camera_frame 팩` 스탬프.

**Steps:**
- [ ] 팩 v2 디렉토리 작성 (v1 사본 + 신규 3스템 — PERSPECTIVE_GUIDES·ref header 정본 이식)
- [ ] `shot_conti_light.py`: MAP·시그니처 확장(camera_frame_en, shot_place_by_tag, labeled_refs)·조립 v2
- [ ] 스텝: staging 로드·shot_place 구성·char ref 로더·gen_fn labeled_refs 화·config_hash 스탬프
- [ ] 유닛: OFF 조립 byte-identical·v2 조립 순서(cam→guides 말미)·place 우선순위·ref header 분기·지문 변화

### Task 3: 스틸 2단 체인 + 2택1 롤 구조 (②후반+③)

**Files:**
- Create: `prompts/_base/still_recipe/7.YYYYMMDDHHmm/{bg_reproject_head.md, bg_reproject_tail.md, stage_head.md, bg_label.md, sketch_label.md, judge_header.md}`
- Modify: `backend/app/modules/pipeline/still_recipe.py`
- Modify: `backend/app/services/still_recipe_service.py`
- Test: `backend/tests/unit/test_still_recipe_bgfirst.py`

**설계 (정본 = exp_chainA_true_bgfirst.bg_prompt/final_prompt + exp_bgfirst_v2_gpt 체인):**
- still_recipe.py 신규 (팩 7 전용, 기존 팩 1~6 무변경):
  - `BGFIRST_PROMPT_VERSION = "7"`, `BGFIRST_CONTRACT_VERSION = 1`, MAP += {"7": …}
  - `build_bgfirst_bg_prompt(shot_desc, place_text, time_of_day_en, camera_frame_en)` — 정본 bg_prompt 조립: head(무인+콘티 카메라 권위·플레이트 재질 소스·재투영 계약) + SHOT TEXT/LOCATION/TIME/CAMERA 절 + tail(16:9 빈 실사 배경).
  - `build_bgfirst_final_prompt(base_prompt)` — 정본 final_prompt: stage_head(배경 EXACTLY 유지·스케치=인물 배치만·선 잔류 금지) + base 스틸 프롬프트 전문.
  - `build_bgfirst_refs(bg, conti, char_refs, prop_refs)` — `[(bg_label, bg), (sketch_label, conti), *char, *prop]` (라벨=팩 스템 정본: "SHOT BACKGROUND" / "LAYOUT SKETCH (people placement only)").
  - `bgfirst_eligible(*, conti, bg_only, prev_used, lane_used, complex_ab, structure_seed, seed_bg) -> bool` — 순수 판정: 콘티 실재·비bgonly·비prev·비lane·비complex(구조물 seed/seed-bg 파이프는 2026-07-16 사용자 확정 별도 계약 유지).
- 서비스 (flag ON + eligible 샷 한정, 그 외 전부 기존 분기 그대로):
  1. **Step1 재투영**: OpenAI client + `call_gpt_image_bytes(mode="edit", ref_paths=[conti, plate], size="1536x864", quality="high")`, 프롬프트=`build_bgfirst_bg_prompt`(place=cls.place_en 우선, tod=classify scenes, cam=camera_frame_en — 기존 fix1/fix2 배선 재사용). 산출=`recipe_dir/{tag}__bgfirst_bg.png`. 재개=records `{tag}::bgfirst_bg` 지문(compute_input_fingerprint: 프롬프트+콘티/플레이트 내용+gpt 모델) 일치 시 재사용, mismatch/force=stale 아카이브 후 재생성. moderation=sanitizer 1회 재시도. capture(capture_role="still_recipe_bgfirst_bg", still_id·input ids)로 intermediate ImageAsset+lineage(conti·plate→bg).
  2. **2택1**: `_run_branch(f"still_{tag}", refs_b, tag, recipe_dir/tag, rc=2, jf/jt/cf=count2 어댑터, roll_prompts={"A": build_bgfirst_final_prompt(prompt), "B": prompt}, roll_refs={"A": bgfirst_refs, "B": refs_b}, parallel_rolls=True, judge_flip=True, flip_priority=["A","B"](동점=체인 우선 — tie_keeps_conti 정책 연속), critique_selected_prompt_only=True, judge_prompt_header=팩7 judge_header)`. refs_b=`build_ab_branch_refs` 의 무콘티 브랜치(judge 공유 refs=블라인드). count=2 judge/critique 어댑터 생성 조건을 `variants_on or bgfirst_on` 으로 확대.
  3. **변형 저작 skip**: eligible 샷은 still_variants 4택1 경로 대신 본 경로(사용자 확정 "이전 2장씩 대체"). 비콘티 샷(prev/bgonly/lane/complex)=기존 유지.
  4. **record/lineage**: `record["bgfirst"]={bg_path, winner}`, ref_mode="재투영 배경+콘티+엔티티 (2택1: 체인 승·무콘티 승)". 승자 A=conti+bg asset attach(role: conti_light, bgfirst_bg) / 승자 B=plate attach(기존). 지문 extras: bgfirst 팩·contract·gpt 모델(Step1 엔진).
- 주의: Step1 프롬프트·체인은 콘티 v2 산출 전제(같은 flag) — 콘티 CP 팩 버전이 v2 인지 검증(불일치=해당 샷 fail-closed, 조용한 혼용 금지).

**Steps:**
- [ ] 팩 7 스템 6종 (정본 영문 이식, 범용)
- [ ] still_recipe.py 헬퍼 5종 + 상수
- [ ] 서비스 배선 (Step1 지문·아카이브·sanitizer·capture / 2택1 / 변형 skip / lineage / fingerprint / fail-closed)
- [ ] 유닛: eligibility 전 조합·bg_prompt/final_prompt 조립 정본 일치·refs 순서·OFF byte-identical·2택1 배선(mock run_fn — roll_prompts/roll_refs/flip 인자 검증)·콘티 팩 불일치 fail-closed

### Task 4: 회귀 + 실측 canary

- [ ] 영향권 유닛 전체 (`pytest backend/tests/unit -k "still or conti or background or multiroll"`) + full 스위트 1회 — 기존 실패군 대비 신규 실패 0 확인
- [ ] E2E9 자산(358f9d88…)으로 canary: 신경로 재현 스크립트(scratchpad, 커밋 금지)로 S19sh4·S22sh2·S8sh4 — production 코드 경유 산출이 실험 정본과 계약 일치(콘티 v2 프롬프트·Step1 GPT·Step2 nb2·2택1 record)하는지 실측, 갤러리 행 추가 + LAN 링크 보고

### Task 5: Codex 리뷰 → 수정 → 커밋·푸시

- [ ] 구현 diff Codex 코드 리뷰(tmux 규약) → blocking 반영 → 재리뷰
- [ ] 테스팅 완료 후 커밋·푸시 (flag OFF 안전 — 자율 진행 규약)

## Self-Review 체크
- 스펙 3건 ↔ Task 1/2+3/3 매핑 완료. 비콘티 샷 기존 유지=eligibility 게이트. 4택1 대체=Task 3-2·3-3.
- 타입 일관: ContiGenFn labeled_refs 화는 스텝 gen_fn 과 동시 수정(호출부 2곳: run_shot_conti_light 내부 + 테스트 fixture).
