# 씬 분해 — LLM 이 규칙을 쓰고 코드가 경계를 찍는다

작성 2026-08-08 · 상태: 설계 · 실험 단계(프로덕션 미적용)

## ⓪ 30초 요약

대본을 씬으로 나누는 일을 **LLM 이 경계를 직접 찍는 방식**에서 **LLM 이 그
대본의 분해 규칙을 쓰고 코드가 그 규칙으로 찍는 방식**으로 바꾼다.

바꾸는 이유는 둘이다. ①대본마다 씬 헤딩 형식이 달라 고정 규칙 하나로는
절반도 못 덮는다(22개 대본, 형식 8가지, 최대 12/22). ②LLM 이 경계를 직접
찍으면 형식이 튀는 씬을 놓친다 — 같은 대본에서 `N. 몽타주:` 셋을 두 실행 다
놓쳤고, 실행마다 결과가 흔들린다.

## ① 무엇이 문제인가 (실측)

### 먼저, 문제가 아니었던 것

기록에 "세그먼트 오프셋이 거의 전부 틀렸다(9/113), 오차가 끝에서 1만 자"로
남아 있었다. **재측정 결과 틀리지 않았다.** `start_char` 는 `text_cleanup` 의
정리본 기준인데 그 측정이 DB 원문으로 쟀다 — 좌표계 착오다.

| 기준 | `start_char` 정합 (금월도 2고, 113 세그먼트) |
|---|---|
| DB 원문 77,281자 | 0/113 |
| 정리본 65,783자 | **113/113** |

하류가 실제 읽는 `scene_save` 본문도 113/113 이고, 정리본의 99.8% 를 덮는다.
`text_cleanup` 이 실행마다 흔들리던 것(7/28 실행이 마지막 3씬 손실)도
`_extract_range_resilient` 가 들어간 **7/31 커밋 `5545c660`** 으로 닫혔다.

### 남은 진짜 문제 — 형식

`org/` 의 시나리오 22개에서 씬 헤딩 형식이 **8가지**다.

| 형식 | 대본 |
|---|---|
| `1. 실외. 겨울 갈대숲 - 낮` | 금월도 2고, 파과, 미아, 지금우리학교는2 외 |
| `S#1. 숲속 / 해 질 무렵` | 금월도 1~4부 |
| `**1. 바다 속 – D**` (마크다운 볼드) | 컨트리로드 |
| `*   Scene 181:` + 들여쓴 `181. …`, 번호가 181부터 | 컨트리로드 다른 판 |
| `INT. …` | Digital Love Story |
| `번호.` 와 `INT/EXT` 가 **한 대본에 함께** (116개 + 34개) | srd part 1~6 |
| `안. 가영의 오피스텔 - 낮` + 번호가 **뒤에 단독 줄** | Woman in the Mirror |
| `#1.` 줄바꿈 `산 (밖/밤~새벽)` — **헤딩이 두 줄** | 요괴전 |

한 규칙으로 덮이는 비율: `번호.` 12/22 · `INT/EXT` 6/22 · `S#번호` 4/22.
**어느 것도 절반을 못 넘는다.** 형식이 다양한 정도가 아니라 구조가 다르다 —
번호가 앞에 오기도, 뒤에 오기도, 없기도 하고, 헤딩이 한 줄이기도 두 줄이기도
하다. srd 처럼 두 신호가 공존해 **읽어야만 어느 쪽이 씬 경계인지 아는** 경우도
있다.

### 남은 진짜 문제 — LLM 이 경계를 직접 찍는다

현행 `scene_extractor_v2.py` 는 LLM 에게 `start_line_text` 를 받아
`fulltext.find()` 로 경계를 찾는다. 두 가지가 따라온다.

- **형식이 튀는 씬을 놓친다.** 금월도 2고에서 `4. 몽타주:` `82. 몽타주:`
  `105. 몽타주:` 셋을 **두 실행이 똑같이** 놓쳤다. 다른 씬은 장소·시간이
  붙는데 이 셋만 형식이 달라 LLM 이 씬으로 안 봤다. 흔들림이 아니라
  결정론적 결함이라 **캐시로는 안 고쳐진다** — 틀린 결과가 고정될 뿐이다.
  (본문이 버려지지는 않는다. 앞 씬에 흡수돼 시공간이 다른 두 장면이 한
  씬이 된다.)
- **실행마다 흔들린다.** 같은 원문에 110·113·116 개.

## ② 설계

```
대본 전문
  ↓  ①저작 — gpt 최상위가 읽고 후보 규칙 2~3개를 쓴다
후보 규칙들
  ↓  ②실행 — 코드가 각 규칙으로 경계를 찍는다 (LLM 미개입)
후보별 세그먼트
  ↓  ③검증 — 기계 계약으로 각각 점수를 낸다
  ↓  ④선택 — 절대 문턱이 아니라 **후보 간 상대 비교**로 고른다
  ↓  ⑤재저작 — 최선도 계약을 못 넘으면 실패 이유를 주고 다시 쓰게 (최대 2회)
확정 세그먼트
```

