# CARRIED STATE 가 그 샷에 없는 인물을 「여전히 있다」고 말한다 (1-B)

> ★**이 문서는 처음에 엉뚱한 결함을 1-B 라고 적었다.** 감사 보고서를
> 다시 읽고 다시 썼다. 앞 판은 「무인 샷 + CARRIED」를 다뤘는데, 보고서의
> 1-B 는 **인물 샷**에서 PEOPLE 목록 밖 인물이 주입되는 것이다. 무인 샷
> 건은 실재하지만 별건이고, 이 문서 아래쪽에 함께 적는다 — 뿌리가 같다.

## 결함

한 프롬프트 안에서 두 절이 맞선다.

    PEOPLE: … IF a person appears, they must be one of: 연희
            — never anyone else, and never add a person the shot text
              does not show.
    CARRIED STATE (persist exactly …): Behind Yeonhui, the workbench
            remains shifted aside and the old man remains lying under the
            blanket …, with Minsu crouched beside him.

「연희만」과 「노인·민수도 여전히 있다」가 같은 글에 있다. 모델은 둘 중
하나를 고른다.

★**08-26 reve 변환 반려 사유가 「같은 두 남성이 여러 번 반복」이었다.**
프롬프트가 실제로 그렇게 말하고 있었다.

## 실측 (2026-08-27) — 그림은 한 장도 안 샀다

`backend/tools/prompt_measure/audit_carried_offscreen_people.py`

허용 목록은 **나간 프롬프트 안에 데이터로 들어 있다** — 코드가 인물
이름을 알 필요가 없다. 목록을 그대로 모델에게 넘기고 「이 문안이 말하는
사람이 이 목록 안에 있나」만 묻는다.

**대조를 먼저 태운다.** 양성 2(명백히 목록 밖) · 음성 2(사진 속 인물·
사물만) → **4/4 통과**. 어긋나면 본 표본을 재지 않고 멈춘다.

    골목 끝 판(3씬, da049582)   7샷 중 **4샷(57%)** 이 목록 밖 인물을 말한다

    S1sh2  허용=연희      → 목록 밖 Minsu
    S2sh1  허용=노인      → 목록 밖 Yeonhui, Minsu
    S3sh1  허용=민수,노인  → 목록 밖 Yeonhui
    S3sh2  허용=연희      → 목록 밖 the old man, Minsu

보고서는 5샷이라 했고 4샷이 나왔다. 겹치는 것이 셋이고 서로 하나씩
다르다 — **결함과 규모는 확인되고**, 낱개 샷의 판정은 갈릴 수 있다.

**골목 끝이 특이한 판은 아닌가** — 큰 판들에서 가장 많이 반복된 짝 60개를
표본으로 뽑아 같은 판정기로 잰다(대조 다시 4/4 통과).

    짝 60개 중 목록 밖 **15개(25%)** · 샷 302개 중 **68개(23%)**

골목 끝(57%)이 높은 쪽이다 — 3씬짜리라 같은 인물이 계속 겹친다. 전
저장소로는 **인물 샷의 4분의 1** 가까이가 목록 밖 인물을 말한다.

    S18sh2  허용=수리영  "…empty of Minsuk, and Su-ri-yeong still has…"
    S28sh5  허용=인우    "Su-ri-yeong still possesses the old photograph"
    S16sh3  허용=혜수    "…with Minsuk's saved number displayed for Detective Kim"
    S10sh5  허용=민숙 외 "…as the intruder attacks and bites into her shoulder"

마지막 둘은 사람이 **받는 쪽·행위자**로만 나오는데, 이미지 모델에게는
「그 사람도 프레임에 있다」로 읽힌다.

## 원인 — 팩이 씬 단위로 저작하게 하고, 「보이는 인물만」 제약이 없다

