ClipsAPI reference
dcc/maya/animation/clips_api.py — the
curated Python surface AnimHub hands to the AI Companion. This is
the actual capability boundary for AI-generated code touching clips:
the AI's generated script runs with an instance of ClipsAPI
in its exec namespace, never raw cmds. The boundary is
enforced by what's literally put in that namespace (see
Safety model below), never by asking the
model to behave itself.
If you're extending what the AI
Companion can do to a clip, this file is where that happens — add a
method here, document it in the
"concepts primer" section below (and
in main_window.py's _ai_context_summary()), and it
becomes something the model can call on the very next request.
Construction
from CoreTools.dcc.maya.animation.clips_api import ClipsAPI
api = ClipsAPI(collection) # collection: a model.ClipCollection instance
AnimHub binds a fresh one on every "Ask AI" click:
window.set_context(
context_provider=self._ai_context_summary,
extra_namespace_provider=lambda: {"clips": ClipsAPI(self.clips)},
on_run_complete=self._rebuild_rows)
Note it's a lambda, re-evaluated on every request — not a
one-time-constructed instance. That matters: self.clips (the
window's live ClipCollection) is looked up fresh each time the
AI panel needs a namespace, so it always wraps whatever the current
collection is, even across a scene change that replaced it.
Method reference
Every method below either finds a clip by name (raising
ValueError if it doesn't exist) or operates on the whole
collection. There is no method that takes a raw Maya node name — clips
are always addressed by their logical CF_ClipName, matching how
a human refers to them.
Reading
| Method | Returns | Notes |
|---|---|---|
list() | [{name, start, end, status, checked, groups, color, enable_layers, disable_layers, override_path, loop, export_curve_data, export_curve_data_head}, ...] | One dict per existing clip. status is the string name ("READY" / "IN_PROGRESS" / "IGNORE"), not the enum object. |
checked() | [name, ...] | Raw checkbox state only — does NOT exclude IGNORE-status clips. ClipCollection.checked() (used by the export pipeline) is the checked-and-exportable subset; this is intentionally the broader one. |
in_group(group_name) | [name, ...] | group_name == "All" returns every clip name, matching the UI's own "All" pseudo-group. |
list_groups() | [group_name, ...] | Only registered groups (see create_group below) — not every string that happens to appear in some clip's group membership. |
list_anim_layers() | [layer_name, ...] | Every animLayer in the scene, root layer first. |
Creating / removing
| Method | Signature | Notes |
|---|---|---|
create | create(name=None, start=None, end=None) -> name | THE way to make a new AH clip. name defaults to the next sequential "Clip001"-style name; start/end default to 0/1. There is no add() method — that name was hallucinated by the model once in practice. |
duplicate | duplicate(name, new_name=None) -> name | Copies a clip and its full field set. Calls collection.refresh() internally, so the returned name is guaranteed live-queryable immediately after. |
delete | delete(name) -> None | Removes the clip (and its underlying Maya node) entirely. |
Editing fields
| Method | Signature | Notes |
|---|---|---|
rename | rename(name, new_name) | |
set_range | set_range(name, start, end) | |
set_status | set_status(name, status) | status is case-insensitive, spaces-or-underscores ("ignore", "IGNORE", "Ignore", "in progress", "IN_PROGRESS" all work). Raises ValueError listing valid names on a bad value. |
set_checked | set_checked(name, checked) | |
set_color | set_color(name, r, g, b) | |
set_enable_layers / set_disable_layers | set_enable_layers(name, layers) | Replace semantics, not additive — passing [] clears the list, it doesn't no-op. |
set_override_path | set_override_path(name, path) | path=None/"" clears the override, falling back to the export pipeline's own path-resolution order. |
set_loop | set_loop(name, value) | |
set_export_curve_data / set_export_curve_data_head | set_export_curve_data(name, value) | Schema/UI fields; not currently acted on by the export pipeline. |
Groups
| Method | Signature | Notes |
|---|---|---|
add_to_group | add_to_group(name, group_name) | Tags a clip with a group name. Does NOT by itself make the group appear anywhere in the UI (no tab, not orderable) — see below. |
remove_from_group | remove_from_group(name, group_name) | |
create_group | create_group(group_name) -> group_name | Registers the group so it becomes real — orderable, eventually a UI tab. Tagging every clip in the scene with "Combat" via add_to_group alone would never make a "Combat" tab appear until create_group("Combat") also runs. |
Anim layers
| Method | Signature | Notes |
|---|---|---|
list_anim_layers | see above | |
set_enable_layers / set_disable_layers | see above | Applied at export time via layer_manager.apply_clip_layers() — see AnimHub. |
Preview
| Method | Signature | Notes |
|---|---|---|
capture_preview | capture_preview(name) -> path | None | Playblasts a small hover-preview thumbnail sequence. Returns None if the scene has never been saved — a preview lives next to the scene file, so this is the most common failure. |
Retiming
| Method | Signature | Notes |
|---|---|---|
offset_all_keys_to_start_at_zero | offset_all_keys_to_start_at_zero(objects=None) -> (0, new_end) | None | Shifts every keyframe on objects (or every animated object in the scene if omitted) so the earliest key lands at frame 0, in one safe pass over the underlying animCurve nodes. THE way to retime a scene to start at zero — see why this exists instead of letting the AI write it. Returns None if there's nothing keyed. |
offset_keys_after | offset_keys_after(threshold_frame, offset, objects=None) -> {"moved": int, "failed": [...]} | Shifts every keyframe at/after threshold_frame by offset frames, leaving anything earlier untouched — e.g. to make room for a newly-inserted clip mid-timeline. THE way to do a partial/range keyframe shift — a hand-written per-key cmds.keyframe loop, even a seemingly collision-safe descending-order one, can still hit Maya's own RuntimeError: Cannot move keys; this does one native bulk range-move per curve instead. failed can be non-empty even on an otherwise successful call — confirmed live Maya's own guard can still refuse a specific curve on a real production rig (an animLayer-blended curve is the leading suspect); each curve moves independently, so one refusal doesn't abort the rest. |
Explicitly absent
export()Absent — matches the deliberate scope decision that export stays a manual, human-triggered action (the Export FBX button). No method here can trigger a batch export, and the AI primer says so explicitly so the model doesn't go looking for one.add()Absent — seecreate()above.- Anything that touches raw
cmdsor scene geometry directly — if a request needs that, the model is expected to say it can't be done throughclips, not fall back to writing rawcmdscalls that bypass this layer entirely.
Safety model
The AI never touches the Maya scene directly. The flow, end to end:
- The user types a request in the AI Companion.
- The model returns text only — an explanation plus a proposed Python script. Nothing has run yet.
- The user reads the proposed script and clicks Run.
core/ai/execute.py'srun_generated_code(code, extra_namespace=None)executes it, wrapped in a single Maya undo chunk (cmds.undoInfo), in a namespace of{"cmds": cmds}merged with whateverextra_namespacewas supplied — for AnimHub, that's{"clips": ClipsAPI(self.clips)}.
cmds allow-list, no static analysis of the
proposed script. The ClipsAPI boundary exists so that the
common, expected actions go through a curated, tested path
instead of the model improvising raw node/attribute edits — but a
sufficiently instructed model can still call cmds directly
inside a proposed script, and a human still has to actually read
before clicking Run for the safety model to hold.
A failed run doesn't raise into the caller —
run_generated_code returns (False, traceback_string),
and the whole run (success or failure) is recorded in AI Companion's
persistent Script History (see AI
Companion), so a failed attempt is still there to study, not just
successful ones.
The concepts primer
main_window.py's _ai_context_summary() is sent as a
"concepts primer" alongside every request — plain text describing what
clips is and how to use it. This exists because leaving the
model to infer the right method from a vague description
reliably produces hallucinated methods and wrong assumptions. Five
real, reported failures shaped what the current primer says, in order:
Fabricated a raw node instead of calling create()
Asked to create a clip, the model fabricated a raw
cmds.createNode("network", ...) with hand-added
clipName/startFrame/endFrame attributes as a
stand-in — invisible to AnimHub, not a real clip. Fix: the primer
now lists every real method signature explicitly, and says outright
never to invent a raw-node representation of a clip.
Hand-wrote a retiming loop that doubled the offset
Asked to retime a scene, the model hand-wrote a keyframe-shift
loop that shifted every curve twice — once via each object's own
transform, once via the underlying animCurve nodes directly —
silently doubling the offset. Fix:
offset_all_keys_to_start_at_zero() was added specifically
so the model has a tested primitive to call instead.
Confused clips with Maya's own Trax cmds.clip()
Even with clips.create() spelled out, the model called
cmds.clip(...) — Maya's own built-in command for
its legacy Trax nonlinear-animation clip system, a completely
unrelated feature. Fix: the primer now explicitly disambiguates.
Hand-wrote a per-key move loop that hit Maya's own collision guard
Asked to shift keys mid-timeline (not from zero, at/after a
threshold frame), the model correctly avoided
offset_all_keys_to_start_at_zero() (wrong operation for
this) but hand-wrote its own per-key cmds.keyframe loop
instead — moving keys one at a time in descending order
specifically to dodge collisions, a reasonable-sounding approach
that still hit Maya's own RuntimeError: Cannot move keys.
Fix: offset_keys_after() was added, using one native
bulk range-move per curve instead of a per-key loop — confirmed
against a clean synthetic test curve.
The bulk-range-move fix wasn't complete either
Confirmed live on a real production rig (144 objects selected)
that Maya's own "Cannot move keys" guard can still reject the
bulk range-move for one specific curve (an animLayer-blended
curve is the leading suspect, not independently confirmed) —
something the synthetic test curve never exposed. The original
version let that one refusal abort the entire scene-wide
shift, discarding every other curve's already-succeeded move along
with it. Fix: each curve's move is now independently
try/excepted — offset_keys_after() returns which
curves moved and which failed instead of all-or-nothing.
If you add a new ClipsAPI method and skip updating the primer,
expect the model to either not use it or use something that looks
similar but isn't — write the primer entry at the same time you write
the method, not after something breaks.
Why this exists instead of letting the AI write it
offset_all_keys_to_start_at_zero() is worth calling out as
the general pattern for when to add a ClipsAPI method versus
leaving something to the model's own generated code: if an operation has
a real correctness trap that's easy to get subtly wrong, don't rely on
prompt wording to keep the model from falling into it. Give it a tested
function instead. The prompt can say "don't do X" as many times as you
like; a curated primitive means there's nothing to do X with.
Extending ClipsAPI for a new capability
- Add the method to
clips_api.py, delegating to the real implementation module (model.py,scene_retime.py, etc.) rather than reimplementing logic inline —ClipsAPIitself should stay a thin, curated wrapper. - Add its signature and a one-line behavior note to the method reference table above.
- Add an entry to
_ai_context_summary()inanimhub/main_window.pydescribing the method and, if it has a real correctness trap, why the AI should call it instead of writing the equivalent by hand. - Write a test in
tests/test_clips_api.pycovering the delegation.
Adding AI Companion support to a different app
ClipsAPI is AnimHub's own extension of a generic
mechanism any application can use — see
AI Companion's Embedding
section for AIPanel.set_context()'s full contract
(context_provider, extra_namespace_provider,
on_run_complete). A different app wanting its own curated AI
surface would write its own equivalent class (its own
TakesAPI-shaped wrapper over its own domain model) rather than
reusing ClipsAPI directly.