Tier 2 · AI Core
API Reference

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

MethodReturnsNotes
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

MethodSignatureNotes
createcreate(name=None, start=None, end=None) -> nameTHE 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.
duplicateduplicate(name, new_name=None) -> nameCopies a clip and its full field set. Calls collection.refresh() internally, so the returned name is guaranteed live-queryable immediately after.
deletedelete(name) -> NoneRemoves the clip (and its underlying Maya node) entirely.

Editing fields

MethodSignatureNotes
renamerename(name, new_name)
set_rangeset_range(name, start, end)
set_statusset_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_checkedset_checked(name, checked)
set_colorset_color(name, r, g, b)
set_enable_layers / set_disable_layersset_enable_layers(name, layers)Replace semantics, not additive — passing [] clears the list, it doesn't no-op.
set_override_pathset_override_path(name, path)path=None/"" clears the override, falling back to the export pipeline's own path-resolution order.
set_loopset_loop(name, value)
set_export_curve_data / set_export_curve_data_headset_export_curve_data(name, value)Schema/UI fields; not currently acted on by the export pipeline.

Groups

MethodSignatureNotes
add_to_groupadd_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_groupremove_from_group(name, group_name)
create_groupcreate_group(group_name) -> group_nameRegisters 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

MethodSignatureNotes
list_anim_layerssee above
set_enable_layers / set_disable_layerssee aboveApplied at export time via layer_manager.apply_clip_layers() — see AnimHub.

Preview

MethodSignatureNotes
capture_previewcapture_preview(name) -> path | NonePlayblasts 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

MethodSignatureNotes
offset_all_keys_to_start_at_zerooffset_all_keys_to_start_at_zero(objects=None) -> (0, new_end) | NoneShifts 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_afteroffset_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

Safety model

The AI never touches the Maya scene directly. The flow, end to end:

  1. The user types a request in the AI Companion.
  2. The model returns text only — an explanation plus a proposed Python script. Nothing has run yet.
  3. The user reads the proposed script and clicks Run.
  4. core/ai/execute.py's run_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 whatever extra_namespace was supplied — for AnimHub, that's {"clips": ClipsAPI(self.clips)}.
That review step — a human reading the code before it runs — is the entire safety mechanism. There's no sandboxing, no restricted 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:

1

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.

2

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.

3

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.

4

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.

5

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

  1. Add the method to clips_api.py, delegating to the real implementation module (model.py, scene_retime.py, etc.) rather than reimplementing logic inline — ClipsAPI itself should stay a thin, curated wrapper.
  2. Add its signature and a one-line behavior note to the method reference table above.
  3. Add an entry to _ai_context_summary() in animhub/main_window.py describing the method and, if it has a real correctness trap, why the AI should call it instead of writing the equivalent by hand.
  4. Write a test in tests/test_clips_api.py covering 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.