feat(stage-ui): vrm lip-sync (#204)

* feat(stage-ui): vrm lip-sync

* fix: update package.json

---------

Co-authored-by: Neko <neko@ayaka.moe>
This commit is contained in:
藍+85CD
2025-06-14 02:30:11 +08:00
committed by GitHub
co-authored by Neko
parent 400b52d0c7
commit 1db823d9e3
5 changed files with 49 additions and 0 deletions
+1
View File
@@ -192,6 +192,7 @@ words:
- wavefile - wavefile
- webgpu - webgpu
- weeb - weeb
- wlipsync
- worklet - worklet
- xast - xast
- xastscript - xastscript
+1
View File
@@ -108,6 +108,7 @@
"reka-ui": "^2.3.1", "reka-ui": "^2.3.1",
"unist-builder": "^4.0.0", "unist-builder": "^4.0.0",
"unspeech": "^0.1.7", "unspeech": "^0.1.7",
"wlipsync": "^1.2.1",
"xast-util-to-xml": "^4.0.0", "xast-util-to-xml": "^4.0.0",
"xastscript": "^4.0.0" "xastscript": "^4.0.0"
}, },
File diff suppressed because one or more lines are too long
@@ -0,0 +1,38 @@
import type { VRMCore } from '@pixiv/three-vrm-core'
import type { Profile } from 'wlipsync'
import { useAsyncState } from '@vueuse/core'
import { onUnmounted, watch } from 'vue'
import { createWLipSyncNode } from 'wlipsync'
import profile from '../../assets/lip-sync-profile.json' with { type: 'json' }
import { useAudioContext } from '../../stores/audio'
export function useVRMLipSync(audioNode: AudioNode) {
const { audioContext } = useAudioContext()
const { state: lipSyncNode, isReady } = useAsyncState(createWLipSyncNode(audioContext, profile as Profile), undefined)
// https://github.com/mrxz/wLipSync/blob/c3bc4b321dc7e1ca333d75f7aa1e9e746cbbb23a/example/index.js#L50-L66
const lipSyncMap = {
A: 'aa',
E: 'ee',
I: 'ih',
O: 'oh',
U: 'ou',
}
watch(isReady, () => isReady.value && audioNode.connect(lipSyncNode.value!))
onUnmounted(() => audioNode.disconnect())
function update(vrm: VRMCore | undefined) {
if (!vrm?.expressionManager || !lipSyncNode.value)
return
for (const key of Object.keys(lipSyncNode.value.weights)) {
const weight = lipSyncNode.value.weights[key] * lipSyncNode.value.volume
vrm.expressionManager?.setValue(lipSyncMap[key as keyof typeof lipSyncMap], weight)
}
}
return { update }
}
+8
View File
@@ -970,6 +970,9 @@ importers:
unspeech: unspeech:
specifier: ^0.1.7 specifier: ^0.1.7
version: 0.1.7 version: 0.1.7
wlipsync:
specifier: ^1.2.1
version: 1.2.1
xast-util-to-xml: xast-util-to-xml:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
@@ -11760,6 +11763,9 @@ packages:
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
engines: {node: '>=18'} engines: {node: '>=18'}
wlipsync@1.2.1:
resolution: {integrity: sha512-kyT1mcuqEyX7mrhYRfhWYg6g5Ymub2v3JRE/aBzhi+JUN6p11NFtz6PxSSUv4lS/h1lt0Sbk981xEuk8aw0p1g==}
wordwrapjs@5.1.0: wordwrapjs@5.1.0:
resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==}
engines: {node: '>=12.17'} engines: {node: '>=12.17'}
@@ -24748,6 +24754,8 @@ snapshots:
dependencies: dependencies:
string-width: 7.2.0 string-width: 7.2.0
wlipsync@1.2.1: {}
wordwrapjs@5.1.0: {} wordwrapjs@5.1.0: {}
workbox-background-sync@7.3.0: workbox-background-sync@7.3.0: