# UGC 영상 광고 생성 파이프라인 — 전체 해설

> **이 문서는 누구를 위한 것인가**
> creagen 의 UGC 영상 생성 기능을 **처음 보는 사람**이 읽고, 단계별(step-by-step) 워크플로 화면을 설계·구현할 수 있을 만큼 이해하는 것이 목표다. 사전 지식을 가정하지 않는다.
>
> 기준 코드: `pion-bot` `apps/agent/src/mastra/` (2026-09-01 실측)

---

## 0. 먼저 — 이게 대체 뭘 만드는 건가

**"일반인이 자기 폰으로 찍은 것 같은 15초짜리 제품 광고 영상"** 을 AI 가 자동으로 만든다.

광고주가 주는 것은 이 정도다:

- 제품 사진 1장
- 인물 레퍼런스 사진 1장 (누가 나올지)
- "이 제품 뭐가 좋은데요?" 에 대한 한두 줄 (= **효용**)
- 원하는 길이 (보통 15초), 언어 (한국어·영어·일본어)

받는 것은 **영상 생성 모델에 그대로 넣을 수 있는 영어 프롬프트 한 덩어리** + 그 안에 박힌 **대사**다.

### 왜 이게 어려운가

"광고 대본 써줘" 를 LLM 에게 그냥 시키면 안 되는 이유가 실측으로 나와 있다.

| 방식 | FAIL (화면이 틀리게 나옴) | PASS |
|---|---|---|
| 런타임 LLM 1패스 조립 | **13/30 (43%)** | 2/30 |
| 케이스당 사람이 저작 + 자기검토 | **1/30 (3%)** | 15/30 |

*동일 30조합 · 동일 적대 감사, 2026-08-13 실측*

**43% → 3%.** 그래서 이 파이프라인은 "LLM 에게 다 맡기기" 와 "사람이 다 쓰기" 사이의 중간을 택했다. 무엇을 고정하고 무엇을 열지를 **실측으로 갈랐다**는 게 핵심이다.

---

## 1. 용어 사전

이 문서 전체에서 쓰는 말이다. 여기만 알면 나머지는 읽힌다.

| 용어 | 뜻 |
|---|---|
| **UGC** | User Generated Content. "일반인이 폰으로 찍은 것 같은" 스타일의 광고 영상. 광고 티가 안 나는 게 상품성이다. |
| **연출 (format)** | 영상의 형식. "제품을 언박싱한다", "카메라 시점으로 본다", "미스터리 박스를 연다" 같은 것. **26종.** |
| **훅 (hook)** | 0~2초에 시선을 잡는 장치. "메신저 알림이 쏟아진다", "갑자기 바람이 분다", "영수증이 주르륵 풀린다" 같은 것. **15종.** |
| **상황 (setting)** | 장소·분위기. "한강 둔치", "낮 시간 부엌", "차 안", "찜질방" 같은 것. **26종.** |
| **골격 (skeleton)** | **완성된 촬영 대본 틀.** 초 단위 타임라인·카메라 문법·훅 이음새가 사람 손으로 저작돼 박혀 있고, 제품마다 달라져야 하는 부분만 빈칸(슬롯)으로 열려 있다. **26종.** |
| **슬롯 (slot)** | 골격의 빈칸. `{{SETTING_SCENE}}` 처럼 생겼고 LLM 이 채운다. |
| **앵글 (angle)** | 대사가 취할 **논리 구조**. "그냥 좋다", "다른 걸 쓰다 갈아탔다", "계속 실패하다 성공했다", "단점을 인정한다" 등. **7종.** |
| **게이트 (gate)** | 결과물을 검사하는 관문. 통과 못 하면 다시 쓰게 시킨다. **4겹**이 있고 성격이 전부 다르다. |
| **라운드 (round)** | 재작성 시도 횟수. 게이트에 걸리면 지적을 붙여 LLM 에게 다시 시킨다. |
| **작성기 (writer)** | 대사와 슬롯을 채우는 LLM. |
| **심사역 (judge)** | 나온 대본의 **뜻**을 읽고 판정하는 별도의 LLM. |
| **fail-open** | 검사에 걸려도 **작업을 죽이지 않는다.** 재시도를 다 쓰면 경고만 달고 통과시킨다. 이 파이프라인의 모든 게이트가 이 원칙이다. |

---

## 2. 전체 그림

```mermaid
flowchart TD
  I["입력<br/>제품·인물·효용·길이·언어"] --> A

  subgraph S1["① 후보 좁히기 — 코드가 한다"]
    A["버티컬 적합성 필터<br/>product_fit"] --> B["훅 ↔ 연출 이음새 검사<br/>UGC_DELIVERS_OK"]
    B --> C["좁혀진 후보 목록"]
  end

  C --> D

  subgraph S2["② 선택 + 작문 — LLM 이 한다"]
    D["연출·훅·상황·앵글 선택"] --> E["골격 조회<br/>findUniversalSkeleton"]
    E --> F["대사 N줄 + 슬롯 채우기<br/>+ PHYSICS + 연기 지시"]
  end

  F --> G["프롬프트 조립<br/>composeUgcPrompt"]

  subgraph S3["③ 게이트 4겹 — 코드 3 + LLM 1"]
    G --> H1["0차 표기 검사<br/>한글 무결성·숫자·카메라"]
    H1 --> H2["1차 계수 게이트<br/>문장 수·글자 밀도"]
    H2 --> H3["2차 슬롯 검산<br/>빈칸이 남았나"]
    H3 --> H4["3차 심사역 LLM<br/>뜻이 통하나 · 8항목"]
  end

  H1 -.->|"걸림"| R["지적을 붙여 재작성<br/>축마다 별도 예산"]
  H2 -.->|"걸림"| R
  H3 -.->|"걸림"| R
  H4 -.->|"걸림"| R
  R --> F

  H4 --> O["출력<br/>영어 프롬프트 + 대사 + 경고"]

  style S1 fill:#eef5ff,stroke:#4a7fd0
  style S2 fill:#fff4e6,stroke:#d08a3a
  style S3 fill:#eefaf0,stroke:#3fa060
  style R fill:#fdecec,stroke:#c04040
```

### 분업 원칙 — 왜 이렇게 갈랐나

> **코드가 결정**: 제품 버티컬 적합성 · 훅×연출 개연성 · 후보 좁히기
> **LLM 이 결정**: 좁혀진 후보 중 무엇이 사용자 요구에 맞는지 · 대사 본문

근거가 실측으로 있다:

- **67개를 통째로 LLM 에 주면** → 요약해 버린다 (제대로 안 고른다)
- **좁히기만 하고 LLM 을 빼면** → 사용자 요구가 반영되지 않는다

그래서 **후보를 좁혀서 LLM 에 넘기는 것까지** 코드가 한다.

---

## 3. 단계별 상세

### 단계 ① — 후보 좁히기 (코드, LLM 없음)

**조합 공간이 얼마나 큰가:**

```
연출 26 × 훅 15 × 상황 26 = 10,140 조합
```

이걸 그대로 LLM 에게 주면 안 되므로 코드가 먼저 거른다.

#### ①-1 버티컬 적합성

카탈로그의 각 항목에는 `product_fit` 이 붙어 있다 — "이 연출은 어떤 제품군에 맞나". 비어 있으면 전 버티컬 허용으로 본다.

```ts
function fitsVertical(item, vertical) {
  return !item.product_fit?.length || item.product_fit.includes(vertical)
}
```

#### ①-2 훅 ↔ 연출 이음새 (가장 중요)

**훅이 끝난 시점의 제품 상태**와 **연출이 시작하는 제품 상태**가 이어져야 한다. 안 그러면 화면이 깨진다.

> 예: "신발 신고 걷는 연출" + "제품이 날아오는 훅" → 신발을 이미 신고 있는데 신발이 날아온다. 말이 안 된다.

전이표가 코드에 있다:

```ts
UGC_DELIVERS_OK = {
  arrives:  ["absent"],
  unsealed: ["sealed", "absent"],
  revealed: ["absent", "sealed", "in_hand", "in_use", "on_surface"],
  stressed: ["in_hand", "in_use", "on_surface", "absent"]
}
```

읽는 법: 훅이 `arrives`(제품이 도착함) 로 끝나면, 연출은 `absent`(제품이 아직 없음) 로 시작하는 것만 이어진다.

**조합으로 세면 26 × 15 = 390 이지만, 상태로 세면 4 × 5 = 20 이다.** 이게 나중에 확장할 때 쓸 열쇠다.

#### 🖥️ 워크플로 화면에서 보여줄 것

- 조합 공간 10,140 → 필터 후 남은 후보 수 (숫자가 줄어드는 것 자체가 정보다)
- 왜 걸러졌는지 (버티컬 불일치 / 이음새 불가)
- **이 단계에는 LLM 이 없다** — "AI 가 생각 중" 이 아니라 즉시 끝난다

---

### 단계 ② — 선택 + 작문 (LLM 1회)

여기가 유일하게 **작성기 LLM** 이 도는 자리다.

**모델·설정 (프로덕션 기준):**

| 항목 | 값 | 이유 |
|---|---|---|
| temperature | **0.4** | screenwriter(0.8)보다 낮다. 필요한 건 카피 다양성이 아니라 "주어진 후보 중 올바른 것 고르기" + "조각을 규칙대로 잇기" 라 **결정성**이 더 중요하다 |
| presencePenalty / frequencyPenalty | 0 | — |
| 추론(reasoning) | **OFF** | structured output(tool_choice 강제)과의 양립이 미검증이라 프로덕션은 끈다 |

#### ②-1 LLM 이 고르는 것

좁혀진 후보 중에서 **연출 · 훅 · 상황 · 앵글**을 고른다. 사용자가 대놓고 요청한 카메라 무브가 있으면 그것도 반영한다.

**앵글 7종** (대사의 논리 구조):

| id | 뜻 |
|---|---|
| `plain_benefit` | 그냥 좋다 |
| `alternative_switch` | 다른 걸 쓰다 갈아탔다 |
| `failure_streak` | 계속 실패하다 이걸로 됐다 |
| `unexpected_use` | 예상 못 한 용도로 쓴다 |
| `social_proof` | 남들이 좋다더라 |
| `price_defiance` | 가격 대비 놀랍다 |
| `admitted_flaw` | 단점을 인정하고도 산다 |

#### ②-2 골격 조회

고른 연출로 골격을 찾는다. **26종 전부 "보편형(universal)"** 이다 — 훅·상황을 요구하지 않는다.

