cutan.expression

Facial expression for the cutout face (an#98, epic #9 Wave 6).

The vocabulary (axes), our presets (presets), how they reach a character’s slots and which mouth set a line uses (binding), the provider seam that turns authored leaves and dialogue sugar into per-frame curves (provider), and the 52-coefficient import/export mapping (blendshapes). Renderer-free throughout: the cutout compiler’s face solver consumes these; an validate and an character validate share the same resolution.

Functions

binding_for(desc)

The descriptor's declared expression_binding (additive field), else the default.

clamp_axes(values)

Clamp every numeric axis to its range; an unknown axis is an error.

declared_mouth_variants(desc)

{form: set name} for every viseme@<form> set the descriptor declares.

default_binding(desc)

The binding the default rig implies, from the slots it actually has.

expression_problems(desc, *, preset[, axes])

Every reason an expression cannot resolve on desc — empty means it can.

expression_spans(shot, entity_id)

Every expression contributor on entity_id: authored leaves, then dialogue sugar.

from_blendshapes(coefficients)

Fold unipolar coefficients onto the axes (summed, then clamped).

known_presets()

The preset names, in declaration order.

lid_key(value, *, available)

The eyelid key a lid state selects, degraded to the art the rig declares.

mouth_form_of(preset)

The viseme@<form> a preset prefers, or None for the neutral set.

preset_axes(preset, *[, axes, intensity])

The numeric axis offsets an expression asks for: the preset's, with axes layered over them, scaled by intensity and clamped.

resolve_mouth_set(desc, preset, *, keys_used)

Which mouth set a line under preset uses — the one chain, shared.

variant_set_name(form)

The swap-set name for a mouth form (@ is a legal set-name character).

Classes

Axis(name, lo, hi[, rest])

One numeric axis: its range and its rest (neutral) value.

AxisCurve(axis, samples)

One axis sampled at the frame times 0, 1/fps, …, n/fps (offline, deterministic).

ChannelBinding(axis, slot, property, gain[, ...])

A numeric axis driving one transform property of one slot's node.

DefaultExpressionProvider()

Sum of the shot's expression spans on the entity, ramped, per frame.

ExpressionProvider(*args, **kwargs)

The seam: whatever produces per-axis curves for one entity of one shot.

ExpressionSpan(start, end, preset[, axes, ...])

One expression contributor on one entity, in absolute shot time.

Preset(name[, axes, mouth_form, anchor])

A named expression: axis offsets, the mouth form it prefers, its anchor.

SetBinding(axis, slot[, set_family])

A lid axis driving one slot's swap set through the ladder.

Exceptions

ExpressionResolutionError(who, problems)

An expression that cannot resolve on a character; problems says why.

class cutan.expression.Axis(name, lo, hi, rest=0.0)[source]

Bases: object

One numeric axis: its range and its rest (neutral) value.

class cutan.expression.AxisCurve(axis, samples)[source]

Bases: object

One axis sampled at the frame times 0, 1/fps, …, n/fps (offline, deterministic).

class cutan.expression.ChannelBinding(axis, slot, property, gain, rig_scaled=False)[source]

Bases: object

A numeric axis driving one transform property of one slot’s node.

rig_scaled: bool = False

Whether the gain is a view-box length (scaled by the rig factor).

class cutan.expression.DefaultExpressionProvider[source]

Bases: object

Sum of the shot’s expression spans on the entity, ramped, per frame.

mouth_preset_at(shot, entity_id, t)[source]

The preset whose mouth form is in force at t: the heaviest span at t that prefers a form, or None (the neutral set).

Whole-line by construction when called at a line’s start — the solver asks once per line, never per frame, so at most one mouth swap property is live per instant.

Return type:

str | None

class cutan.expression.ExpressionProvider(*args, **kwargs)[source]

Bases: Protocol

The seam: whatever produces per-axis curves for one entity of one shot.

exception cutan.expression.ExpressionResolutionError(who, problems)[source]

Bases: ValueError

An expression that cannot resolve on a character; problems says why.

class cutan.expression.ExpressionSpan(start, end, preset, axes=dict(), intensity=1.0, blend=0.0, source='action')[source]

Bases: object

One expression contributor on one entity, in absolute shot time.

offsets()[source]

The unscaled axis offsets this span asks for.

Return type:

dict[str, float]

source: str = 'action'

"action" for an authored leaf, "dialogue" for the [emotion] sugar.

weight_at(t)[source]

The ramped intensity at t: 0 outside, ramping over blend at each end.

Return type:

float

class cutan.expression.Preset(name, axes=dict(), mouth_form=None, anchor='')[source]

Bases: object

A named expression: axis offsets, the mouth form it prefers, its anchor.

anchor: str = ''

FACS AU cross-reference (a comment, never a source).

mouth_form: str | None = None

The viseme@<form> set this preset’s mouth prefers; None = viseme.

class cutan.expression.SetBinding(axis, slot, set_family='eyelid')[source]

Bases: object

A lid axis driving one slot’s swap set through the ladder.

cutan.expression.binding_for(desc)[source]

The descriptor’s declared expression_binding (additive field), else the default.

A declared binding is a list of dicts in the two dataclasses’ shapes ({"axis", "slot", "property", "gain"[, "rig_scaled"]} or {"axis", "slot", "set_family"}). An unknown axis in it is an error.

Return type:

list[Union[ChannelBinding, SetBinding]]

cutan.expression.clamp_axes(values)[source]

Clamp every numeric axis to its range; an unknown axis is an error.

>>> clamp_axes({"brow_height_l": 2.0, "lid_open_r": -3.0})
{'brow_height_l': 1.0, 'lid_open_r': -1.0}
>>> clamp_axes({"eyebrow": 1.0})
Traceback (most recent call last):
...
ValueError: unknown expression axis 'eyebrow' (known: brow_angle_l, ...)
Return type:

dict[str, float]

cutan.expression.declared_mouth_variants(desc)[source]

{form: set name} for every viseme@<form> set the descriptor declares.

>>> declared_mouth_variants(CharacterDescriptor(name="m"))
{}
Return type:

dict[str, str]

cutan.expression.default_binding(desc)[source]

The binding the default rig implies, from the slots it actually has.

The brow angle’s screen sign: PixiJS rotation is clockwise-positive with y down, so on the LEFT brow (screen-left) a clockwise turn drops the inner end while on the RIGHT brow it lifts it — the axis says “+ = inner end up”, hence -travel on the left and +travel on the right.

Return type:

list[Union[ChannelBinding, SetBinding]]

cutan.expression.expression_problems(desc, *, preset, axes=(), who)[source]

Every reason an expression cannot resolve on desc — empty means it can.

Shared by an validate (each becomes an error Finding) and the compiler (which raises ExpressionResolutionError with the same list).

>>> expression_problems(CharacterDescriptor(name="m"), preset="joyful", who="m")
["unknown expression preset 'joyful' (known: neutral, happy, sad, angry, surprised, afraid, disgusted, thinking, skeptical, amused)"]
>>> expression_problems(CharacterDescriptor(name="m", face_overlay=False), preset="happy", who="m")[0].startswith("'m' has its face baked")
True
Return type:

list[str]

cutan.expression.expression_spans(shot, entity_id)[source]

Every expression contributor on entity_id: authored leaves, then dialogue sugar. duration=None runs to the shot end (the looping-play rule); a span never extends past the shot.

Return type:

list[ExpressionSpan]

cutan.expression.from_blendshapes(coefficients)[source]

Fold unipolar coefficients onto the axes (summed, then clamped).

Unknown names raise — a misspelt coefficient must not vanish quietly.

Return type:

dict[str, float]

cutan.expression.known_presets()[source]

The preset names, in declaration order.

>>> known_presets()[:3]
('neutral', 'happy', 'sad')
Return type:

tuple[str, ...]

cutan.expression.lid_key(value, *, available)[source]

The eyelid key a lid state selects, degraded to the art the rig declares.

wide above +0.25, open, half below −0.35, closed below −0.85; a rig without half stays open until the lower threshold and one without wide stays open above the upper one — never a blend of two drawings.

Return type:

str

cutan.expression.mouth_form_of(preset)[source]

The viseme@<form> a preset prefers, or None for the neutral set.

>>> mouth_form_of("amused"), mouth_form_of("thinking"), mouth_form_of(None)
('happy', None, None)
Return type:

str | None

cutan.expression.preset_axes(preset, *, axes=None, intensity=1.0)[source]

The numeric axis offsets an expression asks for: the preset’s, with axes layered over them, scaled by intensity and clamped. Only non-zero offsets are returned, so a neutral expression is {}.

An unknown preset or axis is a ValueError — validate reports it as an error, the compiler refuses it.

Return type:

dict[str, float]

cutan.expression.resolve_mouth_set(desc, preset, *, keys_used, who=None)[source]

Which mouth set a line under preset uses — the one chain, shared.

viseme@<form> if the preset prefers a form the descriptor declares and that set covers keys_used; else viseme with a warning naming what was missing; else ExpressionResolutionError. A descriptor with no viseme set and no covering variant cannot speak at all — that is the error, not a fallback.

Return type:

str

cutan.expression.variant_set_name(form)[source]

The swap-set name for a mouth form (@ is a legal set-name character).

>>> variant_set_name("happy")
'viseme@happy'
Return type:

str

Modules

axes

The facial expression axes: what a cutout face can be asked to do (an#98).

binding

How the axes reach a character: the binding and the mouth-set resolver (an#98).

blendshapes

The 52-coefficient blendshape vocabulary, as an import/export mapping (an#98).

presets

Expression presets: our art direction on the axes (an#98).

provider

The expression provider: authored leaves + dialogue sugar → per-axis curves (an#98).

registration

The face side of the cut-out genre, as declarations: expression and [emotion].