# Task 8 리포트 — 엔티티 단계 마네킹 교체 계약 + 콘티 참조 생략

- **커밋**: `2183895f` — `feat(lane): 엔티티 단계 마네킹 교체 계약 + 콘티 참조 생략`
- **브랜치**: `feat/w19-w20-bg-planning-cleanup` (푸시 안 함)
- **베이스**: `6872d15a`

---

## 1. 이 태스크가 닫는 사슬

lane(map_marker) 샷의 확정 흐름은 이렇다.

1. 마네킹 콘티(스케치) — 회색 마네킹이 배치·크기·방향의 권위
2. **Step1** `bg_fill_head` 로 참조 0 i2i 채색 — 배경만 실사화하고
   마네킹은 **보존**한다 ("do not turn them into people")
3. **Step2** 마네킹을 실제 인물로 교체 ← **이번 태스크**

T8 이전에는 3번에 교체 지시가 없어 회색 마네킹이 최종 스틸까지
살아남는 구조였다.

## 2. 신규 스템 — `stage_head_mannequin.md`

`prompts/_base/still_recipe/12.202607270537/stage_head_mannequin.md`
(브리프 원문 verbatim, 11줄).

```
Replace every grey mannequin in the FIRST attached image with the real
person the CHARACTER REFERENCE images show, and output a photorealistic
film still.

KEEP EXACTLY: the background of the first image, the camera framing,
and each mannequin's position, size, pose and the direction it is
turned. A person must stand where their mannequin stood, at the same
scale, facing the same way — never mirrored, never re-staged. No
mannequin, grey figure or sketch line may remain anywhere in the
output.
```

**팩 v13 을 만들지 않은 이유**: `12.202607270537` 은 T7(두 커밋 전)에서
발행됐고 아직 **어떤 런도 소비하지 않았다**. lane 전용 스템이 사는
디렉토리가 여기이고, 덮어쓰기가 아니라 **파일 추가**다(기존 11개 스템
무변경). `bgfirst_lane_pack` 은 이미 config hash 에 스탬프돼 있다
(`image_steps.py:791`) — 팩 디렉토리 이름이 그대로이므로 hash 는
안 움직이지만, 소비 이력이 0 이라 stale CP 재사용 위험이 없다.

**contract bump 안 함**: `BGFIRST_LANE_CONTRACT_VERSION = "1"` 유지 —
같은 이유(미소비 팩). 만약 이 팩으로 이미 런을 돌린 뒤였다면 Step2
산출 실질 변경이므로 bump 가 필요했을 것이다.

## 3. 스템을 **교체**했지 덧붙이지 않은 이유 (T7 리뷰 지적의 본질)

v7 `stage_head` 를 lane 이 그대로 쓰면 두 문장이 능동적으로 **틀리다**.

| v7 문장 | lane 에서의 결과 |
|---|---|
| "keep it EXACTLY: … every fixed feature stay untouched" | 배경본의 **회색 마네킹을 유지하라**로 읽힌다 |
| "The SECOND attached image (LAYOUT SKETCH) tells you ONLY where the people go … Ignore the sketch's background lines." | lane 은 `conti=None` — **그 참조가 아예 없다**. 없는 이미지를 가리키니 모델이 인물 배치를 즉흥으로 지어낼 여지가 생긴다 |

앞에 절을 덧대는 방식이면 두 문장이 그대로 남아 새 계약과 싸운다.
그래서 스템 **파일 자체를 가르는** 분기(`stem = "stage_head_mannequin"
if mannequin else "stage_head"`)로 갔다. 새 스템에는 LAYOUT SKETCH
언급이 없고, 유지 대상이 "the background of the first image"로 좁혀져
마네킹이 유지 대상에서 빠진다.

이 계약을 테스트로 핀했다 —
`test_mannequin_stage_head_points_only_at_attached_refs` 가
`"LAYOUT SKETCH" not in stem` / `"SECOND attached image" not in stem`
을 검사한다(스템을 덧붙임으로 되돌리면 붉어진다).

## 4. 파일별 변경

### `backend/app/modules/pipeline/still_recipe.py`

`build_bgfirst_final_prompt(base_prompt, prompt_version=…, mannequin=False)`
— 인자 1개 추가 + stem 분기 2줄. Korean docstring 에 ①마네킹 보존↔교체
사슬 ②덧붙임이 아닌 교체인 이유 ③base 전문 유지(자세 정본·조명·표정
사실감·소품 계약) ④기본값 byte-identical 을 기록.

