Skip to main content

4. Resolve plans and readiness

A valid request is not necessarily a renderable request. JSON Schema can prove that seconds is an integer; it cannot prove that the selected model is mounted, accepts that duration or supports your reference mode. Studio resolves those facts against the live cluster before starting work.

Make a request concrete​

A user may leave the model, seed, size or generation settings blank. Planning chooses concrete values from the selected card and current placement. Studio supports text-only shots, image references, timed keyframes and card-supported controls. The planner checks:

  • The model's modes and duration bounds.
  • Frame rate and legal frame-count grid.
  • Trained canvas or draft geometry, dimension multiples and maximum pixels.
  • Supported sampler, scheduler, step count and shift settings.
  • Reference roles, timing, control strength and control window.
  • Ready placement on an eligible video engine.

These are card-specific rules. A RenderRequest allows 1–60 seconds, but an individual card can accept a narrower range. A field appearing in the schema is not proof that every model implements it.

Read RenderRequest in contracts.py and plan_render in planning.py together. The first describes the vocabulary; the second resolves its meaning for the selected model.

Choose the seed before review​

Here is the actual entry point:

studio:child.py:Studio.plan
async def plan(self, payload: dict[str, JsonValue]) -> RenderPlan:
"""Resolve a request against the live catalog and fleet.

A request that leaves the seed blank gets one here, so every plan
names the seed it will render with and a finished clip can be run
again or varied. The caller sends the plan's seed back with its
digest; ``plan_render`` itself stays pure, so a start re-plans to
exactly the reviewed plan.
"""
request = RenderRequest.model_validate_json(json.dumps(payload))
if request.seed is None:
request = request.model_copy(update={"seed": secrets.randbelow(2**32)})
# The keyframe's shape comes from the library on disk, off the loop;
# the same completion runs again at submission, so the digests agree.
completed = await asyncio.to_thread(
complete_from_keyframe, request, self._asset_dimensions
)
keyframe = completed is not request
models, nodes, placed = await self.api.catalog_and_fleet()
plan = plan_render(
completed,
models,
nodes,
placed,
default_model=self.settings.default_model,
timings=self.timings.timings(),
)
return plan.model_copy(update={"canvas_from": "keyframe"}) if keyframe else plan

Notice three choices:

  1. Pydantic validates the payload before cluster reads.
  2. A missing seed is chosen once, then included in the returned plan.
  3. Asset inspection moves off the event loop; catalog and placement are read asynchronously.

plan_render stays a pure function. Tests can give it cards, nodes and placements without starting an HTTP server. This is an effective pattern for your own application: isolate resolution from network access.

For timed keyframes, Studio converts times into model frames. Two distinct times that round to the same frame are refused. It also checks collisions with first/last-frame references. A request can look reasonable in seconds and still be impossible on the model's frame grid.

Review and start the same work​

There are two planning interfaces:

InterfaceResultUse
video.planFull RenderPlanShow geometry, settings, placement, warnings and estimates in the application.
video.render with op: "plan"SDK OperationPlanReview and launch through the common durable-operation interface.

Studio connects them here:

studio:child.py:Studio._plan_operation
async def _plan_operation(self, payload: dict[str, JsonValue]) -> OperationPlan:
plan = await self.plan(payload)
summary = plan.model_dump_json()
# The same digest a start echoes back: one value binds the review. The
# effective input carries the seed the plan chose and the digest, so a
# caller starts exactly what it reviewed without reading the summary.
effective: dict[str, JsonValue] = {
**payload,
"seed": plan.seed,
"plan_digest": plan.plan_digest,
}
return OperationPlan(
plan_digest=plan.plan_digest,
summary=summary[:512],
estimated_seconds=plan.estimated_seconds,
expires_at=int(time.time()) + PLAN_VALID_SECONDS,
input=effective,
)

The effective input carries both the chosen seed and plan_digest. Send that returned input to start; do not generate a fresh seed or reconstruct it from the shortened summary. The summary is for display and can be clipped to 512 characters.

The digest binds the resolved work, including prompt, references, model settings and placement. Display-only warnings and timing estimates are excluded. canvas_from and canvas_edge annotations are also excluded. A batch group labels related takes but is not an engine input.

When starting, Studio resolves again and rejects a supplied digest that no longer matches. This catches a changed card, placement or setting between review and submission. The digest does not reserve hardware and does not grant permission to perform an effect.

The operation plan has an expiry timestamp. Treat it as a review lifetime, not a promise that placement will remain unchanged until then; start still performs the live comparison.

Readiness answers a different question​

video.readiness checks API reachability, available video cards, video engine tags and ready placement reported by Skulk™. Skulk™ owns engine qualification and placement eligibility; Studio consumes those live facts rather than implementing a second support matrix. It reports reasons and busy state so the interface can explain what is missing. It does not mount models or repair the cluster automatically.

A host advertising an engine name alone is insufficient. The relevant model must have ready placement on a compatible engine; system-model placement is not interchangeable with video capacity.

Timing estimates come from local observations for the model and host. No history means no learned estimate. Present an unknown estimate honestly instead of inventing a duration.

Try this in your own app​

Build a pure planner with a fake catalog. Test a valid request, an unsupported mode and a changed placement. Then add the network layer. You should be able to explain why the third case needs a new review without making a generation request.

Next: durable rendering.