Skip to content

Actors — Movement & Dialog

A Character (minted from a rig, e.g. bob = brian("Bob")) is what you direct in a shot. Its methods only record intent inside with shot:; the keyframes are built when the block closes. This page covers every way an actor can move, act, and speak.

The action methods at a glance

Method What it does
idle() hold a neutral pose (inside a shot: holds until the shot ends)
say(line, action_name=…) speak a line — TTS audio + a chosen gesture. One speaker per shot.
walk_to(point) walk to a point/anchor (looped locomotion + root motion)
move_to(point, action=…) like walk_to, but choose the gait
walk_to_join(target, blocking, start=…) walk into a 2-person formation beside another actor
walk_to_shot(anchor) walk to a metadata shot anchor
.actor.perform("<name>") play any of the ~100 animations in place (escape hatch)
.actor.face_towards(p) · .actor.set_forward_axis(ax) orientation helpers

Movement

walk_to — travel to a point or anchor

Looped walking gait + root motion, from the current position to a point or set anchor.

with canal_bridge as shot:
    shot.add(sam, at=canal_bridge.Canal_Bridge1_North_End.D1)
    sam.walk_to(canal_bridge.Canal_Bridge3_South_End.F1)
    shot.camera("tracking_moving", mode="medium", R=4.0)
    shot.clip(5.0)
start (north end) end (south end)
walk start walk end

move_to — choose the gait

move_to is walk_to with a selectable locomotion action:

sam.move_to(target, action="jogging")     # or "sprint", "sneak_walk", "crouch_walking", …

Every locomotion gait, verified for travel (the LOCOMOTION_ANIMS whitelist in dsl/actors.py):

Gait Travel Gait Travel Gait Travel
walking happy_walk running ⚠️ broken
walk_forward sad_walk run_forward
walking_backwards drunk_walk sprint
sneak_walk ✅ (low, by design) female_walk fast_run
crouch_walking ✅ (low, by design) strut_walking jogging
injured_walking zombie_walk injured_run

17 of 18 gaits travel correctly. Only running fails.

jogging running ⚠️ (collapses)
jogging running broken

action="running" is broken — use another run gait

move_to(action="running") progressively collapses the actor (lean → crouch → horizontal), and a longer clip is worse. It's specific to running via move_torunning in place is fine. For run-travel use jogging, sprint, fast_run, or run_forward. See Known Issues #9.

walk_to_join — enter a formation

The walker walks into one slot of a 2-person blocking; the target is placed at the other.

walker.walk_to_join(target, face_to_face(gap=1.6), start=(0, -6, 0), at=(0, 0, 0))

walk_to_join

Give it room to walk

start defaults to the anchor (right next to the formation → a tiny step). Pass an explicit start= a few units away for a real entrance, as above.

walk_to_shot

Convenience that walks the actor to a metadata shot anchor (same as walk_to(anchor)):

sam.walk_to_shot(canal_bridge.Tree2.C1)

Standing actions — any of the ~100 animations

idle

bob.idle()          # inside a shot, holds the pose until the shot ends

perform — the escape hatch for any animation

Character exposes idle/say/walk_to/move_to; to play any other action (gestures, combat, sit, dance, …) reach the underlying actor and mark it so the auto-idle isn't layered on:

bob.actor.perform("thinking", duration=48)
shot.mark_actor_has_action(bob)

The full list + a per-family gallery are on the Animation page.


Dialog (say)

say() synthesizes per-character TTS audio, plays action_name while speaking, and muxes the audio into the clip (cached on disk by a hash of the text). Only one actor may speak per shot.

A gesture while speaking — action_name

detective.say("We need to talk. Right now.", action_name="talking")   # gestures
witness.say("...", action_name="idle")                                # just speaks, still
action_name="talking" action_name="idle"
say talking say idle

Per-character voices