### 저작물의 형태 — 정규식 하나 + 번호 그룹

관찰된 8형식을 다시 보면 전부 **헤딩 줄을 식별하는 정규식 하나**로 잡힌다.
그래서 저작물을 코드가 아니라 **규칙 명세**로 받는다.

```json
{
  "candidates": [
    {
      "name": "줄머리 번호",
      "pattern": "^[ \\t]*(\\d+)[ \\t]*\\.",
      "number_group": 1,
      "rationale": "3. 실내. 갈대숲 창고 밖 - 낮 / 4. 몽타주: 둘 다 걸린다"
    }
  ]
}
```

- `pattern` — 헤딩 **줄**을 찾는 정규식. `re.MULTILINE` 로 실행한다.
- `number_group` — 씬 번호를 담은 캡처 그룹 번호. 번호가 없는 형식이면 `null`.
- `rationale` — 그 대본에서 걸리는 실제 줄 예시. 사람이 읽고 판단할 근거다.

**파이썬 코드를 저작하게 하지 않는 이유**: 실행 격리 비용이 크고, 지금 필요한
표현력은 정규식으로 전부 덮인다. 정규식은 타임아웃과 길이 제한만으로 가둘 수
있다. 이보다 큰 표현력이 실제로 필요한 대본이 나오면 그때 넓힌다 — 미리
넓히면 검증할 수 없는 자유도만 늘어난다.

**헤딩이 두 줄인 형식**(요괴전)은 경계를 첫 줄에서 찍는 것으로 충분하다.
헤딩 표시 문자열이 두 줄이어야 하는지는 검증에서 문제가 드러나면 그때 더한다.

### 검증 계약

각 후보를 코드가 실행하고 다음을 **전부 기계로** 잰다.

| 항목 | 내용 | 성격 |
|---|---|---|
| C1 매치 수 | 2개 미만이면 분해가 아니다 | 필수 |
| C2 단조증가 | 경계가 앞으로만 간다 | 구현 무결성 |
| C3 전체 커버 | 경계 사이에 틈이 없다(첫 매치 앞 머리말 제외) | 구현 무결성 |
| C4 자기 헤딩에서 시작 | 각 세그먼트 첫 줄이 그 정규식에 걸린다 | 구현 무결성 |
| C5 번호 | 되돌아감·못 읽음은 **실패**, 촘촘함은 점수 | 필수 + 점수 |
| C6 정규식 안전 | 실행 타임아웃, 패턴 길이 상한 | 안전 |
| C7 헤딩 내용 | 매치한 줄에 번호·기호 말고 내용이 있는 비율 | 점수 |

★**C2·C3·C4 는 "규칙이 옳은가"를 거의 검증하지 않는다**(Codex 지적, 확인함).
매치 위치부터 다음 매치까지 잘라 세그먼트를 만들면 순서·틈·시작은 **자동으로**
맞는다. 이 셋은 코드가 안 깨졌는지만 본다. 규칙의 옳고 그름을 실제로 가르는
것은 **C5 와 C7** 이다.

**후보 선택은 C7 → C5 순의 상대 비교이고, 매치 수는 순위에 쓰지 않는다.**
많이 잡을수록 좋다고 두면 쪽 번호가 씬 헤딩을 이긴다 — 아래 반례가 실측이다.

### 실측 반례 둘 (이 설계가 실제로 걸러야 하는 것)

**① 씬 안의 항목 번호** — 금월도 2고의 `4. 몽타주:` 아래에 `1. …` `2. …` 이
줄머리로 온다. 항목과 씬 헤딩이 형식이 같아 정규식으로는 못 가른다(116개가
144개가 된다). 어휘로 "몽타주"를 찾는 것은 작품마다 말이 달라 금지다. 남는
신호는 **번호가 되돌아간다**는 것 하나뿐이라 `ascending_numbers` 축으로 다룬다.

**② 쪽 번호** — srd part 1 은 각 쪽에 번호만 있는 줄이 두 번씩 들어 있다.
줄머리 번호로 잡으면 122개가 걸리고, 되돌아감을 걸러도 **61개가 남아 오름차순도
연속성도 완벽하다.** 진짜 헤딩인 `INT/EXT` 는 37개뿐이라 매치 수 순위면 쪽
번호가 이긴다. 가르는 신호는 **그 줄에 헤딩 내용이 있는가**(C7)뿐이다 — 쪽
번호 줄에는 번호와 마침표밖에 없다. C7 을 넣은 뒤 srd part 1~6 이 전부
`INT/EXT` 를 고른다.

