From b95f57b545c4a35182423d49c768b599027527bc Mon Sep 17 00:00:00 2001 From: Vladyslav Hrytsenko Date: Wed, 28 Oct 2026 15:58:03 +0200 Subject: [PATCH] feat: send settings.multiFrame on job submit Vendor the current Nexrender API spec, which adds settings.multiFrame to job, nested job and join job settings, and regenerate the client. Before this, job submit parsed the file into a struct without the field and silently dropped it. The spec sync also picks up updated descriptions for asset and template sources and S3 endpoints, and maxLength on two fields. Co-Authored-By: Claude Opus 5.5 --- internal/api/api.gen.go | 21 +++++++---- internal/validate/job_test.go | 43 ++++++++++++++++++++++- openapi/openapi.json | 20 +++++++++-- skills/nexrender/references/operations.md | 1 + 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index b674671..a1b26fd 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -428,7 +428,7 @@ type JobAsset struct { // Params Optional parameters for function assets. For supported NX layer functions (`nx:layer-autoscale`, `nx:layer-duration-set`, `nx:layer-start-set`, `nx:layer-state-set`, `nx:solid-color-set`, `nx:text-params-set`), `params.layerName` accepts a single string or an array of strings. When an array is provided, the API expands the function asset into multiple per-layer assets while preserving the original `assets` order. Params *JobAsset_Params `json:"params,omitempty"` - // Src Source URL, script data or file path for the asset (supports HTTP/HTTPS URLs or local paths) + // Src Public HTTP(S), native s3:/gs:, inline data:, or a file: URL inside the current job work directory. The worker rejects local paths and symlinks escaping that directory. Other schemes and non-public network destinations are rejected. Src *string `json:"src,omitempty"` // Type Asset type (e.g., image, video, audio, text, data, script, job) @@ -476,6 +476,9 @@ type JobAssetNested struct { // Frames Frame specification for image output Frames *JobAssetNested_Settings_Frames `json:"frames,omitempty"` + // MultiFrame Turn After Effects Multi-Frame Rendering on (true) or off (false) for the child job. If omitted, the worker's own setting applies + MultiFrame *bool `json:"multiFrame,omitempty"` + // Quality Render quality Quality *JobAssetNestedSettingsQuality `json:"quality,omitempty"` @@ -553,6 +556,9 @@ type JobCreation struct { // Frames Frame specification - single frame number or [start, end] range. Required for image type. Frames *JobCreation_Settings_Frames `json:"frames,omitempty"` + // MultiFrame Turn After Effects Multi-Frame Rendering on (true) or off (false) for this job. If omitted, the worker's own setting applies + MultiFrame *bool `json:"multiFrame,omitempty"` + // Quality Render quality setting - 'draft' for faster low-quality preview, 'full' for high-quality output Quality *JobCreationSettingsQuality `json:"quality,omitempty"` @@ -582,7 +588,7 @@ type JobCreation struct { // Bucket Target bucket name for file uploads (required) Bucket string `json:"bucket"` - // Endpoint Custom S3-compatible endpoint URL + // Endpoint Public HTTP(S) S3-compatible endpoint URL. Explicit customer credentials are required; worker credentials are not used for arbitrary endpoints. Endpoint *string `json:"endpoint,omitempty"` // Region AWS region or storage region identifier (required) @@ -670,7 +676,7 @@ type JobTemplate struct { // Id Unique template identifier in ULID format (26 characters, uppercase alphanumeric) Id string `json:"id"` - // Src Source URL to the After Effects template (.aep, .mogrt, or .zip file) + // Src Public HTTP(S), native s3:/gs:, inline data:, or a file: URL inside the current job work directory for an After Effects template (.aep, .mogrt, or .zip file). The worker rejects paths and symlinks escaping that directory and non-public network destinations. Src *string `json:"src,omitempty"` AdditionalProperties map[string]interface{} `json:"-"` } @@ -709,9 +715,12 @@ type JoinJobAsset struct { Codec *string `json:"codec,omitempty"` // Engine After Effects render engine version for the child job - Engine *JoinJobAssetSettingsEngine `json:"engine,omitempty"` - Frames *JoinJobAsset_Settings_Frames `json:"frames,omitempty"` - Quality *JoinJobAssetSettingsQuality `json:"quality,omitempty"` + Engine *JoinJobAssetSettingsEngine `json:"engine,omitempty"` + Frames *JoinJobAsset_Settings_Frames `json:"frames,omitempty"` + + // MultiFrame Turn After Effects Multi-Frame Rendering on (true) or off (false) for the child job. If omitted, the worker's own setting applies + MultiFrame *bool `json:"multiFrame,omitempty"` + Quality *JoinJobAssetSettingsQuality `json:"quality,omitempty"` } `json:"settings,omitempty"` // Template Template configuration for the child job diff --git a/internal/validate/job_test.go b/internal/validate/job_test.go index aaab508..feaf70d 100644 --- a/internal/validate/job_test.go +++ b/internal/validate/job_test.go @@ -1,6 +1,10 @@ package validate -import "testing" +import ( + "encoding/json" + "strings" + "testing" +) func TestJobPayloadRules(t *testing.T) { valid := []byte(`{"template":{"id":"tpl","composition":"Main"},"settings":{"type":"video","codec":"video_h264_vbr_15mbps"}}`) @@ -16,3 +20,40 @@ func TestJobPayloadRules(t *testing.T) { t.Fatal("expected short codec to fail") } } + +func TestJobPayloadKeepsMultiFrame(t *testing.T) { + for _, tc := range []struct { + name string + payload string + want string + }{ + {"off", `{"template":{"id":"tpl","composition":"Main"},"settings":{"multiFrame":false}}`, `"multiFrame":false`}, + {"on", `{"template":{"id":"tpl","composition":"Main"},"settings":{"multiFrame":true}}`, `"multiFrame":true`}, + } { + t.Run(tc.name, func(t *testing.T) { + payload, err := JobPayload([]byte(tc.payload)) + if err != nil { + t.Fatalf("payload failed: %v", err) + } + encoded, err := json.Marshal(payload) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + if !strings.Contains(string(encoded), tc.want) { + t.Fatalf("submitted payload %s does not contain %s", encoded, tc.want) + } + }) + } + + omitted, err := JobPayload([]byte(`{"template":{"id":"tpl","composition":"Main"},"settings":{"type":"video"}}`)) + if err != nil { + t.Fatalf("payload failed: %v", err) + } + encoded, err := json.Marshal(omitted) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + if strings.Contains(string(encoded), "multiFrame") { + t.Fatalf("submitted payload %s should leave multiFrame out when it is not set", encoded) + } +} diff --git a/openapi/openapi.json b/openapi/openapi.json index 04c4af8..f6b301b 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -54,7 +54,7 @@ "properties": { "src": { "type": "string", - "description": "Source URL, script data or file path for the asset (supports HTTP/HTTPS URLs or local paths)" + "description": "Public HTTP(S), native s3:/gs:, inline data:, or a file: URL inside the current job work directory. The worker rejects local paths and symlinks escaping that directory. Other schemes and non-public network destinations are rejected." }, "type": { "type": "string", @@ -178,6 +178,10 @@ "enum": ["ae2025", "ae2026"], "default": "ae2026", "description": "After Effects render engine version for the child job" + }, + "multiFrame": { + "type": "boolean", + "description": "Turn After Effects Multi-Frame Rendering on (true) or off (false) for the child job. If omitted, the worker's own setting applies" } } } @@ -263,6 +267,10 @@ "enum": ["ae2025", "ae2026"], "default": "ae2026", "description": "After Effects render engine version for the child job" + }, + "multiFrame": { + "type": "boolean", + "description": "Turn After Effects Multi-Frame Rendering on (true) or off (false) for the child job. If omitted, the worker's own setting applies" } } } @@ -434,7 +442,7 @@ }, "src": { "type": "string", - "description": "Source URL to the After Effects template (.aep, .mogrt, or .zip file)" + "description": "Public HTTP(S), native s3:/gs:, inline data:, or a file: URL inside the current job work directory for an After Effects template (.aep, .mogrt, or .zip file). The worker rejects paths and symlinks escaping that directory and non-public network destinations." }, "composition": { "type": "string", @@ -527,6 +535,10 @@ "enum": ["ae2025", "ae2026"], "default": "ae2026", "description": "After Effects render engine version used when the job is picked up by a worker" + }, + "multiFrame": { + "type": "boolean", + "description": "Turn After Effects Multi-Frame Rendering on (true) or off (false) for this job. If omitted, the worker's own setting applies" } }, "additionalProperties": false @@ -556,7 +568,7 @@ "endpoint": { "type": "string", "default": "https://s3.amazonaws.com", - "description": "Custom S3-compatible endpoint URL" + "description": "Public HTTP(S) S3-compatible endpoint URL. Explicit customer credentials are required; worker credentials are not used for arbitrary endpoints." }, "region": { "type": "string", @@ -713,6 +725,7 @@ }, "displayName": { "type": "string", + "maxLength": 255, "description": "Human-readable name for the template shown in UI and listings" }, "src": { @@ -731,6 +744,7 @@ "properties": { "displayName": { "type": "string", + "maxLength": 255, "description": "Updated human-readable name for the template" } } diff --git a/skills/nexrender/references/operations.md b/skills/nexrender/references/operations.md index ce45805..115713e 100644 --- a/skills/nexrender/references/operations.md +++ b/skills/nexrender/references/operations.md @@ -5,6 +5,7 @@ - Layer names are case-sensitive. Use the `name` returned by `template layers` as `layerName`. - Codec identifiers use their full prefix, such as `video_h264_vbr_15mbps`, `image_png`, or `video_prores_422`. - `preview: true` and `settings` are mutually exclusive. Preview mode ignores normal output settings. +- `settings.multiFrame` turns After Effects Multi-Frame Rendering on (`true`) or off (`false`) for one job. Leave it out to keep the render node's default. Set `false` when a template renders incorrectly with Multi-Frame Rendering. - `${secrets.NAME}` is the secret reference form. - `missingFonts` is a visual-correctness warning. A render may finish with fallback fonts and still be wrong.