`build_bgfirst_refs` 는 **무변경** — T6 에서 이미
`conti: Optional[Path]` + `if conti is not None:` 조건부 슬롯 + 브리프
docstring 문단이 들어가 있다(브리프 Step 4 는 재기술이었고, 확인만 함).
테스트 `test_bgfirst_refs_omits_sketch_slot_when_conti_is_none` 도 이미
존재 — 근사 중복을 만들지 않았다.

### `backend/app/services/still_recipe_service.py`

`_chain_prompt` 조립(`:2599`)을 브리프대로 배선 + Korean 주석.

```python
_chain_prompt = build_bgfirst_final_prompt(
    prompt_chain or prompt,
    prompt_version=(
        _BGF_LANE_PACK_SEL if lane_chain
        else _BGFIRST_PACK
    ),
    mannequin=lane_chain,
)
```

`_BGF_LANE_PACK_SEL` 스코프를 AST 로 확인했다 — import(`body[11]`)와
이 대입(`body[21]`)이 **같은 `if bgfirst_used:` 본문**에 있고 import 가
앞선다. NameError 경로 없음.

비 lane 은 `_BGFIRST_PACK`(=`BGFIRST_PROMPT_VERSION` = `"7"`) +
`mannequin=False` → 인자 기본값과 정확히 같은 조합이라 byte-identical.
(full 경로도 오늘과 동일하게 v7 `stage_head` 를 쓴다 — 오늘의 호출이
`prompt_version` 을 아예 안 넘겨 기본값 v7 이었기 때문. full 스템 팩으로
바꾸는 것은 이 태스크의 범위가 아니다.)

### `backend/tests/unit/test_still_recipe_bgfirst.py`

신규 4건 (기존 파일 말미, `T8` 섹션 헤더 동반).

| 테스트 | 잠그는 것 |
|---|---|
| `test_mannequin_stage_head_replaces_figures` | 브리프 원문 — mannequin/never mirrored/base 전문 유지 |
| `test_mannequin_stage_head_points_only_at_attached_refs` | LAYOUT SKETCH·SECOND attached image 부재, 유지 대상=background, CHARACTER REFERENCE 지시 존재, KEEP EXACTLY 뒤 마네킹 언급 |
| `test_final_prompt_default_stays_byte_identical` | 기본값 == `mannequin=False` == v7 `stage_head`.strip() + "\n\n" + base |
| `test_service_threads_mannequin_stem_for_lane` | 호출부 배선 전문(문 시작 앵커) — 순수 함수 테스트는 기본값 회귀에 전부 초록이라 |

RED 확인: 구현 전 4건 모두 FAIL(`TypeError: unexpected keyword argument
'mannequin'` 3건 + 소스 앵커 부재 1건), `omits_sketch` 1건은 T6 산출이라
이미 PASS.

## 5. 검증

```
$ .venv/bin/python -m pytest tests/unit/test_still_recipe_bgfirst.py \
    tests/unit/test_still_recipe.py tests/core/test_shot_conti_light_lane.py -q
164 passed in 0.47s
```

브리프 Step 7 전체 스윕(+`test_outdoor_marker_map` `test_outdoor_lane_plan`
`test_outdoor_place_spec`): **287 passed**, 실패 0.

서비스를 소비하는 나머지 스위트(`test_multiroll_judge_pack`,
`test_still_recipe_target_scope`): 11 passed.

## 6. lane 샷 Step2 실측 — 프롬프트와 참조가 이제 일치하는가

`build_bgfirst_refs(bg=…, conti=None, char_refs=[…], prop_refs=[…])` 실행:

```
1. [SHOT BACKGROUND] -> <Step1 산출: 배경이 실사화되고 회색 마네킹이 남은 콘티>
2. [CHARACTER REFERENCE — <이름>: the exact person appearing in this shot;
    match face, hair and build exactly.]
3. [PROP REFERENCE — <이름>: the exact object appearing in this shot;
    match its look, material and wear exactly.]
```

(인물 N명·소품 M개면 2·3 슬롯이 그만큼 반복. LAYOUT SKETCH 슬롯 없음.)

스템 = 위 §2 원문 전문, 그 뒤 `\n\n` + base 스틸 프롬프트 전문.

**일치 검증**: 스템이 가리키는 대상은 "the FIRST attached image"(=슬롯 1
SHOT BACKGROUND)와 "the CHARACTER REFERENCE images"(=슬롯 2) 둘뿐이고,
둘 다 실제로 첨부된다. 첨부되지 않는 이미지를 가리키는 문장은 0.
마네킹의 위치·크기·자세·방향은 슬롯 1 안에 그대로 있으므로 "그 자리에,
같은 크기로, 같은 방향으로" 지시가 참조 가능한 근거를 갖는다.

## 7. 우려 / 후속

