## Description
The VRM and MMD renderers had the same two boundary problems:
- Both imported the Stage audio store from the `stage-ui` source tree.
- Both kept copies of the wLipSync profile and model-neutral vowel
policy.
This PR solves both problems as one architecture change.
```text
Stage owns AudioContext and the current audio source
-> ThreeScene / MMDScene receive both as inputs
-> model-driver-lipsync owns the profile and shared vowel policy
-> VRM maps AEIOU to aa / ee / ih / oh / ou
-> MMD maps AEIOU to vowelA / vowelE / vowelI / vowelO / vowelU
```
The shared driver owns viseme projection, winner and runner selection,
silence handling, and smoothing. It has no Vue, Three.js, VRM, or MMD
dependency. Each renderer keeps only its model-specific mapping.
The renderer packages no longer import the Stage audio store or depend
directly on `wlipsync`. Stage passes the owning `AudioContext` down with
the current audio source. Cleanup disconnects only the wLipSync node
owned by that renderer.
## Current solution
The package-boundary review identified two related findings:
- Finding 3: The VRM and MMD renderers imported `stage-ui` audio state
through source paths.
- Finding 4: The same renderers duplicated the wLipSync profile and
model-neutral vowel policy.
Both findings came from one missing boundary between Stage, the lip-sync
driver, and the renderer adapters. This PR fixes that boundary with one
ownership change.
| Layer | Responsibility |
| --- | --- |
| `stage-ui` | Owns the `AudioContext` and the current audio source. It
passes both values to each renderer. |
| `stage-ui-three` and `stage-ui-mmd` | Connect renderer components to
the shared driver. They do not import the Stage audio store. |
| `model-driver-lipsync/runtime/wlipsync` | Exposes `createWLipSyncNode`
as the public runtime entry. |
| `model-driver-lipsync/shared/wlipsync` | Owns the profile, public
types, and model-neutral vowel policy. |
Runtime flow:
1. `Stage.vue` passes `audioContext` and `currentAudioSource` to
`ThreeScene` or `MMDScene`.
2. The renderer composable watches the context and creates one wLipSync
node for that context.
3. The source watcher connects the current audio source to that node.
4. Each render frame sends the wLipSync values to
`createWLipSyncVowelDriver`.
5. The renderer adapter applies the returned AEIOU weights to its model
controls.
6. Cleanup disconnects the exact source and node pair that the renderer
owns.
The shared policy keeps the current renderer behavior:
- It maps the raw `S` viseme to `I`.
- It selects the two strongest vowels.
- It uses the existing volume and silence thresholds.
- It keeps the 160 ms silence window.
- It keeps the existing attack, release, cap, and output scale values.
The renderer-specific mappings stay in their renderer packages:
- VRM maps AEIOU to `aa`, `ee`, `ih`, `oh`, and `ou`.
- MMD maps AEIOU to `vowelA`, `vowelE`, `vowelI`, `vowelO`, and
`vowelU`.
This solution removes both duplicate profile files. It also removes the
direct `wlipsync` dependencies from both renderer packages.
## Linked Issues
None.
## Additional Context
There is no intended visual change. This refactor preserves the existing
vowel weights and renderer mappings.
Verification:
- `sem diff --staged --no-cosmetics -v --file-exts .ts .tsx`
- `sem diff --staged --no-cosmetics -v --file-exts .vue`
- `sem impact --entity-id <useVRMLipSync> --json --dependents`
- `sem impact --entity-id <useMMDLipSync> --json --dependents`
- `pnpm -F @proj-airi/model-driver-lipsync test` — 3 tests passed
- `pnpm -F @proj-airi/stage-ui-three exec vitest run` — 18 tests passed
- `pnpm -F @proj-airi/stage-ui-mmd exec vitest run` — 23 tests passed
- Type checks passed for `model-driver-lipsync`, `stage-ui-three`,
`stage-ui-mmd`, and `stage-ui`
- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm lint` — 0 errors and 7 existing warnings outside this change
- `git diff --check`
The full workspace `pnpm typecheck` was also run. In the isolated
worktree it stopped in unchanged `electron-vueuse` and
`plugin-sdk-tamagotchi` because their generated dependency outputs were
not present. All affected package type checks above passed.
50 lines
1.8 KiB
Markdown
50 lines
1.8 KiB
Markdown
# `@proj-airi/model-driver-lipsync`
|
|
|
|
Shared lip-sync profiles and model-neutral mouth-driving policies for AIRI.
|
|
|
|
## What It Does
|
|
|
|
- Exposes the shared wLipSync profile.
|
|
- Converts raw AEIOUS frames into stable AEIOU weights.
|
|
- Owns winner selection, silence detection, and weight smoothing.
|
|
- Provides the existing Live2D lip-sync driver.
|
|
- Exposes the browser-only wLipSync node factory through a separate runtime entry.
|
|
|
|
The package does not write weights to VRM expressions or MMD morphs. Each renderer owns that mapping.
|
|
|
|
## Exports
|
|
|
|
- `@proj-airi/model-driver-lipsync`: the Live2D driver.
|
|
- `@proj-airi/model-driver-lipsync/shared/wlipsync`: the profile, types, and pure vowel driver.
|
|
- `@proj-airi/model-driver-lipsync/runtime/wlipsync`: the browser-only audio-node factory.
|
|
|
|
The shared entry has no Web Audio side effects. Node-based tools and tests can import it safely.
|
|
|
|
## How To Use It
|
|
|
|
```ts
|
|
import { createWLipSyncNode } from '@proj-airi/model-driver-lipsync/runtime/wlipsync'
|
|
import {
|
|
createWLipSyncVowelDriver,
|
|
wlipsyncProfile,
|
|
} from '@proj-airi/model-driver-lipsync/shared/wlipsync'
|
|
|
|
const node = await createWLipSyncNode(audioContext, wlipsyncProfile)
|
|
const driver = createWLipSyncVowelDriver()
|
|
const weights = driver.update(node, deltaSeconds)
|
|
```
|
|
|
|
The caller owns the `AudioContext`, the source node, and the source lifecycle.
|
|
|
|
## When To Use It
|
|
|
|
- Use the shared entry when a renderer needs standard AEIOU weights.
|
|
- Use the runtime entry when browser code creates a wLipSync audio node.
|
|
- Keep renderer-specific expression and morph mappings in the renderer package.
|
|
|
|
## When Not To Use It
|
|
|
|
- Do not import the runtime entry from Node-only code.
|
|
- Do not add Vue, Three.js, VRM, or MMD dependencies to this package.
|
|
- Do not move renderer-specific model writes into the shared driver.
|