import fs from 'node:fs/promises'; import path from 'node:path'; import sharp from 'sharp'; const buildDir = 'C:/Users/30214/Desktop/sub2本地/sub2新站/.codex-artifacts/juheai-astra-20260915'; const revision = process.argv[2] || '初稿'; const metrics = JSON.parse(await fs.readFile(path.join(buildDir, `text-metrics-${revision}.json`), 'utf8')); const checks = []; const failures = []; const collisions = []; for (let slideNumber = 1; slideNumber <= 10; slideNumber++) { const file = path.join(buildDir, `rendered-${revision}`, `slide-${slideNumber}.png`); const { data, info } = await sharp(file).removeAlpha().raw().toBuffer({ resolveWithObject: true }); const scaleX = info.width / 1280; const scaleY = info.height / 720; const slideTexts = metrics.filter(x => x.slide === slideNumber); for (const item of slideTexts) { const expected = [1, 3, 5].map(i => Number.parseInt(item.color.slice(i, i + 2), 16)); const x1 = Math.max(0, Math.floor(item.left * scaleX)); const y1 = Math.max(0, Math.floor(item.top * scaleY)); const x2 = Math.min(info.width, Math.ceil((item.left + item.width) * scaleX)); const y2 = Math.min(info.height, Math.ceil((item.top + item.height) * scaleY)); let pixelsWithTextColor = 0; for (let y = y1; y < y2; y++) { for (let x = x1; x < x2; x++) { const offset = (y * info.width + x) * info.channels; if (Math.abs(data[offset] - expected[0]) < 28 && Math.abs(data[offset + 1] - expected[1]) < 28 && Math.abs(data[offset + 2] - expected[2]) < 28) pixelsWithTextColor++; } } const pass = pixelsWithTextColor >= 12; checks.push({ slide: slideNumber, name: item.name, text: item.value, pixelsWithTextColor, pass }); if (!pass) failures.push({ slide: slideNumber, name: item.name, reason: 'No visible text-color pixels' }); } const boxes = slideTexts.map(item => ({ name: item.name, left: item.name.startsWith('folio-') ? item.left + item.width - item.measuredWidth : item.left, right: item.name.startsWith('folio-') ? item.left + item.width : item.left + item.measuredWidth, top: item.top + (item.height - item.measuredHeight) / 2, bottom: item.top + (item.height + item.measuredHeight) / 2, })); for (let a = 0; a < boxes.length; a++) { for (let b = a + 1; b < boxes.length; b++) { const x = Math.min(boxes[a].right, boxes[b].right) - Math.max(boxes[a].left, boxes[b].left); const y = Math.min(boxes[a].bottom, boxes[b].bottom) - Math.max(boxes[a].top, boxes[b].top); if (x > 1 && y > 1) collisions.push({ slide: slideNumber, first: boxes[a].name, second: boxes[b].name, overlapWidth: x, overlapHeight: y }); } } } const result = { claimBoundary: 'Pixel presence and measured text geometry only. No human visual review or OCR.', slideCount: 10, textRegionsChecked: checks.length, failures, collisions, checks }; await fs.writeFile(path.join(buildDir, `render-check-${revision}.json`), JSON.stringify(result, null, 2)); console.log(JSON.stringify({ slideCount: 10, textRegionsChecked: checks.length, failures, collisions }, null, 2)); if (failures.length || collisions.length) process.exitCode = 1;