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:
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.pngOutput
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.
<!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>