← back to Wallco Ai

scripts/detect-blank-designs.js

87 lines

#!/usr/bin/env node
/**
 * detect-blank-designs.js
 * ───────────────────────────────────────────────────────────────────────────
 * Scans every design image referenced by data/designs.json and flags the
 * "failed generation" blanks — near-white PNGs the generator produced when a
 * batch (e.g. the `drunk-animals` run, design ids ~7679+) silently failed.
 *
 * A real wallpaper pattern fills the canvas: grayscale mean ~0.30-0.85.
 * A failed blank is ~99% white: mean > 0.95 and/or std-dev < 0.04.
 *
 * Output: data/blank-designs.json — consumed by server.js so /designs,
 * /api/designs, random, by-color and search never surface a blank tile.
 *
 * Re-run any time after a generation batch:  node scripts/detect-blank-designs.js
 */
'use strict';
const fs   = require('fs');
const path = require('path');
const { execFile } = require('child_process');

const ROOT       = path.join(__dirname, '..');
const DATA_FILE  = path.join(ROOT, 'data', 'designs.json');
const IMG_DIR    = path.join(ROOT, 'data', 'generated');
const OUT_FILE   = path.join(ROOT, 'data', 'blank-designs.json');
const CONCURRENCY = 16;

// Blank if the image is overwhelmingly white OR essentially flat.
const MEAN_MAX = 0.95;   // mean luminance above this ⇒ near-white canvas
const STD_MIN  = 0.04;   // std-dev below this ⇒ no real pattern variation

function measure(file) {
  return new Promise((resolve) => {
    execFile('magick', [file, '-colorspace', 'Gray',
      '-format', '%[fx:mean] %[fx:standard_deviation]', 'info:'],
      { timeout: 20000 }, (err, stdout) => {
        if (err) return resolve(null);
        const [mean, std] = String(stdout).trim().split(/\s+/).map(Number);
        if (!Number.isFinite(mean) || !Number.isFinite(std)) return resolve(null);
        resolve({ mean, std });
      });
  });
}

(async () => {
  const designs = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
  console.log(`[detect-blank] ${designs.length} designs in designs.json`);

  const blank = [];
  const missing = [];
  let measured = 0, idx = 0;

  async function worker() {
    while (idx < designs.length) {
      const d = designs[idx++];
      const fn = d.filename ||
        (d.image_url && d.image_url.startsWith('/designs/img/')
          ? d.image_url.replace('/designs/img/', '') : null);
      if (!fn) continue;
      const file = path.join(IMG_DIR, fn);
      if (!fs.existsSync(file)) { missing.push(d.id); continue; }
      const m = await measure(file);
      measured++;
      if (measured % 500 === 0) console.log(`[detect-blank] measured ${measured}…`);
      if (!m) continue;
      if (m.mean > MEAN_MAX || m.std < STD_MIN) {
        blank.push({ id: d.id, filename: fn, mean: +m.mean.toFixed(4), std: +m.std.toFixed(4) });
      }
    }
  }
  await Promise.all(Array.from({ length: CONCURRENCY }, worker));

  blank.sort((a, b) => a.id - b.id);
  const out = {
    generated_at: new Date().toISOString(),
    criteria: { mean_max: MEAN_MAX, std_min: STD_MIN },
    designs_scanned: measured,
    images_missing: missing.length,
    blank_count: blank.length,
    blank_ids: blank.map(b => b.id),
    detail: blank,
  };
  fs.writeFileSync(OUT_FILE, JSON.stringify(out, null, 2));
  console.log(`[detect-blank] ${blank.length} blank designs flagged → ${OUT_FILE}`);
  console.log(`[detect-blank] ${missing.length} designs have no image file on disk`);
})();