> **역사**: 초판 골격은 (연출+훅+상황) 조합 키라 10,140 중 **9개만 덮었다(적중률 0.089%)**. 사실상 모든 판이 폴백 조립으로 갔고, 그게 FAIL 43% 경로였다. 지금은 연출당 1종이라 **26/26 커버**다.

#### ②-3 골격 안에서 고정된 것 vs 열린 것

**🔒 고정 (사람이 저작, 건드리지 않음)** — 이게 43%→3% 를 만든 부분이다:

1. **훅 ↔ 연출 이음새** (0초 충돌 해소)
2. **카메라 문법** (손·시선·전환)
3. **초 단위 타임라인** (`0–2s`, `2–6.5s`, `6.5–9s`, `9–12.5s`, `12.5–15s`)

**🔓 열림 (LLM 이 채우는 슬롯)** — 제품마다 달라야 하는 부분:

| 슬롯 | 무엇 |
|---|---|
| `{{SETTING_SCENE}}` | 장소 |
| `{{PROP_ON_SURFACE}}` | 부속물 |
| `{{HOLD_POSE}}` | 자세 |
| `{{CONTROL_ACTION}}` | 조작 |
| `{{USE_MOMENT}}` | 사용 순간 |
| `{{EVIDENCE}}` | 효과가 보이는 방식 |
| `{{LINE_1}}` ~ `{{LINE_N}}` | 대사 |
| `{{PHYSICS_BLOCK}}` | 제품 물성 |
| `{{AUDIO_BED}}` | 음향 |

> 🩸 **왜 이렇게 갈랐나 — 실사고 2026-08-25**
> 초판 골격은 **장면 내용까지 통째로 고정**했다. 그래서 이어버드 제품에 그래놀라 봉지용 골격이 걸리자 **화면에 시리얼·우유·숟가락이 나갔다.** 대사만 이어폰 얘기였고 게이트 4겹 어느 것도 못 잡았다.
>
> 예외인지 규칙인지 A/B 로 쟀다 — 같은 골격 자리·같은 제품·같은 대사·같은 물성·같은 인물, 변수는 "골격이 장면을 고정하나 마나" 하나뿐:
>
> **고정형 6종 중 5종 실패(83%) · 문법형 6종 전부 통과.** 고정형이 성공한 유일한 1종은 **그 골격의 원래 주인**이었다.
>
> 판정관 인용(빨래바구니 판): *"이 영상은 팝업 빨래바구니가 아니라 **그래놀라 제품 광고**로, 파우치를 열어 우유에 클러스터를 붓고 먹는 장면이 전부다. 대사도 '접어지네요' 등 빨래바구니를 언급하지만 실제 화면 속 제품과 행동은 그래놀라 파우치이므로 완전히 불일치."*
>
> **대사만 제 제품 얘기를 하고 화면은 전부 남의 제품이었다.**

#### ②-4 문장 수는 LLM 이 안 정한다 — 코드가 계산한다

이게 비직관적인데 중요하다.

**문제**: 창(2~6문장)만 주면 작성기는 창 안 아무 값이나 고른다. 그런데 **문장당 글자수는 길이와 무관하게 거의 고정**이다 (동일 브리프 30판 중앙 15.0자, 문장 수 중앙 5). 그래서 15초 클립에서 5문장 × 15자 = 75자가 나오고 밀도 상한(78자)에 바짝 붙는다. **30판 중 7판이 창 밖으로 넘어가 하드 실패**했고, 재작성 6라운드를 다 태우고도 절반이 위반을 단 채 통과했다.

**즉 문장 수는 취향이 아니라 길이의 함수인데, 그 계산을 작성기에게 맡기고 있었다.** (LLM 은 자기가 쓴 글자를 못 센다 — 실측: 169자를 쓰고 "범위 안"이라 자기보고)

**해법**: 코드가 계산해서 지시에 싣는다.

```
nSent = round(총글자창 중앙 ÷ typical_syl)   // n_sent 창 [2,6] 으로 클램프
문장당 예산 = 총글자창 ÷ nSent
```

**언어별 실측 상수:**

| 언어 | 밀도 (자/초) | `typical_syl` (문장 평균) | 15초 → 지시 |
|---|---|---|---|
| ko | 3.6 ~ 5.2 | 15 (30판 실측) | **4문장**, 권고창 54~78자 |
| en | 11.0 ~ 16.0 | 48 (6판 실측, 종전 60은 20% 과대) | **4문장**, 권고창 165~240자 |
| ja | 4.6 ~ 6.4 | 16 (6판 실측) | **5문장**, 권고창 69~96자 |

> ⚠️ **글자 세는 법도 함정이었다.** 구 구현이 `[가-힣0-9a-zA-Z]` 라 **가나·한자를 0자로 세어 일본어 밀도가 항상 0** 이었다. 지금은 유니코드 인지(`\p{L}\p{N}`)로 센다.

#### 🖥️ 워크플로 화면에서 보여줄 것

- 고른 연출/훅/상황/앵글 — **왜 골랐는지(LLM 의 이유)** 까지
- 골격 원문과 채워진 슬롯을 **나란히** (무엇이 고정이고 무엇이 채워졌는지가 한눈에 보여야 한다)
- 코드가 계산한 지시값: `nSent`, 문장당 예산, 권고창 — **이 숫자가 어디서 나왔는지**가 이 단계의 핵심 정보다
- 여기가 유일하게 시간이 걸리는 LLM 호출이다 (라운드당 수 초~수십 초)