`prompts/_base/shot_continuity/2.202607161500/carried_system.md`

    1) 대상 = 샷 사이에 지속되는 시각 상태: 움직일 수 없는 인물
       (사망·의식불명)의 자세·위치와 그가 쥐거나 걸친 물건, 공간에
       남는 상태물(표식·흔적·물건의 위치), 인물이 계속 지니는 소지물.
    2) ★양방향 전파: 어떤 샷에서 확정되는 상태가 물리적으로 그
       이전부터 존재했어야 하면 … 이전 샷들의 carried 에도 소급해
       명시하라.

양방향 전파가 씬 전체로 퍼뜨리는 장치이고, **그 샷에 보이는 인물만**이라는
말이 어디에도 없다. 조립도 인물 필터 없이 붙인다.

> 보고서는 조립부를 `still_recipe.py:1752-1756` 이라 적었는데 지금 그
> 줄은 다른 코드다 — 파일이 그 뒤로 커졌다. 현재 조립은
> `still_recipe.py:1880-1884`.

## 계약 (2026-08-27 확정 — Codex 와 두 판 의논)

★**자가신고는 계약이 아니다** (표기 저작 v3 에서 배운 것). 모델에게
「이 사람은 이 샷에 보인다」를 신고시키지 않는다. 모델은 **누구의 무슨
상태**만 말하고, **화면 안인지는 코드가 정한다.**

**옆 산출에 이미 같은 장치가 있다** — `pose_canon` 이 그렇다. 저작
함수가 `char_name_to_short` 를 이미 받고(`shot_continuity_author.py:61`),
정본 명단을 프롬프트에 넣고(`:112`), 모델이 이름으로 답하면 코드가
**정확 일치**로 short_id 를 붙인다(`:140-141`, 부분매칭 금지).
`carried` 만 그 계약 밖에 있었다.

### 저작 산출 (팩 v3)

    objects_en             그 샷에 저작된 사물·설치물·흔적·날씨·빛
    people[]               { character_ko, state_en, basis_ko }
    offscreen_effects_en   화면 밖 원인의 **결과가 화면 안에 보일 때**

팩에 **정본 명단**을 넣는다(pose_canon 과 같이).

### 코드가 저작 직후

    character_short_id = char_name_to_short.get(character_ko)   정확 일치

못 맞추면 **기록만 하고 주입하지 않는다.** 「모르겠으니 넣는다」로 가면
이 판이 막으려던 것이 그대로 돌아온다.

### 소비 — 중앙 helper 하나

    carried_text_for_shot(row, *, visible_short_ids, bg_only, pack_selector)

순서는 `objects → visible_people → offscreen_effects` 로 고정. 네 조립부는
**만들어진 한 문자열만** 받는다 — 각자 이어 붙이면 한 곳이 빠진다.

    인물 샷   objects + (visible_short_ids 안의 사람만) + effects
    bg_only   objects + effects        ← 사람 칸은 통째로 안 붙는다

### ★추측 집합을 visible 판정에 쓰지 않는다

`ve_ids_for_shot_ex`(`still_recipe.py:977-993`)는 샷 VE 가 비면 **같은 씬
다른 샷의 합집합**을 돌려주고 `exact=False` 를 함께 준다. 그 추측을
visible 로 쓰면 이 판이 막으려는 것이 그대로 돌아온다. 그 fallback 은
PEOPLE 절의 배타 조항을 뗄지 정하는 용도이지 **사람을 넣을 권한이
아니다.** exact 목록이 없으면 사람은 0명이다.

### 옛 값 — fail-closed

「옛 그림은 이미 나왔다」와 「옛 CP 가 앞으로 안 쓰인다」는 다르다.
사용자가 `scene_image`/`conti` 만 force 하면 옛 한 칸으로 **새 그림을
다시 산다.** 그래서 데이터에 계약 표시를 넣고 SCHEMA 를 올린 뒤, 한
칸짜리 옛 값을 만나면 **제공자 호출 전에** 「`shot_continuity` force
재실행 필요」로 멈춘다. 이미 만들어진 자산을 열어 보는 것은 안 막는다.

