전 과정 생성 이미지 DB 영속화 (All Generated Images → image_asset)

TheRoad Scene Lab · 설계서 · 2026-06-30 · 작성: Claude · 리뷰: Codex(요청 예정) / 사용자
경로: docs/w21b-all-image-db-persistence-20260630/design.html · 선행: 파이프라인 캔버스(b431e01c/0f2d8301)
한 줄 요약. 이미지 파이프라인이 생성하는 모든 이미지(최종물 + 중간 가이드/스케치/항공뷰/블로킹/거부 후보 등 전부)를 image_asset DB 행으로 영속화한다. 목적: 새 파이프라인 캔버스 UI에서 전 과정을 보고, 나중에 사람이 수정/재생성/제어할 수 있게 하기 위함. (사용자 결정: "전부, 복잡해도 무관, UI는 계속 교체")

1. 목표 / 비목표

목표

비목표 (YAGNI / 후속)

2. 현재 상태 (감사 결과)

이미지는 단일 image_asset(backend/app/models/project.py:150)에 저장된다. 최종물만 등록되고 중간물은 temp/checkpoint에 저장 후 버려진다.

분류현재 image_asset 영속?
최종물reference(char/outlook/prop), floor_plan, chain_bg, scene, composite예 (_register_image_assets 5개 사이트 등)
중간 가이드registered pose guide, shared-model 구도 가이드, anchor 스케치아니오 (temp/checkpoint)
중간 항공/레이아웃outdoor 항공뷰 base, 샷 블로킹, site layout아니오
후보/중간 i2i거부된 candidate, judge 입력, 재생성 이전 버전아니오

핵심 realization. 모든 이미지 생성 호출의 메타(프롬프트·참조 라벨·still_id·operation_type)는 이미 llm_call_log에 기록된다(image_tracer 통합). 빠진 건 출력 이미지 자체를 조회 가능한 image_asset으로 남기는 것뿐이다.

모델 호출 진입점 (계측 대상)

모델저수준 함수현황
Gemini flash imageGeminiImageClient.generate_image (app/modules/llm/gemini_image_client.py)단일 함수(~10 호출부 전부 통과) + image_tracer 통합 → 깔끔한 choke-point
gpt-image-2openai_client.images.generate/edit 직접 호출 (location_floor_plan / outdoor_site_layout_provider / registered_pose_guide 등 분산)단일 래퍼 없음 → 중앙화 필요
fal (angle)apply_fal_angle (app/services/fal_angle_helpers.py)단일 함수

3. 접근안 (A안 채택 — 사용자 승인)

A. 중앙 sink + contextvars + 모델호출 계측 채택B. 사이트별 명시 persistC. tracer 최소 포착
완전성높음(진입점 계측=누락 불가, 신규 단계 자동)중(사이트마다 추가, 미래 누락)높음(호출=포착)
lineage 품질높음(contextvars ambient)높음(사이트 컨텍스트)낮음(프롬프트/타임스탬프만)
수정 범위중(3 진입점 + gpt-image 중앙화 + 스텝 컨텍스트 설정)큼(20+ 사이트)작음
리스크core 생성경로 접촉(→ non-fatal 필수)누락 위험edge/그룹 빈약

4. 아키텍처 (A안)

4.1 GenerationContext (contextvars)

파이프라인 스텝/서비스가 자기 작업을 with generation_context(...)로 감싼다. sink가 ambient로 읽어 lineage를 채운다. 설정 없으면(미배선 경로) sink는 최소 메타로라도 포착(누락 0).

@dataclass
class GenContext:
    project_id: str
    episode_id: str | None
    stage: str                 # 파이프라인 단계 (step name): "background_render", "pose_guide", ...
    role: str                  # 용도: "aerial_base"|"shot_blocking"|"pose_guide"|"composition_guide"|
                               #       "anchor_sketch"|"candidate"|"i2i_step"|"final_*"
    still_id: str | None
    scene_index: int | None; shot_index: int | None
    entity_id: str | None
    source_image_ids: list[str]   # i2i/ref 입력 자산 UUID (엣지 형성)
    is_intermediate: bool
    candidate_index: int | None

# contextvar — set/reset 은 contextmanager 로. 비어 있으면 None.
_gen_ctx: ContextVar[GenContext | None]

4.2 capture sink

def capture_generated_image(png_bytes, *, db, gen_call_id=None, override=None) -> str | None:
    """생성된 이미지 바이트를 파일로 저장 + image_asset 행 삽입. 실패 시 None(절대 raise 안 함)."""
    ctx = current_gen_context() merged with override
    if ctx is None or ctx.is_final:   # 최종물은 기존 _register_image_assets 가 담당(중복 방지)
        return None
    path = projects//episodes//images/generated//.png
    write file (+ thumb)
    row = ImageAsset(asset_type='generated', stage=ctx.stage, pipeline_role=ctx.role,
                     is_intermediate=True, entity_id, still_id, shot_index,
                     source_image_id=first(ctx.source_image_ids),
                     reference_image_ids=json(ctx.source_image_ids),
                     generation_call_id=gen_call_id, candidate_index, variant_label=ctx.role,
                     file_path=path, prompt_used=ctx.prompt, status='generated')
    db.add(row); db.flush()
    return row.id

4.3 계측 지점 (3 진입점)

  1. Gemini: GeminiImageClient.generate_image 반환부에서 capture_generated_image(out_bytes) 호출(이미 image_tracer 있는 자리 옆). DB 세션 접근이 필요 → §6 참고(세션 주입 또는 deferred queue).
  2. gpt-image-2: 분산된 openai_client.images.generate/edit단일 wrapper generate_gpt_image(...)로 중앙화(새 모듈)하고, 그 wrapper에서 capture. 기존 호출부(location_floor_plan/outdoor_site_layout_provider/registered_pose_guide 등)는 wrapper 사용으로 치환.
  3. fal: apply_fal_angle 반환부에서 capture.

중복(dedup). 최종물(reference/fp/chain_bg/scene/composite)은 기존 _register_image_assets/서비스가 이미 image_asset을 만든다. sink가 같은 바이트를 또 만들면 2행이 된다. 규칙: GenContext.role 이 final_* 또는 컨텍스트가 "최종 등록 예정"이면 sink는 skip. 즉 sink는 중간물만 포착. (대안: sink가 모든 것을 만들고 최종 등록은 "promote"로 전환 — 큰 리팩토링이라 1차 제외.)

4.4 lineage / 엣지

캔버스 엣지는 source_image_id / reference_image_ids / still_id 구조키로 형성된다(이미 구현됨). 따라서 GenContext.source_image_ids를 스텝이 채우면 중간물도 자동으로 상류 엣지를 갖는다. 예: 포즈 가이드는 source=[배경 plate, 등록 마네킹 base], 항공뷰 base는 source 없음(루트), 블로킹은 source=[항공뷰 base], 씬 스틸은 source=[블로킹/스케치, 배경, char/outlook ref...].

5. 스키마 변경 (alembic 1개)

image_asset에 nullable 컬럼 소량 추가. 기존 컬럼(source_image_id/parent_image_id/reference_image_ids/still_id/shot_index/prompt_used)은 재사용.

컬럼타입용도
stageText null파이프라인 단계명(step). 캔버스 그룹/필터.
pipeline_roleText null용도(aerial_base/shot_blocking/pose_guide/composition_guide/anchor_sketch/candidate/i2i_step...).
is_intermediateBoolean default false중간물 여부(캔버스 기본 접기/펼치기·필터).
generation_call_idText null (→ llm_call_log)생성 호출과 연결(프롬프트/검증 SOT 조인).
candidate_indexInteger null같은 호출 내 후보 번호(거부 후보 구분).

asset_type에 신규 값 'generated' 추가(또는 role을 type로). 캔버스 nodeStyle가 stage/role로 색/라벨 매핑. 마이그레이션은 add-column만(삭제 0, drift 없음 — 부팅 fail-fast 통과).

6. 안전성 / 트랜잭션 (★중요)

7. 캔버스(소비자) 변경

8. 단계 계획 (writing-plans 에서 상세화)

  1. 스키마 마이그레이션(add-column) + ImageAsset 모델 필드.
  2. GenContext(contextvars) + deferred capture queue + capture sink(non-fatal) + 결정론 단위테스트(컨텍스트→행 매핑, dedup skip, 실패 무해).
  3. 모델 진입점 계측: Gemini generate_image / gpt-image 중앙 wrapper / fal. (gpt-image 중앙화 리팩토링 포함)
  4. 주요 스텝에 generation_context(...) 배선(stage/role/source 채움) — 우선 가이드/스케치/항공뷰/블로킹/후보부터.
  5. 실데이터 E2E(작은 에피소드 재생성) → 중간물 행 생성·lineage 육안 → 캔버스에서 전 과정 표시.
  6. 캔버스 소비자 업데이트(stage 색/필터, 아웃룩 엣지/색).
  7. Codex 리뷰(메인 코드라 Claude 구현·Codex 검토만) → 사용자 GO → 커밋.

9. 미해결 / 리스크

10. Codex 리뷰 반영 (2026-06-30) — APPROVED_DIRECTION_WITH_REQUIRED_REFINEMENTS

Codex가 설계서 + 실제 코드 진입점을 추적해 A안 방향을 승인하고, "전부 영속화"를 실제로 만족시키기 위한 필수 보강 5건을 요구했다. 아래가 §4·§5·§6·§8 대비 우선한다.

10.1 deferred queue → file-backed + worker 전파 + finally flush + 독립 세션

10.2 dedup → explicit intermediate capture policy (role 문자열 금지)

10.3 schema → reference_image_ids 의미 오염 금지 + 신규 컬럼

10.4 진입점 — "3개"는 누락. 실제 audit 결과

10.5 gpt-image 중앙화 → 저수준 primitive wrapper (고수준 통합 금지)

10.6 확정 구현 순서 (Codex 권고)

  1. A: schema + GenContext + file-backed deferred queue + capture_artifact API 먼저 + 단위테스트(결정론).
  2. B: GeminiImageClient + gemini_i2i_editor + FAL + gpt primitive wrapper 계측. gpt direct call sites 전부 wrapper로 치환(고수준 동작 유지, audit checklist로 잠금).
  3. C: 주요 intermediate(registered pose guide / outdoor aerial-base-blocking-sketch / zoom crop)에 explicit context 배선 → E2E로 DB row + canvas edge 확인.
  4. D: 전체 호출부 확장.

최대 리스크(Codex): "전부라 했는데 non-model/PIL 산출물·일부 direct image call이 빠지는 것" + "reference_image_ids 의미 오염으로 캔버스 edge가 섞이는 것". → 10.3/10.4로 봉쇄.

— 끝. A안 + 캔버스 선커밋(완료) + Codex 필수보강(§10) 반영. writing-plans 로 구현 계획서(Phase A부터)를 작성한다.