inspect_rendered.mjs 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. import fs from 'node:fs/promises';
  2. import path from 'node:path';
  3. import sharp from 'sharp';
  4. const buildDir = 'C:/Users/30214/Desktop/sub2本地/sub2新站/.codex-artifacts/juheai-astra-20260915';
  5. const revision = process.argv[2] || '初稿';
  6. const metrics = JSON.parse(await fs.readFile(path.join(buildDir, `text-metrics-${revision}.json`), 'utf8'));
  7. const checks = [];
  8. const failures = [];
  9. const collisions = [];
  10. for (let slideNumber = 1; slideNumber <= 10; slideNumber++) {
  11. const file = path.join(buildDir, `rendered-${revision}`, `slide-${slideNumber}.png`);
  12. const { data, info } = await sharp(file).removeAlpha().raw().toBuffer({ resolveWithObject: true });
  13. const scaleX = info.width / 1280;
  14. const scaleY = info.height / 720;
  15. const slideTexts = metrics.filter(x => x.slide === slideNumber);
  16. for (const item of slideTexts) {
  17. const expected = [1, 3, 5].map(i => Number.parseInt(item.color.slice(i, i + 2), 16));
  18. const x1 = Math.max(0, Math.floor(item.left * scaleX));
  19. const y1 = Math.max(0, Math.floor(item.top * scaleY));
  20. const x2 = Math.min(info.width, Math.ceil((item.left + item.width) * scaleX));
  21. const y2 = Math.min(info.height, Math.ceil((item.top + item.height) * scaleY));
  22. let pixelsWithTextColor = 0;
  23. for (let y = y1; y < y2; y++) {
  24. for (let x = x1; x < x2; x++) {
  25. const offset = (y * info.width + x) * info.channels;
  26. 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++;
  27. }
  28. }
  29. const pass = pixelsWithTextColor >= 12;
  30. checks.push({ slide: slideNumber, name: item.name, text: item.value, pixelsWithTextColor, pass });
  31. if (!pass) failures.push({ slide: slideNumber, name: item.name, reason: 'No visible text-color pixels' });
  32. }
  33. const boxes = slideTexts.map(item => ({
  34. name: item.name,
  35. left: item.name.startsWith('folio-') ? item.left + item.width - item.measuredWidth : item.left,
  36. right: item.name.startsWith('folio-') ? item.left + item.width : item.left + item.measuredWidth,
  37. top: item.top + (item.height - item.measuredHeight) / 2,
  38. bottom: item.top + (item.height + item.measuredHeight) / 2,
  39. }));
  40. for (let a = 0; a < boxes.length; a++) {
  41. for (let b = a + 1; b < boxes.length; b++) {
  42. const x = Math.min(boxes[a].right, boxes[b].right) - Math.max(boxes[a].left, boxes[b].left);
  43. const y = Math.min(boxes[a].bottom, boxes[b].bottom) - Math.max(boxes[a].top, boxes[b].top);
  44. if (x > 1 && y > 1) collisions.push({ slide: slideNumber, first: boxes[a].name, second: boxes[b].name, overlapWidth: x, overlapHeight: y });
  45. }
  46. }
  47. }
  48. const result = { claimBoundary: 'Pixel presence and measured text geometry only. No human visual review or OCR.', slideCount: 10, textRegionsChecked: checks.length, failures, collisions, checks };
  49. await fs.writeFile(path.join(buildDir, `render-check-${revision}.json`), JSON.stringify(result, null, 2));
  50. console.log(JSON.stringify({ slideCount: 10, textRegionsChecked: checks.length, failures, collisions }, null, 2));
  51. if (failures.length || collisions.length) process.exitCode = 1;