@financial-freedom/i-spark-ai-digital

VZai 数字人组件:Vue 3 语音对话组件(ASR/TTS + S0–S4 状态机),自包含样式、零第三方 UI 依赖。
107
Install
npm install @financial-freedom/i-spark-ai-digital
Latest Version:0.2.5
Vue:3
License:MIT
Last Updated:Jul 14, 2026
Links: npm
Maintainer: jsoon

@financial-freedom/i-spark-ai-digital

VZai 数字人组件 —— Vue 3 语音对话组件,内置 ASR/TTS 驱动的 S0–S4 状态机,形象姿态由使用者通过插槽提供。样式自包含、零第三方 UI 依赖,仅以 Vue 3 为 peer 依赖。

组件只负责前端采集 / 播放 / 渲染,权威状态机与 ASR/TTS 在后端(FastAPI + DashScope),二者经 WebSocket 通信。后端不在本包内。

安装

pnpm add @financial-freedom/i-spark-ai-digital
# 或 npm i / yarn add

peer 依赖:vue@^3.5。

使用

样式已内联进 JS,import 组件即自动注入,无需额外引入 CSS。

按需引入

<script setup lang="ts">
import { VZai } from '@financial-freedom/i-spark-ai-digital'
</script>

<template>
  <VZai
    base-api="https://your-backend-host"
    :size="200"
    auto-start
    @state-change="(s) => console.log(s)"
  >
    <template #opening>
      <video src="/avatar-opening.mp4" autoplay loop muted playsinline />
    </template>
    <template #standing>
      <video src="/avatar-standing.mp4" autoplay loop muted playsinline />
    </template>
    <template #listening>
      <video src="/avatar-listening.mp4" autoplay loop muted playsinline />
    </template>
    <template #thinking>
      <video src="/avatar-thinking.mp4" autoplay loop muted playsinline />
    </template>
    <template #speaking>
      <video src="/avatar-speaking.mp4" autoplay loop muted playsinline />
    </template>
  </VZai>
</template>

全局注册(插件)

import { createApp } from 'vue'
import VZaiPlugin from '@financial-freedom/i-spark-ai-digital'
import App from './App.vue'

createApp(App).use(VZaiPlugin).mount('#app')
// 之后模板里可直接用 <VZai />

CDN(UMD)

<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/@financial-freedom/i-spark-ai-digital"></script>
<script>
  Vue.createApp({}).use(VZaiDigital.default).mount('#app')
</script>

AudioWorklet 资产

麦克风降采样运行在独立的音频线程,必须以独立 URL 文件加载,无法被打包内联。包内随附 dist/vzai-worklet.js,请将其放到你站点可访问的路径,并通过 worklet-url 指定:

import workletUrl from '@financial-freedom/i-spark-ai-digital/worklet?url' // Vite 写法
<VZai base-api="https://your-backend-host" :worklet-url="workletUrl" />

未指定时默认从 ${BASE_URL}vzai-worklet.js 加载(即站点根目录),你也可手动把该文件拷到 public/。

Props

Prop 类型 默认 说明
baseApi string http://127.0.0.1:8765 后端基础接口地址;为空时回退环境变量 VITE_APP_BASE_API,组件默认由它派生 /digital/ws/vzai 与 /digital/tts/stream
baseUrl string /(部署 base 路径) 运行期资产路径前缀;为空时回退环境变量 BASE_URL。默认用于派生 ${baseUrl}vzai-worklet.js,姿态素材由具名插槽自行提供
workletUrl string ${baseUrl}vzai-worklet.js 降采样 AudioWorklet 脚本地址
autoStart boolean false 挂载即建连开始对话(getUserMedia 授权仍需用户首次交互)
openingText string 你好,我是V仔,很高兴见到你 开场姿态实时合成播报文本;设为空字符串则不播开场音频
openingTtsUrl string 由 baseApi 推导 开场 TTS HTTP 接口;显式传入时优先,否则使用 ${baseApi}/digital/tts/stream?text=...,后端使用配置的 TTS_VOICE 保持声音一致
size number | string 160 形象尺寸,数字按 px 处理,也可传任意 CSS 长度
ptt boolean false 手动触发模式(按住说话、松开后一次性识别,识别完成才进入思考),适用于移动端
keywords { keyword: string; callback: () => void }[] [] 关键词注册表,ASR 命中即触发回调(不进入正常问答流程)
agent AgentConfig undefined 智能体接入配置。建连时随 hello 帧透传给服务端,逐字段覆盖服务端 .env 默认值;不传或某字段缺省时回退服务端自身配置。token / appId 会暴露在客户端,仅限可信环境使用

回调 Props

onStateChange(state)、onError(message)、onClose()、onReplyStart()、onReplyChunk(text)、onReplyEnd()、onEvent(event)(服务端原始事件 + 客户端合成的 audio_first 透传,供自建面板 / 埋点)。

agent 类型

import { type AgentConfig } from '@financial-freedom/i-spark-ai-digital'

const agent: AgentConfig = {
  baseApi: 'https://your-agent-host/oms', // 智能体后端基础地址
  appId: '...',                          // 应用 ID
  userName: 'digital_vzai',              // 用户名
  token: '...',                          // 鉴权 token(会暴露在客户端,仅可信环境)
  systemCode: '...',                     // 系统编码(可选)
  fileInterType: 0,                      // 文件交互类型(可选)
}

暴露方法(ref)

const vzai = ref<InstanceType<typeof VZai>>()
// vzai.value.start() / stop() / wakeup() / interrupt() / pttStart() / pttEnd()
// await vzai.value.resumeMedia() // 必须在真实点击/触摸处理函数中调用;成功返回 true
// vzai.value.state(只读)/ holding(只读)/ connecting(只读)

微信内置浏览器(包括鸿蒙 NEXT)会拦截自动播放的 Web Audio 与有声音频输出。使用 autoStart 或 ptt 时,宿主页面应先显示点击蒙版,在点击处理函数中 await resumeMedia(),仅当返回 true 后撤掉蒙版。resumeMedia 在用户手势内对一段运行时合成的近静音 WAV 调用 HTMLAudioElement.play() 一次以打通音频输出路由,同时 resume() 播放上下文并逐个 play() 姿态视频解锁自动播放。组件在解锁成功前保留 opening 姿态、不提前请求开场 TTS,避免被静音消费掉;解锁后 TTS 一律走 Web Audio 流式播放(首帧即播,支持 tts_play 同步滚动)。

语音-文字同步滚动

第三方界面常需让回答文字随语音播放同步滚动 / 高亮。组件经 on-event 透传两类事实信号:

  • tts_text_chunk { text } —— 文本增量(逐块到达)。
  • tts_play { position, duration, ended? } —— 播放时钟:position 已播秒数、duration 已缓冲秒数、ended 终态帧。

两者都不含“哪块文本对应哪段音频”的逐块边界——那是估计,由包内独立的 createTtsLocator helper 承担:把文本块按字符权重等比映射到真实总时长,估算每块起止,再据 position 定位当前块。helper 不依赖组件、不持响应式状态,按需引入:

import { createTtsLocator, type TtsSegment } from '@financial-freedom/i-spark-ai-digital'

const loc = createTtsLocator()
// loc.push(text)            // on tts_text_chunk
// loc.reset()               // on tts_start
// loc.locate(position, dur) // → { index, text, start, end } | null
// loc.segments              // 当前段表(只读)
// loc.totalWeight           // 当前总权重

⚠️ 不要把组件 tts_play.duration 直接喂给 locate

