ruiyun-human 1.0.9 → 1.0.11

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 ADDED
@@ -0,0 +1,7 @@
1
+ # shuzirenzujian
2
+
3
+ 数字人前端自助机组件
4
+ npm 发包
5
+ npm publish
6
+
7
+ npm所有人 睿政云-陆洋
@@ -385,8 +385,8 @@ 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://127.0.0.1:8002";
389
- const ASR_URL = "http://127.0.0.1:9988/transcribe";
388
+ const baseTTSUrl = "http://192.168.3.182:8002";
389
+ const ASR_URL = "http://192.168.3.182:9988";
390
390
  const _sfc_main = {
391
391
  __name: "index",
392
392
  props: {
@@ -649,22 +649,150 @@ const _sfc_main = {
649
649
  currentAudio.value = null;
650
650
  }
651
651
  }
652
- function waitWithSignal(ms, signal) {
653
- return new Promise((resolve, reject) => {
654
- if (signal == null ? void 0 : signal.aborted) {
655
- reject(new DOMException("Aborted", "AbortError"));
656
- return;
657
- }
658
- const timer = setTimeout(resolve, ms);
659
- const onAbort = () => {
660
- clearTimeout(timer);
661
- reject(new DOMException("Aborted", "AbortError"));
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 };
667
+ };
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
662
674
  };
663
- signal == null ? void 0 : signal.addEventListener("abort", onAbort, { once: true });
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 } 对象");
696
+ };
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
664
703
  });
665
- }
704
+ if (!res.ok) {
705
+ const text = await res.text().catch(() => "");
706
+ throw new Error(`克隆任务提交失败 HTTP ${res.status} ${text}`);
707
+ }
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;
711
+ };
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 合成失败");
748
+ }
749
+ eventName = "message";
750
+ }
751
+ }
752
+ throw new Error("OmniVoice 合成结果流提前中断");
753
+ };
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();
758
+ const objectUrl = URL.createObjectURL(audioBlob);
759
+ if (signal.aborted || currentTtsRequestId.value !== requestId) {
760
+ URL.revokeObjectURL(objectUrl);
761
+ return;
762
+ }
763
+ const audio = new Audio(objectUrl);
764
+ currentAudio.value = audio;
765
+ const cleanup = () => {
766
+ URL.revokeObjectURL(objectUrl);
767
+ if (currentAudio.value === audio) currentAudio.value = null;
768
+ };
769
+ audio.onplaying = () => {
770
+ console.log("[OmniVoice] 音频开始播放");
771
+ speak();
772
+ };
773
+ audio.onended = () => {
774
+ console.log("[OmniVoice] 音频播放结束");
775
+ cleanup();
776
+ stopSpeaking();
777
+ };
778
+ audio.onerror = () => {
779
+ console.error("[OmniVoice] 音频播放失败");
780
+ cleanup();
781
+ stopSpeaking();
782
+ };
783
+ await audio.play();
784
+ };
666
785
  const playTTS = async (text) => {
667
- var _a, _b, _c, _d, _e, _f, _g, _h;
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
+ }
668
796
  if (!text || !((_a = currentHuman.value) == null ? void 0 : _a.isMuted)) return;
669
797
  abortTTS();
670
798
  ttsAbortController = new AbortController();
