Checkpoint Type B pipeline refinement for run-002 and run-003

This commit is contained in:
2026-04-07 12:16:58 +09:00
parent f48dbe5227
commit 11e9165a8f
71 changed files with 1318 additions and 1051 deletions

View File

@@ -21,7 +21,10 @@ if str(DESIGN_AGENT_ROOT) not in sys.path:
sys.path.insert(0, str(DESIGN_AGENT_ROOT))
from src.renderer import render_slide_from_html # type: ignore
from src.slide_measurer import measure_rendered_heights # type: ignore
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot, settings # type: ignore
from selenium import webdriver # type: ignore
from selenium.webdriver.chrome.options import Options # type: ignore
from selenium.webdriver.common.by import By # type: ignore
COMPARISON_MARKER = "comparison-summary-card"
RELATION_MARKER = "relation-diagram-card"
@@ -52,7 +55,13 @@ def zone_overflow_names(measurement: dict) -> list[str]:
return names
def validate_outputs(generated: dict, measurement: dict, required_titles: list[str], run_mode: str) -> tuple[str, list[str], list[str]]:
def validate_outputs(
generated: dict,
measurement: dict,
required_titles: list[str],
run_mode: str,
layout_template: 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", "")
@@ -69,50 +78,74 @@ def validate_outputs(generated: dict, measurement: dict, required_titles: list[s
if measurement_missing:
failures.append("Verify-Measurement")
actions.append("?? ??? ?? ???? stage 3/4 ?? ? ?? ??? ?? ???? ?? ??? ?? ????.")
actions.append("?? ??? ?? ?? stage 3/4 ???? ?? ???? ??.")
if slide_overflow:
failures.append("Verify-RenderSlide")
actions.append("slide 전체 overflow를 해소하도록 layout budget 또는 전체 레이아웃 구조를 조정한다.")
actions.append("slide ?? overflow? ????? layout budget ?? ?? ???? ??? ????.")
if zone_overflows:
failures.append("Verify-RenderZone")
actions.append(f"overflow가 발생한 zone({', '.join(zone_overflows)}) content budget, block , typography를 재조정한다.")
actions.append(f"overflow? ??? zone({', '.join(zone_overflows)})? content budget, block ?, typography? ?????.")
if '???' in visible_text or '?? ??' in visible_text:
failures.append("Verify-Placeholder")
actions.append("placeholder나 깨진 라벨을 제거하고, 원문 제목/문장으로 다시 채운다.")
actions.append("placeholder? ?? ??? ????, ?? ??/???? ?? ???.")
matched_titles = sum(1 for title in required_titles if title and title in visible_text)
visible_len = len(re.sub(r'\s+', ' ', visible_text).strip())
if matched_titles < max(2, min(len(required_titles), 3)):
failures.append("Verify-SectionTitles")
actions.append("원문 섹션 제목을 가시 텍스트에 더 직접적으로 유지한다.")
actions.append("?? ?? ??? ?? ???? ? ????? ????.")
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("원문 표현을 유지하되 `상위 개념`과 `핵심 기술/핵심 인프라 기술` 판단이 가시 텍스트에 분명히 드러나도록 정리한다.")
actions.append("?? ??? ???? `?? ??`? `?? ??/?? ??? ??` ??? ?? ???? ??? ????? ????.")
if IMAGE_REFERENCE_KEY not in visible_text:
failures.append("Verify-ImageRef")
actions.append("이미지/도해 참조 문구 `DX와 핵심기술간 상호관계`를 숨김 영역이 아닌 가시 블록으로 유지한다.")
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축(범위, 프로세스, 성과품, 확장성)을 화면에 바로 보이는 요약 블록으로 강제한다.")
actions.append("?? ?? 4?(??, ????, ???, ???)? ??? ?? ??? ?? ???? ????.")
if RELATION_MARKER not in body_html:
failures.append("Verify-DesignStructure")
actions.append("핵심 관계를 설명하는 시각적 관계도 블록을 본문 중심 구조로 유지한다.")
actions.append("?? ??? ???? ??? ??? ??? ?? ?? ??? ????.")
else:
if len(re.sub(r'\s+', ' ', visible_text).strip()) < 260:
if visible_len < 420:
failures.append("Verify-ContentDensity")
actions.append("본문과 보조 영역의 원문 문장 보존량을 높여 내용 밀도를 보강한다.")
if not body_html or not sidebar_html:
actions.append("??? ?? ??? ?? ?? ???? ?? ?? ??? ????.")
if not body_html:
failures.append("Verify-DesignStructure")
actions.append("body와 sidebar의 역할을 분리하여 섹션별 배치를 다시 잡는다.")
actions.append("body ??? ?? ??? ?? ?? ?? ??? ???.")
if matched_titles < max(3, len([title for title in required_titles if title]) - 1):
failures.append("Verify-SectionTitles")
actions.append("?? ?? ??? ? ?? ?? ?? ???? ????.")
if layout_template == "B_GOAL":
for marker, reason in [
("Goal details", "?? ?? ?? ?? ?? ???? ????."),
("Process details", "?? ?? ?? ?? ???? ????."),
("Stakeholder details", "??? ???? ?? ?? ???? ????."),
]:
if marker not in body_html:
failures.append("Verify-DesignStructure")
actions.append(reason)
if body_html.count("<li") < 10:
failures.append("Verify-ContentDensity")
actions.append("??/??/?? ??? ?? bullet ?? ?? ?? ??? ???.")
elif layout_template == "B_RPP":
for title in required_titles[:3]:
if title and title not in body_html:
failures.append("Verify-SectionTitles")
actions.append("??/??/??? ?? ?? ??? ??? ?? ????.")
if body_html.count("<li") < 14:
failures.append("Verify-ContentDensity")
actions.append("??/??/?? ??? ?? bullet ?? ??? ????? ??? ???.")
if failures:
return "revise", sorted(set(failures)), list(dict.fromkeys(actions))
@@ -224,6 +257,42 @@ def post_comment_if_configured(repo: str, issue_number: int, body_file: Path) ->
create_comment(base_url, token, repo, issue_number, body)
def refresh_final_screenshot(final_html_path: Path, output_dir: Path) -> None:
if not final_html_path.exists():
return
driver = None
try:
options = Options()
options.add_argument("--headless=new")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--force-device-scale-factor=1")
options.add_argument(f"--window-size={settings.slide_width},{settings.slide_height + 200}")
driver = webdriver.Chrome(options=options)
driver.get(final_html_path.resolve().as_uri())
slide = driver.find_element(By.CSS_SELECTOR, ".slide")
screenshot_bytes = slide.screenshot_as_png
(output_dir / "final-screenshot-current.png").write_bytes(screenshot_bytes)
(output_dir / "final-screenshot.png").write_bytes(screenshot_bytes)
except Exception:
html = final_html_path.read_text(encoding="utf-8")
screenshot_b64 = capture_slide_screenshot(html)
if not screenshot_b64:
return
import base64
screenshot_bytes = base64.b64decode(screenshot_b64)
(output_dir / "final-screenshot-current.png").write_bytes(screenshot_bytes)
(output_dir / "final-screenshot.png").write_bytes(screenshot_bytes)
finally:
if driver:
try:
driver.quit()
except Exception:
pass
def compact_text(text: str, max_len: int) -> str:
normalized = re.sub(r"\s+", " ", text).strip()
if len(normalized) <= max_len:
@@ -496,13 +565,18 @@ KPI / 판정 결과
post_comment_if_configured(args.repo_slug, issue_numbers[5], step_comment_bodies[6])
continue
screenshot_path = output_dir / "final-screenshot-current.png"
if (not screenshot_path.exists()) or (screenshot_path.stat().st_mtime < final_html_path.stat().st_mtime):
refresh_final_screenshot(final_html_path, output_dir)
generated = read_json(generated_path)
measurement = read_json(measurement_path)
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)
layout_template = str(stage1a_data.get("analysis", {}).get("layout_template", "") or "")
status, failures, actions = validate_outputs(generated, measurement, required_titles, run_mode, layout_template)
final_html_text = final_html_path.read_text(encoding="utf-8")
if 'width:100%; height:28px' in final_html_text:
status = "revise"