fix(render): real AE render — pass -comp, fix export insert, ensure exports bucket
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 <name>`, 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 <noreply@anthropic.com>
This commit is contained in:
@@ -433,6 +433,7 @@ func (a *Agent) runJob(ctx context.Context, job *client.ClaimedJob) {
|
|||||||
HasMusic: job.HasMusic,
|
HasMusic: job.HasMusic,
|
||||||
HasVoiceover: job.HasVoiceover,
|
HasVoiceover: job.HasVoiceover,
|
||||||
AEPFilePath: aepPath,
|
AEPFilePath: aepPath,
|
||||||
|
CompName: job.CompName,
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress := func(ctx context.Context, pct int, msg string) error {
|
onProgress := func(ctx context.Context, pct int, msg string) error {
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ type ClaimedJob struct {
|
|||||||
// BundleMD5 identifies the bundle content; used as a local cache key so repeated
|
// BundleMD5 identifies the bundle content; used as a local cache key so repeated
|
||||||
// renders of the same template download + extract it only once.
|
// renders of the same template download + extract it only once.
|
||||||
BundleMD5 string `json:"bundle_md5,omitempty"`
|
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.
|
// OutputUploadURLResponse is returned by GetOutputUploadURL.
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ type Job struct {
|
|||||||
// AEPFilePath is the local path to the downloaded .aep project file.
|
// AEPFilePath is the local path to the downloaded .aep project file.
|
||||||
// In a full implementation the agent downloads this from MinIO before calling Run.
|
// In a full implementation the agent downloads this from MinIO before calling Run.
|
||||||
AEPFilePath string
|
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.
|
// 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:
|
// aerender flags:
|
||||||
// -project <path.aep>
|
// -project <path.aep>
|
||||||
|
// -comp <name> (or -rqindex 1 when no comp name is known)
|
||||||
// -output <output.mp4>
|
// -output <output.mp4>
|
||||||
args := []string{
|
// Without -comp/-rqindex, aerender ignores -output and renders nothing.
|
||||||
"-project", job.AEPFilePath,
|
args := []string{"-project", job.AEPFilePath}
|
||||||
"-output", outputPath,
|
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)
|
log.Printf("[ae] running: %s %v", aePath, args)
|
||||||
cmd := exec.CommandContext(ctx, aePath, args...)
|
cmd := exec.CommandContext(ctx, aePath, args...)
|
||||||
|
|||||||
@@ -60,6 +60,16 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("minio client: %v", err)
|
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 + handlers ──────────────────────────────────────────────────────
|
||||||
store := db.NewStore(pool)
|
store := db.NewStore(pool)
|
||||||
|
|||||||
@@ -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 export starts with a placeholder path `exports/{export_id}/output.mp4`.
|
||||||
// The node agent uploads the MP4 to that MinIO path, then calls CompleteJob
|
// The node agent uploads the MP4 to that MinIO path, then calls CompleteJob
|
||||||
// with the returned export_id.
|
// 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) {
|
func (s *Store) CreateExportForJob(ctx context.Context, jobID uuid.UUID) (*models.Export, error) {
|
||||||
// Look up the job to get tenant/user/project context
|
// Look up the job to get tenant/user/project context
|
||||||
job, err := s.getJobByIDInternal(ctx, jobID)
|
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)
|
delete_notified, created_at)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5,
|
($1, $2, $3, $4, $5,
|
||||||
$6, $7, 'mp4', 'video', $8,
|
$6, $7, 'mp4', 'video'::render.export_file_type, $8::render.render_quality,
|
||||||
'render', 0, $9, $10,
|
'render'::render.export_create_type, 0, $9, $10,
|
||||||
false, $9)`,
|
false, $9)`,
|
||||||
exportID, job.TenantID, job.UserID, job.SavedProjectID, job.OriginalProjectID,
|
exportID, job.TenantID, job.UserID, job.SavedProjectID, job.OriginalProjectID,
|
||||||
job.ID, path, job.Quality,
|
job.ID, path, job.Quality,
|
||||||
|
|||||||
@@ -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{
|
c.JSON(http.StatusOK, models.ClaimedJob{
|
||||||
JobID: job.ID,
|
JobID: job.ID,
|
||||||
SavedProjectID: job.SavedProjectID,
|
SavedProjectID: job.SavedProjectID,
|
||||||
@@ -300,6 +303,7 @@ func (h *InternalHandler) Claim(c *gin.Context) {
|
|||||||
AEPDownloadURL: aepURL,
|
AEPDownloadURL: aepURL,
|
||||||
IsBundle: isBundle,
|
IsBundle: isBundle,
|
||||||
BundleMD5: bundleMD5,
|
BundleMD5: bundleMD5,
|
||||||
|
CompName: compName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -428,6 +428,10 @@ type ClaimedJob struct {
|
|||||||
// BundleMD5 is the stored object's ETag/MD5 — the node uses it as a cache key so
|
// 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.
|
// repeated renders of the same template download + extract the bundle only once.
|
||||||
BundleMD5 string `json:"bundle_md5,omitempty"`
|
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.
|
// OutputUploadURLResponse is returned by POST .../output-upload-url.
|
||||||
|
|||||||
Reference in New Issue
Block a user