---

### 단계 ③ — 프롬프트 조립

작성기 산출물을 최종 프롬프트로 합친다. **골격 본문을 건드리지 않고 뒤에 절을 덧붙이는** 방식이다.

**절 순서 (10개, 고정):**

```
STYLE → REFERENCES → LOCATION → FIRST FRAME → ACTION TIMING
     → DIALOGUE → PERFORMANCE → PHYSICS → AUDIO → LOCKS
```

이 중 3개는 **업계 레퍼런스에서 흡수**한 것이다 (2026-08-24):

| 절 | 하는 일 | 왜 필요했나 |
|---|---|---|
| **PERFORMANCE** | 대사별 의도·전술 + 시선·깜빡임 안전줄 | 우리 대본은 대사만 나열했다. 저쪽 표현으로 *"the work lives in the eyes"* |
| **TEXTURE** | 모공 단위 피부 사실성 · 보정 금지 | 없었다 |
| **REFERENCE SCOPE** | 레퍼런스마다 "외모+연기 / 외모만 / 분위기만" 범위 명시 | `@Image` 가 일치율만 말하고 범위를 안 말했다 |

> **흡수하며 밝혀진 것 — 밀도 철학이 정반대였다.**
> 레퍼런스 대사를 우리 게이트에 넣으면 5개 중 4개가 "너무 짧다"로 반려된다. **저들은 침묵을 연기 지시로 점유하고, 우리는 글자로 메운다.** 우리 밀도 하한의 근거("짧으면 영상 모델이 남는 시간을 지어낸 말로 채운다")가 **다른 도구가 없어서 생긴 값**일 수 있다는 뜻이다.

**흡수하지 않은 것**과 그 이유도 기록돼 있다:

| 항목 | 왜 안 가져왔나 |
|---|---|
| 65mm · f/1.4 · 180° 셔터 등 광학 규격 | 저들은 **시네마틱**, 우리는 **폰 영상**. 그대로 넣으면 UGC 느낌이 죽는다 |
| 네거티브 대량 나열 (`NOT a 3D render`…) | 우리 원칙은 금지 최소화(쌓으면 그 대상을 활성화한다). **정면 충돌 — 실험으로 판정할 사안** |
| 샷 단위 분해 | 15초 단일 클립엔 과하다 |

**PERFORMANCE 절의 실측 교훈 — 역할 분담을 안 적으면 공정이 두 배로 망가진다:**

| 상태 | 1라운드 통과 | 평균 라운드 | 평균 소요 | 게이트 하드 |
|---|---|---|---|---|
| ① 흡수 전 | 6/8 | 1.25 | 49s | — |
| ② 연기 빈칸만 추가 | **1/8** | 2.12 | 84s | **10건** |
| ③ **역할 분담 명시 후** | **7/8** | **1.12** | **48s** | **2건** |

②에서 왜 망가졌나 — **진동**이 일어났다:

```
[#5] 라운드1  93자 (한 문장 45자)  → hard "너무 길다"
     라운드2  46자                → hard "너무 짧다"   ← 과교정
     라운드3  73자                → 통과
```

**연기 지시 빈칸이 생기니 작성기가 대사까지 무겁게 썼다.** 레퍼런스의 구조는 `짧은 대사 + 풍부한 연기`인데 그 역할 분담을 안 적어줬기 때문이다.

---

### 단계 ④ — 게이트 4겹

**모두 fail-open** 이다. 걸리면 재작성 지적을 만들고, 예산을 소진하면 경고를 달아 통과시킨다. **작업을 죽이지 않는다.**

```mermaid
flowchart LR
  P["조립된 프롬프트"] --> G0
  G0["0차 · 표기<br/>코드"] --> G1["1차 · 계수<br/>코드"]
  G1 --> G2["2차 · 슬롯<br/>코드"]
  G2 --> G3["3차 · 뜻<br/>LLM"]
  G3 --> OK["통과"]
  G0 -.->|"한글 깨짐 · 숫자 표기 · 카메라 누락"| RW["재작성"]
  G1 -.->|"문장 수 · 글자 밀도"| RW
  G2 -.->|"빈칸 잔존 · 대사 미탑재"| RW
  G3 -.->|"8항목 중 하나라도 실패"| RW
  style G0 fill:#eef5ff
  style G1 fill:#eef5ff
  style G2 fill:#eef5ff
  style G3 fill:#fff4e6
```

#### 0차 — 표기 검사 (코드)

**(a) 한글 무결성** — LLM 이 희귀 음절을 UTF-8 byte-fallback 토큰으로 조합하다 깨뜨리는 현상을 잡는다. 판정 기준은 KS X 1001 완성형 2350자이고, **코퍼스 421판 전수 대조에서 거짓양성 0 · 거짓음성 0** 으로 세운 값이다.

> ⚠️ 알려진 한계: 이 기준은 현대 한글 11,172자 중 8,822자를 "밖"으로 본다. 즉 `똠`·`펲`·`샾` 같은 정상 음절을 오탐할 수 있다. **일부러 안 고치고 있다** — fail-open 이라 경로를 막지 않고, 재측정 없이 좁히면 진짜 깨짐(거짓음성)이 들어온다. 실오탐이 관측되면 그때 연다.