### ★`still_recipe_service.py:2741` 은 **모든 칸이 아니다**

앞 판에 「사람 판정에 쓰이니 모든 칸을 다 넣어야 한다」고 적었는데
**틀렸다.** 그 자리는 밀폐 공간 적용 여부를 묻는다 — 목록 밖 사람의
좌석·위치를 넣으면 판정이 참이 되어 **1-B 가 다른 길로 재현된다.**
렌더와 같은 것만 넣는다: objects + visible people + 검증된 effects.

## 별건 — 무인 샷에도 사람 얘기가 나간다

`backend/tools/prompt_measure/audit_person_clauses_in_bgonly.py` (19판 전수)

    무인 조항이 든 프롬프트 1,831건 · 무인 샷 882개
    CARRIED 동반   프롬프트 451건(25%) · **샷 212개(24%)**
    MOVEMENT 0건 · FIGURES 0건

★조립부만 보고 「셋 다 나갈 것」이라 의심했는데 실측은 CARRIED 하나였다.
`bg_only` 가 끄는 것은 `pose_clauses` 뿐이지만, `movement_en`·`figures_en`
은 애초에 무인 샷에 값이 없다.

고유 CARRIED 문안 **210개 전수**를 「사람의 몸을 말하는가」로 갈랐다.

    사람만    16개  ( 8%)
    섞임     128개  (61%)
    사물만    66개  (31%)
    ─────────────────────
    사람을 말한다  144개 (69%)

무인 샷 212개 × 69% ≈ **146샷**이 「무인인데 사람 얘기」다.

★**「통째로 빼기」가 왜 나쁜 안이었는지도 여기서 보인다** — 61%가 사물과
사람을 **함께** 말한다. 통째로 빼면 그 샷들의 사물 연속성을 다 잃는다.
세 갈래로 나눈 것이 맞다.

사물만 말하는 문안이 실제로 섞여 있다:

> ★**「gemini-flash 판정」이라 부르면 기록 오독이다** (Codex 지적).
> 이 도구가 `call_structured` 의 후퇴 사다리를 안 껐고, 실제 로그에
> `gemini-flash` 빈 응답 → 정화 실패 → **GPT 후퇴**가 찍혔다. 폭력
> 묘사가 검열에 걸린 것이다. 수치는 **여러 제공자가 섞인 것**으로
> 읽어야 한다. 이 판의 계약 판단에는 영향이 없다 — 프로덕션 저작은
> 어차피 gpt 이고, visible/offscreen 은 모델이 아니라 코드가 id 로
> 정하기 때문이다.

    문제 없음   "The rusty iron entrance door remains half open."
    문제 있음   "Jihu still wears her antler-bone pendant."

위 계약이 이것도 함께 닫는다 — bg_only 는 `people[]` 을 통째로 안 붙인다.

## 소비 넷 — 재료가 다 있는가

    ① 최종 스틸   still_recipe_service.py:2969-2979 의 `ve_detail`/
                  `sid_by_eid` 에 **exact short_id 가 이미 있다**
    ②③ 마커 맵   shot_conti_light_step.py 안에서 불린다
    ④ 일반 콘티   같은 스텝

★②③④ 는 **그대로는 부족하다.** `_char_refs_by_tag()` 가 SceneStill VE 를
읽기는 하는데 ㉠ 이름+참조 경로만 돌려주고 **short_id 를 버리며**
㉡ `_selector != PROMPT_VERSION` 일 때만 불린다. visibility 계약에 쓸 수
없다. 별도 loader 를 **항상** 만들고, 참조 이미지 유무와 visibility id 를
**묶지 않는다** — 참조가 없어도 그 사람은 화면에 있다.

그러면 `shot_conti_light` 가 `scene_detail` 산출을 계약상 직접 소비하게
되므로 `step_manifest.py` 의 `depends_on` 에 `scene_detail` 을 넣어야
한다. 순서만 믿으면 `scene_detail` 을 다시 돌려도 콘티가 낡은 채로 남는다.

