Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ed5de7d
Add Flutter port: aval_graph, aval_format, and Rust aval_decode core
johndpope Jul 16, 2026
d9328dc
Add flutter helper scripts: setup, analyze, test
johndpope Jul 16, 2026
d48fda4
Add aval_player scheduler dependency modules and run.sh
johndpope Jul 16, 2026
e84a411
Add grass_rabbit Flutter example with graph-driven unit playback
johndpope Jul 16, 2026
cca1993
Port path-scheduler family to aval_player — Phase 2 complete
johndpope Jul 16, 2026
ace91cb
Add aval-content-pipeline skill
johndpope Jul 16, 2026
c1c746b
updates
johndpope Jul 16, 2026
6a6d3f7
Close Phase 2 exit criterion: golden-trace parity vs TS scheduler
johndpope Jul 16, 2026
67c4c0e
Merge remote-tracking branch 'upstream/main'
johndpope Jul 16, 2026
337a236
Realign aval_decode to format-1.0 chunk protocol (H.264-only decode)
johndpope Jul 17, 2026
1d40b86
ok
johndpope Jul 17, 2026
fdefdfe
aval_format: rewrite 1.0 public barrel + fix codec_string import
johndpope Jul 17, 2026
f69c7d7
updates
johndpope Jul 17, 2026
188c8e4
updates
johndpope Jul 17, 2026
0693d1a
ok
johndpope Jul 17, 2026
8d3355c
updates
johndpope Jul 17, 2026
e5a5f32
flutter/grass_rabbit: GPU compositor + VideoToolbox decode + UX fixes
johndpope Jul 17, 2026
78b6097
flutter: web/WASM support + extract aval_flutter widget package
johndpope Jul 17, 2026
82cc1b9
feat(graph): rings and chained turn-step traversal
johndpope Jul 26, 2026
1308c98
feat(format): carry rings and turn membership in the compiled manifest
johndpope Jul 26, 2026
4f099bc
feat(compiler): expand rings into turn edges with V1-V8 diagnostics
johndpope Jul 26, 2026
7d68d9f
feat(player-web,element): expose rings, planFor, and the turnstep event
johndpope Jul 26, 2026
4aa5d4a
docs: document rings, turn edges, planFor, and the turnstep event
johndpope Jul 26, 2026
c562a10
feat(player-web): accept turnPolicy so a host can honour reduced motion
johndpope Jul 26, 2026
d7a71c5
fix(graph,format): key ring adjacency pairs without a raw control cha…
johndpope Jul 26, 2026
271ce5d
fix(compiler): strip ring metadata fields from compiled edge output
johndpope Jul 26, 2026
3b9645d
feat(rings): Dart/Rust planFor + Flutter climbing atlas with L/R/Up j…
johndpope Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
94 changes: 94 additions & 0 deletions .claude/skills/aval-content-pipeline/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
name: aval-content-pipeline
description: Production pipeline for authoring AVAL interactive video assets from AI-generated video (Grok Imagine or similar). Use when creating/planning .avl content, writing motion.json state graphs, crafting video-generation prompts, fixing loop seams or transition pops, or setting up the generate→normalize→compile→preview loop.
---

# AVAL Content Production Pipeline

Guidance for producing `.avl` interactive video assets from AI-generated
clips. The format's core constraint drives everything: units are frame
ranges of source video, transitions commit at portal frames with
`exact-authored` continuity — **the player cuts, it never blends**. All
production work reduces to making boundary frames pixel-matched.

## Format budgets (hard caps per asset, lower-only overrides)

32 states · 64 edges · 96 units · 16 ports/body · 32 input bindings ·
4 renditions · one fixed frame rate per asset (grass-rabbit: 24fps).
Source: `packages/format/src/constants.ts` (`FORMAT_DEFAULT_BUDGETS`),
`packages/graph/src/limits.ts` (`GRAPH_LIMITS`).

## Workflow order

