← back to Secrets Manager
coordonne-collab-audit/settlement-gate-coco.js
72 lines
#!/usr/bin/env node
// Settlement post-gen-vision gate for the 54 Coco Dávez Colortherapills images.
// Runs the 5 detectors as ONE structured Gemini 2.0-flash vision call per image,
// then applies the settlement-verdict combinator (BLOCK iff A1&A2&A3&B, else OK;
// any "unknown" fails closed to BLOCK).
// Gemini vision only (Anthropic vision API is banned, per CLAUDE.md 2026-06-10).
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ENV = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8');
const GKEY = (ENV.match(/^GEMINI_API_KEY=(.+)$/m) || [])[1];
if (!GKEY) { console.error('no GEMINI_API_KEY'); process.exit(1); }
const rows = JSON.parse(fs.readFileSync(path.join(__dirname, 'coco-rows.json'), 'utf8'));
const PROMPT = `You are a strict legal image-compliance detector for a wallpaper settlement gate. Look ONLY at the pixels in this image. Answer these 5 questions about the artwork as a JSON object with boolean values (use "unknown" only if the image is unreadable):
- a1_directional_leaves: Does the design contain repeating leaves / palm fronds / similar FOLIAGE with directional variation (leaves pointing in more than one orientation across the repeat)? true/false
- a2_open_space: Is there visible open negative space BETWEEN foliage/leaf motifs (as opposed to edge-to-edge coverage, OR the design simply has no foliage)? Answer true ONLY if there IS foliage AND there is open space between the leaves; if there is no foliage at all, answer false.
- a3_multiple_colors: Does the LEAF/FOLIAGE layer specifically use more than one ink color? If there is no foliage, answer false.
- b_prohibited: Does the artwork depict any of these SPECIFIC elements: a banana, banana pod, grapes, a BIRD, or a BUTTERFLY? true/false
- acceptable: Does it contain tree trunks, clearly-represented branches, or a fruit/animal element OTHER than banana/grape/bird/butterfly? true/false
Respond with ONLY the JSON object, no prose. Example: {"a1_directional_leaves":false,"a2_open_space":false,"a3_multiple_colors":false,"b_prohibited":false,"acceptable":false}`;
function verdict(d) {
const vals = [d.a1_directional_leaves, d.a2_open_space, d.a3_multiple_colors, d.b_prohibited];
if (vals.some(v => v === 'unknown' || v === null || v === undefined)) return { v: 'BLOCK', reason: 'unknown detector input — fail closed' };
const partA = d.a1_directional_leaves === true && d.a2_open_space === true && d.a3_multiple_colors === true;
if (partA && d.b_prohibited === true) return { v: 'BLOCK', reason: 'Part A fully satisfied AND Part B present' };
return { v: 'OK', reason: partA ? 'Part A only, no prohibited element' : 'Part A not fully satisfied' };
}
async function gemini(imgB64, mime) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GKEY}`;
const body = {
contents: [{ parts: [{ text: PROMPT }, { inline_data: { mime_type: mime, data: imgB64 } }] }],
generationConfig: { temperature: 0, response_mime_type: 'application/json' },
};
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const j = await r.json();
const txt = j?.candidates?.[0]?.content?.parts?.[0]?.text;
if (!txt) throw new Error('no gemini text: ' + JSON.stringify(j).slice(0, 300));
return JSON.parse(txt);
}
(async () => {
const out = [];
let calls = 0;
for (const row of rows) {
let det, verd, err = null;
try {
// fetch image bytes
const ir = await fetch(row.image_url);
if (!ir.ok) throw new Error('img fetch ' + ir.status);
const buf = Buffer.from(await ir.arrayBuffer());
const mime = ir.headers.get('content-type') || 'image/jpeg';
det = await gemini(buf.toString('base64'), mime);
calls++;
verd = verdict(det);
} catch (e) {
det = null; verd = { v: 'BLOCK', reason: 'error: ' + e.message }; err = e.message;
}
out.push({ dw_sku: row.dw_sku, pattern: row.pattern_name, verdict: verd.v, reason: verd.reason, detectors: det, error: err });
process.stderr.write(`${row.dw_sku} ${row.pattern_name} -> ${verd.v}\n`);
await new Promise(r => setTimeout(r, 300));
}
const blocks = out.filter(o => o.verdict === 'BLOCK');
const summary = { total: out.length, ok: out.filter(o => o.verdict === 'OK').length, block: blocks.length, gemini_calls: calls, results: out };
fs.writeFileSync(path.join(__dirname, 'coco-settlement-result.json'), JSON.stringify(summary, null, 2));
console.log(JSON.stringify({ total: summary.total, ok: summary.ok, block: summary.block, gemini_calls: calls, blocked: blocks.map(b => ({ sku: b.dw_sku, pattern: b.pattern, reason: b.reason })) }, null, 2));
})();