# Review package: 6872d15a..2183895f ## Commits 2183895f feat(lane): 엔티티 단계 마네킹 교체 계약 + 콘티 참조 생략 ## Files changed backend/app/modules/pipeline/still_recipe.py | 23 +++++- backend/app/services/still_recipe_service.py | 13 +++- backend/tests/unit/test_still_recipe_bgfirst.py | 86 ++++++++++++++++++++++ .../12.202607270537/stage_head_mannequin.md | 10 +++ 4 files changed, 129 insertions(+), 3 deletions(-) ## Diff diff --git a/backend/app/modules/pipeline/still_recipe.py b/backend/app/modules/pipeline/still_recipe.py index bef46602..e291c2f2 100644 --- a/backend/app/modules/pipeline/still_recipe.py +++ b/backend/app/modules/pipeline/still_recipe.py @@ -1211,26 +1211,45 @@ def build_bgfirst_bg_prompt( parts.append( load_prompt( _MODULE, "bg_reproject_tail", version=resolved).strip() ) return "\n\n".join(parts) def build_bgfirst_final_prompt( base_prompt: str, prompt_version: str = BGFIRST_PROMPT_VERSION, + mannequin: bool = False, ) -> str: """Step2 인물 삽입 프롬프트 — 정본 final_prompt: stage_head(배경 - EXACTLY 유지·스케치=인물 배치만·선 잔류 금지) + base 스틸 전문.""" + EXACTLY 유지·스케치=인물 배치만·선 잔류 금지) + base 스틸 전문. + + mannequin=True (2026-07-26 확정 흐름): 배경본의 회색 마네킹을 실제 + 인물로 교체하는 계약 스템(stage_head_mannequin)을 쓴다 — 위치·크기· + 자세·방향 유지, 미러 금지, 마네킹/스케치 선 잔류 0. lane 체인의 + Step1 은 마네킹을 **보존한 채** 배경만 실사화하므로(bg_fill_head + "do not turn them into people"), 이 교체 계약이 없으면 회색 마네킹이 + 최종 스틸까지 살아남는다. + + 스템 교체이지 덧붙임이 아닌 이유: v7 stage_head 는 ①"배경을 EXACTLY + 유지"(=마네킹 유지로 읽힌다)와 ②"SECOND attached image (LAYOUT + SKETCH)"(lane 은 conti=None 이라 그 참조 자체가 없다)를 품고 있어, + 앞에 절을 덧대면 두 문장이 그대로 남아 서로 싸운다. + + base 스틸 프롬프트 전문은 두 경로 모두 그대로 유지한다 — 자세 정본· + 조명·표정 사실감·소품 계약을 잃지 않는다. 기본값(False)은 기존 + 산출과 byte-identical. + """ resolved = resolve_prompt_version(prompt_version) + stem = "stage_head_mannequin" if mannequin else "stage_head" return ( - load_prompt(_MODULE, "stage_head", version=resolved).strip() + load_prompt(_MODULE, stem, version=resolved).strip() + "\n\n" + base_prompt ) def build_bgfirst_refs( *, bg: Path, conti: Optional[Path], char_refs: Sequence[Tuple[str, Any]], diff --git a/backend/app/services/still_recipe_service.py b/backend/app/services/still_recipe_service.py index 3d957241..05864d27 100644 --- a/backend/app/services/still_recipe_service.py +++ b/backend/app/services/still_recipe_service.py @@ -2589,22 +2589,33 @@ def run_still_recipe_generation( conti=conti, char_refs=char_refs, prop_refs=prop_refs, structure_seed=structure_seed_path, seed_bg=( seed_bg_path if _authority_kind == "seed_bg" else None ), prompt_version=_pack, ) _labels2 = roll_labels(2) + # lane 체인만 마네킹 교체 스템(v12 stage_head_mannequin) — + # Step1 이 마네킹을 보존한 채 배경만 실사화하고 Step2 참조에 + # 콘티가 없으므로, v7 stage_head("배경 EXACTLY 유지"+없는 + # LAYOUT SKETCH 지시)를 그대로 쓰면 회색 마네킹이 최종까지 + # 살아남는다. 비 lane 은 인자 기본값과 동일한 v7 그대로. _chain_prompt = build_bgfirst_final_prompt( - prompt_chain or prompt) + prompt_chain or prompt, + prompt_version=( + _BGF_LANE_PACK_SEL if lane_chain + else _BGFIRST_PACK + ), + mannequin=lane_chain, + ) if _chain_only: # 표준 멀티롤 — 같은 체인 프롬프트/참조로 N롤 생성 후 # 기본 판정이 최선을 고른다(후보 비교가 아니라 품질 # 선택). bgfirst 2택1 전용 judge/texts 는 2라벨 스키마라 # 여기 쓰지 않는다. _labels_n = roll_labels(roll_count) sel_path, record = _run_branch( f"still_{tag}", refs_chain, tag, recipe_dir / tag, roll_prompts={ln: _chain_prompt for ln in _labels_n}, roll_refs={ln: refs_chain for ln in _labels_n}, diff --git a/backend/tests/unit/test_still_recipe_bgfirst.py b/backend/tests/unit/test_still_recipe_bgfirst.py index 6654573e..2654878f 100644 --- a/backend/tests/unit/test_still_recipe_bgfirst.py +++ b/backend/tests/unit/test_still_recipe_bgfirst.py @@ -1222,10 +1222,96 @@ def test_lane_facts_are_not_wired_into_non_lane_paths(): src = _service_src() assert "\n lane_fill=_lane_fill," in src assert ( "\n place_facts_block=(\n" " _lane_place_facts(" "lane_entry.get(\"group_id\") or \"\")\n" " if _lane_fill else \"\"\n" " )," in src ) assert "_lane_fill = _authority_kind == LANE_CONTI_ONLY" in src + + +# ── T8: 엔티티 단계 마네킹 교체 계약 (2026-07-26 확정 흐름) ─────────── +# +# Step1 이 마네킹을 **보존한 채** 배경만 실사화하므로, Step2 가 마네킹을 +# 실제 인물로 바꾸지 않으면 회색 마네킹이 최종 스틸까지 살아남는다. + + +def test_mannequin_stage_head_replaces_figures(): + from app.modules.pipeline.still_recipe import ( + BGFIRST_LANE_PROMPT_VERSION, + build_bgfirst_final_prompt, + ) + + p = build_bgfirst_final_prompt( + "SAMPLE_FIXTURE_BASE_STILL_PROMPT", + prompt_version=BGFIRST_LANE_PROMPT_VERSION, mannequin=True) + low = p.lower() + assert "mannequin" in low + assert "never mirrored" in low or "not mirror" in low + # base 스틸 프롬프트 전문이 유지돼야 한다(자세·조명 계약 유실 금지) + assert "SAMPLE_FIXTURE_BASE_STILL_PROMPT" in p + + +def test_mannequin_stage_head_points_only_at_attached_refs(): + """lane Step2 참조는 [배경본, 엔티티] — LAYOUT SKETCH 슬롯이 없다. + + v7 stage_head 를 그대로 쓰면 ①"SECOND attached image (LAYOUT SKETCH)" + 가 **없는** 참조를 가리켜 모델이 인물 배치를 즉흥으로 지어내고 + ②"배경을 EXACTLY 유지"가 곧 "회색 마네킹을 유지"로 읽힌다. 스템을 + 앞에 덧붙이는 것으로는 두 문장이 남아 있어 못 고친다. + """ + from app.modules.pipeline.still_recipe import ( + BGFIRST_LANE_PROMPT_VERSION, + build_bgfirst_final_prompt, + ) + + p = build_bgfirst_final_prompt( + "SAMPLE_FIXTURE_BASE_STILL_PROMPT", + prompt_version=BGFIRST_LANE_PROMPT_VERSION, mannequin=True) + stem = p[: -len("\n\nSAMPLE_FIXTURE_BASE_STILL_PROMPT")] + assert "LAYOUT SKETCH" not in stem + assert "SECOND attached image" not in stem + # 배경 자체는 여전히 고정 — 유지 대상이 '첫 이미지의 배경'으로 + # 좁혀졌는지(=마네킹은 유지 대상이 아님) 확인 + low = stem.lower() + assert "background" in low + assert "character reference" in low + # 잔류 0 계약 + assert "mannequin" in low.split("keep exactly", 1)[-1] + + +def test_final_prompt_default_stays_byte_identical(): + """비-lane 경로 불변 — 새 인자 기본값이 오늘의 산출을 바꾸지 않는다.""" + from app.modules.prompt_loader import load_prompt + + base = "SAMPLE_FIXTURE_BASE_STILL_PROMPT" + default = build_bgfirst_final_prompt(base) + assert default == build_bgfirst_final_prompt(base, mannequin=False) + assert default == ( + load_prompt( + "still_recipe", "stage_head", + version=resolve_prompt_version(BGFIRST_PROMPT_VERSION), + ).strip() + + "\n\n" + + base + ) + + +def test_service_threads_mannequin_stem_for_lane(): + """lane 만 마네킹 스템·lane 팩을 받는다 — 배선을 소스로 잠근다. + + 순수 함수 테스트는 기본값을 되돌려도 전부 초록이라(서비스가 인자를 + 안 넘기면 v7 stage_head 가 그대로 나간다) 호출부를 함께 핀한다. + """ + src = _service_src() + assert ( + "\n _chain_prompt = build_bgfirst_final_prompt(\n" + " prompt_chain or prompt,\n" + " prompt_version=(\n" + " _BGF_LANE_PACK_SEL if lane_chain\n" + " else _BGFIRST_PACK\n" + " ),\n" + " mannequin=lane_chain,\n" + " )" in src + ) diff --git a/prompts/_base/still_recipe/12.202607270537/stage_head_mannequin.md b/prompts/_base/still_recipe/12.202607270537/stage_head_mannequin.md new file mode 100644 index 00000000..af9fc295 --- /dev/null +++ b/prompts/_base/still_recipe/12.202607270537/stage_head_mannequin.md @@ -0,0 +1,10 @@ +Replace every grey mannequin in the FIRST attached image with the real +person the CHARACTER REFERENCE images show, and output a photorealistic +film still. + +KEEP EXACTLY: the background of the first image, the camera framing, +and each mannequin's position, size, pose and the direction it is +turned. A person must stand where their mannequin stood, at the same +scale, facing the same way — never mirrored, never re-staged. No +mannequin, grey figure or sketch line may remain anywhere in the +output.