npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

v0.2.5

Published

VZai 数字人组件:Vue 3 语音对话组件(ASR/TTS + S0–S4 状态机),自包含样式、零第三方 UI 依赖。

Readme

@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 与有声音频输出。使用 autoStartptt 时,宿主页面应先显示点击蒙版,在点击处理函数中 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_startduration=0locate(0,0) 走 fallback 命中末块;随后 duration 涨成小值、position 更小,position/duration 极小 → 退回首块。
  • 上下跳跃抖动duration 离散跳档、position 平滑上涨,每帧 position/duration 忽小忽大 → 高亮块在首/中/末来回摆、回退。

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

历史语速 × 总权重估出整段总时长喂给 locateposition 用组件真实值。一轮内语速固定 → 估算总时长固定 → 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.durationtts_end 前随入队音频增长、离散跳档,position 则平滑上涨;把前者当总时长喂入会让二者比值忽小忽大,高亮块抖动回退。改用 speakRate × totalWeight:一轮内 speakRate 固定 → 估算总时长固定 → locateposition 单调递增。
  • 单调护栏:仅当 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),插槽参数提供 posestateonOpeningEndopening 只在组件初始化时展示一次;开场音频通过后端 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 }">
    <img src="/avatar-speaking.webp" :alt="state" />
  </template>
</VZai>

示例姿态视频

组件本身不内置默认姿态,但包内随附一组「V仔」示例姿态视频,供快速测试:dist/poses/vzai-opening.mp4 / vzai-standing.mp4 / vzai-speaking.mp4listening / 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