1. **contract bump 판단이 "팩 미소비" 전제에 의존한다.** 이 브랜치의
   팩 12 로 이미 런을 돌린 이력이 있다면 `BGFIRST_LANE_CONTRACT_VERSION`
   을 올려야 CP 재사용이 무효화된다. 브리프가 미소비를 명시해 그대로
   갔다.
2. **육안 검증 미실시.** 마네킹→인물 교체는 이미지 모델 행동이라
   deterministic 테스트로 증명할 수 없다 — E2E 에서 lane 샷의 최종
   스틸에 회색 형상 잔류가 0 인지, 그리고 인물 **방향**이 마네킹과
   같은지(반전 결함이 이 흐름의 원래 동기) 육안 확인이 필요하다.
3. **Step2 스템의 "grey mannequin" 이 Step1 산출 실물과 맞물리는지**는
   Step1 이 마네킹을 실제로 회색으로 남겼을 때만 성립한다. Step1 이
   마네킹을 부분적으로 인물화해 버리면 Step2 의 "every grey mannequin"
   이 일부를 놓칠 수 있다 — 같은 E2E 관찰 항목.
4. lane 이 아닌 full 경로는 여전히 v7 `stage_head` 를 쓴다(오늘과 동일).
   팩 11/12 에 `stage_head` 사본이 없어서 selector 를 올리면 즉시
   FileNotFoundError 다 — 의도적으로 건드리지 않았다.

## 캔ary 전 수정

유료 이미지 캔ary 직전, 리뷰 지적 6건을 팩 `12.202607270537` **제자리**
수정으로 반영했다(이 팩은 어떤 런도 소비한 적이 없어 v13 신설 불필요).

### F1 — 체인 Step2 가 첨부되지 않은 이미지를 가리키던 결함 (코드 변경)

체인 저작 base 는 `prev_used=False` / `lane_ref_mode=""` 로 조립돼
`build_still_prompt` 의 **기본 fallback** 을 탔다:

> `... The shot takes place here — the attached LOCATION PHOTOGRAPH shows the exact spot.`

lane Step2 참조는 `[SHOT BACKGROUND, CHARACTER/PROP REFERENCE]` 뿐이라 그
사진은 어디에도 없다. v3 팩이 lane 샷에 대해 한 번 제거한 모순
(`PROMPT_VERSION_MAP` "존재하지 않는 LOCATION PHOTOGRAPH 참조 모순 제거")
이 체인 경로로 재발한 것이다.

- 새 스템 `location_lock_chain_bg.md` (팩 12): 첫 참조(SHOT BACKGROUND)가
  **이 장소 자체이며 이미 지어져 있다**, 사진은 첨부되지 않았다, 그
  이미지에 없는 지형·구조물·차량·설비를 더하지 말라 — 단, 이 lock 은
  장소만 관할하고 인물은 staging/pose 지시를 따른다(마네킹 교체와 충돌
  방지).
- `build_still_prompt(chain_bg_mode=False)` 인자 추가. 스템은 샷 팩이
  아니라 전용 selector `CHAIN_BG_LOCATION_PROMPT_VERSION = "12"` 로만
  로드한다 — v6 `camera_frame_clause` / v8 `lighting_mood_clause` 단일
  스템 팩 선례 동형(체인 저작 base 는 팩 "1" 이라 팩 경유 로드가 불가).
- 다른 권위 모드(prev/lane/seed_bg/structure_seed)와 **상호 배타**
  (ValueError). 기본값 False = 조립 byte-identical, 테스트로 잠금.
- 배선은 `still_recipe_service.py` 체인 저작 1곳뿐. 브리프 지시대로
  **비-lane bgfirst Step2** 의 같은 문제(체인 미저작 샷이 `prompt` 를
  그대로 Step2 로 보내 LOCATION PHOTOGRAPH 를 가리키는 경우)는 범위 밖으로
  두었다 — 아래 우려 1.
- config_hash: 스템이 팩 12 안에 있어 `bgfirst_lane_pack` 스탬프
  (`still_lane_prev_bgfirst_enabled` 게이트 = 체인 저작이 열리는 유일한
  조건)가 그대로 덮는다. 별도 bump 불요.

### F2~F5 — `stage_head_mannequin.md` 재작성