**(b) 숫자 표기** — 대사에 아라비아 숫자가 남아 있으면 잡는다 (읽어야 하는 대사인데 `3` 이라고 쓰면 영상 모델이 어떻게 읽을지 모른다).

**(c) 카메라 누락** — `BLANK 0` 같은 필수 카메라 지시가 빠졌는지.

#### 1차 — 계수 게이트 (코드)

**⚠️ 계수 전용이다.** 문장 수·길이·밀도만 본다.

> **왜 계수만 하나 — Hard Rule 10**
> 종결어미 완결성·존대 일관성·담화표지·1인칭 경험담 같은 건 여기서 판정하지 **않는다.** 한국어 어미/감탄사 목록에 종속된 정규식이라 **다개국어에서 무너졌다** — 실측: en/ja 정상 대본 4문장이 전부 "체언 종결" 하드 실패. 그 기준들은 애초에 **작문 지침**이지 검산 대상이 아니라서, 판정자를 바꾸지 않고 작성기 지침으로 옮겼다.
>
> **예외는 계수(counting)뿐이다** — LLM 이 못 세는 값만 코드가 센다.

**층이 셋이다. 섞으면 안 된다:**

| 층 | 무엇 | 어긋나면 |
|---|---|---|
| **지시** (`targetLo/Hi`) | 권고창 안쪽으로 당긴 목표 | — (작성기에게만 간다) |
| **권고** (`density`) | 캘리브레이션 창 | **soft** — 경고만 |
| **판정** (`density × density_tol`) | 권고창 + 여유 12% | **상한·하한 모두 soft** |

> **왜 갈랐나**: 글자 수는 연속량이다. 3.57 자/초와 3.60 자/초 사이에 결과물의 질적 차이는 없는데, 종전엔 그 경계에서 하드 실패시켜 재작성 6라운드를 태웠다 — **15초 기준 소진율 50%**.
>
> **상한도 hard 가 아니다** (2026-08-25 정정). 상한을 hard 로 둔 이유는 "길면 15초에 다 못 넣어 잘린다" 였는데 **실측에서 안 잘렸다** — 제품 3종으로 91·96·97·103자를 뽑아 전부 끝까지 발화됐다. 지금은 길면 말이 빨라질 뿐이라 경고만 하고 통과시킨다.

**하드로 남은 것**: 문장 수 `[2, 6]` 밖 · 문장당 `syl_max: 28` 초과(넘으면 립싱크가 드리프트한다).

**soft 로 내린 것**: 길이 균질성(`short_ratio_max: 0.7`). 하드였을 때 **하드 지적 1위**였다(30판 중 11건). 길이 균질성은 발화 물리가 아니라 문체 취향이고, 하드로 두면 (a) 취향 때문에 예산을 태우고 (b) 문장당 글자 예산과 **논리적으로 충돌**한다 — 모든 문장이 예산 하한 이상이면 최단÷평균이 정의상 0.7 밑으로 못 내려간다. 지시가 자기모순이었다.

그래서 코드가 **"한 문장은 예외"의 상한(`shortHi`)까지 계산해서** 지시에 싣는다:

```
s ≤ r(n-1)L / (n-r)     // r=0.7, n=문장수, L=계획 평균
```

#### 2차 — 슬롯 검산 (코드)

조립된 프롬프트에 **제품 불문 추상 절**이 그대로 남았는지 본다.

카탈로그가 12종 버티컬을 공용으로 서빙하므로 조각이 제품 사용을 일부러 추상어로 쓴다:

```
"the one action the product exists to do"
"the single thing the product is for"
"the one detail she just mentioned"
"the thing that keeps going wrong"
"works the one control it has"
...
```

이게 안 풀리면 **영상 모델이 아무거나 연출한다.** 원두 봉지를 기울여 액체 커피를 쏟는 화면이 그 결과다 (사용자 실측 2026-08-11).

> ⚠️ **문체·의도 판정이 아니다.** 우리가 직접 적어 둔 고정 문자열이 살아남았는지만 비교한다 — 어휘 목록으로 뜻을 읽는 게 아니라서 다개국어 종속이 없다.
> ⚠️ **선택된 조각에 실제로 들어 있던 절만** 검사한다. 목록 전체를 무조건 훑으면 이번 조합과 무관한 문구까지 걸려 멀쩡한 프롬프트를 재작성시킨다.

같이 보는 것: **대사 미탑재**(승인된 대사가 프롬프트에 안 실렸나) · **스펙 헤더 붕괴**(절이 사라졌나).

#### 3차 — 심사역 (LLM, temperature 0.1)

**`gate.ts` 는 형식만 센다.** 그래서 이런 게 다 통과했다:

```
"스티치가 굉장히 촘촘하게 박혀 있어서 쉽게 안 풀려요. 못 벗겠어요."   ← 뜻이 안 통한다
"저 이거 그냥 장식장에 놔둘래요."                                ← 사겠다는 건지 처박겠다는 건지
```

**형식 규칙으로는 절대 못 잡는다. 뜻을 읽어야 잡힌다.**

**판정 8항목** (전부 참이어야 통과 — 논리곱):

