# Rooftop Source Grounding Experiment — plan v1 (Codex APPROVED + Q1 fix)

> Codex sanity 2026-05-23 → **Q1 fix**: GeminiTextClient 직접 import **금지** (log_llm_call 이 LLMCallLog commit → DB write 위반). script-local urllib wrapper 만 사용 (Phase D LVM 패턴). 그 외 Q2~Q5 권고 (+4 필드, luxury candidate only, enum, plate grouping, quote limit) 모두 반영 예정.


> standalone (production pipeline 0 수정, DB write 0). episode 1 + 기획안 → 옥탑방 내부 source bible 추출 + Gemini 3.5 Flash 비교 + 현재 base plate (run `20260523_1956_d22e35`) 와 evidence mismatch 분석.
>
> 의도: Phase D base plate 가 "공간 자체가 source 와 어긋났을" 가능성 검증. 생성 단계 전 source grounding 결함 진단.

## 0. 출처 / 절대 규칙

- 대상: project `6cb862d9-590c-4dce-86e6-d10c2977db19` / episode `08ad2cd3-3e96-4d84-808f-869ee628473c` / location L05 옥탑방 내부.
- DB write **0**, checkpoint write **0**, backend/app/ 수정 **0**. import 만 허용.
- LLM (Gemini) 호출 데이터 자르기 **금지** (CLAUDE.md 절대 규칙) — episode 전문 + 기획안 전문 그대로 전달.
- Gemini 는 **evidence/unknown 중심** — 최종 판정자 아님. 확신 못 하면 `unknowns` 에.
- API key/secret 로그 기록 금지 (Codex Phase D 검증 OK 동일 정책).
- 사용자 명시: Phase E shot plate 진입 보류, 현재 base plate 결과 보존.

## 1. 입력 수집 (Phase α 확정)

| source | 위치 | 길이 | found |
|---|---|---|---|
| episode 1 fulltext | `episode.fulltext` where id=08ad2cd3 | 10,606자 | ✅ |
| 기획안 | `project_registry.planning_doc_text` where id=6cb862d9 | 20,566자 | ✅ |
| world_summary | `project_settings.world_summary` | (row 없음) | ❌ → `missing_inputs` |
| style_rules_json | `project_settings.style_rules_json` | (row 없음) | ❌ → `missing_inputs` |
| screenplay PDF | `episode.source_path` | 별도 file | not loaded (fulltext 가 이미 추출됨) |

`source_bundle.json` 에 source/위치/길이/found 매트릭스 기록.

## 2. 디렉토리 구조

```
scripts_output/rooftop_source_grounding/<run_id>/
  source_bundle.md            # episode + 기획안 + missing inputs 통합
  source_bundle.json          # 위와 동일, schema
  source_evidence.tsv         # deterministic keyword extraction
  source_evidence.json        # 위와 동일
  gemini_rooftop_bible.json   # Gemini strict JSON (실 호출 시만)
  gemini_call_meta.jsonl      # latency/model/status
  comparison_report.tsv       # base plate vs source bible mismatch 후보
  comparison_report.json      # 위와 동일
  index.html                  # 현재 base plate inline + 근거 문장 + Gemini 추출 + 비교표
  run_meta.json
  _serve.log
```

## 3. CLI / 안전장치

```
backend/scripts/experiment_rooftop_source_grounding.py
  --run-id <id>                # 자동
  --generate                   # Gemini 실호출 허용 (default OFF)
  --gemini-model <name>        # default 'gemini-3.5-flash' (settings.gemini_flash_model 와 일치)
  --base-run-dir <path>        # 비교 대상 base plate run. default = 가장 최근
                               # scripts_output/rooftop_spatial_bg_experiment/*
  --no-serve / --serve-port / --bind  # Phase D 와 동일 (default 127.0.0.1)
  --estimate-only              # cost 출력 후 exit
```

기본 동작 (no flag):
1. dry-run: source_bundle / evidence / comparison report 모두 생성. Gemini 호출 0. webserver launch.
2. `--generate`: dry-run 산출물 + Gemini 호출 + bible 추출 + comparison 채움.

## 4. 4 단계 흐름

### Stage 1 — Source bundle 수집 (Phase α 결과 활용)
- DB read-only: `Episode.fulltext` + `ProjectRegistry.planning_doc_text`.
- missing: `world_summary`, `style_rules_json` → `missing_inputs` 에 명시 + grep 결과.
- `source_bundle.md`: 두 입력 전문 + meta header.

### Stage 2 — Deterministic keyword extraction (Phase γ)
- 키워드 카테고리 (사용자 명시):
  - **sub_space**: 옥탑방, 옥상, 거실, 방, 침실, 욕실, 화장실, 주방, 부엌, 현관
  - **fixture**: 식탁, 싱크대, 침대, 욕조, 변기, 거울, 창문, 커튼, 벽면, 바닥, 천장, 문, 계단
  - **state**: 피, 시신, 핏자국, 정돈, 어지럽, 깨끗, 어두운, 빈, 표식
  - **condition/socio**: 낡은, 좁은, 오래된, 작은, 옥탑, 다세대, 빌라
- 각 매칭마다 다음 추출:
  - source (`episode` | `planning_doc`)
  - quote (해당 문장 — 마침표 단위)
  - char_offset (source 안 위치)
  - candidate_field (예: `옥탑방` → `space.location`, `식탁` → `furniture`)
  - confidence (rule 강도)
  - notes (인근 context)
- `source_evidence.tsv` (header + N rows).

