Generalize retry rendering for run-002 and run-003
This commit is contained in:
@@ -51,7 +51,7 @@ def zone_overflow_names(measurement: dict) -> list[str]:
|
||||
return names
|
||||
|
||||
|
||||
def validate_outputs(generated: dict, measurement: dict) -> tuple[str, list[str], list[str]]:
|
||||
def validate_outputs(generated: dict, measurement: dict, required_titles: list[str], run_mode: str) -> tuple[str, list[str], list[str]]:
|
||||
body_html = generated.get("body_html", "")
|
||||
sidebar_html = generated.get("sidebar_html", "")
|
||||
footer_html = generated.get("footer_html", "")
|
||||
@@ -71,28 +71,41 @@ def validate_outputs(generated: dict, measurement: dict) -> tuple[str, list[str]
|
||||
failures.append("Verify-RenderZone")
|
||||
actions.append(f"overflow가 발생한 zone({', '.join(zone_overflows)})의 content budget, block 수, typography를 재조정한다.")
|
||||
|
||||
core_message_ok = all(any(marker in visible_text for marker in variants) for variants in CORE_MESSAGE_MARKERS)
|
||||
if not core_message_ok:
|
||||
failures.append("Verify-CoreMessage")
|
||||
actions.append("원문 표현을 유지하되 `상위 개념`과 `핵심 기술/핵심 인프라 기술` 판단이 가시 텍스트에 분명히 드러나도록 정리한다.")
|
||||
if '???' in visible_text or '?? ??' in visible_text:
|
||||
failures.append("Verify-Placeholder")
|
||||
actions.append("placeholder나 깨진 라벨을 제거하고, 원문 제목/문장으로 다시 채운다.")
|
||||
|
||||
if IMAGE_REFERENCE_KEY not in visible_text:
|
||||
failures.append("Verify-ImageRef")
|
||||
actions.append("이미지/도해 참조 문구 `DX와 핵심기술간 상호관계`를 숨김 영역이 아닌 가시 블록으로 유지한다.")
|
||||
matched_titles = sum(1 for title in required_titles if title and title in visible_text)
|
||||
if matched_titles < max(2, min(len(required_titles), 3)):
|
||||
failures.append("Verify-SectionTitles")
|
||||
actions.append("원문 섹션 제목을 가시 텍스트에 더 직접적으로 유지한다.")
|
||||
|
||||
comparison_visible = (COMPARISON_MARKER in body_html) and all(key in visible_text for key in COMPARE_KEYS)
|
||||
if not comparison_visible:
|
||||
failures.append("Verify-ComparisonVisible")
|
||||
actions.append("비교 핵심 4축(범위, 프로세스, 성과품, 확장성)을 화면에 바로 보이는 요약 블록으로 강제한다.")
|
||||
if run_mode == 'run001':
|
||||
core_message_ok = all(any(marker in visible_text for marker in variants) for variants in CORE_MESSAGE_MARKERS)
|
||||
if not core_message_ok:
|
||||
failures.append("Verify-CoreMessage")
|
||||
actions.append("원문 표현을 유지하되 `상위 개념`과 `핵심 기술/핵심 인프라 기술` 판단이 가시 텍스트에 분명히 드러나도록 정리한다.")
|
||||
|
||||
if RELATION_MARKER not in body_html:
|
||||
failures.append("Verify-DesignStructure")
|
||||
actions.append("핵심 관계를 설명하는 시각적 관계도 블록을 본문 중심 구조로 유지한다.")
|
||||
if IMAGE_REFERENCE_KEY not in visible_text:
|
||||
failures.append("Verify-ImageRef")
|
||||
actions.append("이미지/도해 참조 문구 `DX와 핵심기술간 상호관계`를 숨김 영역이 아닌 가시 블록으로 유지한다.")
|
||||
|
||||
comparison_visible = (COMPARISON_MARKER in body_html) and all(key in visible_text for key in COMPARE_KEYS)
|
||||
if not comparison_visible:
|
||||
failures.append("Verify-ComparisonVisible")
|
||||
actions.append("비교 핵심 4축(범위, 프로세스, 성과품, 확장성)을 화면에 바로 보이는 요약 블록으로 강제한다.")
|
||||
|
||||
if RELATION_MARKER not in body_html:
|
||||
failures.append("Verify-DesignStructure")
|
||||
actions.append("핵심 관계를 설명하는 시각적 관계도 블록을 본문 중심 구조로 유지한다.")
|
||||
else:
|
||||
if len(re.sub(r'\s+', ' ', visible_text).strip()) < 260:
|
||||
failures.append("Verify-ContentDensity")
|
||||
actions.append("본문과 보조 영역의 원문 문장 보존량을 높여 내용 밀도를 보강한다.")
|
||||
if not body_html or not sidebar_html:
|
||||
failures.append("Verify-DesignStructure")
|
||||
actions.append("body와 sidebar의 역할을 분리하여 섹션별 배치를 다시 잡는다.")
|
||||
|
||||
narrative_markers = ["\uc6a9\uc5b4\uc758 \ud63c\uc6a9", "\ud63c\uc6a9 \ub300\ud45c \uc0ac\ub840", "\uc6a9\uc5b4 \uc815\uc758", "\uc6a9\uc5b4\uac04 \uc0c1\ud638\uad00\uacc4", "DX\uc640 BIM\uc758 \uad6c\ubd84", "\ud575\uc2ec \uc694\uc57d"]
|
||||
if sum(1 for marker in narrative_markers if marker in visible_text) < 4:
|
||||
failures.append("Verify-DesignNarrative")
|
||||
actions.append("\uc6d0\ubb38 \uc8fc\uc694 \uc18c\uc81c\ubaa9(\uc6a9\uc5b4\uc758 \ud63c\uc6a9, \uc0ac\ub840, \uc815\uc758, \uc0c1\ud638\uad00\uacc4, \ube44\uad50, \uc694\uc57d)\uacfc \uc77d\uae30 \uc21c\uc11c\uac00 \uac00\uc2dc \ud14d\uc2a4\ud2b8\uc5d0 \uc720\uc9c0\ub418\ub3c4\ub85d \uc7ac\uad6c\uc131\ud55c\ub2e4.")
|
||||
if failures:
|
||||
return "revise", sorted(set(failures)), list(dict.fromkeys(actions))
|
||||
return "pass", [], []
|
||||
@@ -238,23 +251,38 @@ def load_stage_artifacts(output_dir: Path) -> dict[str, Any]:
|
||||
return artifacts
|
||||
|
||||
|
||||
def derive_retry_plan(failures: list[str], artifacts: dict[str, Any]) -> dict[str, Any]:
|
||||
def derive_retry_plan(failures: list[str], artifacts: dict[str, Any], stage1b_data: dict[str, Any], run_mode: str) -> dict[str, Any]:
|
||||
stage_1_5b = artifacts.get("stage_1_5b_context.json", {})
|
||||
stage_2v = artifacts.get("stage_2_verification.json", {})
|
||||
|
||||
rollback_stage = "stage_2"
|
||||
reasons: list[str] = []
|
||||
mutations: list[dict[str, Any]] = []
|
||||
concepts = stage1b_data.get("concepts", [])
|
||||
topic_ids = [c.get("topic_id") for c in concepts if c.get("topic_id")]
|
||||
|
||||
if any(f in failures for f in ["Verify-CoreMessage", "Verify-ImageRef", "Verify-ComparisonVisible", "Verify-DesignStructure"]):
|
||||
rollback_stage = "stage_1b"
|
||||
reasons.append("가시 메시지/관계도/비교 요약이 부족하여 topic 표현 지시를 다시 강화해야 함")
|
||||
mutations.extend([
|
||||
{"topic_id": 2, "change": "summary", "strategy": "core_message_strengthen"},
|
||||
{"topic_id": 3, "change": "expression_hint", "strategy": "force_relation_diagram_visible"},
|
||||
{"topic_id": 5, "change": "expression_hint", "strategy": "force_visible_comparison_summary"},
|
||||
{"topic_id": 6, "change": "summary", "strategy": "strong_footer_conclusion"},
|
||||
])
|
||||
if run_mode == 'run001':
|
||||
if any(f in failures for f in ["Verify-CoreMessage", "Verify-ImageRef", "Verify-ComparisonVisible", "Verify-DesignStructure", "Verify-SectionTitles", "Verify-Placeholder"]):
|
||||
rollback_stage = "stage_1b"
|
||||
reasons.append("가시 메시지/관계도/비교 요약이 부족하여 topic 표현 지시를 다시 강화해야 함")
|
||||
mutations.extend([
|
||||
{"topic_id": 2, "change": "summary", "strategy": "core_message_strengthen"},
|
||||
{"topic_id": 3, "change": "expression_hint", "strategy": "force_relation_diagram_visible"},
|
||||
{"topic_id": 5, "change": "expression_hint", "strategy": "force_visible_comparison_summary"},
|
||||
{"topic_id": 6, "change": "summary", "strategy": "strong_footer_conclusion"},
|
||||
])
|
||||
else:
|
||||
if any(f in failures for f in ["Verify-Placeholder", "Verify-SectionTitles", "Verify-ContentDensity", "Verify-DesignStructure"]):
|
||||
rollback_stage = "stage_1b"
|
||||
reasons.append("원문 섹션 제목과 내용 밀도를 더 직접적으로 살리도록 generic topic 표현을 강화해야 함")
|
||||
if len(topic_ids) >= 1:
|
||||
mutations.append({"topic_id": topic_ids[0], "change": "summary", "strategy": "strengthen_intro_from_source"})
|
||||
if len(topic_ids) >= 2:
|
||||
mutations.append({"topic_id": topic_ids[1], "change": "summary", "strategy": "strengthen_main_from_source"})
|
||||
if len(topic_ids) >= 3:
|
||||
mutations.append({"topic_id": topic_ids[2], "change": "summary", "strategy": "strengthen_support_from_source"})
|
||||
if len(topic_ids) >= 4:
|
||||
mutations.append({"topic_id": topic_ids[-1], "change": "summary", "strategy": "strong_footer_conclusion_generic"})
|
||||
|
||||
if any(f in failures for f in ["Verify-RenderZone", "Verify-RenderSlide"]):
|
||||
if rollback_stage != "stage_1b":
|
||||
@@ -318,6 +346,14 @@ def apply_retry_plan_to_stage1b(stage1b_path: Path, retry_plan: dict[str, Any],
|
||||
elif strategy == "reduce_density_and_split_visibility":
|
||||
concept["summary"] = compact_text(summary, preserve_80_percent(summary, floor=90, ceiling=200))
|
||||
concept["expression_hint"] = ensure_phrase(hint, "표현 밀도를 낮추고, 장문 설명 대신 짧은 bullet/card 구조로 나눈다.")
|
||||
elif strategy == "strengthen_intro_from_source":
|
||||
concept["expression_hint"] = ensure_phrase(hint, "첫 섹션 제목과 핵심 bullet을 그대로 가시 블록으로 유지한다.")
|
||||
elif strategy == "strengthen_main_from_source":
|
||||
concept["expression_hint"] = ensure_phrase(hint, "둘째 섹션의 원문 bullet과 소제목을 직접적으로 유지한다.")
|
||||
elif strategy == "strengthen_support_from_source":
|
||||
concept["expression_hint"] = ensure_phrase(hint, "보조 섹션도 placeholder 없이 원문 bullet 중심으로 노출한다.")
|
||||
elif strategy == "strong_footer_conclusion_generic":
|
||||
concept["expression_hint"] = ensure_phrase(hint, "핵심 요약 문장을 footer에서 축약하지 말고 직접 노출한다.")
|
||||
|
||||
write_json(stage1b_path, data)
|
||||
retry_plan_path = stage1b_path.parent / "retry-plan.json"
|
||||
@@ -452,7 +488,11 @@ KPI / 판정 결과
|
||||
|
||||
generated = read_json(generated_path)
|
||||
measurement = read_json(measurement_path)
|
||||
status, failures, actions = validate_outputs(generated, measurement)
|
||||
stage1a_data = read_json(stage1a)
|
||||
required_titles = [item.get("title", "") for item in stage1a_data.get("topics", [])]
|
||||
topic_count = len(required_titles)
|
||||
run_mode = "run001" if topic_count >= 5 else "generic"
|
||||
status, failures, actions = validate_outputs(generated, measurement, required_titles, run_mode)
|
||||
final_html_text = final_html_path.read_text(encoding="utf-8")
|
||||
if 'width:100%; height:28px' in final_html_text:
|
||||
status = "revise"
|
||||
@@ -462,7 +502,7 @@ KPI / 판정 결과
|
||||
|
||||
if status != "pass" and iteration < args.max_iterations:
|
||||
artifacts = load_stage_artifacts(output_dir)
|
||||
retry_plan = derive_retry_plan(failures, artifacts)
|
||||
retry_plan = derive_retry_plan(failures, artifacts, read_json(stage1b), run_mode)
|
||||
apply_retry_plan_to_stage1b(stage1b, retry_plan, iteration)
|
||||
|
||||
validation_path.write_text(build_validation_markdown(args.run_id, status, failures, actions, measurement, retry_plan), encoding="utf-8")
|
||||
@@ -543,3 +583,4 @@ KPI / 판정 결과
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -335,6 +335,25 @@ def _plain_text(value: str) -> str:
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
return text
|
||||
|
||||
|
||||
def _bulletish_lines(text: str, limit: int = 6) -> list[str]:
|
||||
normalized = re.sub(r"\s+", " ", text or "").strip()
|
||||
if not normalized:
|
||||
return []
|
||||
parts = re.split(r"(?:•|\*\*[^*]+\*\*:?|\s+-\s+|\.\s+)", normalized)
|
||||
cleaned = []
|
||||
for part in parts:
|
||||
item = re.sub(r"\s+", " ", part).strip(" -•")
|
||||
if not item:
|
||||
continue
|
||||
if len(item) < 6:
|
||||
continue
|
||||
cleaned.append(item)
|
||||
if cleaned:
|
||||
return cleaned[:limit]
|
||||
sentences = [s.strip() for s in re.split(r"(?<=[.!?])\s+", normalized) if s.strip()]
|
||||
return sentences[:limit]
|
||||
|
||||
def _markdown_section(text: str, start_marker: str, end_marker: str | None = None) -> str:
|
||||
start = text.find(start_marker)
|
||||
if start == -1:
|
||||
@@ -546,40 +565,183 @@ def _relation_visual(image_src: str, caption: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _is_run001_style_document(ctx: PipelineContext, raw: str) -> bool:
|
||||
relation_types = {getattr(t, 'relation_type', '') for t in ctx.topics}
|
||||
if {'hierarchy', 'comparison', 'definition', 'problem'} & relation_types:
|
||||
return True
|
||||
return all(keyword in raw for keyword in ['건설산업', 'BIM', 'DX']) and bool(_parse_comparison_rows_from_raw(raw))
|
||||
|
||||
|
||||
def _section_card(title: str, lines: list[str], tone: str = 'blue') -> str:
|
||||
palette = {
|
||||
'orange': ('#fff7ed', '#fdba74', '#9a3412'),
|
||||
'blue': ('#eff6ff', '#93c5fd', '#1e3a8a'),
|
||||
'slate': ('#f8fafc', '#cbd5e1', '#334155'),
|
||||
'green': ('#ecfdf5', '#86efac', '#166534'),
|
||||
}
|
||||
bg, border, text = palette.get(tone, palette['blue'])
|
||||
items_html = ''.join(
|
||||
f'<li style="margin-left:16px; margin-bottom:6px;">{_trim_visible_copy(item, floor=160, ceiling=460)}</li>'
|
||||
for item in lines if item
|
||||
)
|
||||
return (
|
||||
f'<div style="background:{bg}; border:1px solid {border}; border-radius:14px; padding:12px 14px;">'
|
||||
f'<div style="font-size:13px; font-weight:900; color:{text}; margin-bottom:8px;">{title}</div>'
|
||||
f'<ul style="font-size:10.4px; line-height:1.6; color:#334155; padding-left:0; margin:0; list-style:disc;">{items_html}</ul>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
def _component_placeholder(title: str, summary: str) -> str:
|
||||
return (
|
||||
'<div style="background:#ffffff; border:1px dashed #94a3b8; border-radius:14px; padding:14px;">'
|
||||
f'<div style="font-size:13px; font-weight:900; color:#334155; margin-bottom:8px;">{title}</div>'
|
||||
f'<div style="font-size:10.4px; line-height:1.62; color:#475569;">{_trim_visible_copy(summary, floor=240, ceiling=560)}</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
|
||||
raw = ctx.raw_content or ''
|
||||
is_run001_style = _is_run001_style_document(ctx, raw)
|
||||
|
||||
problem_topic = _topic(ctx, 1)
|
||||
definitions_topic = _topic(ctx, 2)
|
||||
relation_topic = _topic(ctx, 3)
|
||||
evidence_topic = _topic(ctx, 4)
|
||||
comparison_topic = _topic(ctx, 5)
|
||||
if is_run001_style:
|
||||
problem_topic = _topic(ctx, 1)
|
||||
definitions_topic = _topic(ctx, 2)
|
||||
relation_topic = _topic(ctx, 3)
|
||||
evidence_topic = _topic(ctx, 4)
|
||||
comparison_topic = _topic(ctx, 5)
|
||||
|
||||
problem_title = problem_topic.title if problem_topic and problem_topic.title else '??? ??'
|
||||
definitions_title = definitions_topic.title if definitions_topic and definitions_topic.title else '?? ??'
|
||||
relation_title = relation_topic.title if relation_topic and relation_topic.title else '??? ????'
|
||||
evidence_title = evidence_topic.title if evidence_topic and evidence_topic.title else '?? ?? ??'
|
||||
comparison_title = comparison_topic.title if comparison_topic and comparison_topic.title else 'DX? BIM? ??'
|
||||
problem_title = problem_topic.title if problem_topic and problem_topic.title else '용어의 혼용'
|
||||
definitions_title = definitions_topic.title if definitions_topic and definitions_topic.title else '용어 정의'
|
||||
relation_title = relation_topic.title if relation_topic and relation_topic.title else '용어간 상호관계'
|
||||
evidence_title = evidence_topic.title if evidence_topic and evidence_topic.title else '혼용 대표 사례'
|
||||
comparison_title = comparison_topic.title if comparison_topic and comparison_topic.title else 'DX와 BIM의 구분'
|
||||
|
||||
problem_bullets = _problem_bullets_from_raw(raw)[:2]
|
||||
all_evidence_bullets = _evidence_bullets_from_raw(raw)
|
||||
evidence_bullets = all_evidence_bullets[:2]
|
||||
definition_sections = _definition_sections_from_raw(raw)[:3]
|
||||
relation_bullets = _relation_bullets_from_raw(raw)[:5]
|
||||
comparison_rows = _parse_comparison_rows_from_raw(raw)
|
||||
problem_bullets = _problem_bullets_from_raw(raw)[:2]
|
||||
all_evidence_bullets = _evidence_bullets_from_raw(raw)
|
||||
evidence_bullets = all_evidence_bullets[:2]
|
||||
definition_sections = _definition_sections_from_raw(raw)[:3]
|
||||
relation_bullets = _relation_bullets_from_raw(raw)[:5]
|
||||
comparison_rows = _parse_comparison_rows_from_raw(raw)
|
||||
|
||||
preferred_axes = ['??', '????', '???', '???']
|
||||
picked_rows = [row for row in comparison_rows if row[0] in preferred_axes]
|
||||
if len(picked_rows) < 4:
|
||||
seen = {row[0] for row in picked_rows}
|
||||
for row in comparison_rows:
|
||||
if row[0] not in seen:
|
||||
picked_rows.append(row)
|
||||
seen.add(row[0])
|
||||
if len(picked_rows) >= 4:
|
||||
break
|
||||
picked_rows = picked_rows[:4]
|
||||
preferred_axes = ['범위', '프로세스', '성과품', '확장성']
|
||||
picked_rows = [row for row in comparison_rows if row[0] in preferred_axes]
|
||||
if len(picked_rows) < 4:
|
||||
seen = {row[0] for row in picked_rows}
|
||||
for row in comparison_rows:
|
||||
if row[0] not in seen:
|
||||
picked_rows.append(row)
|
||||
seen.add(row[0])
|
||||
if len(picked_rows) >= 4:
|
||||
break
|
||||
picked_rows = picked_rows[:4]
|
||||
|
||||
image_src = _extract_image_src_from_raw(raw)
|
||||
if image_src and ctx.base_path:
|
||||
candidate = Path(ctx.base_path) / image_src.lstrip('/\\').replace('/', '\\')
|
||||
if not candidate.exists():
|
||||
image_src = ''
|
||||
else:
|
||||
image_src = ''
|
||||
image_caption = _extract_caption_from_raw(raw)
|
||||
conclusion_text = _conclusion_from_raw(raw)
|
||||
|
||||
problem_items_html = ''.join(
|
||||
f'<li style="margin-left:16px; margin-bottom:5px;">{_trim_visible_copy(item, floor=130, ceiling=280)}</li>'
|
||||
for item in problem_bullets
|
||||
)
|
||||
evidence_items_html = ''.join(
|
||||
f'<li style="margin-left:16px; margin-bottom:5px;">{_trim_visible_copy(item, floor=140, ceiling=320)}</li>'
|
||||
for item in evidence_bullets
|
||||
)
|
||||
relation_items_html = ''.join(
|
||||
f'<li style="margin-left:18px; margin-bottom:6px;">{_trim_visible_copy(item, floor=120, ceiling=260)}</li>'
|
||||
for item in relation_bullets
|
||||
)
|
||||
|
||||
definition_cards_html = ''
|
||||
for idx, section in enumerate(definition_sections, start=1):
|
||||
definition_cards_html += (
|
||||
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-radius:14px; padding:12px; display:flex; gap:10px; align-items:flex-start; min-height:108px;">'
|
||||
f'<div style="width:34px; height:34px; border-radius:999px; background:#2563eb; color:#fff; font-size:15px; font-weight:800; display:flex; align-items:center; justify-content:center; flex-shrink:0;">{idx}</div>'
|
||||
'<div style="flex:1;">'
|
||||
f'<div style="font-size:13px; font-weight:800; color:#0f172a; margin-bottom:6px; line-height:1.35;">{section["title"]}</div>'
|
||||
f'<div style="font-size:10px; line-height:1.58; color:#334155; word-break:keep-all;">{_trim_visible_copy(section["body"], floor=220, ceiling=520)}</div>'
|
||||
'</div></div>'
|
||||
)
|
||||
|
||||
comparison_rows_html = ''
|
||||
for axis, dx, bim in picked_rows:
|
||||
comparison_rows_html += (
|
||||
'<div style="display:grid; grid-template-columns:1fr 86px 1fr; border-top:1px solid #dbe5f2; align-items:stretch;">'
|
||||
f'<div style="padding:7px 10px; font-size:9.8px; line-height:1.42; color:#1e3a8a; font-weight:600; background:#ffffff;">{_trim_visible_copy(dx, floor=110, ceiling=220)}</div>'
|
||||
f'<div style="padding:7px 6px; font-size:9.6px; line-height:1.25; color:#1d4ed8; font-weight:800; text-align:center; background:#eff6ff; border-left:1px solid #dbe5f2; border-right:1px solid #dbe5f2; display:flex; align-items:center; justify-content:center;">{axis}</div>'
|
||||
f'<div style="padding:7px 10px; font-size:9.8px; line-height:1.42; color:#475569; text-align:right; background:#ffffff;">{_trim_visible_copy(bim, floor=110, ceiling=220)}</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
evidence_popup_html = _popup_overlay('popup-evidence', evidence_title, _popup_list_html(all_evidence_bullets, floor=220, ceiling=520))
|
||||
comparison_popup_html = _popup_overlay('popup-comparison', comparison_title, _popup_comparison_table(comparison_rows))
|
||||
|
||||
intro_html = (
|
||||
'<div style="background:linear-gradient(135deg,#fff5f5 0%,#ffe8e8 100%); border:2px solid #f8a4a4; border-radius:12px; padding:12px 16px;">'
|
||||
'<div style="display:flex; gap:12px; align-items:flex-start;">'
|
||||
'<div style="font-size:24px; line-height:1; color:#f59e0b; margin-top:2px;">⚠</div>'
|
||||
'<div style="flex:1;">'
|
||||
f'<div style="font-size:12.5px; font-weight:900; color:#b42318; margin-bottom:6px;">{problem_title}</div>'
|
||||
f'<ul style="font-size:9.4px; line-height:1.5; color:#7a271a; padding-left:0; margin:0 0 6px 0; list-style:disc;">{problem_items_html}</ul>'
|
||||
f'<div style="font-size:9px; line-height:1.42; color:#9a3412; margin-top:4px;"><span style="font-weight:800;">{evidence_title}</span></div>'
|
||||
f'<ul style="font-size:8.9px; line-height:1.42; color:#9a3412; padding-left:0; margin:2px 0 0 0; list-style:disc;">{evidence_items_html}</ul>'
|
||||
f'{_popup_button("popup-evidence", "상세 사례")}'
|
||||
'<div style="margin-top:8px; background:#991b1b; color:#ffffff; border-radius:4px; padding:5px 10px; font-size:10px; font-weight:800; word-break:keep-all;">→ 각 용어의 정의, 역할, 상호관계에 대한 체계적 정리 필요</div>'
|
||||
'</div></div></div>'
|
||||
)
|
||||
|
||||
relation_html = (
|
||||
'<div style="background:#ffffff; border:1px solid #d6e2ef; border-radius:14px; padding:10px 12px;">'
|
||||
f'<div style="font-size:14px; font-weight:900; color:#1f3b63; margin-bottom:6px;">{relation_title}</div>'
|
||||
'<div style="display:grid; grid-template-columns:250px 1fr; gap:14px; align-items:start;">'
|
||||
'<div>'
|
||||
f'{_relation_visual(image_src, image_caption).replace("height:220px", "height:210px").replace("padding:10px", "padding:12px")}'
|
||||
f'<div style="margin-top:8px; background:#dcfce7; border:1px solid #86efac; color:#166534; font-size:9px; line-height:1.3; border-radius:999px; padding:4px 10px; text-align:center;">{image_caption}</div>'
|
||||
'</div>'
|
||||
'<div style="display:flex; flex-direction:column; gap:8px;">'
|
||||
f'<ul style="font-size:9px; line-height:1.46; color:#334155; padding-left:0; margin:0; list-style:disc;">{relation_items_html}</ul>'
|
||||
'<div style="margin-top:4px; border:1px solid #b9d3ff; border-radius:10px; overflow:hidden;">'
|
||||
'<div style="display:grid; grid-template-columns:1fr 86px 1fr; background:linear-gradient(135deg,#0d47a1,#1565c0); color:#fff; font-weight:800; font-size:12px; text-align:center;">'
|
||||
'<div style="padding:7px 10px;">DX</div>'
|
||||
'<div style="padding:7px 6px; background:rgba(0,0,0,0.14); font-size:11px;">구분</div>'
|
||||
'<div style="padding:7px 10px;">BIM</div>'
|
||||
'</div>'
|
||||
f'{comparison_rows_html}'
|
||||
'</div>'
|
||||
f'{_popup_button("popup-comparison", "상세 비교 보기")}'
|
||||
'</div></div></div>'
|
||||
)
|
||||
|
||||
body_html = '<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; color:#0f172a; display:flex; flex-direction:column; gap:8px;">' + intro_html + relation_html + evidence_popup_html + comparison_popup_html + '</div>'
|
||||
sidebar_html = '<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; display:flex; flex-direction:column; gap:10px;">' + f'<div style="font-size:12px; font-weight:800; color:#475569; padding:2px 6px;">{definitions_title}</div>' + definition_cards_html + '</div>'
|
||||
footer_html = '<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:58px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">' + f'<div style="font-size:13px; font-weight:900; line-height:1.35;">{conclusion_text}</div>' + '</div>'
|
||||
return {'body_html': body_html, 'sidebar_html': sidebar_html, 'footer_html': footer_html, 'reasoning': 'retry regrouping by content importance: grouped problem+evidence with popup details, relation block, visible comparison summary with full popup, numbered definition cards'}
|
||||
|
||||
main_topics = [t for t in ctx.topics if getattr(t, 'layer', '') != 'conclusion']
|
||||
intro_topic = main_topics[0] if len(main_topics) > 0 else None
|
||||
body_topic = main_topics[1] if len(main_topics) > 1 else None
|
||||
support_topic = main_topics[2] if len(main_topics) > 2 else None
|
||||
conclusion_topic = next((t for t in ctx.topics if getattr(t, 'layer', '') == 'conclusion'), ctx.topics[-1] if ctx.topics else None)
|
||||
|
||||
intro_title = intro_topic.title if intro_topic and intro_topic.title else ctx.analysis.title
|
||||
body_title = body_topic.title if body_topic and body_topic.title else '본문'
|
||||
support_title = support_topic.title if support_topic and support_topic.title else '보조 정보'
|
||||
conclusion_text = _prefer_source_text(conclusion_topic, ctx.analysis.core_message if ctx.analysis else '')
|
||||
|
||||
intro_lines = _bulletish_lines(_prefer_source_text(intro_topic, ''), 6)
|
||||
body_lines = _bulletish_lines(_prefer_source_text(body_topic, ''), 8)
|
||||
support_lines = _bulletish_lines(_prefer_source_text(support_topic, ''), 8)
|
||||
details = _details_blocks(raw)
|
||||
detail_source = details[0] if details else _prefer_source_text(support_topic, '')
|
||||
detail_popup = _popup_overlay('popup-detail', support_title, _popup_list_html(_bulletish_lines(detail_source, 14), floor=200, ceiling=560))
|
||||
|
||||
image_src = _extract_image_src_from_raw(raw)
|
||||
if image_src and ctx.base_path:
|
||||
@@ -588,124 +750,42 @@ def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
|
||||
image_src = ''
|
||||
else:
|
||||
image_src = ''
|
||||
image_caption = _extract_caption_from_raw(raw)
|
||||
conclusion_text = _conclusion_from_raw(raw)
|
||||
image_caption = _extract_caption_from_raw(raw) or body_title
|
||||
|
||||
problem_items_html = ''.join(
|
||||
f'<li style="margin-left:16px; margin-bottom:5px;">{_trim_visible_copy(item, floor=130, ceiling=280)}</li>'
|
||||
for item in problem_bullets
|
||||
)
|
||||
evidence_items_html = ''.join(
|
||||
f'<li style="margin-left:16px; margin-bottom:5px;">{_trim_visible_copy(item, floor=140, ceiling=320)}</li>'
|
||||
for item in evidence_bullets
|
||||
)
|
||||
relation_items_html = ''.join(
|
||||
f'<li style="margin-left:18px; margin-bottom:6px;">{_trim_visible_copy(item, floor=120, ceiling=260)}</li>'
|
||||
for item in relation_bullets
|
||||
)
|
||||
intro_card = _section_card(intro_title, intro_lines[:5], tone='orange')
|
||||
body_card = _section_card(body_title, body_lines[:6], tone='blue')
|
||||
|
||||
definition_cards_html = ''
|
||||
for idx, section in enumerate(definition_sections, start=1):
|
||||
definition_cards_html += (
|
||||
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-radius:14px; padding:12px; display:flex; gap:10px; align-items:flex-start; min-height:108px;">'
|
||||
f'<div style="width:34px; height:34px; border-radius:999px; background:#2563eb; color:#fff; font-size:15px; font-weight:800; display:flex; align-items:center; justify-content:center; flex-shrink:0;">{idx}</div>'
|
||||
'<div style="flex:1;">'
|
||||
f'<div style="font-size:13px; font-weight:800; color:#0f172a; margin-bottom:6px; line-height:1.35;">{section["title"]}</div>'
|
||||
f'<div style="font-size:10px; line-height:1.58; color:#334155; word-break:keep-all;">{_trim_visible_copy(section["body"], floor=220, ceiling=520)}</div>'
|
||||
'</div>'
|
||||
if image_src:
|
||||
visual_block = (
|
||||
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-radius:14px; padding:12px;">'
|
||||
f'{_relation_visual(image_src, image_caption).replace("height:220px", "height:215px")}'
|
||||
f'<div style="margin-top:8px; font-size:9px; color:#166534; text-align:center;">{image_caption}</div>'
|
||||
'</div>'
|
||||
)
|
||||
elif support_topic and '<DxEffect' in (support_topic.source_data or support_topic.summary or ''):
|
||||
visual_block = _component_placeholder(support_title, _prefer_source_text(support_topic, '주체별 기대효과를 보조 영역에서 요약함.'))
|
||||
else:
|
||||
visual_block = _section_card(support_title, support_lines[:5], tone='slate')
|
||||
|
||||
comparison_rows_html = ''
|
||||
for axis, dx, bim in picked_rows:
|
||||
comparison_rows_html += (
|
||||
'<div style="display:grid; grid-template-columns:1fr 86px 1fr; border-top:1px solid #dbe5f2; align-items:stretch;">'
|
||||
f'<div style="padding:7px 10px; font-size:9.8px; line-height:1.42; color:#1e3a8a; font-weight:600; background:#ffffff;">{_trim_visible_copy(dx, floor=110, ceiling=220)}</div>'
|
||||
f'<div style="padding:7px 6px; font-size:9.6px; line-height:1.25; color:#1d4ed8; font-weight:800; text-align:center; background:#eff6ff; border-left:1px solid #dbe5f2; border-right:1px solid #dbe5f2; display:flex; align-items:center; justify-content:center;">{axis}</div>'
|
||||
f'<div style="padding:7px 10px; font-size:9.8px; line-height:1.42; color:#475569; text-align:right; background:#ffffff;">{_trim_visible_copy(bim, floor=110, ceiling=220)}</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
evidence_popup_html = _popup_overlay(
|
||||
'popup-evidence',
|
||||
evidence_title,
|
||||
_popup_list_html(all_evidence_bullets, floor=220, ceiling=520),
|
||||
)
|
||||
comparison_popup_html = _popup_overlay(
|
||||
'popup-comparison',
|
||||
comparison_title,
|
||||
_popup_comparison_table(comparison_rows),
|
||||
)
|
||||
|
||||
intro_html = (
|
||||
'<div style="background:linear-gradient(135deg,#fff5f5 0%,#ffe8e8 100%); border:2px solid #f8a4a4; border-radius:12px; padding:12px 16px;">'
|
||||
'<div style="display:flex; gap:12px; align-items:flex-start;">'
|
||||
'<div style="font-size:24px; line-height:1; color:#f59e0b; margin-top:2px;">⚠</div>'
|
||||
'<div style="flex:1;">'
|
||||
f'<div style="font-size:12.5px; font-weight:900; color:#b42318; margin-bottom:6px;">{problem_title}</div>'
|
||||
f'<ul style="font-size:9.4px; line-height:1.5; color:#7a271a; padding-left:0; margin:0 0 6px 0; list-style:disc;">{problem_items_html}</ul>'
|
||||
f'<div style="font-size:9px; line-height:1.42; color:#9a3412; margin-top:4px;"><span style="font-weight:800;">{evidence_title}</span></div>'
|
||||
f'<ul style="font-size:8.9px; line-height:1.42; color:#9a3412; padding-left:0; margin:2px 0 0 0; list-style:disc;">{evidence_items_html}</ul>'
|
||||
f'{_popup_button("popup-evidence", "?? ???")}'
|
||||
'<div style="margin-top:8px; background:#991b1b; color:#ffffff; border-radius:4px; padding:5px 10px; font-size:10px; font-weight:800; word-break:keep-all;">→ ? ??? ??, ??, ????? ?? ??? ?? ??</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
relation_html = (
|
||||
'<div style="background:#ffffff; border:1px solid #d6e2ef; border-radius:14px; padding:10px 12px;">'
|
||||
f'<div style="font-size:14px; font-weight:900; color:#1f3b63; margin-bottom:6px;">{relation_title}</div>'
|
||||
'<div style="display:grid; grid-template-columns:250px 1fr; gap:14px; align-items:start;">'
|
||||
'<div>'
|
||||
f'{_relation_visual(image_src, image_caption).replace("height:220px", "height:210px").replace("padding:10px", "padding:12px")}'
|
||||
f'<div style="margin-top:8px; background:#dcfce7; border:1px solid #86efac; color:#166534; font-size:9px; line-height:1.3; border-radius:999px; padding:4px 10px; text-align:center;">{image_caption}</div>'
|
||||
'</div>'
|
||||
'<div style="display:flex; flex-direction:column; gap:8px;">'
|
||||
f'<ul style="font-size:9px; line-height:1.46; color:#334155; padding-left:0; margin:0; list-style:disc;">{relation_items_html}</ul>'
|
||||
'<div style="margin-top:4px; border:1px solid #b9d3ff; border-radius:10px; overflow:hidden;">'
|
||||
'<div style="display:grid; grid-template-columns:1fr 86px 1fr; background:linear-gradient(135deg,#0d47a1,#1565c0); color:#fff; font-weight:800; font-size:12px; text-align:center;">'
|
||||
'<div style="padding:7px 10px;">DX</div>'
|
||||
'<div style="padding:7px 6px; background:rgba(0,0,0,0.14); font-size:11px;">??</div>'
|
||||
'<div style="padding:7px 10px;">BIM</div>'
|
||||
'</div>'
|
||||
f'{comparison_rows_html}'
|
||||
'</div>'
|
||||
f'{_popup_button("popup-comparison", "??? ?? ??")}'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
)
|
||||
sidebar_inner = _section_card(support_title, support_lines[:5], tone='slate') if support_lines else _component_placeholder(support_title, _prefer_source_text(support_topic, '보조 정보가 없음.'))
|
||||
if support_lines:
|
||||
sidebar_inner += _popup_button('popup-detail', '상세 내용 보기')
|
||||
|
||||
body_html = (
|
||||
'<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; color:#0f172a; display:flex; flex-direction:column; gap:8px;">'
|
||||
f'{intro_html}'
|
||||
f'{relation_html}'
|
||||
f'{evidence_popup_html}'
|
||||
f'{comparison_popup_html}'
|
||||
'<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; color:#0f172a; display:flex; flex-direction:column; gap:10px;">'
|
||||
f'{intro_card}'
|
||||
'<div style="display:grid; grid-template-columns:1.05fr 0.95fr; gap:12px; align-items:start;">'
|
||||
f'{body_card}'
|
||||
f'{visual_block}'
|
||||
'</div>'
|
||||
f'{detail_popup}'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
sidebar_html = (
|
||||
'<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; display:flex; flex-direction:column; gap:10px;">'
|
||||
f'<div style="font-size:12px; font-weight:800; color:#475569; padding:2px 6px;">{definitions_title}</div>'
|
||||
f'{definition_cards_html}'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
footer_html = (
|
||||
'<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:58px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">'
|
||||
f'<div style="font-size:13px; font-weight:900; line-height:1.35;">{conclusion_text}</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
return {
|
||||
'body_html': body_html,
|
||||
'sidebar_html': sidebar_html,
|
||||
'footer_html': footer_html,
|
||||
'reasoning': 'retry regrouping by content importance: grouped problem+evidence with popup details, relation block, visible comparison summary with full popup, numbered definition cards',
|
||||
}
|
||||
sidebar_html = '<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; display:flex; flex-direction:column; gap:10px;">' + sidebar_inner + '</div>'
|
||||
|
||||
footer_html = '<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:58px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">' + f'<div style="font-size:13px; font-weight:900; line-height:1.35;">{_trim_visible_copy(conclusion_text, floor=90, ceiling=240)}</div>' + '</div>'
|
||||
return {'body_html': body_html, 'sidebar_html': sidebar_html, 'footer_html': footer_html, 'reasoning': 'generic retry layout for non-run001 documents: preserve original section titles, visible intro/body/support blocks, and popup detail support'}
|
||||
|
||||
async def _stage_2(ctx: PipelineContext, retry_plan: dict | None = None) -> PipelineContext:
|
||||
analysis_dict = {
|
||||
@@ -842,3 +922,5 @@ async def main() -> None:
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user