| 항목 | 뜻 |
|---|---|
| `makesSense` **뜻이 통함** | 읽고 무슨 말인지 알 수 있는가. 화제가 튀는 것 자체는 문제가 아니다 |
| `makesWant` **살 이유가 있음** | 이 제품이 내 생활에서 무엇을 해주는지가 대본 안에 있는가. 외형만 읊으면 실패 |
| `staysInBenefit` **효용 안에 머묾** | 받은 적 없는 **다른 종류의 결과**를 주장하면 실패. 같은 효용의 과장·비유는 허용 |
| `noInventedSpecifics` **지어낸 사실 없음** | 준 적 없는 수치·가격·기간·타인의 말·비교 대상을 지어내지 않았는가. **준 근거를 한 단계 더 세게 극화한 것도 실패.** 감각·질감 묘사는 제외 |
| `matchesScene` **화면과 안 부딪힘** | 같은 사건을 말할 필요는 없고 **모순될 때만** 실패 |
| `endsPositive` **긍정 마무리** | 마지막 문장이 제품을 원하는 쪽으로 끝나는가. 애매하면 실패 쪽으로 기운다 |
| `soundsSpoken` **입말** | 실제로 입으로 낼 문장 구조인가. 광고 문형 재활용·구식 말투면 실패 |
| `speaksToViewer` **시청자에게 말함** | 카메라 너머의 사람에게 건네는 말인가. 전체가 혼잣말·자기다짐이면 실패 |

별도 신호 하나 (**품질 기준이 아니다**):

- `inputInsufficient` **입력 부족** — 재작성으로 해결할 수 없는 입력 부족인가. polarity 가 반대다(true = 재작성 불가). 이건 호출자에게 "더 써봐야 소용없다"를 알리는 신호다.

> **⚠️ 절대 되돌리면 안 되는 완화 — `matchesScene`**
> 이걸 "같은 사건을 말하는가"로 두면 **72편 중 12편만 통과**했다. 욕실 화면에서 "여행 갈 때 편하다"까지 반려했는데 광고로 아무 문제가 없다. **"부딪히지 않으면 통과"로 낮춰 55/72.** 이 완화를 되돌리면 심사역이 쓸모없어진다.

> **⚠️ `endsPositive` 의 역사 — 어미 목록으로 퇴화시켰다가 되돌렸다**
> 원본 4항목 중 `ends_positive` 를 이식할 때 **어미 금지 목록**으로 바꿨었다 (`~할 것 같아요`·`~해야겠어요` 금지). 안 먹었다 — 금지 규칙이 프롬프트에 있는데도 대조군 2편이 전부 `~것 같아요` 로 끝났고, 반대로 화자에게 **할 말(앵글)** 이 있으면 12편 중 0편이었다. **어미가 아니라 메시지가 원인이었다.**

**심사역도 fail-open** 이다 — LLM 호출이 실패하면 통과로 삼킨다. 대신 원장(`review-ledger`)에 fail-open 사실을 남긴다.

#### 🖥️ 워크플로 화면에서 보여줄 것

- 4겹을 **세로로 쌓아** 어디서 걸렸는지 즉시 보이게
- 게이트마다 **코드 / LLM** 배지 (성격이 다르다는 게 전달돼야 한다)
- 계수 게이트는 **숫자를 그대로** — 실제 글자 수 vs 권고창 vs 판정창을 막대로
- 심사역은 **8항목 체크리스트** + 실패 항목의 `why` (대본 원문을 인용해준다)
- **soft / hard 구분**을 색으로 (soft 는 경고, hard 만 재작성)

---

## 4. 재작성 예산 — 축마다 따로 센다

이게 이 파이프라인에서 가장 비직관적이고, **가장 비싸게 배운 부분**이다.

### 문제

게이트가 여러 개인데 예산을 하나로 쓰면, **두 층이 핑퐁할 때 매 라운드 한쪽 지적만 남은 채 6라운드를 소진한다.**

실측: 같은 조합(`ugc_tutorial`)이 baseline 에선 4·2 라운드에 통과하다가, **슬롯 재작성이 대본 라운드를 먹으면서 둘 다 6라운드 소진으로 뒤집혔다.**

### 해법 — 축을 분리한다

| 축 | 예산 | 대본 라운드 소비? |
|---|---|---|
| **대본 품질** (계수 + 뜻) | `MAX_GATE_ROUNDS` = **6** | — |
| **슬롯 미치환** | 별도 | ❌ 안 함 (`round--`) |
| **한글 무결성** | 별도 | ❌ 안 함 |
| **숫자 표기** | 별도 | ❌ 안 함 |
| **카메라 누락** | 별도 | ❌ 안 함 |
| **빈 장면 칸** | 별도 | ❌ 안 함 |

**원칙**: 품질이 수렴하는 것과, 표기가 깨진 걸 다시 받는 것은 **별개의 일**이다. 서로 라운드를 뺏으면 안 된다.

### 지적도 각자 슬롯을 갖는다

```ts
let gateNote = ""    // 계수
let judgeNote = ""   // 뜻
let slotNote = ""    // 슬롯
// ... 축마다 하나씩
```

합치면 핑퐁할 때 **한쪽 지적이 유실된다.**

### 그리고 — 지난 라운드 지적은 버린다

