@zeewain/3d-avatar-sdk 2.1.2 → 2.1.4

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 CHANGED
@@ -48,7 +48,7 @@
48
48
  > - 示例:SDK版本为 2.1.3,则 `assetsUrl` 应配置为:`https://cdn.zeewain3d.com/webgl/2.1`
49
49
  >
50
50
  > **3. 动作资源扩展(可选)**
51
- > - 动作资源可单独从开放平台下载**(版本号前两位也必须跟SDK版本号一致)**,下载后解压到资源包所在目录即可。
51
+ > - 动作资源可单独从开放平台下载(**版本号前两位也必须跟SDK版本号一致**),下载后解压到资源包所在目录即可(**覆盖合并**)。
52
52
  > - 新增的动作将自动集成到现有资源体系中。
53
53
  >
54
54
  > **⚠️ 版本兼容性要求**
@@ -516,6 +516,7 @@ interface IAvatarSDKConfig {
516
516
  env?: 'dev' | 'test' | 'prod' | 'custom'; // 运行环境,默认'prod'
517
517
  apiUrl?: string; // 自定义API地址(env='custom'时需要)
518
518
  idleMotionList?: string[]; // 待机动作编码列表,随机播放(排重方式),可选
519
+ operationTimeout?: number; // 操作超时时间(毫秒),默认30000ms,可选
519
520
 
520
521
  // 回调函数
521
522
  onProgress?: (progress: number) => void; // 加载进度回调,可选
@@ -690,9 +691,39 @@ interface IBroadcastParams {
690
691
  **🚀 播报队列特性:**
691
692
  - **智能排队**:多次追加播报自动按调用顺序排队
692
693
  - **顺序保证**:即使网络响应乱序,也确保音频按序播报
693
- - **高效处理**:支持并发请求,同时保证播报顺序
694
+ - **串行请求**:队列中的任务按顺序依次请求,避免占用过多并发
694
695
  - **自动清理**:已播报内容自动从队列中移除,节省内存
695
696
 
697
+ **💡 队列播报最佳实践(GPT流式场景):**
698
+
699
+ 针对大模型流式响应的播报队列场景,推荐以下分段策略以优化首句响应速度和服务调用次数:
700
+
701
+ | 段落序号 | 建议字数 | 说明 |
702
+ |:--------:|:--------:|:-----|
703
+ | 第1段 | 20-30字 | 快速响应,减少用户等待时间 |
704
+ | 第2段 | 80字左右 | 适中长度,平衡响应速度 |
705
+ | 第3段及以后 | 200-250字 | 较长文本,节省服务调用次数 |
706
+
707
+ ```javascript
708
+ // 示例:智能分段播报
709
+ async function smartBroadcast(textStream) {
710
+ const segments = splitTextSmartly(textStream); // 按上述策略分段
711
+
712
+ for (let i = 0; i < segments.length; i++) {
713
+ await sdk.startBroadcast({
714
+ type: BroadcastType.TEXT,
715
+ humanCode: 'avatar001',
716
+ text: segments[i],
717
+ voiceCode: 'VOICE001',
718
+ volume: 1.0,
719
+ isSubtitle: false
720
+ }, i > 0); // 第一段 isAppend=false,后续 isAppend=true
721
+ }
722
+ }
723
+ ```
724
+
725
+ > **优势**:此策略能在不影响播放流畅度的情况下,有效减少API调用次数,降低服务成本。
726
+
696
727
  **使用示例:**
697
728
  ```javascript
698
729
  // 开始新的播报(清空队列)
@@ -806,9 +837,10 @@ getBroadcastStatus(): {
806
837
  hasReceivedAudio: boolean;
807
838
  queueInfo?: {
808
839
  totalTasks: number; // 队列中总任务数
840
+ pendingTasks: number; // 等待请求的任务数
809
841
  requestingTasks: number; // 正在请求中的任务数
810
842
  completedTasks: number; // 已完成的任务数
811
- failedTasks: number; // 失败的任务数
843
+ failedTasks: number; // 失败的任务数
812
844
  totalPendingResponses: number; // 待发送的响应总数
813
845
  currentSendingSequence: number; // 当前发送的序号
814
846
  };
@@ -823,6 +855,7 @@ getBroadcastStatus(): {
823
855
  - `hasReceivedAudio`: 是否已收到至少一条音频数据
824
856
  - `queueInfo`: 队列详细信息
825
857
  - `totalTasks`: 队列中的总任务数
858
+ - `pendingTasks`: 等待请求的任务数(尚未开始请求)
826
859
  - `requestingTasks`: 正在发起SSE请求的任务数
827
860
  - `completedTasks`: 已完成的任务数(即将被清理)
828
861
  - `failedTasks`: 失败的任务数
@@ -838,6 +871,7 @@ console.log('播报服务状态:', {
838
871
  活跃状态: status.isActive,
839
872
  正在生成: status.isGeneratingAudio,
840
873
  队列任务数: status.queueInfo?.totalTasks || 0,
874
+ 等待请求: status.queueInfo?.pendingTasks || 0,
841
875
  待发送响应: status.queueInfo?.totalPendingResponses || 0
842
876
  });
843
877
 
@@ -845,7 +879,7 @@ console.log('播报服务状态:', {
845
879
  setInterval(() => {
846
880
  const status = sdk.getBroadcastStatus();
847
881
  if (status.queueInfo && status.queueInfo.totalTasks > 0) {
848
- console.log(`队列进度: ${status.queueInfo.completedTasks}/${status.queueInfo.totalTasks} 已完成`);
882
+ console.log(`队列进度: 等待${status.queueInfo.pendingTasks} / 请求中${status.queueInfo.requestingTasks} / 已完成${status.queueInfo.completedTasks}`);
849
883
  }
850
884
  }, 1000);
851
885
  ```
@@ -16,7 +16,7 @@
16
16
  "lint:all-fix": "npm run lint:fix && npm run lint:css-fix"
17
17
  },
18
18
  "dependencies": {
19
- "@zeewain/3d-avatar-sdk": "^2.1.2",
19
+ "@zeewain/3d-avatar-sdk": "^2.1.4",
20
20
  "core-js": "^3.8.3",
21
21
  "element-ui": "^2.15.13",
22
22
  "vue": "^2.6.14"
@@ -24,7 +24,7 @@
24
24
  "@element-plus/icons-vue": "^2.3.1",
25
25
  "@vueuse/core": "^13.5.0",
26
26
  "@vueuse/integrations": "^13.5.0",
27
- "@zeewain/3d-avatar-sdk": "^2.1.2",
27
+ "@zeewain/3d-avatar-sdk": "^2.1.4",
28
28
  "dayjs": "^1.11.13",
29
29
  "element-plus": "^2.10.4",
30
30
  "vite-plugin-html": "^3.2.2",
@@ -463,6 +463,7 @@ async function handleTextBroadcast(params: {
463
463
  voiceCode?: string;
464
464
  speed?: number;
465
465
  broadcastMotionString?: string;
466
+ isAppend?: boolean; // 队列播报模式下会传递此参数
466
467
  }): Promise<void> {
467
468
  if (!sdk.value) return;
468
469
 
@@ -481,9 +482,12 @@ async function handleTextBroadcast(params: {
481
482
  motionPlayMode: 'random'
482
483
  };
483
484
 
484
- console.warn('handleTextBroadcast: ', broadcastParams, sdkStatus.isStartBroadcast);
485
- await sdk.value.startBroadcast(broadcastParams, sdkStatus.isStartBroadcast);
486
- addLog('文本播报已开始', 'success');
485
+ // 队列播报模式下使用传入的isAppend参数,单次播报模式下使用sdkStatus.isStartBroadcast
486
+ const isAppend = params.isAppend !== undefined ? params.isAppend : sdkStatus.isStartBroadcast;
487
+
488
+ console.warn('handleTextBroadcast: ', broadcastParams, isAppend);
489
+ addLog(`${isAppend ? '追加播报' : '开始播报'}:${JSON.stringify({'isAppend': isAppend})}`, 'success');
490
+ await sdk.value.startBroadcast(broadcastParams, isAppend);
487
491
  // ElMessage.success('文本播报已开始');
488
492
  } catch (error) {
489
493
  const message = error instanceof Error ? error.message : String(error);
@@ -54,7 +54,16 @@
54
54
 
55
55
  <!-- 文本播报表单 -->
56
56
  <div v-if="broadcastMode === 'text'" class="form-content">
57
- <el-form :model="textFormData" label-width="80px" size="default">
57
+ <!-- 文本播报子模式选择 -->
58
+ <div class="text-mode-tabs">
59
+ <el-tabs v-model="textBroadcastMode" type="card">
60
+ <el-tab-pane label="单次播报" name="single" />
61
+ <el-tab-pane label="队列播报" name="queue" />
62
+ </el-tabs>
63
+ </div>
64
+
65
+ <!-- 单次播报表单 -->
66
+ <el-form v-if="textBroadcastMode === 'single'" :model="textFormData" label-width="80px" size="default">
58
67
  <el-form-item label="播报文本" required>
59
68
  <el-input
60
69
  v-model="textFormData.text"
@@ -129,6 +138,120 @@
129
138
  </div>
130
139
  </el-form-item>
131
140
  </el-form>
141
+
142
+ <!-- 队列播报表单 -->
143
+ <el-form v-if="textBroadcastMode === 'queue'" :model="queueFormData" label-width="80px" size="default">
144
+ <el-form-item label="队列文本" required>
145
+ <el-input
146
+ v-model="queueFormData.queueTexts"
147
+ type="textarea"
148
+ :rows="8"
149
+ placeholder="请输入多段播报文本,每行一段(模拟GPT流式输出)..."
150
+ :disabled="!sdkStatus.canBroadcast || isQueueBroadcasting"
151
+ maxlength="2000"
152
+ show-word-limit
153
+ />
154
+ <div class="input-help">
155
+ <span class="help-text">
156
+ 每行一段文本,将按顺序依次播报,默认每间隔500ms调用一次SDK
157
+ </span>
158
+ </div>
159
+ </el-form-item>
160
+
161
+ <el-form-item label="音色编码" required>
162
+ <el-input
163
+ v-model="queueFormData.voiceCode"
164
+ placeholder="请输入音色编码"
165
+ prefix-icon="microphone"
166
+ :disabled="!sdkStatus.canBroadcast || isQueueBroadcasting"
167
+ />
168
+ </el-form-item>
169
+
170
+ <el-form-item label="音量" required>
171
+ <el-slider
172
+ v-model="queueFormData.volume"
173
+ :min="0"
174
+ :max="1"
175
+ :step="0.1"
176
+ :disabled="!sdkStatus.canBroadcast || isQueueBroadcasting"
177
+ show-input
178
+ />
179
+ </el-form-item>
180
+
181
+ <el-form-item label="语速">
182
+ <el-slider
183
+ v-model="queueFormData.speed"
184
+ :min="0.5"
185
+ :max="2.0"
186
+ :step="0.1"
187
+ :disabled="!sdkStatus.canBroadcast || isQueueBroadcasting"
188
+ show-input
189
+ />
190
+ </el-form-item>
191
+
192
+ <el-form-item label="动作列表">
193
+ <el-input
194
+ v-model="queueFormData.broadcastMotionString"
195
+ placeholder="请输入动作列表"
196
+ :disabled="isQueueBroadcasting"
197
+ />
198
+ <div class="input-help">
199
+ <span class="help-text">
200
+ 动作编码列表,多个动作编码用逗号分隔
201
+ </span>
202
+ </div>
203
+ </el-form-item>
204
+
205
+ <el-form-item label="发送间隔">
206
+ <el-input-number
207
+ v-model="queueFormData.interval"
208
+ :min="100"
209
+ :max="5000"
210
+ :step="100"
211
+ :disabled="isQueueBroadcasting"
212
+ />
213
+ <span class="interval-unit">毫秒</span>
214
+ <div class="input-help">
215
+ <span class="help-text">
216
+ 每段文本之间的发送间隔(模拟GPT流式输出),建议100-1000ms
217
+ </span>
218
+ </div>
219
+ </el-form-item>
220
+
221
+ <!-- 队列播报进度 -->
222
+ <el-form-item v-if="isQueueBroadcasting || queueProgress.total > 0" label="队列进度">
223
+ <div class="queue-progress">
224
+ <el-progress
225
+ :percentage="queueProgressPercent"
226
+ :status="queueProgress.current === queueProgress.total ? 'success' : ''"
227
+ />
228
+ <span class="progress-text">
229
+ 已发送{{ queueProgress.current }} / 总数{{ queueProgress.total }}
230
+ </span>
231
+ </div>
232
+ </el-form-item>
233
+
234
+ <el-form-item class="action-buttons">
235
+ <div class="action-buttons-container">
236
+ <el-button
237
+ type="primary"
238
+ :loading="isQueueBroadcasting"
239
+ :disabled="!sdkStatus.canBroadcast || !queueFormData.queueTexts.trim() || !queueFormData.voiceCode || !globalConfig.avatarCode || isQueueBroadcasting"
240
+ @click="handleQueueBroadcast"
241
+ >
242
+ {{ isQueueBroadcasting ? `播报中 (${queueProgress.current}/${queueProgress.total})...` : '执行队列播报' }}
243
+ </el-button>
244
+ <el-button
245
+ type="info"
246
+ size="default"
247
+ style="width: 170px;"
248
+ :disabled="isQueueBroadcasting"
249
+ @click="handleGetBroadcastStatus">
250
+ 获取播报状态
251
+ </el-button>
252
+ </div>
253
+ </el-form-item>
254
+ </el-form>
132
255
  </div>
133
256
 
134
257
  <!-- 音频播报表单 -->
@@ -295,7 +418,7 @@
295
418
  </template>
296
419
 
297
420
  <script setup lang="ts">
298
- import { ref, reactive, watch, onMounted } from 'vue';
421
+ import { ref, reactive, watch, onMounted, computed } from 'vue';
299
422
  import { ElMessage } from 'element-plus';
300
423
  import { VideoPause, VideoPlay, Close, Refresh } from '@element-plus/icons-vue';
301
424
  import { loadFromCache, saveToCache } from '@/utils';
@@ -323,12 +446,15 @@ const emit = defineEmits<{
323
446
 
324
447
  // 本地缓存键名常量
325
448
  const BROADCAST_MODE_CACHE_KEY = 'broadcast-mode';
449
+ const TEXT_BROADCAST_MODE_CACHE_KEY = 'text-broadcast-mode';
326
450
  const TEXT_FORM_CACHE_KEY = 'broadcast-text-form-data';
451
+ const QUEUE_FORM_CACHE_KEY = 'broadcast-queue-form-data';
327
452
  const AUDIO_FORM_CACHE_KEY = 'broadcast-audio-form-data';
328
453
 
329
454
 
330
455
  // 初始化表单数据,优先从缓存加载
331
456
  const broadcastMode = ref<'text' | 'audio'>(loadFromCache(BROADCAST_MODE_CACHE_KEY, 'text')!);
457
+ const textBroadcastMode = ref<'single' | 'queue'>(loadFromCache(TEXT_BROADCAST_MODE_CACHE_KEY, 'single')!);
332
458
 
333
459
  const textFormData = reactive(loadFromCache(TEXT_FORM_CACHE_KEY, {
334
460
  text: '欢迎使用ZEEAvatarSDK智能体播报功能。这是一个强大的数字人解决方案,支持文本和音频两种播报模式,让您的应用更具互动性!',
@@ -338,6 +464,16 @@ const textFormData = reactive(loadFromCache(TEXT_FORM_CACHE_KEY, {
338
464
  broadcastMotionString: 'DH_ACTION_MODEL103_000003,DH_ACTION_MODEL103_000005',
339
465
  })!);
340
466
 
467
+ // 队列播报表单数据
468
+ const queueFormData = reactive(loadFromCache(QUEUE_FORM_CACHE_KEY, {
469
+ queueTexts: '你好,欢迎使用队列播报功能。\n这是第二段文本,将在指定间隔后发送。\n第三段文本模拟GPT流式输出。\n最后一段文本,队列播报即将完成。',
470
+ volume: 1.0,
471
+ speed: 1.0,
472
+ voiceCode: 'VOICE_EXT_W_000011',
473
+ broadcastMotionString: 'DH_ACTION_MODEL103_000003,DH_ACTION_MODEL103_000005',
474
+ interval: 500, // 发送间隔,默认500ms
475
+ })!);
476
+
341
477
  const audioFormData = reactive(loadFromCache(AUDIO_FORM_CACHE_KEY, {
342
478
  audioText: '欢迎使用ZEEAvatarSDK智能体播报功能',
343
479
  audioUrl: 'https://example.com/audio/sample.mp3',
@@ -354,6 +490,17 @@ const pauseLoading = ref(false);
354
490
  const resumeLoading = ref(false);
355
491
  const stopLoading = ref(false);
356
492
 
493
+ // 队列播报相关状态
494
+ const isQueueBroadcasting = ref(false);
495
+ const queueProgress = reactive({
496
+ current: 0,
497
+ total: 0
498
+ });
499
+ const queueProgressPercent = computed(() => {
500
+ if (queueProgress.total === 0) return 0;
501
+ return Math.round((queueProgress.current / queueProgress.total) * 100);
502
+ });
503
+
357
504
  // 监听表单数据变化,自动保存到本地缓存
358
505
  watch(
359
506
  broadcastMode,
@@ -362,6 +509,13 @@ watch(
362
509
  }
363
510
  );
364
511
 
512
+ watch(
513
+ textBroadcastMode,
514
+ (newMode) => {
515
+ saveToCache(TEXT_BROADCAST_MODE_CACHE_KEY, newMode);
516
+ }
517
+ );
518
+
365
519
  watch(
366
520
  textFormData,
367
521
  (newData) => {
@@ -370,6 +524,14 @@ watch(
370
524
  { deep: true }
371
525
  );
372
526
 
527
+ watch(
528
+ queueFormData,
529
+ (newData) => {
530
+ saveToCache(QUEUE_FORM_CACHE_KEY, newData);
531
+ },
532
+ { deep: true }
533
+ );
534
+
373
535
  watch(
374
536
  audioFormData,
375
537
  (newData) => {
@@ -382,7 +544,9 @@ watch(
382
544
  onMounted(() => {
383
545
  console.log('BroadcastAPI: 已从本地缓存加载表单数据', {
384
546
  broadcastMode: broadcastMode.value,
547
+ textBroadcastMode: textBroadcastMode.value,
385
548
  textFormData: textFormData,
549
+ queueFormData: queueFormData,
386
550
  audioFormData: audioFormData
387
551
  });
388
552
  });
@@ -417,6 +581,75 @@ async function handleTextBroadcast() {
417
581
  }
418
582
  }
419
583
 
584
+ async function handleQueueBroadcast() {
585
+ const { queueTexts, volume, speed, voiceCode, broadcastMotionString, interval } = queueFormData;
586
+ const { avatarCode } = props.globalConfig;
587
+
588
+ if (!avatarCode) {
589
+ result.value = '❌ 请先在全局配置中设置Avatar Code';
590
+ ElMessage.warning('请先设置Avatar Code');
591
+ return;
592
+ }
593
+
594
+ if (!queueTexts.trim() || !voiceCode) {
595
+ result.value = '❌ 队列文本和音色编码不能为空';
596
+ ElMessage.warning('请填写完整的播报信息');
597
+ return;
598
+ }
599
+
600
+ // 解析文本为数组,过滤空行
601
+ const textLines = queueTexts.split('\n').filter(line => line.trim());
602
+ if (textLines.length === 0) {
603
+ result.value = '❌ 请至少输入一段播报文本';
604
+ ElMessage.warning('请至少输入一段播报文本');
605
+ return;
606
+ }
607
+
608
+ isQueueBroadcasting.value = true;
609
+ queueProgress.current = 0;
610
+ queueProgress.total = textLines.length;
611
+
612
+ result.value = `🔄 开始队列播报...\n📝 共 ${textLines.length} 段文本\n🎤 音色编码: ${voiceCode}\n🔊 音量: ${volume}\n⚡ 语速: ${speed}\n⏱️ 发送间隔: ${interval}ms\n\n📡 模拟GPT流式输出...`;
613
+
614
+ try {
615
+ for (let i = 0; i < textLines.length; i++) {
616
+ const text = textLines[i].trim();
617
+ const isAppend = i > 0; // 第一个不是追加,后续都是追加
618
+
619
+ queueProgress.current = i + 1;
620
+ result.value = `🔄 队列播报中...\n📝 正在发送第 ${i + 1}/${textLines.length} 段\n📄 文本: ${text.substring(0, 30)}${text.length > 30 ? '...' : ''}\n${isAppend ? '📎 追加模式' : '🆕 新播报'}\n\n⏳ 进度: ${queueProgress.current}/${queueProgress.total}`;
621
+
622
+ // 发送播报请求
623
+ emit('text-broadcast', {
624
+ avatarCode,
625
+ text,
626
+ volume,
627
+ speed,
628
+ voiceCode,
629
+ broadcastMotionString,
630
+ isAppend
631
+ });
632
+
633
+ // 如果不是最后一个,等待配置的间隔时间再发送下一个
634
+ if (i < textLines.length - 1) {
635
+ await new Promise(resolve => setTimeout(resolve, interval));
636
+ }
637
+ }
638
+
639
+ result.value = `✅ 队列播报请求已全部发送完成\n📋 共发送 ${textLines.length} 段文本\n📡 SDK正在按顺序处理队列...\n\n📝 说明:\n- 第一段以新播报方式发送\n- 后续段落以追加模式发送\n- 每段间隔${interval}ms发送\n- 请查看右侧日志面板获取详细信息`;
640
+
641
+ // 延迟重置状态,让用户看到完成提示
642
+ setTimeout(() => {
643
+ isQueueBroadcasting.value = false;
644
+ }, 1000);
645
+
646
+ } catch (error: any) {
647
+ isQueueBroadcasting.value = false;
648
+ result.value = `❌ 队列播报失败: ${error.message}\n\n🔍 可能的原因:\n- 网络连接问题\n- 音色编码不存在\n- 服务器繁忙`;
649
+ ElMessage.error(`队列播报失败: ${error.message}`);
650
+ }
651
+ }
652
+
420
653
  async function handleAudioBroadcast() {
421
654
  const { audioText, audioUrl, volume, broadcastMotionString } = audioFormData;
422
655
  const { avatarCode } = props.globalConfig;
@@ -559,6 +792,18 @@ function clearResult() {
559
792
  }
560
793
  }
561
794
  .form-content {
795
+ .text-mode-tabs {
796
+ margin-bottom: 15px;
797
+ :deep(.el-tabs__header) {
798
+ margin-bottom: 0;
799
+ }
800
+ :deep(.el-tabs__item) {
801
+ font-weight: 500;
802
+ &.is-active {
803
+ color: #409eff;
804
+ }
805
+ }
806
+ }
562
807
  .el-form-item {
563
808
  margin-bottom: 18px;
564
809
  :deep(.el-form-item__label) {
@@ -587,6 +832,29 @@ function clearResult() {
587
832
  line-height: 1.4;
588
833
  }
589
834
  }
835
+ .interval-unit {
836
+ margin-left: 8px;
837
+ font-size: 13px;
838
+ color: #606266;
839
+ }
840
+ .queue-progress {
841
+ display: flex;
842
+ align-items: center;
843
+ gap: 15px;
844
+ width: 100%;
845
+ .el-progress {
846
+ flex: 1;
847
+ :deep(.el-progress__text) {
848
+ min-width: 20px;
849
+ }
850
+ }
851
+ .progress-text {
852
+ font-size: 12px;
853
+ color: #606266;
854
+ font-weight: 500;
855
+ white-space: nowrap;
856
+ }
857
+ }
590
858
  }
591
859
  }
592
860