Phase X-B 진행중: 유형 B 조립 + 텍스트 보존 강화 + 원본 MDX 복구

X-B-3~5 완료:
- space_allocator: build_containers_type_b() 추가
- assemble_stage2: _assemble_type_b() 추가 (소제목 카드형)
- pipeline.py: layout_template 분기 (A/B)
- pipeline_context: Analysis.layout_template 필드
- validators: 유형 B 검증 완화

텍스트 보존 강화:
- KEI_PROMPT: 제목 원본 그대로, 텍스트 재작성 금지
- KEI_STRUCTURED_TEXT_PROMPT: 소제목 유지, 원본 문장 그대로

원본 MDX 복구:
- samples/mdx_batch/02.mdx: 표 데이터 누락 수정 (원본에서 재복사)

미해결 (다음 세션):
- 들여쓰기: 대제목→중제목→소제목→본문 계층 구조
- 이미지 캡션: [그림 제목] 형식 (대괄호 포함)
- 상단 컨테이너: 빈칸 위로 붙이기
- 카드 디자인: 안전과품질/생산성향상/소통과신뢰 디자인 개선
- 제목: Kei가 원본 제목 바꾸는 문제 잔존

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 11:28:03 +09:00
parent bc7829b08b
commit a8fe20e08e
7 changed files with 545 additions and 32 deletions

View File

@@ -439,6 +439,143 @@ def calculate_container_specs(
return specs
# ══════════════════════════════════════
# Phase X-B: 유형 B 컨테이너 생성
# ══════════════════════════════════════
def build_containers_type_b(
page_structure: dict[str, Any],
slide_width: int = 1280,
slide_height: int = 720,
image_sizes: list[dict] | None = None,
) -> dict[str, ContainerSpec]:
"""유형 B: 상단(top) + 하단 2분할(bottom_left/right) + 결론(footer).
기존 유형 A(calculate_container_specs)를 건드리지 않는 별도 함수.
모든 크기는 슬라이드 크기 + weight + zone에서 동적 계산. 하드코딩 없음.
Args:
page_structure: Kei 판단 {"핵심목표": {"zone": "top", "topic_ids": [1], "weight": 0.45}, ...}
slide_width: 슬라이드 너비
slide_height: 슬라이드 높이
image_sizes: 이미지 정보 (비율 계산용)
"""
from src.fit_verifier import _load_design_tokens
tokens = _load_design_tokens()
pad = tokens["spacing_page"]
header_h = tokens.get("header_height", 66)
gap_block = tokens["spacing_block"]
gap_small = tokens["spacing_small"]
inner_w = slide_width - pad * 2
# 역할을 zone별로 분류
top_roles = [] # zone=top
bottom_roles = [] # zone=bottom_left, bottom_right
footer_role = None # zone=footer
for role_name, info in page_structure.items():
if not isinstance(info, dict):
continue
zone = info.get("zone", "")
if zone == "top":
top_roles.append((role_name, info))
elif zone in ("bottom_left", "bottom_right"):
bottom_roles.append((role_name, info))
elif zone == "footer":
footer_role = (role_name, info)
# 전체 가용 높이: 슬라이드 - 패딩*2 - 헤더 - gap
total_available = slide_height - pad * 2 - header_h - gap_block
# footer 높이: weight 비율 (최소 보장)
footer_weight = footer_role[1].get("weight", 0.1) if footer_role else 0.1
footer_h_raw = int(total_available * footer_weight)
_footer_min = int(14 * tokens.get("line_height_ko", 1.7) + pad)
footer_h = max(_footer_min, footer_h_raw)
# 중간 영역: footer + gap 제외
middle_h = total_available - footer_h - gap_block
# 상단/하단 높이: weight 비율로
top_weight = sum(info.get("weight", 0) for _, info in top_roles)
bottom_weight = sum(info.get("weight", 0) for _, info in bottom_roles)
total_mid_weight = top_weight + bottom_weight
if total_mid_weight <= 0:
total_mid_weight = 1
top_h = int(middle_h * top_weight / total_mid_weight)
bottom_h = middle_h - top_h - gap_small # gap_small: 상단-하단 사이
# 상단: 이미지가 있으면 좌텍스트+우이미지 나란히 → 폭 분할
img_ratio = 0
if image_sizes:
for img in image_sizes:
r = img.get("ratio", 0)
if r > 0:
img_ratio = r
break
if img_ratio > 0:
# 이미지 높이 = top_h, 이미지 폭 = top_h * ratio
img_w = min(int(top_h * img_ratio), int(inner_w * 0.45)) # 최대 45%
text_w = inner_w - img_w - gap_block
else:
text_w = inner_w
img_w = 0
specs = {}
# 상단 역할
for role_name, info in top_roles:
specs[role_name] = ContainerSpec(
role=role_name,
zone="top",
topic_ids=info.get("topic_ids", []),
weight=info.get("weight", 0),
height_px=top_h,
width_px=text_w if img_w > 0 else inner_w, # 이미지 있으면 텍스트 폭만
max_height_cost=_max_allowed_height_cost(top_h),
block_constraints={
"img_width_px": img_w,
"img_height_px": top_h if img_w > 0 else 0,
"has_image": img_w > 0,
},
)
# 하단 역할: 2분할
bottom_col_w = (inner_w - gap_block) // 2
for role_name, info in bottom_roles:
specs[role_name] = ContainerSpec(
role=role_name,
zone=info.get("zone", "bottom_left"),
topic_ids=info.get("topic_ids", []),
weight=info.get("weight", 0),
height_px=bottom_h,
width_px=bottom_col_w,
max_height_cost=_max_allowed_height_cost(bottom_h),
block_constraints={},
)
# 결론
if footer_role:
rn, info = footer_role
specs[rn] = ContainerSpec(
role=rn,
zone="footer",
topic_ids=info.get("topic_ids", []),
weight=info.get("weight", 0),
height_px=footer_h,
width_px=inner_w,
max_height_cost="low",
block_constraints={},
)
logger.info(
f"[X-B-3] 유형 B 컨테이너: "
+ ", ".join(f"{r}={s.height_px}px(w={s.width_px})" for r, s in specs.items())
)
return specs
def _max_allowed_height_cost(container_height_px: int) -> str:
"""컨테이너 높이에서 허용되는 최대 height_cost.