Gestures (Essence 1)

Play a named gesture (wave, nod, clap) on an avatar exactly when your code asks, on a cloud avatar or a self-hosted one.

Gestures are named clips baked into an avatar, such as mini_wave_hello or clap_cheer. Your code plays one by name, when it chooses: on an app event, a timer, or an allow-listed tool call. Nothing plays at random.

Gestures are an Essence 1 feature; Essence 2 and Expression 2 avatars have no gesture clips.

Before you start

  • An Essence 1 agent of yours with gestures generated (Gestures API).
  • A LiveKit agent worker with the bitHuman plugin (LiveKit).

1. List the gesture names

curl -s https://api.bithuman.ai/v1/dynamics/$AGENT_CODE -H "api-secret: $BITHUMAN_API_SECRET"
# → {"success": true, "data": {"status": "ready", "gestures": {"mini_wave_hello": "…", "clap_cheer": "…"}}}

The keys of gestures are the names you play, for example mini_wave_hello or clap_cheer.

2. Play one

Cloud avatar (AvatarSession(avatar_id=…)): send the avatar participant a trigger_dynamics call.

import json

resp = await ctx.room.local_participant.perform_rpc(
    destination_identity=avatar.avatar_identity,
    method="trigger_dynamics",
    payload=json.dumps({"action": "mini_wave_hello"}),
)
# → {"action": "mini_wave_hello", "animation_triggered": true, "status": "success"}

Self-hosted avatar (AvatarSession(model_path="avatar.imx")): push a VideoControl to the runtime.

from bithuman import VideoControl

await avatar.runtime.push(VideoControl(action="mini_wave_hello"))

3. Wire it to your events

As an allow-listed tool, so the language model can ask for a gesture only from your set. This tool is for a self-hosted avatar (AvatarSession(model_path=…)). For a cloud avatar, put the perform_rpc call from step 2 in the tool body instead.

from livekit.agents import function_tool, RunContext

ALLOWED = {"mini_wave_hello", "clap_cheer", "thumbs_up_pulse"}

@function_tool()
async def play_gesture(context: RunContext, gesture: str) -> str:
    if gesture not in ALLOWED:
        return f"Unknown gesture. Options: {sorted(ALLOWED)}"
    await avatar.runtime.push(VideoControl(action=gesture, force_action=True))
    return f"Played {gesture}"

Other triggers work the same way: a LiveKit data message, a participant event or a timer calls the same line.

Check it worked

The cloud call returns animation_triggered: true; the avatar plays the clip, then returns to idle or speech.

Variations

VideoControl fieldEffect
force_action=Trueplay even if another gesture is running
stop_on_user_speech=Truestop the gesture when the user starts talking
stop_on_agent_speech=Truestop the gesture when the agent starts talking
target_video="<clip>"switch the idle loop to another base clip

avatar.runtime.interrupt() stops a gesture at once.

Troubleshooting

SymptomCauseFix
animation_triggered: falsethe name is not one of the avatar’s gestures, or it has nonelist the names (step 1); generate gestures with the Gestures API
Nothing plays on a self-hosted avatarthe .imx was downloaded before gestures were generateddownload the model again after generation

Next