Consistent Characters

Prompting for Consistent Characters and Objects

Text alone draws a different bird each time. For a recurring character, pass the approved image as a reference and say "the same bird, same scarf and proportions," as the second command did: Nestor kept his shape, colors and scarf in a new scene. Keep approved references in a folder and reuse the same prompt parts.

nestor.mjs (Flare, $0.0064), the scarf edit and the kitchen scene from the reference (Sunburst, $0.0142 and $0.0143), all low quality
nestor.mjs (Flare, $0.0064), the scarf edit and the kitchen scene from the reference (Sunburst, $0.0142 and $0.0143), all low quality
Text alone draws a different bird each time; a reference image keeps Nestor the same across scenesHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
</style>
<p><button id="again">Generate four more of each</button></p>
<canvas id="c" width="600" height="300"></canvas>
<script>
  const ctx = document.getElementById('c').getContext('2d');
  const scenes = [['#fff3e0', 'studio'], ['#e3efe0', 'garden'], ['#f6e6d0', 'kitchen'], ['#dde8f2', 'library']];
  function bird(x, y, t, scene) {
    ctx.fillStyle = scene[0];  ctx.fillRect(x, y, 140, 120);
    ctx.fillStyle = '#8d6e63';                             // a hint of the scene
    if (scene[1] === 'kitchen') { ctx.fillRect(x + 90, y + 20, 12, 22);  ctx.fillRect(x + 108, y + 26, 12, 16); }
    if (scene[1] === 'library') ['#1f5f8b', '#b5452f', '#3f7d3a'].forEach((c, i) => { ctx.fillStyle = c; ctx.fillRect(x + 96 + i * 12, y + 14, 9, 34); });
    if (scene[1] === 'garden') { ctx.fillStyle = '#3f7d3a'; ctx.beginPath(); ctx.arc(x + 112, y + 34, 16, 0, 7); ctx.fill(); }
    ctx.save();  ctx.translate(x + 60, y + 66);
    ctx.fillStyle = t.body;  ctx.beginPath();  ctx.ellipse(0, 0, 28 * t.w, 30 * t.h, 0, 0, 7);  ctx.fill();
    ctx.fillStyle = t.belly;  ctx.beginPath();  ctx.ellipse(4, 8, 16 * t.w, 18 * t.h, 0, 0, 7);  ctx.fill();
    ctx.fillStyle = '#263238';  ctx.beginPath();  ctx.arc(10, -12, 3.5, 0, 7);  ctx.fill();
    ctx.fillStyle = t.beak;  ctx.beginPath();  ctx.moveTo(26 * t.w, -6);  ctx.lineTo(38 * t.w, -2);  ctx.lineTo(26 * t.w, 2);  ctx.fill();
    if (t.scarf) { ctx.fillStyle = '#3f7d3a';  ctx.fillRect(-20, -4, 40, 7);  ctx.fillRect(-14, 0, 7, 16); }
    if (t.crest) { ctx.fillStyle = t.body;  ctx.beginPath();  ctx.moveTo(-4, -28);  ctx.lineTo(4, -44);  ctx.lineTo(10, -26);  ctx.fill(); }
    ctx.restore();
  }
  const pick = a => a[Math.floor(Math.random() * a.length)];
  const randomBird = () => ({ body: pick(['#1f5f8b', '#2a9d8f', '#5b3f99', '#3a78b0']), belly: pick(['#e09a10', '#f2cc8f', '#fff3e0']),
    beak: pick(['#e09a10', '#b5452f', '#263238']), w: 0.8 + Math.random() * 0.4, h: 0.85 + Math.random() * 0.3,
    scarf: Math.random() < 0.5, crest: Math.random() < 0.4 });
  const nestor = { body: '#1f5f8b', belly: '#e09a10', beak: '#e09a10', w: 1, h: 1, scarf: true, crest: false };
  function draw() {
    ctx.clearRect(0, 0, 600, 300);
    ctx.fillStyle = '#263238';  ctx.font = 'bold 12px system-ui';
    ctx.fillText('text only: "a small round bluebird with a scarf" × 4', 10, 14);
    ctx.fillText('with the approved image as reference: "the same bird, same scarf and proportions"', 10, 160);
    scenes.forEach((scene, i) => {
      bird(10 + i * 148, 22, randomBird(), scenes[0]);      // a different bird every time
      bird(10 + i * 148, 168, nestor, scene);               // one character, four scenes
    });
  }
  document.getElementById('again').onclick = draw;
  draw();
</script>