### Stage 3 — Gemini bible 추출 (Phase δ, `--generate` 시만)
- 입력: full `source_bundle.md` (episode fulltext + planning_doc_text 합본, 자르지 않음).
- 모델: `--gemini-model` (default `gemini-3.5-flash`).
- 시스템 인스트럭션:
  - "옥탑방 내부 (L05) 의 source bible 을 evidence 기반으로 추출하라."
  - "확실한 것만 채우고, 불확실하면 `unknowns` 에 명시."
  - "최종 판정자 아님 — 시각화 결정은 별도. evidence/unknown 중심."
  - "각 필드에 source citation (quote + 출처) 의무."
- strict JSON schema (`rooftop_interior_source_bible`):
  - `sub_spaces[]`: name, evidence_quotes, layout_relations
  - `layout_relations[]`: from_sub_space, to_sub_space, relation (e.g. "방 옆 거실"), evidence
  - `doors_windows[]`: type, location, evidence
  - `kitchen[]`: present, evidence (싱크대/식탁 등)
  - `bathroom[]`: present, evidence
  - `furniture[]`: name, sub_space, evidence
  - `materials[]`: floor/wall/ceiling 재질, evidence
  - `condition_age`: text (낡음/새것/연식), evidence
  - `socioeconomic_tone`: text (저소득/평범/풍족), evidence
  - `required_visual_cues[]`: source 가 명시 요구
  - `forbidden_luxury_cues[]`: source 와 어긋나는 시각 요소 (e.g. "샹들리에", "대리석")
  - `unknowns[]`: 추출 불가 항목
- response Gemini API 호출 — `gemini_text_client` 직접 import 또는 thin local wrapper.
- 결과 schema 검증 (jsonschema) → fail 시 retry 1, 그래도 fail 이면 `gemini_call_meta.jsonl` 에 error 기록.

### Stage 4 — Comparison report (Phase ε)
- 현재 base plate run dir 자동 검색 (`scripts_output/rooftop_spatial_bg_experiment/2026*` 가장 최근) 또는 `--base-run-dir`.
- 입력:
  - `prompts/base/*.txt` (4장 prompt 텍스트)
  - `realized_spatial_cards/*.json` (4 LVM card)
- 비교축:
  - **luxury_cue_candidate**: base prompt 또는 LVM fixed_objects 에 source 와 어긋나는 럭셔리 단어 (e.g. "marble", "chandelier", "modern apartment", "western", "한옥") — source 의 `forbidden_luxury_cues` 또는 deterministic luxury keyword list 와 매칭.
  - **source_not_supports**: base prompt / LVM 에서 등장한 요소가 source 에 evidence 0 (예: prompt 에 "샹들리에" 있는데 source 에 없음).
  - **source_requires_missing**: source `required_visual_cues` 중 LVM fixed_objects 에 없음 (예: source "식탁/싱크대" 요구 — LVM 에 없음).
  - **unknown**: source 도 모르고 base 도 모름.
- `comparison_report.tsv`: 행 = (issue_type, plate_id, evidence_quote, base_evidence, severity_candidate).
- **판정 X, candidate 표시만** (사용자 명시).

## 5. HTML

- §1 source_bundle 요약 (length, missing_inputs, sample quotes)
- §2 source_evidence 표 (deterministic keyword)
- §3 Gemini rooftop_bible 추출 (sub_spaces / forbidden_luxury_cues / required_visual_cues / unknowns 트리 표시) — 미실행 시 placeholder
- §4 현재 base plate 4장 inline + LVM 요약 (Phase D 결과 그대로)
- §5 Comparison candidates 표 (luxury_cue / source_not_supports / source_requires_missing / unknown)
- §6 run_meta + args

## 6. 모델 / 호출 / 비용

- Gemini 3.5 Flash 단가 ~$0.005/1K input + $0.015/1K output. 두 입력 합 ~31K char ≈ ~10K token. 1 call ≈ $0.05 + $0.10 (output) = ~$0.15.
- retry 1 max → 최대 $0.3.
- LVM 안 함 (Phase D 가 이미 실행).

## 7. import 범위

OK:
- `app.core.database.SessionLocal`, `app.models.project.Episode`, `app.models.catalog.ProjectRegistry`
- (Gemini) `app.modules.llm.gemini_text_client.GeminiTextClient` 또는 thin local wrapper (Codex Q 결정)
- `app.core.config.settings` (모델명 + API key)

비 import:
- production scene_image_pipeline / scene_generation_coordinator
- ImageReviewService
- DB write 경로

## 8. Out of scope

- shot plate 생성 / 변경
- chain_bg / floor_plan 재설계
- production prompt template 수정
- DB schema 변경
- frontend UI

## 9. Codex 결정 요청 (Phase β)

Q1. Gemini text client = `app.modules.llm.gemini_text_client.GeminiTextClient` 직접 import OK 인지? 또는 thin local urllib wrapper (Phase D LVM 패턴) 가 schema 통제에 더 적합?
Q2. strict JSON schema 11 필드 (위 §4 Stage 3) 가 source bible 로 충분한지? 빠진 카테고리?
Q3. luxury_cue keyword list — 사용자 의도가 "한국 옥탑방 = 저소득 좁은 공간" 인 듯한데, deterministic luxury list (`marble`/`chandelier`/`open-plan kitchen`/`large windows overlooking ocean` 등) 추가 OK?
Q4. comparison "판정 X 후보 표시만" 의 severity_candidate enum — `info | suspicious | likely_mismatch` 3 단계 OK?
Q5. base plate 가 4장인데 LVM card 4개 + source bible 1개 → 16 비교 row × 4 issue_type = 최대 64 row. report 너무 크면 plate 별 grouping?