```ts
// 계수 게이트에 걸려 재작성할 때
judgeNote = ""   // 뜻 지적은 지난 라운드 대사에 대한 것이라 같이 버린다
```

> 안 지우면 이번 라운드의 계수 지적 옆에 **두 라운드 전 대사를 겨냥한 뜻 지적이 유령처럼 남아** 작성기를 헷갈리게 한다.

### 소진하면 어떻게 되나

경고를 달고 **통과시킨다.** 다만 소진 경로에도 심사역은 한 번 돌린다 — 안 그러면 `judge` 가 아예 비어서 나간다(실측: 동일 입력 30판 중 화자향 마무리 17판의 5판이 이 경로였다).

#### 🖥️ 워크플로 화면에서 보여줄 것

- **축별 예산 게이지** 6개를 나란히 (대본 6칸 + 나머지 각자)
- 라운드마다 **어느 축이 소비됐는지** 타임라인
- 지적이 **버려진 순간**도 표시 (왜 사라졌는지 안 보이면 사용자가 혼란스럽다)

---

## 5. 출력

```
{
  prompt:       "영상 모델에 넣을 영어 프롬프트 전문 (절 10개)",
  lines:        ["대사 1", "대사 2", ...],
  physicsBlock: "제품 물성",
  actingBlock:  "대사별 연기 지시",
  sceneFill:    { settingScene, propOnSurface, holdPose, ... },
  judge:        { makesSense: true, ..., why: "..." },
  rounds:       3,
  warnings:     ["...소진 경고..."]
}
```

---

## 6. 왜 이렇게 만들었나 — 설계 원칙 5가지

이 파이프라인 전체를 관통하는 규칙이다. 워크플로 화면을 만들 때도 이 원칙이 보이게 하면 좋다.

### ① LLM 이 못 하는 것만 코드가 한다

**계수(counting)뿐이다.** LLM 은 자기가 쓴 글자를 못 센다 (실측: 169자를 쓰고 "범위 안"이라 자기보고). 그래서 문장 수·글자 수·밀도는 코드가 센다.

반대로 **뜻·의도·문체 판정에는 키워드/정규식을 박지 않는다.** 특정 언어의 어휘·어미에 종속돼 다개국어에서 조용히 무너진다.

### ② 앞단에 이미 LLM 이 있으면 판정자를 추가하지 말고 그 LLM 의 지침으로 옮긴다

판정자를 정규식에서 소형 모델로 갈아끼우는 것은 **계층만 늘린다.**

### ③ 모든 게이트는 fail-open

**작업을 죽이지 않는다.** 오발동해도 손해가 0 인 자리에만 하드 게이트를 둔다.

### ④ 실측 없이 상수를 움직이지 않는다

모든 숫자에 근거와 판수가 주석으로 붙어 있다. 되돌리는 법까지 적혀 있다.

> 예: `"형태 라우팅은 커밋 852f8c7d8 에 있다. 문법형을 되돌릴 일이 생기면 템플릿과 라우팅을 **함께** 되살려라 — 문법형 템플릿에 형태 라우팅을 걸면 아무것도 안 걸리고, 고정형 템플릿에서 라우팅을 빼면 그 사고가 그대로 재발한다. 둘은 짝이다."`

### ⑤ 무신호 결함을 가장 무서워한다

**죽지 않고, 경고도 없고, 품질만 나빠지는 것**이 최악이다. 이 파이프라인의 게이트 대부분은 그런 결함을 잡으려고 있다.

지금도 가드 테스트가 26골격 × 192 케이스로 이걸 고정하고 있다 — 제품 낱말·장면 슬롯·장소/복장 전제·보편형 불변식.

---

## 7. 알려진 한계 (숨기지 않고 적는다)

| # | 한계 | 상태 |
|---|---|---|
| 1 | **낱말 목록 가드는 원리상 완전해질 수 없다** | 낱말이 아닌 "박제된 동작"(`sock-slide spin` — 양말과 미끄러운 마루를 전제한다)은 목록으로 못 잡는다. 그건 판정관 몫 |
| 2 | **문장 수 지시가 안 먹는다** | 코드가 4문장을 지시해도 작성기는 **언어와 무관하게 항상 5문장**을 쓴다. `n_sent` 하드창이 `[2,6]` 이라 그대로 통과한다. 지금은 창이 그 드리프트를 품어서 급하지 않다 |
| 3 | **한글 검출기 오탐 가능성** | KS X 1001 밖 정상 음절(`똠`·`펲`·`샾`)을 손상으로 볼 수 있다. fail-open 이라 경로는 안 막힌다. 실오탐 관측이 조건 |
| 4 | **영상 품질은 코드로 못 잰다** | 피부 질감·연기는 영상을 봐야 판정된다. 프롬프트가 절을 싣고 게이트를 안 깬다는 것까지가 코드의 한계 |
| 5 | **검증 수단 자체가 부족하다** | 소스 안에서 이미지·영상을 한 판 태워 결과를 받는 실행 유틸이 없다. 프리미티브(`MediaClient.queueRequest`/`getTaskStatus`)는 있고 엮은 스크립트가 없다 |

---

## 8. 워크플로 화면 설계 제안

지금까지의 내용을 step-by-step UI 로 옮기면 이렇게 된다.