@@ -674,132 +802,33 @@ const _sfc_main = {
674
802
  try {
675
803
  const cleanText = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/[#*_~]/g, "").replace(/\n+/g, ",").replace(/\s+/g, " ").replace(/[,。!?]{2,}/g, ",").trim();
676
804
  if (!cleanText) return;
677
- const baseUrl = baseTTSUrl;
678
- const sessionHash = "session_" + Date.now();
679
- let speed = ((_c = (_b = currentHuman.value) == null ? void 0 : _b.speakSet) == null ? void 0 : _c.speed) || 1;
680
- let age = ((_e = (_d = currentHuman.value) == null ? void 0 : _d.speakSet) == null ? void 0 : _e.age) || "Young Adult / 青年";
681
- let sex = ((_g = (_f = currentHuman.value) == null ? void 0 : _f.speakSet) == null ? void 0 : _g.sex) || "Female / 女";
682
- const data = [cleanText, "Auto", 32, 2, true, speed, null, true, true, sex, age, "Auto", "Auto", "Auto", "Auto"];
683
- console.log("[OmniVoice] 提交请求:", { text: cleanText, length: cleanText.length, fn_index: 1 });
684
- const joinRes = await fetch(`${baseUrl}/gradio_api/queue/join`, {
685
- method: "POST",
686
- headers: { "Content-Type": "application/json" },
687
- body: JSON.stringify({ data, session_hash: sessionHash, fn_index: 1, trigger_id: 50 }),
688
- signal
689
- });
690
- const joinData = await joinRes.json();
691
- console.log("[OmniVoice] Step1 响应:", joinData);
692
- console.log("[OmniVoice] Step2: 轮询等待结果");
693
- let audioUrl = "";
694
- let maxPolls = 60;
695
- let pollInterval = 1e3;
696
- while (!audioUrl && maxPolls > 0) {
697
- if (signal.aborted || currentTtsRequestId.value !== requestId) return;
698
- await waitWithSignal(pollInterval, signal);
699
- maxPolls--;
700
- try {
701
- const pollRes = await fetch(`${baseUrl}/gradio_api/queue/data?session_hash=${sessionHash}`, {
702
- signal
703
- });
704
- const pollText = await pollRes.text();
705
- const lines = pollText.split("\n");
706
- for (const line of lines) {
707
- const trimLine = line.trim();
708
- if (!trimLine.startsWith("data: ")) continue;
709
- try {
710
- const json = JSON.parse(trimLine.replace("data: ", ""));
711
- console.log("[OmniVoice] 轮询消息:", json);
712
- if (json.msg === "process_completed" && ((_h = json.output) == null ? void 0 : _h.data)) {
713
- const fileData = json.output.data[0];
714
- console.log("[OmniVoice] 获取到音频:", fileData);
715
- if (fileData == null ? void 0 : fileData.url) audioUrl = fileData.url;
716
- else if (fileData == null ? void 0 : fileData.path) audioUrl = `${baseUrl}/gradio_api/file=${fileData.path}`;
717
- else if (typeof fileData === "string") audioUrl = fileData.startsWith("http") ? fileData : `${baseUrl}/gradio_api/file=${fileData}`;
718
- else if (Array.isArray(fileData) && fileData.length >= 2) {
719
- const samplingRate = fileData[0];
720
- const waveform = fileData[1];
721
- console.log("[OmniVoice] 直接获取到音频数组:", { samplingRate, waveformLength: waveform == null ? void 0 : waveform.length });
722
- if (samplingRate && waveform && Array.isArray(waveform)) {
723
- const audioBuffer = new ArrayBuffer(waveform.length * 2);
724
- const view = new DataView(audioBuffer);
725
- for (let i = 0; i < waveform.length; i++) {
726
- view.setInt16(i * 2, waveform[i], true);
727
- }
728
- const wavBlob = createWavBlob(audioBuffer, samplingRate, 1, 16);
729
- const audioObjectUrl = URL.createObjectURL(wavBlob);
730
- if (signal.aborted || currentTtsRequestId.value !== requestId) {
731
- URL.revokeObjectURL(audioObjectUrl);
732
- return;
733
- }
734
- currentAudio.value = new Audio(audioObjectUrl);
735
- currentAudio.value.onplaying = () => {
736
- console.log("[OmniVoice] 音频开始播放");
737
- speak();
738
- };
739
- currentAudio.value.onended = () => {
740
- console.log("[OmniVoice] 音频播放结束");
741
- stopSpeaking();
742
- currentAudio.value = null;
743
- URL.revokeObjectURL(audioObjectUrl);
744
- };
745
- currentAudio.value.onerror = () => {
746
- console.error("[OmniVoice] 音频播放失败");
747
- stopSpeaking();
748
- currentAudio.value = null;
749
- URL.revokeObjectURL(audioObjectUrl);
750
- };
751
- await currentAudio.value.play();
752
- return;
753
- }
754
- }
755
- break;
756
- } else if (json.msg === "process_error") {
757
- console.error("[OmniVoice] 处理错误:", json);
758
- standby();
759
- return;
760
- }
761
- } catch (e) {
762
- console.error("[OmniVoice] 解析轮询消息失败:", e);
763
- }
764
- }
765
- } catch (e) {
766
- if ((e == null ? void 0 : e.name) === "AbortError") throw e;
767
- console.warn("[OmniVoice] 轮询失败:", e.message);
768
- }
769
- }
770
- if (audioUrl) {
771
- if (audioUrl.startsWith("/")) audioUrl = baseUrl + audioUrl;
772
- console.log("[OmniVoice] 下载音频:", audioUrl);
773
- if (signal.aborted || currentTtsRequestId.value !== requestId) return;
774
- const audioRes = await fetch(audioUrl, { signal });
775
- const audioBlob = await audioRes.blob();
776
- const audioObjectUrl = URL.createObjectURL(audioBlob);
777
- if (signal.aborted || currentTtsRequestId.value !== requestId) {
778
- URL.revokeObjectURL(audioObjectUrl);
779
- return;
780
- }
781
- currentAudio.value = new Audio(audioObjectUrl);
782
- currentAudio.value.onplaying = () => {
783
- console.log("[OmniVoice] 音频开始播放");
784
- speak();
785
- };
786
- currentAudio.value.onended = () => {
787
- console.log("[OmniVoice] 音频播放结束");
788
- stopSpeaking();
789
- currentAudio.value = null;
790
- URL.revokeObjectURL(audioObjectUrl);
791
- };
792
- currentAudio.value.onerror = () => {
793
- console.error("[OmniVoice] 音频播放失败");
794
- stopSpeaking();
795
- currentAudio.value = null;
796
- URL.revokeObjectURL(audioObjectUrl);
797
- };
798
- await currentAudio.value.play();
799
- } else {
800
- console.error("[OmniVoice] 未获取到音频");
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,无法进行音色克隆");
801
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);
802
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 });
826
+ if (signal.aborted || currentTtsRequestId.value !== requestId) return;
827
+ if (!audioFile || !audioFile.path && !audioFile.url) {
828
+ throw new Error(statusText || "OmniVoice 未返回合成音频");
829
+ }
830
+ const audioUrl = audioFile.url && audioFile.url.startsWith("http") ? audioFile.url : `${baseTTSUrl}/gradio_api/file=${audioFile.path}`;
831
+ await playAudioFromUrl(audioUrl, requestId, signal);
803
832
  } catch (e) {
804
833
  if ((e == null ? void 0 : e.name) === "AbortError") {
805
834
  console.log("[OmniVoice] TTS已取消");
@@ -836,7 +865,7 @@ const _sfc_main = {
836
865
  const formData = new FormData();
837
866
  formData.append("file", blob, "recorded_audio.wav");
838
867
  try {
839
- const res = await fetch(ASR_URL, {
868
+ const res = await fetch(ASR_URL + "/transcribe", {
840
869
  method: "POST",
841
870
  body: formData
842
871
  });
@@ -849,42 +878,6 @@ const _sfc_main = {
849
878
  return { status: e, text: "" };
850
879
  }
851
880
  };
852
- const createWavBlob = (audioBuffer, sampleRate, channels, bitsPerSample) => {
853
- const bytesPerSample = bitsPerSample / 8;
854
- const blockAlign = channels * bytesPerSample;
855
- const dataSize = audioBuffer.byteLength;
856
- const buffer = new ArrayBuffer(44 + dataSize);
857
- const view = new DataView(buffer);
858
- view.setUint8(0, 82);
859
- view.setUint8(1, 73);
860
- view.setUint8(2, 70);
861
- view.setUint8(3, 70);
862
- view.setUint32(4, 36 + dataSize, true);
863
- view.setUint8(8, 87);
864
- view.setUint8(9, 65);
865
- view.setUint8(10, 86);
866
- view.setUint8(11, 69);
867
- view.setUint8(12, 102);
868
- view.setUint8(13, 109);
869
- view.setUint8(14, 116);
870
- view.setUint8(15, 32);
871
- view.setUint32(16, 16, true);
872
- view.setUint16(20, 1, true);
873
- view.setUint16(22, channels, true);
874
- view.setUint32(24, sampleRate, true);
875
- view.setUint32(28, sampleRate * blockAlign, true);
876
- view.setUint16(32, blockAlign, true);
877
- view.setUint16(34, bitsPerSample, true);
878
- view.setUint8(36, 100);
879
- view.setUint8(37, 97);
880
- view.setUint8(38, 116);
881
- view.setUint8(39, 97);
882
- view.setUint32(40, dataSize, true);
883
- const audioData = new Uint8Array(audioBuffer);
884
- const destData = new Uint8Array(buffer, 44);
885
- destData.set(audioData);
886
- return new Blob([buffer], { type: "audio/wav" });
887
- };
888
881
  let recorder2 = vue.ref(null);
889
882
  const initRecorder = () => {
890
883
  console.log("initRecorder");
@@ -936,7 +929,7 @@ const _sfc_main = {
936
929
  };
937
930
  }
938
931
  };
939
- const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-711ac421"]]);
932
+ const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-9c25016c"]]);
940
933
  DigitalHuman.install = (app) => app.component("DigitalHuman", DigitalHuman);
941
934
  const components = [DemoButton, DemoInput, DigitalHuman];
942
935
  const install = (app) => {
@@ -383,8 +383,8 @@ 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://127.0.0.1:8002";
387
- const ASR_URL = "http://127.0.0.1:9988/transcribe";
386
+ const baseTTSUrl = "http://192.168.3.182:8002";
387
+ const ASR_URL = "http://192.168.3.182:9988";
388
388
  const _sfc_main = {
389
389
  __name: "index",
390
390
  props: {
@@ -647,22 +647,150 @@ const _sfc_main = {
647
647
  currentAudio.value = null;
648
648
  }
649
649
  }
650
- function waitWithSignal(ms, signal) {
651
- return new Promise((resolve, reject) => {
652
- if (signal == null ? void 0 : signal.aborted) {
653
- reject(new DOMException("Aborted", "AbortError"));
654
- return;
655
- }
656
- const timer = setTimeout(resolve, ms);
657
- const onAbort = () => {
658
- clearTimeout(timer);
659
- reject(new DOMException("Aborted", "AbortError"));
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 };
665
+ };
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
660
672
  };
661
- signal == null ? void 0 : signal.addEventListener("abort", onAbort, { once: true });
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 } 对象");
694
+ };
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
662
701
  });
663
- }
702
+ if (!res.ok) {
703
+ const text = await res.text().catch(() => "");
704
+ throw new Error(`克隆任务提交失败 HTTP ${res.status} ${text}`);
705
+ }
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;
709
+ };
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 合成失败");
746
+ }
747
+ eventName = "message";
748
+ }
749
+ }
750
+ throw new Error("OmniVoice 合成结果流提前中断");
751
+ };
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();
756
+ const objectUrl = URL.createObjectURL(audioBlob);
757
+ if (signal.aborted || currentTtsRequestId.value !== requestId) {
758
+ URL.revokeObjectURL(objectUrl);
759
+ return;
760
+ }
761
+ const audio = new Audio(objectUrl);
762
+ currentAudio.value = audio;
763
+ const cleanup = () => {
764
+ URL.revokeObjectURL(objectUrl);
765
+ if (currentAudio.value === audio) currentAudio.value = null;
766
+ };
767
+ audio.onplaying = () => {
768
+ console.log("[OmniVoice] 音频开始播放");
769
+ speak();
770
+ };
771
+ audio.onended = () => {
772
+ console.log("[OmniVoice] 音频播放结束");
773
+ cleanup();
774
+ stopSpeaking();
775
+ };
776
+ audio.onerror = () => {
777
+ console.error("[OmniVoice] 音频播放失败");
778
+ cleanup();
779
+ stopSpeaking();
780
+ };
781
+ await audio.play();
782
+ };
664
783
  const playTTS = async (text) => {
665
- var _a, _b, _c, _d, _e, _f, _g, _h;
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
+ }
666
794
  if (!text || !((_a = currentHuman.value) == null ? void 0 : _a.isMuted)) return;