### ★텍스트 표현 계약 — 저작·실행·검증이 같은 바이트를 본다

**규칙은 반드시 `text_cleanup` 의 정리본에서 저작하고 정리본에 실행한다.**
PDF 화면·PDF 직접 추출·정리본은 서로 다른 텍스트다. 한 표현에서 저작해 다른
표현에 실행하면 규칙이 맞아도 실패한다.

실측으로 확인했다. PDF 직접 추출에서는 쪽 번호가 남고 헤딩이 두 줄로 갈린다
(파과 `3. ` → `(과거) 강도영의 집 / 낮`, 요괴전 `#1.` → `산 (밖/밤~새벽)`).
**정리본에서는 둘 다 사라진다.** 쪽 번호는 지워지고, 두 줄 헤딩은 한 줄로
합쳐진다 — 요괴전을 실제로 정리해 확인했다(2026-08-08):

```
PDF 직접 추출 : '#1.'  다음 줄  '산 (밖/밤~새벽)'
정리본        : '#1. 산 (밖/밤~새벽)'
```

★단 `text_cleaner.py` 의 프롬프트에 "씬 번호와 헤딩을 한 줄로 합쳐라"는 계약은
**없다**(Codex 확인). PDF 화면을 보는 모델이 원래 배치대로 복원해서 그렇게
나오는 것이지 코드가 보장하는 것이 아니다. 다른 대본에서 안 합쳐질 수 있고,
그러면 `#1.` 같은 옳은 경계의 C7 이 0 이 되어 후보 선택이 달라진다. 22개
정리본에서 이 경우가 실제로 나오는지 세어야 한다.

즉 PDF 추출로 실험하면 **프로덕션에 없는 문제를 만들어 놓고 푸는** 셈이 된다.

### 저작 모델

사용자 지시는 "GPT 계열 최상위"다. 후보가 둘이다 — `gpt`(설정 경유,
기본값 `gpt-5.6-sol`)와 `gpt-terra`(물리 모델 `gpt-5.6-terra` 직결).
`CLAUDE.md` 는 Sol 을 "분석 주력"이라 할 뿐 최상위라고 하지 않는다.

**둘 다 돌려 같은 대본에서 규칙을 저작시키고 검증 점수로 비교한다.** 이름으로
등급을 추측하지 않고 실측으로 정한다. 대본 전문을 **자르지 않고** 넣는다
(프로젝트 절대 규칙). 가장 긴 정리본이 65,783자다.

### 교차 점검

저작된 규칙을 **gpt · gemini · qwen** 세 모델에게 각각 검토시킨다 — "이 규칙이
이 대본의 씬 경계를 옳게 잡는가, 놓치는 형식은 없는가". 코드를 만드는 코드의
점검이다. 판정이 갈리는 대본은 직접 확인한다.

## ③ 범위 밖

- **캐싱·해시 고정** — 규칙이나 프롬프트 버전이 바뀌면 어차피 다시 만들어야
  한다. 정확해진 뒤에 따로 다룬다.
- **프로덕션 배선** — 실험이 22개 대본에서 검증된 뒤에만
  `scene_extractor_v2.py` 에 붙인다. 지금 도는 재실행에는 손대지 않는다.
- **`text_cleanup`** — 7/31 에 닫혔다.

## ④ 검증 방법 (실측 계획)

`org/` 의 22개 시나리오 전부에 걸어 잰다.

1. **형식 8가지를 다 통과하는가** — 대본별 매치 수와 계약 통과 여부
2. **`N. 몽타주:` 셋을 잡는가** — 금월도 2고에서 116/116 이어야 한다
   (현행 LLM 세그먼터는 113/116)
3. **현행 대비** — 같은 대본에서 현행 세그먼터와 씬 수·경계를 나란히 본다

산출은 `artifact/20260808_세그먼테이션_저작/` 에 갤러리로 남긴다.

## ⑤ 위험

- **저작된 규칙이 틀리면 대본 전체가 한꺼번에 틀린다.** LLM 이 경계를 찍으면
  틀려도 그 씬만 틀리는데, 규칙이 틀리면 전부 밀린다. 그래서 검증 계약이
  선택이 아니라 짝이다. C1~C4 를 못 넘으면 그 후보는 버린다.
- **정규식 폭주(ReDoS)** — 타임아웃과 패턴 길이 상한으로 가둔다.
- **표본 편향** — 22개가 전부 한국·영어 장편 극본이다. 다른 매체(예능 대본,
  소설)는 표본에 없다. 그런 문서가 오면 계약이 실패로 잡아내야 하고,
  실패했을 때 무엇을 하는지는 프로덕션 배선 시점에 정한다.
