feat(stage-ui-three-performance-runtime): init new experimental three performance runtime design (#809)
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"@proj-airi/server-sdk": "workspace:^",
|
||||
"@proj-airi/stage-ui": "workspace:^",
|
||||
"@proj-airi/stage-ui-three": "workspace:^",
|
||||
"@proj-airi/stage-ui-three-performance-runtime": "workspace:^",
|
||||
"@proj-airi/ui": "workspace:^",
|
||||
"@proj-airi/ui-transitions": "workspace:^",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import type { TTSChunkItem } from '@proj-airi/stage-ui/utils/tts'
|
||||
import type { ChatProvider, SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { ThreeScene } from '@proj-airi/stage-ui-three'
|
||||
import { animations } from '@proj-airi/stage-ui-three/assets/vrm'
|
||||
import { useDelayMessageQueue, useEmotionsMessageQueue, usePipelineCharacterSpeechPlaybackQueueStore, usePipelineWorkflowTextSegmentationStore } from '@proj-airi/stage-ui/composables/queues'
|
||||
import { llmInferenceEndToken } from '@proj-airi/stage-ui/constants'
|
||||
import { EMOTION_EmotionMotionName_value, EMOTION_VRMExpressionName_value, EmotionThinkMotionName } from '@proj-airi/stage-ui/constants/emotions'
|
||||
import { useAudioContext, useSpeakingStore } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { createQueue } from '@proj-airi/stage-ui/utils/queue'
|
||||
import { generateSpeech } from '@xsai/generate-speech'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
// VRM scene refs
|
||||
const sceneRef = ref<InstanceType<typeof ThreeScene>>()
|
||||
|
||||
// Playback + lip sync (VRM uses currentAudioSource)
|
||||
const characterSpeechPlaybackQueue = usePipelineCharacterSpeechPlaybackQueueStore()
|
||||
const { connectAudioContext, connectAudioAnalyser, clearAll, onPlaybackStarted, onPlaybackFinished } = characterSpeechPlaybackQueue
|
||||
const { currentAudioSource, playbackQueue } = storeToRefs(characterSpeechPlaybackQueue)
|
||||
|
||||
// Audio context / analyser
|
||||
const { audioContext } = useAudioContext()
|
||||
connectAudioContext(audioContext)
|
||||
const audioAnalyser = ref<AnalyserNode>()
|
||||
function setupAnalyser() {
|
||||
if (!audioAnalyser.value) {
|
||||
audioAnalyser.value = audioContext.createAnalyser()
|
||||
connectAudioAnalyser(audioAnalyser.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Settings + force VRM model
|
||||
const settingsStore = useSettings()
|
||||
const { stageModelRenderer, stageModelSelected, stageModelSelectedUrl, stageViewControlsEnabled } = storeToRefs(settingsStore)
|
||||
onMounted(async () => {
|
||||
// Preserve existing VRM selection if available; otherwise fall back to preset VRM
|
||||
const needsFallback = !stageModelSelectedUrl.value || stageModelRenderer.value !== 'vrm'
|
||||
if (needsFallback)
|
||||
stageModelSelected.value = 'preset-vrm-1'
|
||||
|
||||
await settingsStore.updateStageModel()
|
||||
setupAnalyser()
|
||||
})
|
||||
|
||||
// Speech
|
||||
const providersStore = useProvidersStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const { activeSpeechProvider, activeSpeechVoice, activeSpeechModel, ssmlEnabled, pitch } = storeToRefs(speechStore)
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { activeProvider: activeChatProvider, activeModel: activeChatModel } = storeToRefs(consciousnessStore)
|
||||
|
||||
// Text segmentation
|
||||
const textSegmentationStore = usePipelineWorkflowTextSegmentationStore()
|
||||
const { onTextSegmented, clearHooks: clearTextSegmentationHooks } = textSegmentationStore
|
||||
const { textSegmentationQueue } = storeToRefs(textSegmentationStore)
|
||||
clearTextSegmentationHooks()
|
||||
|
||||
// Emotion/delay queues (special tokens)
|
||||
const delaysQueue = useDelayMessageQueue()
|
||||
const emotionMessageQueue = useEmotionsMessageQueue(createQueue({ handlers: [] }))
|
||||
emotionMessageQueue.on('enqueue', (token) => {
|
||||
log(` - special 入队:${token}`)
|
||||
})
|
||||
emotionMessageQueue.on('dequeue', (token) => {
|
||||
log(`special 出队处理:${token}`)
|
||||
})
|
||||
|
||||
// State
|
||||
const { mouthOpenSize } = storeToRefs(useSpeakingStore())
|
||||
const nowSpeaking = ref(false)
|
||||
const currentMotion = ref<{ group: string }>({ group: EmotionThinkMotionName })
|
||||
const logLines = ref<string[]>([])
|
||||
const chatInput = ref('')
|
||||
const chatStore = useChatStore()
|
||||
const chatMessages = computed(() => {
|
||||
return chatStore.messages
|
||||
.filter(msg => msg.role !== 'system')
|
||||
.map((msg) => {
|
||||
const text = typeof msg.content === 'string'
|
||||
? msg.content
|
||||
: Array.isArray(msg.content)
|
||||
? msg.content.map((part: any) => typeof part === 'string' ? part : part.text ?? '').join('')
|
||||
: JSON.stringify(msg.content ?? '')
|
||||
return { role: msg.role as 'user' | 'assistant', text }
|
||||
})
|
||||
})
|
||||
|
||||
function log(line: string) {
|
||||
logLines.value = [line, ...logLines.value].slice(0, 50)
|
||||
}
|
||||
|
||||
// TTS generation handler
|
||||
async function handleSpeechGeneration(ctx: { data: TTSChunkItem }) {
|
||||
try {
|
||||
if (!activeSpeechProvider.value || !activeSpeechVoice.value) {
|
||||
console.warn('No active speech provider configured')
|
||||
return
|
||||
}
|
||||
const provider = await providersStore.getProviderInstance(activeSpeechProvider.value) as SpeechProviderWithExtraOptions<string, any>
|
||||
if (!provider) {
|
||||
console.error('Failed to initialize speech provider')
|
||||
return
|
||||
}
|
||||
if (ctx.data.chunk === '' && !ctx.data.special)
|
||||
return
|
||||
if (ctx.data.chunk === '' && ctx.data.special) {
|
||||
// log(`特殊标记:${ctx.data.special}`)
|
||||
emotionMessageQueue.enqueue(ctx.data.special)
|
||||
return
|
||||
}
|
||||
const providerConfig = providersStore.getProviderConfig(activeSpeechProvider.value)
|
||||
const input = ssmlEnabled.value
|
||||
? speechStore.generateSSML(ctx.data.chunk, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value })
|
||||
: ctx.data.chunk
|
||||
|
||||
const res = await generateSpeech({
|
||||
...provider.speech(activeSpeechModel.value, providerConfig),
|
||||
input,
|
||||
voice: activeSpeechVoice.value.id,
|
||||
})
|
||||
|
||||
const audioBuffer = await audioContext.decodeAudioData(res)
|
||||
log(` - 排队:${ctx.data.chunk}${ctx.data.special ? ` [special: ${ctx.data.special}]` : ''}`)
|
||||
playbackQueue.value.enqueue({ audioBuffer, text: ctx.data.chunk, special: ctx.data.special })
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Speech generation failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const ttsQueue = createQueue<TTSChunkItem>({
|
||||
handlers: [
|
||||
handleSpeechGeneration,
|
||||
],
|
||||
})
|
||||
|
||||
// text segmentation hooks
|
||||
onTextSegmented((chunkItem) => {
|
||||
ttsQueue.enqueue(chunkItem)
|
||||
})
|
||||
|
||||
async function sendChat() {
|
||||
const content = chatInput.value.trim()
|
||||
if (!content)
|
||||
return
|
||||
|
||||
const provider = await providersStore.getProviderInstance(activeChatProvider.value)
|
||||
if (!provider || !activeChatModel.value) {
|
||||
log('未配置聊天模型或 provider')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await chatStore.send(content, {
|
||||
model: activeChatModel.value,
|
||||
chatProvider: provider as ChatProvider,
|
||||
})
|
||||
chatInput.value = ''
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
log('发送到 LLM 失败')
|
||||
}
|
||||
}
|
||||
|
||||
function resetChat() {
|
||||
chatStore.cleanupMessages()
|
||||
chatInput.value = ''
|
||||
logLines.value = []
|
||||
clearAll()
|
||||
}
|
||||
|
||||
// Chat hooks (reuse Stage pipeline but Live2D removed)
|
||||
const { onBeforeMessageComposed, onBeforeSend, onTokenLiteral, onTokenSpecial, onStreamEnd } = chatStore
|
||||
const chatHookCleanups: Array<() => void> = []
|
||||
|
||||
chatHookCleanups.push(onBeforeMessageComposed(async () => {
|
||||
clearAll()
|
||||
setupAnalyser()
|
||||
logLines.value = []
|
||||
}))
|
||||
|
||||
chatHookCleanups.push(onBeforeSend(async () => {
|
||||
currentMotion.value = { group: EmotionThinkMotionName }
|
||||
}))
|
||||
|
||||
chatHookCleanups.push(onTokenLiteral(async (literal) => {
|
||||
textSegmentationQueue.value.enqueue({ type: 'literal', value: literal })
|
||||
}))
|
||||
|
||||
chatHookCleanups.push(onTokenSpecial(async (special) => {
|
||||
textSegmentationQueue.value.enqueue({ type: 'special', value: special })
|
||||
}))
|
||||
|
||||
chatHookCleanups.push(onStreamEnd(async () => {
|
||||
delaysQueue.enqueue(llmInferenceEndToken)
|
||||
}))
|
||||
|
||||
// Wire playback to VRM + logs
|
||||
onPlaybackFinished(({ special }) => {
|
||||
nowSpeaking.value = false
|
||||
mouthOpenSize.value = 0
|
||||
if (special) {
|
||||
log(`播放结束,special: ${special}`)
|
||||
const motion = EMOTION_EmotionMotionName_value[special as keyof typeof EMOTION_EmotionMotionName_value]
|
||||
const expression = EMOTION_VRMExpressionName_value[special as keyof typeof EMOTION_VRMExpressionName_value]
|
||||
if (motion)
|
||||
currentMotion.value = { group: motion }
|
||||
if (expression)
|
||||
sceneRef.value?.setExpression(expression)
|
||||
}
|
||||
})
|
||||
|
||||
onPlaybackStarted(({ text }) => {
|
||||
nowSpeaking.value = true
|
||||
log(`播放开始:${text}`)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
chatHookCleanups.forEach(dispose => dispose?.())
|
||||
clearAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div p-4 space-y-4>
|
||||
<div text-lg font-600>
|
||||
Performance Layer Playground(复刻 Stage,去掉 Live2D)
|
||||
</div>
|
||||
<div grid gap-4 lg:grid-cols-2>
|
||||
<div border="1 solid neutral-300/40 dark:neutral-700/40" h-100 min-h-80 overflow-hidden rounded-2xl>
|
||||
<ThreeScene
|
||||
v-if="stageModelRenderer === 'vrm'"
|
||||
ref="sceneRef"
|
||||
:model-src="stageModelSelectedUrl"
|
||||
:idle-animation="animations.idleLoop.toString()"
|
||||
:current-audio-source="currentAudioSource"
|
||||
:show-axes="stageViewControlsEnabled"
|
||||
:paused="false"
|
||||
@error="console.error"
|
||||
/>
|
||||
<div v-else p-4 text-sm text-red-500>
|
||||
请选择 VRM 模型(当前模型类型不支持)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-neutral-300/50 rounded-xl p-3 text-xs leading-relaxed space-y-3 dark:border-neutral-700/60">
|
||||
<div font-600>
|
||||
聊天 / 播放
|
||||
</div>
|
||||
<div class="h-60 overflow-auto border border-neutral-200/60 rounded-lg p-2 dark:border-neutral-700/60">
|
||||
<div v-for="(msg, idx) in chatMessages" :key="idx" class="mb-2">
|
||||
<div class="text-[11px] text-neutral-500">
|
||||
{{ msg.role === 'user' ? 'User' : 'AIRI' }}
|
||||
</div>
|
||||
<div class="whitespace-pre-wrap break-words text-sm">
|
||||
{{ msg.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!chatMessages.length" class="text-sm text-neutral-500">
|
||||
输入消息进行对话。
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="chatInput"
|
||||
class="flex-1 border border-neutral-300/60 rounded-lg bg-white px-3 py-2 text-sm dark:bg-neutral-900/60"
|
||||
placeholder="输入消息,点击发送"
|
||||
@keyup.enter="sendChat"
|
||||
>
|
||||
<button
|
||||
class="rounded-lg bg-primary-500 px-3 py-2 text-white disabled:bg-neutral-400"
|
||||
:disabled="!chatInput.trim()"
|
||||
@click="sendChat"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
<button
|
||||
class="border border-neutral-300/60 rounded-lg px-3 py-2 text-sm"
|
||||
@click="resetChat"
|
||||
>
|
||||
重置对话
|
||||
</button>
|
||||
</div>
|
||||
<div class="border border-neutral-200/60 rounded-lg p-2 dark:border-neutral-700/60">
|
||||
<div mb-1 font-600>
|
||||
播放队列 / 日志
|
||||
</div>
|
||||
<ul class="max-h-60 overflow-auto space-y-1">
|
||||
<li v-for="line in logLines" :key="line">
|
||||
{{ line }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
</route>
|
||||
@@ -68,6 +68,12 @@ const menu = computed(() => [
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/providers-transcription-realtime-aliyun-nls',
|
||||
},
|
||||
{
|
||||
title: 'Performance Playground',
|
||||
description: 'Test performance runtime: VRM expressions + TTS lip sync',
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/performance-playground',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@proj-airi/stage-ui-three-performance-runtime",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "Runtime timeline + blackboard utilities for stage-ui-three performances",
|
||||
"author": {
|
||||
"name": "Moeru AI Project AIRI Team",
|
||||
"email": "airi@moeru.ai",
|
||||
"url": "https://github.com/moeru-ai"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/moeru-ai/airi.git",
|
||||
"directory": "packages/stage-ui-three-performance-runtime"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export interface BlackboardGazeTarget {
|
||||
type: 'screen' | 'world'
|
||||
position?: { x: number, y: number, z?: number }
|
||||
}
|
||||
|
||||
export interface BlackboardSpeechState {
|
||||
speaking: boolean
|
||||
lastText?: string
|
||||
lastSpecial?: string
|
||||
playheadMs?: number
|
||||
}
|
||||
|
||||
export interface BlackboardEmotionState {
|
||||
tag: string | null
|
||||
intensity: number
|
||||
}
|
||||
|
||||
export interface BlackboardPoseState {
|
||||
current: string | null
|
||||
weight?: number
|
||||
layer?: string
|
||||
}
|
||||
|
||||
export interface BlackboardActorState {
|
||||
emotion: BlackboardEmotionState
|
||||
expression: string | null
|
||||
gaze: BlackboardGazeTarget | null
|
||||
pose: BlackboardPoseState
|
||||
speech: BlackboardSpeechState
|
||||
flags: Record<string, string | number | boolean>
|
||||
markers: Record<string, number>
|
||||
}
|
||||
|
||||
function createDefaultActorState(): BlackboardActorState {
|
||||
return {
|
||||
emotion: { tag: null, intensity: 0 },
|
||||
expression: null,
|
||||
gaze: null,
|
||||
pose: { current: null, weight: 0 },
|
||||
speech: { speaking: false },
|
||||
flags: {},
|
||||
markers: {},
|
||||
}
|
||||
}
|
||||
|
||||
export const useBlackboardStore = defineStore('stage-performance-blackboard', () => {
|
||||
const actors = ref<Record<string, BlackboardActorState>>({
|
||||
default: createDefaultActorState(),
|
||||
})
|
||||
|
||||
const activeActorId = ref('default')
|
||||
|
||||
const activeActor = computed(() => actors.value[activeActorId.value] ?? actors.value.default)
|
||||
|
||||
function ensureActor(id: string) {
|
||||
if (!actors.value[id])
|
||||
actors.value[id] = createDefaultActorState()
|
||||
}
|
||||
|
||||
function setActiveActor(id: string) {
|
||||
ensureActor(id)
|
||||
activeActorId.value = id
|
||||
}
|
||||
|
||||
function updateEmotion(id: string, emotion: Partial<BlackboardEmotionState>) {
|
||||
ensureActor(id)
|
||||
actors.value[id].emotion = { ...actors.value[id].emotion, ...emotion }
|
||||
}
|
||||
|
||||
function updateExpression(id: string, expression: string | null) {
|
||||
ensureActor(id)
|
||||
actors.value[id].expression = expression
|
||||
}
|
||||
|
||||
function updateGaze(id: string, gaze: BlackboardGazeTarget | null) {
|
||||
ensureActor(id)
|
||||
actors.value[id].gaze = gaze
|
||||
}
|
||||
|
||||
function updatePose(id: string, pose: Partial<BlackboardPoseState>) {
|
||||
ensureActor(id)
|
||||
actors.value[id].pose = { ...actors.value[id].pose, ...pose }
|
||||
}
|
||||
|
||||
function updateSpeech(id: string, speech: Partial<BlackboardSpeechState>) {
|
||||
ensureActor(id)
|
||||
actors.value[id].speech = { ...actors.value[id].speech, ...speech }
|
||||
}
|
||||
|
||||
function setFlag(id: string, key: string, value: string | number | boolean) {
|
||||
ensureActor(id)
|
||||
actors.value[id].flags[key] = value
|
||||
}
|
||||
|
||||
function clearFlag(id: string, key: string) {
|
||||
if (!actors.value[id])
|
||||
return
|
||||
delete actors.value[id].flags[key]
|
||||
}
|
||||
|
||||
function setMarker(id: string, key: string, timestampMs: number) {
|
||||
ensureActor(id)
|
||||
actors.value[id].markers[key] = timestampMs
|
||||
}
|
||||
|
||||
function clearActor(id: string) {
|
||||
if (!actors.value[id])
|
||||
return
|
||||
actors.value[id] = createDefaultActorState()
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
actors.value = { default: createDefaultActorState() }
|
||||
activeActorId.value = 'default'
|
||||
}
|
||||
|
||||
return {
|
||||
actors,
|
||||
activeActorId,
|
||||
activeActor,
|
||||
|
||||
setActiveActor,
|
||||
updateEmotion,
|
||||
updateExpression,
|
||||
updateGaze,
|
||||
updatePose,
|
||||
updateSpeech,
|
||||
setFlag,
|
||||
clearFlag,
|
||||
setMarker,
|
||||
clearActor,
|
||||
resetAll,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './blackboard'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,71 @@
|
||||
// Core event/timeline contracts for the performance runtime; kept UI-agnostic so
|
||||
// adapters (VRM, audio, devtools) can build on top without pulling stage-ui in.
|
||||
|
||||
export type PerformanceEventKind = 'speech' | 'viseme' | 'expression' | 'body' | 'marker'
|
||||
|
||||
export interface SpeechEventPayload {
|
||||
text: string
|
||||
buffer?: AudioBuffer
|
||||
durationMs?: number
|
||||
special?: string | null
|
||||
}
|
||||
|
||||
export interface VisemeEventPayload {
|
||||
id: string
|
||||
weight?: number
|
||||
durationMs?: number
|
||||
}
|
||||
|
||||
export interface ExpressionEventPayload {
|
||||
name: string
|
||||
weight?: number
|
||||
durationMs?: number
|
||||
}
|
||||
|
||||
export interface BodyEventPayload {
|
||||
clip: string
|
||||
fadeMs?: number
|
||||
durationMs?: number
|
||||
}
|
||||
|
||||
export interface MarkerEventPayload {
|
||||
key: string
|
||||
}
|
||||
|
||||
interface PerformanceEventPayloadMap {
|
||||
speech: SpeechEventPayload
|
||||
viseme: VisemeEventPayload
|
||||
expression: ExpressionEventPayload
|
||||
body: BodyEventPayload
|
||||
marker: MarkerEventPayload
|
||||
}
|
||||
|
||||
export type PerformanceEventPayload = PerformanceEventPayloadMap[keyof PerformanceEventPayloadMap]
|
||||
|
||||
export interface PerformanceEvent<T extends PerformanceEventKind = PerformanceEventKind> {
|
||||
id: string
|
||||
kind: T
|
||||
t: number // milliseconds since timeline start
|
||||
payload: PerformanceEventPayloadMap[T]
|
||||
}
|
||||
|
||||
export interface PerformanceTrack {
|
||||
id: string
|
||||
type: PerformanceEventKind
|
||||
events: PerformanceEvent[]
|
||||
}
|
||||
|
||||
export interface PerformanceTimeline {
|
||||
id: string
|
||||
lengthMs?: number
|
||||
tracks: PerformanceTrack[]
|
||||
}
|
||||
|
||||
export interface SchedulerHandlers {
|
||||
onSpeechStart?: (event: PerformanceEvent<'speech'>) => void
|
||||
onSpeechEnd?: (event: PerformanceEvent<'speech'>) => void
|
||||
onViseme?: (event: PerformanceEvent<'viseme'>) => void
|
||||
onExpression?: (event: PerformanceEvent<'expression'>) => void
|
||||
onBody?: (event: PerformanceEvent<'body'>) => void
|
||||
onMarker?: (event: PerformanceEvent<'marker'>) => void
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -32,7 +32,8 @@
|
||||
"./workers/vad": "./src/workers/vad/index.ts",
|
||||
"./workers/*": "./src/workers/*.ts",
|
||||
"./workers": "./src/workers/index.ts",
|
||||
"./utils": "./src/utils/index.ts"
|
||||
"./utils": "./src/utils/index.ts",
|
||||
"./utils/tts": "./src/utils/tts.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
|
||||
Generated
+16
@@ -720,6 +720,9 @@ importers:
|
||||
'@proj-airi/stage-ui-three':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/stage-ui-three
|
||||
'@proj-airi/stage-ui-three-performance-runtime':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/stage-ui-three-performance-runtime
|
||||
'@proj-airi/ui':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/ui
|
||||
@@ -1811,6 +1814,19 @@ importers:
|
||||
specifier: ^3.1.8
|
||||
version: 3.1.8(typescript@5.9.3)
|
||||
|
||||
packages/stage-ui-three-performance-runtime:
|
||||
dependencies:
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
||||
vue:
|
||||
specifier: ^3.5.25
|
||||
version: 3.5.25(typescript@5.9.3)
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.6.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/tauri-plugin-mcp:
|
||||
dependencies:
|
||||
'@tauri-apps/api':
|
||||
|
||||
Reference in New Issue
Block a user