667
795
  abortTTS();
668
796
  ttsAbortController = new AbortController();
@@ -672,132 +800,33 @@ const _sfc_main = {
672
800
  try {
673
801
  const cleanText = text.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/[#*_~]/g, "").replace(/\n+/g, ",").replace(/\s+/g, " ").replace(/[,。!?]{2,}/g, ",").trim();
674
802
  if (!cleanText) return;
675
- const baseUrl = baseTTSUrl;
676
- const sessionHash = "session_" + Date.now();
677
- let speed = ((_c = (_b = currentHuman.value) == null ? void 0 : _b.speakSet) == null ? void 0 : _c.speed) || 1;
678
- let age = ((_e = (_d = currentHuman.value) == null ? void 0 : _d.speakSet) == null ? void 0 : _e.age) || "Young Adult / 青年";
679
- let sex = ((_g = (_f = currentHuman.value) == null ? void 0 : _f.speakSet) == null ? void 0 : _g.sex) || "Female / 女";
680
- const data = [cleanText, "Auto", 32, 2, true, speed, null, true, true, sex, age, "Auto", "Auto", "Auto", "Auto"];
681
- console.log("[OmniVoice] 提交请求:", { text: cleanText, length: cleanText.length, fn_index: 1 });
682
- const joinRes = await fetch(`${baseUrl}/gradio_api/queue/join`, {
683
- method: "POST",
684
- headers: { "Content-Type": "application/json" },
685
- body: JSON.stringify({ data, session_hash: sessionHash, fn_index: 1, trigger_id: 50 }),
686
- signal
687
- });
688
- const joinData = await joinRes.json();
689
- console.log("[OmniVoice] Step1 响应:", joinData);
690
- console.log("[OmniVoice] Step2: 轮询等待结果");
691
- let audioUrl = "";
692
- let maxPolls = 60;
693
- let pollInterval = 1e3;
694
- while (!audioUrl && maxPolls > 0) {
695
- if (signal.aborted || currentTtsRequestId.value !== requestId) return;
696
- await waitWithSignal(pollInterval, signal);
697
- maxPolls--;
698
- try {
699
- const pollRes = await fetch(`${baseUrl}/gradio_api/queue/data?session_hash=${sessionHash}`, {
700
- signal
701
- });
702
- const pollText = await pollRes.text();
703
- const lines = pollText.split("\n");
704
- for (const line of lines) {
705
- const trimLine = line.trim();
706
- if (!trimLine.startsWith("data: ")) continue;
707
- try {
708
- const json = JSON.parse(trimLine.replace("data: ", ""));
709
- console.log("[OmniVoice] 轮询消息:", json);
710
- if (json.msg === "process_completed" && ((_h = json.output) == null ? void 0 : _h.data)) {
711
- const fileData = json.output.data[0];
712
- console.log("[OmniVoice] 获取到音频:", fileData);
713
- if (fileData == null ? void 0 : fileData.url) audioUrl = fileData.url;
714
- else if (fileData == null ? void 0 : fileData.path) audioUrl = `${baseUrl}/gradio_api/file=${fileData.path}`;
715
- else if (typeof fileData === "string") audioUrl = fileData.startsWith("http") ? fileData : `${baseUrl}/gradio_api/file=${fileData}`;
716
- else if (Array.isArray(fileData) && fileData.length >= 2) {
717
- const samplingRate = fileData[0];
718
- const waveform = fileData[1];
719
- console.log("[OmniVoice] 直接获取到音频数组:", { samplingRate, waveformLength: waveform == null ? void 0 : waveform.length });
720
- if (samplingRate && waveform && Array.isArray(waveform)) {
721
- const audioBuffer = new ArrayBuffer(waveform.length * 2);
722
- const view = new DataView(audioBuffer);
723
- for (let i = 0; i < waveform.length; i++) {
724
- view.setInt16(i * 2, waveform[i], true);
725
- }
726
- const wavBlob = createWavBlob(audioBuffer, samplingRate, 1, 16);
727
- const audioObjectUrl = URL.createObjectURL(wavBlob);
728
- if (signal.aborted || currentTtsRequestId.value !== requestId) {
729
- URL.revokeObjectURL(audioObjectUrl);
730
- return;
731
- }
732
- currentAudio.value = new Audio(audioObjectUrl);
733
- currentAudio.value.onplaying = () => {
734
- console.log("[OmniVoice] 音频开始播放");
735
- speak();
736
- };
737
- currentAudio.value.onended = () => {
738
- console.log("[OmniVoice] 音频播放结束");
739
- stopSpeaking();
740
- currentAudio.value = null;
741
- URL.revokeObjectURL(audioObjectUrl);
742
- };
743
- currentAudio.value.onerror = () => {
744
- console.error("[OmniVoice] 音频播放失败");
745
- stopSpeaking();
746
- currentAudio.value = null;
747
- URL.revokeObjectURL(audioObjectUrl);
748
- };
749
- await currentAudio.value.play();
750
- return;
751
- }
752
- }
753
- break;
754
- } else if (json.msg === "process_error") {
755
- console.error("[OmniVoice] 处理错误:", json);
756
- standby();
757
- return;
758
- }
759
- } catch (e) {
760
- console.error("[OmniVoice] 解析轮询消息失败:", e);
761
- }
762
- }
763
- } catch (e) {
764
- if ((e == null ? void 0 : e.name) === "AbortError") throw e;
765
- console.warn("[OmniVoice] 轮询失败:", e.message);
766
- }
767
- }
768
- if (audioUrl) {
769
- if (audioUrl.startsWith("/")) audioUrl = baseUrl + audioUrl;
770
- console.log("[OmniVoice] 下载音频:", audioUrl);
771
- if (signal.aborted || currentTtsRequestId.value !== requestId) return;
772
- const audioRes = await fetch(audioUrl, { signal });
773
- const audioBlob = await audioRes.blob();
774
- const audioObjectUrl = URL.createObjectURL(audioBlob);
775
- if (signal.aborted || currentTtsRequestId.value !== requestId) {
776
- URL.revokeObjectURL(audioObjectUrl);
777
- return;
778
- }
779
- currentAudio.value = new Audio(audioObjectUrl);
780
- currentAudio.value.onplaying = () => {
781
- console.log("[OmniVoice] 音频开始播放");
782
- speak();
783
- };
784
- currentAudio.value.onended = () => {
785
- console.log("[OmniVoice] 音频播放结束");
786
- stopSpeaking();
787
- currentAudio.value = null;
788
- URL.revokeObjectURL(audioObjectUrl);
789
- };
790
- currentAudio.value.onerror = () => {
791
- console.error("[OmniVoice] 音频播放失败");
792
- stopSpeaking();
793
- currentAudio.value = null;
794
- URL.revokeObjectURL(audioObjectUrl);
795
- };
796
- await currentAudio.value.play();
797
- } else {
798
- console.error("[OmniVoice] 未获取到音频");
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,无法进行音色克隆");
799
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);
800
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 });
824
+ if (signal.aborted || currentTtsRequestId.value !== requestId) return;
825
+ if (!audioFile || !audioFile.path && !audioFile.url) {
826
+ throw new Error(statusText || "OmniVoice 未返回合成音频");
827
+ }
828
+ const audioUrl = audioFile.url && audioFile.url.startsWith("http") ? audioFile.url : `${baseTTSUrl}/gradio_api/file=${audioFile.path}`;
829
+ await playAudioFromUrl(audioUrl, requestId, signal);
801
830
  } catch (e) {
802
831
  if ((e == null ? void 0 : e.name) === "AbortError") {
803
832
  console.log("[OmniVoice] TTS已取消");
@@ -834,7 +863,7 @@ const _sfc_main = {
834
863
  const formData = new FormData();
835
864
  formData.append("file", blob, "recorded_audio.wav");
836
865
  try {
837
- const res = await fetch(ASR_URL, {
866
+ const res = await fetch(ASR_URL + "/transcribe", {
838
867
  method: "POST",
839
868
  body: formData
840
869
  });
@@ -847,42 +876,6 @@ const _sfc_main = {
847
876
  return { status: e, text: "" };
848
877
  }
849
878
  };
850
- const createWavBlob = (audioBuffer, sampleRate, channels, bitsPerSample) => {
851
- const bytesPerSample = bitsPerSample / 8;
852
- const blockAlign = channels * bytesPerSample;
853
- const dataSize = audioBuffer.byteLength;
854
- const buffer = new ArrayBuffer(44 + dataSize);
855
- const view = new DataView(buffer);
856
- view.setUint8(0, 82);
857
- view.setUint8(1, 73);
858
- view.setUint8(2, 70);
859
- view.setUint8(3, 70);
860
- view.setUint32(4, 36 + dataSize, true);
861
- view.setUint8(8, 87);
862
- view.setUint8(9, 65);
863
- view.setUint8(10, 86);
864
- view.setUint8(11, 69);
865
- view.setUint8(12, 102);
866
- view.setUint8(13, 109);
867
- view.setUint8(14, 116);
868
- view.setUint8(15, 32);
869
- view.setUint32(16, 16, true);
870
- view.setUint16(20, 1, true);
871
- view.setUint16(22, channels, true);
872
- view.setUint32(24, sampleRate, true);
873
- view.setUint32(28, sampleRate * blockAlign, true);
874
- view.setUint16(32, blockAlign, true);
875
- view.setUint16(34, bitsPerSample, true);
876
- view.setUint8(36, 100);
877
- view.setUint8(37, 97);
878
- view.setUint8(38, 116);
879
- view.setUint8(39, 97);
880
- view.setUint32(40, dataSize, true);
881
- const audioData = new Uint8Array(audioBuffer);
882
- const destData = new Uint8Array(buffer, 44);
883
- destData.set(audioData);
884
- return new Blob([buffer], { type: "audio/wav" });
885
- };
886
879
  let recorder2 = ref(null);
887
880
  const initRecorder = () => {
888
881
  console.log("initRecorder");
@@ -934,7 +927,7 @@ const _sfc_main = {
934
927
  };
935
928
  }
936
929
  };
937
- const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-711ac421"]]);
930
+ const DigitalHuman = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-9c25016c"]]);
938
931
  DigitalHuman.install = (app) => app.component("DigitalHuman", DigitalHuman);
939
932
  const components = [DemoButton, DemoInput, DigitalHuman];
940
933
  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-711ac421]{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.digital-human canvas[data-v-711ac421]{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-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%}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruiyun-human",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "type": "module",
5
5
  "description": "数字人初步实现",
6
6
  "main": "./dist/ruiyun-human.cjs.js",
@@ -29,11 +29,11 @@
29
29
  "postcss": "^8.5.20",
30
30
  "tailwindcss": "^4.3.3",
31
31
  "terser": "^5.49.0",
32
- "vite": "^5.1.4",
33
- "vue": "^3.4.21"
32
+ "vite": "^5.1.4"
34
33
  },
35
34
  "dependencies": {
36
35
  "js-audio-recorder": "^1.0.7",
37
- "vue-router": "^4.6.4"
36
+ "vue-router": "^4.6.4",
37
+ "vue": "^3.4.21"
38
38
  }
39
39
  }
package/dist/favicon.svg DELETED
@@ -1,4 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
- <rect width="100" height="100" rx="20" fill="#42b883"/>
3
- <text y=".9em" font-size="90" fill="white" text-anchor="middle" x="50">V</text>
4
- </svg>