Generalize retry rendering for run-002 and run-003

This commit is contained in:
2026-04-03 17:41:42 +09:00
parent 62d75f53ed
commit 6cf4b2ec33
58 changed files with 1023 additions and 1012 deletions

View File

@@ -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()