feat(IMP-15): 실행-1 — Step 14 image_aspect_mismatch detection
Issue: #45 (IMP-15 실행-1, image axis only). Adds Selenium-based <img> aspect ratio measurement to Step 14 run_overflow_check + numeric tolerance gate. Tolerance lives as module-scope constant so tests can import it. src/phase_z2_pipeline.py (+73/-2): - L131-L135 IMAGE_ASPECT_DELTA_TOL = 0.05 (module scope, importable) - L2216-L2261 JS payload extension: image_events[] per <img> (src, zone_position via closest('.zone') with 'unknown' fallback, zone_template_id, natural/rendered w+h+ratio, delta, slide-rel bbox) - L2262 run_overflow_check return extended with image_events - L2302-L2320 Python aggregation: abs(delta) > TOL ⇒ fail_reasons append 'image aspect mismatch in zone--<pos>: natural=<n> rendered=<r> delta=<+d> (template=<tid>, tol=0.05, src=<src>)'. Null-delta entries (image not loaded) are skipped — no false positive. Branch placed AFTER existing non-image branches; ordering & strings for slide/slide-body/zone/clipped_inner unchanged. - L4425-L4429 Step 14 note: image half closed, table half deferred to 실행-2. tests/phase_z2/test_phase_z2_step14_image_check.py (+196, new): - 3-tier chromedriver resolver mirroring pipeline (PROJECT_ROOT/ chromedriver{,.exe} → PATH → Selenium Manager probe). - pytestmark: skip when chromedriver unresolvable AND PHASE_Z_REQUIRE_SELENIUM != '1'; xfail(strict=True) opt-in when =='1'. - Fixture A: 200×100 img rendered 200×100 → aspect_delta < 0.05, passed. - Fixture B: 200×100 intrinsic forced to 200×200 → delta > 0.30, fail_reason present. - Fixture C: <img> with no .zone ancestor → zone_position == 'unknown'. Verification (Stage 4 PASS, Claude + Codex independent): - pytest -q tests/phase_z2/test_phase_z2_step14_image_check.py → 3 passed - PHASE_Z_REQUIRE_SELENIUM=1 same suite → 3 passed (strict opt-in) - pytest -q tests/phase_z2 → 90 passed (no regression) - pytest -q --ignore=tests/matching → 174 passed Scope-locked: no slide_base.html / catalog / classifier / debug.json / spec-doc changes. table_events (실행-2), visual_check_passed flip (실행-3), debug.json image_events surfacing + PHASE-Z spec doc row (실행-4) remain queued as separate IMP-15 child execution issues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,11 @@ GRID_GAP = 14 # zone 간격 (사용자 직설 2026-05-07)
|
||||
# token-based font (var(--font-body) 11px 등) 기준 최소 가독 높이.
|
||||
DEFAULT_ZONE_MIN_HEIGHT_PX = 100
|
||||
|
||||
# Step 14 image_aspect_mismatch tolerance — |natural_ratio - rendered_ratio| > TOL ⇒ fail.
|
||||
# Local anchor : IMP-15 실행-1 (Gitea issue #45) — image axis acceptance criteria.
|
||||
# Spec doc row (PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC) update deferred to IMP-15 실행-4.
|
||||
IMAGE_ASPECT_DELTA_TOL = 0.05
|
||||
|
||||
# content_weight 계산 가중치
|
||||
CONTENT_WEIGHT_COEFFS = {
|
||||
"text_per_chars": 800, # text_len / 800 = score
|
||||
@@ -2208,7 +2213,53 @@ def run_overflow_check(html_path: Path) -> dict:
|
||||
});
|
||||
});
|
||||
|
||||
return { slide: slideM, slide_body: bodyM, zones, frame_slot_metrics, zone_geometries_px };
|
||||
// IMP-15 실행-1 (issue #45) — image_events[] for image_aspect_mismatch detection.
|
||||
// 하나의 entry per <img> under .slide. natural vs rendered aspect 비교.
|
||||
// zone_position : closest('.zone') data-zone-position. 없으면 literal "unknown".
|
||||
const image_events = [];
|
||||
slide.querySelectorAll('img').forEach((img) => {
|
||||
const parentZone = img.closest('.zone');
|
||||
const zonePos = parentZone
|
||||
? (parentZone.getAttribute('data-zone-position') || 'unknown')
|
||||
: 'unknown';
|
||||
const zoneTid = parentZone
|
||||
? (parentZone.getAttribute('data-template-id') || '?')
|
||||
: '?';
|
||||
const imgRect = img.getBoundingClientRect();
|
||||
const rendered_w = imgRect.width;
|
||||
const rendered_h = imgRect.height;
|
||||
const natural_w = img.naturalWidth;
|
||||
const natural_h = img.naturalHeight;
|
||||
const natural_ratio = (natural_w > 0 && natural_h > 0)
|
||||
? (natural_w / natural_h)
|
||||
: null;
|
||||
const rendered_ratio = (rendered_w > 0 && rendered_h > 0)
|
||||
? (rendered_w / rendered_h)
|
||||
: null;
|
||||
const delta = (natural_ratio !== null && rendered_ratio !== null)
|
||||
? (rendered_ratio - natural_ratio)
|
||||
: null;
|
||||
image_events.push({
|
||||
src: img.getAttribute('src') || '',
|
||||
zone_position: zonePos,
|
||||
zone_template_id: zoneTid,
|
||||
natural_w: natural_w,
|
||||
natural_h: natural_h,
|
||||
rendered_w: Math.round(rendered_w),
|
||||
rendered_h: Math.round(rendered_h),
|
||||
natural_ratio: natural_ratio,
|
||||
rendered_ratio: rendered_ratio,
|
||||
delta: delta,
|
||||
bbox: {
|
||||
x: Math.round(imgRect.left - slideRect.left),
|
||||
y: Math.round(imgRect.top - slideRect.top),
|
||||
w: Math.round(rendered_w),
|
||||
h: Math.round(rendered_h),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { slide: slideM, slide_body: bodyM, zones, frame_slot_metrics, zone_geometries_px, image_events };
|
||||
""")
|
||||
|
||||
screenshot_path = html_path.parent / "preview.png"
|
||||
@@ -2248,6 +2299,25 @@ def run_overflow_check(html_path: Path) -> dict:
|
||||
f"(content {c['scrollHeight']} vs container {c['clientHeight']})"
|
||||
)
|
||||
|
||||
# IMP-15 실행-1 (issue #45) — image_aspect_mismatch aggregation.
|
||||
# |natural_ratio - rendered_ratio| > IMAGE_ASPECT_DELTA_TOL ⇒ fail_reason append.
|
||||
# Entries with null ratio (image not loaded / natural dims = 0) are skipped (no false positive).
|
||||
for ev in result.get("image_events", []):
|
||||
delta = ev.get("delta")
|
||||
if delta is None:
|
||||
continue
|
||||
if abs(delta) > IMAGE_ASPECT_DELTA_TOL:
|
||||
n_ratio = ev.get("natural_ratio")
|
||||
r_ratio = ev.get("rendered_ratio")
|
||||
src = ev.get("src", "")
|
||||
pos = ev.get("zone_position", "unknown")
|
||||
tid = ev.get("zone_template_id", "?")
|
||||
fail_reasons.append(
|
||||
f"image aspect mismatch in zone--{pos}: "
|
||||
f"natural={n_ratio:.3f} rendered={r_ratio:.3f} delta={delta:+.3f} "
|
||||
f"(template={tid}, tol={IMAGE_ASPECT_DELTA_TOL}, src={src})"
|
||||
)
|
||||
|
||||
result["passed"] = len(fail_reasons) == 0
|
||||
result["fail_reasons"] = fail_reasons
|
||||
return result
|
||||
@@ -4355,7 +4425,8 @@ def run_phase_z2_mvp1(
|
||||
note=(
|
||||
"Selenium 실측 — clientHeight / scrollHeight / excess_y / frame_slot_metrics. "
|
||||
"Step 8 의 계획값과 비교 시 어느 cell 이 overflow 했는지 박힘. "
|
||||
"image / table 검사 부재 — Step 14 ⚠ partial."
|
||||
"image_aspect_mismatch 검사 추가 (IMP-15 실행-1, issue #45) — image_events[] + fail_reasons. "
|
||||
"table 검사 부재 (실행-2 잔류) — Step 14 ⚠ partial (table only)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user