Docs index: /llms.txt · every page as Markdown: add .md

‹ Guides

Gestures (Essence 1)

Play a named gesture, like a wave or nod, when your code asks.

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).

Steps

3 steps

  1. 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.

    Expected

    A JSON body whose gestures object lists your gesture names, such as mini_wave_hello.

  2. 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"))
    Expected

    The cloud call returns animation_triggered: true, and the avatar plays the gesture.

  3. 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.

    Expected

    Asked for a gesture from your set, the language model calls play_gesture, and the avatar plays it.

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