ruiyun-human 1.0.13 → 1.0.15
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/dist/ruiyun-human.cjs.js +162 -33
- package/dist/ruiyun-human.es.js +162 -33
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/dist/ruiyun-human.cjs.js
CHANGED
|
@@ -393,15 +393,90 @@ const TTS_MAX_RETRIES = 5;
|
|
|
393
393
|
const _sfc_main = {
|
|
394
394
|
__name: "index",
|
|
395
395
|
props: {
|
|
396
|
-
|
|
397
|
-
type:
|
|
398
|
-
default:
|
|
396
|
+
human: {
|
|
397
|
+
type: Object,
|
|
398
|
+
default: null
|
|
399
399
|
}
|
|
400
400
|
},
|
|
401
401
|
emits: ["loaded", "changed", "paused", "resumed", "audio-ready"],
|
|
402
402
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
403
|
-
const TTS_URL = "
|
|
404
|
-
const ASR_URL = "
|
|
403
|
+
const TTS_URL = "/api";
|
|
404
|
+
const ASR_URL = "/api";
|
|
405
|
+
const szrUrl = "http://116.198.239.205:8443/szrimage/";
|
|
406
|
+
const HUMAN_PRESETS = {
|
|
407
|
+
1: {
|
|
408
|
+
humanId: 1,
|
|
409
|
+
title: "西装动漫男",
|
|
410
|
+
// TODO: 替换为真实资源
|
|
411
|
+
folder: `${szrUrl}/xiangyu-green-suit`,
|
|
412
|
+
folder_mobile: `${szrUrl}/xiangyu-green-suit-header`,
|
|
413
|
+
standby: { interval: 100, standbyNum: 11 },
|
|
414
|
+
speak: { interval: 30, speakNum: 0 },
|
|
415
|
+
humanType: 1,
|
|
416
|
+
timbre: 47
|
|
417
|
+
},
|
|
418
|
+
2: {
|
|
419
|
+
humanId: 2,
|
|
420
|
+
title: "西装动漫女",
|
|
421
|
+
folder: `${szrUrl}/anime-woman`,
|
|
422
|
+
folder_mobile: `${szrUrl}/anime-woman-header`,
|
|
423
|
+
standby: { interval: 100, standbyNum: 1 },
|
|
424
|
+
speak: { interval: 30, speakNum: 75 },
|
|
425
|
+
humanType: 1,
|
|
426
|
+
timbre: 45
|
|
427
|
+
},
|
|
428
|
+
3: {
|
|
429
|
+
humanId: 3,
|
|
430
|
+
title: "真人形象男",
|
|
431
|
+
folder: `${szrUrl}/real-man`,
|
|
432
|
+
folder_mobile: `${szrUrl}/real-man-header`,
|
|
433
|
+
standby: { interval: 100, standbyNum: 1 },
|
|
434
|
+
speak: { interval: 30, speakNum: 139 },
|
|
435
|
+
humanType: 1,
|
|
436
|
+
timbre: 47
|
|
437
|
+
},
|
|
438
|
+
4: {
|
|
439
|
+
humanId: 4,
|
|
440
|
+
title: "真人形象女",
|
|
441
|
+
folder: `${szrUrl}/real-woman`,
|
|
442
|
+
folder_mobile: `${szrUrl}/real-woman-header`,
|
|
443
|
+
standby: { interval: 100, standbyNum: 2 },
|
|
444
|
+
speak: { interval: 40, speakNum: 139 },
|
|
445
|
+
humanType: 1,
|
|
446
|
+
timbre: 34
|
|
447
|
+
},
|
|
448
|
+
5: {
|
|
449
|
+
humanId: 5,
|
|
450
|
+
title: "Q版形象",
|
|
451
|
+
folder: `${szrUrl}/xiangyu-q`,
|
|
452
|
+
folder_mobile: `${szrUrl}/xiangyu-q-header`,
|
|
453
|
+
standby: { interval: 100, standbyNum: 28 },
|
|
454
|
+
speak: { interval: 30, speakNum: 118 },
|
|
455
|
+
humanType: 1,
|
|
456
|
+
timbre: 47
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const isMobile = () => {
|
|
460
|
+
if (typeof navigator === "undefined") return false;
|
|
461
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|HarmonyOS/i.test(navigator.userAgent);
|
|
462
|
+
};
|
|
463
|
+
const resolveFolder = (human) => {
|
|
464
|
+
const finalType = human.humanType || (isMobile() ? 2 : 1);
|
|
465
|
+
if (finalType === 2 && human.folder_mobile) return human.folder_mobile;
|
|
466
|
+
return human.folder;
|
|
467
|
+
};
|
|
468
|
+
const resolveHuman = (input) => {
|
|
469
|
+
if (input == null) return null;
|
|
470
|
+
const humanId = typeof input === "number" ? input : input.humanId;
|
|
471
|
+
const preset = HUMAN_PRESETS[humanId];
|
|
472
|
+
if (!preset) {
|
|
473
|
+
console.error("[DigitalHuman] 未找到 humanId=", humanId, "的预设形象,可用 id:", Object.keys(HUMAN_PRESETS));
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const merged = { ...preset, ...typeof input === "object" ? input : {} };
|
|
477
|
+
merged.folder = resolveFolder(merged);
|
|
478
|
+
return merged;
|
|
479
|
+
};
|
|
405
480
|
const props = __props;
|
|
406
481
|
const emit = __emit;
|
|
407
482
|
const containerRef = vue.ref(null);
|
|
@@ -423,27 +498,38 @@ const _sfc_main = {
|
|
|
423
498
|
standbyNum: currentHuman.value.standby.standbyNum,
|
|
424
499
|
speakNum: currentHuman.value.speak.speakNum
|
|
425
500
|
};
|
|
426
|
-
|
|
501
|
+
const makeImage = (url) => {
|
|
427
502
|
const image = new Image();
|
|
428
|
-
image.src = encodeURI(
|
|
429
|
-
|
|
503
|
+
image.src = encodeURI(url);
|
|
504
|
+
image.onload = () => {
|
|
505
|
+
image.__broken = false;
|
|
506
|
+
};
|
|
507
|
+
image.onerror = () => {
|
|
508
|
+
image.__broken = true;
|
|
509
|
+
console.warn("[DigitalHuman] 帧加载失败:", url);
|
|
510
|
+
};
|
|
511
|
+
return image;
|
|
512
|
+
};
|
|
513
|
+
for (let i = 1; i <= standbyNum; i++) {
|
|
514
|
+
frames.value.standby.push(makeImage(`${folder}/standby/standby (${i}).png`));
|
|
515
|
+
}
|
|
516
|
+
if (speakNum <= 0) {
|
|
517
|
+
console.warn("[DigitalHuman] 当前形象 speakNum=0,说话动画暂用待机帧替代");
|
|
518
|
+
frames.value.speakStart = [...frames.value.standby];
|
|
519
|
+
frames.value.speakLoop = [...frames.value.standby];
|
|
520
|
+
frames.value.speakEnd = [...frames.value.standby];
|
|
521
|
+
return;
|
|
430
522
|
}
|
|
431
523
|
const startEnd = Math.floor(speakNum * 0.6);
|
|
432
524
|
const loopEnd = Math.floor(speakNum * 0.9);
|
|
433
525
|
for (let i = 1; i <= startEnd; i++) {
|
|
434
|
-
|
|
435
|
-
image.src = encodeURI(`${folder}/speak/speak (${i}).png`);
|
|
436
|
-
frames.value.speakStart.push(image);
|
|
526
|
+
frames.value.speakStart.push(makeImage(`${folder}/speak/speak (${i}).png`));
|
|
437
527
|
}
|
|
438
528
|
for (let i = startEnd; i <= loopEnd; i++) {
|
|
439
|
-
|
|
440
|
-
image.src = encodeURI(`${folder}/speak/speak (${i}).png`);
|
|
441
|
-
frames.value.speakLoop.push(image);
|
|
529
|
+
frames.value.speakLoop.push(makeImage(`${folder}/speak/speak (${i}).png`));
|
|
442
530
|
}
|
|
443
531
|
for (let i = loopEnd; i <= speakNum; i++) {
|
|
444
|
-
|
|
445
|
-
image.src = encodeURI(`${folder}/speak/speak (${i}).png`);
|
|
446
|
-
frames.value.speakEnd.push(image);
|
|
532
|
+
frames.value.speakEnd.push(makeImage(`${folder}/speak/speak (${i}).png`));
|
|
447
533
|
}
|
|
448
534
|
};
|
|
449
535
|
const draw = (image) => {
|
|
@@ -604,20 +690,39 @@ const _sfc_main = {
|
|
|
604
690
|
};
|
|
605
691
|
allFrames.forEach((img) => {
|
|
606
692
|
if (img.complete) {
|
|
693
|
+
if (!img.naturalWidth) img.__broken = true;
|
|
607
694
|
checkLoaded();
|
|
608
695
|
} else {
|
|
609
|
-
img.onload =
|
|
610
|
-
|
|
696
|
+
img.onload = () => {
|
|
697
|
+
img.__broken = false;
|
|
698
|
+
checkLoaded();
|
|
699
|
+
};
|
|
700
|
+
img.onerror = () => {
|
|
701
|
+
img.__broken = true;
|
|
702
|
+
checkLoaded();
|
|
703
|
+
};
|
|
611
704
|
}
|
|
612
705
|
});
|
|
613
706
|
};
|
|
614
707
|
const init = () => {
|
|
615
708
|
vue.nextTick(() => {
|
|
709
|
+
var _a, _b;
|
|
616
710
|
if (!containerRef.value || !canvasRef.value) return;
|
|
617
|
-
|
|
618
|
-
if (!
|
|
619
|
-
|
|
711
|
+
const rawInput = props.human;
|
|
712
|
+
if (!rawInput) return;
|
|
713
|
+
const human = resolveHuman(rawInput);
|
|
714
|
+
if (!human) return;
|
|
715
|
+
const prevType = (_a = currentHuman.value) == null ? void 0 : _a.humanType;
|
|
716
|
+
if (((_b = currentHuman.value) == null ? void 0 : _b.humanId) === human.humanId && prevType === human.humanType) {
|
|
717
|
+
Object.assign(currentHuman.value, human);
|
|
718
|
+
return;
|
|
620
719
|
}
|
|
720
|
+
abortTTS();
|
|
721
|
+
stop();
|
|
722
|
+
timestamp.value = null;
|
|
723
|
+
frameIndex.value = 1;
|
|
724
|
+
currentHuman.value = human;
|
|
725
|
+
console.log("init", currentHuman.value);
|
|
621
726
|
const canvas = canvasRef.value;
|
|
622
727
|
const dpr = window.devicePixelRatio || 1;
|
|
623
728
|
const width = containerRef.value.clientWidth;
|
|
@@ -694,9 +799,11 @@ const _sfc_main = {
|
|
|
694
799
|
return pcmToWavBlob(buf);
|
|
695
800
|
};
|
|
696
801
|
const requestTTSAudio = async (text, signal) => {
|
|
802
|
+
var _a, _b;
|
|
697
803
|
let lastError = null;
|
|
698
804
|
for (let attempt = 1; attempt <= TTS_MAX_RETRIES; attempt++) {
|
|
699
805
|
if (signal == null ? void 0 : signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
806
|
+
console.log("[TTS] 提交合成请求1111:", ((_a = currentHuman.value) == null ? void 0 : _a.timbre) ?? 1);
|
|
700
807
|
const res = await fetch(`${TTS_URL}/web/voice/jd/text-to-speech`, {
|
|
701
808
|
method: "POST",
|
|
702
809
|
headers: {
|
|
@@ -704,7 +811,7 @@ const _sfc_main = {
|
|
|
704
811
|
clientid: VOICE_CLIENT_ID,
|
|
705
812
|
"tenant-id": VOICE_TENANT_ID
|
|
706
813
|
},
|
|
707
|
-
body: JSON.stringify({ text, user: TTS_USER, timbre:
|
|
814
|
+
body: JSON.stringify({ text, user: TTS_USER, timbre: ((_b = currentHuman.value) == null ? void 0 : _b.timbre) ?? 1 }),
|
|
708
815
|
signal
|
|
709
816
|
});
|
|
710
817
|
if (!res.ok) {
|
|
@@ -775,8 +882,12 @@ const _sfc_main = {
|
|
|
775
882
|
}
|
|
776
883
|
};
|
|
777
884
|
const playTTS = async (text) => {
|
|
778
|
-
|
|
779
|
-
|
|
885
|
+
if (!text) return;
|
|
886
|
+
const latest = props.human ? resolveHuman(props.human) : null;
|
|
887
|
+
if (latest) {
|
|
888
|
+
if (!latest.isMuted) return;
|
|
889
|
+
Object.assign(currentHuman.value || {}, latest);
|
|
890
|
+
}
|
|
780
891
|
abortTTS();
|
|
781
892
|
ttsAbortController = new AbortController();
|
|
782
893
|
const { signal } = ttsAbortController;
|
|
@@ -785,7 +896,7 @@ const _sfc_main = {
|
|
|
785
896
|
try {
|
|
786
897
|
const cleanText = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/[#*_~]/g, "").replace(/\n+/g, ",").replace(/\s+/g, " ").replace(/[,。!?]{2,}/g, ",").trim();
|
|
787
898
|
if (!cleanText) return;
|
|
788
|
-
console.log("[TTS] 提交合成请求:", { text: cleanText, length: cleanText.length });
|
|
899
|
+
console.log("[TTS] 提交合成请求:", { human: currentHuman.value, text: cleanText, length: cleanText.length });
|
|
789
900
|
const audioBlob = await requestTTSAudio(cleanText, signal);
|
|
790
901
|
if (signal.aborted || currentTtsRequestId.value !== requestId) return;
|
|
791
902
|
if (!audioBlob || audioBlob.size === 0) {
|
|
@@ -865,6 +976,27 @@ const _sfc_main = {
|
|
|
865
976
|
});
|
|
866
977
|
}
|
|
867
978
|
};
|
|
979
|
+
vue.watch(
|
|
980
|
+
() => [props.human],
|
|
981
|
+
() => {
|
|
982
|
+
init();
|
|
983
|
+
},
|
|
984
|
+
{ deep: true }
|
|
985
|
+
);
|
|
986
|
+
const switchHuman = (humanId) => {
|
|
987
|
+
const next = resolveHuman(humanId);
|
|
988
|
+
if (!next) return;
|
|
989
|
+
abortTTS();
|
|
990
|
+
stop();
|
|
991
|
+
timestamp.value = null;
|
|
992
|
+
frameIndex.value = 1;
|
|
993
|
+
currentHuman.value = next;
|
|
994
|
+
loadFrames();
|
|
995
|
+
waitFramesLoaded(() => {
|
|
996
|
+
draw(frames.value.standby[0]);
|
|
997
|
+
emit("changed", currentHuman.value);
|
|
998
|
+
});
|
|
999
|
+
};
|
|
868
1000
|
__expose({
|
|
869
1001
|
standby,
|
|
870
1002
|
speak,
|
|
@@ -873,14 +1005,11 @@ const _sfc_main = {
|
|
|
873
1005
|
resume,
|
|
874
1006
|
playTTS,
|
|
875
1007
|
startRecord,
|
|
876
|
-
stopRecord
|
|
1008
|
+
stopRecord,
|
|
1009
|
+
switchHuman
|
|
877
1010
|
});
|
|
878
|
-
vue.watch(() => props.humans, (val) => {
|
|
879
|
-
if (val && val.length > 0 && !currentHuman.value) {
|
|
880
|
-
init();
|
|
881
|
-
}
|
|
882
|
-
}, { deep: true });
|
|
883
1011
|
vue.onMounted(() => {
|
|
1012
|
+
console.log("onMounted", props.human);
|
|
884
1013
|
initRecorder();
|
|
885
1014
|
init();
|
|
886
1015
|
});
|
|
@@ -905,7 +1034,7 @@ const _sfc_main = {
|
|
|
905
1034
|
};
|
|
906
1035
|
}
|
|
907
1036
|
};
|
|
908
|
-
const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
|
|
1037
|
+
const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-ab1e5de1"]]);
|
|
909
1038
|
DigitalHuman.install = (app) => app.component("DigitalHuman", DigitalHuman);
|
|
910
1039
|
const components = [DemoButton, DemoInput, DigitalHuman];
|
|
911
1040
|
const install = (app) => {
|
package/dist/ruiyun-human.es.js
CHANGED
|
@@ -391,15 +391,90 @@ const TTS_MAX_RETRIES = 5;
|
|
|
391
391
|
const _sfc_main = {
|
|
392
392
|
__name: "index",
|
|
393
393
|
props: {
|
|
394
|
-
|
|
395
|
-
type:
|
|
396
|
-
default:
|
|
394
|
+
human: {
|
|
395
|
+
type: Object,
|
|
396
|
+
default: null
|
|
397
397
|
}
|
|
398
398
|
},
|
|
399
399
|
emits: ["loaded", "changed", "paused", "resumed", "audio-ready"],
|
|
400
400
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
401
|
-
const TTS_URL = "
|
|
402
|
-
const ASR_URL = "
|
|
401
|
+
const TTS_URL = "/api";
|
|
402
|
+
const ASR_URL = "/api";
|
|
403
|
+
const szrUrl = "http://116.198.239.205:8443/szrimage/";
|
|
404
|
+
const HUMAN_PRESETS = {
|
|
405
|
+
1: {
|
|
406
|
+
humanId: 1,
|
|
407
|
+
title: "西装动漫男",
|
|
408
|
+
// TODO: 替换为真实资源
|
|
409
|
+
folder: `${szrUrl}/xiangyu-green-suit`,
|
|
410
|
+
folder_mobile: `${szrUrl}/xiangyu-green-suit-header`,
|
|
411
|
+
standby: { interval: 100, standbyNum: 11 },
|
|
412
|
+
speak: { interval: 30, speakNum: 0 },
|
|
413
|
+
humanType: 1,
|
|
414
|
+
timbre: 47
|
|
415
|
+
},
|
|
416
|
+
2: {
|
|
417
|
+
humanId: 2,
|
|
418
|
+
title: "西装动漫女",
|
|
419
|
+
folder: `${szrUrl}/anime-woman`,
|
|
420
|
+
folder_mobile: `${szrUrl}/anime-woman-header`,
|
|
421
|
+
standby: { interval: 100, standbyNum: 1 },
|
|
422
|
+
speak: { interval: 30, speakNum: 75 },
|
|
423
|
+
humanType: 1,
|
|
424
|
+
timbre: 45
|
|
425
|
+
},
|
|
426
|
+
3: {
|
|
427
|
+
humanId: 3,
|
|
428
|
+
title: "真人形象男",
|
|
429
|
+
folder: `${szrUrl}/real-man`,
|
|
430
|
+
folder_mobile: `${szrUrl}/real-man-header`,
|
|
431
|
+
standby: { interval: 100, standbyNum: 1 },
|
|
432
|
+
speak: { interval: 30, speakNum: 139 },
|
|
433
|
+
humanType: 1,
|
|
434
|
+
timbre: 47
|
|
435
|
+
},
|
|
436
|
+
4: {
|
|
437
|
+
humanId: 4,
|
|
438
|
+
title: "真人形象女",
|
|
439
|
+
folder: `${szrUrl}/real-woman`,
|
|
440
|
+
folder_mobile: `${szrUrl}/real-woman-header`,
|
|
441
|
+
standby: { interval: 100, standbyNum: 2 },
|
|
442
|
+
speak: { interval: 40, speakNum: 139 },
|
|
443
|
+
humanType: 1,
|
|
444
|
+
timbre: 34
|
|
445
|
+
},
|
|
446
|
+
5: {
|
|
447
|
+
humanId: 5,
|
|
448
|
+
title: "Q版形象",
|
|
449
|
+
folder: `${szrUrl}/xiangyu-q`,
|
|
450
|
+
folder_mobile: `${szrUrl}/xiangyu-q-header`,
|
|
451
|
+
standby: { interval: 100, standbyNum: 28 },
|
|
452
|
+
speak: { interval: 30, speakNum: 118 },
|
|
453
|
+
humanType: 1,
|
|
454
|
+
timbre: 47
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
const isMobile = () => {
|
|
458
|
+
if (typeof navigator === "undefined") return false;
|
|
459
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|HarmonyOS/i.test(navigator.userAgent);
|
|
460
|
+
};
|
|
461
|
+
const resolveFolder = (human) => {
|
|
462
|
+
const finalType = human.humanType || (isMobile() ? 2 : 1);
|
|
463
|
+
if (finalType === 2 && human.folder_mobile) return human.folder_mobile;
|
|
464
|
+
return human.folder;
|
|
465
|
+
};
|
|
466
|
+
const resolveHuman = (input) => {
|
|
467
|
+
if (input == null) return null;
|
|
468
|
+
const humanId = typeof input === "number" ? input : input.humanId;
|
|
469
|
+
const preset = HUMAN_PRESETS[humanId];
|
|
470
|
+
if (!preset) {
|
|
471
|
+
console.error("[DigitalHuman] 未找到 humanId=", humanId, "的预设形象,可用 id:", Object.keys(HUMAN_PRESETS));
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
const merged = { ...preset, ...typeof input === "object" ? input : {} };
|
|
475
|
+
merged.folder = resolveFolder(merged);
|
|
476
|
+
return merged;
|
|
477
|
+
};
|
|
403
478
|
const props = __props;
|
|
404
479
|
const emit = __emit;
|
|
405
480
|
const containerRef = ref(null);
|
|
@@ -421,27 +496,38 @@ const _sfc_main = {
|
|
|
421
496
|
standbyNum: currentHuman.value.standby.standbyNum,
|
|
422
497
|
speakNum: currentHuman.value.speak.speakNum
|
|
423
498
|
};
|
|
424
|
-
|
|
499
|
+
const makeImage = (url) => {
|
|
425
500
|
const image = new Image();
|
|
426
|
-
image.src = encodeURI(
|
|
427
|
-
|
|
501
|
+
image.src = encodeURI(url);
|
|
502
|
+
image.onload = () => {
|
|
503
|
+
image.__broken = false;
|
|
504
|
+
};
|
|
505
|
+
image.onerror = () => {
|
|
506
|
+
image.__broken = true;
|
|
507
|
+
console.warn("[DigitalHuman] 帧加载失败:", url);
|
|
508
|
+
};
|
|
509
|
+
return image;
|
|
510
|
+
};
|
|
511
|
+
for (let i = 1; i <= standbyNum; i++) {
|
|
512
|
+
frames.value.standby.push(makeImage(`${folder}/standby/standby (${i}).png`));
|
|
513
|
+
}
|
|
514
|
+
if (speakNum <= 0) {
|
|
515
|
+
console.warn("[DigitalHuman] 当前形象 speakNum=0,说话动画暂用待机帧替代");
|
|
516
|
+
frames.value.speakStart = [...frames.value.standby];
|
|
517
|
+
frames.value.speakLoop = [...frames.value.standby];
|
|
518
|
+
frames.value.speakEnd = [...frames.value.standby];
|
|
519
|
+
return;
|
|
428
520
|
}
|
|
429
521
|
const startEnd = Math.floor(speakNum * 0.6);
|
|
430
522
|
const loopEnd = Math.floor(speakNum * 0.9);
|
|
431
523
|
for (let i = 1; i <= startEnd; i++) {
|
|
432
|
-
|
|
433
|
-
image.src = encodeURI(`${folder}/speak/speak (${i}).png`);
|
|
434
|
-
frames.value.speakStart.push(image);
|
|
524
|
+
frames.value.speakStart.push(makeImage(`${folder}/speak/speak (${i}).png`));
|
|
435
525
|
}
|
|
436
526
|
for (let i = startEnd; i <= loopEnd; i++) {
|
|
437
|
-
|
|
438
|
-
image.src = encodeURI(`${folder}/speak/speak (${i}).png`);
|
|
439
|
-
frames.value.speakLoop.push(image);
|
|
527
|
+
frames.value.speakLoop.push(makeImage(`${folder}/speak/speak (${i}).png`));
|
|
440
528
|
}
|
|
441
529
|
for (let i = loopEnd; i <= speakNum; i++) {
|
|
442
|
-
|
|
443
|
-
image.src = encodeURI(`${folder}/speak/speak (${i}).png`);
|
|
444
|
-
frames.value.speakEnd.push(image);
|
|
530
|
+
frames.value.speakEnd.push(makeImage(`${folder}/speak/speak (${i}).png`));
|
|
445
531
|
}
|
|
446
532
|
};
|
|
447
533
|
const draw = (image) => {
|
|
@@ -602,20 +688,39 @@ const _sfc_main = {
|
|
|
602
688
|
};
|
|
603
689
|
allFrames.forEach((img) => {
|
|
604
690
|
if (img.complete) {
|
|
691
|
+
if (!img.naturalWidth) img.__broken = true;
|
|
605
692
|
checkLoaded();
|
|
606
693
|
} else {
|
|
607
|
-
img.onload =
|
|
608
|
-
|
|
694
|
+
img.onload = () => {
|
|
695
|
+
img.__broken = false;
|
|
696
|
+
checkLoaded();
|
|
697
|
+
};
|
|
698
|
+
img.onerror = () => {
|
|
699
|
+
img.__broken = true;
|
|
700
|
+
checkLoaded();
|
|
701
|
+
};
|
|
609
702
|
}
|
|
610
703
|
});
|
|
611
704
|
};
|
|
612
705
|
const init = () => {
|
|
613
706
|
nextTick(() => {
|
|
707
|
+
var _a, _b;
|
|
614
708
|
if (!containerRef.value || !canvasRef.value) return;
|
|
615
|
-
|
|
616
|
-
if (!
|
|
617
|
-
|
|
709
|
+
const rawInput = props.human;
|
|
710
|
+
if (!rawInput) return;
|
|
711
|
+
const human = resolveHuman(rawInput);
|
|
712
|
+
if (!human) return;
|
|
713
|
+
const prevType = (_a = currentHuman.value) == null ? void 0 : _a.humanType;
|
|
714
|
+
if (((_b = currentHuman.value) == null ? void 0 : _b.humanId) === human.humanId && prevType === human.humanType) {
|
|
715
|
+
Object.assign(currentHuman.value, human);
|
|
716
|
+
return;
|
|
618
717
|
}
|
|
718
|
+
abortTTS();
|
|
719
|
+
stop();
|
|
720
|
+
timestamp.value = null;
|
|
721
|
+
frameIndex.value = 1;
|
|
722
|
+
currentHuman.value = human;
|
|
723
|
+
console.log("init", currentHuman.value);
|
|
619
724
|
const canvas = canvasRef.value;
|
|
620
725
|
const dpr = window.devicePixelRatio || 1;
|
|
621
726
|
const width = containerRef.value.clientWidth;
|
|
@@ -692,9 +797,11 @@ const _sfc_main = {
|
|
|
692
797
|
return pcmToWavBlob(buf);
|
|
693
798
|
};
|
|
694
799
|
const requestTTSAudio = async (text, signal) => {
|
|
800
|
+
var _a, _b;
|
|
695
801
|
let lastError = null;
|
|
696
802
|
for (let attempt = 1; attempt <= TTS_MAX_RETRIES; attempt++) {
|
|
697
803
|
if (signal == null ? void 0 : signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
804
|
+
console.log("[TTS] 提交合成请求1111:", ((_a = currentHuman.value) == null ? void 0 : _a.timbre) ?? 1);
|
|
698
805
|
const res = await fetch(`${TTS_URL}/web/voice/jd/text-to-speech`, {
|
|
699
806
|
method: "POST",
|
|
700
807
|
headers: {
|
|
@@ -702,7 +809,7 @@ const _sfc_main = {
|
|
|
702
809
|
clientid: VOICE_CLIENT_ID,
|
|
703
810
|
"tenant-id": VOICE_TENANT_ID
|
|
704
811
|
},
|
|
705
|
-
body: JSON.stringify({ text, user: TTS_USER, timbre:
|
|
812
|
+
body: JSON.stringify({ text, user: TTS_USER, timbre: ((_b = currentHuman.value) == null ? void 0 : _b.timbre) ?? 1 }),
|
|
706
813
|
signal
|
|
707
814
|
});
|
|
708
815
|
if (!res.ok) {
|
|
@@ -773,8 +880,12 @@ const _sfc_main = {
|
|
|
773
880
|
}
|
|
774
881
|
};
|
|
775
882
|
const playTTS = async (text) => {
|
|
776
|
-
|
|
777
|
-
|
|
883
|
+
if (!text) return;
|
|
884
|
+
const latest = props.human ? resolveHuman(props.human) : null;
|
|
885
|
+
if (latest) {
|
|
886
|
+
if (!latest.isMuted) return;
|
|
887
|
+
Object.assign(currentHuman.value || {}, latest);
|
|
888
|
+
}
|
|
778
889
|
abortTTS();
|
|
779
890
|
ttsAbortController = new AbortController();
|
|
780
891
|
const { signal } = ttsAbortController;
|
|
@@ -783,7 +894,7 @@ const _sfc_main = {
|
|
|
783
894
|
try {
|
|
784
895
|
const cleanText = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/[#*_~]/g, "").replace(/\n+/g, ",").replace(/\s+/g, " ").replace(/[,。!?]{2,}/g, ",").trim();
|
|
785
896
|
if (!cleanText) return;
|
|
786
|
-
console.log("[TTS] 提交合成请求:", { text: cleanText, length: cleanText.length });
|
|
897
|
+
console.log("[TTS] 提交合成请求:", { human: currentHuman.value, text: cleanText, length: cleanText.length });
|
|
787
898
|
const audioBlob = await requestTTSAudio(cleanText, signal);
|
|
788
899
|
if (signal.aborted || currentTtsRequestId.value !== requestId) return;
|
|
789
900
|
if (!audioBlob || audioBlob.size === 0) {
|
|
@@ -863,6 +974,27 @@ const _sfc_main = {
|
|
|
863
974
|
});
|
|
864
975
|
}
|
|
865
976
|
};
|
|
977
|
+
watch(
|
|
978
|
+
() => [props.human],
|
|
979
|
+
() => {
|
|
980
|
+
init();
|
|
981
|
+
},
|
|
982
|
+
{ deep: true }
|
|
983
|
+
);
|
|
984
|
+
const switchHuman = (humanId) => {
|
|
985
|
+
const next = resolveHuman(humanId);
|
|
986
|
+
if (!next) return;
|
|
987
|
+
abortTTS();
|
|
988
|
+
stop();
|
|
989
|
+
timestamp.value = null;
|
|
990
|
+
frameIndex.value = 1;
|
|
991
|
+
currentHuman.value = next;
|
|
992
|
+
loadFrames();
|
|
993
|
+
waitFramesLoaded(() => {
|
|
994
|
+
draw(frames.value.standby[0]);
|
|
995
|
+
emit("changed", currentHuman.value);
|
|
996
|
+
});
|
|
997
|
+
};
|
|
866
998
|
__expose({
|
|
867
999
|
standby,
|
|
868
1000
|
speak,
|
|
@@ -871,14 +1003,11 @@ const _sfc_main = {
|
|
|
871
1003
|
resume,
|
|
872
1004
|
playTTS,
|
|
873
1005
|
startRecord,
|
|
874
|
-
stopRecord
|
|
1006
|
+
stopRecord,
|
|
1007
|
+
switchHuman
|
|
875
1008
|
});
|
|
876
|
-
watch(() => props.humans, (val) => {
|
|
877
|
-
if (val && val.length > 0 && !currentHuman.value) {
|
|
878
|
-
init();
|
|
879
|
-
}
|
|
880
|
-
}, { deep: true });
|
|
881
1009
|
onMounted(() => {
|
|
1010
|
+
console.log("onMounted", props.human);
|
|
882
1011
|
initRecorder();
|
|
883
1012
|
init();
|
|
884
1013
|
});
|
|
@@ -903,7 +1032,7 @@ const _sfc_main = {
|
|
|
903
1032
|
};
|
|
904
1033
|
}
|
|
905
1034
|
};
|
|
906
|
-
const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
|
|
1035
|
+
const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-ab1e5de1"]]);
|
|
907
1036
|
DigitalHuman.install = (app) => app.component("DigitalHuman", DigitalHuman);
|
|
908
1037
|
const components = [DemoButton, DemoInput, DigitalHuman];
|
|
909
1038
|
const install = (app) => {
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.demo-btn[data-v-9a48516a]{padding:6px 14px;border-radius:4px;border:none;cursor:pointer;font-size:14px}.primary[data-v-9a48516a]{background:#409eff;color:#fff}.success[data-v-9a48516a]{background:#67c23a;color:#fff}.warning[data-v-9a48516a]{background:#e6a23c;color:#fff}.demo-input-wrap[data-v-28a3493c]{display:inline-flex;align-items:center;border:1px solid #dcdcdc;border-radius:4px;padding:0 10px}.demo-input-wrap[data-v-28a3493c]:focus-within{border-color:#409eff}.demo-input[data-v-28a3493c]{border:none;outline:none;padding:8px 6px;flex:1}.prefix[data-v-28a3493c],.suffix[data-v-28a3493c]{color:#909399}.digital-human[data-v-
|
|
1
|
+
.demo-btn[data-v-9a48516a]{padding:6px 14px;border-radius:4px;border:none;cursor:pointer;font-size:14px}.primary[data-v-9a48516a]{background:#409eff;color:#fff}.success[data-v-9a48516a]{background:#67c23a;color:#fff}.warning[data-v-9a48516a]{background:#e6a23c;color:#fff}.demo-input-wrap[data-v-28a3493c]{display:inline-flex;align-items:center;border:1px solid #dcdcdc;border-radius:4px;padding:0 10px}.demo-input-wrap[data-v-28a3493c]:focus-within{border-color:#409eff}.demo-input[data-v-28a3493c]{border:none;outline:none;padding:8px 6px;flex:1}.prefix[data-v-28a3493c],.suffix[data-v-28a3493c]{color:#909399}.digital-human[data-v-ab1e5de1]{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.digital-human canvas[data-v-ab1e5de1]{max-width:100%;max-height:100%}
|