persona-community-3/.claude/guides/album.md
jordan f53b908499
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-23 11:10:35 +00:00

7.2 KiB

Album Generation Guide

Albums are the right abstraction for generating multiple images of the same subject with visual consistency. One anchor image + N directed shots.

Mental Model: Photography Session

Term Meaning Example
Subject What's being photographed "Woman, dark curly hair, early 30s, artistic style"
Anchor The reference image that ties all shots together Generated from subject description
Shot One image with a specific direction "Headshot, direct eye contact, studio lighting"
Album The full session: subject + anchor + shots "Jordan Headshots"

Use Cases

The album abstraction covers persona headshots, product photography, character sheets, and brand assets — same mechanism, different subject descriptions.

Personas:   subject="Woman, dark hair, 30s" + shots=[Headshot, Casual, Professional]
Products:   subject="Titanium water bottle, brushed finish" + shots=[Hero, Lifestyle, Detail]
Characters: subject="Cartoon raccoon mascot, mischievous" + shots=[Neutral, Expression, Action]

API Endpoints

All generation endpoints return 202 Accepted with a job ID. Results arrive via SSE.

POST   /api/example-api/albums                   Create album with shots
GET    /api/example-api/albums                   List user's albums
GET    /api/example-api/albums/{id}              Get album with shot statuses
DELETE /api/example-api/albums/{id}              Delete album

POST   /api/example-api/albums/{id}/anchor       Enqueue anchor generation → {jobId}
POST   /api/example-api/albums/{id}/shots        Enqueue all pending shots → {jobIds:[...]}
POST   /api/example-api/albums/{id}/shots/{i}    Regenerate one shot → {jobId}
DELETE /api/example-api/albums/{id}/shots/{i}    Reset shot to pending

Create Album

POST /albums
{
  "name": "Jordan Headshots",
  "subjectDesc": "Professional woman, dark curly hair, early 30s, warm smile",
  "shots": [
    {"label": "Headshot", "direction": "close-up, direct eye contact, studio lighting"},
    {"label": "Casual", "direction": "relaxed smile, natural light, outdoor setting"}
  ]
}

Or use a built-in template set:

POST /albums
{
  "name": "Jordan Headshots",
  "subjectDesc": "Professional woman, dark curly hair, early 30s, warm smile",
  "templateSet": "portrait"
}

Available template sets: portrait (6 shots), product (4 shots), character (4 shots).

Generation Order

  1. Generate anchor first — POST /albums/{id}/anchor
  2. Wait for album_anchor_complete SSE — anchor URL arrives
  3. Generate shots — POST /albums/{id}/shots (returns 422 if no anchor)
  4. Shots completealbum_shot_complete events arrive per shot

Enforcement: The service returns 422 Unprocessable Entity if shots are requested before the anchor exists. The frontend disables "Generate All Shots" until the anchor is ready.

SSE Events

All events arrive on the user:<userId> channel (existing subscription).

// Anchor events
{ type: "album_anchor_complete", result: { albumId, anchorUrl } }
{ type: "album_anchor_failed",   result: { albumId, error } }

// Shot events
{ type: "album_shot_generating", result: { albumId, shotIndex } }
{ type: "album_shot_complete",   result: { albumId, shotIndex, imageUrl } }
{ type: "album_shot_failed",     result: { albumId, shotIndex, error } }

Frontend Usage

useAlbumGeneration Hook

import { useAlbumGeneration } from '@example-project/realtime';

function AlbumPage({ albumId }: { albumId: string }) {
  const { user, token } = useAuth();

  const {
    album,
    isLoading,
    error,
    loadAlbum,
    generateAnchor,
    generateAllShots,
    regenerateShot,
    resetShot,
  } = useAlbumGeneration({
    apiBase: '/api/example-api',
    userId: user.id,
    albumId,
    authToken: token,
  });

  // Load on mount
  useEffect(() => { void loadAlbum(); }, [loadAlbum]);

  return (
    <AlbumGrid
      name={album?.name}
      anchorUrl={album?.anchorUrl}
      shots={album?.shots ?? []}
      onGenerateAnchor={generateAnchor}
      onGenerateAllShots={generateAllShots}
      onRegenerateShot={regenerateShot}
      onResetShot={resetShot}
    />
  );
}

AlbumGrid Component

import { AlbumGrid } from '@example-project/ui';

<AlbumGrid
  name="Jordan Headshots"
  anchorUrl={album.anchorUrl}
  anchorGenerating={isAnchorGenerating}
  shots={album.shots}
  onGenerateAnchor={generateAnchor}
  onRegenerateAnchor={generateAnchor}
  onGenerateAllShots={generateAllShots}
  onRegenerateShot={(index) => regenerateShot(index)}
  onResetShot={(index) => resetShot(index)}
  onImageClick={(indexOrAnchor) => openLightbox(indexOrAnchor)}
/>

Individual Components

import { AnchorPreview, ShotCard } from '@example-project/ui';

// Anchor card
<AnchorPreview
  anchorUrl={album.anchorUrl}
  isGenerating={!!album.anchorJobId && !album.anchorUrl}
  onGenerate={generateAnchor}
  onRegenerate={generateAnchor}
/>

// Individual shot card
<ShotCard
  label="Headshot"
  status={shot.status}   // 'pending' | 'generating' | 'complete' | 'failed'
  imageUrl={shot.imageUrl}
  error={shot.error}
  anchorReady={!!album.anchorUrl}
  onGenerate={() => regenerateShot(shot.index)}
  onRegenerate={() => regenerateShot(shot.index)}
  onReset={() => resetShot(shot.index)}
/>

Backend Architecture

Skeleton Package (pkg/album/)

Ships in every project. Provides:

  • Album, Shot, ShotStatus, ShotTemplate types
  • AlbumUpdater interface (minimal interface for job handlers)
  • AnchorHandler(mg, store, pub, updater, logger) — job handler for generate_anchor
  • ShotHandler(mg, store, pub, updater, logger) — job handler for generate_shot
  • Built-in template sets: PortraitSession, ProductShoot, CharacterSheet

Job Handler Architecture

Anchor bytes are NOT stored in the job payload (megabytes in DB = bad). Instead:

  1. Anchor is generated and stored at albums/{userId}/{albumId}/anchor.png
  2. The anchor URL is stored in the album record
  3. Shot jobs carry anchorUrl in their payload
  4. The ShotHandler fetches anchor bytes at execution time via HTTP
  5. Bytes are passed as ReferenceImage to the mediagen provider

Component Service Layer

port.AlbumRepository     — CRUD + AlbumUpdater
memory.AlbumRepository   — in-memory (standalone dev mode)
postgres.AlbumRepository — postgres (production, not yet implemented)
service.AlbumService     — business logic (create, list, get, generateAnchor, generateShots)
handlers.Album           — HTTP handlers

Dev Mode

In standalone mode (DATABASE_URL not set), albums persist in memory until the server restarts.

# Start the backend
go run ./services/example-api/cmd/server

# Create an album
curl -X POST http://localhost:8001/api/example-api/albums \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Test","subjectDesc":"A cat","templateSet":"portrait"}'

# Generate anchor
curl -X POST http://localhost:8001/api/example-api/albums/<id>/anchor \
  -H "Authorization: Bearer <token>"

# Watch SSE for album_anchor_complete
curl -N "http://localhost:8001/api/example-api/events?channel=user:<userId>" \
  -H "Authorization: Bearer <token>"