# 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:` 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 Component ```tsx import { AlbumGrid } from '@example-project/ui'; regenerateShot(index)} onResetShot={(index) => resetShot(index)} onImageClick={(indexOrAnchor) => openLightbox(indexOrAnchor)} /> ``` ### Individual Components ```tsx import { AnchorPreview, ShotCard } from '@example-project/ui'; // Anchor card // Individual shot card 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 " \ -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//anchor \ -H "Authorization: Bearer " # Watch SSE for album_anchor_complete curl -N "http://localhost:8001/api/example-api/events?channel=user:" \ -H "Authorization: Bearer " ```