| Step | 이름 | 주체 | 소요 | 화면의 핵심 |
|---|---|---|---|---|
| 1 | 입력 받기 | 사용자 | — | 제품·인물·효용·길이·언어 |
| 2 | 후보 좁히기 | **코드** | 즉시 | `10,140 → N` 숫자가 줄어드는 것, 걸러진 이유 |
| 3 | 조합 선택 | **LLM** | 수 초 | 고른 연출·훅·상황·앵글 + **이유** |
| 4 | 골격 조회 | 코드 | 즉시 | 골격 원문 — 🔒고정 / 🔓슬롯 색 구분 |
| 5 | 지시값 계산 | **코드** | 즉시 | `nSent`·문장당 예산·권고창 + **그 숫자의 유래** |
| 6 | 작문 | **LLM** | 수 초~수십 초 | 대사 + 채워진 슬롯 |
| 7 | 프롬프트 조립 | 코드 | 즉시 | 절 10개, 덧붙인 3절 강조 |
| 8 | 게이트 0차 (표기) | 코드 | 즉시 | 한글·숫자·카메라 |
| 9 | 게이트 1차 (계수) | 코드 | 즉시 | **숫자 막대** — 실제 vs 권고창 vs 판정창 |
| 10 | 게이트 2차 (슬롯) | 코드 | 즉시 | 남은 추상 절 하이라이트 |
| 11 | 게이트 3차 (뜻) | **LLM** | 수 초 | **8항목 체크리스트** + `why` |
| 12 | 결과 | — | — | 프롬프트 전문 + 대사 + 경고 + 라운드 수 |

**꼭 지켜야 할 표현:**

1. **코드 / LLM 을 시각적으로 구분** — 성격이 완전히 다르다. 코드 단계는 즉시 끝나고 결정적이며, LLM 단계는 느리고 비결정적이다.
2. **재작성이 일어나면 그 자리에서 되감기** — 게이트 8~11 에서 실패하면 6번으로 돌아가는 화살표가 보여야 한다.
3. **soft / hard 를 색으로** — soft 는 경고만이고 재작성이 없다. 이걸 같은 빨강으로 칠하면 사용자가 "왜 실패인데 통과했지" 로 혼란스럽다.
4. **축별 예산을 따로** — 대본 6칸 + 슬롯/한글/숫자/카메라/장면칸 각자. 하나의 진행바로 합치면 이 설계의 핵심이 사라진다.
5. **모든 숫자에 유래를 붙인다** — 이 파이프라인의 상수는 전부 실측값이다. 툴팁으로라도 "30판 실측" 을 보여주면 신뢰가 생긴다.

---

## 부록 A — 파일 지도

| 파일 | 줄 수 | 역할 |
|---|---|---|
| `tools/creagen/ugc-director/ugc-director.ts` | 1,406 | 오케스트레이터 — 전 단계 조율, 라운드 루프 |
| `tools/creagen/ugc-director/gate.ts` | 368 | 계수 게이트 + `scriptShapeHint` 지시 계산 |
| `tools/creagen/ugc-director/judge.ts` | 366 | 심사역 LLM (8항목) |
| `tools/creagen/ugc-director/schemas.ts` | 538 | 입출력 스키마 |
| `tools/creagen/ugc-director/slots.ts` | 89 | 잔여 슬롯 검산 |
| `tools/creagen/ugc-director/hangul-integrity.ts` | 100 | 한글 무결성 |
| `tools/creagen/ugc-director/spoken-numerals.ts` | 78 | 숫자 표기 |
| `tools/creagen/ugc-director/review-ledger.ts` | 119 | 판정 원장 (코퍼스 적재) |
| `tools/creagen/ugc-director/metrics.ts` | 100 | 텔레메트리 (대본·제품명 배제) |
| `prompts/creagen/ugc-catalog.ts` | 2,055 | 연출 26 · 훅 15 · 상황 26 + 이음새표 |
| `prompts/creagen/ugc-skeletons.ts` | 612 | 골격 26종 |
| `prompts/creagen/ugc-spec-sections.ts` | 377 | PERFORMANCE·TEXTURE·REFERENCE SCOPE + 조립 |
| `prompts/creagen/ugc-angles.ts` | 176 | 앵글 7종 |

## 부록 B — 핵심 수치 한 장

```
조합 공간      연출 26 × 훅 15 × 상황 26 = 10,140
골격           26종 (전부 보편형 · 연출당 1)
앵글           7종
추상 절        12종
심사 항목      8개 (논리곱) + 입력부족 신호 1
게이트         4겹 (코드 3 + LLM 1) · 전부 fail-open
대본 라운드    최대 6 · 표기/슬롯 축은 별도 예산
작성기         temperature 0.4 · 추론 OFF
심사역         temperature 0.1

품질 실측      LLM 1패스 조립 FAIL 43% → 손저작 골격 FAIL 3%
형태 종속      고정형 6종 중 5종 실패(83%) → 문법형 6종 전부 통과
PERFORMANCE   역할분담 명시 후 1라운드 통과 7/8 · 하드 지적 10건→2건
matchesScene  "같은 사건" 12/72 → "안 부딪히면 통과" 55/72
```

---

*작성 2026-09-01 · 기준 코드 `pion-bot@main` · 모든 수치는 코드 주석에 박제된 실측값이다*
