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
- Generate anchor first — POST
/albums/{id}/anchor - Wait for
album_anchor_completeSSE — anchor URL arrives - Generate shots — POST
/albums/{id}/shots(returns 422 if no anchor) - Shots complete —
album_shot_completeevents 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,ShotTemplatetypesAlbumUpdaterinterface (minimal interface for job handlers)AnchorHandler(mg, store, pub, updater, logger)— job handler forgenerate_anchorShotHandler(mg, store, pub, updater, logger)— job handler forgenerate_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:
- Anchor is generated and stored at
albums/{userId}/{albumId}/anchor.png - The anchor URL is stored in the album record
- Shot jobs carry
anchorUrlin their payload - The
ShotHandlerfetches anchor bytes at execution time via HTTP - Bytes are passed as
ReferenceImageto 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>"