| 지적 | 이전 | 이후 |
|---|---|---|
| F2 단수 대상 + 배정 규칙 부재 | "the real person the ... **images** show" | "the real **people**" + "Each mannequin becomes the character whose POSE CANON and the shot text match that figure's pose and position; never repeat one person across two mannequins." |
| F3 "must stand" 가 immobile 계약과 충돌 | "A person **must stand** where their mannequin stood" | "A person must **be exactly where** their mannequin was, at the same scale, **in the same pose**, facing the same way — ... never straightened up, stood upright or otherwise re-posed." |
| F4 CHARACTER REFERENCE 0 장 가능 | (전제) | "If no CHARACTER REFERENCE image is attached for a figure, still replace it with a plausible real person consistent with the text." |
| F5 "grey" 단일 키 + 지우기 우회 | "every **grey** mannequin" | "every **grey or outlined mannequin figure**" + "erasing a figure, or leaving one out, is not a replacement, and the number of figures never changes." |

F4 는 pose-locked 인물이 `char_refs` 에서 제외되는데(`still_recipe_service
.py:1954-1961`) lane 체인엔 prev 스틸도 첨부되지 않아 참조 `[SHOT
BACKGROUND]` 단독으로 Step2 에 도달할 수 있는 실경로를 막는다 — 그
경우의 최악이 "마네킹 유지"라 이 작업의 목적 자체와 충돌했다.

### F6 — 테스트 2건이 읽히는 것보다 약했던 문제

- `test_mannequin_stage_head_points_only_at_attached_refs`:
  `low.split("keep exactly", 1)[-1]` 는 구분자가 **없으면 원문 전체**를
  돌려주고 1문단에 이미 "mannequin" 이 있어, `KEEP EXACTLY` 를 통째로
  지워도 초록이었다(vacuous). → `assert "KEEP EXACTLY" in stem` 선행 +
  `[1]` 슬라이스 + 잔류 0 문장 토큰까지 검사.
- `test_mannequin_stage_head_replaces_figures`: 방향 계약을
  `"never mirrored"` 하나로만 핀해, "facing the same way"·"position"·
  "size"·"same scale" 을 전부 지워도 초록이었다. → 5개 실질 토큰 +
  `"must stand" not in low` + F2/F4/F5 문구를 각각 검사.
- 스템이 하드랩이라 토큰이 줄바꿈을 가로지른다 — 두 테스트 모두
  `" ".join(p.split())` 공백 정규화 후 검사하도록 바꿨다.

신규 5건: `chain_bg_mode` 의 tail 교체 / **off = byte-identical**(lane·
prev 경로 포함) / 상호 배타 3종 / 전용 selector 로드 / 서비스 배선 핀.

### 검증

```
$ .venv/bin/python -m pytest tests/unit/test_still_recipe_bgfirst.py \
    tests/unit/test_still_recipe.py tests/core/test_shot_conti_light_lane.py -q
169 passed in 0.60s
```

`tests/unit tests/core tests/pipeline` full = 4513 passed / 10 failed —
10건 전부 **변경 전 HEAD 에서도 동일하게 실패**(stash 대조 확인, d6 hash·
visible_entities·outdoor_place_spec 등 무관 계열).

**F1 수용 — 합성 lane 샷 Step2 프롬프트 전문 대조**: 조립된 프롬프트가
이름으로 부르는 이미지는 `SHOT BACKGROUND`(슬롯 1)와 `CHARACTER
REFERENCE`(슬롯 2·3) 둘뿐이고 둘 다 실제 첨부다. `LOCATION PHOTOGRAPH`,
`LAYOUT SKETCH`, `PREVIOUS SHOT STILL`, `STRUCTURE LOOK`, `STORYBOARD
SKETCH` 는 전문에 **0회** 등장한다. (`PROP REFERENCE` 는 첨부되지만 본문이
호명하지 않는다 — 라벨 자체가 설명을 담아 무해.)

### 우려 / 후속

1. **비-lane bgfirst Step2 는 여전히 같은 모순을 안고 있다.** 체인 저작이
   없는 샷(`prompt_chain == ""`)은 `prompt` 를 그대로 Step2 롤에 쓰는데,
   그 base 는 LOCATION PHOTOGRAPH/STRUCTURE LOOK 을 가리키면서 Step2
   참조엔 그 슬롯이 없다. 브리프 지시로 범위 밖 — 별도 작업 필요.
2. **F3 은 문구 충돌만 제거했다.** 앉음/쓰러짐 마네킹이 실제로 그 자세로
   유지되는지는 이미지 모델 행동이라 캔ary 육안 항목이다.
3. **F2 배정 규칙은 POSE CANON 이름↔마네킹 대응을 모델 추론에 맡긴다.**
   스케치 단계에서 식별 정보가 의도적으로 파괴되므로(`bg_fill_tail` 의
   텍스트·마커 전면 금지) 결정론 채널이 없다 — 인물 2명 이상 lane 샷에서
   인물이 서로 뒤바뀌는지가 캔ary 최우선 관찰 항목.
