feat: save prompt caption alongside every generated image

After each successful image upload to storage, a sidecar `.caption`
file is uploaded at the same path with `.caption` extension containing
the exact prompt used to generate the image.

Coverage:
- generation/handlers.go: ImageHandler → media/{userID}/images/{jobID}_{i}.caption
- album/handler.go: AnchorHandler → albums/{userID}/{albumID}/anchor.caption
- album/handler.go: ShotHandler → albums/{userID}/{albumID}/shots/{shotIndex}.caption
- personagen/service.go: generatePosition → personas/{specID}/images/{pos:02d}.caption

Caption failures are logged at warn level and never abort the job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-23 02:38:08 -07:00
parent 3979ef2d08
commit 062a828a00
3 changed files with 19 additions and 0 deletions

View File

@ -83,6 +83,10 @@ func AnchorHandler(mg *mediagen.Manager, store storage.Store, pub realtime.Event
logger.Warn("failed to persist anchor to storage, using inline URL", "error", uploadErr, "job_id", job.ID) logger.Warn("failed to persist anchor to storage, using inline URL", "error", uploadErr, "job_id", job.ID)
} else { } else {
anchorURL = url anchorURL = url
captionPath := fmt.Sprintf("albums/%s/%s/anchor.caption", userID, albumID)
if _, captionErr := store.Upload(ctx, captionPath, []byte(subjectDesc), "text/plain"); captionErr != nil {
logger.Warn("failed to persist anchor caption", "error", captionErr, "job_id", job.ID)
}
} }
} }
@ -213,6 +217,10 @@ func ShotHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventPu
"error", uploadErr, "job_id", job.ID) "error", uploadErr, "job_id", job.ID)
} else { } else {
imageURL = url imageURL = url
captionPath := fmt.Sprintf("albums/%s/%s/shots/%d.caption", userID, albumID, shotIndex)
if _, captionErr := store.Upload(ctx, captionPath, []byte(prompt), "text/plain"); captionErr != nil {
logger.Warn("failed to persist shot caption", "error", captionErr, "job_id", job.ID)
}
} }
} }

View File

@ -100,6 +100,10 @@ func ImageHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventP
if uploadErr != nil { if uploadErr != nil {
logger.Warn("failed to persist image to storage", "error", uploadErr, "job_id", job.ID) logger.Warn("failed to persist image to storage", "error", uploadErr, "job_id", job.ID)
} else { } else {
captionPath := fmt.Sprintf("media/%s/images/%s_%d.caption", userID, job.ID, i)
if _, captionErr := store.Upload(ctx, captionPath, []byte(prompt), "text/plain"); captionErr != nil {
logger.Warn("failed to persist image caption", "error", captionErr, "job_id", job.ID)
}
images[i] = GeneratedImage{Data: url, IsURL: true, Seed: resp.Seed} images[i] = GeneratedImage{Data: url, IsURL: true, Seed: resp.Seed}
continue continue
} }

View File

@ -228,6 +228,13 @@ func (s *Service) generatePosition(ctx context.Context, spec *persona.PersonaSpe
return fmt.Errorf("storing position %d: %w", pos, err) return fmt.Errorf("storing position %d: %w", pos, err)
} }
if imgSpec.Prompt != "" {
captionPath := fmt.Sprintf("personas/%s/images/%02d.caption", spec.ID, pos)
if _, captionErr := s.store.Upload(ctx, captionPath, []byte(imgSpec.Prompt), "text/plain"); captionErr != nil {
s.logger.Warn("failed to persist image caption", "error", captionErr, "position", pos)
}
}
imgSpec.URL = url imgSpec.URL = url
imgSpec.Status = persona.ImageStatusComplete imgSpec.Status = persona.ImageStatusComplete
return nil return nil