IPL — Interpretable Pose Latent
Open specification for a rig-agnostic, human-interpretable body-pose latent.
| Standard | IPL — Interpretable Pose Latent |
| Version | 1.1.2 |
| Released | 2026-10-15 |
| Status | Stable baseline for the current axis set. The parameter registry (§4) MAY be extended by a future major change (Y, §8). |
| Symbol | P (the IPL vector) |
| Namespace | signthai.com/ipl |
| Media type | application/vnd.ipl+json (provisional) |
| License | Open specification; document © 2026 AAT research team, DHCB, National Science and Technology Development Agency (NSTDA), Thailand, all rights reserved (see License) |
| Supersedes | 1.1.1 |
Cite as: IPL 1.1.2 (2026).
1.Scope
IPL defines a flat, named, normalized, rig-agnostic parameter vector (P) that represents an upper-body signing posture — hands, arms, head, body, face, and mouth — at a single instant (one keyframe). Every dimension is explicitly defined and human-interpretable: a person can read a dimension's definition, understand what it controls, and edit it by hand. The same vector can be produced by an encoder (sign-language pose → P) or authored by a human, and decoded to a rendered avatar (and, in future revisions, to other modalities).
IPL is a representation standard, not an algorithm. It specifies what the vector means, not how any particular encoder or renderer is implemented.
1.1.Design intent (non-normative)
- Designed, not discovered. Unlike a learned latent whose axes are entangled and opaque, IPL axes are hand-designed with human definitions. Only the mapping into IPL is learned; the axes themselves are fixed by this specification.
- Interpretable. Each dimension corresponds to one human-nameable articulatory concept (cf. concept-bottleneck representations), enabling manual inspection and correction.
- Rig-agnostic. Values are normalized and skeleton-independent, so any conforming renderer can decode the same vector to its own humanoid.
2.Terminology
| Term | Meaning |
|---|---|
| IPL vector (P) | The complete ordered parameter vector defined by this standard. |
| Dimension / parameter | One named scalar of P. |
| Group | A named partition of dimensions (§3.1). |
| DOF | Degrees of freedom contributed by a parameter or group. |
| Pose (articulation) | The base configuration: what the body is doing. |
| Expression (affect) | An additive layer on affect groups: how it is coloured (§3.3). |
| Hyper condition | A named discrete/conditional channel carried alongside P that conditions the decode — NOT a latent dimension (§3.4). Currently: eye look-at, viseme, contact, anchor. |
| Body site | A named reference point on the decoded body (per-rig calibrated bone + offset → world point) — the domain of the contact and anchor hyper conditions (§3.4). |
| Post-processing | The final decode stage: applied on the fully decoded pose (pose ⊕ expression → rig), after all latent decoding (§3.4). |
| Producer | Any implementation that emits P (encoder or human tool). |
| Consumer | Any implementation that reads P (renderer, exporter, analyser). |
The key words MUST, MUST NOT, SHOULD, MAY are to be interpreted as in RFC 2119.
3.Representation model
3.1.Groups
P is partitioned into seven continuous groups (71 dimensions in all):
| Code | Group | Role |
|---|---|---|
a | arm | articulation |
w | palm facing (wrist orientation) | articulation |
g | fingers | articulation |
h | head | articulation + affect |
b | body | articulation + affect |
f | face | articulation + affect |
m | mouth | articulation + affect |
Plus hyper conditions (§3.4): eye look-at, viseme, contact, and anchor — conditional channels riding alongside the vector, not latent dimensions.
Articulation-only groups (
a w g) MUST NOT carry expression deltas — they encode pure form. Affect-capable groups areh b f monly (§3.3).
3.2.Value convention
- Each continuous dimension is normalized to the closed interval [−1, 1].
- Consumers MUST clamp out-of-range inputs to [−1, 1].
- A dimension's real-unit value is recovered by denormalizing with its
maxand mode: bipolar (default); unipolar (uni) for axes with no negative meaning (e.g.a_spread); circular stores degrees directly and wraps atmax. This mapping is a decode convenience — P on the wire is always the normalizedn. - The canonical body frame is neck-centric, right-handed: +x = body-right, +y = up, +z = toward viewer. Arm positions use the calibrated axis units in §4.1; P itself is normalized, not raw distances.
- Handedness is significant. Left/right MUST NOT be swapped silently; mirroring is a
correctness concern, not a cosmetic one. A sided dimension exists as an
_l/_rpair sharing one definition; the same normalized value on both sides is a symmetric pose, and x-like axes are negated for the right side at decode time.
3.3.Pose ⊕ Expression
IPL carries two stacked layers:
- pose — the base P across all seven groups.
- expression — an additive delta on affect groups
h b f monly. - α — a per-use gain in [0, 1] scaling the expression layer.
A consumer that ignores expression MUST still render pose correctly. Expression adds
no new dimensions to the registry ordering; it is a parallel delta over the affect
groups.
3.4.Hyper conditions & post-processing
Alongside the continuous vector, IPL carries hyper conditions — named discrete/conditional channels that condition how P is decoded without being latent dimensions. They are designed (not learned), do not occupy registry positions, never interpolate as scalars, and removing them MUST restore the pure decode of P. All other values remain the latent, unchanged.
The full decode order is:
P(pose) ⊕ α·P(expression) ──decode──▶ rig pose ──POST-PROCESSING──▶ rendered body
(latent, §3.3) (eye/viseme applied) (contact/anchor solved)
Four hyper conditions are defined (registry §4.8):
eye_lookat,viseme— decode-time conditions: consumed while decoding P into the rendered face/mouth (gaze aim, mouthing morph). (Reclassified from "categorical channels" in pre-release drafts — same semantics, new grouping.)contact— a post-processing condition:{mover, target}over body sites, meaning "the mover site connects to the target site". It is solved on the fully decoded body — after pose ⊕ expression and the complete rig decode — by moving ONLY the mover-side arm chain so the two site points coincide; the target side and torso MUST NOT move. The latent already places the mover close; contact closes the small residual gap (per-rig proportion differences) so the touch is exact on every rig. Out-of-reach targets clamp to the closest reachable point.anchor— a post-processing condition, per arm side:{l?: {site}, r?: {site}}over body sites, meaning "this arm's position (a_hand_*) is measured from the site instead of the neck". Each entry rides ITS arm's keyframes — an arm whose keyframes carry no anchor is never moved (a resting arm stays neck-origined). Solved on the fully decoded body by translating the anchored arm's target by the world delta site − neck-site on the posed rig (soanchor: neckis exactly a no-op) and re-solving; the torso MUST NOT move. Same P → correct placement on every rig (per-rig site calibration resolves the delta). Allowed sites:forehead,mouth,navel,shoulder_l,shoulder_r(andneck= default/no-op) — hand sites are excluded (circular). Priority:contact>anchor— on a side where a contact fires, the anchor is ignored (contact fully determines that hand); the other side still anchors. A rig with no resolvable site falls back to neck (graceful, never an error).
Body sites. The contact domain is a fixed vocabulary of 32 named reference points
on the decoded body: neck, navel, forehead, mouth, and per side (_l/_r)
shoulder, elbow, wrist, palm, plus {thumb,index,middle,ring,pinky}_{base,tip}.
A site resolves to a world point as primary bone + bone-local offset (+ optional
secondary bones); the offsets are calibrated per rig and are renderer state, not
part of P. Constraints: the mover MUST be a hand site; the target MUST NOT be on
the mover's hand nor be the mover's same-side elbow. At most one contact may be
active per keyframe.
4.Parameter registry (normative)
The registry is the authoritative list of every dimension. Each row is one dimension
(sided dimensions exist as an _l/_r pair sharing the listed definition). Defaults are
the neutral, normalized value.
| Column | Meaning |
|---|---|
id | Stable machine identifier (e.g. a_hand_x). Sided ids append _l/_r. MUST NOT be reused or redefined within the same paradigm version (X, §8). |
side | L/R (paired) or none (centerline). |
definition | One-sentence human-readable meaning. |
−1 … +1 | Semantics of the two poles (real-unit max in parentheses). |
dof | Degrees of freedom. |
affect | Whether the dimension accepts an expression delta (h b f m only). |
default | Neutral normalized value. |
4.1.arm
Hand position in the neck-centric frame, with a separate calibrated length scale
for each axis, plus elbow spread. All bipolar except a_spread (unipolar).
| Axis | Calibrated reference distance d |
|---|---|
| X | wrist_r to middle_tip_r: wrist to middle fingertip, hand in its open bind pose |
| Y | neck to navel: neck to navel, body in its bind pose |
| Z | shoulder_r to wrist_r: shoulder to wrist with the arm fully extended, preserving its native wrist orientation |
Distances MUST be measured between calibrated Body sites, including their bone-local offsets, in fixed reference poses. They MUST NOT be remeasured from the animated posture: bending the elbow, curling a finger or turning the body must not change the units. The right-side reference defines the common units for both arms; left/right position signs still follow §3.2. A change to calibrated sites or effective hand geometry requires recomputing the affected reference distances.
For each axis, the physical unit is , where r is a fixed per-axis
reference ratio defined by the official reference avatar (based): the corresponding
distance divided by shoulder width on that avatar. It retains the established numeric
scale. A normalized arm coordinate
n produces a displacement along its body-frame axis, before
anchor/contact corrections. Equivalently, a consumer using shoulder-width units
multiplies that axis by . Thus shoulder width alone no
longer defines arm placement across avatars. The registry ids, maxima and defaults
below remain unchanged.
A consumer lacking a valid reference measurement SHOULD use its legacy shoulder-width unit for that axis and report the missing calibration. Such a fallback does not establish calibrated-axis conformance for that avatar.
| id | side | definition | −1 … +1 | dof | default |
|---|---|---|---|---|---|
a_hand_x | L/R | Hand sideways position, scaled by calibrated wrist-to-middle-tip length | −1 = across to far side · +1 = out past own shoulder (max 1.5) | 1 | 0.333 |
a_hand_y | L/R | Hand height, scaled by calibrated neck-to-navel length | −1 = low/by hip · +1 = high overhead (max 1.5) | 1 | −0.933 |
a_hand_z | L/R | Hand forward reach, scaled by calibrated extended shoulder-to-wrist length | −1 = tucked to body · +1 = fully forward (max 1.5) | 1 | 0.35 |
a_spread | L/R | Elbow spread | 0 = tucked to ribs · +1 = elbow out (unipolar, max 1) | 1 | 0.4 |
4.2.palm facing
Wrist orientation as the vector part of a body-frame global unit quaternion (w
reconstructed on decode); gimbal-free. Identity (0,0,0) = palm to front, fingers up.
The right hand mirrors across the sagittal plane. Defaults encode "hand hangs following
the forearm, palm toward body".
A consumer MUST reconstruct the scalar part from the unit-length constraint:
If the squared norm of the vector part exceeds 1, the consumer MUST rescale the vector
part to length at most 1 before reconstructing, and SHOULD clamp it slightly below 1
(e.g. 0.98) to avoid the singularity near w = 0. Taking w ≥ 0 always also makes the
representation unambiguous (q and −q are the same rotation).
| id | side | definition | −1 … +1 | dof | default |
|---|---|---|---|---|---|
w_qx | L/R | Quaternion x (pitch axis) | −1 … +1 (max 1) | 1 | 0.885 |
w_qy | L/R | Quaternion y (pronate axis) | −1 … +1 (max 1) | 1 | −0.093 |
w_qz | L/R | Quaternion z (roll axis) | −1 … +1 (max 1) | 1 | −0.429 |
4.3.fingers
Per finger curl / fold / side on both hands; the thumb adds a 4th DOF (roll).
The thumb is bound ~90° rotated, so its fold = opposition (across the palm) and side
= abduction (in/out of the palm plane). g_thumb_roll is frequently unobservable from
2D pose; a producer that cannot estimate it MUST emit the default (0). All bipolar,
max 1, default 0.
| id | side | definition | −1 … +1 | dof | default |
|---|---|---|---|---|---|
g_{thumb,index,middle,ring,pinky}_curl | L/R | Finger curl (PIP/DIP toward palm) | −1 = bent back (hyperextended) · 0 = straight · +1 = fully curled | 1 | 0 |
g_{index,middle,ring,pinky}_fold | L/R | MCP base-knuckle fold | −1 = folded backward · 0 = straight · +1 = folded toward palm | 1 | 0 |
g_thumb_fold | L/R | Thumb opposition (sweep across palm to pinky base) | −1 = away · +1 = across palm | 1 | 0 |
g_{index,middle,ring,pinky}_side | L/R | Finger spread | −1 = toward thumb · +1 = toward pinky | 1 | 0 |
g_thumb_side | L/R | Thumb abduction (in/out of palm plane) | −1 = in · +1 = out | 1 | 0 |
g_thumb_roll | L/R | Thumb roll/pronation | −1 = pad out of palm facing · +1 = pad in | 1 | 0 |
4.4.head (affect-capable)
Head Euler angles (stored bipolar, degrees). Affect-capable.
| id | side | definition | −1 … +1 | dof | affect | default |
|---|---|---|---|---|---|---|
h_yaw | none | Turn left/right | ±80° (max 80) | 1 | yes | 0 |
h_pitch | none | Nod up/down | ±60° (max 60) | 1 | yes | 0 |
h_roll | none | Tilt head | ±50° (max 50) | 1 | yes | 0 |
4.5.body (affect-capable)
Torso motion distributed along the spine (degrees). Affect-capable.
| id | side | definition | −1 … +1 | dof | affect | default |
|---|---|---|---|---|---|---|
b_twist | none | Twist torso left/right | ±60° (max 60) | 1 | yes | 0 |
b_bend | none | Bend forward/back | ±50° (max 50) | 1 | yes | 0 |
b_lean | none | Lean sideways | ±40° (max 40) | 1 | yes | 0 |
4.6.face (affect-capable)
Bipolar expression dims; each drives one or more ARKit-named morph channels per direction. All max 1, default 0. Affect-capable.
| id | side | definition | −1 … +1 | dof | affect | default |
|---|---|---|---|---|---|---|
f_brow_inner | none | Inner brow | −1 = furrow (angry) · +1 = raise | 1 | yes | 0 |
f_brow | L/R | Outer brow | −1 = down · +1 = up | 1 | yes | 0 |
f_eye | L/R | Eyelid | −1 = blink-shut · +1 = wide | 1 | yes | 0 |
f_squint | L/R | Eye squint | 0 = neutral · +1 = squeezed (plus-only) | 1 | yes | 0 |
f_nose | none | Nose wrinkle / sneer | 0 = neutral · +1 = wrinkled (plus-only) | 1 | yes | 0 |
4.7.mouth (affect-capable)
Bipolar mouth/oral shapes; each recruits several ARKit morphs per direction so the extremes reach strong shapes (yawn, kiss, bared teeth). Cheeks, tongue, and teeth are oral articulators grouped here. All max 1, default 0. Affect-capable.
| id | side | definition | −1 … +1 | dof | affect | default |
|---|---|---|---|---|---|---|
m_mouth | L/R | Mouth corner | −1 = frown · +1 = smile | 1 | yes | 0 |
m_jaw | none | Jaw | −1 = clench · +1 = open (to yawn) | 1 | yes | 0 |
m_lips | none | Lip rounding | −1 = funnel · +1 = pucker | 1 | yes | 0 |
m_wide | none | Mouth width | −1 = narrow (/u/) · +1 = wide (signed) | 1 | yes | 0 |
m_roll | none | Lip roll | −1 = lower over upper · +1 = upper over lower | 1 | yes | 0 |
m_press | none | Lip press | −1 = relaxed-parted · +1 = pressed/tensed (signed) | 1 | yes | 0 |
m_fwd | none | Mouth/jaw thrust | 0 = neutral · +1 = forward (plus-only) | 1 | yes | 0 |
m_cheek | none | Cheek | −1 = suck in · +1 = puff out (signed) | 1 | yes | 0 |
m_tongue | none | Tongue out | 0 = in · +1 = out/lick (plus-only, best-effort) | 1 | yes | 0 |
m_teeth | none | Teeth bared (lips retracted) | 0 = neutral · +1 = bared (plus-only) | 1 | yes | 0 |
When mouthing is active, the artist viseme_* morph remains the primary shape and
m_* is a bounded delta from that shape. The normalized viseme mix supplies a
share-weighted implied baseline b; decode sends only
through the ARKit mouth channels. Therefore an
authored zero leaves the artist viseme untouched, a same-direction adjustment uses
only the remaining range, and an opposing adjustment can cancel it. The conservative
seed table is viseme_aa → m_jaw +0.7 and
viseme_PP → m_jaw −0.7, m_press +0.7; other pairs imply zero until tuned.
4.8.hyper conditions
Discrete/conditional channels carried alongside the continuous vector — see §3.4. (Section renamed from "4.cat categorical channels" in pre-release drafts; ids unchanged.)
| id | domain | owner group | stage | definition |
|---|---|---|---|---|
eye_lookat | enum | face | decode | Gaze target (front / left hand / right hand / glance left, right, up, down). |
viseme | sparse map viseme_* → 0…1 | mouth | decode | Mouthing: a sparse map of viseme_* shape ids to their share, e.g. {viseme_PP: 0.7, viseme_aa: 0.3} — an absent id is 0, {} is no mouthing, and the shares sum to at most 1. SEVERAL shapes may be active at once (each id is its own morph channel, so decode assigns each share directly), and because the value is continuous two mouth keys crossfade on interpolation instead of snapping. A map that arrives over budget is clamped per entry then scaled back onto 1. |
contact | mover site → target site | arm (mover side) | post-processing | The mover body site connects to the target site on the fully decoded body; target never moves (§3.4). |
anchor | body site per arm side | arm (per side) | post-processing | An anchored arm's position is measured from its site instead of the neck; an arm with no anchored keys never moves; contact wins on its mover side (§3.4). |
5.Encoding conventions
- Ordering. The registry order (§4) is the canonical serialization order.
- Serialization. A conforming JSON encoding maps
id → value; unknown ids MUST be ignored by consumers (forward compatibility) and SHOULD be logged. - Interpolation. Between keyframes, continuous dimensions interpolate independently; hyper conditions do not interpolate as values (nearest / hold). A consumer SHOULD engage/release the contact solve smoothly across a keyframe transition (ramp its correction weight), but the condition itself is discrete.
- Missing dimensions. A producer MAY omit dimensions it cannot estimate; consumers
MUST substitute the registry
default.
6.Conformance
An implementation is a conforming producer if it emits only registered ids, in range, with correct handedness and group assignment.
An implementation is a conforming consumer if it: clamps to [−1, 1]; applies
registry defaults for absent dimensions; renders pose correctly with expression
ignored; and, if it supports expression, applies over
affect groups only. For calibrated arm placement, it MUST apply the fixed-reference
axis definitions and normalization in §4.1.
A consumer MAY ignore hyper conditions (the pure decode of P MUST still be correct).
A consumer that supports contact MUST solve it as post-processing on the fully
decoded body, MUST NOT move the target side or torso, and MUST NOT alter P.
A consumer that supports anchor MUST resolve it as post-processing per arm side
(only sides carrying the annotation move; torso untouched), MUST give contact
priority on the contact's mover side, and MUST NOT alter P; an unresolvable site
falls back to the neck origin.
Conformance is stated against a specific version, e.g. "conforms to IPL 1.1.2".
7.Composition grammar
A composition is a one-line text script that places stored keyframe runs of P — glosses (named lexicon signs) and expressions (named affect deltas, §3.3) — on one timeline. This section defines that script language.
Each construct carries a since tag naming the tier in which it entered the grammar: Since 1.1.0 (basic), Since 1.1.1 (special functions and operators), and Since 1.1.2 (user functions). All three tiers are part of this IPL 1.1.2 document; the tags are not separately published versions.
7.1.Principle
- Every construct only places, scales in time, offsets, mirrors, holds or fills, or references keyframes of the existing P and its hyper conditions (§3.4). No construct adds a latent dimension, changes the registry (§4), or redefines a dimension.
- Composition is compose-time. A script is evaluated once into ordinary keyframes (plus expression references, §7.6.7), and the result decodes like any other P data. A consumer that only decodes P never needs to parse a script.
- The script is the authored source. A producer SHOULD store it with the composed result so that the result can be re-composed when a source gloss or function changes.
Terms used in this section:
| Term | Meaning |
|---|---|
| Lexicon | The producer's store of named glosses, expressions, and user functions. |
| Gloss | A named run of P keyframes (one sign). Its native length is its last keyframe index (1 if that is 0). |
| Subgroup | A group split by side where the group is sided: a_l a_r w_l w_r g_l g_r h b f m. A gloss drives a subgroup when it has at least one keyframe on it. |
| Gloss flags | Lexicon flags read by the grammar: draft (unfinished), spelling (a fingerspelling letter), fixed_position (body-anchored; never shifted or anchored), left_dominant (the left hand leads). |
| Neutral pose | The rest pose configured for the composition. Where none is configured, the registry defaults (§4). |
| Frame | One step of the composition timeline. All pause, gap and ramp values are in frames. |
7.2.Lexical conventions (Since 1.1.0)
- A script is a sequence of tokens separated by
|or||. Leading and trailing whitespace of every token is ignored; an empty token is skipped. - The reserved characters are
| ; ( ) { } [ ] # ! $ @ %.$starts a native function,@an expression marker,%a user-function call (§7.8);#separates a variation name;!ends a gloss token;;is the compound operator. A gloss name, expression name, or label MUST NOT contain| ; ( ) { } [ ], MUST NOT start with$,@or%, and a gloss name MUST NOT end with!. A gloss name additionally MUST NOT contain%anywhere, and contains#at most once and never as its first or last character. - There is no comment syntax and no escaping or quoting.
- Numbers are written as plain decimals (optional sign, digits, optional fraction). A producer MUST emit that form; a consumer MAY accept other numeric spellings. A non-numeric value in a numeric slot is an error.
- Arguments are positional and comma-separated inside one pair of parentheses, which
MUST NOT nest. Every argument is optional: an empty or missing slot takes its default,
and omitted parentheses mean all defaults. A trailing
labelslot takes the rest of the argument text, so a label may contain commas. - Case. Gloss names, expression names (including
noneandbreak), user-function names, and anchor sites are matched case-insensitively. Native function keywords ($pause,$shift,$reset,$spell,$anchor,$mirror,$buoy,$compound,$viseme) are lowercase and case-sensitive. |splits only at bracket and brace depth 0, so the bodies of[ … ]and{ … }keep their inner separators.;splits only at parenthesis, bracket and brace depth 0.
7.3.Grammar summary
Informative EBNF. Whitespace between tokens is free; the semantic rules in §7.4–§7.8 are normative.
script = [ item ] , { ( "|" | "||" ) , [ item ] } ;
item = operand , [ ";" , operand ] (* 1.1.1 *)
| pause | shift | reset | expr | "@none" | "@break" ;
operand = gloss | group | spell | anchor | mirror | buoy | compound | viseme ;
gloss = name , [ "(" , [ number ] , [ "," , label ] , ")" ] , [ "!" ] ; (* "!" 1.1.1 *)
name = word , [ "#" , word ] ;
group = "[" , member , { "|" , member } , "]" ,
[ "(" , [ number ] , [ "," , [ number ] , [ "," , label ] ] , ")" ] ;
member = gloss | spell ;
run = unit , { "|" , unit } ;
unit = gloss | group ;
spell = "$spell" , [ "(" , [ number ] , [ "," , [ number ] , [ "," , [ number ] ,
[ "," , [ number ] , [ "," , label ] ] ] ] , ")" ] , "{" , run , "}" ;
num4 = [ number ] , [ "," , [ number ] , [ "," , [ number ] , [ "," , [ number ] ] ] ] ;
pause = "$pause" , [ "(" , [ number ] , ")" ] ;
shift = "$shift" , [ "(" , num4 , ")" ] ;
reset = "$reset" ;
expr = "@" , word , [ "(" , [ number ] , [ "," , [ number ] , [ "," , [ number ] ] ] , ")" ] ;
anchor = "$anchor" , "(" , site , ")" , "{" , run , "}" ; (* 1.1.1 *)
mirror = "$mirror" , [ args ] , "{" , run , "}" ; (* 1.1.1 *)
buoy = "$buoy" , [ args ] , "{" , run , "}" , "{" , run , "}" ; (* 1.1.1 *)
compound = "$compound" , [ args ] , "{" , cop , "}" , "{" , cop , "}" ; (* 1.1.1 *)
cop = operand , [ ";" , operand ] ;
viseme = "$viseme" , "(" , text , [ "," , number ] , ")" ; (* 1.1.1 *)
args = "(" , [ number ] , [ "," , label ] , ")" ;
site = "neck" | "forehead" | "mouth" | "navel" | "shoulder_l" | "shoulder_r" ;
usercall = "%" , ident , "(" , { char - ( "(" | ")" ) } , ")" ; (* 1.1.2, pre-pass *)
ident = ( letter | "_" ) , { letter | digit | "_" } ; (* ASCII *)
usercall is not part of the token grammar: calls are expanded to text before
tokenizing (§7.8), so after expansion a script contains no % call.
7.4.Name resolution (Since 1.1.0)
A gloss token's name (after removing a trailing ! and any (args)) resolves against
the lexicon, case-insensitively:
- Exact match. If exactly one entry has that name, it is used. A draft entry
resolves when it is the only match. If more than one entry has that name (a concrete
entry and a draft included), the script is in error (
ambiguous gloss name). - Explicit variation
NAME#var. If no entry is namedNAME#varexactly, the token falls back to the entry namedNAME(the same exact-match rule applies), never to a sibling variation. If neither exists, the name is unknown. - Bare variation family. A name without
#that matches no entry exactly, but heads entries namedNAME#…, picks one member uniformly at random, choosing among concrete (non-draft) members when any exist, otherwise among all members. The pick is re-rolled on every compose, and a producer serializing the script MUST keep the bareNAME. - Otherwise the name is unknown (
unknown gloss), which is an error.
Expression names (@name) resolve the same way, without variations: one match is used;
none is unknown expression; more than one is ambiguous expression name.
Aliases and composed glosses are lexicon-level entries, defined here only for resolution:
- An alias name resolves to its main gloss (one hop) and uses that gloss's keyframes. A source dependency is recorded against the main gloss.
- A composed gloss stores a script and the keyframes composed from it. In a sequence
it is placed like any gloss. The script of a composed gloss MUST NOT reference another
composed gloss (no chains, so no cycles) and MUST NOT use a bare variation family
(it must be stable); each is an error. When a composed gloss is composed, its keys are
re-based so the first keyframe is frame 0 (no lead-in or tail-out), expression markers
are baked into the pose keyframes of the affect groups
h b f mas instead of being kept as references, andNAME!fills from the registry defaults. A script that places nothing is an error.
7.5.Timeline and automatic gaps (Since 1.1.0)
Items are placed in order. A timed item (a gloss, a block, $viseme, or $pause)
occupies frames; markers ($shift, $reset, expression markers) occupy none and are
transparent to gap sizing.
A gloss with speed
soccupies frames, from its start frametto .Between consecutive timed items, and before the first (lead-in) and after the last (tail-out), the composer inserts a gap of neutral transition frames. With , where
minGapis the composition's minimum gap (default 2):dis the distance between the outgoing pose (the previous item's last keyframe pose) and the incoming pose (the next item's first keyframe pose): the Euclidean distance over the normalized P values, with each side's wrist orientation compared as a rotation (sign-invariant quaternion chord). Both poses are measured with the shift active for that item (§7.6.4); afixed_positiongloss, a pause, and the lead-in/tail-out boundary are measured unshifted, and a boundary or pause uses the registry default pose. The cap 15 never falls belowm.The gap is replaced, in this order of precedence, by: the fixed gap of a
[ ]group member (§7.6.5);mfor an item joined with||(§7.7.6); 3 frames between two consecutive glosses flaggedspelling.The timeline length is , where
endis the frame after the tail-out gap.
7.6.Basic constructs (Since 1.1.0)
7.6.1.Sequence A | B
Items placed in order on one timeline, separated by automatic gaps (§7.5).
7.6.2.Gloss NAME, NAME(speed), NAME(speed,label)
Places the resolved gloss (§7.4). speed MUST be a number > 0 (default 1); speed 2
plays in half the native length. label is an optional display caption for the placed
item; it does not affect P and MUST NOT contain ; (an error) or any other reserved
separator.
7.6.3.Pause $pause, $pause(N)
Holds the neutral pose for N frames, rounded. N MUST be ≥ 1; a bare $pause holds
15. A pause is a timed item: it has automatic gaps on both sides and carries no
keyframes.
7.6.4.Shift $shift(x,y,z,twist), $reset
A stateful marker that offsets every following gloss until the next $shift or $reset
(or the end of a $spell block, §7.6.6). Every axis is optional and defaults to 0;
a bare $shift (all zero) is the same as $reset. For each keyframe the gloss itself
authored:
a_hand_x_l += x,a_hand_x_r −= x(x is mirrored, so a positive x moves both hands toward the same body side);a_hand_y_{l,r} += y,a_hand_z_{l,r} += z;b_twist += twist;
each result clamped to [−1, 1]. Dimensions a gloss does not author are not created. A
fixed_position gloss is not shifted. The offset is applied to the placed keyframes;
P's definitions are unchanged.
7.6.5.Group [A | B](speed,gap,label)
Members placed as one unit, connected at a fixed gap instead of the automatic gap.
speed MUST be > 0 (default 1) and applies to members whose own speed is 1; a
member's own speed wins. gap MUST be ≥ 0 (default 3) and is rounded; the next
member starts frames after the previous member's last frame. Members are
glosses or $spell runs; a $spell member is expanded into its letters with the shift
ladder of §7.6.6 baked into them, without changing the shift state outside the group. Groups MUST NOT nest. An empty group, or any other member kind,
is an error. The group is one unit for captions (label, or the member names).
7.6.6.Spelling run $spell(x,y,z,twist,label){A | B | …}
A fingerspelling run with its own shift ladder. The shift defaults to (0.1, 0, 0, 0).
Members are glosses or [ ] groups; a group counts as one letter. For letter i of
N (0-based), the active shift is the start shift scaled by : the
first letter is at the full shift and the last at zero. A single letter keeps the full
shift. After the block, the shift state is zero (an earlier $shift is not restored).
With a label, the run is one unit for captions. An empty block is an error.
7.6.7.Expressions @name(alpha,rampIn,rampOut), @none, @break
@name opens an expression range on the first free of 3 reference lanes, starting at
the start of the next timed item. alpha is the peak gain and MUST satisfy
(default 1). rampIn/rampOut are frames ≥ 0, rounded; when omitted,
the consumer's expression ramp applies (6 frames unless configured). A fourth
concurrent expression is an error. @break closes the most recently opened range and
@none closes all open ranges, at the start of the next timed item. A range left open
ends at the end of the last item it covers (not at the tail-out). A range that
covers no frames is dropped. A lane freed and reopened at the same frame starts one frame
later. In a sequence the ranges are references: the expression's deltas are applied
at decode as over h b f m (§3.3) and are never
written into the pose keyframes. (A composed gloss bakes them instead, §7.4.)
7.7.Special functions and operators (Since 1.1.1)
The constructs below take a block body. Unless stated otherwise, a block body is a run:
glosses and [ ] groups only. A marker ($…, @…) inside a body, an empty body, or a
|| inside a body is an error (a block sets its own member gaps).
7.7.1.Anchor block $anchor(site){A | B}
Measures the arm position of the covered glosses from a body site instead of the neck,
by stamping the anchor hyper condition (§3.4, §4.8) on each arm keyframe the gloss
authors: an a_l keyframe gets {l: {site}}, an a_r keyframe {r: {site}}. An arm the
gloss does not drive gets nothing. site MUST be one of neck, forehead, mouth,
navel, shoulder_l, shoulder_r (case-insensitive); any other value is an error.
neck stamps nothing (the default origin). A keyframe that already carries an authored
anchor for that side keeps it. fixed_position glosses and subgroups filled by !
(§7.7.7) are not stamped. The block is additive with contact, which keeps its priority
(§3.4). The re-origin itself happens at decode post-processing, per rig.
7.7.2.Mirror block $mirror(speed,label){A | B}
Plays the run side-swapped: _l/_r subgroup tracks and dimensions exchange sides;
h_yaw, h_roll, b_twist, b_lean are negated; eye look-at, contact and anchor
sites swap sides. Mirroring twice is the identity. speed MUST be > 0 (default 1)
and applies to members whose own speed is 1. With a label, the run is one unit for
captions. The block takes exactly one body; a second {…} is an error.
7.7.3.Buoy $buoy(speed,label){primary…}{secondary…}
The primary run plays as written, and every primary member composes as if marked !
(§7.7.7). The secondary run then plays fully mirrored (as $mirror), and on every
subgroup a secondary member does not drive, the primary run's final keyframe value is
held across that member. A secondary member marked ! takes the neutral fill instead
of the hold. Both bodies are runs and both MUST be non-empty. speed (> 0, default 1)
applies to members whose own speed is 1. The whole buoy is one unit for captions.
7.7.4.Compound $compound(speed,label){P}{S} and P ; S
Two-handed simultaneous composition: one placed item built from a primary P and a
secondary S. Each operand MUST parse to exactly one sign-producing item (a gloss, group,
$spell, $anchor, $mirror, $buoy, $compound, or $viseme); otherwise it is an
error. The shorthand P ; S has no argument slots and takes exactly two operands: a
second top-level ; or an empty side is an error. Operands may nest; nesting deeper than
32 levels is an error (compound-limit). speed MUST be > 0 (default 1).
- Hands. The target hand is the primary's dominant side (left if it is flagged
left_dominant, else right), or the opposite side whenSis a$mirrorblock.Sis mirrored when its dominant side differs from the target.S's finger track on the target hand replacesP's; every other subgroup ofSis used only wherePdrives nothing. Both are time-scaled to the longer of the two (at least 1 frame). - Viseme operand rule. When exactly one operand is a
$viseme, the sign operand owns the duration in either order. If the sign is the primary and already drives the mouth, the viseme adds nothing (compound-noop). A mouth run longer than the sign is compressed to the sign's length if the sign spans at least one frame per syllable plus the run's two 3-frame guards; otherwise the compound is in error (viseme-too-short). A shorter run is held at no mouthing until the sign ends. - A secondary that contributes no subgroup yields the diagnostic
compound-noop. !on a gloss operand applies to the whole compound.
7.7.5.Viseme $viseme(word), $viseme(word,speed)
A mouth-only item generated from Thai script or romanized Thai: keyframes on the mouth
subgroup carrying only the viseme hyper condition (§4.8). The word is split into
chunks on whitespace and -; a chunk with a Thai code point is read as Thai script,
otherwise as romanization. Tone marks are ignored; ๆ repeats the whole preceding chunk.
The input MUST be at most 512 code points and expand to at most 128 syllables
(after ๆ repetition); exceeding either is an error (viseme-limit). A word with no
readable syllable is an error. Unreadable syllables are dropped with the diagnostic
viseme-incomplete; otherwise heuristic readings give viseme-guessed. The two are mutually
exclusive: when both apply, only viseme-incomplete is raised. speed MUST be > 0
(default 1). More than two arguments is an error. The per-syllable timing is consumer
configuration and is not part of this grammar.
7.7.6.Minimum-gap join A || B
B joins A at the composition's minimum gap m (§7.5), skipping distance scaling and
the spelling rule; B remains its own item. B MUST be a gloss or a block construct
(group, $spell, $anchor, $mirror, $buoy, $compound, $viseme); for a block,
the join applies to its first placed gloss. A || before a pause or marker, a leading or
trailing ||, or a || inside a block body is an error. An empty token between does not
cancel the join (A || | B joins B).
7.7.7.Neutral fill NAME!
Every subgroup the gloss does not drive is keyed with the neutral pose at the gloss's
first and last frame (one key if they coincide). In addition, one neutral key is placed
rampIn frames before the first frame and one rampOut frames after the last frame,
using the composition's rest ramps (default 6 each, the outer frame rounded and not
below 0), but only when that frame lies within the same captioned unit, no key already
sits on that frame, and no other key lies strictly between it and the gloss. $shift and
$anchor skip the filled subgroups. ! is the last character of a gloss token, after
any #variation and (args); it is a gloss suffix only (not valid after ] or }).
7.8.User functions %name(args) (Since 1.1.2)
A user function is a lexicon entry {name, arity, rules} that rewrites a call into script
text. Expansion is a text pre-pass that runs before tokenizing (§7.2), so the token
grammar never contains %.
- Call.
%followed byidentand a parenthesized argument text that contains no parentheses. The%namespace is separate from$:%pauseand$pausemay coexist. - Name. MUST match
[A-Za-z_][A-Za-z0-9_]*, at most 64 characters (a store MAY truncate a longer name), unique case-insensitively; calls resolve case-insensitively. - Rules. An ordered list of 1 to 200
{pattern, template}pairs. The argument text is split on,, each argument trimmed, and the parts re-joined with,. Rules are tried in order; the first whose pattern matches anywhere in that string wins (patterns are unanchored unless written with^/$), and the call is replaced by its template. - Templates.
\1–\9insert a capture group;\{1}–\{9}is the same, for use before a digit;\\is a literal backslash. A reference to a group the pattern does not define is rejected when the function is saved; an unmatched optional group inserts nothing. - Arity is 0–9 and is metadata only; it never gates matching.
- Passes. One pass replaces every call in the current text, left to right; text
produced by a pass is scanned again only in the next pass. At most 32 passes run,
and at most 500 substitutions are made per script. Calls left after 32 passes are an
error (
expansion too deep); exceeding 500 is an error (expanded too many items). - Portable regex subset. A pattern MUST be at most 2000 characters and a template
at most 4000; a store MAY truncate longer values. Patterns are matched over Unicode
code points, with
\d,\w,\smeaning their ASCII classes. A pattern MUST NOT use backreferences (\1–\9,\k), lookahead or lookbehind ((?=,(?!,(?<=,(?<!), named groups ((?P<,(?<,(?'), or any other(?form except the non-capturing(?:. Outside a character class, a backslash MAY only escape one ofd D w W s S b B n r t f v 0 x u \ ^ $ . | ? * + ( ) [ ] { } /. A violating function is rejected when it is saved. - Errors. An unknown function, a call whose arguments no rule matches, and a pattern that fails to compile are errors; the failed call is removed from the text and the error is reported with the script's other errors.
7.9.Errors and diagnostics
A blocking error stops composition: the script produces no keyframes until it is fixed. A diagnostic is non-blocking: composition proceeds, and the producer SHOULD show it to the author.
| class | condition | raised when |
|---|---|---|
| blocking | unknown gloss, ambiguous gloss name | a gloss name does not resolve, or matches more than one entry (§7.4) |
| blocking | unknown expression, ambiguous expression name | the same for @name |
| blocking | bad gloss speed, bad group args, bad group gap, bad spell args, bad pause, bad shift, bad mirror speed, bad buoy speed, bad compound speed, bad viseme args, bad viseme speed, bad expression alpha, bad expression ramp | an argument is non-numeric or out of its range |
| blocking | reserved label | a label contains ; |
| blocking | bad group, empty group, no nested groups, group member | a malformed group, or a member other than a gloss or $spell |
| blocking | empty block, block member, block arity | an empty body, a marker in a body, or the wrong number of {…} bodies |
| blocking | unknown anchor site | a site outside §7.7.1 |
| blocking | expression lanes | a fourth concurrent expression |
| blocking | compound operand, compound shorthand | an operand that is not one sign-producing item; ; without exactly two operands |
| blocking | compound-limit | compound nesting deeper than 32 |
| blocking | viseme-limit, viseme unreadable | the 512 / 128 limits; no readable syllable |
| blocking | viseme-too-short | a sign too short for its compound mouth run |
| blocking | misplaced join | a misplaced || (§7.7.6) |
| blocking | unknown function | an unknown native $ function |
| blocking | unknown user function, no rule matches, bad pattern, expansion too deep, expanded too many items | user-function expansion (§7.8) |
| blocking | nothing to place, variation family in composed gloss, composed gloss chain | composed-gloss rules (§7.4) |
| diagnostic | viseme-guessed | a heuristic syllable reading |
| diagnostic | viseme-incomplete | some syllables unreadable and dropped |
| diagnostic | compound-noop | the secondary contributes nothing |
Hyphenated names (compound-limit, viseme-limit, viseme-too-short, viseme-guessed,
viseme-incomplete, compound-noop) are machine-readable diagnostic kinds; the other
entries name the condition. Message wording is not normative.
7.10.Construct index and examples
| construct | syntax | since |
|---|---|---|
sequence | A | B | 1.1.0 |
gloss | NAME, NAME(speed), NAME(speed,label) | 1.1.0 |
variation | NAME#var, bare NAME family | 1.1.0 |
alias-composed | alias and composed-gloss resolution | 1.1.0 |
auto-gap | implicit between items, lead-in, tail-out | 1.1.0 |
pause | $pause, $pause(N) | 1.1.0 |
shift | $shift(x,y,z,twist), $reset | 1.1.0 |
group | [A | B](speed,gap,label) | 1.1.0 |
spell | $spell(x,y,z,twist,label){A | B} | 1.1.0 |
expression | @name(alpha,rampIn,rampOut), @none, @break | 1.1.0 |
anchor | $anchor(site){A | B} | 1.1.1 |
mirror | $mirror(speed,label){A | B} | 1.1.1 |
buoy | $buoy(speed,label){P}{S} | 1.1.1 |
compound | $compound(speed,label){P}{S}, P ; S | 1.1.1 |
viseme | $viseme(word,speed) | 1.1.1 |
min-gap-join | A || B | 1.1.1 |
neutral-fill | NAME! | 1.1.1 |
user-function | %name(args) | 1.1.2 |
Examples (gloss and expression names are illustrative):
1.1.0 $shift(0.3,0,0,0.2) | HELLO | MY NAME(0.75) | @smile(0.5) | $spell{ก | ข} | @none | $pause(15) | $reset
1.1.1 $anchor(mouth){EAT} || WATER! | $compound(1,caption){A}{B} | SIGN ; $viseme(sa-wat-dee)
1.1.2 %num(123) | YEAR
8.Versioning & compatibility
An IPL version is written X.Y.Z. Each digit answers one question about what a release changed; the digits to its right reset to 0 when it increments.
- X — paradigm shift. The representation model changes, or existing P vectors
stop being valid. This covers a replaced or re-partitioned vector (the group model of
§3 changes, the [−1, 1] convention changes, or P stops being the canonical
representation) and every breaking registry edit (§4): an axis removed, an
idreused, or an axis's meaning or range redefined. A new X starts a new vector lineage: vectors, decoders and scripts of an earlier X are not expected to carry over without conversion, and the release MUST be recorded as breaking with a migration note. - Y — major change. The document gains normative capability within the same paradigm while every existing vector stays valid: one or more axes added to the registry (§4) with neutral defaults, so old vectors decode with the new dimensions at their defaults; a new normative section with MUST-level rules (the composition grammar of §7 entered at 1.1.0). A Y release is additive.
- Z — minor change. Everything that leaves P and the pure decode of P
untouched: clarification, wording and non-semantic fixes; additions to the
hyper-condition set (§3.4) and refinements of a hyper condition's domain whose
earlier form remains valid (owner policy, 2026-07-16;
anchorat 1.0.1, thevisememap at 1.1.1) — they ride alongside P without adding axes, so old vectors and pure-latent decoders are unaffected; grammar constructs added to §7 that keep every earlier script valid (1.1.1 and 1.1.2); decode refinements that keep the vector layout, ids, ranges and defaults (the updated arm axes definition of §4.1 at 1.1.2). A minor change MAY alter rendered geometry on some avatars; storage compatibility does not imply identical geometry, and the change log MUST record such an effect.
Rules that hold across releases:
- Within one X, an
idis never reused for a different meaning, no axis is removed or redefined, and the registry order of existing ids never changes. - Consumers MUST ignore unknown ids (§5), so a vector written under a later Y or Z of the same X decodes under an earlier one with the new dimensions dropped, and a vector written under an earlier release decodes under a later one with defaults substituted.
- Each release records supersedes and compatibility (baseline | additive | breaking). Implementers SHOULD advertise the exact version they target, and conformance is always stated against one version (§6).
- Release and numbering of a version are declared by the owner; a Since tag inside §7 names the tier a construct entered and is not a separately published version.
Release history: 1.0.0 (baseline) → 1.0.1 (minor: anchor) → 1.1.0 (major:
composition grammar introduced) → 1.1.1 (minor) → 1.1.2 (minor, current). The full
record is the change log page, ipl.signthai.com/changelog/.
9.Identifiers
| Kind | Value |
|---|---|
| Prose term | IPL — Interpretable Pose Latent |
| Math symbol | P |
| Namespace / URI base | signthai.com/ipl |
| Media type | application/vnd.ipl+json (provisional) |
| Version string | IPL 1.1.2 |
The prose term is IPL; the acronym expands on first use in any document to avoid collision with unrelated uses of "IPL".
10.Change log
The version history (1.0.0 → 1.0.1 → 1.1.0 → 1.1.1 → 1.1.2) is maintained on the change log page, ipl.signthai.com/changelog/.
License
The IPL specification is open: anyone may implement, encode, decode, or exchange IPL data without permission or fee. The text of this document, the website ipl.signthai.com, and the .docx editions are © 2026 AAT research team, DHCB, National Science and Technology Development Agency (NSTDA), Thailand. All rights reserved. Reproduction or redistribution of the document requires written permission.