persona-community-1/.claude/guides/album.md
jordan 4004f88f4a
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-23 10:20:59 +00:00

232 lines
7.2 KiB
Markdown

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