All posts

Animation analysis

/ project notes / draft

I wanted more than an animation name. Where does a foot touch the ground? When is a punch actually extending? Can the clip loop, and which parts of those answers can I check?

I started by baking the movement and rendering it. Then I gave annotation agents the measurements and images, saved their proposals, and reviewed them in later versions. Here are two clips that show what that process looked like.

First, bake and render

Before asking a model to describe anything, I needed a repeatable way to see the motion. I loaded the library clips with Nilo’s ufbx loader and played them on Y Bot, the native Mixamo rig. That avoided adding retargeting differences to the analysis.

I baked world-space bone positions at 30 frames per second. Alongside those poses, I measured the duration, root movement, foot height, limb speed, and the difference between the first and last pose. These measurements suggested candidate moments; they weren’t annotations yet.

The next step was rendering the baked poses as stick figures. I produced a contact sheet for the whole clip, plots of the measured signals, a view of the loop boundary, and a strip of nearby frames for each candidate. The models and the report could work from the same evidence.

Plant-pulling signal plots showing bone heights and speeds, with candidate IDs.
The plant-pulling clip’s measured signals. A low point or a speed peak gives me somewhere to look; it doesn’t decide what the movement means. Open the image for the full-size evidence.

Then, ask the models to annotate it

Each annotation agent worked on one clip. It received the facts, visual evidence, and an output contract: describe the purpose and style, choose a usable segment, place markers, explain rejected candidates, and record checks and issues.

I started with Claude Opus 5.5 and GPT-6.1 Sol, then went back over the clips with Opus. For the larger batch, I mostly used Claude Haiku 5.5, with a few retries using GPT-6.1 Sol and Sonnet.

After the first annotations, I used GPT-6 Luna through Decisions, OpenAI’s version of Jev. I used it to review individual markers: whether to keep them, when they happened, whether two markers were the same event, and which tags were missing.

I tried using Decisions for the first pass too, but it rejected too many candidates. I went back to having an agent annotate the clip first, then used Decisions to review individual markers.

Example 1: grabbing before pulling

“Kneeling Pulling Up a Plant” made the difference between a candidate and an event much clearer. In v1, both grabs were placed at 2.133 seconds, the lowest point of the hands. But the hands had already reached the ground and stayed there for about a second before the pull.

Kneeling Pulling Up a Plant / 1.033 sfrontside
1.033 / 4.733 s
0.00s1.00s2.00s3.00s4.00s4.733sgrabgrabotherotherreleasethrow
blue: left / orange: right / shaded: hitbox or timing window
TimeMarkerSideConfidence
grabright hand reaches the plant at ground level and stays put (grip assumed on arrival)right63%
grableft hand reaches the plant next to the right (grip assumed on arrival)left60%
otherpull begins: both hands at their low point, brace before the yankboth74%
otherplant uprooted: hands leave ground level, torso jerks backboth67%
releaseleft hand lets go so the right hand alone carries the plant into the toss (inferred, no prop)left40%2.800–3.100s window
throwright arm tosses the plant out to the side; fingers open by f102right64%

This starts on v2 with v1 overlaid in faded lines. The earlier grabs are around 2.13 seconds; the revised grabs are near 1.1 seconds. Select v1 or v2 to inspect each set of markers, or open Versions for the recorded changes.

In v2, the right grab moved to 1.033 seconds and the left grab to 1.100 seconds. The old time became “pull begins,” and another marker at 2.333 seconds described the uproot. A left-hand release was added before the toss; the old right-hand release at 3.600 seconds was removed because it described the follow-through rather than a separate let-go.

Candidate evidence around the right hand reaching ground level, used to move the grab to 1.033 seconds.
Evidence around the earlier right-hand arrival. This is where the revised annotation places the grab, rather than at the later low point. Open the image for the full-size evidence.
The plant-pulling contact sheet showing a reach, a hold near the ground, the pull, and a toss.
The full movement makes the hold before the pull visible. The skeleton contains no plant prop, so the grab and release interpretations remain inferred from the pose. Open the image for the full-size evidence.

This changed four markers into six and separated reaching, pulling, uprooting, and throwing. The later Decisions passes kept those times but revised confidence. The left release remained uncertain, falling to 29% in v4. Keeping that uncertainty was more useful than treating every event as equally established.

Example 2: from an impact to an interval

Cross Punch shows why the annotation format changed. The first version placed an impact marker at 0.367 seconds. The next version described a right-hand hitbox interval instead: start at 0.167 seconds, end at 0.367 seconds. It also kept a later marker for returning to guard.

Cross Punch / 0.167 sfrontside
0.167 / 0.867 s
0.00s0.25s0.50s0.75s0.867shitbox-hand-starthitbox-hand-endother
blue: left / orange: right / shaded: hitbox or timing window
TimeMarkerSideConfidence
hitbox-hand-startcross leaves guard on its committed straight pathright78%
hitbox-hand-endcross at full extension, before retractionright84%0.300–0.400s window
otherright hand back in guard (recovery done)right55%0.700–0.800s window

Try v1, then v2 with the previous-version overlay. The shaded orange band is the proposed active interval. Timing windows are shaded beneath their markers; low-confidence markers use dashed lines.

That interval is more useful for an attack than a single point. But it is still a proposal derived from the motion. The stick figures don’t establish collision volume, contact with another character, or how the attack feels in a game.

Cross Punch candidate c8 strip showing the right hand leaving guard, with the proposed start frame outlined in red.
Evidence for the beginning of the punch interval. The highlighted frame is where the hand starts its committed extension. Open the image for the full-size evidence.
Cross Punch contact sheet, showing the guard, extension, and recovery over the full clip.
The whole punch as a contact sheet. The surrounding movement helps distinguish the strike from tightening the guard or retracting the hand. Open the image for the full-size evidence.

Later versions kept the interval and revised the review information. v4 cleaned the purpose tag from cross to punch-cross. It wasn’t a new animation or a new timing decision; it was a vocabulary correction, recorded separately.

Why I kept the versions

I froze the first annotations and wrote later passes as new files. Each revision points to the version it started from and carries a list of changes. That made it possible to compare the actual markers, rather than overwrite the earlier answer and lose the reasoning.

I validated the annotation structure, time bounds, candidate IDs, evidence files, and required checks. Those checks can catch a timestamp outside the clip or a missing image. They don’t establish that every interpretation is correct.

Cross Punch makes that distinction visible: the Decisions review changed its stored full-playback check from pass to fail. Rendered stills aren’t proof of a full playback test, and a later model pass isn’t automatically a better source of truth.

What I took from it

The bake-and-render step gave the rest of the work something concrete to refer to. Instead of debating a description of a movement, I could open its signal plot, a candidate strip, or the frames around a loop boundary.

I also came away with a clearer distinction between finding a candidate and accepting an event. A fast hand might be a punch, a guard adjustment, or recovery. The measurement finds the moment; the surrounding motion helps decide what it means.

The versions made mistakes and disagreements useful. I could see a grab move by about a second, a point marker become an interval, or a tag get cleaned without pretending the whole annotation had been redone.

The next check belongs in actual use: connect these markers to playback, movement, and attack logic, then see where the annotations hold up and where they need another pass.

All posts