Worked example: replacing the mock poser with one that calls an LLM. Rigging and training follow the same three steps.
See CONTRACT.md for the types and rules.
mkdir -p providers/real && touch providers/real/__init__.py
cp providers/mock/posing.py providers/real/posing.pyThe factory resolves PROVIDER_POSING=real to providers.real.posing.RealPoser,
so the class name must be RealPoser.
# providers/real/posing.py
import json
from anthropic import Anthropic
from providers.base import Poser, Progress
from schemas import BONES, Clip, Keyframe, ProviderError, Rig
SYSTEM = f"""You pose a 3D stick figure for a children's learning app.
Reply with JSON only: {{"keyframes": [{{"t": 0.0, "bones": {{"BONE": [x,y,z,w]}}}}]}}
Valid bone names, and no others: {", ".join(BONES)}
Rotations are LOCAL quaternions relative to a rest pose where the figure stands
with arms out to the sides. Omit bones that do not move.
Left arm lies along -X, so raising it is a NEGATIVE z rotation (-90 is straight
up). Right arm is the mirror. Legs swing forward with a negative x rotation and
knees bend with a positive one.
Use 2-5 keyframes over 1-2 seconds."""
class RealPoser(Poser):
def __init__(self):
self.client = Anthropic()
def pose(self, prompt: str, rig: Rig, progress: Progress) -> Clip:
progress(0.2, "Reading your words...")
try:
message = self.client.messages.create(
model="claude-sonnet-5",
max_tokens=2000,
temperature=0, # rule 7: same prompt, same clip
system=SYSTEM,
messages=[{"role": "user", "content": prompt}],
)
except Exception as exc:
# Rule 3: the child sees user_message, the terminal sees detail.
raise ProviderError(
"I couldn't think of that move right now. Try again?",
detail=f"{type(exc).__name__}: {exc}") from exc
progress(0.8, "Moving the joints...")
try:
data = json.loads(message.content[0].text)
keyframes = [Keyframe(t=float(kf["t"]), pose=kf["bones"])
for kf in data["keyframes"]]
except (KeyError, ValueError, TypeError) as exc:
raise ProviderError(
"That move came out muddled. Try describing it differently?",
detail=f"unparseable model output: {exc}") from exc
return Clip(name=prompt[:40], prompt=prompt, keyframes=keyframes,
loop=True)You do not need to validate the pose yourself. app.py runs validate_clip()
on whatever you return, which normalises the quaternions, fills omitted bones
from rest, and raises ContractError naming the offending bone if the model
invents a name. That error is logged as a contract violation and shown to the
user as a generic message.
export PROVIDER_POSING=real
pytest tests/test_contract.py -k PoserThe suite is parameterised over whichever provider is configured, so this is the same file that checks the mock. Expect to have to satisfy:
| Test | Requirement |
|---|---|
test_only_uses_contract_bones |
no invented bone names |
test_is_deterministic |
temperature=0, or cache by prompt |
test_keyframe_times_strictly_increase |
sort your keyframes |
test_unknown_prompt_still_returns_something |
handle nonsense without raising |
test_moves_the_avatar |
the pose differs from rest |
Then run the app and click through:
flask --app app run --debugThe status bar shows posing: REAL while rigging and training stay on mocks.
Rigger. Start by returning Rig(format="procedural") and ignoring the image
— that alone makes the pipeline run. Move to Rig(format="glb", glb_bytes=...)
when you have geometry; bone names in the GLB must match schemas.BONES and the
frontend needs no change.
Trainer. Yield episodes as fast as you compute them and do not sleep; the
server paces the stream so the speed control works. Check stop.is_set() between
episodes. Score with rewards.reward() so the UI sliders keep their meaning.
rewards.world_positions() gives you joint positions in world space if your
policy needs them.
| Symptom | Cause |
|---|---|
RuntimeError naming a missing file at startup |
PROVIDER_X=real with no providers/real/<name>.py |
RuntimeError about a missing class |
class not named Real<Rigger|Poser|Trainer> |
CONTRACT VIOLATION in the server log |
return value failed validate_*; the message names the field |
| UI shows a friendly error, terminal shows detail | your ProviderError — working as intended |