1. **Graph first.** Write `motion.json` before generating any video.
For each interactive behavior author the triple `X-in` (finite),
`X-loop` (loop), `X-out` (finite), plus one shared `idle-loop` and an
optional `intro` one-shot. Template: `examples/grass-rabbit/motion.json`
(5 units, 4 states, 5 edges — copy the folder, swap ranges/source).

2. **Hub-pose pattern** (the key scaling trick). Every clip starts AND
ends at one neutral "hub" pose. Idle = hub→subtle motion→hub; each
gesture = hub→action→hub. All boundaries then match by construction,
portal frames are trivial, and each new gesture costs one generation +
one `units[]` entry. This is what makes 100s-of-gestures characters
(talking heads etc.) feasible within the portal model.

3. **Generation (Grok Imagine or similar):**
- Generate the hub still FIRST (image mode). It is the conditioning
anchor for every clip — never regenerate it mid-project.
- Always image→video conditioned on the hub still, never text→video
(text-only never reproduces a matching first frame).
- When a unit must end somewhere new (e.g. `hover-in` ending at the
hover pose), chain: last frame of clip A becomes the image
conditioning for clip B.

4. **Prompt template** — every prompt needs all four ingredients:
- Camera lock: "static locked-off camera, tripod shot, no camera
movement, no zoom, no pan"
- Single action: "single continuous shot, no cuts" + exactly one
described motion
- Explicit start AND end pose: "begins motionless in the exact
starting pose … returns to the exact same resting pose and holds
still"
- Stability: "consistent lighting, background unchanged"
For loops add: "subtle idle motion, seamless loop, minimal movement,
character stays in place".

