feat(render): node-agent AE snapshot runner (Epic C2) + colour render-binding (Epic B)
Build backend images / build content-svc (push) Failing after 13s
Build backend images / build file-svc (push) Failing after 53s
Build backend images / build gateway (push) Failing after 1m22s
Build backend images / build identity-svc (push) Failing after 19s
Build backend images / build notification-svc (push) Failing after 21s
Build backend images / build render-svc (push) Failing after 20s
Build backend images / build studio-svc (push) Failing after 1m6s
Build backend images / build content-svc (push) Failing after 13s
Build backend images / build file-svc (push) Failing after 53s
Build backend images / build gateway (push) Failing after 1m22s
Build backend images / build identity-svc (push) Failing after 19s
Build backend images / build notification-svc (push) Failing after 21s
Build backend images / build render-svc (push) Failing after 20s
Build backend images / build studio-svc (push) Failing after 1m6s
C2 — real-AE scene snapshots on the node:
- node-agent: runner/snapshot.go RunSnapshot (aerender -comp <key> -s f -e f
→ findRenderedOutput → ffmpeg -frames:v 1 PNG); client ClaimSnapshot /
GetSnapshotUploadURL / ReportSnapshotResult / ReportSnapshotFail; snapshotLoop +
pollSnapshotOnce mirroring the scan loop (reuses the AE-exclusive lock).
- render-svc: GetSnapshotJobMeta + UploadURL handler presigns a PUT to the
public-read user-uploads bucket at snapshots/{project}/{scene}.png and returns a
permanent public_url (not the time-limited export presign); MINIO_UPLOAD_BUCKET +
MINIO_PUBLIC_URL config + compose env + /snapshot/:id/upload-url route.
Epic B — bind edited colours into the render:
- render-svc GetRenderBindings UNIONs studio.saved_shared_colors +
saved_scene_colors (type 'color') so the node writes them before render.
- node-agent binder.go routes type:"color" bindings into the bind-spec colors[]
array that bind.jsx already applies to the frshare colour layers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -140,11 +140,12 @@ func main() {
|
||||
|
||||
// Main loops
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(4)
|
||||
wg.Add(5)
|
||||
go func() { defer wg.Done(); agent.heartbeatLoop(ctx) }()
|
||||
go func() { defer wg.Done(); agent.pollLoop(ctx) }()
|
||||
go func() { defer wg.Done(); agent.fontSyncLoop(ctx) }()
|
||||
go func() { defer wg.Done(); agent.scanLoop(ctx) }()
|
||||
go func() { defer wg.Done(); agent.snapshotLoop(ctx) }()
|
||||
wg.Wait()
|
||||
log.Printf("shutdown complete")
|
||||
}
|
||||
@@ -307,6 +308,91 @@ func (a *Agent) failScan(id, reason string) {
|
||||
_ = a.orch.ReportScanFail(ctx, id, reason)
|
||||
}
|
||||
|
||||
// ── Snapshot loop ─────────────────────────────────────────────────────────────
|
||||
// Claims per-scene snapshot jobs, renders a single frame with AE, extracts a PNG
|
||||
// still and uploads it. Requires the AE app — skipped without AE_PATH.
|
||||
|
||||
func (a *Agent) snapshotLoop(ctx context.Context) {
|
||||
interval := time.Duration(a.cfg.PollIntervalSec) * time.Second
|
||||
if interval < 5*time.Second {
|
||||
interval = 5 * time.Second
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
a.pollSnapshotOnce(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) pollSnapshotOnce(ctx context.Context) {
|
||||
if a.cfg.AEPath == "" {
|
||||
return // snapshots need the real AE app
|
||||
}
|
||||
if a.isBusy() {
|
||||
return // don't contend with a render/scan for AE
|
||||
}
|
||||
|
||||
claim, err := a.orch.ClaimSnapshot(ctx, a.cfg.NodeID, a.cfg.Region)
|
||||
if err != nil {
|
||||
log.Printf("snapshot claim error: %v", err)
|
||||
return
|
||||
}
|
||||
if claim == nil {
|
||||
return // nothing queued
|
||||
}
|
||||
|
||||
a.setScanning(true) // reuse the scan-busy flag (same AE-exclusive lock)
|
||||
defer a.setScanning(false)
|
||||
log.Printf("[snapshot %s] claimed (scene %s)", claim.SnapshotJobID, claim.SceneKey)
|
||||
|
||||
prepCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
|
||||
aepPath, perr := runner.PrepareTemplate(prepCtx, claim.AEPDownloadURL, claim.IsBundle, claim.BundleMD5, a.cfg.WorkDir, "snap-"+claim.SnapshotJobID)
|
||||
cancel()
|
||||
if perr != nil {
|
||||
a.failSnapshot(claim.SnapshotJobID, "prepare template: "+perr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
workDir := filepath.Join(a.cfg.WorkDir, "snapshots", claim.SnapshotJobID)
|
||||
renderCtx, cancel2 := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel2()
|
||||
pngPath, rerr := runner.RunSnapshot(renderCtx, a.cfg.AEPath, aepPath, claim.CompName, claim.Frame, workDir)
|
||||
if rerr != nil {
|
||||
a.failSnapshot(claim.SnapshotJobID, rerr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Upload the still to the public bucket, then report its permanent URL.
|
||||
upCtx, cancel3 := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel3()
|
||||
up, uerr := a.orch.GetSnapshotUploadURL(upCtx, claim.SnapshotJobID)
|
||||
if uerr != nil {
|
||||
a.failSnapshot(claim.SnapshotJobID, "upload-url: "+uerr.Error())
|
||||
return
|
||||
}
|
||||
if _, uperr := runner.UploadFile(upCtx, up.UploadURL, pngPath); uperr != nil {
|
||||
a.failSnapshot(claim.SnapshotJobID, "upload: "+uperr.Error())
|
||||
return
|
||||
}
|
||||
if rperr := a.orch.ReportSnapshotResult(upCtx, claim.SnapshotJobID, up.PublicURL); rperr != nil {
|
||||
log.Printf("[snapshot %s] report result error: %v", claim.SnapshotJobID, rperr)
|
||||
return
|
||||
}
|
||||
log.Printf("[snapshot %s] done → %s", claim.SnapshotJobID, up.PublicURL)
|
||||
}
|
||||
|
||||
func (a *Agent) failSnapshot(id, reason string) {
|
||||
log.Printf("[snapshot %s] failed: %s", id, reason)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_ = a.orch.ReportSnapshotFail(ctx, id, reason)
|
||||
}
|
||||
|
||||
// ── Heartbeat loop ────────────────────────────────────────────────────────────
|
||||
|
||||
func (a *Agent) heartbeatLoop(ctx context.Context) {
|
||||
|
||||
Reference in New Issue
Block a user