virtual-human-cf 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -118,6 +118,7 @@ const onEnded = () => {
118
118
  | end | 数字人对话结束 | 载荷数据 |
119
119
  | pause | 暂停播放 | 载荷数据 |
120
120
  | error | 错误事件 | 错误信息 |
121
+ | play_complete | 播放完成 | - |
121
122
 
122
123
  #### 示例
123
124
 
@@ -17,6 +17,7 @@ declare const __VLS_component: import('vue').DefineComponent<import('vue').Extra
17
17
  showDialog: (...args: any[]) => void;
18
18
  end: (...args: any[]) => void;
19
19
  connected: (...args: any[]) => void;
20
+ play_complete: (...args: any[]) => void;
20
21
  }, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
21
22
  screenClientId: {
22
23
  type: StringConstructor;
@@ -33,6 +34,7 @@ declare const __VLS_component: import('vue').DefineComponent<import('vue').Extra
33
34
  onShowDialog?: ((...args: any[]) => any) | undefined;
34
35
  onEnd?: ((...args: any[]) => any) | undefined;
35
36
  onConnected?: ((...args: any[]) => any) | undefined;
37
+ onPlay_complete?: ((...args: any[]) => any) | undefined;
36
38
  }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
37
39
  declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, ReturnType<typeof __VLS_template>>;
