@lovelace_lol/latticework is the standalone character-runtime package used by
Character Loom for speech, conversation, gaze, blinking, hair, lip-sync, and animation
coordination.
Latticework owns character-behavior agencies and runtime helpers. LoomLarge owns the application shell, UI, backend routes, character asset loading, and product flows that assemble those agencies.
Latticework has been externalized from LoomLarge and is published to npm.
- Package:
@lovelace_lol/latticework - Repo:
meekmachine/Latticework - Current LoomLarge consumption:
@lovelace_lol/latticework@^0.0.5 - LoomLarge no longer contains
frontend/src/latticework - LoomLarge validates linked Latticework PRs before publish when needed
The current package is a bridge extraction that preserves the working runtime
surface while the internals are cleaned up. The long-term architecture is still
to move toward clearer Effect service composition and Most.js stream
contracts without breaking LoomLarge consumers.
npm install @lovelace_lol/latticeworkthree is an optional peer dependency. Install it in consumers that use runtime
surfaces backed by Three.js or animation-engine integration.
npm install threeLatticework should contain reusable character-runtime behavior:
- animation services, snippet state, bundled snippet preloading, and baked clip runtime helpers
- TTS and transcription services
- conversation orchestration for local browser speech flows
- vocal/lip-sync timeline helpers, including Azure/SAPI viseme normalization
- gaze and eye/head tracking services
- blink, hair, and prosodic agencies
- runtime configuration helpers that are not LoomLarge-specific UI state
LoomLarge should keep app-specific behavior:
- React screens, panels, and module UI
- backend API calls and product-specific session orchestration
- character profile selection and product settings
- LiveKit room/token ownership and app-level connection UX
- smoke tests and previews for the assembled LoomLarge experience
The package currently exposes a single root entry point.
import {
createAnimationService,
createConversationService,
createTTSService,
createTranscriptionService,
createVocalService,
createEyeHeadTrackingService,
azureVisemesToTimeline,
} from '@lovelace_lol/latticework';| Area | Key exports | Purpose |
|---|---|---|
| Animation | createAnimationService, snippet observables, snippet preload helpers |
Schedules snippets, tracks playback state, exposes UI/runtime streams |
| TTS | createTTSService, TTS timeline helpers, playback-reference types |
Speaks text, emits timing events, coordinates vocal animation when an animation agency is provided |
| Transcription | createTranscriptionService |
Browser speech recognition, transcript callbacks, interruption/reference-audio hooks |
| Conversation | createConversationService, ConversationFlow |
Coordinates TTS, transcription, turn state, interruption handling, gaze/prosody handoff |
| Vocal / lip-sync | createVocalService, azureVisemesToTimeline, VisemeMapper, PhonemeExtractor |
Converts text/provider visemes into animation timelines and snippets |
| Gaze / eye-head | createEyeHeadTrackingService, gaze config/types |
Drives attention, gaze targets, listening/speaking poses, blink hooks |
| Blink | createBlinkService, BlinkService |
Autonomous and triggered blinking |
| Hair | HairService, hair physics config/types |
Hair runtime state and UI-facing configuration |
| Prosody | source modules, not yet a stable root export | Speech-driven expression and gesture coordination |
import { createTTSService } from '@lovelace_lol/latticework';
const tts = createTTSService(
{
engine: 'webSpeech',
rate: 1,
pitch: 1,
volume: 1,
},
{
onStart: () => console.log('speaking'),
onEnd: () => console.log('done'),
onBoundary: ({ word }) => console.log('word', word),
}
);
await tts.speak('Hello from Latticework.');ConversationService owns the mechanics: TTS, transcription, state,
interruptions, gaze/prosody coordination, and cleanup.
ConversationFlow is the caller-provided content policy. It is a generator that
yields agent utterances and receives final user transcripts through .next().
import {
createConversationService,
createTranscriptionService,
createTTSService,
type ConversationFlow,
} from '@lovelace_lol/latticework';
const tts = createTTSService({ engine: 'webSpeech' });
const transcription = createTranscriptionService({ lang: 'en-US' });
const conversation = createConversationService(tts, transcription);
function* simpleFlow(): ConversationFlow {
const answer = yield 'What should we work on today?';
yield `I heard: ${answer}`;
}
conversation.start(simpleFlow);The generator API is intentionally documented because it is easy to confuse with the service itself. Follow-up API cleanup is tracked in issue #8.
import {
azureVisemesToTimeline,
createVocalService,
} from '@lovelace_lol/latticework';
const vocal = createVocalService({
animationAgency: {
schedule: (snippet) => animationManager.schedule(snippet),
remove: (name) => animationManager.remove(name),
seek: (name, offsetSec) => animationManager.seek?.(name, offsetSec),
},
});
const visemes = azureVisemesToTimeline(
[{ viseme_id: 2, audio_offset: 0.12 }],
1200,
{ visualLeadMs: 35 }
);
vocal.startTimeline({
source: 'azure',
text: 'hello',
visemes,
durationSec: 1.2,
});npm ci
npm run build
npm run typecheck
npm testThe build emits ESM, CJS, and type declarations through tsup. The build also
copies bundled animation snippets into dist so consumers can preload them from
the package.
Publishing mirrors the Embody-style tag/release flow. The checked-in
package.json version is a baseline only; the publish workflow resolves the
actual release version from an existing vX.Y.Z tag or from the latest npm
version, then bumps the patch version in the CI workspace before publishing.
- Open a PR against
main. - PR checks run build, typecheck, and tests.
- Merge to
main. - The
Publish to NPMworkflow determines the next patch version withscripts/ci/determine-release-version.mjs, creates or reuses avX.Y.Ztag, builds, tests, and publishes if that npm version does not already exist. - Verify the published package.
npm view @lovelace_lol/latticework version gitHead --jsonThe workflow currently expects the GitHub npm environment to provide:
NPM_KEYfor npm authenticationPUBLISH_PUSH_TOKENfor pushing release tags
Moving to npm trusted publishing and rotating/removing the long-lived token is tracked in issue #7.
Use linked PR validation when LoomLarge needs to test unreleased Latticework changes before an npm publish.
Typical sequence:
-
Open a Latticework PR with the package change.
-
In the LoomLarge PR body or comments, add a dependency link such as:
Depends-on: meekmachine/Latticework#123 -
LoomLarge CI resolves that Latticework PR to a git dependency for the current validation run.
-
After the Latticework PR merges and publishes, update the LoomLarge PR to use the published npm semver.
-
Do not merge LoomLarge
mainwith a temporary git SHA dependency.
Release and linked-PR documentation is tracked in issue #6.
Near-term work:
- document the release and linked-PR workflow in more detail (#6)
- move publishing to npm trusted publishing and rotate token secrets (#7)
- clarify or simplify
ConversationServiceversusConversationFlow(#8) - keep LoomLarge consumer smoke coverage current
Longer-term work:
- reduce bridge-era runtime coupling
- make agency ownership and cancellation rules explicit
- replace legacy runtime internals behind stable public contracts
- move toward
Effectfor service composition and lifecycle management - use
Most.jsstreams for timed events, fan-out, and observable-style inputs
The package boundary should remain stable while internals improve.