5. **Post-processing:**
- Normalize: `ffmpeg -i clip.mp4 -r 24 -vf scale=1280:720 …` (one
fps/resolution across all clips before compiling).
- Cut at true pose matches, not clip ends: find each clip's
best-matching frame against the hub still (per-frame diff) and trim
there — AI clips rarely end exactly where prompted.
- Loop seams: ping-pong (forward+reversed) for subtle non-directional
idle motion, or optical-flow blend the seam
(`ffmpeg -vf minterpolate`) over the last/first few frames.
- Color-match every clip against the hub still (shared LUT) —
inter-clip color drift is the most common visible transition artifact.
- Concatenate to ONE master mp4 in unit order; the frame ranges become
`units[].range` (grass-rabbit's `sources[0]` is one continuous mp4).

6. **Compile & preview loop:**
- `avl compile motion.json --out public/<name>.avl --force`
(see `examples/grass-rabbit/package.json` scripts).
- Preview in the web playground/example first (instant); check the
Flutter example (`flutter/scripts/run.sh`) after.
- The compiler enforces budgets and continuity — treat its errors as
the authority on legal boundaries.

## Debugging transition pops

- Pop at portal: boundary frames don't match — re-trim both clips to the
hub frame, or re-chain clip B from clip A's actual last frame.
- Pop only in color/brightness: color drift — LUT-match the clips.
- Loop "breathes" or jumps: loop seam — ping-pong or minterpolate, or
regenerate with a stronger "minimal movement" prompt.
- Compiler rejects a range: frame math off by one — ranges are
[start, end) frame indices into the master source at the asset fps.

## First-project checklist

Copy `examples/grass-rabbit/` → make hub still → generate idle-loop +
one gesture clip → normalize/trim/concat → edit ranges in motion.json →
compile → playground → iterate. Scale out gestures only after the
3-state graph plays cleanly.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ test-results/
output/playwright/
*.tsbuildinfo
.DS_Store
/flutter/examples/grass_rabbit/assets/mansion-woman.avl
/examples/mansion-woman
7 changes: 7 additions & 0 deletions docs/accessibility-and-motion.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ capabilities and resources permit it. Reduced motion still processes authored
state changes and bindings, but an infinite body does not advance. The host
fallback must convey an acceptable non-animated meaning.

A player embedding the runtime directly can also set `turnPolicy: "direct"`, so a
multi-step request along a [ring](./project/1.0.md#rings-and-turn-edges) lands in
its target without playing the intermediate states. One `turnstep` still reports
the final landing, which keeps a host's ring bookkeeping identical under either
policy. `chain` is the default and the only policy that preserves frame
continuity across the whole arc.

Light-DOM fallback remains usable without JavaScript. Supply meaningful
alternative text when motion carries information, and empty alternative text
when it is decorative. The element does not capture keyboard events, suppress
Expand Down
20 changes: 14 additions & 6 deletions docs/element-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@ Assets are literal direct-child `<source>` elements. Each requires `src` and
`type='application/vnd.aval; codecs="..."'`; optional integrity applies to that
source alone. Child order is preference order.

Core methods are `prepare()`, `setState()`, `send()`, `readyFor()`, `pause()`,
`resume()`, `getDiagnostics()`, and terminal `dispose()`. Runtime state is read
through `readiness`, `mode`, `staticReason`, `requestedState`, `visualState`,
`isTransitioning`, `paused`, `effectivelyVisible`, `stateNames`, `eventNames`,
and `inputBindings`.
Core methods are `prepare()`, `setState()`, `send()`, `readyFor()`,
`planFor()`, `pause()`, `resume()`, `getDiagnostics()`, and terminal
`dispose()`. Runtime state is read through `readiness`, `mode`, `staticReason`,
`requestedState`, `visualState`, `isTransitioning`, `paused`,
`effectivelyVisible`, `stateNames`, `eventNames`, `inputBindings`, and `rings`.

`rings` lists the ordered state axes the asset declares, each as
`{ id, states, cyclic }`. `planFor(state)` is a dry run of `setState(state)`: it
returns the landings that request would visit, in order, `[]` when the state is
already held, or `null` when there is no route today. It never advances the
graph.

Events are non-cancelable `CustomEvent` instances with immutable bounded
details: `readinesschange`, `requestedstatechange`, `visualstatechange`,
`transitionstart`, `transitionend`, `underflow`, `fallback`, and `error`. Every
`transitionstart`, `transitionend`, `turnstep`, `underflow`, `fallback`, and
`error`. `turnstep` fires once per landing while a request walks a ring, with
`{ ring, from, to, remaining }`; its pixels are already drawn when it fires. Every
event except `error` bubbles and is composed. Listen for `error` directly on
the element; keeping that event local follows native media behavior and avoids
colliding with page-wide error handlers. Every detail includes a positive
Expand Down
12 changes: 12 additions & 0 deletions docs/format/1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ Each codec alternative is a separate asset with its own integrity digest.
Ordered alternatives are an HTML authoring concern; wire 1.0 deliberately
does not impose cross-file identity or fallback policy.

## Rings

A manifest may declare an optional `rings` array: ordered, optionally cyclic
axes of states whose ordered neighbour pairs each have an edge. Edges may carry
`ring`, a signed `step`, and `derived: true` when the compiler expanded them from
a ring rather than the author writing them.

Rings are validated as fully walkable — every member resolves to a state, every
ordered adjacency has an edge in both directions, and no two rings claim the same
pair — so a player never discovers a missing step at request time. The key is
omitted entirely by assets which author no ring, which keeps assets compiled
before rings existed byte-identical.
53 changes: 53 additions & 0 deletions docs/project/1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,56 @@ lookahead, hidden references, and multi-output chunks without changing the
authored presentation timeline or creating dependencies across unit
boundaries.

## Rings and turn edges

A **ring** is an ordered, optionally cyclic set of states along one axis — eight
compass facings, a zoom ladder, a dial. Rather than authoring an edge for every
ordered pair, author the axis once and let the compiler expand it:

```jsonc
{
"rings": [
{
"id": "facing.walk",
"states": ["walk_n", "walk_ne", "walk_e", "walk_se",
"walk_s", "walk_sw", "walk_w", "walk_nw"],
"cyclic": true,
"tieBreak": "forward",
"turn": {
"mode": "cut",
"start": {
"type": "portal", "sourcePort": "default",
"targetPort": "default", "maxWaitFrames": 8
},
"continuity": "exact-authored"
},
"maxChainedSteps": 4,
"overrides": [
{ "from": "walk_nw", "to": "walk_n", "mode": "unit", "unit": "pivot.nw.n" }
]
}
]
}
```

`avl compile` derives one ordinary edge per ordered neighbour pair — 16 for the
eight-way cyclic ring above — so nothing downstream needs to understand rings in
order to play them. Derived edges are named `<ring>.<from>.<to>` with the prefix
every member shares removed (`facing.walk.n.ne`), and carry `derived: true`, which
`avl inspect` reports alongside `ring` and `step`.

`turn.mode` decides what a step plays: `cut` steps straight from one body to the
next at the authored portal boundary, while `unit` plays a bridge clip supplied
per step by an `overrides` entry. `turn.start` is the departure policy every step
shares. Precedence is **explicit edge > ring override > ring default**: an
authored edge between two ring neighbours keeps its own behavior, and the
shadowed step is reported as a build warning rather than silently dropped.

Compilation fails, naming the ring and the states involved, when a member is not
a state, a member repeats, a ring is too short to be an axis (2) or to close a
cycle (3), `mode: "unit"` has no unit for every step, an override or turn edge is
not an adjacency, two rings claim the same pair, or a step's two bodies share no
compatible port.

`maxChainedSteps` is the longest arc the runtime will walk; see
[states and triggers](../states-and-triggers.md) for how a request chooses one.
24 changes: 24 additions & 0 deletions docs/states-and-triggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,27 @@ Partial loops, finite bodies, held bodies, portals, finish routes, locked
bridges, cuts, and resident reversible transitions are compiled graph behavior.
They are not implemented by seeking a video element, so a loop seam does not
pause for a media seek.

## Rings: one request, several steps

When an asset declares a ring, the states on it are one axis rather than a set of
unrelated destinations. `setState()` on a ring member walks the shorter arc one
authored step at a time:

```ts
motion.planFor("walk_e"); // ["walk_ne", "walk_e"] from walk_n
await motion.setState("walk_e");
```

Distances wrap only on a cyclic ring, equal-length arcs are decided by the ring's
`tieBreak` (so a half turn is deterministic), and an arc longer than the ring's
`maxChainedSteps` rejects with `RouteError` instead of walking further than the
author allowed. An explicit edge between two ring neighbours always wins over the
derived step.

Each landing dispatches `turnstep` with `{ ring, from, to, remaining }`. The plan
is replanned at every step boundary: a new `setState()` mid-arc lets the step in
flight finish, then departs on a fresh arc from the state that actually landed —
so a rapid sweep produces continuous motion, with each superseded request
rejecting `AbortError` exactly as it does off a ring. No step seeks media; every
seam lands on frame 0 of the next body.
29 changes: 25 additions & 4 deletions etc/api/compiler.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export interface AssetInspection {
readonly codec: VideoCodec_2;
// (undocumented)
readonly digestClaim: "all-internal-and-whole-file";
readonly edges: readonly {
readonly id: string;
readonly from: string;
readonly to: string;
readonly ring?: string;
readonly step?: 1 | -1;
readonly derived?: true;
}[];
// (undocumented)
readonly file: string;
// (undocumented)
Expand All @@ -97,6 +105,14 @@ export interface AssetInspection {
readonly alphaLayout: AlphaLayout;
}[];
// (undocumented)
readonly rings: readonly {
readonly id: string;
readonly states: readonly string[];
readonly cyclic: boolean;
readonly tieBreak: "forward" | "backward";
readonly maxChainedSteps: number;
}[];
// (undocumented)
readonly sha256: string;
// (undocumented)
readonly states: readonly string[];
Expand Down Expand Up @@ -732,7 +748,6 @@ export interface NormalizedSourceProject {
readonly bindings: readonly SourceBinding[];
// (undocumented)
readonly canvas: Canvas;
// (undocumented)
readonly edges: readonly SourceEdge[];
// (undocumented)
readonly encodings: readonly NormalizedVideoEncoding[];
Expand All @@ -742,6 +757,9 @@ export interface NormalizedSourceProject {
readonly initialState: string;
// (undocumented)
readonly projectVersion: "1.0";
readonly ringNotes?: readonly string[];
// Warning: (ae-forgotten-export) The symbol "SourceRing" needs to be exported by the entry point index.d.ts
readonly rings?: readonly SourceRing[];
// (undocumented)
readonly sources: readonly SourceDescriptor[];
// (undocumented)
Expand Down Expand Up @@ -845,8 +863,10 @@ export type SourceDescriptor = {
readonly frameCount: number;
};

// Warning: (ae-forgotten-export) The symbol "SourceTurnMembership" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export type SourceEdge = {
export type SourceEdge = (SourceTurnMembership & {
readonly id: string;
readonly from: string;
readonly to: string;
Expand All @@ -857,7 +877,7 @@ export type SourceEdge = {
readonly transition?: SourceTransition;
readonly continuity: "exact-authored" | "exact-reverse";
readonly targetRunwayFrames?: never;
} | {
}) | (SourceTurnMembership & {
readonly id: string;
readonly from: string;
readonly to: string;
Expand All @@ -868,7 +888,7 @@ export type SourceEdge = {
readonly transition?: never;
readonly continuity: "cut";
readonly targetRunwayFrames: number;
};
});

// @public (undocumented)
export interface SourcePort {
Expand Down Expand Up @@ -898,6 +918,7 @@ export interface SourceProject {
readonly initialState: string;
// (undocumented)
readonly projectVersion: "1.0";
readonly rings?: readonly SourceRing[];
// (undocumented)
readonly sources: readonly SourceDescriptor[];
// (undocumented)
Expand Down
12 changes: 12 additions & 0 deletions etc/api/element.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export interface AvalDiagnostics {
readonly requestedState: string | null;
// (undocumented)
readonly resizeGeneration: number;
// Warning: (ae-forgotten-export) The symbol "AvalRing" needs to be exported by the entry point index.d.ts
//
// (undocumented)
readonly rings: readonly Readonly<AvalRing>[];
// (undocumented)
readonly runtime: Readonly<{
selectedRendition: string | null;
Expand Down Expand Up @@ -208,6 +212,8 @@ export interface AvalElement extends HTMLElement {
// (undocumented)
readonly paused: boolean;
// (undocumented)
planFor(state: string): readonly string[] | null;
// (undocumented)
prepare(options?: Readonly<AvalPrepareOptions>): Promise<RuntimeReadinessResult>;
// (undocumented)
readonly readiness: RuntimeReadiness;
Expand All @@ -224,6 +230,8 @@ export interface AvalElement extends HTMLElement {
// (undocumented)
resume(): Promise<void>;
// (undocumented)
readonly rings: readonly Readonly<AvalRing>[];
// (undocumented)
send(event: string): boolean;
// (undocumented)
setState(name: string): Promise<void>;
Expand Down Expand Up @@ -280,6 +288,10 @@ export interface AvalElementEventMap {
readonly transitionend: CustomEvent<Readonly<AvalTransitionDetail>>;
// (undocumented)
readonly transitionstart: CustomEvent<Readonly<AvalTransitionDetail>>;
// Warning: (ae-forgotten-export) The symbol "AvalTurnStepDetail" needs to be exported by the entry point index.d.ts
//
// (undocumented)
readonly turnstep: CustomEvent<Readonly<AvalTurnStepDetail>>;
// (undocumented)
readonly underflow: CustomEvent<Readonly<AvalUnderflowDetail>>;
// (undocumented)
Expand Down
Loading