38
40
  export default _default;
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .fade-enter-active[data-v-19d39d2e],.fade-leave-active[data-v-19d39d2e]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-19d39d2e],.fade-leave-to[data-v-19d39d2e]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-19d39d2e]{position:fixed;left:16px;bottom:16px;width:400px;height:500px;z-index:2147483647;overflow:visible}.video-wrapper[data-v-19d39d2e]{position:relative;width:400px;height:500px;border-radius:1rem;overflow:hidden;box-shadow:0 10px 25px #0000001a;background:#fff;transition:background-color .3s ease,box-shadow .3s ease}.virtual-human-container.is-dark .video-wrapper[data-v-19d39d2e]{background:#1f2937;box-shadow:0 10px 25px #00000080}.persona-video[data-v-19d39d2e]{width:100%;height:100%;object-fit:cover;transform-origin:center;transition:transform .1s ease-out}.resize-handle[data-v-19d39d2e]{position:absolute;width:20px;height:20px;background:#00000080;border:2px solid rgba(255,255,255,.8);border-radius:4px;cursor:pointer;z-index:20;opacity:0;transition:opacity .3s ease,background .2s,border-color .2s}.video-wrapper:hover .resize-handle[data-v-19d39d2e]{opacity:1}.resize-handle[data-v-19d39d2e]:hover{background:#000000b3;border-color:#fff}.resize-handle.top-left[data-v-19d39d2e]{top:10px;left:10px;cursor:nwse-resize}.resize-handle.top-right[data-v-19d39d2e]{top:10px;right:10px;cursor:nesw-resize}.resize-handle.bottom-left[data-v-19d39d2e]{bottom:10px;left:10px;cursor:nesw-resize}.resize-handle.bottom-right[data-v-19d39d2e]{bottom:10px;right:10px;cursor:nwse-resize}.overlay[data-v-19d39d2e]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-19d39d2e]{display:flex;align-items:center;gap:.5rem;padding:.25rem .75rem;border-radius:9999px;font-size:.75rem;font-weight:500;color:#fff;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.status-badge.paused[data-v-19d39d2e]{background:#ef4444cc}.status-badge.playing[data-v-19d39d2e]{background:#22c55ecc}.dot[data-v-19d39d2e]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-19d39d2e{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-19d39d2e]{animation:pulse-19d39d2e 2s cubic-bezier(.4,0,.6,1) infinite}
1
+ .fade-enter-active[data-v-5d741303],.fade-leave-active[data-v-5d741303]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-5d741303],.fade-leave-to[data-v-5d741303]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-5d741303]{position:fixed;z-index:2147483647;overflow:visible}.video-wrapper[data-v-5d741303]{position:relative;width:100%;height:100%;border-radius:1rem;overflow:hidden;box-shadow:0 10px 25px #0000001a;background:#fff;border:2px solid transparent;transition:background-color .3s ease,box-shadow .3s ease,border-color .3s ease}.video-wrapper[data-v-5d741303]:hover{border-color:#409eff}.virtual-human-container.is-dark .video-wrapper[data-v-5d741303]{background:#1f2937;box-shadow:0 10px 25px #00000080}.persona-video[data-v-5d741303]{width:100%;height:100%;object-fit:cover;transform-origin:center;transition:transform .1s ease-out}.resize-handle[data-v-5d741303]{position:absolute;width:20px;height:20px;background:#00000080;border:2px solid rgba(255,255,255,.8);border-radius:4px;cursor:pointer;z-index:20;opacity:0;pointer-events:none;transition:opacity .3s ease,background .2s,border-color .2s}.video-wrapper:hover .resize-handle[data-v-5d741303]{opacity:1;pointer-events:auto}.resize-handle[data-v-5d741303]:hover{background:#000000b3;border-color:#fff}.resize-handle.top-left[data-v-5d741303]{top:10px;left:10px;cursor:nwse-resize}.resize-handle.top-right[data-v-5d741303]{top:10px;right:10px;cursor:nesw-resize}.resize-handle.bottom-left[data-v-5d741303]{bottom:10px;left:10px;cursor:nesw-resize}.resize-handle.bottom-right[data-v-5d741303]{bottom:10px;right:10px;cursor:nwse-resize}.overlay[data-v-5d741303]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-5d741303]{display:flex;align-items:center;gap:.5rem;padding:.25rem .75rem;border-radius:9999px;font-size:.75rem;font-weight:500;color:#fff;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.status-badge.paused[data-v-5d741303]{background:#ef4444cc}.status-badge.playing[data-v-5d741303]{background:#22c55ecc}.dot[data-v-5d741303]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-5d741303{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-5d741303]{animation:pulse-5d741303 2s cubic-bezier(.4,0,.6,1) infinite}
@@ -1,11 +1,11 @@
1
- import { defineComponent as T, ref as v, watch as z, onMounted as W, onUnmounted as X, openBlock as h, createBlock as D, Transition as F, withCtx as q, createElementBlock as b, normalizeClass as N, createElementVNode as H, normalizeStyle as O, createCommentVNode as I, createTextVNode as L, renderSlot as J } from "vue";
2
- const j = ["src", "muted"], G = { class: "overlay" }, K = {
1
+ import { defineComponent as j, ref as d, watch as X, onMounted as G, onUnmounted as K, openBlock as P, createBlock as Q, Transition as Z, withCtx as _, createElementBlock as V, normalizeStyle as ee, normalizeClass as te, createElementVNode as R, createCommentVNode as T, createTextVNode as J, renderSlot as ne } from "vue";
2
+ const ae = ["src", "muted"], se = { class: "overlay" }, le = {
3
3
  key: 0,
4
4
  class: "status-badge paused"
5
- }, Q = {
5
+ }, oe = {
6
6
  key: 1,
7
7
  class: "status-badge playing"
8
- }, Z = /* @__PURE__ */ T({
8
+ }, re = /* @__PURE__ */ j({
9
9
  __name: "VirtualHumanPersona",
10
10
  props: {
11
11
  // 视频源URL
@@ -46,131 +46,136 @@ const j = ["src", "muted"], G = { class: "overlay" }, K = {
46
46
  }
47
47
  },
48
48
  emits: ["update:isPlaying", "ended", "update:visible"],
49
- setup(i, { emit: P }) {
50
- const t = i, s = P, l = v(null), k = v(null), n = v(null), m = v(!1), V = v(1), E = v(!1), p = v(null), C = v(0), x = v(0), o = v(1), u = () => {
49
+ setup(c, { emit: x }) {
50
+ const t = c, l = x, u = d(null), Y = d(null), n = d(null), y = d(!1), h = d(400), w = d(500), W = d(16), E = d(16), z = d(!1), b = d(null), A = d(0), D = d(0), s = d(0), o = d(0), r = d(0), v = d(0), g = () => {
51
51
  if (n.value && n.value.close(), !(!t.wsUrl || !t.screenClientId))
52
52
  try {
53
- const r = new URL(t.wsUrl);
54
- r.searchParams.append("sessionId", t.screenClientId + "-persona"), n.value = new WebSocket(r.toString()), n.value.onopen = () => {
55
- m.value = !0, console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`);
53
+ const i = new URL(t.wsUrl);
54
+ i.searchParams.append("sessionId", t.screenClientId + "-persona"), n.value = new WebSocket(i.toString()), n.value.onopen = () => {
55
+ y.value = !0, console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`);
56
56
  }, n.value.onmessage = (e) => {
57
57
  try {
58
58
  const a = JSON.parse(e.data);
59
- c(a);
59
+ B(a);
60
60
  } catch (a) {
61
61
  console.error("[VirtualHumanPersona] Failed to parse message:", e.data, a);
62
62
  }
63
63
  }, n.value.onerror = (e) => {
64
64
  console.error("[VirtualHumanPersona] WebSocket error:", e);
65
65
  }, n.value.onclose = () => {
66
- m.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
66
+ y.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
67
67
  };
68
- } catch (r) {
69
- console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", r);
68
+ } catch (i) {
69
+ console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", i);
70
70
  }
71
- }, c = (r) => {
72
- const { type: e, action: a } = r;
73
- e === "control" && (a === "play" || a === "resume" ? (s("update:isPlaying", !0), s("update:visible", !0)) : a === "pause" ? s("update:isPlaying", !1) : a === "stop" && (s("update:isPlaying", !1), s("update:visible", !1), l.value && (l.value.currentTime = 0)));
71
+ }, B = (i) => {
72
+ const { type: e, action: a } = i;
73
+ e === "control" && (a === "play" || a === "resume" ? (l("update:isPlaying", !0), l("update:visible", !0)) : a === "pause" ? l("update:isPlaying", !1) : a === "stop" && (l("update:isPlaying", !1), l("update:visible", !1), u.value && (u.value.currentTime = 0)));
74
74
  };
75
- z(() => t.screenClientId, () => {
76
- t.screenClientId && t.wsUrl && u();
77
- }), z(() => t.wsUrl, () => {
78
- t.screenClientId && t.wsUrl && u();
79
- }), z(() => t.isPlaying, (r) => {
80
- l.value && (r ? l.value.play().catch((e) => console.error("Video play failed:", e)) : l.value.pause());
75
+ X(() => t.screenClientId, () => {
76
+ t.screenClientId && t.wsUrl && g();
77
+ }), X(() => t.wsUrl, () => {
78
+ t.screenClientId && t.wsUrl && g();
79
+ }), X(() => t.isPlaying, (i) => {
80
+ u.value && (i ? u.value.play().catch((e) => console.error("Video play failed:", e)) : u.value.pause());
81
81
  });
82
- const w = () => {
83
- t.isPlaying || s("update:isPlaying", !0);
84
- }, g = () => {
85
- t.isPlaying && s("update:isPlaying", !1);
86
- }, A = () => {
87
- s("update:isPlaying", !1), s("ended");
88
- }, f = (r, e) => {
89
- E.value = !0, p.value = r, o.value = V.value;
90
- const a = "clientX" in e ? e.clientX : e.touches[0].clientX, S = "clientY" in e ? e.clientY : e.touches[0].clientY;
91
- C.value = a, x.value = S, document.addEventListener("mousemove", y), document.addEventListener("mouseup", d), document.addEventListener("touchmove", y), document.addEventListener("touchend", d);
92
- }, y = (r) => {
93
- if (!E.value || !k.value) return;
94
- const e = "clientX" in r ? r.clientX : r.touches[0].clientX, a = "clientY" in r ? r.clientY : r.touches[0].clientY, S = e - C.value, M = a - x.value, B = k.value.getBoundingClientRect(), Y = 0.5, $ = 2;
95
- let U = o.value;
96
- const R = Math.min(B.width, B.height);
97
- p.value === "bottom-right" ? U = o.value + (S + M) / R : p.value === "bottom-left" ? U = o.value + (-S + M) / R : p.value === "top-right" ? U = o.value + (S - M) / R : p.value === "top-left" && (U = o.value + (-S - M) / R), V.value = Math.max(Y, Math.min($, U));
98
- }, d = () => {
99
- E.value = !1, p.value = null, document.removeEventListener("mousemove", y), document.removeEventListener("mouseup", d), document.removeEventListener("touchmove", y), document.removeEventListener("touchend", d);
82
+ const L = () => {
83
+ t.isPlaying || l("update:isPlaying", !0);
84
+ }, U = () => {
85
+ t.isPlaying && l("update:isPlaying", !1);
86
+ }, f = () => {
87
+ l("update:isPlaying", !1), l("ended");
88
+ }, k = (i, e) => {
89
+ e.preventDefault(), z.value = !0, b.value = i;
90
+ const a = "clientX" in e ? e.clientX : e.touches[0].clientX, C = "clientY" in e ? e.clientY : e.touches[0].clientY;
91
+ A.value = a, D.value = C, s.value = h.value, o.value = w.value, r.value = W.value, v.value = E.value, document.addEventListener("mousemove", M), document.addEventListener("mouseup", $), document.addEventListener("touchmove", M, { passive: !1 }), document.addEventListener("touchend", $);
92
+ }, M = (i) => {
93
+ if (!z.value) return;
94
+ i.preventDefault();
95
+ const e = "clientX" in i ? i.clientX : i.touches[0].clientX, a = "clientY" in i ? i.clientY : i.touches[0].clientY, C = e - A.value, I = a - D.value, F = 200, q = 250, N = 800, O = 1e3;
96
+ let m = s.value, p = o.value, S = r.value, H = v.value;
97
+ b.value === "bottom-right" ? (m = s.value + C, H = v.value - I, p = o.value + I) : b.value === "bottom-left" ? (S = r.value + C, m = s.value - C, H = v.value - I, p = o.value + I) : b.value === "top-right" ? (m = s.value + C, p = o.value - I) : b.value === "top-left" && (S = r.value + C, m = s.value - C, p = o.value - I), m < F && (S !== r.value && (S -= F - m), m = F), p < q && (H !== v.value && (H -= q - p), p = q), m > N && (S !== r.value && (S += m - N), m = N), p > O && (H !== v.value && (H += p - O), p = O), h.value = m, w.value = p, W.value = S, E.value = H;
98
+ }, $ = () => {
99
+ z.value = !1, b.value = null, document.removeEventListener("mousemove", M), document.removeEventListener("mouseup", $), document.removeEventListener("touchmove", M), document.removeEventListener("touchend", $);
100
100
  };
101
- return W(() => {
102
- t.isPlaying && l.value && l.value.play().catch((r) => console.error("Video play failed:", r)), t.screenClientId && t.wsUrl && u();
103
- }), X(() => {
101
+ return G(() => {
102
+ t.isPlaying && u.value && u.value.play().catch((i) => console.error("Video play failed:", i)), t.screenClientId && t.wsUrl && g();
103
+ }), K(() => {
104
104
  n.value && n.value.close();
105
- }), (r, e) => (h(), D(F, { name: "fade" }, {
106
- default: q(() => [
107
- i.visible ? (h(), b("div", {
105
+ }), (i, e) => (P(), Q(Z, { name: "fade" }, {
106
+ default: _(() => [
107
+ c.visible ? (P(), V("div", {
108
108
  key: 0,
109
- class: N(["virtual-human-container", { "is-dark": i.isDark }])
109
+ class: te(["virtual-human-container", { "is-dark": c.isDark }]),
110
+ style: ee({
111
+ width: h.value + "px",
112
+ height: w.value + "px",
113
+ left: W.value + "px",
114
+ bottom: E.value + "px"
115
+ })
110
116
  }, [
111
- H("div", {
117
+ R("div", {
112
118
  class: "video-wrapper",
113
119
  ref_key: "wrapperRef",
114
- ref: k,
115
- style: O({ transform: `scale(${V.value})`, transformOrigin: "left bottom" })
120
+ ref: Y
116
121
  }, [
117
- H("video", {
122
+ R("video", {
118
123
  ref_key: "videoRef",
119
- ref: l,
120
- src: i.videoSrc,
124
+ ref: u,
125
+ src: c.videoSrc,
121
126
  class: "persona-video",
122
- muted: i.muted,
127
+ muted: c.muted,
123
128
  playsinline: "",
124
129
  loop: "",
125
- onPlay: w,
126
- onPause: g,
127
- onEnded: A
128
- }, null, 40, j),
129
- i.visible ? (h(), b("div", {
130
+ onPlay: L,
131
+ onPause: U,
132
+ onEnded: f
133
+ }, null, 40, ae),
134
+ c.visible ? (P(), V("div", {
130
135
  key: 0,
131
136
  class: "resize-handle top-left",
132
- onMousedown: e[0] || (e[0] = (a) => f("top-left", a)),
133
- onTouchstart: e[1] || (e[1] = (a) => f("top-left", a))
134
- }, null, 32)) : I("", !0),
135
- i.visible ? (h(), b("div", {
137
+ onMousedown: e[0] || (e[0] = (a) => k("top-left", a)),
138
+ onTouchstart: e[1] || (e[1] = (a) => k("top-left", a))
139
+ }, null, 32)) : T("", !0),
140
+ c.visible ? (P(), V("div", {
136
141
  key: 1,
137
142
  class: "resize-handle top-right",
138
- onMousedown: e[2] || (e[2] = (a) => f("top-right", a)),
139
- onTouchstart: e[3] || (e[3] = (a) => f("top-right", a))
140
- }, null, 32)) : I("", !0),
141
- i.visible ? (h(), b("div", {
143
+ onMousedown: e[2] || (e[2] = (a) => k("top-right", a)),
144
+ onTouchstart: e[3] || (e[3] = (a) => k("top-right", a))
145
+ }, null, 32)) : T("", !0),
146
+ c.visible ? (P(), V("div", {
142
147
  key: 2,
143
148
  class: "resize-handle bottom-left",
144
- onMousedown: e[4] || (e[4] = (a) => f("bottom-left", a)),
145
- onTouchstart: e[5] || (e[5] = (a) => f("bottom-left", a))
146
- }, null, 32)) : I("", !0),
147
- i.visible ? (h(), b("div", {
149
+ onMousedown: e[4] || (e[4] = (a) => k("bottom-left", a)),
150
+ onTouchstart: e[5] || (e[5] = (a) => k("bottom-left", a))
151
+ }, null, 32)) : T("", !0),
152
+ c.visible ? (P(), V("div", {
148
153
  key: 3,
149
154
  class: "resize-handle bottom-right",
150
- onMousedown: e[6] || (e[6] = (a) => f("bottom-right", a)),
151
- onTouchstart: e[7] || (e[7] = (a) => f("bottom-right", a))
152
- }, null, 32)) : I("", !0),
153
- H("div", G, [
154
- i.isPlaying ? (h(), b("div", Q, [...e[9] || (e[9] = [
155
- H("span", { class: "dot animate-pulse" }, null, -1),
156
- L(" 播放中 ", -1)
157
- ])])) : (h(), b("div", K, [...e[8] || (e[8] = [
158
- H("span", { class: "dot" }, null, -1),
159
- L(" 暂停中 ", -1)
155
+ onMousedown: e[6] || (e[6] = (a) => k("bottom-right", a)),
156
+ onTouchstart: e[7] || (e[7] = (a) => k("bottom-right", a))
157
+ }, null, 32)) : T("", !0),
158
+ R("div", se, [
159
+ c.isPlaying ? (P(), V("div", oe, [...e[9] || (e[9] = [
160
+ R("span", { class: "dot animate-pulse" }, null, -1),
161
+ J(" 播放中 ", -1)
162
+ ])])) : (P(), V("div", le, [...e[8] || (e[8] = [
163
+ R("span", { class: "dot" }, null, -1),
164
+ J(" 暂停中 ", -1)
160
165
  ])]))
161
166
  ])
162
- ], 4)
163
- ], 2)) : I("", !0)
167
+ ], 512)
168
+ ], 6)) : T("", !0)
164
169
  ]),
165
170
  _: 1
166
171
  }));
167
172
  }
168
- }), _ = (i, P) => {
169
- const t = i.__vccOpts || i;
170
- for (const [s, l] of P)
171
- t[s] = l;
173
+ }), ie = (c, x) => {
174
+ const t = c.__vccOpts || c;
175
+ for (const [l, u] of x)
176
+ t[l] = u;
172
177
  return t;
173
- }, ee = /* @__PURE__ */ _(Z, [["__scopeId", "data-v-19d39d2e"]]), te = /* @__PURE__ */ T({
178
+ }, ue = /* @__PURE__ */ ie(re, [["__scopeId", "data-v-5d741303"]]), ce = /* @__PURE__ */ j({
174
179
  __name: "VirtualHumanEventAdapter",
175
180
  props: {
176
181
  // 屏幕客户端ID
@@ -184,113 +189,122 @@ const j = ["src", "muted"], G = { class: "overlay" }, K = {
184
189
  required: !0
185
190
  }
186
191
  },
187
- emits: ["highlight", "showDialog", "end", "pause", "connected", "error"],
188
- setup(i, { emit: P }) {
189
- const t = i, s = P, l = v(null), k = v(!1);
190
- let n = null, m = 0;
191
- const V = () => {
192
+ emits: ["highlight", "showDialog", "end", "pause", "connected", "error", "play_complete"],
193
+ setup(c, { emit: x }) {
194
+ const t = c, l = x, u = d(null), Y = d(!1);
195
+ let n = null, y = 0, h = !1, w = 0;
196
+ const W = () => {
192
197
  n || (n = new (window.AudioContext || window.webkitAudioContext)({
193
198
  sampleRate: 24e3
194
199
  })), n.state === "suspended" && n.resume();
195
- }, E = (o) => {
196
- if (V(), !!n)
200
+ }, E = () => {
201
+ h && w === 0 && (l("play_complete", t.screenClientId), h = !1);
202
+ }, z = (s) => {
203
+ if (W(), !!n)
197
204
  try {
198
- const u = window.atob(o), c = u.length, w = new Uint8Array(c);
199
- for (let d = 0; d < c; d++)
200
- w[d] = u.charCodeAt(d);
201
- const g = new Int16Array(w.buffer), A = new Float32Array(g.length);
202
- for (let d = 0; d < g.length; d++)
203
- A[d] = g[d] / 32768;
204
- const f = n.createBuffer(1, A.length, 24e3);
205
- f.getChannelData(0).set(A);
206
- const y = n.createBufferSource();
207
- y.buffer = f, y.connect(n.destination), m < n.currentTime && (m = n.currentTime), y.start(m), m += f.duration;
208
- } catch (u) {
209
- console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:", u);
205
+ const o = window.atob(s), r = o.length, v = new Uint8Array(r);
206
+ for (let f = 0; f < r; f++)
207
+ v[f] = o.charCodeAt(f);
208
+ const g = new Int16Array(v.buffer), B = new Float32Array(g.length);
209
+ for (let f = 0; f < g.length; f++)
210
+ B[f] = g[f] / 32768;
211
+ const L = n.createBuffer(1, B.length, 24e3);
212
+ L.getChannelData(0).set(B);
213
+ const U = n.createBufferSource();
214
+ U.buffer = L, U.connect(n.destination), y < n.currentTime && (y = n.currentTime), U.start(y), y += L.duration, w++, U.onended = () => {
215
+ w--, E();
216
+ };
217
+ } catch (o) {
218
+ console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:", o);
210
219
  }
211
- }, p = (o) => {
212
- if (n)
213
- switch (o) {
220
+ }, b = (s) => {
221
+ if (!(!n && s !== "stop"))
222
+ switch (s) {
214
223
  case "play":
224
+ h = !1, w = 0, y = 0, n && n.state === "suspended" && n.resume();
225
+ break;
215
226
  case "resume":
216
- n.state === "suspended" && n.resume();
227
+ n && n.state === "suspended" && n.resume();
217
228
  break;
218
229
  case "pause":
219
- n.state === "running" && n.suspend();
230
+ n && n.state === "running" && n.suspend();
220
231
  break;
221
232
  case "stop":
222
- n.close(), n = null, m = 0;
233
+ n && (n.close(), n = null), y = 0, h = !1, w = 0;
234
+ break;
235
+ case "tts_complete":
236
+ h = !0, E();
223
237
  break;
224
238
  default:
225
- console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${o}`);
239
+ console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${s}`);
226
240
  }
227
- }, C = () => {
228
- l.value && l.value.close();
241
+ }, A = () => {
242
+ u.value && u.value.close();
229
243
  try {
230
- const o = new URL(t.wsUrl);
231
- o.searchParams.append("sessionId", t.screenClientId + "-event"), l.value = new WebSocket(o.toString()), l.value.onopen = () => {
232
- k.value = !0, s("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`);
233
- }, l.value.onmessage = (u) => {
244
+ const s = new URL(t.wsUrl);
245
+ s.searchParams.append("sessionId", t.screenClientId + "-event"), u.value = new WebSocket(s.toString()), u.value.onopen = () => {
246
+ Y.value = !0, l("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`);
247
+ }, u.value.onmessage = (o) => {
234
248
  try {
235
- const c = JSON.parse(u.data);
236
- x(c);
237
- } catch (c) {
238
- console.error("[VirtualHumanEventAdapter] Failed to parse message:", u.data, c);
249
+ const r = JSON.parse(o.data);
250
+ D(r);
251
+ } catch (r) {
252
+ console.error("[VirtualHumanEventAdapter] Failed to parse message:", o.data, r);
239
253
  }
240
- }, l.value.onerror = (u) => {
241
- console.error("[VirtualHumanEventAdapter] WebSocket error:", u), s("error", u);
242
- }, l.value.onclose = () => {
243
- k.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
254
+ }, u.value.onerror = (o) => {
255
+ console.error("[VirtualHumanEventAdapter] WebSocket error:", o), l("error", o);
256
+ }, u.value.onclose = () => {
257
+ Y.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
244
258
  };
245
- } catch (o) {
246
- console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", o), s("error", o);
259
+ } catch (s) {
260
+ console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", s), l("error", s);
247
261
  }
248
- }, x = (o) => {
249
- const { type: u, payload: c, action: w } = o;
250
- switch (console.log("msgmsg", o), u) {
262
+ }, D = (s) => {
263
+ const { type: o, payload: r, action: v } = s;
264
+ switch (console.log("msgmsg", s), o) {
251
265
  case "audio":
252
- const g = (c == null ? void 0 : c.data) || o.data;
253
- g && E(g);
266
+ const g = (r == null ? void 0 : r.data) || s.data;
267
+ g && z(g);
254
268
  break;
255
269
  case "dialog_event":
256
- o.event && s(o.event, o.params);
270
+ s.event && l(s.event, s.params);
257
271
  break;
258
272
  case "control":
259
- w && p(w);
273
+ v && b(v);
260
274
  break;
261
275
  case "highlight":
262
- s("highlight", c);
276
+ l("highlight", r);
263
277
  break;
264
278
  case "showDialog":
265
- s("showDialog", c);
279
+ l("showDialog", r);
266
280
  break;
267
281
  case "end":
268
- s("end", c);
282
+ l("end", r);
269
283
  break;
270
284
  case "pause":
271
- s("pause", c);
285
+ l("pause", r);
272
286
  break;
273
287
  default:
274
- console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${u}`);
288
+ console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${o}`);
275
289
  }
276
290
  };
277
- return z(() => t.screenClientId, () => {
278
- t.screenClientId && t.wsUrl && C();
279
- }), z(() => t.wsUrl, () => {
280
- t.screenClientId && t.wsUrl && C();
281
- }), W(() => {
282
- t.screenClientId && t.wsUrl && C();
283
- }), X(() => {
284
- l.value && l.value.close(), n && n.close();
285
- }), (o, u) => J(o.$slots, "default");
291
+ return X(() => t.screenClientId, () => {
292
+ t.screenClientId && t.wsUrl && A();
293
+ }), X(() => t.wsUrl, () => {
294
+ t.screenClientId && t.wsUrl && A();
295
+ }), G(() => {
296
+ t.screenClientId && t.wsUrl && A();
297
+ }), K(() => {
298
+ u.value && u.value.close(), n && n.close();
299
+ }), (s, o) => ne(s.$slots, "default");
286
300
  }
287
- }), ne = (i) => {
288
- i.component("VirtualHumanPersona", ee), i.component("VirtualHumanEventAdapter", te);
289
- }, oe = {
290
- install: ne
301
+ }), de = (c) => {
302
+ c.component("VirtualHumanPersona", ue), c.component("VirtualHumanEventAdapter", ce);
303
+ }, fe = {
304
+ install: de
291
305
  };
292
306
  export {
293
- te as VirtualHumanEventAdapter,
294
- ee as VirtualHumanPersona,
295
- oe as default
307
+ ce as VirtualHumanEventAdapter,
308
+ ue as VirtualHumanPersona,
309
+ fe as default
296
310
  };
@@ -1 +1 @@
1
- (function(p,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(p=typeof globalThis<"u"?globalThis:p||self,e(p.VirtualHumanCf={},p.Vue))})(this,function(p,e){"use strict";const z=["src","muted"],N={class:"overlay"},T={key:0,class:"status-badge paused"},R={key:1,class:"status-badge playing"},I=((c,V)=>{const n=c.__vccOpts||c;for(const[s,r]of V)n[s]=r;return n})(e.defineComponent({__name:"VirtualHumanPersona",props:{videoSrc:{type:String,required:!0},visible:{type:Boolean,default:!1},isPlaying:{type:Boolean,default:!1},muted:{type:Boolean,default:!0},isDark:{type:Boolean,default:!1},screenClientId:{type:String,required:!1},wsUrl:{type:String,required:!1}},emits:["update:isPlaying","ended","update:visible"],setup(c,{emit:V}){const n=c,s=V,r=e.ref(null),v=e.ref(null),o=e.ref(null),g=e.ref(!1),E=e.ref(1),S=e.ref(!1),h=e.ref(null),b=e.ref(0),H=e.ref(0),l=e.ref(1),d=()=>{if(o.value&&o.value.close(),!(!n.wsUrl||!n.screenClientId))try{const i=new URL(n.wsUrl);i.searchParams.append("sessionId",n.screenClientId+"-persona"),o.value=new WebSocket(i.toString()),o.value.onopen=()=>{g.value=!0,console.log(`[VirtualHumanPersona] Connected to ${n.wsUrl} for session ${n.screenClientId}-persona`)},o.value.onmessage=t=>{try{const a=JSON.parse(t.data);u(a)}catch(a){console.error("[VirtualHumanPersona] Failed to parse message:",t.data,a)}},o.value.onerror=t=>{console.error("[VirtualHumanPersona] WebSocket error:",t)},o.value.onclose=()=>{g.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(i){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",i)}},u=i=>{const{type:t,action:a}=i;t==="control"&&(a==="play"||a==="resume"?(s("update:isPlaying",!0),s("update:visible",!0)):a==="pause"?s("update:isPlaying",!1):a==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),r.value&&(r.value.currentTime=0)))};e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&d()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&d()}),e.watch(()=>n.isPlaying,i=>{r.value&&(i?r.value.play().catch(t=>console.error("Video play failed:",t)):r.value.pause())});const k=()=>{n.isPlaying||s("update:isPlaying",!0)},y=()=>{n.isPlaying&&s("update:isPlaying",!1)},P=()=>{s("update:isPlaying",!1),s("ended")},m=(i,t)=>{S.value=!0,h.value=i,l.value=E.value;const a="clientX"in t?t.clientX:t.touches[0].clientX,C="clientY"in t?t.clientY:t.touches[0].clientY;b.value=a,H.value=C,document.addEventListener("mousemove",w),document.addEventListener("mouseup",f),document.addEventListener("touchmove",w),document.addEventListener("touchend",f)},w=i=>{if(!S.value||!v.value)return;const t="clientX"in i?i.clientX:i.touches[0].clientX,a="clientY"in i?i.clientY:i.touches[0].clientY,C=t-b.value,U=a-H.value,x=v.value.getBoundingClientRect(),W=.5,X=2;let B=l.value;const A=Math.min(x.width,x.height);h.value==="bottom-right"?B=l.value+(C+U)/A:h.value==="bottom-left"?B=l.value+(-C+U)/A:h.value==="top-right"?B=l.value+(C-U)/A:h.value==="top-left"&&(B=l.value+(-C-U)/A),E.value=Math.max(W,Math.min(X,B))},f=()=>{S.value=!1,h.value=null,document.removeEventListener("mousemove",w),document.removeEventListener("mouseup",f),document.removeEventListener("touchmove",w),document.removeEventListener("touchend",f)};return e.onMounted(()=>{n.isPlaying&&r.value&&r.value.play().catch(i=>console.error("Video play failed:",i)),n.screenClientId&&n.wsUrl&&d()}),e.onUnmounted(()=>{o.value&&o.value.close()}),(i,t)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[c.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":c.isDark}])},[e.createElementVNode("div",{class:"video-wrapper",ref_key:"wrapperRef",ref:v,style:e.normalizeStyle({transform:`scale(${E.value})`,transformOrigin:"left bottom"})},[e.createElementVNode("video",{ref_key:"videoRef",ref:r,src:c.videoSrc,class:"persona-video",muted:c.muted,playsinline:"",loop:"",onPlay:k,onPause:y,onEnded:P},null,40,z),c.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:"resize-handle top-left",onMousedown:t[0]||(t[0]=a=>m("top-left",a)),onTouchstart:t[1]||(t[1]=a=>m("top-left",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:1,class:"resize-handle top-right",onMousedown:t[2]||(t[2]=a=>m("top-right",a)),onTouchstart:t[3]||(t[3]=a=>m("top-right",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:2,class:"resize-handle bottom-left",onMousedown:t[4]||(t[4]=a=>m("bottom-left",a)),onTouchstart:t[5]||(t[5]=a=>m("bottom-left",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:3,class:"resize-handle bottom-right",onMousedown:t[6]||(t[6]=a=>m("bottom-right",a)),onTouchstart:t[7]||(t[7]=a=>m("bottom-right",a))},null,32)):e.createCommentVNode("",!0),e.createElementVNode("div",N,[c.isPlaying?(e.openBlock(),e.createElementBlock("div",R,[...t[9]||(t[9]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",T,[...t[8]||(t[8]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])],4)],2)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-19d39d2e"]]),M=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["highlight","showDialog","end","pause","connected","error"],setup(c,{emit:V}){const n=c,s=V,r=e.ref(null),v=e.ref(!1);let o=null,g=0;const E=()=>{o||(o=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),o.state==="suspended"&&o.resume()},S=l=>{if(E(),!!o)try{const d=window.atob(l),u=d.length,k=new Uint8Array(u);for(let f=0;f<u;f++)k[f]=d.charCodeAt(f);const y=new Int16Array(k.buffer),P=new Float32Array(y.length);for(let f=0;f<y.length;f++)P[f]=y[f]/32768;const m=o.createBuffer(1,P.length,24e3);m.getChannelData(0).set(P);const w=o.createBufferSource();w.buffer=m,w.connect(o.destination),g<o.currentTime&&(g=o.currentTime),w.start(g),g+=m.duration}catch(d){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",d)}},h=l=>{if(o)switch(l){case"play":case"resume":o.state==="suspended"&&o.resume();break;case"pause":o.state==="running"&&o.suspend();break;case"stop":o.close(),o=null,g=0;break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${l}`)}},b=()=>{r.value&&r.value.close();try{const l=new URL(n.wsUrl);l.searchParams.append("sessionId",n.screenClientId+"-event"),r.value=new WebSocket(l.toString()),r.value.onopen=()=>{v.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${n.wsUrl} for session ${n.screenClientId}-event`)},r.value.onmessage=d=>{try{const u=JSON.parse(d.data);H(u)}catch(u){console.error("[VirtualHumanEventAdapter] Failed to parse message:",d.data,u)}},r.value.onerror=d=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",d),s("error",d)},r.value.onclose=()=>{v.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(l){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",l),s("error",l)}},H=l=>{const{type:d,payload:u,action:k}=l;switch(console.log("msgmsg",l),d){case"audio":const y=(u==null?void 0:u.data)||l.data;y&&S(y);break;case"dialog_event":l.event&&s(l.event,l.params);break;case"control":k&&h(k);break;case"highlight":s("highlight",u);break;case"showDialog":s("showDialog",u);break;case"end":s("end",u);break;case"pause":s("pause",u);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${d}`)}};return e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&b()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&b()}),e.onMounted(()=>{n.screenClientId&&n.wsUrl&&b()}),e.onUnmounted(()=>{r.value&&r.value.close(),o&&o.close()}),(l,d)=>e.renderSlot(l.$slots,"default")}}),L={install:c=>{c.component("VirtualHumanPersona",I),c.component("VirtualHumanEventAdapter",M)}};p.VirtualHumanEventAdapter=M,p.VirtualHumanPersona=I,p.default=L,Object.defineProperties(p,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ (function(h,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(h=typeof globalThis<"u"?globalThis:h||self,e(h.VirtualHumanCf={},h.Vue))})(this,function(h,e){"use strict";const F=["src","muted"],O={class:"overlay"},j={key:0,class:"status-badge paused"},J={key:1,class:"status-badge playing"},$=((d,A)=>{const n=d.__vccOpts||d;for(const[s,u]of A)n[s]=u;return n})(e.defineComponent({__name:"VirtualHumanPersona",props:{videoSrc:{type:String,required:!0},visible:{type:Boolean,default:!1},isPlaying:{type:Boolean,default:!1},muted:{type:Boolean,default:!0},isDark:{type:Boolean,default:!1},screenClientId:{type:String,required:!1},wsUrl:{type:String,required:!1}},emits:["update:isPlaying","ended","update:visible"],setup(d,{emit:A}){const n=d,s=A,u=e.ref(null),N=e.ref(null),a=e.ref(null),y=e.ref(!1),w=e.ref(400),k=e.ref(500),I=e.ref(16),S=e.ref(16),x=e.ref(!1),b=e.ref(null),H=e.ref(0),z=e.ref(0),l=e.ref(0),r=e.ref(0),i=e.ref(0),f=e.ref(0),g=()=>{if(a.value&&a.value.close(),!(!n.wsUrl||!n.screenClientId))try{const c=new URL(n.wsUrl);c.searchParams.append("sessionId",n.screenClientId+"-persona"),a.value=new WebSocket(c.toString()),a.value.onopen=()=>{y.value=!0,console.log(`[VirtualHumanPersona] Connected to ${n.wsUrl} for session ${n.screenClientId}-persona`)},a.value.onmessage=t=>{try{const o=JSON.parse(t.data);T(o)}catch(o){console.error("[VirtualHumanPersona] Failed to parse message:",t.data,o)}},a.value.onerror=t=>{console.error("[VirtualHumanPersona] WebSocket error:",t)},a.value.onclose=()=>{y.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(c){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",c)}},T=c=>{const{type:t,action:o}=c;t==="control"&&(o==="play"||o==="resume"?(s("update:isPlaying",!0),s("update:visible",!0)):o==="pause"?s("update:isPlaying",!1):o==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),u.value&&(u.value.currentTime=0)))};e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&g()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&g()}),e.watch(()=>n.isPlaying,c=>{u.value&&(c?u.value.play().catch(t=>console.error("Video play failed:",t)):u.value.pause())});const W=()=>{n.isPlaying||s("update:isPlaying",!0)},B=()=>{n.isPlaying&&s("update:isPlaying",!1)},m=()=>{s("update:isPlaying",!1),s("ended")},C=(c,t)=>{t.preventDefault(),x.value=!0,b.value=c;const o="clientX"in t?t.clientX:t.touches[0].clientX,V="clientY"in t?t.clientY:t.touches[0].clientY;H.value=o,z.value=V,l.value=w.value,r.value=k.value,i.value=I.value,f.value=S.value,document.addEventListener("mousemove",L),document.addEventListener("mouseup",M),document.addEventListener("touchmove",L,{passive:!1}),document.addEventListener("touchend",M)},L=c=>{if(!x.value)return;c.preventDefault();const t="clientX"in c?c.clientX:c.touches[0].clientX,o="clientY"in c?c.clientY:c.touches[0].clientY,V=t-H.value,U=o-z.value,R=200,X=250,Y=800,D=1e3;let p=l.value,v=r.value,E=i.value,P=f.value;b.value==="bottom-right"?(p=l.value+V,P=f.value-U,v=r.value+U):b.value==="bottom-left"?(E=i.value+V,p=l.value-V,P=f.value-U,v=r.value+U):b.value==="top-right"?(p=l.value+V,v=r.value-U):b.value==="top-left"&&(E=i.value+V,p=l.value-V,v=r.value-U),p<R&&(E!==i.value&&(E-=R-p),p=R),v<X&&(P!==f.value&&(P-=X-v),v=X),p>Y&&(E!==i.value&&(E+=p-Y),p=Y),v>D&&(P!==f.value&&(P+=v-D),v=D),w.value=p,k.value=v,I.value=E,S.value=P},M=()=>{x.value=!1,b.value=null,document.removeEventListener("mousemove",L),document.removeEventListener("mouseup",M),document.removeEventListener("touchmove",L),document.removeEventListener("touchend",M)};return e.onMounted(()=>{n.isPlaying&&u.value&&u.value.play().catch(c=>console.error("Video play failed:",c)),n.screenClientId&&n.wsUrl&&g()}),e.onUnmounted(()=>{a.value&&a.value.close()}),(c,t)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[d.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":d.isDark}]),style:e.normalizeStyle({width:w.value+"px",height:k.value+"px",left:I.value+"px",bottom:S.value+"px"})},[e.createElementVNode("div",{class:"video-wrapper",ref_key:"wrapperRef",ref:N},[e.createElementVNode("video",{ref_key:"videoRef",ref:u,src:d.videoSrc,class:"persona-video",muted:d.muted,playsinline:"",loop:"",onPlay:W,onPause:B,onEnded:m},null,40,F),d.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:"resize-handle top-left",onMousedown:t[0]||(t[0]=o=>C("top-left",o)),onTouchstart:t[1]||(t[1]=o=>C("top-left",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:1,class:"resize-handle top-right",onMousedown:t[2]||(t[2]=o=>C("top-right",o)),onTouchstart:t[3]||(t[3]=o=>C("top-right",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:2,class:"resize-handle bottom-left",onMousedown:t[4]||(t[4]=o=>C("bottom-left",o)),onTouchstart:t[5]||(t[5]=o=>C("bottom-left",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:3,class:"resize-handle bottom-right",onMousedown:t[6]||(t[6]=o=>C("bottom-right",o)),onTouchstart:t[7]||(t[7]=o=>C("bottom-right",o))},null,32)):e.createCommentVNode("",!0),e.createElementVNode("div",O,[d.isPlaying?(e.openBlock(),e.createElementBlock("div",J,[...t[9]||(t[9]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",j,[...t[8]||(t[8]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])],512)],6)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-5d741303"]]),q=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["highlight","showDialog","end","pause","connected","error","play_complete"],setup(d,{emit:A}){const n=d,s=A,u=e.ref(null),N=e.ref(!1);let a=null,y=0,w=!1,k=0;const I=()=>{a||(a=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),a.state==="suspended"&&a.resume()},S=()=>{w&&k===0&&(s("play_complete",n.screenClientId),w=!1)},x=l=>{if(I(),!!a)try{const r=window.atob(l),i=r.length,f=new Uint8Array(i);for(let m=0;m<i;m++)f[m]=r.charCodeAt(m);const g=new Int16Array(f.buffer),T=new Float32Array(g.length);for(let m=0;m<g.length;m++)T[m]=g[m]/32768;const W=a.createBuffer(1,T.length,24e3);W.getChannelData(0).set(T);const B=a.createBufferSource();B.buffer=W,B.connect(a.destination),y<a.currentTime&&(y=a.currentTime),B.start(y),y+=W.duration,k++,B.onended=()=>{k--,S()}}catch(r){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",r)}},b=l=>{if(!(!a&&l!=="stop"))switch(l){case"play":w=!1,k=0,y=0,a&&a.state==="suspended"&&a.resume();break;case"resume":a&&a.state==="suspended"&&a.resume();break;case"pause":a&&a.state==="running"&&a.suspend();break;case"stop":a&&(a.close(),a=null),y=0,w=!1,k=0;break;case"tts_complete":w=!0,S();break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${l}`)}},H=()=>{u.value&&u.value.close();try{const l=new URL(n.wsUrl);l.searchParams.append("sessionId",n.screenClientId+"-event"),u.value=new WebSocket(l.toString()),u.value.onopen=()=>{N.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${n.wsUrl} for session ${n.screenClientId}-event`)},u.value.onmessage=r=>{try{const i=JSON.parse(r.data);z(i)}catch(i){console.error("[VirtualHumanEventAdapter] Failed to parse message:",r.data,i)}},u.value.onerror=r=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",r),s("error",r)},u.value.onclose=()=>{N.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(l){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",l),s("error",l)}},z=l=>{const{type:r,payload:i,action:f}=l;switch(console.log("msgmsg",l),r){case"audio":const g=(i==null?void 0:i.data)||l.data;g&&x(g);break;case"dialog_event":l.event&&s(l.event,l.params);break;case"control":f&&b(f);break;case"highlight":s("highlight",i);break;case"showDialog":s("showDialog",i);break;case"end":s("end",i);break;case"pause":s("pause",i);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${r}`)}};return e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&H()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&H()}),e.onMounted(()=>{n.screenClientId&&n.wsUrl&&H()}),e.onUnmounted(()=>{u.value&&u.value.close(),a&&a.close()}),(l,r)=>e.renderSlot(l.$slots,"default")}}),G={install:d=>{d.component("VirtualHumanPersona",$),d.component("VirtualHumanEventAdapter",q)}};h.VirtualHumanEventAdapter=q,h.VirtualHumanPersona=$,h.default=G,Object.defineProperties(h,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-human-cf",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Vue3 Digital Human Component Package by cf ",
5
5
  "main": "dist/virtual-human-cf.umd.js",
6
6
  "module": "dist/virtual-human-cf.es.js",
@@ -18,7 +18,7 @@ const props = defineProps({
18
18
  }
19
19
  });
20
20
 
21
- const emit = defineEmits(['highlight', 'showDialog', 'end', 'pause', 'connected', 'error']);
21
+ const emit = defineEmits(['highlight', 'showDialog', 'end', 'pause', 'connected', 'error', 'play_complete']);
22
22
 
23
23
  const ws = ref<WebSocket | null>(null);
24
24
  const isConnected = ref(false);
@@ -26,6 +26,8 @@ const isConnected = ref(false);
26
26
  // Audio playback variables
27
27
  let audioContext: AudioContext | null = null;
28
28
  let nextStartTime = 0;
29
+ let isTtsComplete = false;
30
+ let activeSources = 0;
29
31
 
30
32
  const initAudioContext = () => {
31
33
  if (!audioContext) {
@@ -38,6 +40,13 @@ const initAudioContext = () => {
38
40
  }
39
41
  };
40
42
 
43
+ const checkPlayComplete = () => {
44
+ if (isTtsComplete && activeSources === 0) {
45
+ emit('play_complete', props.screenClientId);
46
+ isTtsComplete = false;
47
+ }
48
+ };
49
+
41
50
  const handleAudioMessage = (base64Data: string) => {
42
51
  initAudioContext();
43
52
  if (!audioContext) return;
@@ -70,29 +79,50 @@ const handleAudioMessage = (base64Data: string) => {
70
79
  }
71
80
  source.start(nextStartTime);
72
81
  nextStartTime += audioBuffer.duration;
82
+
83
+ activeSources++;
84
+ source.onended = () => {
85
+ activeSources--;
86
+ checkPlayComplete();
87
+ };
73
88
  } catch (error) {
74
89
  console.error('[VirtualHumanEventAdapter] Failed to decode and play audio:', error);
75
90
  }
76
91
  };
77
92
 
78
93
  const handleControlMessage = (action: string) => {
79
- if (!audioContext) return;
94
+ if (!audioContext && action !== 'stop') return;
80
95
  switch (action) {
81
96
  case 'play':
97
+ isTtsComplete = false;
98
+ activeSources = 0;
99
+ nextStartTime = 0;
100
+ if (audioContext && audioContext.state === 'suspended') {
101
+ audioContext.resume();
102
+ }
103
+ break;
82
104
  case 'resume':
83
- if (audioContext.state === 'suspended') {
105
+ if (audioContext && audioContext.state === 'suspended') {
84
106
  audioContext.resume();
85
107
  }
86
108
  break;
87
109
  case 'pause':
88
- if (audioContext.state === 'running') {
110
+ if (audioContext && audioContext.state === 'running') {
89
111
  audioContext.suspend();
90
112
  }
91
113
  break;
92
114
  case 'stop':
93
- audioContext.close();
94
- audioContext = null;
115
+ if (audioContext) {
116
+ audioContext.close();
117
+ audioContext = null;
118
+ }
95
119
  nextStartTime = 0;
120
+ isTtsComplete = false;
121
+ activeSources = 0;
122
+ break;
123
+ case 'tts_complete':
124
+ isTtsComplete = true;
125
+ checkPlayComplete();
96
126
  break;
97
127
  default:
98
128
  console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${action}`);
@@ -4,12 +4,14 @@
4
4
  v-if="visible"
5
5
  class="virtual-human-container"
6
6
  :class="{ 'is-dark': isDark }"
7
+ :style="{
8
+ width: containerWidth + 'px',
9
+ height: containerHeight + 'px',
10
+ left: containerLeft + 'px',
11
+ bottom: containerBottom + 'px'
12
+ }"
7
13
  >
8
- <div
9
- class="video-wrapper"
10
- ref="wrapperRef"
11
- :style="{ transform: `scale(${scale})`, transformOrigin: 'left bottom' }"
12
- >
14
+ <div class="video-wrapper" ref="wrapperRef">
13
15
  <video
14
16
  ref="videoRef"
15
17
  :src="videoSrc"
@@ -114,12 +116,19 @@ const ws = ref<WebSocket | null>(null);
114
116
  const isConnected = ref(false);
115
117
 
116
118
  // 缩放相关状态
117
- const scale = ref(1);
119
+ const containerWidth = ref(400);
120
+ const containerHeight = ref(500);
121
+ const containerLeft = ref(16);
122
+ const containerBottom = ref(16);
123
+
118
124
  const isResizing = ref(false);
119
125
  const resizeHandle = ref<string | null>(null);
120
126
  const startX = ref(0);
121
127
  const startY = ref(0);
122
- const startScale = ref(1);
128
+ const startW = ref(0);
129
+ const startH = ref(0);
130
+ const startL = ref(0);
131
+ const startB = ref(0);
123
132
 
124
133
  const connectWebSocket = () => {
125
134
  if (ws.value) {
@@ -220,23 +229,29 @@ const handleEnded = () => {
220
229
 
221
230
  // 缩放功能
222
231
  const startResize = (handle: string, event: MouseEvent | TouchEvent) => {
232
+ event.preventDefault();
223
233
  isResizing.value = true;
224
234
  resizeHandle.value = handle;
225
- startScale.value = scale.value;
226
235
 
227
236
  const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
228
237
  const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
229
238
  startX.value = clientX;
230
239
  startY.value = clientY;
231
240
 
241
+ startW.value = containerWidth.value;
242
+ startH.value = containerHeight.value;
243
+ startL.value = containerLeft.value;
244
+ startB.value = containerBottom.value;
245
+
232
246
  document.addEventListener('mousemove', handleResize);
233
247
  document.addEventListener('mouseup', stopResize);
234
- document.addEventListener('touchmove', handleResize);
248
+ document.addEventListener('touchmove', handleResize, { passive: false });
235
249
  document.addEventListener('touchend', stopResize);
236
250
  };
237
251
 
238
252
  const handleResize = (event: MouseEvent | TouchEvent) => {
239
- if (!isResizing.value || !wrapperRef.value) return;
253
+ if (!isResizing.value) return;
254
+ event.preventDefault();
240
255
 
241
256
  const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
242
257
  const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
@@ -244,25 +259,57 @@ const handleResize = (event: MouseEvent | TouchEvent) => {
244
259
  const deltaX = clientX - startX.value;
245
260
  const deltaY = clientY - startY.value;
246
261
 
247
- const rect = wrapperRef.value.getBoundingClientRect();
248
- const minScale = 0.5;
249
- const maxScale = 2;
262
+ const minWidth = 200;
263
+ const minHeight = 250;
264
+ const maxWidth = 800;
265
+ const maxHeight = 1000;
250
266
 
251
- // 根据拖动的角计算缩放比例
252
- let newScale = startScale.value;
253
- const baseSize = Math.min(rect.width, rect.height);
267
+ let newW = startW.value;
268
+ let newH = startH.value;
269
+ let newL = startL.value;
270
+ let newB = startB.value;
254
271
 
255
272
  if (resizeHandle.value === 'bottom-right') {
256
- newScale = startScale.value + (deltaX + deltaY) / baseSize;
273
+ newW = startW.value + deltaX;
274
+ newB = startB.value - deltaY;
275
+ newH = startH.value + deltaY;
257
276
  } else if (resizeHandle.value === 'bottom-left') {
258
- newScale = startScale.value + (-deltaX + deltaY) / baseSize;
277
+ newL = startL.value + deltaX;
278
+ newW = startW.value - deltaX;
279
+ newB = startB.value - deltaY;
280
+ newH = startH.value + deltaY;
259
281
  } else if (resizeHandle.value === 'top-right') {
260
- newScale = startScale.value + (deltaX - deltaY) / baseSize;
282
+ newW = startW.value + deltaX;
283
+ newH = startH.value - deltaY;
261
284
  } else if (resizeHandle.value === 'top-left') {
262
- newScale = startScale.value + (-deltaX - deltaY) / baseSize;
285
+ newL = startL.value + deltaX;
286
+ newW = startW.value - deltaX;
287
+ newH = startH.value - deltaY;
263
288
  }
264
289
 
265
- scale.value = Math.max(minScale, Math.min(maxScale, newScale));
290
+ // 约束最小宽高
291
+ if (newW < minWidth) {
292
+ if (newL !== startL.value) newL -= (minWidth - newW);
293
+ newW = minWidth;
294
+ }
295
+ if (newH < minHeight) {
296
+ if (newB !== startB.value) newB -= (minHeight - newH);
297
+ newH = minHeight;
298
+ }
299
+ // 约束最大宽高
300
+ if (newW > maxWidth) {
301
+ if (newL !== startL.value) newL += (newW - maxWidth);
302
+ newW = maxWidth;
303
+ }
304
+ if (newH > maxHeight) {
305
+ if (newB !== startB.value) newB += (newH - maxHeight);
306
+ newH = maxHeight;
307
+ }
308
+
309
+ containerWidth.value = newW;
310
+ containerHeight.value = newH;
311
+ containerLeft.value = newL;
312
+ containerBottom.value = newB;
266
313
  };
267
314
 
268
315
  const stopResize = () => {
@@ -304,23 +351,25 @@ onUnmounted(() => {
304
351
 
305
352
  .virtual-human-container {
306
353
  position: fixed;
307
- left: 16px;
308
- bottom: 16px;
309
- width: 400px;
310
- height: 500px;
354
+ /* left, bottom, width, height are set dynamically */
311
355
  z-index: 2147483647;
312
356
  overflow: visible;
313
357
  }
314
358
 
315
359
  .video-wrapper {
316
360
  position: relative;
317
- width: 400px;
318
- height: 500px;
361
+ width: 100%;
362
+ height: 100%;
319
363
  border-radius: 1rem;
320
364
  overflow: hidden;
321
365
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
322
366
  background: #ffffff;
323
- transition: background-color 0.3s ease, box-shadow 0.3s ease;
367
+ border: 2px solid transparent;
368
+ transition: background-color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
369
+ }
370
+
371
+ .video-wrapper:hover {
372
+ border-color: #409EFF;
324
373
  }
325
374
 
326
375
  .virtual-human-container.is-dark .video-wrapper {
@@ -346,11 +395,13 @@ onUnmounted(() => {
346
395
  cursor: pointer;
347
396
  z-index: 20;
348
397
  opacity: 0;
398
+ pointer-events: none;
349
399
  transition: opacity 0.3s ease, background 0.2s, border-color 0.2s;
350
400
  }
351
401
 
352
402
  .video-wrapper:hover .resize-handle {
353
403
  opacity: 1;
404
+ pointer-events: auto;
354
405
  }
355
406
 
356
407
  .resize-handle:hover {