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.
@@ -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 baseTTSUrl = "http://192.168.3.182:8002";
389
- const ASR_URL = "http://192.168.3.182:9988";
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 voiceFileCache = /* @__PURE__ */ new Map();
653
- const FILEDATA_META = { _type: "gradio.FileData" };
654
- const uploadReferenceAudio = async (blob, filename, signal) => {
655
- const form = new FormData();
656
- form.append("files", blob, filename);
657
- const res = await fetch(`${baseTTSUrl}/gradio_api/upload`, {
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 resolveVoiceFileData = async (voiceSource, signal) => {
669
- if (voiceSource && typeof voiceSource === "object" && !Array.isArray(voiceSource) && voiceSource.path) {
670
- return {
671
- path: voiceSource.path,
672
- orig_name: voiceSource.orig_name || "reference.mp3",
673
- meta: FILEDATA_META
674
- };
675
- }
676
- if (voiceSource instanceof Blob) {
677
- const name = voiceSource.name || `reference_${Date.now()}.mp3`;
678
- return await uploadReferenceAudio(voiceSource, name, signal);
679
- }
680
- if (typeof voiceSource === "string" && voiceSource) {
681
- if (voiceSource.startsWith(baseTTSUrl)) {
682
- const match = voiceSource.match(/\/gradio_api\/file=(.+)$/);
683
- if (match) {
684
- const path = decodeURIComponent(match[1]);
685
- const origName = path.split(/[\\/]/).pop() || "reference.mp3";
686
- return { path, orig_name: origName, meta: FILEDATA_META };
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 submitCloneJob = async (data, signal) => {
698
- const res = await fetch(`${baseTTSUrl}/gradio_api/call/_clone_fn`, {
699
- method: "POST",
700
- headers: { "Content-Type": "application/json" },
701
- body: JSON.stringify({ data }),
702
- signal
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
- const json = await res.json();
709
- if (!(json == null ? void 0 : json.event_id)) throw new Error("克隆任务提交失败:未返回 event_id");
710
- return json.event_id;
693
+ console.log("[TTS] 检测到裸 PCM,补 WAV 头后播放", { size: buf.byteLength });
694
+ return pcmToWavBlob(buf);
711
695
  };
712
- const waitCloneResult = async (eventId, signal) => {
713
- const res = await fetch(`${baseTTSUrl}/gradio_api/call/_clone_fn/${eventId}`, {
714
- signal,
715
- headers: { Accept: "text/event-stream" }
716
- });
717
- if (!res.ok) throw new Error(`合成结果监听失败 HTTP ${res.status}`);
718
- const reader = res.body.getReader();
719
- const decoder = new TextDecoder("utf-8");
720
- let buffer = "";
721
- let eventName = "message";
722
- while (true) {
723
- const { done, value } = await reader.read();
724
- if (done) break;
725
- buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
726
- let sep;
727
- while ((sep = buffer.indexOf("\n\n")) !== -1) {
728
- const frame = buffer.slice(0, sep);
729
- buffer = buffer.slice(sep + 2);
730
- const dataLines = [];
731
- for (const line of frame.split("\n")) {
732
- if (line.startsWith("event:")) eventName = line.slice(6).trim();
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
- eventName = "message";
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("OmniVoice 合成结果流提前中断");
741
+ throw lastError || new Error("TTS 请求失败");
753
742
  };
754
- const playAudioFromUrl = async (audioUrl, requestId, signal) => {
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("[OmniVoice] 音频开始播放");
756
+ console.log("[TTS] 音频开始播放");
771
757
  speak();
772
758
  };
773
759
  audio.onended = () => {
774
- console.log("[OmniVoice] 音频播放结束");
760
+ console.log("[TTS] 音频播放结束");
775
761
  cleanup();
776
762
  stopSpeaking();
777
763
  };
778
764
  audio.onerror = () => {
779
- console.error("[OmniVoice] 音频播放失败");
765
+ console.error("[TTS] 音频播放失败", audio.error);
780
766
  cleanup();
781
767
  stopSpeaking();
782
768
  };
783
- await audio.play();
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, _b, _c, _d;
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
- const speed = Number((_c = (_b = currentHuman.value) == null ? void 0 : _b.speakSet) == null ? void 0 : _c.speed) || 1;
806
- const voiceSource = voiceUrl || ((_d = currentHuman.value) == null ? void 0 : _d.voiceUrl);
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 (!audioFile || !audioFile.path && !audioFile.url) {
828
- throw new Error(statusText || "OmniVoice 未返回合成音频");
791
+ if (!audioBlob || audioBlob.size === 0) {
792
+ throw new Error("TTS 返回空音频");
829
793
  }
830
- const audioUrl = audioFile.url && audioFile.url.startsWith("http") ? audioFile.url : `${baseTTSUrl}/gradio_api/file=${audioFile.path}`;
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("[OmniVoice] TTS已取消");
797
+ console.log("[TTS] 已取消");
835
798
  return;
836
799
  }
837
- console.error("[OmniVoice] TTS失败:", e);
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(ASR_URL + "/transcribe", {
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 text = (resJson == null ? void 0 : resJson.text) || (resJson == null ? void 0 : resJson.text);
874
- const message = (resJson == null ? void 0 : resJson.success) || (resJson == null ? void 0 : resJson.success);
875
- return { status: message, text };
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-9c25016c"]]);
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) => {
@@ -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 baseTTSUrl = "http://192.168.3.182:8002";
387
- const ASR_URL = "http://192.168.3.182:9988";
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 voiceFileCache = /* @__PURE__ */ new Map();
651
- const FILEDATA_META = { _type: "gradio.FileData" };
652
- const uploadReferenceAudio = async (blob, filename, signal) => {
653
- const form = new FormData();
654
- form.append("files", blob, filename);
655
- const res = await fetch(`${baseTTSUrl}/gradio_api/upload`, {
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 resolveVoiceFileData = async (voiceSource, signal) => {
667
- if (voiceSource && typeof voiceSource === "object" && !Array.isArray(voiceSource) && voiceSource.path) {
668
- return {
669
- path: voiceSource.path,
670
- orig_name: voiceSource.orig_name || "reference.mp3",
671
- meta: FILEDATA_META
672
- };
673
- }
674
- if (voiceSource instanceof Blob) {
675
- const name = voiceSource.name || `reference_${Date.now()}.mp3`;
676
- return await uploadReferenceAudio(voiceSource, name, signal);
677
- }
678
- if (typeof voiceSource === "string" && voiceSource) {
679
- if (voiceSource.startsWith(baseTTSUrl)) {
680
- const match = voiceSource.match(/\/gradio_api\/file=(.+)$/);
681
- if (match) {
682
- const path = decodeURIComponent(match[1]);
683
- const origName = path.split(/[\\/]/).pop() || "reference.mp3";
684
- return { path, orig_name: origName, meta: FILEDATA_META };
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 submitCloneJob = async (data, signal) => {
696
- const res = await fetch(`${baseTTSUrl}/gradio_api/call/_clone_fn`, {
697
- method: "POST",
698
- headers: { "Content-Type": "application/json" },
699
- body: JSON.stringify({ data }),
700
- signal
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
- const json = await res.json();
707
- if (!(json == null ? void 0 : json.event_id)) throw new Error("克隆任务提交失败:未返回 event_id");
708
- return json.event_id;
691
+ console.log("[TTS] 检测到裸 PCM,补 WAV 头后播放", { size: buf.byteLength });
692
+ return pcmToWavBlob(buf);
709
693
  };
710
- const waitCloneResult = async (eventId, signal) => {
711
- const res = await fetch(`${baseTTSUrl}/gradio_api/call/_clone_fn/${eventId}`, {
712
- signal,
713
- headers: { Accept: "text/event-stream" }
714
- });
715
- if (!res.ok) throw new Error(`合成结果监听失败 HTTP ${res.status}`);
716
- const reader = res.body.getReader();
717
- const decoder = new TextDecoder("utf-8");
718
- let buffer = "";
719
- let eventName = "message";
720
- while (true) {
721
- const { done, value } = await reader.read();
722
- if (done) break;
723
- buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
724
- let sep;
725
- while ((sep = buffer.indexOf("\n\n")) !== -1) {
726
- const frame = buffer.slice(0, sep);
727
- buffer = buffer.slice(sep + 2);
728
- const dataLines = [];
729
- for (const line of frame.split("\n")) {
730
- if (line.startsWith("event:")) eventName = line.slice(6).trim();
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
- eventName = "message";
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("OmniVoice 合成结果流提前中断");
739
+ throw lastError || new Error("TTS 请求失败");
751
740
  };
752
- const playAudioFromUrl = async (audioUrl, requestId, signal) => {
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("[OmniVoice] 音频开始播放");
754
+ console.log("[TTS] 音频开始播放");
769
755
  speak();
770
756
  };
771
757
  audio.onended = () => {
772
- console.log("[OmniVoice] 音频播放结束");
758
+ console.log("[TTS] 音频播放结束");
773
759
  cleanup();
774
760
  stopSpeaking();
775
761
  };
776
762
  audio.onerror = () => {
777
- console.error("[OmniVoice] 音频播放失败");
763
+ console.error("[TTS] 音频播放失败", audio.error);
778
764
  cleanup();
779
765
  stopSpeaking();
780
766
  };
781
- await audio.play();
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, _b, _c, _d;
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
- const speed = Number((_c = (_b = currentHuman.value) == null ? void 0 : _b.speakSet) == null ? void 0 : _c.speed) || 1;
804
- const voiceSource = voiceUrl || ((_d = currentHuman.value) == null ? void 0 : _d.voiceUrl);
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 (!audioFile || !audioFile.path && !audioFile.url) {
826
- throw new Error(statusText || "OmniVoice 未返回合成音频");
789
+ if (!audioBlob || audioBlob.size === 0) {
790
+ throw new Error("TTS 返回空音频");
827
791
  }
828
- const audioUrl = audioFile.url && audioFile.url.startsWith("http") ? audioFile.url : `${baseTTSUrl}/gradio_api/file=${audioFile.path}`;
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("[OmniVoice] TTS已取消");
795
+ console.log("[TTS] 已取消");
833
796
  return;
834
797
  }
835
- console.error("[OmniVoice] TTS失败:", e);
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(ASR_URL + "/transcribe", {
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 text = (resJson == null ? void 0 : resJson.text) || (resJson == null ? void 0 : resJson.text);
872
- const message = (resJson == null ? void 0 : resJson.success) || (resJson == null ? void 0 : resJson.success);
873
- return { status: message, text };
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-9c25016c"]]);
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-9c25016c]{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.digital-human canvas[data-v-9c25016c]{max-width:100%;max-height:100%}
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%}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruiyun-human",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "type": "module",
5
5
  "description": "数字人初步实现",
6
6
  "main": "./dist/ruiyun-human.cjs.js",