← back to Wallco Ai

lib/bad-aesthetic.js

73 lines

// lib/bad-aesthetic.js — closes the marquee-Delete-and-learn loop.
//
// Reads data/bad-aesthetic-patterns.jsonl (written by both the Ghost Review UI's
// bulk-delete endpoint and scripts/learn-and-purge-flagged.js) and exposes a
// tiny API the generators can call before spending a Gemini/SDXL call on a
// category that keeps producing defects.
//
// Schema per line:
//   { id, category, prompt, motifs, reason, ts }
//
// Behavior:
//   getCategoryFailureCount(category)   → integer
//   isCategoryCooldown(category, n=50)  → boolean ("skip this tick entirely")
//   getAvoidanceAddendum(category)      → string to append to a fresh prompt
//                                          when the category has ≥1 prior failure
//
// File is reloaded on every call (small file, append-only — millisecond cost).
// If the file is missing the lib returns the no-failures defaults.

'use strict';

const fs   = require('fs');
const path = require('path');

const FILE = path.join(__dirname, '..', 'data', 'bad-aesthetic-patterns.jsonl');

function loadRows() {
  try {
    const raw = fs.readFileSync(FILE, 'utf8');
    const rows = [];
    for (const line of raw.split('\n')) {
      if (!line.trim()) continue;
      try { rows.push(JSON.parse(line)); } catch {}
    }
    return rows;
  } catch { return []; }
}

function getCategoryFailureCount(category) {
  if (!category) return 0;
  const want = String(category).toLowerCase();
  return loadRows().filter(r => (r.category || '').toLowerCase() === want).length;
}

function isCategoryCooldown(category, threshold = 50) {
  return getCategoryFailureCount(category) >= threshold;
}

// Build a category-targeted avoidance addendum from the reason strings actually
// observed in past failures. Falls back to a generic anti-ghost addendum if the
// category has prior failures but no parseable reasons.
function getAvoidanceAddendum(category) {
  const rows = loadRows().filter(r => (r.category || '').toLowerCase() === String(category || '').toLowerCase());
  if (!rows.length) return '';
  const reasons = [...new Set(rows.map(r => r.reason || '').filter(Boolean))].slice(0, 3);
  const n = rows.length;
  return (
    `\n\n[LEARNED FROM PRIOR FAILURES — ${n} prior generation${n === 1 ? '' : 's'} in this ` +
    `"${category}" category were rejected with these reasons: ${reasons.join(' · ')}. ` +
    `Take EXTRA CARE to avoid: ghost-layer / faded duplicate motifs / atmospheric haze over ` +
    `flat patterns / multi-opacity copies of the same motif / "halo" pale silhouettes echoing ` +
    `sharp ones. Render as a SINGLE flat layer, hard razor-sharp silhouette edges, uniform ` +
    `opacity across every motif, ground is one flat solid color edge-to-edge.]`
  );
}

module.exports = {
  loadRows,
  getCategoryFailureCount,
  isCategoryCooldown,
  getAvoidanceAddendum,
};