## 곁가지로 나온 결함 — `fix or base` 가 빈 값을 되살린다

    fix.get("carried_en") or (carried.get(tag) or {}).get("carried_en") or ""

`pose_fix` 가 **일부러 비운** 칸이 옛 값으로 되살아난다. 새 계약에서는
`fix` 가 있으면 **레코드 전체**를 고른다.

## 우회가 남는가 (2026-08-27, Codex 요청)

사람 칸을 코드가 걸러도 **모델이 사람 얘기를 다른 칸으로 흘려보내면**
같은 결함이 다른 길로 돌아온다. 옛 판에서 목록 밖 인물을 말한 12건을
세 갈래로 나눈 뒤 **최종 조립 문자열**을 같은 판정기에 태웠다.

    전(옛 계약)   12/12 목록밖
    후(v3 계약)    5/12 목록밖   (58% 줄었다)

남은 5건은 두 부류다.

**㉠ 소유격** — `her backpack`, `from Jihu's meal`. 사물을 말하면서
사람을 지목한다. **그대로 둔다** — 그것은 배낭을 그리라는 말이지 사람을
그리라는 말이 아니고, 막으면 사물 연속성이 흐려진다(금지를 쌓지 않는다).

**㉡ state 안의 타인** — `displayed for Detective Kim`,
`as she identifies … and calls Hye-soo`. 이건 진짜다. 허용된 사람의
상태를 말하면서 **다른 사람을 프레임에 끌어들인다.** 팩을 조였다:

    ★state_en 은 **그 한 사람만** 말한다. 다른 사람을 그 문장에
    끌어들이지 마라 — 상대·목격자·받는 사람도 쓰지 않는다.
    그 사람도 이어받는 상태가 있으면 **자기 항목**으로 따로 낸다.

### ★이 실측의 한계

**v3 저작을 태운 것이 아니다.** 이미 만들어진 옛 문안을 즉석 프롬프트로
쪼갠 것이고, 진짜 v3 는 **씬 원문에서 시작**한다. 그러니 이 수치를
「v3 의 성능」으로 읽으면 안 된다 — **우회 통로가 있는지**만 본 것이다.
있었고, 그 통로를 팩으로 막았다.

## 아직 안 정한 것

- **지출** — 팩·schema 가 바뀌면 그 스텝이 다시 돌고, 조립 바이트가
  바뀌면 롤부터 다시 산다. 무인 샷뿐 아니라 **전 샷**이 대상이다.
- **`offscreen_effects` 의 근거 검사** — 표기 저작처럼 `source_quote` 를
  받아 그 샷 텍스트에 있는지 코드가 볼지. 늘리는 값어치를 따져야 한다.

## 고칠 자리

    prompts/_base/shot_continuity/<새 판>/carried_schema.json   칸 나누기
    prompts/_base/shot_continuity/<새 판>/carried_system.md     저작 계약 + 정본 명단
    prompts/_base/shot_continuity/<새 판>/pose_fix_*            같은 계약
    app/modules/pipeline/shot_continuity_author.py              팩 갈래 + short_id
    app/services/still_recipe_service.py:2741,3336              판별 입력 · 값 읽기
    app/core/steps/shot_conti_light_step.py                     VE loader + 두 경로
    app/core/steps/step_manifest.py                             depends_on
    app/core/steps/image_steps.py                               지문에 접기

★**끝점을 먼저 잡는다.** 표기 저작 판에서 저작만 재고 「됐다」고 했다가
`build_signage_section` 이 산출을 전량 버리는 것을 놓쳤다. 저작 출력 →
고르기 → **네 소비자에게 나가는 프롬프트**까지 시험을 먼저 세운다.

★**바깥 재개 관문에도 접혀야 한다.** 샷별 지문만 바꾸면 그 스텝이 샷
루프에 들어가기 전에 통째로 건너뛴 전례가 있다.
