WIP: overflow 루프 구현 + Selenium 기반 에스컬레이션 트리거
- pipeline: filled→측정→Kei 재판단→재조립 루프 (최대 3회) - pipeline: Selenium overflow가 있으면 calculate_fit과 무관하게 에스컬레이션 - 문제: build_escalation_report가 Selenium 측정 결과를 포함하지 않아 Kei가 빈 결정 반환 - 다음: content family 기반 범용 파이프라인 설계 필요 (Phase X-C) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -517,46 +517,59 @@ async def generate_slide(
|
||||
# context에 sub_layouts 반영 후 filled 생성
|
||||
context = context.model_copy(update={"sub_layouts": pre_sub_layouts})
|
||||
|
||||
# ── filled: before 컨테이너에 블록+텍스트 채움 → Selenium 측정 ──
|
||||
# ── filled→측정→Kei 재판단 루프 (최대 3회) ──
|
||||
kei_decisions = []
|
||||
updated_containers = dict(context.containers)
|
||||
MAX_FIT_RETRIES = 3
|
||||
|
||||
for fit_round in range(MAX_FIT_RETRIES):
|
||||
# context에 현재 kei_decisions 반영 (2회차부터 popup 결정이 반영됨)
|
||||
if kei_decisions:
|
||||
context = context.model_copy(update={
|
||||
"enhancement_result": {
|
||||
**(context.enhancement_result or {}),
|
||||
"kei_decisions": kei_decisions,
|
||||
},
|
||||
"containers": updated_containers,
|
||||
})
|
||||
|
||||
# ── filled: 컨테이너에 블록+텍스트 채움 ──
|
||||
filled_html = assemble_slide_html(context)
|
||||
(steps_dir / "stage_1_8_filled.html").write_text(
|
||||
(steps_dir / f"stage_1_8_filled{'_r'+str(fit_round) if fit_round else ''}.html").write_text(
|
||||
filled_html.replace('</head><body>', '</head><body>\n'
|
||||
'<div style="font-size:16px;font-weight:bold;margin-bottom:4px;">'
|
||||
'Stage 1.8: filled (블록+텍스트 채운 상태)</div>\n'
|
||||
f'<div style="font-size:16px;font-weight:bold;margin-bottom:4px;">'
|
||||
f'Stage 1.8: filled (round {fit_round+1}/{MAX_FIT_RETRIES})</div>\n'
|
||||
'<div style="font-size:11px;color:#666;margin-bottom:8px;">'
|
||||
'before 컨테이너에 블록+텍스트를 채움. 넘치는 영역 확인.</div>\n', 1),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# ── Selenium 측정 ──
|
||||
filled_measurement = await asyncio.to_thread(measure_rendered_heights, filled_html)
|
||||
logger.info(f"[Stage 1.8] filled 측정 완료")
|
||||
|
||||
# ── 판단: 넘치는 영역 처리 ──
|
||||
updated_containers = dict(context.containers) # 복사
|
||||
logger.info(f"[Stage 1.8] round {fit_round+1} 측정 완료")
|
||||
|
||||
# ── overflow 확인 ──
|
||||
has_overflow = False
|
||||
for zone_name, zone_data in filled_measurement.get("zones", {}).items():
|
||||
if zone_data.get("overflowed"):
|
||||
excess = zone_data.get("excess_px", 0)
|
||||
scroll_h = zone_data.get("scrollHeight", 0)
|
||||
has_overflow = True
|
||||
|
||||
if zone_name == "sidebar":
|
||||
# sidebar 예외: 세로 확장 허용
|
||||
for role, ci in updated_containers.items():
|
||||
if ci.zone == "sidebar":
|
||||
new_h = max(ci.height_px, scroll_h + 10) # 여유 10px
|
||||
new_h = max(ci.height_px, scroll_h + 10)
|
||||
updated_containers[role] = ci.model_copy(update={"height_px": new_h})
|
||||
logger.info(f"[Stage 1.8] sidebar 예외 확장: {role} {ci.height_px}px → {new_h}px")
|
||||
logger.info(f"[Stage 1.8] sidebar 확장: {role} → {new_h}px")
|
||||
else:
|
||||
logger.info(f"[Stage 1.8] {zone_name} overflow +{excess}px")
|
||||
|
||||
elif zone_name == "body":
|
||||
# body: 배경↔본심 재배분으로 처리 (후속 redistribute에서)
|
||||
logger.info(f"[Stage 1.8] body overflow +{excess}px — 재배분 필요")
|
||||
if not has_overflow:
|
||||
logger.info(f"[Stage 1.8] round {fit_round+1}: overflow 없음 — 완료")
|
||||
break
|
||||
|
||||
elif zone_name in ("top", "bottom"):
|
||||
# Type B: top/bottom overflow → 후속 에스컬레이션에서 Kei 요약 요청
|
||||
logger.info(f"[Stage 1.8] {zone_name} overflow +{excess}px — 콘텐츠 축소 필요")
|
||||
|
||||
# containers_dict 업데이트 (sidebar 확장 반영)
|
||||
# ── fit 계산 + 재배분 ──
|
||||
for role, ci in updated_containers.items():
|
||||
containers_dict[role] = {
|
||||
"height_px": ci.height_px,
|
||||
@@ -564,7 +577,6 @@ async def generate_slide(
|
||||
"zone": ci.zone,
|
||||
}
|
||||
|
||||
# ── fit 계산 + 재배분 (업데이트된 컨테이너 기준) ──
|
||||
fit_analysis = calculate_fit(
|
||||
topics=[t.model_dump() for t in context.topics],
|
||||
page_structure=context.page_structure.roles,
|
||||
@@ -576,8 +588,7 @@ async def generate_slide(
|
||||
)
|
||||
fit_analysis = redistribute(fit_analysis, containers_dict)
|
||||
|
||||
# Type B: zone 간 재배분 (top↔bottom)
|
||||
# redistribute는 같은 zone 내에서만 동작하므로, Type B는 zone 간 여유를 수동 이전
|
||||
# Type B: zone 간 재배분
|
||||
if context.analysis.layout_template in ("B", "B'"):
|
||||
deficit_roles = [(r, rf.shortfall_px) for r, rf in fit_analysis.roles.items() if rf.shortfall_px > 0]
|
||||
surplus_roles = [(r, abs(rf.shortfall_px)) for r, rf in fit_analysis.roles.items() if rf.shortfall_px < -8]
|
||||
@@ -594,41 +605,41 @@ async def generate_slide(
|
||||
share = transferable * (surplus / total_surplus)
|
||||
old = fit_analysis.redistribution.get(role, fit_analysis.roles[role].allocated_px)
|
||||
fit_analysis.redistribution[role] = old - share
|
||||
logger.info(f"[Stage 1.8] Type B zone 간 재배분: {transferable:.0f}px 이전")
|
||||
logger.info(f"[Stage 1.8] zone 간 재배분: {transferable:.0f}px 이전")
|
||||
|
||||
# ── after: 조정된 컨테이너 ──
|
||||
# 재배분된 컨테이너 크기 적용
|
||||
for role, ci in updated_containers.items():
|
||||
new_h = fit_analysis.redistribution.get(role, ci.height_px) if fit_analysis.redistribution else ci.height_px
|
||||
updated_containers[role] = ci.model_copy(update={"height_px": int(new_h)})
|
||||
|
||||
logger.info(f"[Stage 1.8] after: " + ", ".join(
|
||||
logger.info(f"[Stage 1.8] round {fit_round+1} after: " + ", ".join(
|
||||
f"{r}={ci.height_px}px" for r, ci in updated_containers.items()
|
||||
))
|
||||
|
||||
# Step 3: Kei 에스컬레이션 (필요 시)
|
||||
if fit_analysis.needs_escalation:
|
||||
# ── Kei 에스컬레이션: overflow 있으면 팝업 분리 판단 요청 ──
|
||||
# calculate_fit의 needs_escalation 또는 Selenium 측정의 실제 overflow
|
||||
if fit_analysis.needs_escalation or has_overflow:
|
||||
from src.kei_client import call_kei_fit_escalation
|
||||
report = build_escalation_report(fit_analysis)
|
||||
logger.info(f"[Stage 1.8] 에스컬레이션 필요:\n{report}")
|
||||
logger.info(f"[Stage 1.8] round {fit_round+1} 에스컬레이션 필요")
|
||||
kei_result = await call_kei_fit_escalation(
|
||||
fit_report=report,
|
||||
topics=[t.model_dump() for t in context.topics],
|
||||
content_summary=context.raw_content[:1500],
|
||||
role_names=list(context.page_structure.roles.keys()),
|
||||
)
|
||||
kei_decisions = []
|
||||
if kei_result:
|
||||
kei_decisions = kei_result.get("decisions", [])
|
||||
logger.info(f"[Stage 1.8] Kei 결정: {len(kei_decisions)}건")
|
||||
for d in kei_decisions:
|
||||
action = d.get("action", "")
|
||||
target_role = d.get("role", "")
|
||||
detail = d.get("detail", "")
|
||||
logger.info(f"[V-4] {target_role} → {action}: {detail}")
|
||||
if action == "restructure" and target_role in fit_analysis.roles:
|
||||
fit_analysis = redistribute(fit_analysis, containers_dict)
|
||||
new_decisions = kei_result.get("decisions", [])
|
||||
kei_decisions.extend(new_decisions)
|
||||
logger.info(f"[Stage 1.8] Kei 결정 {len(new_decisions)}건 추가 (누적 {len(kei_decisions)}건)")
|
||||
for d in new_decisions:
|
||||
logger.info(f"[V-4] {d.get('role','')} → {d.get('action','')}: {d.get('detail','')[:60]}")
|
||||
else:
|
||||
kei_decisions = []
|
||||
logger.warning(f"[Stage 1.8] round {fit_round+1} Kei 응답 없음 — 루프 종료")
|
||||
break
|
||||
else:
|
||||
logger.info(f"[Stage 1.8] round {fit_round+1}: 재배분으로 해결됨")
|
||||
break
|
||||
|
||||
# Step 4: 보강 제안 분석
|
||||
enhancements = analyze_enhancements(
|
||||
|
||||
Reference in New Issue
Block a user