feat(snapshots): AE scene-snapshot pipeline + admin trigger (Epic C, C1)
Build backend images / build content-svc (push) Failing after 31s
Build backend images / build file-svc (push) Failing after 30s
Build backend images / build gateway (push) Failing after 30s
Build backend images / build identity-svc (push) Failing after 30s
Build backend images / build notification-svc (push) Failing after 31s
Build backend images / build render-svc (push) Failing after 31s
Build backend images / build studio-svc (push) Failing after 31s
Build backend images / build content-svc (push) Failing after 31s
Build backend images / build file-svc (push) Failing after 30s
Build backend images / build gateway (push) Failing after 30s
Build backend images / build identity-svc (push) Failing after 30s
Build backend images / build notification-svc (push) Failing after 31s
Build backend images / build render-svc (push) Failing after 31s
Build backend images / build studio-svc (push) Failing after 31s
Per-scene preview thumbnails for templates. Admin clicks "ساخت پیشنمایش صحنهها" → one single-frame AE render per scene → content.scenes.snapshot_url → shown as a thumbnail in the admin scene list (and available to the studio). - migration 30_render_snapshot_jobs.sql: render.snapshot_jobs (queued|running| done|error, per scene, image_url). - render-svc: db/snapshotjobs.go (EnqueueSceneSnapshots, List, Claim, SetResult -> writes content.scenes.snapshot_url cross-schema, SetError); handlers/ snapshotjobs.go (admin POST/GET /v1/scene-snapshots/:project_id + node-facing internal claim/result/fail); main.go routes; gateway route. - devworker: RunSnapshots — fulfils snapshot jobs with a generated placeholder PNG (data: URL, scene-key-tinted) so the flow is verifiable without an AE node. Gated by RENDER_DEV_SNAPSHOTS (default off; never hijacks real render jobs). - admin UI: ProjectScenes "generate snapshots" button (enqueue + poll + reload) and a thumbnail (snapshot_url || image) per scene row. Verified e2e via the dev mock: enqueue -> jobs run -> content.scenes.snapshot_url populated -> scenes API returns it -> admin renders the thumbnail. Remaining (C2): node-agent real-AE runner — claim snapshot, aerender -s0 -e0 -> ffmpeg still -> upload to a PERMANENT URL (mirror file-svc, not the time-limited export presign) -> post result. Needs a live AE node to build + verify. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,62 @@ func (w *Worker) simulate(ctx context.Context, jobID uuid.UUID) {
|
||||
log.Printf("[devworker] job %s done", jobID)
|
||||
}
|
||||
|
||||
// ── Snapshot mock ─────────────────────────────────────────────────────────────
|
||||
// devSnapshotNode is the synthetic node id the mock records on claimed snapshots.
|
||||
var devSnapshotNode = uuid.MustParse("00000000-0000-0000-0000-0000000000aa")
|
||||
|
||||
// RunSnapshots fulfils queued scene-snapshot jobs with a generated placeholder
|
||||
// image (no AE) so the snapshot flow is exercisable in development. Gated by its
|
||||
// own flag so it never touches real render jobs. Production uses real nodes.
|
||||
func (w *Worker) RunSnapshots(ctx context.Context) {
|
||||
log.Printf("[devworker] snapshot mock started (poll %s) — NOT for production", w.interval)
|
||||
ticker := time.NewTicker(w.interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.snapTick(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) snapTick(ctx context.Context) {
|
||||
claim, err := w.store.ClaimSnapshotJob(ctx, devSnapshotNode)
|
||||
if err != nil {
|
||||
log.Printf("[devworker] snapshot claim error: %v", err)
|
||||
return
|
||||
}
|
||||
if claim == nil {
|
||||
return // queue empty
|
||||
}
|
||||
if err := w.store.SetSnapshotResult(ctx, claim.ID, snapshotPlaceholder(claim.SceneKey)); err != nil {
|
||||
log.Printf("[devworker] snapshot %s result failed: %v", claim.ID, err)
|
||||
_ = w.store.SetSnapshotError(ctx, claim.ID, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("[devworker] snapshot %s (scene %s) done", claim.ID, claim.SceneKey)
|
||||
}
|
||||
|
||||
// snapshotPlaceholder builds a 480×270 PNG card tinted by the scene key with a
|
||||
// little "play" block, returned as a data: URL so the dev path needs no storage.
|
||||
func snapshotPlaceholder(sceneKey string) string {
|
||||
const w, h = 480, 270
|
||||
var sum uint32
|
||||
for _, r := range sceneKey {
|
||||
sum = sum*31 + uint32(r)
|
||||
}
|
||||
base := color.RGBA{uint8(40 + sum%120), uint8(40 + (sum/120)%120), uint8(80 + (sum/7)%150), 255}
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{base}, image.Point{}, draw.Src)
|
||||
draw.Draw(img, image.Rect(0, h/2-30, w, h/2+30), &image.Uniform{color.RGBA{0, 0, 0, 60}}, image.Point{}, draw.Over)
|
||||
draw.Draw(img, image.Rect(w/2-18, h/2-18, w/2+18, h/2+18), &image.Uniform{color.RGBA{255, 255, 255, 230}}, image.Point{}, draw.Over)
|
||||
var buf bytes.Buffer
|
||||
_ = png.Encode(&buf, img)
|
||||
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
}
|
||||
|
||||
// previewB64 builds a 320×180 PNG with a progress bar — same idea as the node
|
||||
// agent's GeneratePreviewB64, kept local so render-svc has no node-agent dep.
|
||||
func previewB64(pct int) string {
|
||||
|
||||
Reference in New Issue
Block a user