Skip to content
C.W.K.
Lesson 01 of 04 · published

Who Appears, Who Pays

~13 min · video, ownership, references, catalog

Level 0Sketch
0 XP0/43 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

The same pipeline, with money on the line

An image from Avatars can be paid for in several ways: a subscription, prepaid credits, a local machine's time or a metered key. A clip is paid for one way only. Every clip goes through Ember's Pro Video, which hands it to a vendor model and spends ElevenLabs credits by the second. Those credits are the same pool my voice speaks from, and the account has no top-up.

So the clip workflow reuses what the image studio already does well (subjects, references, the composed prompt, a durable batch of tasks) and adds one thing on top: an explicit split of who decides what. The architecture doc says it in one sentence. Avatars decides who appears and with which files. Ember validates, prices and owns the spending policy. Nothing about a video model is copied into Avatars.

Two kinds of video inside Ember, kept apart

Ember makes video in two ways. Local video runs on the family's own machines and costs nothing per clip. Pro Video calls a vendor and costs credits. Dad ruled on 2026-09-30 (coop todo #282) that the two never mix: a local request never becomes a paid one, a paid request never falls back to a local model, and no automatic route crosses between them. Avatars talks only to the paid side, through its endpoints under /api/v1/api-video.

That ruling changes what auto means. For images, the automatic generator is Ember's walk down its tiers, which the Plan Before You Spend track covers. For a clip, auto (or default, or automatic) resolves to a single name: the default_generator that Ember's video catalog publishes. Ember names its configured preference, Veo 3.1 Fast through ElevenLabs, when that row is ready, and otherwise the first ready generator. When nothing is ready, the plan stops with a 503: No paid video generator is ready in Ember.

The owner table

QuestionWho answersHow
Who is in the clip, in which lookAvatarsSubjects resolve to their reference profiles, as for an image
May this soul depict these subjectsPippa's policyAvatars asks Pippa for the whole subject set before it prices a single clip
Which files go out, in which roleAvatarsReference images, or a start and end frame
Which generators exist, with which lengths, resolutions and limitsEmberGET /api-video/options
The settled settings and the price of one clipEmberPOST /api-video/plan, which spends nothing
The spending limit and the reserveEmberIts own configuration, published as the catalog's policy
The total for the whole request, and whether it may runAvatarsplan_clip() adds up every clip and applies Ember's policy to the sum
The vendor call, its journal and the first copyEmberPro Video's jobs and archive
A second copy, with its lineageAvatarsThe finished clip becomes an asset with its subjects and references

Who appears

A clip's subjects work exactly as an image's do. Each named subject brings its default references, a named look such as dad:anime brings that look's images, and explicit references come along too. The architecture doc's advice on the reference itself is modest: a shoulders-up portrait carries the likeness, and the prompt does the rest.