Voice is split from the rig: each character has a curated default_voice + tone, but you pick either at cast time. Each OpenAI voice is a gender-tagged singleton (full list in the Voice Catalogue); tone is the character's instructions.

Define once at the top, then reuse in every shot (the recommended pattern):

from dsl.actors import james, megan
from dsl.voices import echo, nova            # gender-matched voices

marcus = james("Marcus", voice=echo)         # male rig + male voice
nina   = megan("Nina",  voice=nova)

# … then in any shot:
marcus.say("We need to talk.", action_name="talking")

Or override inline for a one-off — by Voice singleton or string name, plus a delivery note:

brian("Detective Hayes")                                  # default voice + tone
brian("The Stranger", voice="onyx")                       # string lookup
brian("Hayes (paranoid)", instructions="Whisper. Paranoid, glancing back.")  # same rig+voice, new delivery
brian("The Stranger", voice=onyx, instructions="Cold. Clipped. Military.")    # both overridden

by_gender("male") returns the gender-matched voices (plus neutral) for filtered casting, and the same rig can back different voices/tones for different characters in one scene.

The casting dials:

Dial Type Default What it does
voice Voice · str · None character's default_voice The voice timbre — a singleton (echo) or its name ("echo").
instructions str · None character's default_instructions Tone / delivery direction handed to the TTS (e.g. "Whisper. Paranoid.").
default_voice (in character_descriptions.json) per character The curated fallback voice; gender-matched across all 36 rigs.
by_gender(g) helper → list[Voice] Voices of gender g plus neutral, for filtered casting.

second voice

Voice/gender mismatch — fixed

Voice + gender are now first-class, so a male character can't silently get a female voice — this closed Issue #12. The three original mispairings (james→echo, peasant_girl→coral, alex→verse) are corrected and all 36 character↔voice pairings are gender-matched.

One speaker per shot

A second say() in the same shot raises. Put each speaker in their own shot and cut them together (see Editing).


Orientation helpers

Lower-level MixamoActor controls for facing:

sam.actor.face_towards((3, 0, 0))     # rotate to face a point
sam.actor.set_forward_axis("-Y")      # fix a rig whose "forward" isn't -Y

Parameter reference & tuning

Every movement/dialog method funnels through actor.perform(...) — these are its dials:

Dial Type Default What it does
action_name / action str "walking" (move_to) · "idle" (say/idle) Which of the ~100 animations plays. In move_to it's the gait; in say it's the gesture while speaking ("talking" to gesture, "idle" to stay still).
to (x,y,z) / anchor None Travel target. Present → locomotion (the gait loops + root-moves there); absent → the action plays in place.
duration int frames None Explicit length. Honoured for locomotion; ignored for in-place gestures (reset to the action's native length — #10). Bound the shot with shot.clip() instead.
motion_scale float (units / loop) actor's motion_scale Stride length — distance one locomotion cycle covers (repeat = distance / motion_scale). Larger = fewer loops, longer strides (faster ground cover); smaller = more loops, busier footwork.
dialog str None A TTS line spoken during the action (talk-while-walking). One-speaker-per-shot still applies.
hold bool per-action Whether the actor holds the last pose after the action ends (vs. the strip emptying). Set False for a hard stop.
gap int frames 0 A pause inserted after this action before the next starts — spacing for sequential actions on one actor.
  • say(line, action_name=…, duration=…) = perform(action_name, dialog=line, duration=…). Clip length follows the gesture animation, not the speech (#8) — split long lines (which also re-triggers the gesture so it doesn't freeze, #19).
  • move_to(location, action="jogging", motion_scale=…) — pick the gait and tune the stride; walk_to is move_to fixed to walking.

Combining these into directed moments: see Production-Quality Tuning.


Caveats

Three things to know

  • move_to(action="running") is the one broken gait — use jogging / sprint / fast_run instead (#9).
  • Dialog clip length follows the talking animation, not the speech (#8).
  • perform(duration=…) is ignored for in-place actions — bound a shot with shot.clip() (#10).