/* vfxr_build_preview.jsx — VFXReview render pipeline, preview build stage. Runs headlessly: AfterFX.com -noui -r .jsx where the wrapper sets var VFXR_CONTEXT_PATH = "...json"; then $.evalFile()s this script. It does NOT render. It opens the studio slate/overlay template, rebuilds the shot around the *rendered* EXR sequence (OCIO → show LUT → overlay), duplicates the export template into a preview comp with the slate filled in, queues the MOV and MP4 output modules, and saves a throwaway AEP. The worker then runs `aerender -project ` with no -comp, which renders the whole queue in one launch. Build logic mirrors the VFXReviewConnector panel functions buildShotFromCode / updateOverlayForComp / buildPreviewForShot so the farm and the artist produce identical slates and burn-ins. Everything is reported back through the result JSON — never a dialog. */ (function vfxrBuildPreview() { var ctx = null; var result = { ok: false, error: null, warnings: [], tempAep: null, previewComp: null, queued: [], frameCount: null, width: null, height: null }; var resultPath = null; // ── tiny helpers (ES3 / ExtendScript safe) ─────────────────────────────── function readTextFile(path) { var f = new File(path); var text; if (!f.exists) { return null; } f.encoding = "UTF-8"; if (!f.open("r")) { return null; } text = f.read(); f.close(); return text; } function writeTextFile(path, text) { var f = new File(path); try { f.encoding = "UTF-8"; if (!f.open("w")) { return false; } f.write(text); f.close(); return true; } catch (writeError) { return false; } } function jsonEscape(value) { var text = String(value); var out = ""; var i; var ch; var code; for (i = 0; i < text.length; i += 1) { ch = text.charAt(i); code = text.charCodeAt(i); if (ch === "\"") { out += "\\\""; } else if (ch === "\\") { out += "\\\\"; } else if (ch === "\n") { out += "\\n"; } else if (ch === "\r") { out += "\\r"; } else if (ch === "\t") { out += "\\t"; } else if (code < 32) { out += "\\u" + ("000" + code.toString(16)).slice(-4); } else { out += ch; } } return out; } function toJSON(value) { var parts = []; var i; var key; if (value === null || value === undefined) { return "null"; } if (typeof value === "number") { return isFinite(value) ? String(value) : "null"; } if (typeof value === "boolean") { return String(value); } if (typeof value === "string") { return "\"" + jsonEscape(value) + "\""; } if (value instanceof Array) { for (i = 0; i < value.length; i += 1) { parts.push(toJSON(value[i])); } return "[" + parts.join(",") + "]"; } for (key in value) { if (value.hasOwnProperty(key) && value[key] !== undefined) { parts.push("\"" + jsonEscape(key) + "\":" + toJSON(value[key])); } } return "{" + parts.join(",") + "}"; } function warn(message) { result.warnings.push(String(message)); } function findComp(name) { var i; var item; for (i = 1; i <= app.project.numItems; i += 1) { item = app.project.item(i); if (item instanceof CompItem && item.name === name) { return item; } } return null; } function findFolder(name) { var i; var item; for (i = 1; i <= app.project.numItems; i += 1) { item = app.project.item(i); if (item instanceof FolderItem && item.name === name) { return item; } } return null; } // Mirrors the panel's addOCIOToLayer: effect added but disabled, so the // artist/farm chain matches exactly. function addOCIOToLayer(layer) { var ocio; var outputProp; try { ocio = layer.property("Effects").addProperty("OCIO Color Space Transform"); ocio.enabled = false; } catch (ocioError) { warn("OCIO effect unavailable: " + ocioError.toString()); return; } try { outputProp = ocio.property("Output Color Space"); } catch (byNameError) { outputProp = null; } if (!outputProp) { try { outputProp = ocio.property(2); } catch (byIndexError) { outputProp = null; } } if (outputProp) { try { outputProp.setValue(93); } catch (setError) { warn("Could not set OCIO output colour space: " + setError.toString()); } } } function setEssentialProperty(layer, propName, value) { if (value === null || value === undefined || value === "") { return false; } try { layer.property("Essential Properties").property(propName).setValue(value); return true; } catch (setError) { return false; } } function setEssentialPropertyByIndex(layer, index, value) { try { layer.property("Essential Properties").property(index).setValue(value); return true; } catch (setError) { return false; } } function listEssentialPropertyNames(layer) { var names = []; var group; var i; try { group = layer.property("Essential Properties"); for (i = 1; i <= group.numProperties; i += 1) { try { names.push(i + ":" + group.property(i).name); } catch (oneError) { } } } catch (groupError) { } return names; } // Per-submission fields (VFX Scope / Submission Note). The property names // come from config; if one does not exist, report the template's actual // property names so it can be corrected without another build. function setSlateField(layer, propName, value, label) { if (!propName) { return; } if (value === null || value === undefined || value === "") { return; // nothing to write; leave the template's own default } if (!setEssentialProperty(layer, propName, value)) { warn(label + ": Essential Property \"" + propName + "\" not found or not settable. " + "Available: " + listEssentialPropertyNames(layer).join(", ")); } } function setCompStartFrame(comp, frameNumber) { try { comp.displayStartFrame = frameNumber; return; } catch (frameError) { } try { comp.displayStartTime = frameNumber / comp.frameRate; } catch (timeError) { } } function applyOutputModuleTemplate(outputModule, templateName) { var templates; var i; try { templates = outputModule.templates; for (i = 0; i < templates.length; i += 1) { if (templates[i] === templateName) { outputModule.applyTemplate(templateName); return true; } } } catch (templateError) { } return false; } function importExrSequence(dirPath) { var folder = new Folder(dirPath); var files; var importOptions; var footage; if (!folder.exists) { throw new Error("Render output folder not found: " + dirPath); } files = folder.getFiles("*.exr"); if (!files || files.length < 1) { throw new Error("No EXR files found in " + dirPath); } files.sort(); importOptions = new ImportOptions(files[0]); importOptions.sequence = true; footage = app.project.importFile(importOptions); try { footage.mainSource.conformFrameRate = ctx.fps || 24; } catch (conformError) { warn("Could not conform frame rate: " + conformError.toString()); } return footage; } function queueOutput(comp, templateName, outputPath, label) { var item; var outputModule; if (!templateName || !outputPath) { warn("Skipping " + label + ": no template or output path configured"); return false; } try { item = app.project.renderQueue.items.add(comp); outputModule = item.outputModule(1); if (!applyOutputModuleTemplate(outputModule, templateName)) { item.remove(); throw new Error("Output module template not found: \"" + templateName + "\""); } outputModule.file = new File(outputPath); result.queued.push({ label: label, template: templateName, output: outputPath }); return true; } catch (queueError) { try { if (item) { item.remove(); } } catch (removeError) { } throw queueError; } } // ── main ───────────────────────────────────────────────────────────────── try { if (typeof VFXR_CONTEXT_PATH === "undefined" || !VFXR_CONTEXT_PATH) { throw new Error("VFXR_CONTEXT_PATH was not set by the wrapper script"); } var contextText = readTextFile(VFXR_CONTEXT_PATH); if (!contextText) { throw new Error("Could not read job context: " + VFXR_CONTEXT_PATH); } ctx = eval("(" + contextText + ")"); resultPath = ctx.resultPath; try { app.beginSuppressDialogs(); } catch (suppressError) { } // 1. Open the studio template (slate, overlay, show LUT, export template) var templateFile = new File(ctx.templateAep); if (!templateFile.exists) { throw new Error("Template project not found: " + ctx.templateAep); } app.open(templateFile); var templateComp = findComp(ctx.templateComp); if (!templateComp) { throw new Error("Export template comp \"" + ctx.templateComp + "\" not found in template project"); } // 2. Import the rendered EXR sequence var footage = importExrSequence(ctx.outputDirLocal); var footageFolder = findFolder("_FOOTAGE_4K"); if (footageFolder) { footage.parentFolder = footageFolder; } footage.name = ctx.shotCode + "_" + ctx.versionString; var width = footage.width; var height = footage.height; var duration = footage.duration; var fps = ctx.fps || 24; result.width = width; result.height = height; result.frameCount = Math.round(duration * fps); // 3. Footage precomp (mirrors buildShotFromCode) var precompFolder = findFolder("_PRECOMPS"); var footageComp = app.project.items.addComp( ctx.shotCode + "_FOOTAGE", width, height, 1, duration, fps); if (precompFolder) { footageComp.parentFolder = precompFolder; } if (ctx.frameStart !== null && ctx.frameStart !== undefined) { setCompStartFrame(footageComp, ctx.frameStart); } addOCIOToLayer(footageComp.layers.add(footage)); // 4. Shot comp: footage + show LUT + overlay var shotComp = app.project.items.addComp(ctx.shotCode, width, height, 1, duration, fps); if (ctx.frameStart !== null && ctx.frameStart !== undefined) { setCompStartFrame(shotComp, ctx.frameStart); } shotComp.layers.add(footageComp); if (ctx.lutComp) { var showLut = findComp(ctx.lutComp); if (showLut) { var lutLayer = shotComp.layers.add(showLut); lutLayer.collapseTransformation = true; } else { warn("Show LUT comp \"" + ctx.lutComp + "\" not found — rendering without it"); } } if (ctx.overlayComp) { var overlayComp = findComp(ctx.overlayComp); if (overlayComp) { var overlayLayer = shotComp.layers.add(overlayComp); overlayLayer.moveToBeginning(); overlayLayer.enabled = true; setEssentialProperty(overlayLayer, "DATE", ctx.slate.date); setEssentialProperty(overlayLayer, "SHOT NAME", ctx.slate.versionName); } else { warn("Overlay comp \"" + ctx.overlayComp + "\" not found — rendering without burn-ins"); } } // 5. Preview comp from the export template (mirrors buildPreviewForShot) var previewsFolder = findFolder("_PREVIEWS"); var previewComp = templateComp.duplicate(); previewComp.name = ctx.shotCode + "_PREVIEW"; if (previewsFolder) { previewComp.parentFolder = previewsFolder; } previewComp.layer("SHOT").replaceSource(shotComp, false); try { previewComp.layer("THUMBNAIL").replaceSource(shotComp, false); } catch (thumbError) { warn("THUMBNAIL layer not updated: " + thumbError.toString()); } var slateLayer = previewComp.layer("NETFLIX_SLATE"); if (slateLayer) { // Index-addressed properties match the panel exactly (3 = version // name, 10 = shot code); the rest are addressed by name. setEssentialPropertyByIndex(slateLayer, 3, ctx.slate.versionName); setEssentialProperty(slateLayer, "Date", ctx.slate.date); setEssentialProperty(slateLayer, "Desc", ctx.slate.description); setEssentialPropertyByIndex(slateLayer, 10, ctx.slate.shotCode); setEssentialProperty(slateLayer, "Episode", ctx.slate.episode); setEssentialProperty(slateLayer, "Scene", ctx.slate.scene); setEssentialProperty(slateLayer, "Frames", result.frameCount); setSlateField(slateLayer, ctx.slateScopeProp, ctx.slate.vfxScope, "VFX Scope"); // The slate's Notes field carries this submission's note; when the // artist left it blank, fall back to the shot's own notes so the // slate looks the same as an artist-built preview. setSlateField(slateLayer, ctx.slateSubmissionProp, (ctx.slate.submissionNote !== null && ctx.slate.submissionNote !== undefined && ctx.slate.submissionNote !== "") ? ctx.slate.submissionNote : ctx.slate.notes, "Submission Note"); } else { warn("NETFLIX_SLATE layer not found in template — slate fields not filled"); } var shotLayer = previewComp.layer("SHOT"); var targetOutPoint = shotLayer.inPoint + shotComp.duration; if (targetOutPoint > previewComp.duration) { previewComp.duration = targetOutPoint; } shotLayer.outPoint = targetOutPoint; previewComp.duration = targetOutPoint; result.previewComp = previewComp.name; // 6. Queue both outputs against the same preview comp. aerender will // render the whole queue in one launch. app.project.renderQueue.showWindow(false); while (app.project.renderQueue.numItems > 0) { app.project.renderQueue.item(1).remove(); } queueOutput(previewComp, ctx.movTemplate, ctx.movOutput, "mov"); queueOutput(previewComp, ctx.mp4Template, ctx.mp4Output, "mp4"); if (result.queued.length < 1) { throw new Error("Nothing was queued — check output module templates"); } // 7. Save the throwaway project the worker will hand to aerender var tempFile = new File(ctx.tempAep); var tempParent = tempFile.parent; if (tempParent && !tempParent.exists) { tempParent.create(); } app.project.save(tempFile); result.tempAep = ctx.tempAep; result.ok = true; } catch (buildError) { result.ok = false; result.error = buildError && buildError.toString ? buildError.toString() : String(buildError); try { if (buildError && buildError.line) { result.error += " (line " + buildError.line + ")"; } } catch (lineError) { } } try { app.endSuppressDialogs(false); } catch (endSuppressError) { } if (!resultPath && ctx && ctx.resultPath) { resultPath = ctx.resultPath; } if (resultPath) { writeTextFile(resultPath, toJSON(result)); } // -noui leaves the app running otherwise; the worker waits on exit. try { app.quit(); } catch (quitError) { } }());