Files
twotalesanimation 70cf45ead4
Deploy / deploy (push) Successful in 7m2s
feat(pipeline): standalone Blender renders on the same queue
Blender work is not a shot submission: no Export, no version, no
validation, QC or delivery. It is "Ctrl+F12, but on the farm", so it is
modelled as what it is — a queue entry and nothing more.

That turned out to need very little. Claim, progress, fail and
complete-render already tolerated a job with no Export behind them, so a
Blender job reuses the whole lifecycle; RenderJob gains projectId, label
and submittedBy so a job with no shot code still has something to show.

Capability gating is new and was already missing: the claim now filters
job types by the machine's reported tools. Without it a workstation
without Blender would claim Blender work and fail it repeatedly, burning
the attempts while a machine that could have rendered it sat idle. The
worker asks for Blender jobs unconditionally and the server refuses them
— trusting the worker to self-select would have left the same hole.

The runner treats a clean exit with no saved frames as a failure:
Blender exits 0 when a frame range is empty or an output path is
unwritable, and a job that silently rendered nothing is the worst
outcome available.

Standalone jobs auto-requeue on a transient failure like any other work;
delivery builds are deliberately excluded, since a half-written package
wants a human first.

Adds the Blender add-on (BlenderPanel/) and its docs. The add-on refuses
to submit an unsaved file — the farm opens the .blend from disk, so
unsaved changes would render the previous state without warning. Its
payload is pinned by a contract test, since add-on and server only ever
meet over JSON.

Worker 1.5.0. Adds migration 20260815000000_blender_render_jobs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:53:19 +02:00

51 lines
1.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { blenderManifestSchema, createBlenderJobSchema } from "@/lib/render-pipeline/blender";
/**
* The add-on and the server are separate codebases that only meet over JSON,
* so the exact payload the add-on builds is pinned here. A field renamed on
* either side otherwise surfaces as a 422 in front of an artist.
*/
describe("Blender submission contract", () => {
// Verbatim shape of scene_summary() in BlenderPanel/vfxreview_submit.py
const addonManifest = {
blendFile: "X:/shows/UNG/cg/UNG_110_030_010_smoke.blend",
scene: "Scene",
frameStart: 1001,
frameEnd: 1110,
outputPath: "X:/shows/UNG/cg/renders/smoke/frame_",
engine: "CYCLES",
blenderVersion: "4.2.1",
};
it("accepts what the add-on sends", () => {
const parsed = blenderManifestSchema.safeParse(addonManifest);
expect(parsed.success).toBe(true);
});
it("accepts the full submission body", () => {
const parsed = createBlenderJobSchema.safeParse({
projectCode: "UNG_S1",
label: "UNG_110_030_010_smoke",
manifest: addonManifest,
submittedByEmail: "chris@twotalesanimation.com",
urgent: false,
});
expect(parsed.success).toBe(true);
});
it("defaults the stage so the add-on need not send it", () => {
const parsed = blenderManifestSchema.parse(addonManifest);
expect(parsed.stage).toBe("BLENDER");
});
it("rejects a backwards frame range rather than queueing a no-op render", () => {
const parsed = blenderManifestSchema.safeParse({ ...addonManifest, frameEnd: 900 });
expect(parsed.success).toBe(false);
});
it("accepts EEVEE, which is what the add-on reports on a non-Cycles scene", () => {
expect(blenderManifestSchema.safeParse({ ...addonManifest, engine: "BLENDER_EEVEE_NEXT" }).success).toBe(true);
});
});