From 9d499a89deeb6d9b2050ef0fdb70ba0467ad7aa5 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Fri, 5 Jun 2026 22:40:20 +0330 Subject: [PATCH] =?UTF-8?q?fix(render):=20real=20AE=20render=20=E2=80=94?= =?UTF-8?q?=20pass=20-comp,=20fix=20export=20insert,=20ensure=20exports=20?= =?UTF-8?q?bucket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs surfaced bringing up a real After Effects node (verified: AE 2026 claimed + ran, but produced no usable output): 1. aerender got no -comp/-rqindex → "output argument ignored", nothing rendered. - Claim now returns comp_name from content.projects.render_aep_comp (e.g. "frfinal") via new Store.GetTemplateCompName; threaded through ClaimedJob → runner.Job → aerender args (`-comp `, or `-rqindex 1` fallback when unknown). 2. CreateExportForJob INSERT passed render_quality as a bare param into an enum column → 500 ("output-upload-url HTTP 500"), so completed renders had no export. - Cast $8::render.render_quality (+ explicit casts for file_type/create_type enums). 3. flatrender-exports bucket didn't exist → uploads would fail anyway. - render-svc now MakeBucket(exports, templates) idempotently at startup. Co-Authored-By: Claude Opus 4.8 --- services/node-agent/cmd/agent/main.go | 1 + services/node-agent/internal/client/client.go | 3 +++ services/node-agent/internal/runner/runner.go | 14 ++++++++++--- services/render/cmd/server/main.go | 10 ++++++++++ services/render/internal/db/db.go | 20 +++++++++++++++++-- services/render/internal/handlers/internal.go | 4 ++++ services/render/internal/models/models.go | 4 ++++ 7 files changed, 51 insertions(+), 5 deletions(-) diff --git a/services/node-agent/cmd/agent/main.go b/services/node-agent/cmd/agent/main.go index 32f8e25..f64dfc0 100644 --- a/services/node-agent/cmd/agent/main.go +++ b/services/node-agent/cmd/agent/main.go @@ -433,6 +433,7 @@ func (a *Agent) runJob(ctx context.Context, job *client.ClaimedJob) { HasMusic: job.HasMusic, HasVoiceover: job.HasVoiceover, AEPFilePath: aepPath, + CompName: job.CompName, } onProgress := func(ctx context.Context, pct int, msg string) error { diff --git a/services/node-agent/internal/client/client.go b/services/node-agent/internal/client/client.go index c91ee8c..6a421fc 100644 --- a/services/node-agent/internal/client/client.go +++ b/services/node-agent/internal/client/client.go @@ -159,6 +159,9 @@ type ClaimedJob struct { // BundleMD5 identifies the bundle content; used as a local cache key so repeated // renders of the same template download + extract it only once. BundleMD5 string `json:"bundle_md5,omitempty"` + // CompName is the AE composition to render (-comp), e.g. "frfinal". Empty → the + // node falls back to the project's render queue (-rqindex 1). + CompName string `json:"comp_name,omitempty"` } // OutputUploadURLResponse is returned by GetOutputUploadURL. diff --git a/services/node-agent/internal/runner/runner.go b/services/node-agent/internal/runner/runner.go index 5ae077e..4292ee9 100644 --- a/services/node-agent/internal/runner/runner.go +++ b/services/node-agent/internal/runner/runner.go @@ -32,6 +32,9 @@ type Job struct { // AEPFilePath is the local path to the downloaded .aep project file. // In a full implementation the agent downloads this from MinIO before calling Run. AEPFilePath string + // CompName is the composition to render (-comp), e.g. "frfinal". When empty the + // node renders the project's render queue (-rqindex 1) instead. + CompName string } // Run executes the render job, calling onProgress and onPreview as it advances. @@ -103,11 +106,16 @@ func aeRender(ctx context.Context, aePath string, job *Job, outputPath string, o // aerender flags: // -project + // -comp (or -rqindex 1 when no comp name is known) // -output - args := []string{ - "-project", job.AEPFilePath, - "-output", outputPath, + // Without -comp/-rqindex, aerender ignores -output and renders nothing. + args := []string{"-project", job.AEPFilePath} + if job.CompName != "" { + args = append(args, "-comp", job.CompName) + } else { + args = append(args, "-rqindex", "1") } + args = append(args, "-output", outputPath) log.Printf("[ae] running: %s %v", aePath, args) cmd := exec.CommandContext(ctx, aePath, args...) diff --git a/services/render/cmd/server/main.go b/services/render/cmd/server/main.go index 8e99075..1be9bb4 100644 --- a/services/render/cmd/server/main.go +++ b/services/render/cmd/server/main.go @@ -60,6 +60,16 @@ func main() { if err != nil { log.Fatalf("minio client: %v", err) } + // Ensure the render output bucket exists (node agents PUT exports here). + for _, b := range []string{minioBucket, minioTemplatesBucket} { + if exists, berr := mc.BucketExists(context.Background(), b); berr == nil && !exists { + if merr := mc.MakeBucket(context.Background(), b, minio.MakeBucketOptions{}); merr != nil { + log.Printf("warning: could not create bucket %q: %v", b, merr) + } else { + log.Printf("created bucket %q", b) + } + } + } // ── Store + handlers ────────────────────────────────────────────────────── store := db.NewStore(pool) diff --git a/services/render/internal/db/db.go b/services/render/internal/db/db.go index 796456d..2a8d110 100644 --- a/services/render/internal/db/db.go +++ b/services/render/internal/db/db.go @@ -626,6 +626,22 @@ func (s *Store) ClaimJob(ctx context.Context, nodeID uuid.UUID, region string) ( // The export starts with a placeholder path `exports/{export_id}/output.mp4`. // The node agent uploads the MP4 to that MinIO path, then calls CompleteJob // with the returned export_id. +// GetTemplateCompName returns the After Effects composition to render for a +// template (content.projects.render_aep_comp), e.g. "frfinal". aerender needs +// this via -comp; without it AE opens the project but renders nothing. +func (s *Store) GetTemplateCompName(ctx context.Context, originalProjectID uuid.UUID) (string, error) { + var comp *string + err := s.pool.QueryRow(ctx, + `SELECT render_aep_comp FROM content.projects WHERE id = $1`, originalProjectID).Scan(&comp) + if err != nil { + return "", err + } + if comp == nil { + return "", nil + } + return *comp, nil +} + func (s *Store) CreateExportForJob(ctx context.Context, jobID uuid.UUID) (*models.Export, error) { // Look up the job to get tenant/user/project context job, err := s.getJobByIDInternal(ctx, jobID) @@ -646,8 +662,8 @@ func (s *Store) CreateExportForJob(ctx context.Context, jobID uuid.UUID) (*model delete_notified, created_at) VALUES ($1, $2, $3, $4, $5, - $6, $7, 'mp4', 'video', $8, - 'render', 0, $9, $10, + $6, $7, 'mp4', 'video'::render.export_file_type, $8::render.render_quality, + 'render'::render.export_create_type, 0, $9, $10, false, $9)`, exportID, job.TenantID, job.UserID, job.SavedProjectID, job.OriginalProjectID, job.ID, path, job.Quality, diff --git a/services/render/internal/handlers/internal.go b/services/render/internal/handlers/internal.go index de921d8..0761e4e 100644 --- a/services/render/internal/handlers/internal.go +++ b/services/render/internal/handlers/internal.go @@ -289,6 +289,9 @@ func (h *InternalHandler) Claim(c *gin.Context) { } } + // Composition to render (-comp). Non-fatal: empty → node uses the render queue. + compName, _ := h.store.GetTemplateCompName(c.Request.Context(), job.OriginalProjectID) + c.JSON(http.StatusOK, models.ClaimedJob{ JobID: job.ID, SavedProjectID: job.SavedProjectID, @@ -300,6 +303,7 @@ func (h *InternalHandler) Claim(c *gin.Context) { AEPDownloadURL: aepURL, IsBundle: isBundle, BundleMD5: bundleMD5, + CompName: compName, }) } diff --git a/services/render/internal/models/models.go b/services/render/internal/models/models.go index 5592c8d..3e8c15f 100644 --- a/services/render/internal/models/models.go +++ b/services/render/internal/models/models.go @@ -428,6 +428,10 @@ type ClaimedJob struct { // BundleMD5 is the stored object's ETag/MD5 — the node uses it as a cache key so // repeated renders of the same template download + extract the bundle only once. BundleMD5 string `json:"bundle_md5,omitempty"` + // CompName is the After Effects composition to render (-comp). From the + // template's render_aep_comp (e.g. "frfinal"). Empty → node falls back to the + // project's render queue. + CompName string `json:"comp_name,omitempty"` } // OutputUploadURLResponse is returned by POST .../output-upload-url.