232 lines
7.2 KiB
Markdown
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>"
|
|
```
|