Let creative commissions pin an image/video render backend (local vs. Grok vs. Codex)#3148
Merged
Conversation
Which backend actually rendered a commission's image/video was an accidental
side effect: the CD planner LLM authors `media_enqueue{Image,Video}Job` params
freehand, never set `params.mode`, and `mediaJobQueue` dispatches purely on that
key — so every plan-driven commission silently rendered on local diffusion.
Adds ability-scoped `generation.imageMode`/`videoMode` (+ optional
`imageModelId`/`videoModelId`) defaulting to `auto`. The pin rides the CD project
as `renderBackend` (the enqueue site only sees `ctx.projectId`, not the
commission), where a new `enforceRenderBackendPin` step forces it into the job
params AFTER the planner authored them — parallel to `enforceVideoRenderPreset`,
sharing one project read.
`auto` is a strict no-op: no `renderBackend` key is stored and the enqueued
params are byte-identical to before, so existing commissions see no change.
A pin is re-resolved against live settings each fire, so a since-disabled cloud
toggle degrades to a usable backend instead of failing every nightly run.
Formalizes `VIDEO_GEN_MODE` in a new `videoGen/modes.js`, deriving its values
from the `IMAGE_GEN_MODE` constants the video lane already discriminates on
inline. Video defaults to local rather than preferring an enabled cloud backend
(the image ladder's rule): a Grok clip spends remote quota and only comes in
6s/10s, so enabling Grok for images must not silently redirect video renders.
`creativeCommissions` 2 → 3: `generation` is a federated field and a v2 peer's
`ABILITY_GENERATION_KEYS` doesn't list these keys, so it would strip the pin on
receive and push the downgrade back via LWW — the exact #2769 failure mode.
`generation.model` and the legacy `sceneRunner.js` scene flow are untouched.
Closes #3135
…strips a planner-authored grok discriminator Two ways the pin could still lose to the planner LLM it exists to override: - The video lane's backend discriminator and its t2v/i2v semantic share `params.mode`, so a local pin that preserved the key verbatim would let a planner-authored `mode: 'grok'` dispatch to grok anyway — and would defeat the grok-disabled fallback for the same reason. Strip that one value (local.js infers the semantic from sourceImagePath/keyframes when mode is unset) while still preserving a real semantic like 'fflf'/'a2v'/an IC-LoRA id, which overwriting would silently drop keyframes for. - Both local paths applied `pin.modelId` only when the planner named no model, so the model the user picked in the commission form lost to the planner's guess. The pinned model now wins; an unpinned model still leaves the planner's alone.
… video mode, and a grok pin translates the clip length - Stripping only 'grok' from `params.mode` left a planner-authored 'local' in place, and local.js reads an unrecognized non-empty mode as plain text-to-video — so it would have silently ignored the step's sourceImagePath / keyframes. Strip any VIDEO_GEN_MODES token (both are backend names, not semantics) and let local.js infer the semantic, while still preserving a real one like 'fflf'/'a2v'/an IC-LoRA id. - Clip length crosses a contract boundary the pin has to bridge: the local lane derives frames from `durationSeconds`, but grok's worker reads `duration` and silently defaults to its 6s minimum for anything absent — so a 10s commission rendered a 6s clip. Translate the step's durationSeconds (else the project target) through a new `nearestGrokDuration`, which rounds UP to the shortest deliverable clip. It's separate from `resolveGrokDuration` on purpose: that one validates an already-grok-shaped request and defaults a third value to 6s, which is right at the route boundary but would truncate an 8s brief here.
…no pin, not "no stored key" Third review round flagged that `renderBackend: null` lands on every newly created project, contradicting a comment claiming an unpinned commission "adds no key at all". The comment was wrong, not the code: buildProjectRecord runs only at CREATE, and a null field on a fresh record is the same additive shape this record already uses for musicBed/plan/directive (verbatim JSONB round-trip, no schema-version bump, no HASH_FIELDS entry). Existing projects are never rewritten and sanitizeProjectForSync fabricates nothing — locked in by a test. The contract that actually matters is unchanged and now stated precisely: an auto/absent pin can never produce a pin, so the enqueue-time forcing step returns the job params byte-identical without even reading settings.
atomantic
force-pushed
the
claim/issue-3135
branch
from
July 27, 2026 08:06
d4c4a73 to
aabc02e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Which backend actually rendered a creative commission's image/video was an accidental side effect. The Creative Director's planner LLM authors
media_enqueueImageJob/media_enqueueVideoJobparams freehand, it never setparams.mode, andmediaJobQueue/index.js#getGenModuleForJobdispatches the backend purely on that key — so every plan-driven commission silently rendered on local diffusion no matter what the user wanted.A commission can now pin its render backend:
generation.imageMode/videoMode(+ optionalimageModelId/videoModelId) asGENERATION_KEY_DEFSdescriptors, so bounds/defaults stay single-sourced and the existing per-abilitysuperRefinerejects an off-type pin (videoModeon animagecommission is a 400, not a silent drop).imageModeonimage,videoModeonvideo, both onmusic-video;music/seriescarry neither. A newtype: 'id'descriptor kind covers the nullable free-string model ids.ctx.projectIdand has no reference back to the commission. So the pin rides the CD project:buildRenderBackendPin→buildProjectParams→buildProjectRecord's newrenderBackendfield, and a newenforceRenderBackendPinstep increative/tools/media.jsinjects the resolved mode + provider knob bundle into the job params after the planner authored them. It runs parallel toenforceVideoRenderPresetand shares one project read (previously the video path read the project itself; that read is hoisted toloadOwningProject). Side benefit: any plan-driven CD project now has the same knob, not just commission-spawned ones.Render backendsection inCommissionConfigForm.jsx(mode selector + conditional local-model picker), mirroring theVisualGenSettings.jsxUX. It renders only for abilities that declare backend fields, so a music/series commission never shows it and fetches no model catalogs.VIDEO_GEN_MODEformalized — newserver/services/videoGen/modes.js, deriving its values from theIMAGE_GEN_MODEconstants the video lane already discriminates on inline, plusisVideoModeUsable/resolveVideoMode. The issue left this as the implementer's call; an enumerable alphabet was cheaper than hand-copying['local','grok']into a schema, a picker and a resolver.autois a strict no-opThe hard acceptance criterion. An
auto/absent pin meansbuildRenderBackendPinreturnsnull, soenforceRenderBackendPinreturnsparamsuntouched (not "auto-resolved to something") without even reading settings — existing commissions enqueue byte-identical params and fall through to the install-wide default exactly as before. Covered by tests at each layer.renderBackendis a new null field on newly created projects, the same additive shape this record already uses formusicBed/plan/directive(verbatim JSONB round-trip); existing projects are never rewritten andsanitizeProjectForSyncfabricates nothing, which a test locks in.The pin genuinely overrides the planner
Three things the first draft got wrong and review caught — each is the difference between a pin that holds and one that politely defers to the LLM it exists to override:
pin.modelIdonly when the planner named none meant the model the user picked in the form lost to the planner's guess. An unpinned model still leaves the planner's alone.params.mode. That key is the t2v/i2v semantic on the local lane (fflf,a2v,extend, an IC-LoRA id), which a pin must not overwrite — but the backend discriminator shares it, so a planner-written'grok'would dispatch to grok in defiance of the pin (and defeat the grok-disabled fallback), and'local'is worse than useless (local.js reads an unrecognized non-empty mode as plain text-to-video and would ignore the step'ssourceImagePath/keyframes). Dropping the key entirely repairs both: an unset mode makes local.js infer the semantic.durationSeconds; grok's worker readsdurationand silently falls back to its 6s minimum for anything absent — so a 10s commission rendered a 6s clip. AddsnearestGrokDurationtolib/grokVideoClip.js, which rounds up to the shortest deliverable clip. Deliberately separate from the existingresolveGrokDuration: that one validates an already-grok-shaped request and defaults a third value to 6s, which is right at the route boundary but would truncate an 8s brief here.Other deliberate decisions
creativeCommissions2 → 3.generationis a federated field and a v2 peer'sABILITY_GENERATION_KEYSdoesn't list these keys, so its sanitizer would strip the user's pinned backend on receive and could push the un-pinned downgrade back via LWW — the exact failure mode Creative Commissions: let the user pick the creative output type and tailor params + directive per type #2769 bumped 1 → 2 for.autodefault thatsanitizeCommissionfills on every read, so no on-disk record needs rewriting.renderBackendis likewise additive on the CD project, so nocreativeDirectorProjectsbump.generation.modeland the legacysceneRunner.jsscene flow are untouched, and thecleanC2PA/denoisepost-processing settings stay unresolved on this path — resolving them only when a backend is pinned would make the same commission post-process differently depending on whether a pin exists, and fixing it for both paths would break the "auto is byte-identical" contract.Test plan
cd server && npx vitest run— 22630 passed. 2 failures (lib/dataRoot,services/installState), both pre-existing worktree-path artifacts that pass in the primary checkout; neither touches a file in this diff.cd client && npm test— 469 files / 5185 tests passed.server/services/creative/tools/media.test.js(23 tests) — the forcing step end to end via the real toolexecute, asserting the params that reachenqueueJob: untouched for unpinned/no-project/unreadable-settings, codex/grok pins forced over a planner-authoredmode, a disabled backend degrading (including when the planner already wrote'grok'), both backend tokens stripped from a local video pin while a real semantic survives, the pinned model winning, grok duration translated from the step / project target / an explicit value, both forcing steps composing, an image pin not leaking into a video enqueue, and audio never reading the project or settings at all.server/services/videoGen/modes.test.js(12 tests) — enum derived from the queue discriminator, the strict-boolean grok gate, and the local-not-cloud default.lib/grokVideoClip.test.js(nearestGrokDurationrounding/clamping/fallbacks, and the deliberate divergence fromresolveGrokDurationat 8s),abilityAdapters.test.js(pin builder incl. every no-pin path),store.test.js(sanitize round-trip, blank-model normalization, legacy record reads asauto, hand-edited garbage falls back),projectsLogic.test.js(normalizeRenderBackend,buildProjectRecorddefault, and no key fabricated onto an existing/synced record),routes/creativeCommissions.test.js(schema accept/reject incl. off-ability and unknown-backend 400s),commissionForm.test.js(per-ability field declaration, payload emission, stale-model-id nulling).Closes #3135