← back to Wallco Ai

scripts/dalle_compare.js

96 lines

'use strict';
/**
 * Column 6 of the refiner comparison — OpenAI image generation, 1024×1024.
 *
 * Defaults to `gpt-image-1` (OpenAI's current image model, ~$0.04/image at
 * high quality 1024×1024). The sk-proj key we route doesn't expose dall-e-3,
 * and gpt-image-1 is the state-of-the-art replacement anyway.
 *
 * Same 25 prompts as refiner_compare.js. gpt-image-1 doesn't accept a seed,
 * and the prompt-rewriter behavior differs by model. We pass the explicit
 * "I NEED to test … AS-IS" preamble to discourage automatic rewriting.
 *
 *   node scripts/dalle_compare.js
 */
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { FASHION_PALETTES, paletteString } = require('./fashion_palettes');
const { STYLES, OUT, haveImg, save } = require('./compare_progress');

const TAG = '[dalle]';
// gpt-image-1 = OpenAI's current image model (DALL-E 3's replacement).
// Set OPENAI_IMAGE_MODEL=dall-e-3 to override if your key has dall-e-3 access.
const MODEL = process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1';

function loadKey() { return process.env.OPENAI_API_KEY || null; }

function buildPrompt(style, pal) {
  // "I NEED to test" is the OpenAI-documented preamble that suppresses
  // DALL-E 3's automatic prompt rewriting — we want the exact prompt to
  // hit the model so column-vs-column is a fair comparison.
  return 'I NEED to test how the model tokenizes this exact request. ' +
    'DO NOT add any detail, just use it AS-IS: ' +
    `A seamless wallpaper pattern, ${style} motif, ${paletteString(pal)}, ` +
    'seamless tile, repeating pattern, no edges, fabric pattern, wallpaper design, archival quality, high detail, ' +
    'screen-printed wallpaper, solid opaque flat ink, every motif fully filled with solid color, ' +
    'no halftone, no dot screens, no gradients, no text, no watermark, no border.';
}

function genDalle(prompt, outPath) {
  const KEY = loadKey();
  if (!KEY) throw new Error('OPENAI_API_KEY not set in wallco-ai/.env');

  const sh = (cmd, opts) => {
    try { return execSync(cmd, opts); }
    catch (e) { throw new Error(String((e && e.message) || e).split(KEY).join('sk-***REDACTED')); }
  };

  // gpt-image-1 always returns b64 (no response_format needed) and has a
  // 4-tier quality enum (low/medium/high/auto). For dall-e-3, override:
  //   OPENAI_IMAGE_MODEL=dall-e-3   (uses standard/hd quality)
  const isGpt = MODEL.startsWith('gpt-image');
  const body = {
    model: MODEL,
    prompt,
    n: 1,
    size: '1024x1024',
    quality: isGpt ? 'high' : 'hd',
    ...(isGpt ? {} : { response_format: 'b64_json' }),
  };

  const raw = sh(
    `curl -sf -m 120 -H 'Authorization: Bearer ${KEY}' -H 'Content-Type: application/json' -X POST 'https://api.openai.com/v1/images/generations' -d @-`,
    { input: JSON.stringify(body), encoding: 'utf8', maxBuffer: 40 * 1024 * 1024 },
  );

  let res;
  try { res = JSON.parse(raw); }
  catch { throw new Error(`DALL-E non-JSON response: ${raw.slice(0, 300)}`); }
  if (res.error) throw new Error(`DALL-E error: ${res.error.message || JSON.stringify(res.error)}`);

  const b64 = res.data && res.data[0] && res.data[0].b64_json;
  if (!b64) throw new Error(`DALL-E returned no image: ${JSON.stringify(res).slice(0, 300)}`);
  fs.writeFileSync(outPath, Buffer.from(b64, 'base64'));
  if (!haveImg(outPath)) throw new Error('DALL-E wrote a sub-1KB file (likely empty)');
  return outPath;
}

(async () => {
  const t0 = Date.now();
  let ok = 0, fail = 0, skip = 0;
  console.log(`${TAG} 25 renders via OpenAI ${MODEL} (HD)`);
  for (let i = 0; i < STYLES.length; i++) {
    const style = STYLES[i], pal = FASHION_PALETTES[i % FASHION_PALETTES.length];
    const outPath = path.join(OUT, `${i}_dalle.png`);
    if (haveImg(outPath)) { skip++; save(); console.log(`  ${i} dalle — skip`); continue; }
    try { genDalle(buildPrompt(style, pal), outPath); ok++; }
    catch (e) { fail++; console.log(`  ${i} dalle ERR ${e.message.slice(0, 200)}`); }
    save();
    console.log(`${TAG} [${i + 1}/25] ${style} · ${pal.brand} — ${haveImg(outPath) ? 'ok' : 'FAIL'} · ${((Date.now() - t0) / 60000).toFixed(1)}min`);
  }
  save();
  console.log(`${TAG} DONE — ${ok} rendered, ${skip} skipped, ${fail} failed · ${((Date.now() - t0) / 60000).toFixed(1)}min`);
})().catch(e => { console.error(`${TAG} FATAL`, e.message); process.exit(1); });