dsh-voice-mode 0.1.0

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/lib/client.js ADDED
@@ -0,0 +1,1750 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-voice-mode", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ "use strict";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (let key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(to, key) && key !== except)
18
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
+ // If the importer is in node compatibility mode or this is not an ESM
24
+ // file that has been converted to a CommonJS file using a Babel-
25
+ // compatible transform (i.e. "__esModule" has not been set), then set
26
+ // "default" to the CommonJS "module.exports" for node compatibility.
27
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
+ mod
29
+ ));
30
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
+
32
+ // src/client.tsx
33
+ var client_exports = {};
34
+ __export(client_exports, {
35
+ MicButton: () => MicButton,
36
+ VoiceOverlay: () => VoiceOverlay,
37
+ VoiceStatusBar: () => VoiceStatusBar,
38
+ apply: () => apply,
39
+ inject: () => inject
40
+ });
41
+ module.exports = __toCommonJS(client_exports);
42
+ var React = __toESM(require("react"), 1);
43
+ var import_react2 = require("react");
44
+
45
+ // src/wakeword.ts
46
+ function normalizeWake(text) {
47
+ return String(text ?? "").replace(/[\s\u3000]+/g, "").toLowerCase().replace(/[,。!?!?;;、,.]/g, "");
48
+ }
49
+ function matchWakeWord(partial, wakeWord) {
50
+ const w = normalizeWake(wakeWord);
51
+ if (!w) return false;
52
+ const p = normalizeWake(partial);
53
+ if (!p) return false;
54
+ if (p.startsWith(w)) return true;
55
+ return false;
56
+ }
57
+
58
+ // src/asr.ts
59
+ var SAMPLE_RATE = 16e3;
60
+ var SPEECH_RMS = 0.015;
61
+ var LEVEL_CEILING = 0.25;
62
+ var MAX_SEGMENT_MS = 3e4;
63
+ var PRE_PAD_MS = 250;
64
+ var PARTIAL_INTERVAL_MS = 900;
65
+ var PARTIAL_MIN_S = 0.4;
66
+ var PARTIAL_MAX_S = 30;
67
+ var BUFFER_SIZE = 1024;
68
+ var INTERRUPT_LEVELS = {
69
+ 0: { rms: 0.1, ms: 500 },
70
+ 1: { rms: 0.06, ms: 400 },
71
+ 2: { rms: 0.035, ms: 300 }
72
+ };
73
+ function createAsrEngine(config, sessionId) {
74
+ let state = "idle";
75
+ const stateListeners = /* @__PURE__ */ new Set();
76
+ const transcriptListeners = /* @__PURE__ */ new Set();
77
+ const partialListeners = /* @__PURE__ */ new Set();
78
+ const speechStartListeners = /* @__PURE__ */ new Set();
79
+ const levelListeners = /* @__PURE__ */ new Set();
80
+ let audioCtx = null;
81
+ let stream = null;
82
+ let processor = null;
83
+ let active = false;
84
+ let inFlush = false;
85
+ let ctxRate = SAMPLE_RATE;
86
+ let speechActive = false;
87
+ let segment = [];
88
+ let segmentMs = 0;
89
+ let silenceMs = 0;
90
+ let prePad = [];
91
+ let holdActive = false;
92
+ const wakeWord = (config.wakeWord ?? "").trim().toLowerCase().replace(/[\s\u3000]+/g, "");
93
+ const intLevel = INTERRUPT_LEVELS[config.interruptLevel] ?? INTERRUPT_LEVELS[0];
94
+ let interruptCandidateMs = 0;
95
+ let bargeInDampingUntil = 0;
96
+ let sincePartialMs = 0;
97
+ let partialInFlight = false;
98
+ let segmentEpoch = 0;
99
+ let forcePending = false;
100
+ const asrUrl = (final) => `${location.origin}${config.basePath.replace(/\/+$/, "")}/asr?sessionId=${encodeURIComponent(sessionId)}&final=${final ? 1 : 0}`;
101
+ const setState = (s) => {
102
+ state = s;
103
+ for (const fn of stateListeners) {
104
+ try {
105
+ fn(s);
106
+ } catch {
107
+ }
108
+ }
109
+ };
110
+ const emit = (listeners, text, meta) => {
111
+ const t2 = text.trim();
112
+ if (!t2) return;
113
+ for (const fn of listeners) {
114
+ try {
115
+ fn(t2, meta);
116
+ } catch {
117
+ }
118
+ }
119
+ };
120
+ const concatSegment = () => {
121
+ const n = segment.reduce((acc, c) => acc + c.length, 0);
122
+ const out = new Float32Array(n);
123
+ let off = 0;
124
+ for (const c of segment) {
125
+ out.set(c, off);
126
+ off += c.length;
127
+ }
128
+ return out;
129
+ };
130
+ const requestPartial = async () => {
131
+ if (partialInFlight || segment.length === 0) return;
132
+ const seconds = segment.reduce((n, c) => n + c.length, 0) / SAMPLE_RATE;
133
+ if (seconds < PARTIAL_MIN_S || seconds > PARTIAL_MAX_S) return;
134
+ const samples = concatSegment();
135
+ const epoch = segmentEpoch;
136
+ partialInFlight = true;
137
+ try {
138
+ let res = await fetch(asrUrl(false), {
139
+ method: "POST",
140
+ headers: { "content-type": "application/octet-stream" },
141
+ body: samples.slice().buffer
142
+ });
143
+ if (res.status === 202) {
144
+ setState("loading-model");
145
+ const retry = await new Promise((resolve) => {
146
+ setTimeout(async () => {
147
+ try {
148
+ const r2 = await fetch(asrUrl(false), {
149
+ method: "POST",
150
+ headers: { "content-type": "application/octet-stream" },
151
+ body: samples.slice().buffer
152
+ });
153
+ resolve(r2);
154
+ } catch {
155
+ resolve(new Response(null, { status: 0 }));
156
+ }
157
+ }, 5e3);
158
+ });
159
+ res = retry;
160
+ }
161
+ if (epoch !== segmentEpoch) return;
162
+ if (!res.ok) return;
163
+ const out = await res.json();
164
+ if (epoch !== segmentEpoch) return;
165
+ if (state === "loading-model") setState("speech");
166
+ if (state === "wake" && wakeWord) {
167
+ if (matchWakeWord(out.text ?? "", wakeWord)) {
168
+ segmentEpoch++;
169
+ segment = [];
170
+ segmentMs = 0;
171
+ silenceMs = 0;
172
+ prePad = [];
173
+ sincePartialMs = 0;
174
+ await resetHostStream();
175
+ if (active) setState("listening");
176
+ }
177
+ return;
178
+ }
179
+ emit(partialListeners, out.text ?? "");
180
+ } catch {
181
+ } finally {
182
+ partialInFlight = false;
183
+ }
184
+ };
185
+ const resetHostStream = async () => {
186
+ try {
187
+ await fetch(`${asrUrl(false)}&reset=1`, { method: "POST" });
188
+ } catch {
189
+ }
190
+ };
191
+ const finalizeSegment = () => {
192
+ if (segment.length === 0) return;
193
+ const samples = concatSegment();
194
+ const epoch = ++segmentEpoch;
195
+ const meta = { force: forcePending };
196
+ forcePending = false;
197
+ segment = [];
198
+ speechActive = false;
199
+ silenceMs = 0;
200
+ segmentMs = 0;
201
+ prePad = [];
202
+ setState("transcribing");
203
+ void (async () => {
204
+ try {
205
+ let res = await fetch(asrUrl(true), {
206
+ method: "POST",
207
+ headers: { "content-type": "application/octet-stream" },
208
+ body: samples.slice().buffer
209
+ });
210
+ if (res.status === 202) {
211
+ setState("loading-model");
212
+ res = await new Promise((resolve) => {
213
+ setTimeout(async () => {
214
+ try {
215
+ resolve(
216
+ await fetch(asrUrl(true), {
217
+ method: "POST",
218
+ headers: { "content-type": "application/octet-stream" },
219
+ body: samples.slice().buffer
220
+ })
221
+ );
222
+ } catch {
223
+ resolve(new Response(null, { status: 0 }));
224
+ }
225
+ }, 5e3);
226
+ });
227
+ }
228
+ if (epoch !== segmentEpoch) return;
229
+ setState(active ? speechActive ? "speech" : "listening" : "idle");
230
+ if (!res.ok) return;
231
+ const out = await res.json();
232
+ if (epoch !== segmentEpoch) return;
233
+ if (out.text) emit(transcriptListeners, out.text, meta);
234
+ } catch {
235
+ setState(active ? speechActive ? "speech" : "listening" : "idle");
236
+ }
237
+ })();
238
+ };
239
+ const handleAudio = (raw) => {
240
+ if (!active || inFlush) return;
241
+ const data = ctxRate !== SAMPLE_RATE ? resampleTo16k(raw, ctxRate) : raw;
242
+ let sum = 0;
243
+ for (let i = 0; i < data.length; i++) sum += data[i] * data[i];
244
+ const rms = Math.sqrt(sum / data.length);
245
+ const durationMs = data.length / SAMPLE_RATE * 1e3;
246
+ for (const fn of levelListeners) {
247
+ try {
248
+ fn(Math.min(1, rms / LEVEL_CEILING));
249
+ } catch {
250
+ }
251
+ }
252
+ if (Date.now() < bargeInDampingUntil) {
253
+ interruptCandidateMs = 0;
254
+ } else if (rms > intLevel.rms) {
255
+ interruptCandidateMs += durationMs;
256
+ if (interruptCandidateMs >= intLevel.ms) {
257
+ interruptCandidateMs = 0;
258
+ bargeInDampingUntil = Date.now() + 800;
259
+ for (const fn of speechStartListeners) {
260
+ try {
261
+ fn();
262
+ } catch {
263
+ }
264
+ }
265
+ }
266
+ } else {
267
+ interruptCandidateMs = 0;
268
+ }
269
+ if (holdActive) {
270
+ if (!speechActive) {
271
+ speechActive = true;
272
+ if (state !== "speech") setState("speech");
273
+ }
274
+ segmentMs += durationMs;
275
+ silenceMs = 0;
276
+ segment.push(data);
277
+ if (segmentMs > MAX_SEGMENT_MS) finalizeSegment();
278
+ } else if (state === "wake") {
279
+ if (rms > SPEECH_RMS) {
280
+ segmentMs += durationMs;
281
+ segment.push(data);
282
+ if (segmentMs > MAX_SEGMENT_MS) {
283
+ segment = [];
284
+ segmentMs = 0;
285
+ silenceMs = 0;
286
+ prePad = [];
287
+ void resetHostStream();
288
+ }
289
+ } else {
290
+ prePad.push(data);
291
+ let total = 0;
292
+ let cut = 0;
293
+ for (let i = prePad.length - 1; i >= 0; i--) {
294
+ total += prePad[i].length / SAMPLE_RATE * 1e3;
295
+ if (total > PRE_PAD_MS) {
296
+ cut = i + 1;
297
+ break;
298
+ }
299
+ }
300
+ if (cut > 0) prePad = prePad.slice(cut);
301
+ }
302
+ } else if (rms > SPEECH_RMS) {
303
+ if (!speechActive) {
304
+ speechActive = true;
305
+ setState("speech");
306
+ for (const p of prePad) segment.push(p);
307
+ prePad = [];
308
+ }
309
+ segmentMs += durationMs;
310
+ silenceMs = 0;
311
+ segment.push(data);
312
+ if (segmentMs > MAX_SEGMENT_MS) finalizeSegment();
313
+ } else if (speechActive) {
314
+ segmentMs += durationMs;
315
+ silenceMs += durationMs;
316
+ segment.push(data);
317
+ if (silenceMs > config.silenceMs) finalizeSegment();
318
+ } else {
319
+ prePad.push(data);
320
+ let total = 0;
321
+ let cut = 0;
322
+ for (let i = prePad.length - 1; i >= 0; i--) {
323
+ total += prePad[i].length / SAMPLE_RATE * 1e3;
324
+ if (total > PRE_PAD_MS) {
325
+ cut = i + 1;
326
+ break;
327
+ }
328
+ }
329
+ if (cut > 0) prePad = prePad.slice(cut);
330
+ }
331
+ sincePartialMs += durationMs;
332
+ if ((speechActive || holdActive || state === "wake") && sincePartialMs >= PARTIAL_INTERVAL_MS) {
333
+ sincePartialMs = 0;
334
+ void requestPartial();
335
+ }
336
+ };
337
+ function resampleTo16k(src, srcRate) {
338
+ const ratio = srcRate / SAMPLE_RATE;
339
+ const outLen = Math.max(1, Math.floor(src.length / ratio));
340
+ const out = new Float32Array(outLen);
341
+ for (let i = 0; i < outLen; i++) {
342
+ const pos = i * ratio;
343
+ const i0 = Math.floor(pos);
344
+ const i1 = Math.min(i0 + 1, src.length - 1);
345
+ const frac = pos - i0;
346
+ out[i] = src[i0] + (src[i1] - src[i0]) * frac;
347
+ }
348
+ return out;
349
+ }
350
+ const startRecorder = async () => {
351
+ stream = await navigator.mediaDevices.getUserMedia({
352
+ audio: {
353
+ channelCount: 1,
354
+ echoCancellation: true,
355
+ noiseSuppression: true,
356
+ autoGainControl: true
357
+ }
358
+ });
359
+ const AC = window.AudioContext ?? window.webkitAudioContext;
360
+ audioCtx = new AC({ sampleRate: SAMPLE_RATE });
361
+ ctxRate = audioCtx.sampleRate;
362
+ const source = audioCtx.createMediaStreamSource(stream);
363
+ processor = audioCtx.createScriptProcessor(BUFFER_SIZE, 1, 1);
364
+ processor.onaudioprocess = (e) => {
365
+ handleAudio(new Float32Array(e.inputBuffer.getChannelData(0)));
366
+ };
367
+ source.connect(processor);
368
+ processor.connect(audioCtx.destination);
369
+ active = true;
370
+ };
371
+ const stopRecorder = async () => {
372
+ if (!active) return;
373
+ active = false;
374
+ inFlush = true;
375
+ segmentEpoch++;
376
+ forcePending = false;
377
+ holdActive = false;
378
+ segment = [];
379
+ speechActive = false;
380
+ silenceMs = 0;
381
+ segmentMs = 0;
382
+ prePad = [];
383
+ interruptCandidateMs = 0;
384
+ try {
385
+ processor?.disconnect();
386
+ } catch {
387
+ }
388
+ processor = null;
389
+ try {
390
+ stream?.getTracks().forEach((t2) => t2.stop());
391
+ } catch {
392
+ }
393
+ stream = null;
394
+ try {
395
+ await audioCtx?.close();
396
+ } catch {
397
+ }
398
+ audioCtx = null;
399
+ ctxRate = SAMPLE_RATE;
400
+ inFlush = false;
401
+ };
402
+ return {
403
+ get state() {
404
+ return state;
405
+ },
406
+ async start() {
407
+ if (active) return;
408
+ segmentEpoch++;
409
+ sincePartialMs = 0;
410
+ interruptCandidateMs = 0;
411
+ holdActive = false;
412
+ setState(wakeWord ? "wake" : "listening");
413
+ try {
414
+ await startRecorder();
415
+ } catch (error) {
416
+ setState("idle");
417
+ throw error;
418
+ }
419
+ },
420
+ async stop() {
421
+ if (!active) {
422
+ setState("idle");
423
+ return;
424
+ }
425
+ await stopRecorder();
426
+ setState("idle");
427
+ },
428
+ forceSend() {
429
+ const speechS = segment.reduce((n, c) => n + c.length, 0) / SAMPLE_RATE;
430
+ if (speechActive && speechS >= 0.25) {
431
+ forcePending = true;
432
+ sincePartialMs = 0;
433
+ finalizeSegment();
434
+ }
435
+ },
436
+ beginHeld() {
437
+ if (!active || holdActive) return;
438
+ holdActive = true;
439
+ forcePending = true;
440
+ segmentEpoch++;
441
+ segment = [];
442
+ segmentMs = 0;
443
+ silenceMs = 0;
444
+ prePad = [];
445
+ speechActive = true;
446
+ sincePartialMs = 0;
447
+ bargeInDampingUntil = Date.now() + 800;
448
+ setState("speech");
449
+ },
450
+ endHeld(cancel = false) {
451
+ if (!active || !holdActive) return;
452
+ holdActive = false;
453
+ if (cancel) {
454
+ segmentEpoch++;
455
+ segment = [];
456
+ segmentMs = 0;
457
+ silenceMs = 0;
458
+ prePad = [];
459
+ speechActive = false;
460
+ setState(wakeWord ? "wake" : "listening");
461
+ return;
462
+ }
463
+ forcePending = true;
464
+ sincePartialMs = 0;
465
+ finalizeSegment();
466
+ },
467
+ onSegment(fn) {
468
+ transcriptListeners.add(fn);
469
+ return () => {
470
+ transcriptListeners.delete(fn);
471
+ };
472
+ },
473
+ onPartial(fn) {
474
+ partialListeners.add(fn);
475
+ return () => {
476
+ partialListeners.delete(fn);
477
+ };
478
+ },
479
+ onSpeechStart(fn) {
480
+ speechStartListeners.add(fn);
481
+ return () => {
482
+ speechStartListeners.delete(fn);
483
+ };
484
+ },
485
+ onState(fn) {
486
+ stateListeners.add(fn);
487
+ fn(state);
488
+ return () => {
489
+ stateListeners.delete(fn);
490
+ };
491
+ },
492
+ onLevel(fn) {
493
+ levelListeners.add(fn);
494
+ return () => {
495
+ levelListeners.delete(fn);
496
+ };
497
+ }
498
+ };
499
+ }
500
+
501
+ // src/settings-form.tsx
502
+ var import_react = require("react");
503
+ var import_jsx_runtime = require("react/jsx-runtime");
504
+ var t = {
505
+ bg: "var(--dsw-alias-bg-layer-3)",
506
+ bgOpen: "var(--dsw-alias-bg-layer-2)",
507
+ border: "var(--dsw-alias-border-l2)",
508
+ label: "var(--dsw-alias-label-primary)",
509
+ term: "var(--dsw-alias-label-tertiary)",
510
+ brand: "var(--dsw-alias-brand-primary)"
511
+ };
512
+ var BASE_PATH = "/voice-mode";
513
+ var cardStyle = {
514
+ border: `1px solid ${t.border}`,
515
+ background: t.bg,
516
+ borderRadius: 12,
517
+ overflow: "hidden"
518
+ };
519
+ var setHeader = {
520
+ appearance: "none",
521
+ width: "100%",
522
+ font: "inherit",
523
+ color: "inherit",
524
+ textAlign: "left",
525
+ cursor: "pointer",
526
+ background: "transparent",
527
+ border: 0,
528
+ borderRadius: 12,
529
+ alignItems: "center",
530
+ gap: 12,
531
+ padding: "14px 16px",
532
+ display: "flex"
533
+ };
534
+ var setHeadText = { flexDirection: "column", flex: 1, gap: 4, minWidth: 0, display: "flex" };
535
+ var setName = { color: t.label, fontSize: 15, fontWeight: 600, lineHeight: 1.4 };
536
+ var setDesc = { color: t.term, fontSize: 13, lineHeight: 1.5 };
537
+ var setChevron = { color: t.term, flex: "none", transition: "transform .16s", display: "inline-flex" };
538
+ var setBody = { borderTop: `1px solid ${t.border}`, margin: "0 16px", paddingBottom: 8 };
539
+ var setRow = { alignItems: "center", gap: 12, padding: "12px 0", display: "flex" };
540
+ var setLabelBox = { flexDirection: "column", flex: 1, gap: 3, minWidth: 0, display: "flex" };
541
+ var setLabel = { fontSize: 13, lineHeight: "20px" };
542
+ var setHint = { color: t.term, fontSize: 12, lineHeight: "18px" };
543
+ var setSeg = { border: `1px solid ${t.border}`, borderRadius: 8, flexShrink: 0, gap: 2, padding: 2, display: "inline-flex" };
544
+ var setSegBtn = (on) => ({
545
+ font: "inherit",
546
+ color: on ? t.label : "var(--dsw-alias-label-secondary)",
547
+ cursor: "pointer",
548
+ background: on ? "var(--dsw-alias-bg-layer-2)" : "transparent",
549
+ border: "none",
550
+ borderRadius: 6,
551
+ padding: "4px 12px",
552
+ fontSize: 12,
553
+ lineHeight: "18px",
554
+ fontWeight: on ? 600 : 400
555
+ });
556
+ var inputStyle = {
557
+ boxSizing: "border-box",
558
+ width: 280,
559
+ maxWidth: "100%",
560
+ padding: "7px 10px",
561
+ borderRadius: 8,
562
+ border: `1px solid ${t.border}`,
563
+ background: "var(--dsw-alias-bg-layer-2)",
564
+ color: t.label,
565
+ fontSize: 13,
566
+ fontFamily: "inherit",
567
+ outline: "none"
568
+ };
569
+ var focusVisibleCss = `
570
+ [data-dshvm-settings="card"] input:focus-visible,
571
+ [data-dshvm-settings="card"] select:focus-visible,
572
+ [data-dshvm-settings="card"] button:focus-visible {
573
+ outline: 2px solid var(--dsw-alias-brand-primary);
574
+ outline-offset: 1px;
575
+ }
576
+ @media (prefers-reduced-motion: reduce) {
577
+ [data-dshvm-settings="card"], [data-dshvm-settings="card"] * { transition: none !important; }
578
+ }`;
579
+ var VOICE_OPTIONS = [
580
+ { v: "zh-CN-XiaoxiaoNeural", label: "\u6653\u6653 \xB7 \u5973 \xB7 \u7B80\u4F53\u4E2D\u6587" },
581
+ { v: "zh-CN-XiaoyiNeural", label: "\u6653\u4F0A \xB7 \u5973 \xB7 \u7B80\u4F53\u4E2D\u6587" },
582
+ { v: "zh-CN-YunxiNeural", label: "\u4E91\u5E0C \xB7 \u7537 \xB7 \u7B80\u4F53\u4E2D\u6587" },
583
+ { v: "zh-CN-YunjianNeural", label: "\u4E91\u5065 \xB7 \u7537 \xB7 \u7B80\u4F53\u4E2D\u6587" },
584
+ { v: "zh-CN-YunyangNeural", label: "\u4E91\u626C \xB7 \u7537 \xB7 \u7B80\u4F53\u4E2D\u6587" },
585
+ { v: "zh-CN-YunxiaNeural", label: "\u4E91\u590F \xB7 \u7537 \xB7 \u7B80\u4F53\u4E2D\u6587" },
586
+ { v: "zh-CN-liaoning-XiaobeiNeural", label: "\u5C0F\u5317 \xB7 \u5973 \xB7 \u4E1C\u5317\u8BDD" },
587
+ { v: "zh-CN-shaanxi-XiaoniNeural", label: "\u5C0F\u59AE \xB7 \u5973 \xB7 \u9655\u897F\u8BDD" },
588
+ { v: "zh-HK-HiuMaanNeural", label: "\u6653\u66FC \xB7 \u5973 \xB7 \u7CA4\u8BED" },
589
+ { v: "zh-HK-WanLungNeural", label: "\u4E91\u9F99 \xB7 \u7537 \xB7 \u7CA4\u8BED" },
590
+ { v: "zh-TW-HsiaoYuNeural", label: "\u5C0F\u96E8 \xB7 \u5973 \xB7 \u53F0\u6E7E\u8154" },
591
+ { v: "zh-TW-YunJheNeural", label: "\u4E91\u54F2 \xB7 \u7537 \xB7 \u53F0\u6E7E\u8154" },
592
+ { v: "en-US-AriaNeural", label: "Aria \xB7 \u5973 \xB7 English" },
593
+ { v: "en-US-GuyNeural", label: "Guy \xB7 \u7537 \xB7 English" }
594
+ ];
595
+ var HOST_OPTIONS = [
596
+ { v: "https://huggingface.co", label: "\u5B98\u65B9\u6E90 huggingface.co" },
597
+ { v: "https://hf-mirror.com", label: "\u56FD\u5185\u955C\u50CF hf-mirror.com" }
598
+ ];
599
+ function NumberField({
600
+ score,
601
+ field,
602
+ value,
603
+ min,
604
+ max,
605
+ step
606
+ }) {
607
+ const [draft, setDraft] = (0, import_react.useState)(String(value ?? ""));
608
+ (0, import_react.useEffect)(() => {
609
+ setDraft((d) => d === String(value ?? "") ? d : String(value ?? ""));
610
+ }, [value]);
611
+ const commit = () => {
612
+ const n = Number(draft);
613
+ if (!Number.isFinite(n) || draft.trim() === "") return;
614
+ const clamped = Math.min(max, Math.max(min, n));
615
+ setDraft(String(clamped));
616
+ void score.set(field, clamped);
617
+ };
618
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
619
+ "input",
620
+ {
621
+ style: inputStyle,
622
+ type: "number",
623
+ step,
624
+ min,
625
+ max,
626
+ value: draft,
627
+ onChange: (e) => setDraft(e.target.value),
628
+ onBlur: commit,
629
+ onKeyDown: (e) => {
630
+ if (e.key === "Enter") commit();
631
+ }
632
+ }
633
+ );
634
+ }
635
+ function TextField({
636
+ score,
637
+ field,
638
+ value,
639
+ placeholder
640
+ }) {
641
+ const [draft, setDraft] = (0, import_react.useState)(String(value ?? ""));
642
+ (0, import_react.useEffect)(() => {
643
+ setDraft((d) => d === String(value ?? "") ? d : String(value ?? ""));
644
+ }, [value]);
645
+ const commit = () => {
646
+ void score.set(field, draft);
647
+ };
648
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
649
+ "input",
650
+ {
651
+ style: inputStyle,
652
+ value: draft,
653
+ placeholder,
654
+ onChange: (e) => setDraft(e.target.value),
655
+ onBlur: commit,
656
+ onKeyDown: (e) => {
657
+ if (e.key === "Enter") commit();
658
+ }
659
+ }
660
+ );
661
+ }
662
+ function SelectField({
663
+ score,
664
+ field,
665
+ value,
666
+ options,
667
+ placeholder,
668
+ footer
669
+ }) {
670
+ const cur = String(value ?? "");
671
+ const inOptions = options.some((o) => o.v === cur);
672
+ const [custom, setCustom] = (0, import_react.useState)(inOptions ? "" : cur);
673
+ (0, import_react.useEffect)(() => {
674
+ if (!options.some((o) => o.v === cur)) setCustom(cur);
675
+ }, [cur, options]);
676
+ const selectStyle = {
677
+ ...inputStyle,
678
+ appearance: "none",
679
+ cursor: "pointer",
680
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")`,
681
+ backgroundPosition: "right 12px center",
682
+ backgroundRepeat: "no-repeat",
683
+ backgroundSize: "12px 12px",
684
+ paddingRight: 32
685
+ };
686
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "flex", flexDirection: "column", gap: 6, width: 280, alignItems: "stretch" }, children: [
687
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
688
+ "select",
689
+ {
690
+ style: selectStyle,
691
+ value: inOptions ? cur : "__custom__",
692
+ onChange: (e) => {
693
+ const v = e.target.value;
694
+ if (v === "__custom__") void score.set(field, custom);
695
+ else void score.set(field, v);
696
+ },
697
+ children: [
698
+ options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: o.v, children: o.label }, o.v)),
699
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "__custom__", children: "\u81EA\u5B9A\u4E49\u2026" })
700
+ ]
701
+ }
702
+ ),
703
+ !inOptions && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
704
+ "input",
705
+ {
706
+ style: inputStyle,
707
+ value: custom,
708
+ placeholder,
709
+ onChange: (e) => setCustom(e.target.value),
710
+ onBlur: () => void score.set(field, custom),
711
+ onKeyDown: (e) => {
712
+ if (e.key === "Enter") void score.set(field, custom);
713
+ }
714
+ }
715
+ ),
716
+ footer?.(inOptions ? cur : custom)
717
+ ] });
718
+ }
719
+ function VoicePreviewButton({ voice, rate }) {
720
+ const [busy, setBusy] = (0, import_react.useState)(false);
721
+ const [note, setNote] = (0, import_react.useState)(null);
722
+ const audioRef = (0, import_react.useRef)(null);
723
+ const play = () => {
724
+ if (busy) return;
725
+ const v = voice.trim();
726
+ if (!v) {
727
+ setNote("\u8BF7\u5148\u586B\u5199\u97F3\u8272\u540D\uFF08ShortName\uFF09");
728
+ return;
729
+ }
730
+ setBusy(true);
731
+ setNote(null);
732
+ const audio = new Audio();
733
+ const prev = audioRef.current;
734
+ if (prev) {
735
+ prev.pause();
736
+ if (prev.src.startsWith("blob:")) URL.revokeObjectURL(prev.src);
737
+ }
738
+ audioRef.current = audio;
739
+ void (async () => {
740
+ try {
741
+ const res = await fetch(`${BASE_PATH}/preview`, {
742
+ method: "POST",
743
+ headers: { "content-type": "application/json" },
744
+ body: JSON.stringify({ voice: v, rate }),
745
+ signal: AbortSignal.timeout(15e3)
746
+ });
747
+ if (res.status === 403) {
748
+ setNote("\u8BED\u97F3\u6A21\u5F0F\u5DF2\u7981\u7528\uFF08\u63D2\u4EF6 enabled=false\uFF09\uFF0C\u65E0\u6CD5\u8BD5\u542C");
749
+ return;
750
+ }
751
+ if (!res.ok) throw new Error(`preview http ${res.status}`);
752
+ const blob = await res.blob();
753
+ const url = URL.createObjectURL(blob);
754
+ audio.src = url;
755
+ audio.onended = () => URL.revokeObjectURL(url);
756
+ audio.onerror = () => {
757
+ URL.revokeObjectURL(url);
758
+ setNote("\u8BD5\u542C\u5931\u8D25\uFF1A\u65E0\u6CD5\u64AD\u653E\u8BE5\u97F3\u8272");
759
+ };
760
+ try {
761
+ await audio.play();
762
+ } catch (e) {
763
+ URL.revokeObjectURL(url);
764
+ setNote(
765
+ e instanceof DOMException && e.name === "NotAllowedError" ? "\u6D4F\u89C8\u5668\u62E6\u622A\u4E86\u81EA\u52A8\u64AD\u653E\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u8BD5\u542C" : "\u8BD5\u542C\u5931\u8D25\uFF1A\u65E0\u6CD5\u64AD\u653E\u8BE5\u97F3\u8272"
766
+ );
767
+ }
768
+ } catch {
769
+ setNote("\u8BD5\u542C\u5931\u8D25\uFF1A\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u97F3\u8272\u540D\uFF08ShortName\uFF09\u662F\u5426\u6B63\u786E");
770
+ } finally {
771
+ setBusy(false);
772
+ }
773
+ })();
774
+ };
775
+ const btnStyle = {
776
+ font: "inherit",
777
+ display: "inline-flex",
778
+ alignItems: "center",
779
+ gap: 5,
780
+ alignSelf: "flex-start",
781
+ cursor: busy ? "default" : "pointer",
782
+ color: t.label,
783
+ background: "var(--dsw-alias-bg-layer-2)",
784
+ border: `1px solid ${t.border}`,
785
+ borderRadius: 6,
786
+ padding: "4px 10px",
787
+ fontSize: 12,
788
+ lineHeight: "18px"
789
+ };
790
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "flex", flexDirection: "column", gap: 4, alignItems: "flex-start" }, children: [
791
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", onClick: play, disabled: busy, style: btnStyle, title: "\u8BD5\u542C\u5F53\u524D\u97F3\u8272\uFF08\u5F53\u524D\u8BED\u901F\uFF09", children: [
792
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 16 16", width: 11, height: 11, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "currentColor", d: "M4 3l9 5-9 5z" }) }),
793
+ busy ? "\u5408\u6210\u4E2D\u2026" : "\u8BD5\u542C"
794
+ ] }),
795
+ note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-error-primary)", fontSize: 12, lineHeight: "18px" }, children: note })
796
+ ] });
797
+ }
798
+ function Row({ name, desc, children }) {
799
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: setRow, children: [
800
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: setLabelBox, children: [
801
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setLabel, children: name }),
802
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setHint, children: desc })
803
+ ] }),
804
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flexShrink: 0, maxWidth: 300 }, children })
805
+ ] });
806
+ }
807
+ function SegGroup({
808
+ score,
809
+ field,
810
+ value,
811
+ options
812
+ }) {
813
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { role: "group", style: setSeg, children: options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: setSegBtn(value === o.v), "aria-pressed": value === o.v, onClick: () => void score.set(field, o.v), children: o.label }, String(o.v))) });
814
+ }
815
+ function VoiceSettingsCard({ scope }) {
816
+ const [snap, setSnap] = (0, import_react.useState)(() => scope.getSnapshot());
817
+ const [collapsed, setCollapsed] = (0, import_react.useState)(true);
818
+ (0, import_react.useEffect)(
819
+ () => scope.subscribe(() => {
820
+ setSnap({ ...scope.getSnapshot() });
821
+ }),
822
+ [scope]
823
+ );
824
+ const value = snap?.value ?? {};
825
+ const unavailable = snap?.status === "unavailable" || snap?.status === "error";
826
+ if (unavailable) {
827
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-dshvm-settings": "card", style: { color: t.term, fontSize: 12, padding: "14px 16px", ...cardStyle }, children: [
828
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: "\u914D\u7F6E\u6682\u4E0D\u53EF\u7528" }),
829
+ "\uFF08\u8BBE\u7F6E\u6587\u6863\u672A\u5C31\u7EEA\uFF0C\u9762\u677F\u5C31\u7EEA\u540E\u4F1A\u81EA\u52A8\u51FA\u73B0\uFF09\u3002"
830
+ ] });
831
+ }
832
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-dshvm-settings": "card", style: cardStyle, children: [
833
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: focusVisibleCss }),
834
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", "aria-expanded": !collapsed, onClick: () => setCollapsed((c) => !c), style: { ...setHeader, background: collapsed ? "transparent" : t.bgOpen }, children: [
835
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: setHeadText, children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setName, children: "\u8BED\u97F3\u6A21\u5F0F" }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: setDesc, children: "\u97F3\u8272 / \u8BED\u901F / \u6253\u65AD\u7075\u654F\u5EA6 / \u9759\u97F3\u505C\u987F / \u7A7A\u95F2\u8D85\u65F6 / \u6A21\u578B\u955C\u50CF / \u81EA\u52A8\u53D1\u9001 / \u4EA4\u4E92\u6A21\u5F0F / \u5524\u9192\u8BCD / \u53E3\u8BED\u5316\u63D0\u793A\u8BCD" })
838
+ ] }),
839
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...setChevron, transform: collapsed ? "rotate(0deg)" : "rotate(180deg)" }, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 16 16", width: 14, height: 14, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "currentColor", d: "M4 6l4 4 4-4z" }) }) })
840
+ ] }),
841
+ !collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: setBody, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: 4 }, children: [
842
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "voice", desc: "Edge TTS \u97F3\u8272\uFF08\u4E0B\u62C9\u5E38\u7528\uFF0C\u5176\u4F59\u9009\u300C\u81EA\u5B9A\u4E49\u300D\u624B\u52A8\u586B ShortName\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
843
+ SelectField,
844
+ {
845
+ score: scope,
846
+ field: "voice",
847
+ value: value.voice ?? "",
848
+ options: VOICE_OPTIONS,
849
+ placeholder: "zh-CN-XiaoxiaoNeural",
850
+ footer: (v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VoicePreviewButton, { voice: v, rate: Number(value.rate ?? 1) })
851
+ }
852
+ ) }),
853
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "rate", desc: "\u6717\u8BFB\u8BED\u901F\u500D\u7387\uFF080.5 \u6162\u901F \uFF5E 2.0 \u5FEB\u901F\uFF0C1.0 \u6B63\u5E38\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "rate", value: value.rate ?? 1, min: 0.5, max: 2, step: 0.1 }) }),
854
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "interruptLevel", desc: "\u53D1\u58F0\u6253\u65AD\u7075\u654F\u5EA6\uFF080 \u9AD8\u95E8\u69DB / 1 \u4E2D / 2 \u4F4E\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
855
+ SegGroup,
856
+ {
857
+ score: scope,
858
+ field: "interruptLevel",
859
+ value: value.interruptLevel,
860
+ options: [
861
+ { v: 0, label: "0 \u9AD8\u95E8\u69DB" },
862
+ { v: 1, label: "1 \u4E2D" },
863
+ { v: 2, label: "2 \u4F4E" }
864
+ ]
865
+ }
866
+ ) }),
867
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "silenceMs", desc: "\u8BF4\u5B8C\u6574\u4E00\u53E5\u7684\u9759\u97F3\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u9ED8\u8BA4 2000 = 2 \u79D2\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "silenceMs", value: value.silenceMs ?? 2e3, min: 500, max: 3e4, step: 100 }) }),
868
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "idleTimeoutMinutes", desc: "\u65E0\u6D3B\u52A8\u81EA\u52A8\u9000\u51FA\u8BED\u97F3\u6A21\u5F0F\u7684\u5206\u949F\u6570\uFF08\u9ED8\u8BA4 10\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberField, { score: scope, field: "idleTimeoutMinutes", value: value.idleTimeoutMinutes ?? 10, min: 1, max: 120, step: 1 }) }),
869
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "modelHost", desc: "ASR \u6A21\u578B\u4E0B\u8F7D\u6E90\uFF08\u5B98\u65B9\u6E90 / \u56FD\u5185\u955C\u50CF\uFF0C\u6216\u9009\u300C\u81EA\u5B9A\u4E49\u300D\u586B\u4EFB\u610F\u955C\u50CF\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectField, { score: scope, field: "modelHost", value: value.modelHost ?? "", options: HOST_OPTIONS, placeholder: "https://..." }) }),
870
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "autoSend", desc: "\u8BC6\u522B\u5B9A\u7A3F\u540E\u81EA\u52A8\u53D1\u9001\uFF08\u5173=\u53EA\u8FDB\u8349\u7A3F\uFF1B\u6309\u4F4F Ctrl / hold \u677E\u624B\u4ECD\u53D1\u9001\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: Boolean(value.autoSend), onChange: (e) => void scope.set("autoSend", e.target.checked) }) }),
871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "spokenFormat", desc: "\u8BED\u97F3\u4F1A\u8BDD\u6CE8\u5165\u53E3\u8BED\u5316\u63D0\u793A\u8BCD\uFF08\u56DE\u590D\u53E3\u8BED\u5316\u3001\u4E0D\u7528 Markdown \u6392\u7248\u7B26\u53F7\uFF0C\u6717\u8BFB\u66F4\u987A\uFF1B\u9ED8\u8BA4\u5173\uFF0C\u6539\u52A8\u5373\u65F6\u751F\u6548\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: Boolean(value.spokenFormat), onChange: (e) => void scope.set("spokenFormat", e.target.checked) }) }),
872
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "mode", desc: "\u4EA4\u4E92\u6A21\u5F0F\uFF08toggle \u6301\u7EED\u8046\u542C+\u9759\u97F3\u65AD\u53E5 / hold \u6309\u4F4F\u8BF4\u8BDD\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
873
+ SegGroup,
874
+ {
875
+ score: scope,
876
+ field: "mode",
877
+ value: value.mode,
878
+ options: [
879
+ { v: "toggle", label: "\u6301\u7EED\u8046\u542C" },
880
+ { v: "hold", label: "\u6309\u4F4F\u8BF4\u8BDD" }
881
+ ]
882
+ }
883
+ ) }),
884
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { name: "wakeWord", desc: "\u5524\u9192\u8BCD\uFF08\u9ED8\u8BA4\u5173\uFF1B\u5982\u300C\u4F60\u597D\u5C0FD\u300D\uFF0C\u8BF4\u51FA\u540E\u5F00\u59CB\u8BC6\u522B\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextField, { score: scope, field: "wakeWord", value: value.wakeWord ?? "", placeholder: "\u5982\uFF1A\u4F60\u597D\u5C0FD" }) })
885
+ ] }) })
886
+ ] });
887
+ }
888
+
889
+ // src/client.tsx
890
+ var import_jsx_runtime2 = require("react/jsx-runtime");
891
+ var inject = ["slots", "sessions", "settingsScope"];
892
+ var WAVE_BARS = 14;
893
+ var BASE_PATH2 = "/voice-mode";
894
+ function apply(ctx) {
895
+ const bus = createVoiceBus(void 0, ctx);
896
+ ctx.slots.inject(
897
+ "conversation.input.right",
898
+ () => ctx.slots.register(
899
+ {
900
+ name: "conversation.input.right",
901
+ id: "voice-mode",
902
+ order: 80,
903
+ inject: () => ({ bus })
904
+ },
905
+ MicButton
906
+ )
907
+ );
908
+ ctx.slots.inject(
909
+ "conversation.input.dock",
910
+ () => ctx.slots.register(
911
+ {
912
+ name: "conversation.input.dock",
913
+ id: "voice-mode-status",
914
+ order: 10,
915
+ inject: () => ({ bus })
916
+ },
917
+ VoiceStatusBar
918
+ )
919
+ );
920
+ ctx.slots.inject(
921
+ "shell.overlay",
922
+ () => ctx.slots.register(
923
+ {
924
+ name: "shell.overlay",
925
+ id: "voice-mode-overlay",
926
+ order: 100,
927
+ inject: () => ({ bus })
928
+ },
929
+ VoiceOverlay
930
+ )
931
+ );
932
+ if (ctx.settingsScope) {
933
+ ctx.slots.inject(
934
+ "settings.plugin.item",
935
+ () => ctx.slots.register(
936
+ {
937
+ name: "settings.plugin.item",
938
+ key: "voice-mode",
939
+ order: 100,
940
+ label: "\u8BED\u97F3\u6A21\u5F0F"
941
+ },
942
+ () => React.createElement(VoiceSettingsCard, { scope: ctx.settingsScope.bind({ namespace: "voice-mode" }) })
943
+ )
944
+ );
945
+ }
946
+ }
947
+ function createAudioEngine(setUi) {
948
+ const queue = [];
949
+ const audio = new Audio();
950
+ const playNext = () => {
951
+ const frame = queue.shift();
952
+ if (!frame) {
953
+ setUi({ playing: false, playingCaption: null });
954
+ return;
955
+ }
956
+ const bin = atob(frame.audio);
957
+ const bytes = new Uint8Array(bin.length);
958
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
959
+ const url = URL.createObjectURL(new Blob([bytes], { type: "audio/mpeg" }));
960
+ audio.src = url;
961
+ audio.onended = () => {
962
+ URL.revokeObjectURL(url);
963
+ playNext();
964
+ };
965
+ audio.onerror = () => {
966
+ URL.revokeObjectURL(url);
967
+ playNext();
968
+ };
969
+ setUi({ playing: true, playingCaption: frame.text, ttsNotice: null });
970
+ void audio.play().catch(() => playNext());
971
+ };
972
+ let beepCtx = null;
973
+ const toolBeep = () => {
974
+ try {
975
+ if (!beepCtx) beepCtx = new AudioContext();
976
+ const osc = beepCtx.createOscillator();
977
+ const gain = beepCtx.createGain();
978
+ osc.frequency.value = 880;
979
+ gain.gain.setValueAtTime(0.08, beepCtx.currentTime);
980
+ gain.gain.exponentialRampToValueAtTime(1e-3, beepCtx.currentTime + 0.1);
981
+ osc.connect(gain);
982
+ gain.connect(beepCtx.destination);
983
+ osc.start();
984
+ osc.stop(beepCtx.currentTime + 0.1);
985
+ } catch {
986
+ }
987
+ };
988
+ return {
989
+ push(frame) {
990
+ queue.push(frame);
991
+ if (audio.paused) playNext();
992
+ },
993
+ skip() {
994
+ queue.length = 0;
995
+ audio.pause();
996
+ audio.onended = null;
997
+ audio.onerror = null;
998
+ setUi({ playing: false, playingCaption: null });
999
+ },
1000
+ toolBeep
1001
+ };
1002
+ }
1003
+ function createVoiceBus(basePath = BASE_PATH2, ctx) {
1004
+ let activeSessionId = null;
1005
+ const DEFAULT_BOOT = {
1006
+ basePath: BASE_PATH2,
1007
+ silenceMs: 2e3,
1008
+ interruptLevel: 0,
1009
+ idleTimeoutMinutes: 10,
1010
+ autoSend: true,
1011
+ mode: "toggle",
1012
+ wakeWord: ""
1013
+ };
1014
+ const ui = {
1015
+ state: "idle",
1016
+ partial: "",
1017
+ levels: [],
1018
+ error: null,
1019
+ playingCaption: null,
1020
+ playing: false,
1021
+ model: null,
1022
+ ttsNotice: null,
1023
+ boot: DEFAULT_BOOT,
1024
+ mode: "toggle",
1025
+ wakeWord: ""
1026
+ };
1027
+ const listeners = /* @__PURE__ */ new Set();
1028
+ const audioListeners = /* @__PURE__ */ new Set();
1029
+ const toolListeners = /* @__PURE__ */ new Set();
1030
+ let source = null;
1031
+ const engine = createAudioEngine((patch) => {
1032
+ Object.assign(ui, patch);
1033
+ notify();
1034
+ });
1035
+ const notify = () => {
1036
+ for (const fn of listeners) {
1037
+ try {
1038
+ fn({ active: activeSessionId, ui: { ...ui, levels: [...ui.levels] } });
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ };
1043
+ const connect = () => {
1044
+ if (source) return;
1045
+ source = new EventSource(`${location.origin}${basePath}/stream`);
1046
+ source.addEventListener("mode", (e) => {
1047
+ try {
1048
+ const active = JSON.parse(e.data).active ?? null;
1049
+ if (active !== activeSessionId) {
1050
+ activeSessionId = active;
1051
+ if (active !== null || ui.playing) engine.skip();
1052
+ notify();
1053
+ }
1054
+ } catch {
1055
+ }
1056
+ });
1057
+ source.addEventListener("audio", (e) => {
1058
+ try {
1059
+ const frame = JSON.parse(e.data);
1060
+ frame.sessionId = frame.sessionId ?? "";
1061
+ for (const fn of audioListeners) {
1062
+ try {
1063
+ fn(frame);
1064
+ } catch {
1065
+ }
1066
+ }
1067
+ } catch {
1068
+ }
1069
+ });
1070
+ source.addEventListener("tool", (e) => {
1071
+ try {
1072
+ const ev = JSON.parse(e.data);
1073
+ for (const fn of toolListeners) {
1074
+ try {
1075
+ fn(ev);
1076
+ } catch {
1077
+ }
1078
+ }
1079
+ } catch {
1080
+ }
1081
+ });
1082
+ source.addEventListener("asr-progress", (e) => {
1083
+ try {
1084
+ const p = JSON.parse(e.data);
1085
+ ui.model = { file: p.file ?? "", percent: p.percent ?? 0 };
1086
+ notify();
1087
+ } catch {
1088
+ }
1089
+ });
1090
+ source.addEventListener("asr-ready", () => {
1091
+ if (ui.model) {
1092
+ ui.model = null;
1093
+ notify();
1094
+ }
1095
+ });
1096
+ source.addEventListener("asr-error", (e) => {
1097
+ try {
1098
+ const p = JSON.parse(e.data);
1099
+ ui.error = `\u8BED\u97F3\u6A21\u578B\u4E0B\u8F7D\u5931\u8D25\uFF08${p.file ?? ""}\uFF09\uFF1A\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u65B0\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u91CD\u8BD5`;
1100
+ ui.model = null;
1101
+ notify();
1102
+ } catch {
1103
+ }
1104
+ });
1105
+ source.addEventListener("tts-error", (e) => {
1106
+ try {
1107
+ const p = JSON.parse(e.data);
1108
+ if (p.sessionId === activeSessionId) {
1109
+ ui.ttsNotice = "\u6717\u8BFB\u8FDE\u63A5\u5931\u8D25\uFF1A\u6B63\u5728\u91CD\u8BD5\u2026";
1110
+ notify();
1111
+ }
1112
+ } catch {
1113
+ }
1114
+ });
1115
+ };
1116
+ connect();
1117
+ audioListeners.add((frame) => {
1118
+ if (frame.sessionId === activeSessionId) engine.push(frame);
1119
+ });
1120
+ toolListeners.add(() => engine.toolBeep());
1121
+ return {
1122
+ get activeSessionId() {
1123
+ return activeSessionId;
1124
+ },
1125
+ ui,
1126
+ subscribe(fn) {
1127
+ listeners.add(fn);
1128
+ fn({ active: activeSessionId, ui: { ...ui, levels: [...ui.levels] } });
1129
+ return () => {
1130
+ listeners.delete(fn);
1131
+ };
1132
+ },
1133
+ setUi(patch) {
1134
+ Object.assign(ui, patch);
1135
+ notify();
1136
+ },
1137
+ async enter(sessionId) {
1138
+ try {
1139
+ const res = await fetch(`${location.origin}${basePath}/toggle`, {
1140
+ method: "POST",
1141
+ headers: { "content-type": "application/json" },
1142
+ body: JSON.stringify({ sessionId, on: true })
1143
+ });
1144
+ const out = await res.json();
1145
+ activeSessionId = out.active ?? null;
1146
+ notify();
1147
+ if (!res.ok) return { ok: false, error: out.error ?? "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25" };
1148
+ return { ok: out.active === sessionId, error: out.active === sessionId ? void 0 : "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25" };
1149
+ } catch {
1150
+ return { ok: false, error: "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25" };
1151
+ }
1152
+ },
1153
+ async exit(sessionId) {
1154
+ try {
1155
+ const res = await fetch(`${location.origin}${basePath}/toggle`, {
1156
+ method: "POST",
1157
+ headers: { "content-type": "application/json" },
1158
+ body: JSON.stringify({ sessionId, on: false })
1159
+ });
1160
+ const out = await res.json();
1161
+ activeSessionId = out.active ?? null;
1162
+ notify();
1163
+ } catch {
1164
+ }
1165
+ },
1166
+ onAudioFrame(fn) {
1167
+ audioListeners.add(fn);
1168
+ return () => {
1169
+ audioListeners.delete(fn);
1170
+ };
1171
+ },
1172
+ onToolEvent(fn) {
1173
+ toolListeners.add(fn);
1174
+ return () => {
1175
+ toolListeners.delete(fn);
1176
+ };
1177
+ },
1178
+ skipAudio() {
1179
+ engine.skip();
1180
+ },
1181
+ cancelTurn(sessionId) {
1182
+ try {
1183
+ ctx?.sessions?.binding?.(sessionId)?.session.cancel?.();
1184
+ } catch {
1185
+ }
1186
+ }
1187
+ };
1188
+ }
1189
+ var styleInjected = false;
1190
+ function useVoiceCss() {
1191
+ (0, import_react2.useEffect)(() => {
1192
+ if (styleInjected) return;
1193
+ styleInjected = true;
1194
+ const el = document.createElement("style");
1195
+ el.textContent = `
1196
+ @keyframes dshvm-fadein { from { opacity: 0; transform: translateY(4px) } to { opacity: 1; transform: none } }
1197
+ @keyframes dshvm-eq { 0%, 100% { transform: scaleY(0.35) } 50% { transform: scaleY(1) } }
1198
+ @keyframes dshvm-spin { to { transform: rotate(360deg) } }
1199
+ .dshvm-bar { width: 3px; border-radius: 99px; transition: height 0.08s linear, opacity 0.08s linear }
1200
+ `;
1201
+ document.head.appendChild(el);
1202
+ }, []);
1203
+ }
1204
+ function MicButton({
1205
+ bus,
1206
+ sessionId,
1207
+ useSession,
1208
+ useInput,
1209
+ inputActions
1210
+ }) {
1211
+ const [local, setLocal] = (0, import_react2.useState)("off");
1212
+ const localRef = (0, import_react2.useRef)("off");
1213
+ const sidRef = (0, import_react2.useRef)(sessionId);
1214
+ const engineRef = (0, import_react2.useRef)(null);
1215
+ const actionsRef = (0, import_react2.useRef)(inputActions);
1216
+ const submitTimerRef = (0, import_react2.useRef)(null);
1217
+ const idleTimerRef = (0, import_react2.useRef)(null);
1218
+ const runningRef = (0, import_react2.useRef)(false);
1219
+ const holdCtrlRef = (0, import_react2.useRef)(false);
1220
+ const bootNow = () => bus.ui.boot ?? { basePath: "/voice-mode", silenceMs: 2e3, interruptLevel: 0, idleTimeoutMinutes: 10, autoSend: true, mode: "toggle", wakeWord: "" };
1221
+ useVoiceCss();
1222
+ const [, bumpUi] = (0, import_react2.useState)(0);
1223
+ (0, import_react2.useEffect)(
1224
+ () => bus.subscribe(() => {
1225
+ bumpUi((t2) => t2 + 1);
1226
+ }),
1227
+ [bus]
1228
+ );
1229
+ const setLocalMode = (m) => {
1230
+ localRef.current = m;
1231
+ setLocal(m);
1232
+ };
1233
+ const fetchConfig = async () => {
1234
+ try {
1235
+ const res = await fetch(`${location.origin}${BASE_PATH2}/config`);
1236
+ if (!res.ok) return bootNow();
1237
+ const c = await res.json();
1238
+ const cur = bootNow();
1239
+ const next = {
1240
+ basePath: c.basePath ?? cur.basePath,
1241
+ silenceMs: c.silenceMs ?? cur.silenceMs,
1242
+ interruptLevel: c.interruptLevel ?? cur.interruptLevel,
1243
+ idleTimeoutMinutes: c.idleTimeoutMinutes ?? cur.idleTimeoutMinutes,
1244
+ autoSend: c.autoSend ?? cur.autoSend,
1245
+ mode: c.mode === "hold" ? "hold" : "toggle",
1246
+ wakeWord: c.wakeWord ?? cur.wakeWord
1247
+ };
1248
+ bus.setUi({ boot: next, mode: next.mode, wakeWord: next.wakeWord });
1249
+ return next;
1250
+ } catch {
1251
+ return bootNow();
1252
+ }
1253
+ };
1254
+ const clearIdle = () => {
1255
+ if (idleTimerRef.current) {
1256
+ clearTimeout(idleTimerRef.current);
1257
+ idleTimerRef.current = null;
1258
+ }
1259
+ };
1260
+ const resetIdle = () => {
1261
+ clearIdle();
1262
+ const idleMs = (bootNow().idleTimeoutMinutes > 0 ? bootNow().idleTimeoutMinutes : 10) * 60 * 1e3;
1263
+ idleTimerRef.current = setTimeout(() => {
1264
+ const sid = sidRef.current;
1265
+ if (localRef.current === "on" && sid) void exitModeRef.current("idle");
1266
+ }, idleMs);
1267
+ };
1268
+ (0, import_react2.useEffect)(() => {
1269
+ return bus.subscribe(() => {
1270
+ const sid = sidRef.current;
1271
+ if (localRef.current !== "on") return;
1272
+ if (bus.activeSessionId !== sid) {
1273
+ setLocalMode("off");
1274
+ clearIdle();
1275
+ void engineRef.current?.stop();
1276
+ engineRef.current = null;
1277
+ }
1278
+ });
1279
+ }, [bus]);
1280
+ const cancelPendingSubmit = () => {
1281
+ if (submitTimerRef.current) {
1282
+ clearTimeout(submitTimerRef.current);
1283
+ submitTimerRef.current = null;
1284
+ }
1285
+ };
1286
+ const exitMode = async (_reason) => {
1287
+ if (localRef.current === "off") return;
1288
+ setLocalMode("off");
1289
+ clearIdle();
1290
+ cancelPendingSubmit();
1291
+ const engine = engineRef.current;
1292
+ engineRef.current = null;
1293
+ if (engine) void engine.stop();
1294
+ bus.setUi({ state: "idle", partial: "", levels: [], error: null, model: null, ttsNotice: null });
1295
+ const sid = sidRef.current;
1296
+ if (sid) void bus.exit(sid);
1297
+ };
1298
+ const enterMode = async () => {
1299
+ const sid = sidRef.current;
1300
+ if (!sid || localRef.current !== "off") return;
1301
+ setLocalMode("pending");
1302
+ try {
1303
+ const entered = await bus.enter(sid);
1304
+ if (!entered.ok) {
1305
+ setLocalMode("off");
1306
+ bus.setUi({
1307
+ error: entered.error === "voice mode disabled" ? "\u8BED\u97F3\u6A21\u5F0F\u5DF2\u7981\u7528\uFF08\u63D2\u4EF6 enabled=false\uFF09" : entered.error ?? "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25"
1308
+ });
1309
+ return;
1310
+ }
1311
+ const cfg = await fetchConfig();
1312
+ const basePath = cfg.basePath;
1313
+ const silenceMs = cfg.silenceMs;
1314
+ const interruptLevel = cfg.interruptLevel;
1315
+ const engine = createAsrEngine({ silenceMs, interruptLevel, basePath, wakeWord: cfg.wakeWord }, sid);
1316
+ bus.setUi({ mode: cfg.mode, wakeWord: cfg.wakeWord });
1317
+ engineRef.current = engine;
1318
+ engine.onState((s) => {
1319
+ bus.setUi({ state: s });
1320
+ if (s === "idle") resetIdle();
1321
+ });
1322
+ engine.onLevel((l) => {
1323
+ const cur = bus.ui.levels;
1324
+ const next = cur.length < WAVE_BARS ? [...cur, l] : [...cur.slice(1), l];
1325
+ bus.setUi({ levels: next });
1326
+ });
1327
+ engine.onPartial((text) => bus.setUi({ partial: text }));
1328
+ engine.onSegment((text, meta) => {
1329
+ resetIdle();
1330
+ const actions = actionsRef.current;
1331
+ const trimmed = text.trim();
1332
+ if (!trimmed) return;
1333
+ try {
1334
+ const curText = draftRef.current;
1335
+ const nextDraft = curText ? `${curText} ${trimmed}` : trimmed;
1336
+ if (typeof actions?.setDraft === "function") actions.setDraft(nextDraft);
1337
+ else if (typeof actions?.setDraft === "function") actions.setDraft(nextDraft);
1338
+ else {
1339
+ }
1340
+ } catch {
1341
+ try {
1342
+ actions?.setDraft?.(trimmed);
1343
+ } catch {
1344
+ }
1345
+ }
1346
+ if (bootNow().autoSend === false && !meta?.force) return;
1347
+ const doSubmit = () => {
1348
+ try {
1349
+ const r = actions?.submit?.();
1350
+ if (r && typeof r.then === "function") {
1351
+ r.catch(() => {
1352
+ bus.setUi({ error: "\u53D1\u9001\u5931\u8D25\uFF0C\u5DF2\u4FDD\u7559\u5728\u8349\u7A3F" });
1353
+ });
1354
+ }
1355
+ } catch {
1356
+ bus.setUi({ error: "\u53D1\u9001\u5931\u8D25\uFF0C\u5DF2\u4FDD\u7559\u5728\u8349\u7A3F" });
1357
+ }
1358
+ };
1359
+ cancelPendingSubmit();
1360
+ doSubmit();
1361
+ submitTimerRef.current = setTimeout(() => {
1362
+ const phase = phaseRef.current;
1363
+ if (phase !== "submitting" && phase !== "adjudicating" && draftRef.current.trim()) doSubmit();
1364
+ }, 800);
1365
+ });
1366
+ engine.onSpeechStart(async () => {
1367
+ resetIdle();
1368
+ bus.skipAudio();
1369
+ try {
1370
+ await fetch(`${location.origin}${BASE_PATH2}/cancel`, {
1371
+ method: "POST",
1372
+ headers: { "content-type": "application/json" },
1373
+ body: JSON.stringify({ sessionId: sidRef.current })
1374
+ });
1375
+ } catch {
1376
+ }
1377
+ if (runningRef.current && sidRef.current) {
1378
+ bus.cancelTurn(sidRef.current);
1379
+ }
1380
+ bus.setUi({ partial: "\u2026" });
1381
+ });
1382
+ bus.setUi({ state: "idle", partial: "", levels: [], error: null, model: null, ttsNotice: null });
1383
+ await engine.start();
1384
+ setLocalMode("on");
1385
+ resetIdle();
1386
+ } catch (e) {
1387
+ setLocalMode("off");
1388
+ const msg = e instanceof DOMException ? e.name === "NotAllowedError" ? "\u9EA6\u514B\u98CE\u88AB\u62D2\u7EDD\uFF1A\u8BF7\u5728\u6D4F\u89C8\u5668\u5730\u5740\u680F\u5141\u8BB8\u9EA6\u514B\u98CE\u6743\u9650" : "\u9EA6\u514B\u98CE\u4E0D\u53EF\u7528" : `\u8BED\u97F3\u6A21\u5F0F\u542F\u52A8\u5931\u8D25\uFF1A${String(e instanceof Error ? e.message : e)}`;
1389
+ bus.setUi({ error: msg });
1390
+ const sid2 = sidRef.current;
1391
+ if (sid2) void bus.exit(sid2);
1392
+ }
1393
+ };
1394
+ const toggle = () => {
1395
+ if (localRef.current === "on") void exitModeRef.current("manual");
1396
+ else if (localRef.current === "off") void enterMode();
1397
+ };
1398
+ const toggleRef = (0, import_react2.useRef)(toggle);
1399
+ toggleRef.current = toggle;
1400
+ const exitModeRef = (0, import_react2.useRef)(exitMode);
1401
+ exitModeRef.current = exitMode;
1402
+ (0, import_react2.useEffect)(() => {
1403
+ actionsRef.current = inputActions;
1404
+ }, [inputActions]);
1405
+ (0, import_react2.useEffect)(() => {
1406
+ sidRef.current = sessionId;
1407
+ }, [sessionId]);
1408
+ const runningSel = useSession ? useSession((s) => s === void 0 ? void 0 : s.running) : void 0;
1409
+ (0, import_react2.useEffect)(() => {
1410
+ runningRef.current = runningSel === true;
1411
+ }, [runningSel]);
1412
+ (0, import_react2.useEffect)(() => {
1413
+ return () => {
1414
+ clearIdle();
1415
+ cancelPendingSubmit();
1416
+ const sid = sidRef.current;
1417
+ if (localRef.current === "on" && sid) {
1418
+ void engineRef.current?.stop();
1419
+ void fetch(`${location.origin}${BASE_PATH2}/toggle`, {
1420
+ method: "POST",
1421
+ headers: { "content-type": "application/json" },
1422
+ body: JSON.stringify({ sessionId: sid, on: false }),
1423
+ keepalive: true
1424
+ }).catch(() => {
1425
+ });
1426
+ }
1427
+ };
1428
+ }, []);
1429
+ (0, import_react2.useEffect)(() => {
1430
+ let ctrlTimer = null;
1431
+ const cancelCtrl = () => {
1432
+ if (ctrlTimer) {
1433
+ clearTimeout(ctrlTimer);
1434
+ ctrlTimer = null;
1435
+ }
1436
+ if (holdCtrlRef.current) {
1437
+ holdCtrlRef.current = false;
1438
+ engineRef.current?.endHeld(false);
1439
+ }
1440
+ };
1441
+ const onKeyDown = (e) => {
1442
+ if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "v" && !e.repeat) {
1443
+ e.preventDefault();
1444
+ cancelCtrl();
1445
+ toggleRef.current();
1446
+ return;
1447
+ }
1448
+ const eng = engineRef.current;
1449
+ if (e.key !== "Control" || e.shiftKey || e.altKey || e.metaKey || e.repeat || !eng) return;
1450
+ if (bootNow().mode === "hold") {
1451
+ ctrlTimer = setTimeout(() => {
1452
+ ctrlTimer = null;
1453
+ holdCtrlRef.current = true;
1454
+ eng.beginHeld();
1455
+ }, 600);
1456
+ } else {
1457
+ eng.forceSend();
1458
+ }
1459
+ };
1460
+ const onKeyUp = (e) => {
1461
+ if (e.key === "Control") cancelCtrl();
1462
+ };
1463
+ const onBlur = () => {
1464
+ cancelCtrl();
1465
+ if (localRef.current === "on" && bootNow().mode === "hold") engineRef.current?.endHeld(true);
1466
+ };
1467
+ window.addEventListener("keydown", onKeyDown);
1468
+ window.addEventListener("keyup", onKeyUp);
1469
+ window.addEventListener("blur", onBlur);
1470
+ return () => {
1471
+ window.removeEventListener("keydown", onKeyDown);
1472
+ window.removeEventListener("keyup", onKeyUp);
1473
+ window.removeEventListener("blur", onBlur);
1474
+ cancelCtrl();
1475
+ };
1476
+ }, []);
1477
+ (0, import_react2.useEffect)(() => {
1478
+ const onInput = (e) => {
1479
+ const t2 = e.target;
1480
+ if (!(t2 instanceof HTMLTextAreaElement)) return;
1481
+ if (localRef.current !== "on") return;
1482
+ void exitModeRef.current("typing");
1483
+ };
1484
+ window.addEventListener("input", onInput, true);
1485
+ return () => window.removeEventListener("input", onInput, true);
1486
+ }, []);
1487
+ (0, import_react2.useEffect)(() => {
1488
+ const onKeyDown = (e) => {
1489
+ if (e.key !== "Escape") return;
1490
+ if (localRef.current !== "on" || bootNow().mode !== "hold") return;
1491
+ engineRef.current?.endHeld(true);
1492
+ holdCtrlRef.current = false;
1493
+ bus.setUi({ partial: "" });
1494
+ };
1495
+ const onVisibility = () => {
1496
+ if (document.hidden && bootNow().mode === "hold") {
1497
+ engineRef.current?.endHeld(true);
1498
+ holdCtrlRef.current = false;
1499
+ }
1500
+ };
1501
+ window.addEventListener("keydown", onKeyDown);
1502
+ document.addEventListener("visibilitychange", onVisibility);
1503
+ return () => {
1504
+ window.removeEventListener("keydown", onKeyDown);
1505
+ document.removeEventListener("visibilitychange", onVisibility);
1506
+ };
1507
+ }, [bus]);
1508
+ (0, import_react2.useEffect)(() => {
1509
+ return bus.subscribe(() => {
1510
+ const sid = sidRef.current;
1511
+ if (localRef.current === "pending" || localRef.current === "on") {
1512
+ if (bus.activeSessionId === sid) {
1513
+ setLocalMode("on");
1514
+ } else if (localRef.current === "pending") {
1515
+ setLocalMode("off");
1516
+ }
1517
+ }
1518
+ });
1519
+ }, [bus]);
1520
+ const on = local === "on";
1521
+ const busy = bus.ui.state === "transcribing" || bus.ui.state === "loading-model";
1522
+ const holdMode = bootNow().mode === "hold";
1523
+ const liveDraft = useInput ? useInput((s) => s?.draft ?? "") : "";
1524
+ const draftRef = (0, import_react2.useRef)("");
1525
+ draftRef.current = liveDraft;
1526
+ const livePhase = useInput ? useInput((s) => s?.phase ?? "") : "";
1527
+ const phaseRef = (0, import_react2.useRef)("");
1528
+ phaseRef.current = livePhase;
1529
+ const label = on ? busy ? "\u8BC6\u522B\u4E2D\u2026" : holdMode ? "\u6309\u4F4F\u8BF4\u8BDD" : "\u8BED\u97F3\u4E2D" : local === "pending" ? "\u8FDB\u5165\u4E2D\u2026" : "\u8BED\u97F3";
1530
+ const holdPtrRef = (0, import_react2.useRef)(null);
1531
+ const onPointerDown = (e) => {
1532
+ if (bootNow().mode !== "hold") return;
1533
+ holdPtrRef.current = { t: Date.now(), y: e.clientY, id: e.pointerId };
1534
+ e.currentTarget.setPointerCapture?.(e.pointerId);
1535
+ if (localRef.current === "on") engineRef.current?.beginHeld();
1536
+ };
1537
+ const onPointerMove = (e) => {
1538
+ const p = holdPtrRef.current;
1539
+ if (!p || p.id !== e.pointerId) return;
1540
+ if (p.y - e.clientY >= 40) {
1541
+ holdPtrRef.current = null;
1542
+ engineRef.current?.endHeld(true);
1543
+ bus.setUi({ partial: "" });
1544
+ }
1545
+ };
1546
+ const onPointerUp = (e) => {
1547
+ const p = holdPtrRef.current;
1548
+ holdPtrRef.current = null;
1549
+ if (!p || p.id !== e.pointerId) return;
1550
+ const ms = Date.now() - p.t;
1551
+ if (ms < 250) {
1552
+ if (localRef.current === "on") {
1553
+ engineRef.current?.endHeld(true);
1554
+ void exitModeRef.current("manual");
1555
+ } else {
1556
+ void enterMode();
1557
+ }
1558
+ return;
1559
+ }
1560
+ if (localRef.current === "on") engineRef.current?.endHeld(false);
1561
+ };
1562
+ const onPointerCancel = () => {
1563
+ holdPtrRef.current = null;
1564
+ engineRef.current?.endHeld(true);
1565
+ };
1566
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1567
+ "button",
1568
+ {
1569
+ onClick: (e) => {
1570
+ if (holdMode) {
1571
+ if (e.detail !== 0) return;
1572
+ }
1573
+ toggle();
1574
+ },
1575
+ onPointerDown,
1576
+ onPointerMove,
1577
+ onPointerUp,
1578
+ onPointerCancel,
1579
+ "data-dshvm": "mic",
1580
+ "aria-label": on ? "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D" : "\u8FDB\u5165\u8BED\u97F3\u5BF9\u8BDD\u6A21\u5F0F",
1581
+ "aria-pressed": on,
1582
+ title: on ? holdMode ? "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D \xB7 \u6309\u4F4F\u8BF4\u8BDD\u3001\u677E\u624B\u53D1\u9001\uFF1B\u77ED\u6309\u9000\u51FA\uFF1BEsc/\u5931\u53BB\u7126\u70B9\u653E\u5F03\uFF1BCtrl+Shift+V \u9000\u51FA" : "\u8BED\u97F3\u6A21\u5F0F\u8FDB\u884C\u4E2D \xB7 \u70B9\u51FB\u9000\u51FA\uFF08Ctrl+Shift+V\uFF09\xB7 \u6309\u4F4F Ctrl \u7ACB\u5373\u53D1\u9001" : "\u8FDB\u5165\u8BED\u97F3\u5BF9\u8BDD\u6A21\u5F0F\uFF08Ctrl+Shift+V\uFF09",
1583
+ style: {
1584
+ border: "none",
1585
+ background: on ? holdMode ? "rgba(88, 166, 255, 0.16)" : "rgba(63, 185, 80, 0.16)" : local === "pending" ? "rgba(88, 166, 255, 0.14)" : "transparent",
1586
+ cursor: "pointer",
1587
+ padding: "4px 8px",
1588
+ borderRadius: 8,
1589
+ display: "flex",
1590
+ alignItems: "center",
1591
+ gap: 6,
1592
+ fontSize: 11,
1593
+ fontFamily: "system-ui, sans-serif",
1594
+ color: on ? holdMode ? "#58a6ff" : "#3fb950" : local === "pending" ? "#58a6ff" : "#8b949e",
1595
+ transition: "background 0.15s ease, color 0.2s ease",
1596
+ touchAction: "none",
1597
+ // 触摸设备上让 pointer 事件独占(滑出取消可用)
1598
+ userSelect: "none"
1599
+ },
1600
+ children: [
1601
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { viewBox: "0 0 24 24", width: 14, height: 14, "aria-hidden": "true", children: [
1602
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1603
+ "path",
1604
+ {
1605
+ fill: "currentColor",
1606
+ d: "M12 14a3 3 0 0 0 3-3V5a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z"
1607
+ }
1608
+ ),
1609
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1610
+ "path",
1611
+ {
1612
+ fill: "currentColor",
1613
+ d: "M17.3 11a.9.9 0 0 0-1.8 0 3.5 3.5 0 0 1-7 0 .9.9 0 0 0-1.8 0 5.3 5.3 0 0 0 4.4 5.2v1.9h-1.7a.9.9 0 0 0 0 1.8h5.2a.9.9 0 0 0 0-1.8h-1.7v-1.9A5.3 5.3 0 0 0 17.3 11Z"
1614
+ }
1615
+ )
1616
+ ] }),
1617
+ label
1618
+ ]
1619
+ }
1620
+ );
1621
+ }
1622
+ function VoiceStatusBar({ bus, sessionId }) {
1623
+ const [b, setB] = (0, import_react2.useState)(() => ({ active: bus.activeSessionId, ui: bus.ui }));
1624
+ (0, import_react2.useEffect)(() => {
1625
+ return bus.subscribe(setB);
1626
+ }, [bus]);
1627
+ const isActive = b.active === sessionId;
1628
+ if (!isActive) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, {});
1629
+ const stateText = b.ui.state === "loading-model" ? "\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u2026" : b.ui.state === "transcribing" ? "\u8BC6\u522B\u4E2D\u2026" : b.ui.state === "speech" ? b.ui.mode === "hold" ? "\u6309\u4F4F\u8BF4\u8BDD\u2026" : "\u8046\u542C\u4E2D\u2026" : b.ui.state === "wake" ? `\u8BF4\u300C${b.ui.wakeWord || "\u5524\u9192\u8BCD"}\u300D\u5F00\u59CB` : b.ui.mode === "hold" ? "\u8BED\u97F3\u6A21\u5F0F \xB7 \u6309\u4F4F\u8BF4\u8BDD\uFF08\u77ED\u6309\u9000\u51FA\uFF09" : "\u8BED\u97F3\u6A21\u5F0F \xB7 \u8046\u542C\u4E2D\u2026";
1630
+ const bars = Array.from({ length: WAVE_BARS }, (_, i) => b.ui.levels[i] ?? 0);
1631
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1632
+ "div",
1633
+ {
1634
+ style: {
1635
+ display: "flex",
1636
+ alignItems: "center",
1637
+ gap: 8,
1638
+ padding: "6px 12px",
1639
+ borderRadius: 10,
1640
+ fontSize: 12,
1641
+ fontFamily: "system-ui, sans-serif",
1642
+ color: "#3fb950",
1643
+ background: "rgba(63, 185, 80, 0.08)",
1644
+ border: "1px solid rgba(63, 185, 80, 0.25)",
1645
+ animation: "dshvm-fadein 0.2s ease"
1646
+ },
1647
+ children: [
1648
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { display: "inline-flex", alignItems: "flex-end", gap: 2, height: 14, flexShrink: 0 }, children: bars.map((v, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1649
+ "span",
1650
+ {
1651
+ className: "dshvm-bar",
1652
+ style: {
1653
+ height: `${3 + v * 12}px`,
1654
+ background: "#3fb950",
1655
+ opacity: 0.4 + v * 0.6
1656
+ }
1657
+ },
1658
+ i
1659
+ )) }),
1660
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flexGrow: 1 }, children: b.ui.error ? b.ui.error : b.ui.state === "loading-model" || b.ui.model ? b.ui.model ? `\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u2026 ${b.ui.model.file} ${b.ui.model.percent}%` : stateText : b.ui.partial ? b.ui.partial : b.ui.ttsNotice ? b.ui.ttsNotice : stateText }),
1661
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1662
+ "button",
1663
+ {
1664
+ onClick: () => {
1665
+ void bus.exit(sessionId);
1666
+ },
1667
+ style: {
1668
+ border: "none",
1669
+ background: "transparent",
1670
+ color: "#8b949e",
1671
+ cursor: "pointer",
1672
+ fontSize: 12,
1673
+ flexShrink: 0
1674
+ },
1675
+ children: "\u9000\u51FA"
1676
+ }
1677
+ )
1678
+ ]
1679
+ }
1680
+ );
1681
+ }
1682
+ function VoiceOverlay({ bus }) {
1683
+ const [b, setB] = (0, import_react2.useState)(() => ({ active: bus.activeSessionId, ui: bus.ui }));
1684
+ (0, import_react2.useEffect)(() => {
1685
+ return bus.subscribe(setB);
1686
+ }, [bus]);
1687
+ if (!b.ui.playing) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, {});
1688
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1689
+ "div",
1690
+ {
1691
+ style: {
1692
+ position: "fixed",
1693
+ right: 16,
1694
+ bottom: 16,
1695
+ zIndex: 9999,
1696
+ display: "flex",
1697
+ alignItems: "center",
1698
+ gap: 10,
1699
+ padding: "8px 14px",
1700
+ borderRadius: 999,
1701
+ fontSize: 12,
1702
+ fontFamily: "system-ui, sans-serif",
1703
+ pointerEvents: "auto",
1704
+ background: "rgba(22, 24, 28, 0.85)",
1705
+ backdropFilter: "blur(14px)",
1706
+ WebkitBackdropFilter: "blur(14px)",
1707
+ border: "1px solid rgba(255, 255, 255, 0.08)",
1708
+ boxShadow: "0 8px 28px rgba(0, 0, 0, 0.4)",
1709
+ color: "#e6e8eb",
1710
+ maxWidth: 480,
1711
+ animation: "dshvm-fadein 0.25s ease"
1712
+ },
1713
+ children: [
1714
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { display: "inline-flex", alignItems: "flex-end", gap: 2, height: 12, flexShrink: 0 }, children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1715
+ "span",
1716
+ {
1717
+ style: {
1718
+ width: 3,
1719
+ height: "100%",
1720
+ borderRadius: 99,
1721
+ background: "#2ea043",
1722
+ transformOrigin: "bottom",
1723
+ animation: `dshvm-eq 0.85s ease-in-out ${i * 0.18}s infinite`
1724
+ }
1725
+ },
1726
+ i
1727
+ )) }),
1728
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: b.ui.playingCaption ?? "\u6717\u8BFB\u4E2D\u2026" }, b.ui.playingCaption ?? "idle"),
1729
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1730
+ "button",
1731
+ {
1732
+ onClick: () => bus.skipAudio(),
1733
+ style: {
1734
+ border: "none",
1735
+ background: "rgba(255, 255, 255, 0.14)",
1736
+ color: "#fff",
1737
+ borderRadius: 999,
1738
+ padding: "3px 12px",
1739
+ fontSize: 11,
1740
+ cursor: "pointer",
1741
+ flexShrink: 0
1742
+ },
1743
+ children: "\u8DF3\u8FC7"
1744
+ }
1745
+ )
1746
+ ]
1747
+ }
1748
+ );
1749
+ }
1750
+ return module.exports; } });