locate(position, duration) 的 duration 语义是整段音频的真实总时长(helper 用 (cumWeight / totalWeight) × duration 给每块定终点)。而组件流式下 tts_play.duration 是已缓冲到的秒数——随音频块到达离散跳档,只有 ended 那一帧才等于真实总时长。直接喂入会出两个症状:

  • 初始化定位到末块再退回首块:tts_start 时 duration=0 → locate(0,0) 走 fallback 命中末块;随后 duration 涨成小值、position 更小,position/duration 极小 → 退回首块。
  • 上下跳跃抖动:duration 离散跳档、position 平滑上涨,每帧 position/duration 忽小忽大 → 高亮块在首/中/末来回摆、回退。

正确用法:语速 × 总权重 估固定总时长

用历史语速 × 总权重估出整段总时长喂给 locate,position 用组件真实值。一轮内语速固定 → 估算总时长固定 → locate(position, 固定值) 随 position 单调递增,不抖不回退。ended 帧用 position / totalWeight 反推语速、snap 末块并校准下轮语速:

<script setup lang="ts">
import { ref } from 'vue'
import { VZai, createTtsLocator, type TtsSegment } from '@financial-freedom/i-spark-ai-digital'

const segs = ref<TtsSegment[]>([])
const activeIdx = ref(-1)
const loc = createTtsLocator()
let speakRate = 0.25                 // sec/字符,≈4 字/秒首轮兜底

function onEvent(e: { type: string; [k: string]: unknown }) {
  switch (e.type) {
    case 'tts_start':               // 新一轮回答:复位
      loc.reset()
      segs.value = []
      activeIdx.value = -1
      break
    case 'tts_text_chunk':           // 入块
      loc.push((e.text as string) || '')
      segs.value = [...loc.segments]
      break
    case 'tts_play': {              // ~10Hz 进度帧
      const pos = (e.position as number) || 0
      if (e.ended) {                // 终态:snap 末块 + 校准下轮语速
        if (loc.totalWeight > 0) speakRate = pos / loc.totalWeight
        activeIdx.value = loc.segments.length - 1
      } else {
        const estTotal = speakRate * loc.totalWeight   // 一轮内固定,不随缓冲抖
        const r = loc.locate(pos, estTotal)            // position 真实,duration 固定估算
        if (r && r.index >= activeIdx.value) activeIdx.value = r.index // 单调护栏:只前进
      }
      break
    }
  }
}
</script>

<template>
  <VZai base-api="https://your-backend-host" :on-event="onEvent" />
  <p class="reply">
    <span
      v-for="s in segs"
      :key="s.index"
      :class="{ active: s.index === activeIdx }"
    >{{ s.text }}</span>
  </p>
</template>

要点:

  • 为何估总时长而非用 duration:组件 tts_play.duration 在 tts_end 前随入队音频增长、离散跳档,position 则平滑上涨;把前者当总时长喂入会让二者比值忽小忽大,高亮块抖动回退。改用 speakRate × totalWeight:一轮内 speakRate 固定 → 估算总时长固定 → locate 随 position 单调递增。
  • 单调护栏:仅当 r.index >= activeIdx 才前进高亮。文本块仍在陆续到达、totalWeight 增大时 locate 可能临时返回更早的块,护栏兜住防回退;position >= estTotal(估算偏低)时落末块、原地保持,只有 ended 才 snap 末块,杜绝抢跑。
  • 语速校准:ended 帧用 position / totalWeight 反推 speakRate,下一轮复用;首轮用 0.25(≈4 字/秒)兜底。要跨句稳定可记多轮均值。
  • 逐字而非逐块:示例按块高亮。要逐字,用 r.start / r.end 在该块文本内再按字符比例细分即可。
  • 局限:估计而非精确,标点停顿、数字/日期展开、中英混排会漂移;纯中文连续句误差小。要逐词精确需换带词边界回调的 TTS 或上 forced-alignment,届时用真实时间戳、绕过本 helper。

自定义形象

