Iterative Edits

Iterating with Edits Instead of Rewriting

When a picture is nearly right, rewriting the prompt rolls the dice on everything. Edit it instead, with one change and "Change nothing else." A short edit.mjs (mask-edit.mjs without the mask) takes the output file, the instruction and any number of input images from the command line:

Two edits: add a scarf, then move Nestor into a kitchenShell
node edit.mjs nestor-scarf.png "Add a green knitted scarf (#3f7d3a). Change nothing else." \
  nestor.png
node edit.mjs nestor-kitchen.png "The same bird, same scarf and proportions, reading a \
cookbook at a kitchen table with spice jars behind. Same flat style and palette." \
  nestor-scarf.png
Output
nestor-scarf.png: 1024 image + 33 text tokens in, 196 out
nestor-kitchen.png: 1024 image + 43 text tokens in, 196 out

The scarf edit kept the pose, eyes and book exactly, for $0.0142.

Edit versus rewrite, measured with a pixel difference map: an edit changes only the scarf, a rewrite changes everythingHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
  .row { display: flex; gap: 6px; flex-wrap: wrap; }
  figure { margin: 0; text-align: center; width: 140px; }
  canvas { display: block; border: 1px solid #e0d8c8; background: #fff; }
</style>
<p><button id="reroll">Rewrite the prompt again (new random bird)</button></p>
<div class="row">
  <figure><canvas id="a" width="140" height="140"></canvas>nestor.png</figure>
  <figure><canvas id="b" width="140" height="140"></canvas>edit: + scarf</figure>
  <figure><canvas id="d1" width="140" height="140"></canvas>changed pixels</figure>
  <figure><canvas id="c" width="140" height="140"></canvas>rewrite: + scarf</figure>
  <figure><canvas id="d2" width="140" height="140"></canvas>changed pixels</figure>
</div>
<p id="out"></p>
<script>
  // Nestor with a set of traits; a rewrite re-rolls the traits, an edit keeps them
  function nestor(g, t, scarf) {
    g.fillStyle = '#fff';  g.fillRect(0, 0, 140, 140);
    g.save();  g.translate(70 + t.dx, 72);  g.rotate(t.tilt);
    g.fillStyle = t.body;  g.beginPath();  g.ellipse(0, 0, 34 * t.w, 38, 0, 0, 7);  g.fill();
    g.fillStyle = '#e09a10';  g.beginPath();  g.ellipse(5, 10, 20 * t.w, 22, 0, 0, 7);  g.fill();
    g.fillStyle = '#fff';  g.beginPath();  g.arc(10, -16, 7, 0, 7);  g.fill();
    g.fillStyle = '#263238';  g.beginPath();  g.arc(12, -16, 3.5, 0, 7);  g.fill();
    g.fillStyle = '#e09a10';  g.beginPath();  g.moveTo(30 * t.w, -8);  g.lineTo(44 * t.w, -3);  g.lineTo(30 * t.w, 2);  g.fill();
    if (scarf) { g.fillStyle = '#3f7d3a';  g.fillRect(-26, -6, 52, 9);  g.fillRect(-18, 0, 9, 22); }
    g.fillStyle = '#fff3e0';  g.fillRect(-26, 16, 44, 16);  g.strokeStyle = t.body;  g.strokeRect(-26, 16, 44, 16);
    g.restore();
  }
  const randomTraits = () => ({ body: ['#1f5f8b', '#2a6f9e', '#194d73'][Math.floor(Math.random() * 3)],
    w: 0.85 + Math.random() * 0.3, tilt: (Math.random() - 0.5) * 0.4, dx: (Math.random() - 0.5) * 20 });
  const base = { body: '#1f5f8b', w: 1, tilt: 0, dx: 0 };
  const ctx = id => document.getElementById(id).getContext('2d');
  function diff(from, to, out) {
    const a = ctx(from).getImageData(0, 0, 140, 140).data, b = ctx(to).getImageData(0, 0, 140, 140).data;
    const d = ctx(out).createImageData(140, 140);
    let changed = 0;
    for (let i = 0; i < a.length; i += 4) {
      const delta = Math.abs(a[i] - b[i]) + Math.abs(a[i + 1] - b[i + 1]) + Math.abs(a[i + 2] - b[i + 2]);
      const hit = delta > 30;  changed += hit;
      d.data.set(hit ? [181, 69, 47, 255] : [240, 240, 240, 255], i);
    }
    ctx(out).putImageData(d, 0, 0);
    return (changed / (140 * 140) * 100).toFixed(1);
  }
  function run() {
    nestor(ctx('a'), base, false);
    nestor(ctx('b'), base, true);                         // "Add a green knitted scarf. Change nothing else."
    nestor(ctx('c'), randomTraits(), true);               // a rewritten prompt rolls the dice on everything
    const e = diff('a', 'b', 'd1'), r = diff('a', 'c', 'd2');
    document.getElementById('out').textContent = `edit changed ${e}% of the pixels; rewrite changed ${r}%. The edit cost $0.0142.`;
  }
  document.getElementById('reroll').onclick = run;
  run();
</script>