Skip to content

Character System Architecture ​

How the character reference system enables consistent multi-scene generation.

Problem Statement ​

Without character persistence:

Character Persistence Problem

Without persistence: token bloat, context limits, inconsistency

Issues:

  • Repeating character descriptions wastes tokens
  • After 4-6 scenes, context limits are reached
  • Starting new sessions loses character consistency

Solution Architecture ​

Character Reference System Architecture

Save character once, generate unlimited consistent scenes

Character Model ​

Database Schema ​

sql
CREATE TABLE characters (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tenant_id TEXT NOT NULL,
    character_name TEXT NOT NULL,
    base_prompt TEXT NOT NULL,
    description TEXT,
    negative_prompt TEXT,
    system_instruction TEXT,
    preferred_aspect_ratio TEXT,
    reference_image_ids TEXT,  -- JSON array of image IDs
    tags TEXT,  -- JSON array of tags
    times_used INTEGER DEFAULT 0,
    last_used_at TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP,

    UNIQUE(tenant_id, character_name),
    FOREIGN KEY (tenant_id) REFERENCES tenants(tenant_id) ON DELETE CASCADE
);

CREATE INDEX idx_characters_tenant ON characters(tenant_id);
CREATE INDEX idx_characters_name ON characters(character_name);
CREATE INDEX idx_characters_usage ON characters(times_used DESC);

Character Record ​

typescript
interface Character {
  id: number;
  tenant_id: string;
  character_name: string;
  base_prompt: string;           // Appearance description
  description?: string;          // Notes for user
  negative_prompt?: string;      // What to avoid
  system_instruction?: string;   // Style guidance
  preferred_aspect_ratio?: string;
  reference_image_ids?: number[]; // Links to images table
  tags?: string[];               // Organization
  times_used: number;            // Usage tracking
  last_used_at?: string;
  created_at: string;
  updated_at: string;
}

Reference Image System ​

Why Reference Images? ​

Text-only prompts can vary in interpretation:

Text: "young woman with red hair"
→ Could be: short/long hair, light/dark red, any style

With reference images:

Image + Text: "The character in Image 1 — keep face, hair, outfit EXACTLY the same"
→ Consistent visual features

Reference Storage Flow ​

Character Reference Storage Flow

Generate image, store, then create character with reference

Loading References ​

typescript
async function loadCharacterReferences(
  env: Env,
  tenantId: string,
  referenceImageIds: number[]
): Promise<ReferenceImage[]> {
  // 1. Get image metadata from D1
  const placeholders = referenceImageIds.map(() => '?').join(',');
  const images = await env.DB.prepare(`
    SELECT id, r2_key, mime_type
    FROM images
    WHERE id IN (${placeholders}) AND tenant_id = ?
  `).bind(...referenceImageIds, tenantId).all();

  // 2. Download from R2 and convert to base64
  const references: ReferenceImage[] = [];

  for (const img of images.results) {
    const r2Object = await env.R2_IMAGES.get(img.r2_key);
    if (!r2Object) continue;

    const arrayBuffer = await r2Object.arrayBuffer();
    const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));

    references.push({
      data: base64,
      mimeType: img.mime_type,
    });
  }

  return references;
}

Generation Strategies ​

Strategy 1: With Reference Images ​

When character has reference images:

typescript
const prompt = `The character in Image 1 — keep the face, hair, and outfit EXACTLY the same. Character details: ${basePrompt}. Scene: ${scenePrompt}`;

await gemini.generateImage({
  prompt,
  referenceImages: loadedReferences,
  systemInstruction: character.system_instruction,
  negativePrompt: character.negative_prompt,
});

Why this works:

  • Gemini sees the visual reference
  • "The character in Image 1 — keep face, hair, outfit EXACTLY the same" is the identity-lock phrase
  • Scene description adds the new context

Strategy 2: Text-Only ​

When no reference images:

typescript
const prompt = `${character.base_prompt}, ${scenePrompt}`;

await gemini.generateImage({
  prompt,
  systemInstruction: character.system_instruction,
  negativePrompt: character.negative_prompt,
});

Trade-offs:

  • Less consistent than visual references
  • More token usage
  • Still better than repeating full description each time

Multi-Character Scenes ​

The Challenge ​

Multiple characters in one scene requires:

  • Loading multiple reference sets
  • Combining prompts without confusion
  • Positioning characters clearly

Implementation ​

Multi-Character Scene Generation

Load characters, combine references, build prompt, generate

Multi-Character Prompt ​

typescript
function buildMultiCharacterPrompt(
  characters: Character[],
  scenePrompt: string
): string {
  const characterDescriptions = characters
    .map((c, i) => `Character ${i + 1}: ${c.character_name}`)
    .join(', ');

  return `Generate an image showing ${characterDescriptions} together. ` +
         `Scene: ${scenePrompt}. ` +
         `Use the reference images provided to maintain each character's appearance.`;
}

Reference Image Limits ​

ModelMax Reference Images
Gemini Flash3
Gemini Pro14

For multi-character with many refs:

  • Prioritize most recent/best references
  • Limit to 2-3 refs per character
  • Total refs should not exceed model limit

Effective Base Prompts ​

Good Practice ​

✅ Physical features only:
"A young woman with bright red hair in a long braid,
emerald green eyes, freckles across her nose, wearing
a brown leather adventurer's outfit with gold trim"

Avoid ​

❌ Scene elements:
"A young woman running through a forest at sunset"

❌ Emotions/actions:
"A young woman looking scared and running away"

❌ Temporary states:
"A young woman who just woke up with messy hair"

Why? ​

Scene elements in base_prompt conflict with scene_prompt:

base_prompt: "woman in forest at sunset"
scene_prompt: "at the beach"
→ Confused output: forest or beach?

Usage Tracking ​

Track Character Usage ​

sql
-- Increment on each generation
UPDATE characters
SET
    times_used = times_used + 1,
    last_used_at = datetime('now')
WHERE id = ? AND tenant_id = ?;

Analytics Queries ​

sql
-- Most used characters
SELECT character_name, times_used, last_used_at
FROM characters
WHERE tenant_id = ?
ORDER BY times_used DESC
LIMIT 10;

-- Recently used
SELECT character_name, last_used_at
FROM characters
WHERE tenant_id = ?
ORDER BY last_used_at DESC
LIMIT 5;

Image Attribution ​

sql
-- Images generated with a character
SELECT id, prompt, thumbnail_url, created_at
FROM images
WHERE character_id = ? AND tenant_id = ?
ORDER BY created_at DESC;

Character Lifecycle ​

Character Lifecycle States

Create, use, update, delete - images are preserved

Best Practices ​

1. Start with Reference Images ​

Generate 2-3 initial images, pick the best, then create character with those as references.

2. Use Consistent Style Presets ​

Combine characters with style presets for brand consistency:

typescript
{
  character_name: "Luna",
  style_preset_name: "Fantasy Illustration"
}

3. Organize with Tags ​

typescript
{
  character_name: "Luna",
  tags: ["fantasy", "protagonist", "book-cover-project"]
}

4. Test Across Different Scenes ​

After creating, test in various contexts:

  • Indoor vs outdoor
  • Different lighting
  • Various poses/actions
  • Multiple aspect ratios

Next Steps ​

Released under the MIT License.