ruiyun-human 1.0.11 → 1.0.13
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 +122 -146
- package/dist/ruiyun-human.es.js +122 -146
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/dist/ruiyun-human.cjs.js
CHANGED
|
@@ -385,8 +385,11 @@ var recorder = { exports: {} };
|
|
|
385
385
|
var recorderExports = recorder.exports;
|
|
386
386
|
var jsAudioRecorder = recorderExports;
|
|
387
387
|
const Recorder = /* @__PURE__ */ getDefaultExportFromCjs(jsAudioRecorder);
|
|
388
|
-
const
|
|
389
|
-
const
|
|
388
|
+
const VOICE_CLIENT_ID = "e5cd2b";
|
|
389
|
+
const VOICE_TENANT_ID = "000000";
|
|
390
|
+
const TTS_USER = "web-test-user";
|
|
391
|
+
const TTS_PCM_SAMPLE_RATE = 16e3;
|
|
392
|
+
const TTS_MAX_RETRIES = 5;
|
|
390
393
|
const _sfc_main = {
|
|
391
394
|
__name: "index",
|
|
392
395
|
props: {
|
|
@@ -397,6 +400,8 @@ const _sfc_main = {
|
|
|
397
400
|
},
|
|
398
401
|
emits: ["loaded", "changed", "paused", "resumed", "audio-ready"],
|
|
399
402
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
403
|
+
const TTS_URL = "http://localhost:6039";
|
|
404
|
+
const ASR_URL = "http://localhost:6039";
|
|
400
405
|
const props = __props;
|
|
401
406
|
const emit = __emit;
|
|
402
407
|
const containerRef = vue.ref(null);
|
|
@@ -649,112 +654,93 @@ const _sfc_main = {
|
|
|
649
654
|
currentAudio.value = null;
|
|
650
655
|
}
|
|
651
656
|
}
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
method: "POST",
|
|
659
|
-
body: form,
|
|
660
|
-
signal
|
|
661
|
-
});
|
|
662
|
-
if (!res.ok) throw new Error(`参考音频上传失败 HTTP ${res.status}`);
|
|
663
|
-
const paths = await res.json();
|
|
664
|
-
const path = Array.isArray(paths) ? paths[0] : paths;
|
|
665
|
-
if (!path) throw new Error("参考音频上传失败:服务未返回文件路径");
|
|
666
|
-
return { path, orig_name: filename, meta: FILEDATA_META };
|
|
657
|
+
const base64ToBlob = (b64, mime = "audio/wav") => {
|
|
658
|
+
const raw = String(b64).replace(/^data:audio\/[\w+-]+;base64,/, "");
|
|
659
|
+
const binary = atob(raw);
|
|
660
|
+
const bytes = new Uint8Array(binary.length);
|
|
661
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
662
|
+
return new Blob([bytes], { type: mime });
|
|
667
663
|
};
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const res = await fetch(voiceSource, { signal });
|
|
690
|
-
if (!res.ok) throw new Error(`参考音频下载失败 HTTP ${res.status}`);
|
|
691
|
-
const blob = await res.blob();
|
|
692
|
-
const fileName = decodeURIComponent(voiceSource.split("?")[0].split("/").pop()) || "reference.mp3";
|
|
693
|
-
return await uploadReferenceAudio(blob, fileName, signal);
|
|
694
|
-
}
|
|
695
|
-
throw new Error("不支持的参考音频来源:voiceUrl 应为音频 URL 字符串、File/Blob 或 { path } 对象");
|
|
664
|
+
const pcmToWavBlob = (pcmBuffer, sampleRate = TTS_PCM_SAMPLE_RATE) => {
|
|
665
|
+
const dataLength = pcmBuffer.byteLength;
|
|
666
|
+
const header = new ArrayBuffer(44);
|
|
667
|
+
const view = new DataView(header);
|
|
668
|
+
const writeStr = (offset, str) => {
|
|
669
|
+
for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
|
|
670
|
+
};
|
|
671
|
+
writeStr(0, "RIFF");
|
|
672
|
+
view.setUint32(4, 36 + dataLength, true);
|
|
673
|
+
writeStr(8, "WAVE");
|
|
674
|
+
writeStr(12, "fmt ");
|
|
675
|
+
view.setUint32(16, 16, true);
|
|
676
|
+
view.setUint16(20, 1, true);
|
|
677
|
+
view.setUint16(22, 1, true);
|
|
678
|
+
view.setUint32(24, sampleRate, true);
|
|
679
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
680
|
+
view.setUint16(32, 2, true);
|
|
681
|
+
view.setUint16(34, 16, true);
|
|
682
|
+
writeStr(36, "data");
|
|
683
|
+
view.setUint32(40, dataLength, true);
|
|
684
|
+
return new Blob([header, pcmBuffer], { type: "audio/wav" });
|
|
696
685
|
};
|
|
697
|
-
const
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
});
|
|
704
|
-
if (!res.ok) {
|
|
705
|
-
const text = await res.text().catch(() => "");
|
|
706
|
-
throw new Error(`克隆任务提交失败 HTTP ${res.status} ${text}`);
|
|
686
|
+
const ensurePlayableWavBlob = async (blob) => {
|
|
687
|
+
const buf = await blob.arrayBuffer();
|
|
688
|
+
if (buf.byteLength < 4) return blob;
|
|
689
|
+
const head = String.fromCharCode(...new Uint8Array(buf, 0, 4));
|
|
690
|
+
if (head === "RIFF") {
|
|
691
|
+
return new Blob([buf], { type: "audio/wav" });
|
|
707
692
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return json.event_id;
|
|
693
|
+
console.log("[TTS] 检测到裸 PCM,补 WAV 头后播放", { size: buf.byteLength });
|
|
694
|
+
return pcmToWavBlob(buf);
|
|
711
695
|
};
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
734
|
-
}
|
|
735
|
-
if (dataLines.length === 0) continue;
|
|
736
|
-
const payload = dataLines.join("\n");
|
|
737
|
-
if (eventName === "complete") {
|
|
738
|
-
return JSON.parse(payload);
|
|
739
|
-
}
|
|
740
|
-
if (eventName === "error") {
|
|
741
|
-
let msg = payload;
|
|
742
|
-
try {
|
|
743
|
-
const parsed = JSON.parse(payload);
|
|
744
|
-
msg = typeof parsed === "string" ? parsed : (parsed == null ? void 0 : parsed.message) || payload;
|
|
745
|
-
} catch {
|
|
746
|
-
}
|
|
747
|
-
throw new Error(msg || "OmniVoice 合成失败");
|
|
696
|
+
const requestTTSAudio = async (text, signal) => {
|
|
697
|
+
let lastError = null;
|
|
698
|
+
for (let attempt = 1; attempt <= TTS_MAX_RETRIES; attempt++) {
|
|
699
|
+
if (signal == null ? void 0 : signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
700
|
+
const res = await fetch(`${TTS_URL}/web/voice/jd/text-to-speech`, {
|
|
701
|
+
method: "POST",
|
|
702
|
+
headers: {
|
|
703
|
+
"Content-Type": "application/json",
|
|
704
|
+
clientid: VOICE_CLIENT_ID,
|
|
705
|
+
"tenant-id": VOICE_TENANT_ID
|
|
706
|
+
},
|
|
707
|
+
body: JSON.stringify({ text, user: TTS_USER, timbre: 47 }),
|
|
708
|
+
signal
|
|
709
|
+
});
|
|
710
|
+
if (!res.ok) {
|
|
711
|
+
const errText = await res.text().catch(() => "");
|
|
712
|
+
lastError = new Error(`TTS HTTP ${res.status} ${errText}`);
|
|
713
|
+
if (res.status >= 500 && attempt < TTS_MAX_RETRIES) {
|
|
714
|
+
console.warn(`[TTS] ${res.status},重试 ${attempt}/${TTS_MAX_RETRIES}`);
|
|
715
|
+
await new Promise((r) => setTimeout(r, 400 * attempt));
|
|
716
|
+
continue;
|
|
748
717
|
}
|
|
749
|
-
|
|
718
|
+
throw lastError;
|
|
750
719
|
}
|
|
720
|
+
const contentType = (res.headers.get("content-type") || "").toLowerCase();
|
|
721
|
+
if (contentType.includes("audio") || contentType.includes("octet-stream")) {
|
|
722
|
+
const raw = await res.blob();
|
|
723
|
+
console.log("[TTS] 收到音频", { size: raw.size, contentType });
|
|
724
|
+
return ensurePlayableWavBlob(raw);
|
|
725
|
+
}
|
|
726
|
+
const json = await res.json();
|
|
727
|
+
const data = json == null ? void 0 : json.data;
|
|
728
|
+
const audioUrl = typeof data === "object" && ((data == null ? void 0 : data.url) || (data == null ? void 0 : data.audioUrl)) || (json == null ? void 0 : json.url) || (json == null ? void 0 : json.audioUrl);
|
|
729
|
+
const b64 = (typeof data === "string" ? data : null) || (data == null ? void 0 : data.audio) || (data == null ? void 0 : data.base64) || (json == null ? void 0 : json.audio) || (json == null ? void 0 : json.base64);
|
|
730
|
+
if (audioUrl && typeof audioUrl === "string" && /^https?:\/\//.test(audioUrl)) {
|
|
731
|
+
const audioRes = await fetch(audioUrl, { signal });
|
|
732
|
+
if (!audioRes.ok) throw new Error(`TTS 音频下载失败 HTTP ${audioRes.status}`);
|
|
733
|
+
return ensurePlayableWavBlob(await audioRes.blob());
|
|
734
|
+
}
|
|
735
|
+
if (b64 && typeof b64 === "string") {
|
|
736
|
+
const mime = (data == null ? void 0 : data.format) ? `audio/${data.format}` : "audio/wav";
|
|
737
|
+
return ensurePlayableWavBlob(base64ToBlob(b64, mime));
|
|
738
|
+
}
|
|
739
|
+
throw new Error((json == null ? void 0 : json.msg) || (json == null ? void 0 : json.message) || "TTS 未返回可播放音频");
|
|
751
740
|
}
|
|
752
|
-
throw new Error("
|
|
741
|
+
throw lastError || new Error("TTS 请求失败");
|
|
753
742
|
};
|
|
754
|
-
const
|
|
755
|
-
const audioRes = await fetch(audioUrl, { signal });
|
|
756
|
-
if (!audioRes.ok) throw new Error(`合成音频下载失败 HTTP ${audioRes.status}`);
|
|
757
|
-
const audioBlob = await audioRes.blob();
|
|
743
|
+
const playAudioBlob = async (audioBlob, requestId, signal) => {
|
|
758
744
|
const objectUrl = URL.createObjectURL(audioBlob);
|
|
759
745
|
if (signal.aborted || currentTtsRequestId.value !== requestId) {
|
|
760
746
|
URL.revokeObjectURL(objectUrl);
|
|
@@ -767,32 +753,29 @@ const _sfc_main = {
|
|
|
767
753
|
if (currentAudio.value === audio) currentAudio.value = null;
|
|
768
754
|
};
|
|
769
755
|
audio.onplaying = () => {
|
|
770
|
-
console.log("[
|
|
756
|
+
console.log("[TTS] 音频开始播放");
|
|
771
757
|
speak();
|
|
772
758
|
};
|
|
773
759
|
audio.onended = () => {
|
|
774
|
-
console.log("[
|
|
760
|
+
console.log("[TTS] 音频播放结束");
|
|
775
761
|
cleanup();
|
|
776
762
|
stopSpeaking();
|
|
777
763
|
};
|
|
778
764
|
audio.onerror = () => {
|
|
779
|
-
console.error("[
|
|
765
|
+
console.error("[TTS] 音频播放失败", audio.error);
|
|
780
766
|
cleanup();
|
|
781
767
|
stopSpeaking();
|
|
782
768
|
};
|
|
783
|
-
|
|
769
|
+
try {
|
|
770
|
+
await audio.play();
|
|
771
|
+
} catch (e) {
|
|
772
|
+
console.error("[TTS] audio.play 被拒绝或失败:", e);
|
|
773
|
+
cleanup();
|
|
774
|
+
throw e;
|
|
775
|
+
}
|
|
784
776
|
};
|
|
785
777
|
const playTTS = async (text) => {
|
|
786
|
-
var _a
|
|
787
|
-
console.log(props.humans);
|
|
788
|
-
let voiceUrl = "";
|
|
789
|
-
let voiceUrl2 = "http://192.168.3.182:8002/gradio_api/file=C:\\Users\\Administrator\\AppData\\Local\\Temp\\gradio\\4eef6d9157de756de02bccca2f66389382e2ac4acb0c674981aa9fe13dcc294c\\李梓萌.mp3";
|
|
790
|
-
let voiceUrl1 = "http://192.168.3.182:8002/gradio_api/file=C:\\Users\\Administrator\\AppData\\Local\\Temp\\gradio\\ff2665f538a4b0e5efbcd79f9f699236f5f0e52cad7e17f0664ddf431169be30\\guozj.mp3";
|
|
791
|
-
if (props.humans[0].speakSet.sex == "Female / 女") {
|
|
792
|
-
voiceUrl = voiceUrl2;
|
|
793
|
-
} else {
|
|
794
|
-
voiceUrl = voiceUrl1;
|
|
795
|
-
}
|
|
778
|
+
var _a;
|
|
796
779
|
if (!text || !((_a = currentHuman.value) == null ? void 0 : _a.isMuted)) return;
|
|
797
780
|
abortTTS();
|
|
798
781
|
ttsAbortController = new AbortController();
|
|
@@ -802,39 +785,19 @@ const _sfc_main = {
|
|
|
802
785
|
try {
|
|
803
786
|
const cleanText = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/[#*_~]/g, "").replace(/\n+/g, ",").replace(/\s+/g, " ").replace(/[,。!?]{2,}/g, ",").trim();
|
|
804
787
|
if (!cleanText) return;
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
if (!voiceSource) {
|
|
808
|
-
console.error("[OmniVoice] 未配置参考音频 voiceUrl,无法进行音色克隆");
|
|
809
|
-
standby();
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
let refFile = voiceFileCache.get(voiceSource);
|
|
813
|
-
if (!refFile) {
|
|
814
|
-
console.log("[OmniVoice] 准备参考音频:", typeof voiceSource === "string" ? voiceSource : "Blob/File");
|
|
815
|
-
refFile = await resolveVoiceFileData(voiceSource, signal);
|
|
816
|
-
voiceFileCache.set(voiceSource, refFile);
|
|
817
|
-
}
|
|
818
|
-
console.log("[OmniVoice] 参考音频就绪:", refFile);
|
|
819
|
-
const data = [cleanText, "Auto", refFile, "", null, 32, 2, true, speed, null, true, true];
|
|
820
|
-
console.log("[OmniVoice] 提交克隆请求:", { text: cleanText, length: cleanText.length });
|
|
821
|
-
const eventId = await submitCloneJob(data, signal);
|
|
822
|
-
const outputs = await waitCloneResult(eventId, signal);
|
|
823
|
-
const audioFile = outputs == null ? void 0 : outputs[0];
|
|
824
|
-
const statusText = outputs == null ? void 0 : outputs[1];
|
|
825
|
-
console.log("[OmniVoice] 合成结果:", { audioFile, statusText });
|
|
788
|
+
console.log("[TTS] 提交合成请求:", { text: cleanText, length: cleanText.length });
|
|
789
|
+
const audioBlob = await requestTTSAudio(cleanText, signal);
|
|
826
790
|
if (signal.aborted || currentTtsRequestId.value !== requestId) return;
|
|
827
|
-
if (!
|
|
828
|
-
throw new Error(
|
|
791
|
+
if (!audioBlob || audioBlob.size === 0) {
|
|
792
|
+
throw new Error("TTS 返回空音频");
|
|
829
793
|
}
|
|
830
|
-
|
|
831
|
-
await playAudioFromUrl(audioUrl, requestId, signal);
|
|
794
|
+
await playAudioBlob(audioBlob, requestId, signal);
|
|
832
795
|
} catch (e) {
|
|
833
796
|
if ((e == null ? void 0 : e.name) === "AbortError") {
|
|
834
|
-
console.log("[
|
|
797
|
+
console.log("[TTS] 已取消");
|
|
835
798
|
return;
|
|
836
799
|
}
|
|
837
|
-
console.error("[
|
|
800
|
+
console.error("[TTS] 失败:", e);
|
|
838
801
|
standby();
|
|
839
802
|
}
|
|
840
803
|
};
|
|
@@ -864,15 +827,28 @@ const _sfc_main = {
|
|
|
864
827
|
const audioToText = async (blob) => {
|
|
865
828
|
const formData = new FormData();
|
|
866
829
|
formData.append("file", blob, "recorded_audio.wav");
|
|
830
|
+
const params = new URLSearchParams({
|
|
831
|
+
domain: "general",
|
|
832
|
+
sampleRate: "16000"
|
|
833
|
+
});
|
|
834
|
+
const url = `${ASR_URL}/web/voice/jd/speech-to-text?${params}`;
|
|
867
835
|
try {
|
|
868
|
-
const res = await fetch(
|
|
836
|
+
const res = await fetch(url, {
|
|
869
837
|
method: "POST",
|
|
838
|
+
headers: {
|
|
839
|
+
clientid: VOICE_CLIENT_ID,
|
|
840
|
+
"tenant-id": VOICE_TENANT_ID
|
|
841
|
+
},
|
|
870
842
|
body: formData
|
|
871
843
|
});
|
|
844
|
+
if (!res.ok) {
|
|
845
|
+
throw new Error(`STT HTTP ${res.status}`);
|
|
846
|
+
}
|
|
872
847
|
const resJson = await res.json();
|
|
873
|
-
const
|
|
874
|
-
const
|
|
875
|
-
|
|
848
|
+
const data = resJson == null ? void 0 : resJson.data;
|
|
849
|
+
const text = (resJson == null ? void 0 : resJson.text) ?? (typeof data === "string" ? data : data == null ? void 0 : data.text) ?? (resJson == null ? void 0 : resJson.result) ?? "";
|
|
850
|
+
const status = (resJson == null ? void 0 : resJson.success) ?? (resJson == null ? void 0 : resJson.code) ?? res.ok;
|
|
851
|
+
return { status, text };
|
|
876
852
|
} catch (e) {
|
|
877
853
|
console.error("STT失败:", e);
|
|
878
854
|
return { status: e, text: "" };
|
|
@@ -929,7 +905,7 @@ const _sfc_main = {
|
|
|
929
905
|
};
|
|
930
906
|
}
|
|
931
907
|
};
|
|
932
|
-
const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
|
|
908
|
+
const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-9379171c"]]);
|
|
933
909
|
DigitalHuman.install = (app) => app.component("DigitalHuman", DigitalHuman);
|
|
934
910
|
const components = [DemoButton, DemoInput, DigitalHuman];
|
|
935
911
|
const install = (app) => {
|
package/dist/ruiyun-human.es.js
CHANGED
|
@@ -383,8 +383,11 @@ var recorder = { exports: {} };
|
|
|
383
383
|
var recorderExports = recorder.exports;
|
|
384
384
|
var jsAudioRecorder = recorderExports;
|
|
385
385
|
const Recorder = /* @__PURE__ */ getDefaultExportFromCjs(jsAudioRecorder);
|
|
386
|
-
const
|
|
387
|
-
const
|
|
386
|
+
const VOICE_CLIENT_ID = "e5cd2b";
|
|
387
|
+
const VOICE_TENANT_ID = "000000";
|
|
388
|
+
const TTS_USER = "web-test-user";
|
|
389
|
+
const TTS_PCM_SAMPLE_RATE = 16e3;
|
|
390
|
+
const TTS_MAX_RETRIES = 5;
|
|
388
391
|
const _sfc_main = {
|
|
389
392
|
__name: "index",
|
|
390
393
|
props: {
|
|
@@ -395,6 +398,8 @@ const _sfc_main = {
|
|
|
395
398
|
},
|
|
396
399
|
emits: ["loaded", "changed", "paused", "resumed", "audio-ready"],
|
|
397
400
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
401
|
+
const TTS_URL = "http://localhost:6039";
|
|
402
|
+
const ASR_URL = "http://localhost:6039";
|
|
398
403
|
const props = __props;
|
|
399
404
|
const emit = __emit;
|
|
400
405
|
const containerRef = ref(null);
|
|
@@ -647,112 +652,93 @@ const _sfc_main = {
|
|
|
647
652
|
currentAudio.value = null;
|
|
648
653
|
}
|
|
649
654
|
}
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
method: "POST",
|
|
657
|
-
body: form,
|
|
658
|
-
signal
|
|
659
|
-
});
|
|
660
|
-
if (!res.ok) throw new Error(`参考音频上传失败 HTTP ${res.status}`);
|
|
661
|
-
const paths = await res.json();
|
|
662
|
-
const path = Array.isArray(paths) ? paths[0] : paths;
|
|
663
|
-
if (!path) throw new Error("参考音频上传失败:服务未返回文件路径");
|
|
664
|
-
return { path, orig_name: filename, meta: FILEDATA_META };
|
|
655
|
+
const base64ToBlob = (b64, mime = "audio/wav") => {
|
|
656
|
+
const raw = String(b64).replace(/^data:audio\/[\w+-]+;base64,/, "");
|
|
657
|
+
const binary = atob(raw);
|
|
658
|
+
const bytes = new Uint8Array(binary.length);
|
|
659
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
660
|
+
return new Blob([bytes], { type: mime });
|
|
665
661
|
};
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const res = await fetch(voiceSource, { signal });
|
|
688
|
-
if (!res.ok) throw new Error(`参考音频下载失败 HTTP ${res.status}`);
|
|
689
|
-
const blob = await res.blob();
|
|
690
|
-
const fileName = decodeURIComponent(voiceSource.split("?")[0].split("/").pop()) || "reference.mp3";
|
|
691
|
-
return await uploadReferenceAudio(blob, fileName, signal);
|
|
692
|
-
}
|
|
693
|
-
throw new Error("不支持的参考音频来源:voiceUrl 应为音频 URL 字符串、File/Blob 或 { path } 对象");
|
|
662
|
+
const pcmToWavBlob = (pcmBuffer, sampleRate = TTS_PCM_SAMPLE_RATE) => {
|
|
663
|
+
const dataLength = pcmBuffer.byteLength;
|
|
664
|
+
const header = new ArrayBuffer(44);
|
|
665
|
+
const view = new DataView(header);
|
|
666
|
+
const writeStr = (offset, str) => {
|
|
667
|
+
for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
|
|
668
|
+
};
|
|
669
|
+
writeStr(0, "RIFF");
|
|
670
|
+
view.setUint32(4, 36 + dataLength, true);
|
|
671
|
+
writeStr(8, "WAVE");
|
|
672
|
+
writeStr(12, "fmt ");
|
|
673
|
+
view.setUint32(16, 16, true);
|
|
674
|
+
view.setUint16(20, 1, true);
|
|
675
|
+
view.setUint16(22, 1, true);
|
|
676
|
+
view.setUint32(24, sampleRate, true);
|
|
677
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
678
|
+
view.setUint16(32, 2, true);
|
|
679
|
+
view.setUint16(34, 16, true);
|
|
680
|
+
writeStr(36, "data");
|
|
681
|
+
view.setUint32(40, dataLength, true);
|
|
682
|
+
return new Blob([header, pcmBuffer], { type: "audio/wav" });
|
|
694
683
|
};
|
|
695
|
-
const
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
});
|
|
702
|
-
if (!res.ok) {
|
|
703
|
-
const text = await res.text().catch(() => "");
|
|
704
|
-
throw new Error(`克隆任务提交失败 HTTP ${res.status} ${text}`);
|
|
684
|
+
const ensurePlayableWavBlob = async (blob) => {
|
|
685
|
+
const buf = await blob.arrayBuffer();
|
|
686
|
+
if (buf.byteLength < 4) return blob;
|
|
687
|
+
const head = String.fromCharCode(...new Uint8Array(buf, 0, 4));
|
|
688
|
+
if (head === "RIFF") {
|
|
689
|
+
return new Blob([buf], { type: "audio/wav" });
|
|
705
690
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
return json.event_id;
|
|
691
|
+
console.log("[TTS] 检测到裸 PCM,补 WAV 头后播放", { size: buf.byteLength });
|
|
692
|
+
return pcmToWavBlob(buf);
|
|
709
693
|
};
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
732
|
-
}
|
|
733
|
-
if (dataLines.length === 0) continue;
|
|
734
|
-
const payload = dataLines.join("\n");
|
|
735
|
-
if (eventName === "complete") {
|
|
736
|
-
return JSON.parse(payload);
|
|
737
|
-
}
|
|
738
|
-
if (eventName === "error") {
|
|
739
|
-
let msg = payload;
|
|
740
|
-
try {
|
|
741
|
-
const parsed = JSON.parse(payload);
|
|
742
|
-
msg = typeof parsed === "string" ? parsed : (parsed == null ? void 0 : parsed.message) || payload;
|
|
743
|
-
} catch {
|
|
744
|
-
}
|
|
745
|
-
throw new Error(msg || "OmniVoice 合成失败");
|
|
694
|
+
const requestTTSAudio = async (text, signal) => {
|
|
695
|
+
let lastError = null;
|
|
696
|
+
for (let attempt = 1; attempt <= TTS_MAX_RETRIES; attempt++) {
|
|
697
|
+
if (signal == null ? void 0 : signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
698
|
+
const res = await fetch(`${TTS_URL}/web/voice/jd/text-to-speech`, {
|
|
699
|
+
method: "POST",
|
|
700
|
+
headers: {
|
|
701
|
+
"Content-Type": "application/json",
|
|
702
|
+
clientid: VOICE_CLIENT_ID,
|
|
703
|
+
"tenant-id": VOICE_TENANT_ID
|
|
704
|
+
},
|
|
705
|
+
body: JSON.stringify({ text, user: TTS_USER, timbre: 47 }),
|
|
706
|
+
signal
|
|
707
|
+
});
|
|
708
|
+
if (!res.ok) {
|
|
709
|
+
const errText = await res.text().catch(() => "");
|
|
710
|
+
lastError = new Error(`TTS HTTP ${res.status} ${errText}`);
|
|
711
|
+
if (res.status >= 500 && attempt < TTS_MAX_RETRIES) {
|
|
712
|
+
console.warn(`[TTS] ${res.status},重试 ${attempt}/${TTS_MAX_RETRIES}`);
|
|
713
|
+
await new Promise((r) => setTimeout(r, 400 * attempt));
|
|
714
|
+
continue;
|
|
746
715
|
}
|
|
747
|
-
|
|
716
|
+
throw lastError;
|
|
748
717
|
}
|
|
718
|
+
const contentType = (res.headers.get("content-type") || "").toLowerCase();
|
|
719
|
+
if (contentType.includes("audio") || contentType.includes("octet-stream")) {
|
|
720
|
+
const raw = await res.blob();
|
|
721
|
+
console.log("[TTS] 收到音频", { size: raw.size, contentType });
|
|
722
|
+
return ensurePlayableWavBlob(raw);
|
|
723
|
+
}
|
|
724
|
+
const json = await res.json();
|
|
725
|
+
const data = json == null ? void 0 : json.data;
|
|
726
|
+
const audioUrl = typeof data === "object" && ((data == null ? void 0 : data.url) || (data == null ? void 0 : data.audioUrl)) || (json == null ? void 0 : json.url) || (json == null ? void 0 : json.audioUrl);
|
|
727
|
+
const b64 = (typeof data === "string" ? data : null) || (data == null ? void 0 : data.audio) || (data == null ? void 0 : data.base64) || (json == null ? void 0 : json.audio) || (json == null ? void 0 : json.base64);
|
|
728
|
+
if (audioUrl && typeof audioUrl === "string" && /^https?:\/\//.test(audioUrl)) {
|
|
729
|
+
const audioRes = await fetch(audioUrl, { signal });
|
|
730
|
+
if (!audioRes.ok) throw new Error(`TTS 音频下载失败 HTTP ${audioRes.status}`);
|
|
731
|
+
return ensurePlayableWavBlob(await audioRes.blob());
|
|
732
|
+
}
|
|
733
|
+
if (b64 && typeof b64 === "string") {
|
|
734
|
+
const mime = (data == null ? void 0 : data.format) ? `audio/${data.format}` : "audio/wav";
|
|
735
|
+
return ensurePlayableWavBlob(base64ToBlob(b64, mime));
|
|
736
|
+
}
|
|
737
|
+
throw new Error((json == null ? void 0 : json.msg) || (json == null ? void 0 : json.message) || "TTS 未返回可播放音频");
|
|
749
738
|
}
|
|
750
|
-
throw new Error("
|
|
739
|
+
throw lastError || new Error("TTS 请求失败");
|
|
751
740
|
};
|
|
752
|
-
const
|
|
753
|
-
const audioRes = await fetch(audioUrl, { signal });
|
|
754
|
-
if (!audioRes.ok) throw new Error(`合成音频下载失败 HTTP ${audioRes.status}`);
|
|
755
|
-
const audioBlob = await audioRes.blob();
|
|
741
|
+
const playAudioBlob = async (audioBlob, requestId, signal) => {
|
|
756
742
|
const objectUrl = URL.createObjectURL(audioBlob);
|
|
757
743
|
if (signal.aborted || currentTtsRequestId.value !== requestId) {
|
|
758
744
|
URL.revokeObjectURL(objectUrl);
|
|
@@ -765,32 +751,29 @@ const _sfc_main = {
|
|
|
765
751
|
if (currentAudio.value === audio) currentAudio.value = null;
|
|
766
752
|
};
|
|
767
753
|
audio.onplaying = () => {
|
|
768
|
-
console.log("[
|
|
754
|
+
console.log("[TTS] 音频开始播放");
|
|
769
755
|
speak();
|
|
770
756
|
};
|
|
771
757
|
audio.onended = () => {
|
|
772
|
-
console.log("[
|
|
758
|
+
console.log("[TTS] 音频播放结束");
|
|
773
759
|
cleanup();
|
|
774
760
|
stopSpeaking();
|
|
775
761
|
};
|
|
776
762
|
audio.onerror = () => {
|
|
777
|
-
console.error("[
|
|
763
|
+
console.error("[TTS] 音频播放失败", audio.error);
|
|
778
764
|
cleanup();
|
|
779
765
|
stopSpeaking();
|
|
780
766
|
};
|
|
781
|
-
|
|
767
|
+
try {
|
|
768
|
+
await audio.play();
|
|
769
|
+
} catch (e) {
|
|
770
|
+
console.error("[TTS] audio.play 被拒绝或失败:", e);
|
|
771
|
+
cleanup();
|
|
772
|
+
throw e;
|
|
773
|
+
}
|
|
782
774
|
};
|
|
783
775
|
const playTTS = async (text) => {
|
|
784
|
-
var _a
|
|
785
|
-
console.log(props.humans);
|
|
786
|
-
let voiceUrl = "";
|
|
787
|
-
let voiceUrl2 = "http://192.168.3.182:8002/gradio_api/file=C:\\Users\\Administrator\\AppData\\Local\\Temp\\gradio\\4eef6d9157de756de02bccca2f66389382e2ac4acb0c674981aa9fe13dcc294c\\李梓萌.mp3";
|
|
788
|
-
let voiceUrl1 = "http://192.168.3.182:8002/gradio_api/file=C:\\Users\\Administrator\\AppData\\Local\\Temp\\gradio\\ff2665f538a4b0e5efbcd79f9f699236f5f0e52cad7e17f0664ddf431169be30\\guozj.mp3";
|
|
789
|
-
if (props.humans[0].speakSet.sex == "Female / 女") {
|
|
790
|
-
voiceUrl = voiceUrl2;
|
|
791
|
-
} else {
|
|
792
|
-
voiceUrl = voiceUrl1;
|
|
793
|
-
}
|
|
776
|
+
var _a;
|
|
794
777
|
if (!text || !((_a = currentHuman.value) == null ? void 0 : _a.isMuted)) return;
|
|
795
778
|
abortTTS();
|
|
796
779
|
ttsAbortController = new AbortController();
|
|
@@ -800,39 +783,19 @@ const _sfc_main = {
|
|
|
800
783
|
try {
|
|
801
784
|
const cleanText = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/[#*_~]/g, "").replace(/\n+/g, ",").replace(/\s+/g, " ").replace(/[,。!?]{2,}/g, ",").trim();
|
|
802
785
|
if (!cleanText) return;
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
if (!voiceSource) {
|
|
806
|
-
console.error("[OmniVoice] 未配置参考音频 voiceUrl,无法进行音色克隆");
|
|
807
|
-
standby();
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
let refFile = voiceFileCache.get(voiceSource);
|
|
811
|
-
if (!refFile) {
|
|
812
|
-
console.log("[OmniVoice] 准备参考音频:", typeof voiceSource === "string" ? voiceSource : "Blob/File");
|
|
813
|
-
refFile = await resolveVoiceFileData(voiceSource, signal);
|
|
814
|
-
voiceFileCache.set(voiceSource, refFile);
|
|
815
|
-
}
|
|
816
|
-
console.log("[OmniVoice] 参考音频就绪:", refFile);
|
|
817
|
-
const data = [cleanText, "Auto", refFile, "", null, 32, 2, true, speed, null, true, true];
|
|
818
|
-
console.log("[OmniVoice] 提交克隆请求:", { text: cleanText, length: cleanText.length });
|
|
819
|
-
const eventId = await submitCloneJob(data, signal);
|
|
820
|
-
const outputs = await waitCloneResult(eventId, signal);
|
|
821
|
-
const audioFile = outputs == null ? void 0 : outputs[0];
|
|
822
|
-
const statusText = outputs == null ? void 0 : outputs[1];
|
|
823
|
-
console.log("[OmniVoice] 合成结果:", { audioFile, statusText });
|
|
786
|
+
console.log("[TTS] 提交合成请求:", { text: cleanText, length: cleanText.length });
|
|
787
|
+
const audioBlob = await requestTTSAudio(cleanText, signal);
|
|
824
788
|
if (signal.aborted || currentTtsRequestId.value !== requestId) return;
|
|
825
|
-
if (!
|
|
826
|
-
throw new Error(
|
|
789
|
+
if (!audioBlob || audioBlob.size === 0) {
|
|
790
|
+
throw new Error("TTS 返回空音频");
|
|
827
791
|
}
|
|
828
|
-
|
|
829
|
-
await playAudioFromUrl(audioUrl, requestId, signal);
|
|
792
|
+
await playAudioBlob(audioBlob, requestId, signal);
|
|
830
793
|
} catch (e) {
|
|
831
794
|
if ((e == null ? void 0 : e.name) === "AbortError") {
|
|
832
|
-
console.log("[
|
|
795
|
+
console.log("[TTS] 已取消");
|
|
833
796
|
return;
|
|
834
797
|
}
|
|
835
|
-
console.error("[
|
|
798
|
+
console.error("[TTS] 失败:", e);
|
|
836
799
|
standby();
|
|
837
800
|
}
|
|
838
801
|
};
|
|
@@ -862,15 +825,28 @@ const _sfc_main = {
|
|
|
862
825
|
const audioToText = async (blob) => {
|
|
863
826
|
const formData = new FormData();
|
|
864
827
|
formData.append("file", blob, "recorded_audio.wav");
|
|
828
|
+
const params = new URLSearchParams({
|
|
829
|
+
domain: "general",
|
|
830
|
+
sampleRate: "16000"
|
|
831
|
+
});
|
|
832
|
+
const url = `${ASR_URL}/web/voice/jd/speech-to-text?${params}`;
|
|
865
833
|
try {
|
|
866
|
-
const res = await fetch(
|
|
834
|
+
const res = await fetch(url, {
|
|
867
835
|
method: "POST",
|
|
836
|
+
headers: {
|
|
837
|
+
clientid: VOICE_CLIENT_ID,
|
|
838
|
+
"tenant-id": VOICE_TENANT_ID
|
|
839
|
+
},
|
|
868
840
|
body: formData
|
|
869
841
|
});
|
|
842
|
+
if (!res.ok) {
|
|
843
|
+
throw new Error(`STT HTTP ${res.status}`);
|
|
844
|
+
}
|
|
870
845
|
const resJson = await res.json();
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
846
|
+
const data = resJson == null ? void 0 : resJson.data;
|
|
847
|
+
const text = (resJson == null ? void 0 : resJson.text) ?? (typeof data === "string" ? data : data == null ? void 0 : data.text) ?? (resJson == null ? void 0 : resJson.result) ?? "";
|
|
848
|
+
const status = (resJson == null ? void 0 : resJson.success) ?? (resJson == null ? void 0 : resJson.code) ?? res.ok;
|
|
849
|
+
return { status, text };
|
|
874
850
|
} catch (e) {
|
|
875
851
|
console.error("STT失败:", e);
|
|
876
852
|
return { status: e, text: "" };
|
|
@@ -927,7 +903,7 @@ const _sfc_main = {
|
|
|
927
903
|
};
|
|
928
904
|
}
|
|
929
905
|
};
|
|
930
|
-
const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
|
|
906
|
+
const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-9379171c"]]);
|
|
931
907
|
DigitalHuman.install = (app) => app.component("DigitalHuman", DigitalHuman);
|
|
932
908
|
const components = [DemoButton, DemoInput, DigitalHuman];
|
|
933
909
|
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-9379171c]{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.digital-human canvas[data-v-9379171c]{max-width:100%;max-height:100%}
|