B' overflow 루프 동작: Kei popup 결정 → 상단 소제목만 유지 + 하위불릿 팝업

- kei_client: 에스컬레이션 prompt 개선 — 소제목 유지 필수, overflow 영역만 대상
- block_assembler B': 상단 popup_roles 체크 추가 — 소제목만 남기고 하위불릿 제거
- block_assembler: \x01 바이트 수정 (r-string 역참조)
- 결과: 03번 top overflow 358px → 143px (루프 2회차에서 popup 반영)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 09:20:23 +09:00
parent 571b057f19
commit bc7c08e575
3 changed files with 1313 additions and 12 deletions

View File

@@ -1055,10 +1055,16 @@ 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 대상이면 소제목만 유지, 하위 불릿 제거
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>'
@@ -1140,7 +1146,7 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
if sub_sections_from_norm and bottom_left_role: if sub_sections_from_norm and bottom_left_role:
rn = bottom_left_role[0] rn = bottom_left_role[0]
sub_title, sub_content = sub_sections_from_norm[0] sub_title, sub_content = sub_sections_from_norm[0]
sub_content = re.sub(r'\*\*(.+?)\*\*', r'<strong></strong>', sub_content) sub_content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', sub_content)
# 표 렌더링 (normalized.tables에서) # 표 렌더링 (normalized.tables에서)
table_html_bl = "" table_html_bl = ""
@@ -1159,7 +1165,7 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
bg = "#f8fafc" if ri % 2 == 0 else "#fff" bg = "#f8fafc" if ri % 2 == 0 else "#fff"
cells = "" cells = ""
for ci_idx, cell in enumerate(row): for ci_idx, cell in enumerate(row):
cell_clean = re.sub(r'\*\*(.+?)\*\*', r'<strong></strong>', str(cell)) cell_clean = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', str(cell))
c_color = "#1e40af" if ci_idx == 0 else "#475569" c_color = "#1e40af" if ci_idx == 0 else "#475569"
c_weight = "600" if ci_idx == 0 else "400" c_weight = "600" if ci_idx == 0 else "400"
cells += f'<div style="padding:{int(font_size*0.2)}px {int(font_size*0.3)}px;font-size:{font_size-2}px;color:{c_color};font-weight:{c_weight};">{cell_clean}</div>' cells += f'<div style="padding:{int(font_size*0.2)}px {int(font_size*0.3)}px;font-size:{font_size-2}px;color:{c_color};font-weight:{c_weight};">{cell_clean}</div>'
@@ -1213,7 +1219,7 @@ def _assemble_slide_html_type_b_prime(ctx: "PipelineContext", title_text: str =
if bottom_right_role and len(sub_sections_from_norm) > 1: if bottom_right_role and len(sub_sections_from_norm) > 1:
rn = bottom_right_role[0] rn = bottom_right_role[0]
sub_title_br, sub_content_br = sub_sections_from_norm[1] sub_title_br, sub_content_br = sub_sections_from_norm[1]
sub_content_br = re.sub(r'\*\*(.+?)\*\*', r'<strong></strong>', sub_content_br) sub_content_br = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', sub_content_br)
bul = "" bul = ""
for line in sub_content_br.split("\n"): for line in sub_content_br.split("\n"):

1291
src/block_assembler_fixed.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1358,17 +1358,21 @@ KEI_FIT_ESCALATION_PROMPT = """당신은 슬라이드 설계 전문가이다.
## 핵심 원칙 ## 핵심 원칙
- **텍스트 원문은 절대 수정/삭제/요약하지 않는다.** - **텍스트 원문은 절대 수정/삭제/요약하지 않는다.**
- 공간이 부족하면 **팝업으로 분리**하여 원문 전체를 팝업에 넣는다. - 공간이 부족하면 **하위 불릿(상세 설명)만 팝업으로 분리.**
- 슬라이드에는 제목 + "바로가기 →" 링크만 남긴다. - **소제목(카드 제목)은 반드시 슬라이드에 유지.** 절대 팝업으로 빼지 않는다.
- 중요도가 높은 영역의 공간을 우선 확보한다. - 슬라이드에는 소제목 + "바로가기 →" 링크. 팝업에 하위 불릿 원문 전체.
- overflow가 없는 영역은 건드리지 않는다.
## 판단 기준 ## 판단 기준
- 넘치는 영역 중 중요도가 낮은 콘텐츠를 팝업으로 분리 - overflow가 발생한 영역만 대상. 다른 영역은 결정하지 않는다.
- 표 데이터가 큰 경우 → 팝업 분리 1순위 - 해당 영역 내에서 **하위 불릿(상세 설명)만** 팝업 대상.
- 이미 팝업이 있는 콘텐츠 → 슬라이드에서 제거하고 팝업으로 통합 - 소제목/카드 제목은 슬라이드에 남겨서 구조를 유지.
- 표 데이터가 큰 경우 → 표를 팝업으로 분리하고 요약만 남김.
- 한 번에 1~2개 역할만 결정. 전부 다 팝업으로 빼지 않는다.
## 출력 (JSON만. 설명 없이.) ## 출력 (JSON만. 설명 없이.)
- role에는 반드시 아래 "역할 목록"에 있는 **정확한 역할명**을 사용하라. - role에는 반드시 아래 "역할 목록"에 있는 **정확한 역할명**을 사용하라.
- overflow가 발생한 역할만 포함. overflow 없는 역할은 포함하지 마라.
```json ```json
{ {
@@ -1376,7 +1380,7 @@ KEI_FIT_ESCALATION_PROMPT = """당신은 슬라이드 설계 전문가이다.
{ {
"role": "역할 목록에 있는 정확한 역할명", "role": "역할 목록에 있는 정확한 역할명",
"action": "popup", "action": "popup",
"detail": "팝업으로 분리할 구체적 내용 (어떤 부분을 팝업으로 빼는지)", "detail": "팝업으로 분리할 구체적 내용 (하위 불릿만. 소제목은 유지)",
"reason": "판단 근거 1문장" "reason": "판단 근거 1문장"
} }
] ]
@@ -1384,7 +1388,7 @@ KEI_FIT_ESCALATION_PROMPT = """당신은 슬라이드 설계 전문가이다.
``` ```
action 종류: action 종류:
- popup: 상세 내용을 팝업으로 분리하고 슬라이드에는 링크만 남김 - popup: 하위 불릿(상세 설명)을 팝업으로 분리. 소제목은 슬라이드에 유지.
""" """