@three-ws/sign-language

@three-ws/sign-language

American Sign Language for 3D avatars. Compile text into one continuous signed animation clip: known words sign from a lexicon of anatomically described signs, everything else fingerspells, with contact solving and non-manual facial markers. Zero runtime dependencies.

npm install @three-ws/sign-language
View on GitHubStar the repo

@three-ws/sign-language

American Sign Language for 3D avatars. Compile text into one continuous signed animation clip: words the lexicon knows are signed from anatomical descriptions (handshape, place on the body, movement), everything else fingerspells with the manual alphabet, and non-manual facial markers ride along. Zero runtime dependencies: no three.js, no DOM, no network. The same module runs in a browser, in Node, and in a test runner.

This package is the published build of the engine that powers three.ws/sign-language and the /docs/sign-language feature. Clips come out as the same clip-JSON document the three.ws animation library serves, so they retarget onto any humanoid rig with finger bones.

#Install

npm i @three-ws/sign-language

#Compile a sentence

import { compileUtterance, signLookup, signGloss } from '@three-ws/sign-language';

const signs = signLookup({ dominant: 'Right' });
const { clip, signed, spelled } = compileUtterance('happy to meet you', { signs });

console.log(signed);            // words signed from the lexicon
console.log(spelled);           // words that fingerspelled instead
console.log(signGloss('happy')); // human-readable description of the sign
console.log(clip.duration, clip.tracks.length);

clip is a plain three.js AnimationClip JSON document. Parse it with AnimationClip.parse(clip) in a three.js app, or feed it to the three.ws animation pipeline as-is.

A runnable version of this lives in example/compile-utterance.mjs:

node example/compile-utterance.mjs "happy to meet you"

#Speak alongside chat

SignSpeaker sequences utterances for a live conversation (chat replies, captions) with the pacing constants in CHAT_TIMING, and estimateDuration / utteranceWords let a UI plan ahead without compiling:

import { SignSpeaker, estimateDuration } from '@three-ws/sign-language';

// `manager` is anything with { injectClip(name, clip, opts), playOnce(name, opts) }:
// the three.ws AnimationManager, or a thin adapter over your own player.
const speaker = new SignSpeaker({ manager, dominant: 'Right' });

console.log(estimateDuration('nice to meet you')); // seconds, before compiling
const { signed, spelled } = await speaker.speak('nice to meet you'); // resolves when the signing ends
speaker.cancel(); // abandon an utterance; a newer speak() supersedes an older one

SignSpeaker needs a manager: it drives an avatar. To get a clip without one, call compileUtterance directly, as above.

#The five layers

The public surface is organised the way the engine is built; import from any layer:

Layer Exports What it is
speak SignSpeaker, compileUtterance, estimateDuration, utteranceWords, CHAT_TIMING Text in, one continuous signed clip out, with no seam between signed and fingerspelled words
spell buildFingerspellingClip, letterPose, LETTER_SHAPES, DEFAULT_TIMING, normalizeWord The manual alphabet: A-Z, 0-9, the traced J and Z, the double-letter bounce
lexicon SIGNS, SIGNABLE_WORDS, buildSignClip, lookupSign, signGloss, signLookup, DEFAULT_SIGN_TIMING The sign vocabulary, each entry a set of phases described anatomically
author HANDSHAPES, applyHandshape, SignTimeline, place, direction, poseHand, posePhase, faceWeights, mirrorPhase, neutralPose, restingPose, SIGNING_BONES, FACE_MARKERS The layer signs are written in: handshapes, body anchors, contact solving, non-manual markers, timelines
kinematics Pose, solveArm, anchorPoint, fingerBones, fingerTip, wristPosition, restWorld, ANCHORS, FINGERS, FINGER_JOINTS and friends Canonical-skeleton math: forward kinematics, two-bone arm IK, hand geometry

Add a word to the vocabulary by authoring phases in the same anatomical vocabulary SIGNS uses and passing your extended lookup to compileUtterance; nothing else changes.

#Play a clip on an avatar

On three.ws every humanoid avatar plays these clips natively. In your own three.js app:

import { AnimationClip, AnimationMixer } from 'three';

const parsed = AnimationClip.parse(clip);
const mixer = new AnimationMixer(avatarRoot);
mixer.clipAction(parsed).play();

Track names target the canonical Avaturn-style bone set (Hips, RightHand, RightHandIndex1, ...). A rig with different bone names retargets through the three.ws pipeline (src/glb-canonicalize.js + src/animation-retarget.js in the monorepo), which maps Mixamo, VRM, Daz, MakeHuman and other conventions automatically.

#What this is, and what it is not

Read this before shipping it to users.

This renders English word order using an ASL lexicon and the manual alphabet. It does not produce ASL grammar, which has its own syntax, topic-comment structure, spatial reference, and classifier system. A fluent signer will recognise the vocabulary and read the sentence; they will also recognise that it is not native ASL. That is a deliberate, honest limit of a text-driven renderer, not a bug to file.

Practical consequences:

#Development

The package is a thin published surface over the monorepo engine (src/sign-*.js at the repo root):

npm run build    # bundle src re-exports into dist/index.mjs (esbuild, no deps)
npm test         # node --test: guards the published surface + dependency-freeness
npm run example  # compile "happy to meet you" to example/utterance.clip.json

Both npm test and npm run example read dist/, so each rebuilds it first (pretest / preexample): a fresh clone runs them with no extra step.

Deep engine coverage lives in the monorepo suite (tests/sign-rig, sign-clip, sign-dictionary, fingerspelling, sign-speech, sign-goldens, sign-linguistics).