Phase W + V' 완료: before→filled→after 파이프라인 + 조립 로직 수정
Phase W: - weight 비율 초기 배정 (space_allocator header 높이 반영) - block_assembler 공통 조립 함수 (filled/assembled 통합) - filled → Selenium 측정 → context 저장 - sidebar overflow 확장 + body 재배분 - sub_layouts 사전 계산 (이미지 누락 해결) Phase V': - 팝업 링크 우측상단 배치 (인라인 → position:absolute) - 표 내용 Kei 판단 (공란 크기 계산 → 행/열 산출 → Kei 요약) - 출처 라벨 삭제 + 이미지 아래 캡션 배치 - after 공란 제거 (결론 바로 위까지 body/sidebar 채움) 추가: - V-10 bold 키워드: 기계적 추출 → Kei 문맥 판단 - ** 마크다운 → <strong> 변환 - [이미지:] 마커 제거 (bold 변환 전 처리) - grid-template-rows AFTER 크기 반영 (Sonnet final) - assemble_stage2 CSS font-size override, white-space fix - 하드코딩 전수 검토 완료 - 본심 여러 topic 텍스트 합침 Phase X 계획 문서 작성 (동적 역할 구조) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
208
scripts/test_phase_t_audit.py
Normal file
208
scripts/test_phase_t_audit.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""Phase T 전수 검사.
|
||||
|
||||
1. 모든 파일 syntax
|
||||
2. 모든 import chain
|
||||
3. pipeline.py 내 이름 참조
|
||||
4. lazy import 유효성
|
||||
5. catalog.yaml
|
||||
6. Pydantic 모델
|
||||
7. 실제 데이터 Stage 0~1.5b
|
||||
8. Stage 3 render 호출
|
||||
9. Stage 2 supplement 생성
|
||||
"""
|
||||
import ast, re, json, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
errors = []
|
||||
|
||||
def check(name, condition, detail=""):
|
||||
if condition:
|
||||
print(f" OK {name}")
|
||||
else:
|
||||
print(f" FAIL {name} -- {detail}")
|
||||
errors.append(f"{name}: {detail}")
|
||||
|
||||
|
||||
print("-- 1. Syntax --")
|
||||
for f in Path("src").glob("*.py"):
|
||||
try:
|
||||
ast.parse(f.read_text(encoding="utf-8"))
|
||||
print(f" OK {f.name}")
|
||||
except SyntaxError as e:
|
||||
print(f" FAIL {f.name}: {e}")
|
||||
errors.append(f"syntax: {f.name}")
|
||||
|
||||
print("\n-- 2. Import --")
|
||||
for mod in ["src.pipeline_context", "src.mdx_normalizer", "src.validators",
|
||||
"src.block_reference", "src.space_allocator", "src.html_generator",
|
||||
"src.content_verifier", "src.renderer", "src.kei_client",
|
||||
"src.image_utils", "src.slide_measurer", "src.config",
|
||||
"src.main", "src.pipeline"]:
|
||||
try:
|
||||
__import__(mod)
|
||||
print(f" OK {mod}")
|
||||
except Exception as e:
|
||||
print(f" FAIL {mod}: {e}")
|
||||
errors.append(f"import: {mod}")
|
||||
|
||||
print("\n-- 3. pipeline.py import 참조 --")
|
||||
psrc = Path("src/pipeline.py").read_text(encoding="utf-8")
|
||||
needed = ["PipelineContext", "Topic", "NormalizedContent", "Analysis",
|
||||
"PageStructure", "ContainerInfo", "TextBudget", "DesignBudget",
|
||||
"FontHierarchy", "BlockReference", "StageFailure",
|
||||
"build_retry_feedback", "create_context"]
|
||||
import_block = re.search(r"from src\.pipeline_context import \((.*?)\)", psrc, re.DOTALL)
|
||||
imported = set()
|
||||
if import_block:
|
||||
imported = {n.strip() for n in import_block.group(1).split(",") if n.strip()}
|
||||
for name in needed:
|
||||
if name in psrc and name not in imported:
|
||||
# 메서드인지 확인
|
||||
is_method = all(("." + name) in line or name not in line
|
||||
for line in psrc.split("\n")
|
||||
if "from src.pipeline_context" not in line)
|
||||
if not is_method:
|
||||
check(f"import {name}", False, "사용되지만 import 안 됨")
|
||||
else:
|
||||
check(f"import {name}", True)
|
||||
else:
|
||||
check(f"import {name}", name in imported or name not in psrc)
|
||||
|
||||
print("\n-- 4. lazy import --")
|
||||
for mod_name, func_name in re.findall(r"from (src\.\w+) import (\w+)", psrc):
|
||||
if "pipeline_context" in mod_name:
|
||||
continue
|
||||
try:
|
||||
mod = __import__(mod_name, fromlist=[func_name])
|
||||
check(f"{mod_name}.{func_name}", hasattr(mod, func_name))
|
||||
except Exception as e:
|
||||
check(f"{mod_name}.{func_name}", False, str(e))
|
||||
|
||||
print("\n-- 5. catalog.yaml --")
|
||||
import yaml
|
||||
data = yaml.safe_load(Path("templates/catalog.yaml").read_text(encoding="utf-8"))
|
||||
blocks = data.get("blocks", [])
|
||||
check("blocks count", len(blocks) == 38, f"got {len(blocks)}")
|
||||
check("schema 38/38", sum(1 for b in blocks if b.get("schema")) == 38)
|
||||
check("visual_diff 20", sum(1 for b in blocks if b.get("visual_diff")) == 20)
|
||||
|
||||
print("\n-- 6. Pydantic --")
|
||||
from src.pipeline_context import *
|
||||
check("create_context", create_context("test") is not None)
|
||||
check("FontHierarchy OK", FontHierarchy(key_msg=14, core=12, bg=11, sidebar=10) is not None)
|
||||
try:
|
||||
FontHierarchy(key_msg=10, core=12, bg=14, sidebar=9)
|
||||
check("FontHierarchy violation", False, "not caught")
|
||||
except:
|
||||
check("FontHierarchy violation", True)
|
||||
check("Topic no weight", "weight" not in Topic.model_fields)
|
||||
check("DesignBudget", DesignBudget(available_height_px=100) is not None)
|
||||
|
||||
print("\n-- 7. 실제 데이터 Stage 0~1.5b --")
|
||||
s0 = json.loads(Path("data/runs/20260401_151426/stage_0_context.json").read_text(encoding="utf-8"))
|
||||
a1 = json.loads(Path("data/runs/1774922951020/step1_analysis.json").read_text(encoding="utf-8"))
|
||||
c1b = json.loads(Path("data/runs/1774922951020/step1b_concepts.json").read_text(encoding="utf-8"))
|
||||
|
||||
# 1A
|
||||
topics = [Topic(**{k: v for k, v in t.items() if k in Topic.model_fields}) for t in a1["topics"]]
|
||||
check("1A Topic 변환", len(topics) == 5)
|
||||
|
||||
# 1B
|
||||
concepts = c1b.get("concepts", [])
|
||||
updated = []
|
||||
for t in topics:
|
||||
m = next((c for c in concepts if c.get("id") == t.id), None)
|
||||
if m:
|
||||
updated.append(t.model_copy(update={
|
||||
"relation_type": m.get("relation_type", ""),
|
||||
"expression_hint": m.get("expression_hint", ""),
|
||||
"source_data": m.get("source_data", ""),
|
||||
}))
|
||||
else:
|
||||
updated.append(t)
|
||||
check("1B 병합", len(updated) == 5)
|
||||
|
||||
# 검증
|
||||
from src.validators import validate_stage_1a, validate_stage_1b
|
||||
e1a = validate_stage_1a(a1, s0["normalized"]["clean_text"])
|
||||
check("1A 검증", not e1a, str(e1a)[:100] if e1a else "")
|
||||
e1b = validate_stage_1b([t.model_dump() for t in updated], s0["normalized"]["clean_text"], raw_content=s0["raw_content"])
|
||||
check("1B 검증", not e1b, str(e1b)[:100] if e1b else "")
|
||||
|
||||
# 1.5a
|
||||
from src.space_allocator import calculate_font_hierarchy, calculate_dynamic_ratio, calculate_container_specs, calculate_design_budget
|
||||
from src.design_director import LAYOUT_PRESETS, select_preset
|
||||
from src.block_reference import select_and_generate_references
|
||||
|
||||
ctx = create_context(s0["raw_content"])
|
||||
ctx = ctx.model_copy(update={
|
||||
"normalized": NormalizedContent(**s0["normalized"]),
|
||||
"topics": updated,
|
||||
"page_structure": PageStructure(roles=a1.get("page_structure", {})),
|
||||
"analysis": Analysis(core_message=a1.get("core_message", ""), title=a1.get("title", "")),
|
||||
})
|
||||
rtl = {role: len(ctx.get_role_content(role)) for role in ["배경", "본심", "첨부", "결론"]}
|
||||
fh_dict = calculate_font_hierarchy(rtl)
|
||||
fh = FontHierarchy(key_msg=fh_dict["핵심"], core=fh_dict["본심"], bg=fh_dict["배경"], sidebar=fh_dict["첨부"])
|
||||
check("1.5a 폰트위계", fh.key_msg > fh.core >= fh.bg > fh.sidebar)
|
||||
|
||||
ratio = calculate_dynamic_ratio(rtl, fh_dict)
|
||||
check("1.5a 비율", ratio[0] + ratio[1] == 100)
|
||||
|
||||
preset_name = select_preset(a1)
|
||||
preset = LAYOUT_PRESETS.get(preset_name, {})
|
||||
specs = calculate_container_specs(a1.get("page_structure", {}), [t.model_dump() for t in updated], preset)
|
||||
check("1.5a 컨테이너", len(specs) >= 3)
|
||||
|
||||
# 1.7
|
||||
refs = select_and_generate_references([t.model_dump() for t in updated], specs, a1.get("page_structure", {}))
|
||||
check("1.7 참고블록", len(refs) >= 3)
|
||||
|
||||
# 1.5b
|
||||
for role, spec in specs.items():
|
||||
ref = refs.get(role, {})
|
||||
schema = ref.get("schema_info", {})
|
||||
font_map = {"본심": fh.core, "배경": fh.bg, "첨부": fh.sidebar, "결론": fh.core}
|
||||
budget = calculate_design_budget(spec.height_px, spec.width_px, schema, font_map.get(role, 12))
|
||||
db = DesignBudget(**budget)
|
||||
check(f"1.5b {role}", True)
|
||||
|
||||
print("\n-- 8. Stage 3 render --")
|
||||
from src.renderer import render_slide_from_html
|
||||
mock_gen = {
|
||||
"body_html": '<div style="overflow:hidden"><div class="key-msg">test</div></div>',
|
||||
"sidebar_html": '<div style="overflow:hidden; padding-left:14px; text-indent:-14px;">side</div>',
|
||||
"footer_html": "<div>foot</div>",
|
||||
}
|
||||
analysis_dict = {
|
||||
"topics": [t.model_dump() for t in updated],
|
||||
"page_structure": a1.get("page_structure", {}),
|
||||
"core_message": a1.get("core_message", ""),
|
||||
"title": a1.get("title", ""),
|
||||
}
|
||||
html = render_slide_from_html(mock_gen, analysis_dict, preset)
|
||||
check("Stage 3 render", len(html) > 100, f"len={len(html)}")
|
||||
|
||||
print("\n-- 9. Stage 2 supplement --")
|
||||
from src.html_generator import _build_phase_t_supplement
|
||||
phase_t_ctx = {
|
||||
"font_hierarchy": fh.model_dump(),
|
||||
"container_ratio": ratio,
|
||||
"references": {r: v for r, v in refs.items()},
|
||||
"design_budgets": {},
|
||||
}
|
||||
for role in ["배경", "본심", "첨부", "결론"]:
|
||||
supp = _build_phase_t_supplement(role, {"phase_t": phase_t_ctx})
|
||||
check(f"supplement {role}", len(supp) > 50, f"len={len(supp)}")
|
||||
|
||||
# 결과
|
||||
print()
|
||||
if errors:
|
||||
print(f"=== FAIL: {len(errors)}건 ===")
|
||||
for e in errors:
|
||||
print(f" - {e}")
|
||||
else:
|
||||
print("=== 전수 검사 통과: 오류 0건 ===")
|
||||
|
||||
sys.exit(1 if errors else 0)
|
||||
Reference in New Issue
Block a user