ruiyun-human 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -3
- package/dist/ruiyun-human.cjs.js +264 -143
- package/dist/ruiyun-human.es.js +265 -144
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,296 @@
|
|
|
1
1
|
# shuzirenzujian
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
数字人前端自助机组件(Vue 3)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 发布 / 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 发包(npm 所有人:睿政云-陆洋)
|
|
5
11
|
npm publish
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
# 使用方安装
|
|
14
|
+
npm install ruiyun-human
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 功能一览
|
|
20
|
+
|
|
21
|
+
| 能力 | 说明 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| 说话同步 | `playTTS(text)` 调京东 TTS → 播放音频 → `onplaying` 驱动说话动画,`onended` 自动回待机 |
|
|
24
|
+
| 音色切换 | 每个 `humanId` 预置 `timbre`,父组件可覆写 |
|
|
25
|
+
| 静音控制 | 父组件改 `human.isMuted` **实时生效**,播放中静音会立即停声并回待机 |
|
|
26
|
+
| 录音转写 | `startRecord()` / `stopRecord()` 录 16kHz / 16bit / 单声道 WAV → ASR → emit `audio-ready` |
|
|
27
|
+
| 形象内置 | 组件内置 5 套预设形象,父组件只需传 `{ humanId }` |
|
|
28
|
+
| PC / 移动端 | 同一 `humanId` 配 `folder`(PC) 与 `folder_mobile`(移动端),由 `humanType` 选择 |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 快速上手
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<template>
|
|
36
|
+
<DigitalHuman
|
|
37
|
+
ref="humanRef"
|
|
38
|
+
:human="human"
|
|
39
|
+
:szraddress="'https://116.198.239.205:8443/'"
|
|
40
|
+
style="width:500px;height:500px"
|
|
41
|
+
@loaded="onLoaded"
|
|
42
|
+
@frames-loaded="onFramesLoaded"
|
|
43
|
+
@load-error="onLoadError"
|
|
44
|
+
@changed="onChanged"
|
|
45
|
+
@audio-ready="onAudioText"
|
|
46
|
+
/>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup>
|
|
50
|
+
import { ref, nextTick } from 'vue'
|
|
51
|
+
import DigitalHuman from '@/components/DigitalHuman'
|
|
52
|
+
|
|
53
|
+
const humanRef = ref(null)
|
|
54
|
+
|
|
55
|
+
// 最小配置:只传 humanId,其余字段取预设默认值
|
|
56
|
+
const human = ref({ humanId: 5 })
|
|
57
|
+
|
|
58
|
+
// 画面就绪:待机帧已加载并完成首次绘制
|
|
59
|
+
const onLoaded = (h, info) => console.log('画面已就绪:', h.title, info.total, '张待机帧')
|
|
60
|
+
// 全部帧(待机 + 说话)加载完成
|
|
61
|
+
const onFramesLoaded = (info) => console.log('全部帧加载完成:', info.allReady, `${info.broken}/${info.total} 损坏`)
|
|
62
|
+
// 待机帧一张都没出来
|
|
63
|
+
const onLoadError = (info) => console.error('图片加载失败:', info.folder)
|
|
64
|
+
const onChanged = (h) => console.log('已切换:', h.title)
|
|
65
|
+
const onAudioText = (res) => console.log('转写结果:', res.text)
|
|
66
|
+
|
|
67
|
+
// 播报(自动驱动说话动画)
|
|
68
|
+
const speak = () => {
|
|
69
|
+
humanRef.value.playTTS('您好,我是政务服务中心 AI 数字人')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 切音色 → 必须 await nextTick() 再播,否则用的是上一次的音色
|
|
73
|
+
const setTimbreAndPlay = async (timbre, text) => {
|
|
74
|
+
human.value = { ...human.value, timbre }
|
|
75
|
+
await nextTick()
|
|
76
|
+
humanRef.value.playTTS(text)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 静音开关:改 human 即可,子组件 watch 立即生效
|
|
80
|
+
const toggleMuted = async () => {
|
|
81
|
+
human.value = { ...human.value, isMuted: !human.value.isMuted }
|
|
82
|
+
await nextTick()
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> 组件根元素是 `width:100%;height:100%`,**父容器必须给出明确宽高**,否则画面会按 300×300 兜底并在控制台告警。
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Props
|
|
92
|
+
|
|
93
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
94
|
+
|---|---|---|---|---|
|
|
95
|
+
| `human` | `Object \| null` | `null` | ✅ | 数字人描述对象,最小只需 `{ humanId }`,其余字段覆盖预设默认值 |
|
|
96
|
+
| `szraddress` | `String` | `https://sqsyky.xzspj.suqian.gov.cn:8443` | ❌ | 服务根地址,同时用于 TTS / ASR / 图片资源 |
|
|
97
|
+
|
|
98
|
+
### `human` 对象字段
|
|
99
|
+
|
|
100
|
+
| 字段 | 类型 | 必填 | 默认 | 说明 |
|
|
101
|
+
|---|---|---|---|---|
|
|
102
|
+
| `humanId` | `number` | ✅ | — | 形象编号 **1~5**,见下方预设表 |
|
|
103
|
+
| `humanType` | `number` | ❌ | 按 UA 自动判断 | `1` PC 端资源;`2` 移动端资源(头部特写) |
|
|
104
|
+
| `isMuted` | `boolean` | ❌ | `false` | **`true` = 静音(不发声);`false` = 正常播放**。实时生效,播放中改也会立刻停声 |
|
|
105
|
+
| `timbre` | `number` | ❌ | 预设值 | 京东 TTS 音色编号,常用:男 `1`/`35`/`47`,女 `0`/`3`/`34`/`45`/`51`/`52` |
|
|
106
|
+
| `title` | `string` | ❌ | 预设值 | 形象名称,用于 `loaded` / `changed` 事件与日志 |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Emits
|
|
111
|
+
|
|
112
|
+
| 事件名 | 负载 | 触发时机 |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `loaded` | `(human, info)` | **画面就绪**:待机帧加载完并完成首次绘制(首帧不可用则不触发,改发 `load-error`) |
|
|
115
|
+
| `frames-loaded` | `(info)` | **全部帧**(待机 + 说话)加载完成或超时;说话帧可达上百张,通常晚于 `loaded` |
|
|
116
|
+
| `load-error` | `(info)` | 待机帧一张都没加载出来(地址错误 / 跨域 / 超时),画面出不来 |
|
|
117
|
+
| `changed` | `(human, info)` | 调用 `switchHuman()` 切换形象并加载完成后 |
|
|
118
|
+
| `paused` | — | 调用 `pause()` 后 |
|
|
119
|
+
| `resumed` | — | 调用 `resume()` 后 |
|
|
120
|
+
| `audio-ready` | `({ status, text })` | `stopRecord()` 转写完成后;录音过短(<1KB)或转写失败**不 emit** |
|
|
121
|
+
|
|
122
|
+
`info` 字段:`{ humanId, title, folder, total, broken, timedOut }`,`frames-loaded` 额外带 `allReady`。
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## defineExpose 方法
|
|
127
|
+
|
|
128
|
+
父组件通过 `ref.value.xxx()` 调用。
|
|
129
|
+
|
|
130
|
+
| 方法 | 参数 | 说明 |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `playTTS(text)` | `text: string` | 合成并播报:中止上一次 TTS → 请求音频 → 播放并驱动说话动画 → 播完回待机。**静音时直接跳过,不发起请求** |
|
|
133
|
+
| `standby()` | — | 立即切回待机动画,不发声 |
|
|
134
|
+
| `speak()` | — | 立即播放说话动画,不发声 |
|
|
135
|
+
| `stopSpeaking()` | — | 中止未完成 TTS → 停当前音频 → 跑说话结束动画 → 自动回待机 |
|
|
136
|
+
| `pause()` | — | 停声 + 冻结画面,emit `paused` |
|
|
137
|
+
| `resume()` | — | 从冻结帧继续播放,emit `resumed`(**不会续播被停掉的音频**,需重新 `playTTS`) |
|
|
138
|
+
| `switchHuman(humanId)` | `humanId: number` | 直接切形象(不经过父组件 `human`),emit `changed` |
|
|
139
|
+
| `startRecord()` | — | 开始录音(需麦克风授权),16kHz / 16bit / 单声道 |
|
|
140
|
+
| `stopRecord()` | — | 停止录音 → 过短丢弃 → ASR 转写 → emit `audio-ready` |
|
|
141
|
+
| `isReady()` | — | 返回 `boolean`:画面是否已就绪(待机帧已加载并完成首次绘制) |
|
|
142
|
+
| `isFullyLoaded()` | — | 返回 `boolean`:全部帧(待机 + 说话)是否已加载完成 |
|
|
143
|
+
| `whenReady(timeout)` | `timeout? = 15000` | 返回 `Promise<boolean>`,等画面就绪;超时 resolve `false` |
|
|
144
|
+
| `whenFullyLoaded(timeout)` | `timeout? = 30000` | 返回 `Promise<boolean>`,等全部帧加载完成;超时或存在损坏帧 resolve `false` |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 判断图片是否加载完成
|
|
149
|
+
|
|
150
|
+
组件提供「事件回调」和「主动查询」两种方式,按场景选一种即可。
|
|
151
|
+
|
|
152
|
+
### 方式一:事件回调(推荐)
|
|
153
|
+
|
|
154
|
+
```vue
|
|
155
|
+
<DigitalHuman
|
|
156
|
+
ref="humanRef"
|
|
157
|
+
:human="human"
|
|
158
|
+
@loaded="onLoaded" <!-- 画面就绪:可以开始交互 / 播报 -->
|
|
159
|
+
@frames-loaded="onFramesLoaded" <!-- 全部帧就绪:口型动画完整 -->
|
|
160
|
+
@load-error="onLoadError" <!-- 加载失败:地址错误 / 跨域 / 超时 -->
|
|
161
|
+
/>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
| 阶段 | 事件 | 含义 |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| 1 | `loaded` | 待机帧加载完并画出第一帧,**画面已可见**,此时调用 `playTTS` 是安全的 |
|
|
167
|
+
| 2 | `frames-loaded` | 待机 + 说话帧全部处理完(或 30s 超时),`info.allReady === true` 表示零损坏 |
|
|
168
|
+
| — | `load-error` | 待机帧一张都没出来,画面空白;与 `loaded` **互斥**,不会同时触发 |
|
|
169
|
+
|
|
170
|
+
`loaded` / `changed` 第一个参数仍是 human 对象(兼容旧写法),第二个参数是 `info`:
|
|
171
|
+
`{ humanId, title, folder, total, broken, timedOut }`。
|
|
172
|
+
|
|
173
|
+
### 方式二:主动查询(ref 方法)
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
humanRef.value.isReady() // boolean:画面是否已就绪
|
|
177
|
+
humanRef.value.isFullyLoaded() // boolean:全部帧(待机+说话)是否加载完
|
|
178
|
+
|
|
179
|
+
await humanRef.value.whenReady(10000) // Promise<boolean>,等画面出来
|
|
180
|
+
await humanRef.value.whenFullyLoaded(30000) // Promise<boolean>,等全部帧
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
典型用法:切换形象后等画面出来再播报
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
const switchAndSpeak = async (humanId, text) => {
|
|
187
|
+
human.value = { ...human.value, humanId } // 触发重新加载
|
|
188
|
+
const ok = await humanRef.value.whenReady(10000)
|
|
189
|
+
if (!ok) {
|
|
190
|
+
console.warn('数字人加载超时')
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
humanRef.value.playTTS(text)
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> 说话帧可达上百张,`isReady()` 通常几百毫秒内为 true,`isFullyLoaded()` 要等全部下载完。
|
|
198
|
+
> **播报不依赖 `isFullyLoaded()`**——说话帧没下完时动画会自动回退待机帧,不会白屏。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 预设形象表
|
|
203
|
+
|
|
204
|
+
| humanId | title | folder (PC) | folder_mobile (移动端) | timbre |
|
|
205
|
+
|---|---|---|---|---|
|
|
206
|
+
| 1 | 西装动漫男 | `xiangyu-green-suit` | `xiangyu-green-suit-header` | 47 |
|
|
207
|
+
| 2 | 西装动漫女 | `anime-woman` | `anime-woman-header` | 45 |
|
|
208
|
+
| 3 | 真人形象男 | `real-man` | `real-man-header` | 47 |
|
|
209
|
+
| 4 | 真人形象女 | `real-woman` | `real-woman-header` | 34 |
|
|
210
|
+
| 5 | Q 版形象 | `xiangyu-q` | `xiangyu-q-header` | 47 |
|
|
211
|
+
|
|
212
|
+
图片实际路径:`{szraddress}/szrimage/{folder}/standby|speak/...`
|
|
213
|
+
|
|
214
|
+
### humanType 资源选择规则
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
1. humanType = 2 且 folder_mobile 存在 → 用 folder_mobile
|
|
218
|
+
2. humanType = 1 → 用 folder
|
|
219
|
+
3. humanType 未传 → 按 UA 自动判断(Android / iPhone / iPad / HarmonyOS → 移动端)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
> `folder_mobile` 未配置时永远回退到 `folder`,不会 404。
|
|
223
|
+
> `humanId` 或 `humanType` 变化才会重新加载帧;只改 `isMuted` / `timbre` 不会重载,画面无闪烁。
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 依赖
|
|
228
|
+
|
|
229
|
+
| 包 | 版本 | 用途 |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `vue` | `^3.4` | Composition API |
|
|
232
|
+
| `js-audio-recorder` | `^1.0.7` | 浏览器录音,固定 16kHz / 16bit / 单声道(与京东 ASR 匹配) |
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 接口对照
|
|
237
|
+
|
|
238
|
+
| 服务 | 方法 | URL | 请求体 | 返回 |
|
|
239
|
+
|---|---|---|---|---|
|
|
240
|
+
| 京东 TTS | POST | `{szraddress}/web/voice/jd/text-to-speech` | `{ text, user, timbre }` + `clientid` / `tenant-id` 头 | 音频二进制(裸 PCM 自动补 WAV 头)或 JSON(兼容 `data.url` / `data.audio` / `data.base64`) |
|
|
241
|
+
| 京东 ASR | POST | `{szraddress}/web/voice/jd/speech-to-text?domain=general&sampleRate=16000` | `multipart/form-data: file=<wav>` + 同上头 | JSON,兼容 `text` / `data.text` / `result` |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## ⚠️ 注意事项
|
|
246
|
+
|
|
247
|
+
### 1. `isMuted` 语义:**true = 静音**
|
|
248
|
+
|
|
249
|
+
早期版本语义是反的(`true` 反而出声),现已修正为符合直觉的语义。升级后原来写 `isMuted: true` 表示"开启声音"的调用方需要改成 `false`。
|
|
250
|
+
|
|
251
|
+
不传该字段时默认 `false`(正常播放)。
|
|
252
|
+
|
|
253
|
+
静音是**实时**的:
|
|
254
|
+
|
|
255
|
+
- 播放前静音 → `playTTS` 直接跳过,不发起合成请求
|
|
256
|
+
- 合成中静音 → 音频合成回来后被丢弃,不播放
|
|
257
|
+
- 播放中静音 → 立即中止 TTS、停掉声音并切回待机动画
|
|
258
|
+
|
|
259
|
+
### 2. 切音色 / 切形象后必须 `await nextTick()`
|
|
260
|
+
|
|
261
|
+
Vue 响应式更新是**微任务**。父组件改完 `human` 同步立刻调 `playTTS()`,子组件 `props.human` 还是旧值,会用上一次的 `timbre` / `isMuted`。
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
// ❌ 会用旧的 timbre
|
|
265
|
+
human.value = { ...human.value, timbre: 47 }
|
|
266
|
+
humanRef.value.playTTS(text)
|
|
267
|
+
|
|
268
|
+
// ✅
|
|
269
|
+
human.value = { ...human.value, timbre: 47 }
|
|
270
|
+
await nextTick()
|
|
271
|
+
humanRef.value.playTTS(text)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`playTTS` 内部也会再读一次 `props.human` 兜底,但建议父组件自己养成 `await nextTick()` 的习惯。
|
|
275
|
+
|
|
276
|
+
### 3. 切换形象 / 再次 playTTS 会中止上一次 TTS
|
|
277
|
+
|
|
278
|
+
组件内部有完整取消链:`playTTS` 开头、`switchHuman` / `init` 重载帧前、`onUnmounted` 都会 `abortTTS()`,不会出现多个 TTS 叠加。`pause()` 同样会停掉正在播的声音。
|
|
279
|
+
|
|
280
|
+
### 4. 录音过短会被静默丢弃
|
|
281
|
+
|
|
282
|
+
`stopRecord()` 内 `blob.size < 1000` 直接 return,既不请求 ASR 也不 emit `audio-ready`(1KB ≈ 0.5 秒语音,用于过滤误触)。
|
|
283
|
+
|
|
284
|
+
### 5. 帧图加载失败有明确事件,不再只是白屏
|
|
285
|
+
|
|
286
|
+
- **单张**帧图失败:不抛错,绘制时自动跳过该帧(`pickDrawable` 找邻近可用帧),动画不中断。
|
|
287
|
+
- **待机帧全部**失败:emit `load-error`(不发 `loaded`),父组件可据此显示错误态;控制台也会打印 `[DigitalHuman] ❌ 帧加载失败` 与 folder 地址。
|
|
288
|
+
- 全部帧处理完后 emit `frames-loaded`,用 `info.broken` 可知道损坏了几张。
|
|
289
|
+
|
|
290
|
+
若整体白屏,先确认父容器宽高非 0,再看控制台日志与 `load-error` 是否触发。
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## npm publish
|
|
295
|
+
|
|
296
|
+
睿政云-陆洋
|