The one difference is frames. Every video model takes either reference images or a start and end frame, never both. When clip.start_frame names an image (the day's thumbnail, say), the plan sends that image with the role first, leaves the subjects' reference images out, and says so in a note shown at review. Identity does not leave with them. Each subject's appearance rules are still appended to the composed prompt, and the test checks that the rule line survives a frame.

The prompt begins with the clip workflow's own template, which Dad can edit in the app: one continuous shot in plain present-tense sentences, every person kept exactly as in the references, and no on-screen text unless the prompt gives the exact words. The aspect ratio is 16:9 unless the request sets one.

Refused before anything is priced

plan_clip() opens with refusals, and each one costs a sentence instead of credits:

  • A set or frame names: A clip is not an avatar frame.
  • The same generator twice in one request.
  • More than four clips in one request (AVATARS_MAX_CLIP_TASKS), because every clip is paid by the second.
  • A clip offered as a reference or a frame. References and frames must be images.
  • A generator Ember no longer lists, or one that does not take the frame the request gives.
  • More reference images than the model declares: Name fewer subjects, or give a start frame instead.

Only then does the plan call Ember's /api-video/plan, once per generator. Ember settles the defaults and refuses, in the model's own words, a length that model does not offer. The image studio's cap is 600 tasks. The clip cap is 4. That gap is the whole story of this track: an image mistake is usually cheap, and a clip mistake never is.

If reference images and first frames are new to you as ideas, the video track of Generative Media Foundations explains them. Here we only care who chooses them.

Why the line sits there

Ember knows models and money. It discovers the vendor's models with their lengths and limits, observes prices after each clip, and holds the only account key. Avatars knows people: which souls exist, what they look like, who may depict whom, and which original every reference came from. If Avatars kept its own copy of Veo's lengths or of a price per second, that copy would be right until the day the vendor changed it, and then quietly wrong. The Ember & Cinder Quest calls this rule the seam: Ember is the catalog authority, and its clients keep no model lists of their own. Clips are where the seam earns its keep, because a stale copy here costs credits.

Principle: Put each decision with the side that holds its facts. People belong to the app that knows them. Prices belong to the service that pays.

Code

cwkAvatars, backend/contracts.py: what only a clip has, consent included (verbatim, lines 27-42)·python
class Clip(Input):
    """What only a video clip has. Who appears is still the request's subjects
    and references; every clip is paid, so consent rides here too."""
    duration_secs: int | None = Field(default=None, ge=1, le=600)
    resolution: str | None = None
    # Off unless asked for: a clip is material and its music is made apart.
    audio: bool = False
    negative_prompt: str | None = Field(default=None, max_length=2000)
    # An image shown as the clip's first (or last) frame. A model takes frames
    # or reference images, never both: with a frame, the subjects' reference
    # images are not sent and identity comes from the frame itself.
    start_frame: str | None = None
    end_frame: str | None = None
    # Consent for a request Ember's spending policy would hold back.
    confirm: bool = False
    max_credits: int | None = Field(default=None, ge=0)
cwkAvatars, backend/studio.py: plan_clip() decides who appears and with which files (verbatim, lines 452-479)·python
    async def plan_clip(self, body: Generation, actor=None):
        """A paid video clip. Avatars decides who appears and with which
        references; Ember's Pro Video validates, prices and owns the policy.
        Every clip spends credits, so the plan states what the whole request
        costs and whether it may run without asking."""
        clip = body.clip or Clip()
        if body.set_id or body.frames:
            raise StudioError("A clip is not an avatar frame: leave the set and the frames empty.")
        if len({p.id for p in body.providers}) != len(body.providers):
            raise StudioError("Choose each generator once; use the count for repetitions.")
        if body.count * len(body.providers) > self.max_clip_tasks:
            raise StudioError(f"One request makes at most {self.max_clip_tasks} clips; every clip is paid by the second.")
        catalog = await self.upstream.video_catalog()
        rows = {g["id"]: g for g in catalog["generators"]}
        refs, subjects, rules = await self.resolve_references(body.subjects, body.references, actor)
        frames = [(role, await self.asset(id, actor)) for role, id in (("first", clip.start_frame), ("last", clip.end_frame)) if id]
        if any(a.get("media") == "video" for a in [*refs, *(a for _, a in frames)]):
            raise StudioError("References and frames for a clip must be images.")
        notes = []
        if frames:
            # Every model takes frames or reference images, never both.
            inputs = [{"id": a["id"], "sha256": a["sha256"], "role": role} for role, a in frames]
            if refs:
                notes.append("A start or end frame was given, so the subjects' reference images are not sent: identity comes from the frame and the identity rules.")
        else:
            inputs = [{"id": a["id"], "sha256": a["sha256"], "role": "reference_image"} for a in refs]
        task_subjects = sorted(set(subjects) | {s for _, a in frames for s in a["subjects"]})
        await self.subjects(task_subjects, actor)

External links

Exercise

Pick a feature in your own software that sends work to a paid external service: a video or image API, a transcription service, a cloud render. Write an owner table for it with three columns: the question, who answers it, and the call or file where the answer lives. Then search your client code for any copy of a fact the service owns (a model list, a maximum length, a price, a size limit) and replace each one with a read from the service's own catalog or a refusal the service returns in its own words.
Hint
A copied fact usually hides as a constant, an enum or a dropdown's option list. For each one, ask what happens on the day the vendor changes it. If the answer is that your app quietly sends a request the vendor will reject or bill differently, that fact belongs to the service.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.