组件不再提供内置默认姿态。使用者必须通过与状态对应的具名插槽提供全部姿态(opening / standing / listening / thinking / speaking),插槽参数提供 pose、state 与 onOpeningEnd。opening 只在组件初始化时展示一次;开场音频通过后端 TTS 实时合成并同步播放,只有开场音频播完后才会稍作延时并切换为 standing,不等待 opening 动画结束。建议 opening 动画自身设置 loop,避免音频未结束前画面停住:

<VZai base-api="https://your-backend-host">
  <template #opening>
    <video
      src="/avatar-opening.mp4"
      autoplay
      loop
      muted
      playsinline
    />
  </template>

  <template #standing>
    <video src="/avatar-standing.mp4" autoplay loop muted playsinline />
  </template>

  <template #listening>
    <video src="/avatar-listening.mp4" autoplay loop muted playsinline />
  </template>

  <template #thinking>
    <video src="/avatar-thinking.mp4" autoplay loop muted playsinline />
  </template>

  <template #speaking="{ state }">
    
  </template>
</VZai>

示例姿态视频

组件本身不内置默认姿态,但包内随附一组「V仔」示例姿态视频,供快速测试:dist/poses/vzai-opening.mp4 / vzai-standing.mp4 / vzai-speaking.mp4(listening / thinking 可复用 standing)。两种引用方式:

1)经 ?url 引入(Vite,推荐) —— 由打包器生成哈希资产地址,无需手动拷文件:

<script setup lang="ts">
import openingUrl from '@financial-freedom/i-spark-ai-digital/poses/vzai-opening.mp4?url'
import standingUrl from '@financial-freedom/i-spark-ai-digital/poses/vzai-standing.mp4?url'
import speakingUrl from '@financial-freedom/i-spark-ai-digital/poses/vzai-speaking.mp4?url'
</script>

<template>
  <VZai base-api="https://your-backend-host" :auto-start="true">
    <template #opening>
      <video :src="openingUrl" autoplay loop muted playsinline />
    </template>
    <template #standing>
      <video :src="standingUrl" autoplay loop muted playsinline />
    </template>
    <template #listening>
      <video :src="standingUrl" autoplay loop muted playsinline />
    </template>
    <template #thinking>
      <video :src="standingUrl" autoplay loop muted playsinline />
    </template>
    <template #speaking>
      <video :src="speakingUrl" autoplay loop muted playsinline />
    </template>
  </VZai>
</template>

2)自托管 —— 把上述文件拷到站点 public/,直接以根路径引用:

cp node_modules/@financial-freedom/i-spark-ai-digital/dist/poses/*.mp4 public/
<template>
  <VZai base-api="https://your-backend-host" :auto-start="true">
    <template #opening>
      <video src="/vzai-opening.mp4" autoplay loop muted playsinline />
    </template>
    <!-- standing / listening / thinking → /vzai-standing.mp4;speaking → /vzai-speaking.mp4 -->
  </VZai>
</template>

正式上线建议替换为自有形象素材。

完整文档

包内随附一份独立 HTML 文档 dist/VZai.html(亦可经子路径 @financial-freedom/i-spark-ai-digital/docs 引用),在浏览器中打开即可查看组件的完整说明:

open node_modules/@financial-freedom/i-spark-ai-digital/dist/VZai.html

License

MIT

Related Packages

vue-text-to-speech

Vue 3 text-to-speech plugin with composables, UI components, and multi-provider...

1,751 19
ebt-vue3

Vue3 Library for SuttaCentral Voice EBT-Sites

406,555 0
@naersen/tts-rich-text

Reusable Vue 3 rich-text editor for TTS workflows

2,492
blind-friendly-library

BFL - Blind friendly library

4,577 1
@pouchy_ai/companion-sdk

Embed the Pouchy companion — chat, voice, tools, memory, live world-state, insta...

14,543

Version History

Version Released Vue License
0.2.5 ^3.5.0 MIT
0.2.4 ^3.5.0 MIT
0.2.3 ^3.5.0 MIT
0.2.2 ^3.5.0 MIT
0.2.1 ^3.5.0 MIT