image_asset DB 행으로 영속화한다. 목적: 새 파이프라인 캔버스 UI에서 전 과정을 보고, 나중에 사람이 수정/재생성/제어할 수 있게 하기 위함. (사용자 결정: "전부, 복잡해도 무관, UI는 계속 교체")
image_asset으로 영속화(중간물 포함).이미지는 단일 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 image | GeminiImageClient.generate_image (app/modules/llm/gemini_image_client.py) | 단일 함수(~10 호출부 전부 통과) + image_tracer 통합 → 깔끔한 choke-point |
| gpt-image-2 | openai_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) | 단일 함수 |
| A. 중앙 sink + contextvars + 모델호출 계측 채택 | B. 사이트별 명시 persist | C. tracer 최소 포착 | |
|---|---|---|---|
| 완전성 | 높음(진입점 계측=누락 불가, 신규 단계 자동) | 중(사이트마다 추가, 미래 누락) | 높음(호출=포착) |
| lineage 품질 | 높음(contextvars ambient) | 높음(사이트 컨텍스트) | 낮음(프롬프트/타임스탬프만) |
| 수정 범위 | 중(3 진입점 + gpt-image 중앙화 + 스텝 컨텍스트 설정) | 큼(20+ 사이트) | 작음 |
| 리스크 | core 생성경로 접촉(→ non-fatal 필수) | 누락 위험 | edge/그룹 빈약 |
파이프라인 스텝/서비스가 자기 작업을 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]
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
GeminiImageClient.generate_image 반환부에서 capture_generated_image(out_bytes) 호출(이미 image_tracer 있는 자리 옆). DB 세션 접근이 필요 → §6 참고(세션 주입 또는 deferred queue).openai_client.images.generate/edit를 단일 wrapper generate_gpt_image(...)로 중앙화(새 모듈)하고, 그 wrapper에서 capture. 기존 호출부(location_floor_plan/outdoor_site_layout_provider/registered_pose_guide 등)는 wrapper 사용으로 치환.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차 제외.)
캔버스 엣지는 source_image_id / reference_image_ids / still_id 구조키로 형성된다(이미 구현됨). 따라서 GenContext.source_image_ids를 스텝이 채우면 중간물도 자동으로 상류 엣지를 갖는다. 예: 포즈 가이드는 source=[배경 plate, 등록 마네킹 base], 항공뷰 base는 source 없음(루트), 블로킹은 source=[항공뷰 base], 씬 스틸은 source=[블로킹/스케치, 배경, char/outlook ref...].
image_asset에 nullable 컬럼 소량 추가. 기존 컬럼(source_image_id/parent_image_id/reference_image_ids/still_id/shot_index/prompt_used)은 재사용.
| 컬럼 | 타입 | 용도 |
|---|---|---|
stage | Text null | 파이프라인 단계명(step). 캔버스 그룹/필터. |
pipeline_role | Text null | 용도(aerial_base/shot_blocking/pose_guide/composition_guide/anchor_sketch/candidate/i2i_step...). |
is_intermediate | Boolean default false | 중간물 여부(캔버스 기본 접기/펼치기·필터). |
generation_call_id | Text null (→ llm_call_log) | 생성 호출과 연결(프롬프트/검증 SOT 조인). |
candidate_index | Integer null | 같은 호출 내 후보 번호(거부 후보 구분). |
asset_type에 신규 값 'generated' 추가(또는 role을 type로). 캔버스 nodeStyle가 stage/role로 색/라벨 매핑. 마이그레이션은 add-column만(삭제 0, drift 없음 — 부팅 fail-fast 통과).
try/except로 감싸 어떤 실패도 생성 파이프라인을 막지 않음(로그만).generate_image)에는 보통 DB 세션이 없다. 옵션 (a) capture를 호출부(스텝, 세션 보유)에서 수행 — 진입점 계측 대신 "스텝이 결과 바이트를 sink에 넘김"; 또는 (b) deferred queue(contextvar에 (bytes, ctx) 적재 → 스텝 경계에서 flush). 권고: (b) deferred — 저수준 함수는 큐에 적재만, 스텝 트랜잭션 커밋 시 일괄 영속화. 생성 경로에 DB 의존성·지연을 안 준다.stage/pipeline_role/is_intermediate 노드 렌더: 색·라벨·아이콘(nodeStyle 확장), 중간물 그룹/필터(기본 접기 토글).reference_image_ids는 char/prop만 담아 outlook이 엣지 없이 떠 있음. 영속화 시 outlook ref도 source에 포함하거나, 캔버스가 별도 구조키로 outlook→씬 엣지 형성.thumb_url/full_url 있으면 이미지 표시(Codex 권고).generation_context(...) 배선(stage/role/source 채움) — 우선 가이드/스케치/항공뷰/블로킹/후보부터.Codex가 설계서 + 실제 코드 진입점을 추적해 A안 방향을 승인하고, "전부 영속화"를 실제로 만족시키기 위한 필수 보강 5건을 요구했다. 아래가 §4·§5·§6·§8 대비 우선한다.
contextvars가 자동 전파 안 됨. image_tracer.set_context / budget bind 패턴처럼 GenerationContext + queue handle을 worker에 명시 전파.temp_path + metadata만. step/runner finally에서 DB row insert + final generated path로 rename.finally에서 flush. 사용자가 "거부 후보까지 전부" 원하므로 step이 partial/failed여도 생성된 candidate는 남아야 함. business transaction rollback에 말려 사라지면 안 됨 → capture flush는 독립 non-fatal 세션.capture_skipped_no_context / capture_skipped_no_flush_scope) 기록 → 누락 감사 가능.role.startswith('final_') 같은 문자열 규칙 금지(사고 위험). 기본 capture_enabled=False — 명시적으로 intermediate capture scope가 열린 호출만 sink 저장. (GeminiImageClient는 reference/scene final도 전부 통과 → default-capture면 즉시 중복.)_register_image_assets 유지 + intermediate만 sink. promote 단일화는 1차 defer(동의).image_asset 중복 0".ImageAsset.reference_image_ids = "visible char/prop UUID lineage"(pipeline_graph_service가 reference edge 복원에 사용). intermediate의 bg/guide/sketch 입력을 여기 넣으면 reference lineage로 오염됨.input_image_ids(또는 source_image_ids_json, Text JSON)에 전체 입력 관계 저장. source_image_id는 primary/first source로 유지.pipeline_metadata_json(group_id, checkpoint key, rejection reason, model params, prompt hash 등 — 앞으로 늘어남).asset_type이 아니라 pipeline_role.(project_id, episode_id, is_intermediate), generation_call_id, (project_id, episode_id, pipeline_role).location_floor_plan.generate_floor_plan_image(공통 래퍼화됨) 외에 background_render.py, background_chain_render.py, floor_plan_render.py, space_set_bg_provider.py에 direct OpenAI 호출 잔존.gemini_i2i_editor.py가 GeminiImageClient 안 거치고 직접 REST 호출 후 b64 decode(scene variation / angle edit 경로).apply_fal_angle 단일 ✓.capture_artifact_bytes/path(...) API 필요(없으면 항공뷰/마스크/underlay 누락).call_gpt_image_bytes(openai_client, prompt, images/ref_paths, model, size, quality, budget_source, capture_context)만 신설 — API 호출 + b64 decode + capture enqueue만. 기존 retry/sanitizer/out_path write는 각 호출부에 남김. budget source label 기존값 유지(background_render.edit_single 등 → 운영 로그 불변). 고수준 정리는 별도 wave.capture_artifact API 만 먼저 + 단위테스트(결정론).gemini_i2i_editor + FAL + gpt primitive wrapper 계측. gpt direct call sites 전부 wrapper로 치환(고수준 동작 유지, audit checklist로 잠금).최대 리스크(Codex): "전부라 했는데 non-model/PIL 산출물·일부 direct image call이 빠지는 것" + "reference_image_ids 의미 오염으로 캔버스 edge가 섞이는 것". → 10.3/10.4로 봉쇄.