03번 B' 정상 동작: 가로 3단 카드 + overflow 해소 + 하단 균형
- block_assembler B': 카드 3개 이상 + 이미지 없음 → 가로(row) 배치 - block_assembler B': section title이 카드 제목, D1은 카드 내 bold 불릿 - block_assembler: overflow:auto → overflow:hidden, [핵심요약:] 마커 필터 - block_assembler: \x01 바이트 수정 - pipeline: Selenium 실측 기반 zone 간 재배분 (allocated-scrollHeight로 slack 계산) - pipeline: surplus 최대 50%만 이전 (하단 최소 공간 보장) - pipeline: bottom_left/bottom_right → Selenium bottom zone 매핑 - kei_client: 상단은 팝업 대상 제외, 하단에서만 팝업 분리 결과: 02번/03번 모두 overflow 없이 정상 출력 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -590,6 +590,8 @@ def _assemble_slide_html_type_b(ctx: "PipelineContext", title_text: str = "") ->
|
|||||||
continue
|
continue
|
||||||
if re.search(r'\[이미지:', stripped) or re.match(r'^!\[', stripped):
|
if re.search(r'\[이미지:', stripped) or re.match(r'^!\[', stripped):
|
||||||
continue
|
continue
|
||||||
|
if re.search(r'\[핵심요약:', stripped):
|
||||||
|
continue
|
||||||
content_lines.append(stripped)
|
content_lines.append(stripped)
|
||||||
|
|
||||||
popup_html = _popup_links_html(popup_titles, font_size)
|
popup_html = _popup_links_html(popup_titles, font_size)
|
||||||
@@ -1012,11 +1014,17 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
|
|||||||
continue
|
continue
|
||||||
if re.search(r'\[이미지:', stripped) or re.match(r'^!\[', stripped):
|
if re.search(r'\[이미지:', stripped) or re.match(r'^!\[', stripped):
|
||||||
continue
|
continue
|
||||||
|
if re.search(r'\[핵심요약:', stripped):
|
||||||
|
continue
|
||||||
content_lines.append(stripped)
|
content_lines.append(stripped)
|
||||||
|
|
||||||
popup_html = _popup_links_html(popup_titles, font_size)
|
popup_html = _popup_links_html(popup_titles, font_size)
|
||||||
|
|
||||||
# 소제목(### 또는 D1:) + 불릿(D2:)을 카드형으로 분리
|
# B': ### (section title) = 카드 제목. D1/D2는 카드 내부 불릿.
|
||||||
|
# ###이 있으면 카드는 section 단위. D1은 카드 안의 bold 불릿.
|
||||||
|
# ###이 없으면 D1이 카드 제목 (02번 방식).
|
||||||
|
has_section_titles = any(line.startswith("### ") for line in content_lines)
|
||||||
|
|
||||||
sections = []
|
sections = []
|
||||||
current_section = ("", [])
|
current_section = ("", [])
|
||||||
for line in content_lines:
|
for line in content_lines:
|
||||||
@@ -1025,8 +1033,12 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
|
|||||||
sections.append(current_section)
|
sections.append(current_section)
|
||||||
current_section = (line.lstrip("# ").strip(), [])
|
current_section = (line.lstrip("# ").strip(), [])
|
||||||
elif re.match(r'^D1:\s*', line):
|
elif re.match(r'^D1:\s*', line):
|
||||||
# D1 = 1단 불릿 = 소제목 (카드 제목)
|
|
||||||
title_text = re.sub(r'^D1:\s*', '', line).lstrip("• ")
|
title_text = re.sub(r'^D1:\s*', '', line).lstrip("• ")
|
||||||
|
if has_section_titles:
|
||||||
|
# ### 카드 안의 bold 불릿
|
||||||
|
current_section[1].append(f'<strong>{_bold(title_text, rn)}</strong>')
|
||||||
|
else:
|
||||||
|
# 02번 방식: D1이 카드 제목
|
||||||
if current_section[0] or current_section[1]:
|
if current_section[0] or current_section[1]:
|
||||||
sections.append(current_section)
|
sections.append(current_section)
|
||||||
current_section = (_bold(title_text, rn), [])
|
current_section = (_bold(title_text, rn), [])
|
||||||
@@ -1055,16 +1067,12 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
|
|||||||
card_gap = max(3, int(font_size * 0.4))
|
card_gap = max(3, int(font_size * 0.4))
|
||||||
indent_body = int(font_size * 1.2)
|
indent_body = int(font_size * 1.2)
|
||||||
|
|
||||||
# B': 상단이 popup 대상이면 소제목만 유지, 하위 불릿 제거
|
# B': 상단은 핵심이므로 항상 불릿 표시 (팝업 대상 아님)
|
||||||
top_is_popup = rn in popup_roles
|
|
||||||
|
|
||||||
bullets = ""
|
bullets = ""
|
||||||
if len(sections) > 1 and sections[0][0]:
|
if len(sections) > 1 and sections[0][0]:
|
||||||
for ci, (sec_title, sec_items) in enumerate(sections):
|
for ci, (sec_title, sec_items) in enumerate(sections):
|
||||||
bg, text_color = _card_colors[ci % len(_card_colors)]
|
bg, text_color = _card_colors[ci % len(_card_colors)]
|
||||||
if top_is_popup:
|
|
||||||
items_html = ""
|
|
||||||
else:
|
|
||||||
items_html = "".join(
|
items_html = "".join(
|
||||||
f'<div style="padding-left:{indent_body}px;margin-bottom:1px;">'
|
f'<div style="padding-left:{indent_body}px;margin-bottom:1px;">'
|
||||||
f'<span style="color:{text_color};font-size:{font_size-1}px;line-height:1.5;">• {item}</span></div>'
|
f'<span style="color:{text_color};font-size:{font_size-1}px;line-height:1.5;">• {item}</span></div>'
|
||||||
@@ -1112,6 +1120,19 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
|
|||||||
|
|
||||||
topic_title = _bold(topic_title_from_section or rn, rn)
|
topic_title = _bold(topic_title_from_section or rn, rn)
|
||||||
|
|
||||||
|
# B': 카드 3개 이상 + 이미지 없음 → 가로 배치
|
||||||
|
card_count = len(sections) if len(sections) > 1 and sections[0][0] else 0
|
||||||
|
use_row = card_count >= 3 and not (has_image and img_html)
|
||||||
|
|
||||||
|
if use_row:
|
||||||
|
top_html = (
|
||||||
|
f'<div style="position:relative;height:100%;padding:{gap_small}px;box-sizing:border-box;'
|
||||||
|
f'display:flex;flex-direction:column;">'
|
||||||
|
f'{popup_html}'
|
||||||
|
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{topic_title}</div>'
|
||||||
|
f'<div style="display:flex;flex-direction:row;gap:{card_gap}px;flex:1;">{bullets}</div></div>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
top_html = (
|
top_html = (
|
||||||
f'<div style="position:relative;height:100%;padding:{gap_small}px;box-sizing:border-box;'
|
f'<div style="position:relative;height:100%;padding:{gap_small}px;box-sizing:border-box;'
|
||||||
f'display:flex;flex-direction:column;justify-content:space-between;">'
|
f'display:flex;flex-direction:column;justify-content:space-between;">'
|
||||||
@@ -1200,6 +1221,8 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
|
|||||||
clean_plain = re.sub(r'<[^>]+>', '', clean).strip()
|
clean_plain = re.sub(r'<[^>]+>', '', clean).strip()
|
||||||
if clean_plain in table_cell_texts or clean_plain == "➠":
|
if clean_plain in table_cell_texts or clean_plain == "➠":
|
||||||
continue
|
continue
|
||||||
|
if re.search(r'\[핵심요약:', clean):
|
||||||
|
continue
|
||||||
if clean:
|
if clean:
|
||||||
clean = _bold(clean, rn)
|
clean = _bold(clean, rn)
|
||||||
_pad = bl_indent * depth
|
_pad = bl_indent * depth
|
||||||
@@ -1208,7 +1231,7 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
|
|||||||
bul += f'<div style="padding-left:{_pad}px;font-size:{fs}px;margin-bottom:2px;{weight}">• {clean}</div>\n'
|
bul += f'<div style="padding-left:{_pad}px;font-size:{fs}px;margin-bottom:2px;{weight}">• {clean}</div>\n'
|
||||||
|
|
||||||
bl_html = (
|
bl_html = (
|
||||||
f'<div style="height:100%;padding:{gap_small}px;box-sizing:border-box;overflow-y:auto;">'
|
f'<div style="height:100%;padding:{gap_small}px;box-sizing:border-box;overflow:hidden;">'
|
||||||
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{_bold(sub_title, rn)}</div>'
|
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{_bold(sub_title, rn)}</div>'
|
||||||
f'{table_html_bl}'
|
f'{table_html_bl}'
|
||||||
f'<div style="line-height:1.55;color:#333;">{bul}</div></div>'
|
f'<div style="line-height:1.55;color:#333;">{bul}</div></div>'
|
||||||
@@ -1232,6 +1255,8 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
|
|||||||
depth = int(dm.group(1))
|
depth = int(dm.group(1))
|
||||||
stripped = re.sub(r'^D\d+:\s*', '', stripped)
|
stripped = re.sub(r'^D\d+:\s*', '', stripped)
|
||||||
clean = stripped.lstrip("- ").lstrip("• ")
|
clean = stripped.lstrip("- ").lstrip("• ")
|
||||||
|
if re.search(r'\[핵심요약:', clean):
|
||||||
|
continue
|
||||||
if clean:
|
if clean:
|
||||||
clean = _bold(clean, rn)
|
clean = _bold(clean, rn)
|
||||||
_pad = bl_indent * depth
|
_pad = bl_indent * depth
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1364,10 +1364,10 @@ KEI_FIT_ESCALATION_PROMPT = """당신은 슬라이드 설계 전문가이다.
|
|||||||
- overflow가 없는 영역은 건드리지 않는다.
|
- overflow가 없는 영역은 건드리지 않는다.
|
||||||
|
|
||||||
## 판단 기준
|
## 판단 기준
|
||||||
- overflow가 발생한 영역만 대상. 다른 영역은 결정하지 않는다.
|
- **상단(top zone)은 핵심 내용이므로 팝업 대상에서 제외.** 상단이 넘치면 하단에서 공간을 확보.
|
||||||
- 해당 영역 내에서 **하위 불릿(상세 설명)만** 팝업 대상.
|
- 하단(bottom zone)에서 중요도가 낮은 콘텐츠를 팝업으로 분리.
|
||||||
|
- 표 데이터가 큰 경우 → 팝업 분리 1순위.
|
||||||
- 소제목/카드 제목은 슬라이드에 남겨서 구조를 유지.
|
- 소제목/카드 제목은 슬라이드에 남겨서 구조를 유지.
|
||||||
- 표 데이터가 큰 경우 → 표를 팝업으로 분리하고 요약만 남김.
|
|
||||||
- 한 번에 1~2개 역할만 결정. 전부 다 팝업으로 빼지 않는다.
|
- 한 번에 1~2개 역할만 결정. 전부 다 팝업으로 빼지 않는다.
|
||||||
|
|
||||||
## 출력 (JSON만. 설명 없이.)
|
## 출력 (JSON만. 설명 없이.)
|
||||||
|
|||||||
@@ -588,14 +588,40 @@ async def generate_slide(
|
|||||||
)
|
)
|
||||||
fit_analysis = redistribute(fit_analysis, containers_dict)
|
fit_analysis = redistribute(fit_analysis, containers_dict)
|
||||||
|
|
||||||
# Type B: zone 간 재배분
|
# Type B: Selenium 실측 기반 zone 간 재배분
|
||||||
if context.analysis.layout_template in ("B", "B'"):
|
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]
|
# Selenium 측정에서 실제 overflow/여유를 가져옴
|
||||||
surplus_roles = [(r, abs(rf.shortfall_px)) for r, rf in fit_analysis.roles.items() if rf.shortfall_px < -8]
|
zone_to_roles = {}
|
||||||
|
for role, ci in updated_containers.items():
|
||||||
|
# Selenium CSS 클래스 매핑: bottom_left/bottom_right → bottom
|
||||||
|
z = ci.zone
|
||||||
|
if z in ("bottom_left", "bottom_right"):
|
||||||
|
z = "bottom"
|
||||||
|
if z not in zone_to_roles:
|
||||||
|
zone_to_roles[z] = []
|
||||||
|
zone_to_roles[z].append(role)
|
||||||
|
|
||||||
|
deficit_roles = []
|
||||||
|
surplus_roles = []
|
||||||
|
for zn, zd in filled_measurement.get("zones", {}).items():
|
||||||
|
excess = zd.get("excess_px", 0)
|
||||||
|
scroll_h = zd.get("scrollHeight", 0)
|
||||||
|
roles_in_zone = zone_to_roles.get(zn, [])
|
||||||
|
if excess > 0:
|
||||||
|
for r in roles_in_zone:
|
||||||
|
deficit_roles.append((r, float(excess)))
|
||||||
|
elif roles_in_zone:
|
||||||
|
# 실제 콘텐츠(scrollHeight)와 할당 높이 차이로 여유 계산
|
||||||
|
allocated = sum(updated_containers[r].height_px for r in roles_in_zone if r in updated_containers)
|
||||||
|
slack = allocated - scroll_h
|
||||||
|
if slack > 8:
|
||||||
|
for r in roles_in_zone:
|
||||||
|
surplus_roles.append((r, float(slack)))
|
||||||
if deficit_roles and surplus_roles:
|
if deficit_roles and surplus_roles:
|
||||||
total_deficit = sum(d for _, d in deficit_roles)
|
total_deficit = sum(d for _, d in deficit_roles)
|
||||||
total_surplus = sum(s for _, s in surplus_roles)
|
total_surplus = sum(s for _, s in surplus_roles)
|
||||||
transferable = min(total_deficit, total_surplus)
|
# surplus의 최대 50%만 이전 — 하단 최소 공간 보장
|
||||||
|
transferable = min(total_deficit, total_surplus * 0.5)
|
||||||
if transferable > 0:
|
if transferable > 0:
|
||||||
for role, deficit in deficit_roles:
|
for role, deficit in deficit_roles:
|
||||||
share = transferable * (deficit / total_deficit)
|
share = transferable * (deficit / total_deficit)
|
||||||
|
|||||||
Reference in New Issue
Block a user