# 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

```json
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:

```json
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 complete** — `album_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).

```typescript
// 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

```tsx
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

```tsx
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

```tsx
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.

```bash
# 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>"
```
