@allmodels/dsh-speech 0.1.1 → 0.1.3
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 +49 -9
- package/lib/client.js +1642 -159
- package/lib/index.d.ts +23 -2
- package/lib/index.js +685 -35
- package/package.json +9 -2
package/lib/client.js
CHANGED
|
@@ -216,9 +216,47 @@ async function request(path, init) {
|
|
|
216
216
|
}
|
|
217
217
|
return body;
|
|
218
218
|
}
|
|
219
|
+
async function audioRequest(input, signal) {
|
|
220
|
+
const response = await fetch("/api/dsh-speech/tts", {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: {
|
|
223
|
+
accept: "audio/mpeg",
|
|
224
|
+
"content-type": "application/json"
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify(input),
|
|
227
|
+
...signal === void 0 ? {} : { signal }
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
const body = await response.json().catch(() => void 0);
|
|
231
|
+
const error = body !== null && typeof body === "object" ? body.error : void 0;
|
|
232
|
+
throw new SpeechApiError(typeof error?.code === "string" ? error.code : `HTTP_${String(response.status)}`, typeof error?.message === "string" ? error.message : "Speech generation failed");
|
|
233
|
+
}
|
|
234
|
+
const blob = await response.blob();
|
|
235
|
+
if (blob.size === 0 || blob.size > 16 * 1024 * 1024) throw new SpeechApiError("INVALID_AUDIO", "The spoken summary audio is invalid");
|
|
236
|
+
return blob.type === "audio/mpeg" ? blob : blob.slice(0, blob.size, "audio/mpeg");
|
|
237
|
+
}
|
|
219
238
|
const speechApi = {
|
|
220
239
|
status: () => request("/api/dsh-speech/status"),
|
|
221
240
|
catalog: (refresh = false) => request(`/api/dsh-speech/catalog${refresh ? "?refresh=1" : ""}`),
|
|
241
|
+
voices: (filters = {}, signal) => {
|
|
242
|
+
const query = new URLSearchParams();
|
|
243
|
+
if (filters.model !== void 0) query.set("model", filters.model);
|
|
244
|
+
if (filters.provider !== void 0) query.set("provider", filters.provider);
|
|
245
|
+
if (filters.q !== void 0) query.set("q", filters.q);
|
|
246
|
+
if (filters.language !== void 0) query.set("language", filters.language);
|
|
247
|
+
return request(`/api/dsh-speech/voices?${query.toString()}`, signal === void 0 ? void 0 : { signal });
|
|
248
|
+
},
|
|
249
|
+
summarize: (input, signal) => request("/api/dsh-speech/summarize", {
|
|
250
|
+
method: "POST",
|
|
251
|
+
body: JSON.stringify(input),
|
|
252
|
+
...signal === void 0 ? {} : { signal }
|
|
253
|
+
}),
|
|
254
|
+
tts: audioRequest,
|
|
255
|
+
prepareTts: (input, signal) => request("/api/dsh-speech/tts/prepare", {
|
|
256
|
+
method: "POST",
|
|
257
|
+
body: JSON.stringify(input),
|
|
258
|
+
...signal === void 0 ? {} : { signal }
|
|
259
|
+
}),
|
|
222
260
|
startAuth: (email) => request("/api/dsh-speech/auth/start", {
|
|
223
261
|
method: "POST",
|
|
224
262
|
body: JSON.stringify({ email })
|
|
@@ -265,6 +303,42 @@ function writePreferredMicrophone(deviceId) {
|
|
|
265
303
|
//#region src/shared.ts
|
|
266
304
|
const CATALOG_TTL_MS = 300 * 1e3;
|
|
267
305
|
const AUDIO_FORMAT = "pcm_16000";
|
|
306
|
+
const MAX_SUMMARY_REQUEST_CHARACTERS = 16e3;
|
|
307
|
+
const MAX_SUMMARY_ANSWER_CHARACTERS = 64e3;
|
|
308
|
+
const DEFAULT_ENGLISH_TTS = {
|
|
309
|
+
model: "fish/s2.1-pro",
|
|
310
|
+
provider: "fish",
|
|
311
|
+
voice: "03397b4c4be74759b72533b663fbd001",
|
|
312
|
+
name: "Elon Musk (Noise reduction)"
|
|
313
|
+
};
|
|
314
|
+
const DEFAULT_CHINESE_TTS = {
|
|
315
|
+
model: "minimax/speech-2.8-hd",
|
|
316
|
+
provider: "minimax",
|
|
317
|
+
voice: "Chinese (Mandarin)_HK_Flight_Attendant",
|
|
318
|
+
name: "HK Flight Attendant"
|
|
319
|
+
};
|
|
320
|
+
function selectTtsBinding(bindings, preferred) {
|
|
321
|
+
if (preferred?.model !== void 0) {
|
|
322
|
+
const preferredModel = preferred.model;
|
|
323
|
+
const matchesModel = (binding) => binding.model === preferredModel || binding.canonical === preferredModel || !preferredModel.includes("/") && binding.model.endsWith(`/${preferredModel}`);
|
|
324
|
+
const exact = bindings.find((binding) => matchesModel(binding) && (preferred.provider === void 0 || binding.provider === preferred.provider));
|
|
325
|
+
if (exact !== void 0) return exact;
|
|
326
|
+
const sameModel = bindings.find(matchesModel);
|
|
327
|
+
if (sameModel !== void 0) return sameModel;
|
|
328
|
+
}
|
|
329
|
+
return bindings.find((binding) => binding.isProviderDefault) ?? bindings[0];
|
|
330
|
+
}
|
|
331
|
+
function preferredTtsSelection(locale) {
|
|
332
|
+
return locale.toLowerCase().startsWith("zh") ? DEFAULT_CHINESE_TTS : DEFAULT_ENGLISH_TTS;
|
|
333
|
+
}
|
|
334
|
+
function selectLocalizedTtsBinding(bindings, locale, preferred) {
|
|
335
|
+
if (preferred?.model !== void 0 || preferred?.provider !== void 0) return selectTtsBinding(bindings, preferred);
|
|
336
|
+
const localized = preferredTtsSelection(locale);
|
|
337
|
+
return selectTtsBinding(bindings, {
|
|
338
|
+
model: localized.model,
|
|
339
|
+
provider: localized.provider
|
|
340
|
+
});
|
|
341
|
+
}
|
|
268
342
|
function selectBinding(bindings, locale, preferred) {
|
|
269
343
|
if (preferred?.model !== void 0) {
|
|
270
344
|
const preferredModel = preferred.model;
|
|
@@ -330,7 +404,8 @@ var SpeechController = class {
|
|
|
330
404
|
amplitude: 0,
|
|
331
405
|
metadataLoading: false,
|
|
332
406
|
microphones: [],
|
|
333
|
-
switchingMicrophone: false
|
|
407
|
+
switchingMicrophone: false,
|
|
408
|
+
hasTranscript: false
|
|
334
409
|
};
|
|
335
410
|
listeners = /* @__PURE__ */ new Set();
|
|
336
411
|
blocks;
|
|
@@ -408,6 +483,12 @@ var SpeechController = class {
|
|
|
408
483
|
}, 5e3);
|
|
409
484
|
await stoppingCapture;
|
|
410
485
|
}
|
|
486
|
+
async stopAndSend() {
|
|
487
|
+
const active = this.active;
|
|
488
|
+
if (active === void 0 || active.finished || !this.snapshot.hasTranscript) return;
|
|
489
|
+
active.sendAfterFinish = true;
|
|
490
|
+
await this.stop();
|
|
491
|
+
}
|
|
411
492
|
cancel() {
|
|
412
493
|
const active = this.active;
|
|
413
494
|
if (active === void 0 || active.finished) return;
|
|
@@ -473,7 +554,8 @@ var SpeechController = class {
|
|
|
473
554
|
microphones: [],
|
|
474
555
|
activeMicrophoneId: void 0,
|
|
475
556
|
switchingMicrophone: false,
|
|
476
|
-
microphoneError: void 0
|
|
557
|
+
microphoneError: void 0,
|
|
558
|
+
hasTranscript: false
|
|
477
559
|
});
|
|
478
560
|
try {
|
|
479
561
|
await new Promise((resolve, reject) => {
|
|
@@ -555,6 +637,7 @@ var SpeechController = class {
|
|
|
555
637
|
...typeof message.languageCode === "string" ? { languageCode: message.languageCode } : {}
|
|
556
638
|
});
|
|
557
639
|
active.inputActions.setDraft(transcriptText(active.transcript));
|
|
640
|
+
if (message.text.trim().length > 0 && !this.snapshot.hasTranscript) this.publish({ hasTranscript: true });
|
|
558
641
|
if (message.type === "final" && this.snapshot.phase === "finalizing") {
|
|
559
642
|
if (active.finalTimer !== void 0) clearTimeout(active.finalTimer);
|
|
560
643
|
active.finalTimer = setTimeout(() => {
|
|
@@ -595,8 +678,10 @@ var SpeechController = class {
|
|
|
595
678
|
activeMicrophoneId: void 0,
|
|
596
679
|
switchingMicrophone: false,
|
|
597
680
|
microphoneError: void 0,
|
|
681
|
+
hasTranscript: false,
|
|
598
682
|
...error === void 0 ? { error: void 0 } : { error }
|
|
599
683
|
});
|
|
684
|
+
if (error === void 0 && active.sendAfterFinish === true) active.inputActions.submit();
|
|
600
685
|
this.ensureMetadata(true);
|
|
601
686
|
}
|
|
602
687
|
publish(patch) {
|
|
@@ -667,7 +752,9 @@ const styles = {
|
|
|
667
752
|
recordingTrack: "dsh-speech-recording-track",
|
|
668
753
|
recordingCanvas: "dsh-speech-recording-canvas",
|
|
669
754
|
recordingCancel: "dsh-speech-recording-cancel",
|
|
755
|
+
recordingActions: "dsh-speech-recording-actions",
|
|
670
756
|
recordingStop: "dsh-speech-recording-stop",
|
|
757
|
+
recordingSend: "dsh-speech-recording-send",
|
|
671
758
|
recordingProgress: "dsh-speech-recording-progress",
|
|
672
759
|
deviceDock: "dsh-speech-device-dock",
|
|
673
760
|
deviceMarker: "dsh-speech-device-marker",
|
|
@@ -683,7 +770,21 @@ const styles = {
|
|
|
683
770
|
srOnly: "dsh-speech-sr-only",
|
|
684
771
|
dock: "dsh-speech-dock",
|
|
685
772
|
dockError: "dsh-speech-dock-error",
|
|
686
|
-
dockDetail: "dsh-speech-dock-detail"
|
|
773
|
+
dockDetail: "dsh-speech-dock-detail",
|
|
774
|
+
switchLabel: "dsh-speech-switch-label",
|
|
775
|
+
switchTrack: "dsh-speech-switch-track",
|
|
776
|
+
summaryPlayer: "dsh-speech-summary-player",
|
|
777
|
+
summaryControl: "dsh-speech-summary-control",
|
|
778
|
+
summaryButton: "dsh-speech-summary-button",
|
|
779
|
+
summaryWaveform: "dsh-speech-summary-waveform",
|
|
780
|
+
summaryLabel: "dsh-speech-summary-label",
|
|
781
|
+
autoplayToggle: "dsh-speech-autoplay-toggle",
|
|
782
|
+
summaryToggles: "dsh-speech-summary-toggles",
|
|
783
|
+
voicePicker: "dsh-speech-voice-picker",
|
|
784
|
+
voiceSelected: "dsh-speech-voice-selected",
|
|
785
|
+
voiceMenu: "dsh-speech-voice-menu",
|
|
786
|
+
voiceOption: "dsh-speech-voice-option",
|
|
787
|
+
voiceNotice: "dsh-speech-voice-notice"
|
|
687
788
|
};
|
|
688
789
|
const STYLE_TEXT = String.raw`
|
|
689
790
|
.dsh-speech-settings{display:grid;gap:16px;max-width:760px;padding:4px 2px 24px;color:var(--color-text,#e8e8e8)}
|
|
@@ -696,15 +797,19 @@ const STYLE_TEXT = String.raw`
|
|
|
696
797
|
.dsh-speech-model-full{grid-column:1/-1}.dsh-speech-context-details{border-top:1px solid var(--color-border,#343434);padding-top:12px}.dsh-speech-context-summary{width:max-content;color:var(--color-text-secondary,#aaa);cursor:pointer;font-size:13px;font-weight:600}.dsh-speech-context-summary:hover{color:var(--color-text,#eee)}.dsh-speech-context-summary span{margin-left:5px;color:var(--color-text-secondary,#888);font-size:11px;font-weight:400}.dsh-speech-context-body{display:grid;gap:8px;padding-top:12px}.dsh-speech-balance-value{font-size:24px;line-height:1.2;color:var(--color-text,#eee)}
|
|
697
798
|
.dsh-speech-stack,.dsh-speech-field{display:grid;gap:7px;align-content:start}.dsh-speech-label{color:var(--color-text-secondary,#b2b2b2);font-size:13px;font-weight:600}.dsh-speech-hint,.dsh-speech-muted{margin:0;color:var(--color-text-secondary,#999);font-size:12px;line-height:1.45}
|
|
698
799
|
.dsh-speech-input,.dsh-speech-textarea{width:100%;box-sizing:border-box;border:1px solid var(--color-border,#444);border-radius:8px;padding:9px 10px;background:var(--color-background,#191919);color:inherit;font:inherit}.dsh-speech-textarea{min-height:84px;resize:vertical}.dsh-speech-input:focus,.dsh-speech-textarea:focus{outline:2px solid color-mix(in srgb,#68a8ff 65%,transparent);outline-offset:1px}.dsh-speech-input:disabled,.dsh-speech-textarea:disabled{opacity:.55}
|
|
800
|
+
.dsh-speech-voice-picker{display:grid;gap:7px;align-content:start;min-width:0}.dsh-speech-voice-selected{display:flex;align-items:baseline;justify-content:space-between;gap:10px;min-width:0;padding:8px 10px;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#68a8ff) 28%,var(--color-border,#444));border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#68a8ff) 7%,transparent)}.dsh-speech-voice-selected strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.dsh-speech-voice-selected span{flex:none;color:var(--color-text-secondary,#999);font-size:11px}.dsh-speech-voice-menu{max-height:300px;overflow:auto;display:grid;gap:2px;padding:4px;border:1px solid var(--color-border,#444);border-radius:9px;background:var(--color-background-elevated,#202020);box-shadow:0 10px 26px rgba(0,0,0,.22)}.dsh-speech-voice-option{width:100%;min-width:0;display:grid;gap:4px;padding:8px 9px;border:0;border-radius:7px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsh-speech-voice-option>span{display:flex;align-items:baseline;justify-content:space-between;gap:12px;min-width:0}.dsh-speech-voice-option strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.dsh-speech-voice-option small{color:var(--color-text-secondary,#999);font-size:11px;line-height:1.35}.dsh-speech-voice-option>span small{flex:none}.dsh-speech-voice-option:hover,.dsh-speech-voice-option:focus-visible,.dsh-speech-voice-option[data-active="true"]{outline:0;background:var(--color-background-hover,rgba(255,255,255,.08))}.dsh-speech-voice-option[aria-selected="true"]{box-shadow:inset 2px 0 var(--dsw-alias-brand-primary,#68a8ff)}.dsh-speech-voice-notice{padding:14px 10px;color:var(--color-text-secondary,#999);font-size:12px;text-align:center}
|
|
699
801
|
.dsh-speech-primary,.dsh-speech-secondary,.dsh-speech-checkout{border:1px solid transparent;border-radius:8px;padding:8px 12px;font:inherit;font-size:13px;font-weight:600;cursor:pointer;text-decoration:none;text-align:center}.dsh-speech-primary{background:#e8e8e8;color:#151515}.dsh-speech-secondary,.dsh-speech-checkout{border-color:var(--color-border,#4a4a4a);background:transparent;color:inherit}.dsh-speech-primary:disabled,.dsh-speech-secondary:disabled{cursor:default;opacity:.45}.dsh-speech-good{color:#65c88a}.dsh-speech-warning{margin:0;color:#efb85c}.dsh-speech-danger,.dsh-speech-dock-error{margin:0;color:#f07878}
|
|
700
802
|
.dsh-speech-top-up{justify-content:flex-start;align-items:end;flex-wrap:wrap}.dsh-speech-top-up .dsh-speech-field{max-width:180px}
|
|
803
|
+
.dsh-speech-summary-toggles{display:flex;align-items:center;justify-content:flex-end;gap:18px;flex-wrap:wrap}.dsh-speech-switch-label{position:relative;display:flex;align-items:center;gap:8px;color:var(--color-text-secondary,#aaa);font-size:12px;cursor:pointer;white-space:nowrap}.dsh-speech-switch-label input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.dsh-speech-switch-track{width:32px;height:18px;display:inline-flex;align-items:center;flex:none;box-sizing:border-box;padding:2px;border:1px solid var(--color-border,#555);border-radius:999px;background:var(--color-background,#242424);transition:background .15s ease,border-color .15s ease}.dsh-speech-switch-track i{width:12px;height:12px;display:block;border-radius:50%;background:var(--color-text-secondary,#aaa);transform:translateX(0);transition:transform .15s ease,background .15s ease}.dsh-speech-switch-label input:checked+.dsh-speech-switch-track,.dsh-speech-autoplay-toggle[aria-checked="true"] .dsh-speech-switch-track{border-color:var(--dsw-alias-brand-primary,#68a8ff);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#68a8ff) 32%,transparent)}.dsh-speech-switch-label input:checked+.dsh-speech-switch-track i,.dsh-speech-autoplay-toggle[aria-checked="true"] .dsh-speech-switch-track i{background:#fff;transform:translateX(14px)}.dsh-speech-switch-label:focus-within .dsh-speech-switch-track{outline:2px solid color-mix(in srgb,#68a8ff 60%,transparent);outline-offset:2px}.dsh-speech-switch-label:has(input:disabled){cursor:default;opacity:.5}
|
|
804
|
+
.dsh-speech-summary-player{width:min(397px,100%);height:34px;display:flex;align-items:center;gap:9px;margin-left:-2px;color:var(--color-text-secondary,#9b9b9b)}.dsh-speech-summary-control{width:194px;height:34px;display:flex;align-items:center;gap:5px;box-sizing:border-box;padding:4px 12px 4px 7px;border:1px solid var(--color-border,#343434);border-radius:999px;background:var(--color-background-secondary,rgba(255,255,255,.025))}.dsh-speech-summary-button{width:24px;height:24px;display:inline-grid;place-items:center;flex:none;padding:0;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer}.dsh-speech-summary-button:hover:not(:disabled),.dsh-speech-summary-button:focus-visible{outline:0;background:var(--color-background-hover,rgba(255,255,255,.08));color:var(--color-text,#eee)}.dsh-speech-summary-button:disabled{cursor:wait;opacity:.6}.dsh-speech-summary-button svg{width:15px;height:15px;fill:currentColor}.dsh-speech-summary-waveform{width:144px;height:22px;display:flex;align-items:center;gap:1px;flex:none;overflow:hidden}.dsh-speech-summary-waveform i{width:2px;height:calc(3px + var(--dsh-speech-peak)*16px);display:block;flex:none;border-radius:2px;background:currentColor;opacity:.28;transition:height 45ms linear,opacity .12s ease,color .12s ease}.dsh-speech-summary-waveform[data-playing="true"] i{color:var(--dsw-alias-brand-primary,#68a8ff);opacity:.82}.dsh-speech-summary-waveform[data-preparing="true"] i{animation:dsh-speech-wave-pulse 1s ease-in-out infinite}.dsh-speech-summary-waveform[data-preparing="true"] i:nth-child(4n+2){animation-delay:.12s}.dsh-speech-summary-waveform[data-preparing="true"] i:nth-child(4n+3){animation-delay:.24s}.dsh-speech-summary-waveform[data-preparing="true"] i:nth-child(4n){animation-delay:.36s}.dsh-speech-summary-label{min-width:0;max-width:190px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.dsh-speech-autoplay-toggle{height:28px;display:inline-flex;align-items:center;gap:7px;padding:0 8px;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--color-text-secondary,#aaa);font:inherit;font-size:11px;cursor:pointer}.dsh-speech-autoplay-toggle .dsh-speech-switch-track{width:28px;height:16px}.dsh-speech-autoplay-toggle .dsh-speech-switch-track i{width:10px;height:10px}.dsh-speech-autoplay-toggle[aria-checked="true"] .dsh-speech-switch-track i{transform:translateX(12px)}.dsh-speech-autoplay-toggle:hover:not(:disabled),.dsh-speech-autoplay-toggle:focus-visible{outline:0;border-color:var(--color-border,#444);background:var(--color-background-hover,rgba(255,255,255,.06));color:var(--color-text,#eee)}.dsh-speech-autoplay-toggle:disabled{opacity:.45;cursor:default}@keyframes dsh-speech-wave-pulse{0%,100%{opacity:.2;transform:scaleY(.55)}50%{opacity:.78;transform:scaleY(1)}}
|
|
701
805
|
.dsh-speech-mic-wrap{position:relative;display:inline-flex;align-items:center;gap:5px;order:1}[data-slot="conversation.input.right"]:has(>.dsh-speech-mic-wrap)~button:last-child{order:2}.dsh-speech-mic{width:30px;height:30px;display:inline-grid;place-items:center;padding:0;border:0;border-radius:7px;background:transparent;color:var(--color-text-secondary,#a8a8a8);cursor:pointer}.dsh-speech-mic:hover:not(:disabled),.dsh-speech-mic:focus-visible{background:var(--color-background-hover,rgba(255,255,255,.08));color:var(--color-text,#eee)}.dsh-speech-mic[aria-disabled="true"]{opacity:.48}.dsh-speech-mic:disabled{cursor:default}.dsh-speech-mic svg{width:18px;height:18px;fill:currentColor}.dsh-speech-mic-active{color:#ef6666;background:rgba(239,102,102,.12)}.dsh-speech-mic-tooltip{position:absolute;right:0;bottom:calc(100% + 8px);z-index:40;width:max-content;max-width:260px;padding:6px 9px;border:1px solid var(--color-border,#343434);border-radius:7px;background:var(--color-background-elevated,#202020);color:var(--color-text,#eee);font-size:12px;line-height:1.35;box-shadow:0 6px 18px rgba(0,0,0,.24);opacity:0;visibility:hidden;transform:translateY(2px);pointer-events:none;transition:opacity .12s ease,transform .12s ease,visibility .12s}.dsh-speech-mic-wrap:hover .dsh-speech-mic-tooltip,.dsh-speech-mic-wrap:focus-within .dsh-speech-mic-tooltip,.dsh-speech-mic-wrap[data-explanation-open="true"] .dsh-speech-mic-tooltip{opacity:1;visibility:visible;transform:translateY(0)}
|
|
702
|
-
:where(div):has(>:where(div)>[data-slot="conversation.input.right"]>.dsh-speech-recording-takeover){position:relative}:where(div):has(>:where(div)>[data-slot="conversation.input.right"]>.dsh-speech-recording-takeover)>*{visibility:hidden}.dsh-speech-recording-takeover{position:absolute;inset:0;z-index:8;visibility:visible;display:flex;align-items:center;gap:10px;padding:4px 8px;box-sizing:border-box;color:var(--color-text-secondary,#a8a8a8)}.dsh-speech-recording-track{height:30px;min-width:0;flex:1;display:flex;align-items:center}.dsh-speech-recording-canvas{display:block;width:100%;height:30px;color:var(--color-text-secondary,#a8a8a8)}.dsh-speech-recording-cancel,.dsh-speech-recording-stop{flex:none;border-radius:999px}.dsh-speech-recording-cancel{color:var(--color-text-secondary,#a8a8a8);background:transparent}.dsh-speech-recording-cancel:hover:not(:disabled),.dsh-speech-recording-cancel:focus-visible{background:var(--color-background-hover,rgba(255,255,255,.08));color:var(--color-text,#eee)}.dsh-speech-recording-stop{color:#ef6666;background:rgba(239,102,102,.12)}.dsh-speech-recording-stop:hover:not(:disabled),.dsh-speech-recording-stop:focus-visible{color:#ff7777;background:rgba(239,102,102,.2)}.dsh-speech-recording-progress{display:flex;align-items:center;justify-content:flex-end;gap:8px;width:100%;color:var(--color-text-secondary,#a8a8a8);font-size:12px}.dsh-speech-recording-takeover[data-phase="starting"] .dsh-speech-recording-progress{justify-content:center}.dsh-speech-recording-progress i{width:13px;height:13px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:dsh-speech-spin .8s linear infinite}.dsh-speech-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@keyframes dsh-speech-spin{to{transform:rotate(360deg)}}
|
|
806
|
+
:where(div):has(>:where(div)>[data-slot="conversation.input.right"]>.dsh-speech-recording-takeover){position:relative}:where(div):has(>:where(div)>[data-slot="conversation.input.right"]>.dsh-speech-recording-takeover)>*{visibility:hidden}.dsh-speech-recording-takeover{position:absolute;inset:0;z-index:8;visibility:visible;display:flex;align-items:center;gap:10px;padding:4px 8px;box-sizing:border-box;color:var(--color-text-secondary,#a8a8a8)}.dsh-speech-recording-track{height:30px;min-width:0;flex:1;display:flex;align-items:center}.dsh-speech-recording-canvas{display:block;width:100%;height:30px;color:var(--color-text-secondary,#a8a8a8)}.dsh-speech-recording-cancel,.dsh-speech-recording-stop{flex:none;border-radius:999px}.dsh-speech-recording-cancel{color:var(--color-text-secondary,#a8a8a8);background:transparent}.dsh-speech-recording-cancel:hover:not(:disabled),.dsh-speech-recording-cancel:focus-visible{background:var(--color-background-hover,rgba(255,255,255,.08));color:var(--color-text,#eee)}.dsh-speech-recording-actions{display:flex;align-items:center;gap:6px;flex:none}.dsh-speech-recording-stop{color:#ef6666;background:rgba(239,102,102,.12)}.dsh-speech-recording-stop:hover:not(:disabled),.dsh-speech-recording-stop:focus-visible{color:#ff7777;background:rgba(239,102,102,.2)}.dsh-speech-recording-send{width:34px;height:34px;display:inline-grid;place-items:center;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-button-info-fill,#679efe);color:#fff;cursor:pointer}.dsh-speech-recording-send:hover:not(:disabled),.dsh-speech-recording-send:focus-visible{outline:0;background:var(--dsw-alias-button-info-hover,#4176e6)}.dsh-speech-recording-send:disabled{cursor:default;opacity:.4}.dsh-speech-recording-send svg{width:16px;height:16px}.dsh-speech-recording-progress{display:flex;align-items:center;justify-content:flex-end;gap:8px;width:100%;color:var(--color-text-secondary,#a8a8a8);font-size:12px}.dsh-speech-recording-takeover[data-phase="starting"] .dsh-speech-recording-progress{justify-content:center}.dsh-speech-recording-progress i{width:13px;height:13px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:dsh-speech-spin .8s linear infinite}.dsh-speech-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@keyframes dsh-speech-spin{to{transform:rotate(360deg)}}
|
|
703
807
|
.dsh-speech-device-marker{display:none}[data-slot="conversation.composer.dock"]>:has(>.dsh-speech-device-dock){overflow:visible}.dsh-speech-device-fallback{min-height:22px;display:flex;align-items:center;justify-content:flex-start;padding:0 8px;overflow:visible}.dsh-speech-device-fallback .dsh-speech-device-dock{margin-left:0}.dsh-speech-device-separator{margin-left:4px;color:var(--dsw-alias-label-tertiary,var(--color-text-secondary,#888))}.dsh-speech-device-dock{position:relative;display:inline-flex;vertical-align:middle;margin-left:5px;color:var(--dsw-alias-label-secondary,var(--color-text-secondary,#a8a8a8));font:inherit}.dsh-speech-device-trigger{max-width:126px;height:20px;display:inline-flex;align-items:center;gap:4px;padding:0 5px;border:0;border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:11px;line-height:20px;cursor:pointer}.dsh-speech-device-trigger:hover,.dsh-speech-device-trigger:focus-visible,.dsh-speech-device-trigger[aria-expanded="true"]{outline:0;background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));color:var(--dsw-alias-label-primary,var(--color-text,#eee))}.dsh-speech-device-trigger:disabled{cursor:wait;opacity:.6}.dsh-speech-device-dock[data-error="true"] .dsh-speech-device-trigger{color:var(--dsw-alias-state-error-primary,#f07878)}.dsh-speech-device-icon,.dsh-speech-device-chevron{display:inline-flex;flex:none}.dsh-speech-device-icon svg{width:12px;height:12px}.dsh-speech-device-chevron svg{width:10px;height:10px}.dsh-speech-device-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-speech-device-menu{position:absolute;right:0;bottom:calc(100% + 6px);z-index:80;min-width:240px;max-width:min(320px,calc(100vw - 32px));display:grid;box-sizing:border-box;padding:4px;border:1px solid var(--dsw-alias-border-l2-darkmode-thin,rgba(255,255,255,.06));border-radius:12px;background:var(--dsw-alias-bg-layer-3,#353638);color:var(--dsw-alias-label-primary,#f9fafb);box-shadow:0 0 1px rgba(0,0,0,.2),0 0 4px rgba(0,0,0,.02),0 12px 32px rgba(0,0,0,.08);font:13px/20px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.dsh-speech-device-menu-item{width:100%;min-width:0;height:38px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 10px;border:0;border-radius:10px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsh-speech-device-menu-item:hover,.dsh-speech-device-menu-item:focus-visible{outline:0;background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08))}.dsh-speech-device-menu-item>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-speech-device-check{width:14px;height:14px;display:inline-flex;flex:none;color:var(--dsw-alias-brand-primary,var(--dsw-alias-label-primary,#f9fafb))}.dsh-speech-device-check svg{width:14px;height:14px}.dsh-speech-device-dock[data-variant="hero"]{margin-left:0;color:var(--dsw-alias-label-primary,var(--color-text,#f9fafb));font:500 13px/20px -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif}.dsh-speech-device-dock[data-variant="hero"] .dsh-speech-device-trigger{max-width:220px;height:28px;padding:0 8px;gap:4px;border-radius:16px;font:inherit;line-height:20px}.dsh-speech-device-dock[data-variant="hero"] .dsh-speech-device-icon svg{width:16px;height:16px}.dsh-speech-device-dock[data-variant="hero"] .dsh-speech-device-chevron svg{width:14px;height:14px}.dsh-speech-device-dock[data-variant="hero"] .dsh-speech-device-menu{left:0;right:auto;top:calc(100% + 6px);bottom:auto;min-width:218px}
|
|
704
808
|
[data-dsh-speech-nav]>svg{display:none}[data-dsh-speech-nav]::before{content:"";width:16px;height:16px;flex:none;background:currentColor;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='7' y='2' width='6' height='11' rx='3' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v3M7 18h6' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E") center/contain no-repeat}
|
|
705
809
|
.dsh-speech-dock,.dsh-speech-dock-error{min-height:22px;display:flex;align-items:center;gap:9px;padding:4px 8px 0;font-size:12px}.dsh-speech-dock-detail{color:var(--color-text-secondary,#8d8d8d)}
|
|
810
|
+
.dsh-speech-summary-player{width:100%}.dsh-speech-summary-player>.dsh-speech-autoplay-toggle{margin-left:auto}
|
|
706
811
|
@media(max-width:680px){.dsh-speech-columns,.dsh-speech-grid,.dsh-speech-balance-grid{grid-template-columns:1fr}.dsh-speech-header{align-items:flex-start}.dsh-speech-device-separator,.dsh-speech-device-dock{display:none}}
|
|
707
|
-
@media(prefers-reduced-motion:reduce){.dsh-speech-mic-tooltip{transition:none}.dsh-speech-recording-progress i{animation:none;border-right-color:currentColor}}
|
|
812
|
+
@media(prefers-reduced-motion:reduce){.dsh-speech-mic-tooltip{transition:none}.dsh-speech-recording-progress i{animation:none;border-right-color:currentColor}.dsh-speech-summary-waveform i{transition:none}.dsh-speech-summary-waveform[data-preparing="true"] i{animation:none}}
|
|
708
813
|
`;
|
|
709
814
|
|
|
710
815
|
//#endregion
|
|
@@ -782,6 +887,16 @@ function CloseIcon() {
|
|
|
782
887
|
})
|
|
783
888
|
});
|
|
784
889
|
}
|
|
890
|
+
function SendIcon() {
|
|
891
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
892
|
+
viewBox: "0 0 16 16",
|
|
893
|
+
"aria-hidden": "true",
|
|
894
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
895
|
+
d: "M8.3125.980183c.35517.072917.66652.223997.9502.452147.22454.18064.46759.4256.71679.67481L14.707 6.83468l-1.414 1.41406L9 3.95577V15.0417H7V3.95577L2.70703 8.24874 1.29297 6.83468l4.72754-4.72754c.2492-.24921.49226-.49417.71679-.67481.23932-.19247.54715-.38831.9502-.452147.2098-.033177.4156-.025023.625 0Z",
|
|
896
|
+
fill: "currentColor"
|
|
897
|
+
})
|
|
898
|
+
});
|
|
899
|
+
}
|
|
785
900
|
const WAVEFORM_PITCH = 4;
|
|
786
901
|
const WAVEFORM_BASELINE = .025;
|
|
787
902
|
function RecordingWaveform({ amplitude }) {
|
|
@@ -851,7 +966,7 @@ function RecordingWaveform({ amplitude }) {
|
|
|
851
966
|
"aria-hidden": "true"
|
|
852
967
|
});
|
|
853
968
|
}
|
|
854
|
-
function RecordingTakeover({ amplitude, cancelLabel, disabled, phase, label, onCancel, onStop, stopLabel }) {
|
|
969
|
+
function RecordingTakeover({ amplitude, cancelLabel, disabled, phase, label, onCancel, onSend, onStop, sendDisabled, sendLabel, stopLabel }) {
|
|
855
970
|
const recording = phase === "recording";
|
|
856
971
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
857
972
|
className: styles.recordingTakeover,
|
|
@@ -875,14 +990,25 @@ function RecordingTakeover({ amplitude, cancelLabel, disabled, phase, label, onC
|
|
|
875
990
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { "aria-hidden": "true" }), label]
|
|
876
991
|
})
|
|
877
992
|
}),
|
|
878
|
-
recording ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
993
|
+
recording ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
994
|
+
className: styles.recordingActions,
|
|
995
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
996
|
+
type: "button",
|
|
997
|
+
className: `${styles.mic} ${styles.recordingStop}`,
|
|
998
|
+
disabled,
|
|
999
|
+
"aria-label": stopLabel,
|
|
1000
|
+
title: stopLabel,
|
|
1001
|
+
onClick: onStop,
|
|
1002
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MicIcon, { stopped: true })
|
|
1003
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1004
|
+
type: "button",
|
|
1005
|
+
className: styles.recordingSend,
|
|
1006
|
+
disabled: sendDisabled,
|
|
1007
|
+
"aria-label": sendLabel,
|
|
1008
|
+
title: sendLabel,
|
|
1009
|
+
onClick: onSend,
|
|
1010
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendIcon, {})
|
|
1011
|
+
})]
|
|
886
1012
|
}) : null,
|
|
887
1013
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
888
1014
|
className: styles.srOnly,
|
|
@@ -943,7 +1069,14 @@ function SpeechMic({ controller, getLocale, sessionId, input, inputActions, t })
|
|
|
943
1069
|
onCancel: () => {
|
|
944
1070
|
controller.cancel();
|
|
945
1071
|
},
|
|
1072
|
+
onSend: () => {
|
|
1073
|
+
controller.stopAndSend().catch((error) => {
|
|
1074
|
+
controller.reportError(error instanceof Error ? error.message : "Unable to send voice input");
|
|
1075
|
+
});
|
|
1076
|
+
},
|
|
946
1077
|
onStop: toggle,
|
|
1078
|
+
sendDisabled: disabled || !state.hasTranscript,
|
|
1079
|
+
sendLabel: t("micSend"),
|
|
947
1080
|
stopLabel: title
|
|
948
1081
|
});
|
|
949
1082
|
}
|
|
@@ -1073,18 +1206,47 @@ function MicrophoneDockPortal({ controller, state, t }) {
|
|
|
1073
1206
|
const markerRef = (0, react.useRef)(null);
|
|
1074
1207
|
const [target, setTarget] = (0, react.useState)(null);
|
|
1075
1208
|
(0, react.useLayoutEffect)(() => {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1209
|
+
let disposed = false;
|
|
1210
|
+
let frame;
|
|
1211
|
+
let observedSlot = null;
|
|
1212
|
+
const observer = new MutationObserver(() => {
|
|
1213
|
+
findTarget();
|
|
1214
|
+
});
|
|
1215
|
+
const retry = () => {
|
|
1216
|
+
if (disposed || frame !== void 0) return;
|
|
1217
|
+
frame = requestAnimationFrame(() => {
|
|
1218
|
+
frame = void 0;
|
|
1219
|
+
findTarget();
|
|
1220
|
+
});
|
|
1221
|
+
};
|
|
1079
1222
|
const findTarget = () => {
|
|
1223
|
+
if (disposed) return;
|
|
1224
|
+
const marker = markerRef.current;
|
|
1225
|
+
const slot = marker?.closest("[data-slot=\"conversation.composer.dock\"]");
|
|
1226
|
+
if (!(slot instanceof HTMLElement)) {
|
|
1227
|
+
setTarget(null);
|
|
1228
|
+
retry();
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (observedSlot !== slot) {
|
|
1232
|
+
observer.disconnect();
|
|
1233
|
+
observedSlot = slot;
|
|
1234
|
+
observer.observe(slot, {
|
|
1235
|
+
childList: true,
|
|
1236
|
+
subtree: true,
|
|
1237
|
+
characterData: true
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1080
1240
|
const metrics = Array.from(slot.children).find((child) => child !== marker && child.textContent?.includes("TTFT"));
|
|
1081
|
-
|
|
1241
|
+
const nextTarget = metrics instanceof HTMLElement ? metrics : null;
|
|
1242
|
+
setTarget((current) => current === nextTarget ? current : nextTarget);
|
|
1243
|
+
if (nextTarget === null) retry();
|
|
1082
1244
|
};
|
|
1083
1245
|
findTarget();
|
|
1084
|
-
const observer = new MutationObserver(findTarget);
|
|
1085
|
-
observer.observe(slot, { childList: true });
|
|
1086
1246
|
return () => {
|
|
1247
|
+
disposed = true;
|
|
1087
1248
|
observer.disconnect();
|
|
1249
|
+
if (frame !== void 0) cancelAnimationFrame(frame);
|
|
1088
1250
|
};
|
|
1089
1251
|
}, []);
|
|
1090
1252
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1202,6 +1364,258 @@ function Field({ label, children, hint }) {
|
|
|
1202
1364
|
]
|
|
1203
1365
|
});
|
|
1204
1366
|
}
|
|
1367
|
+
function voiceKey(voice) {
|
|
1368
|
+
return `${voice.provider ?? ""}\n${voice.model ?? ""}\n${voice.id}`;
|
|
1369
|
+
}
|
|
1370
|
+
function useVoiceOptions(filters, query, enabled = true) {
|
|
1371
|
+
const [voices, setVoices] = (0, react.useState)([]);
|
|
1372
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
1373
|
+
(0, react.useEffect)(() => {
|
|
1374
|
+
if (!enabled) {
|
|
1375
|
+
setVoices([]);
|
|
1376
|
+
setLoading(false);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
const controller = new AbortController();
|
|
1380
|
+
const timer = setTimeout(() => {
|
|
1381
|
+
setLoading(true);
|
|
1382
|
+
speechApi.voices({
|
|
1383
|
+
...filters.model === void 0 ? {} : { model: filters.model },
|
|
1384
|
+
...filters.provider === void 0 ? {} : { provider: filters.provider },
|
|
1385
|
+
...query.trim().length === 0 ? {} : { q: query.trim() }
|
|
1386
|
+
}, controller.signal).then((result) => {
|
|
1387
|
+
setVoices(result.voices.filter((voice) => voice.model !== void 0 && voice.provider !== void 0));
|
|
1388
|
+
}).catch((cause) => {
|
|
1389
|
+
if (!(cause instanceof DOMException && cause.name === "AbortError")) setVoices([]);
|
|
1390
|
+
}).finally(() => {
|
|
1391
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
1392
|
+
});
|
|
1393
|
+
}, 250);
|
|
1394
|
+
return () => {
|
|
1395
|
+
clearTimeout(timer);
|
|
1396
|
+
controller.abort();
|
|
1397
|
+
};
|
|
1398
|
+
}, [
|
|
1399
|
+
enabled,
|
|
1400
|
+
filters.model,
|
|
1401
|
+
filters.provider,
|
|
1402
|
+
query
|
|
1403
|
+
]);
|
|
1404
|
+
return {
|
|
1405
|
+
voices,
|
|
1406
|
+
loading
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function VoicePicker({ label, hint, placeholder, loadingLabel, emptyLabel, query, voices, selected, loading, disabled, onQuery, onSelect }) {
|
|
1410
|
+
const inputId = (0, react.useId)();
|
|
1411
|
+
const listId = (0, react.useId)();
|
|
1412
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1413
|
+
const [active, setActive] = (0, react.useState)(0);
|
|
1414
|
+
const displayed = voices.slice(0, 24);
|
|
1415
|
+
(0, react.useEffect)(() => {
|
|
1416
|
+
setActive(0);
|
|
1417
|
+
}, [voices]);
|
|
1418
|
+
const keyDown = (event) => {
|
|
1419
|
+
if (event.key === "ArrowDown") {
|
|
1420
|
+
event.preventDefault();
|
|
1421
|
+
setOpen(true);
|
|
1422
|
+
setActive((current) => Math.min(displayed.length - 1, current + 1));
|
|
1423
|
+
} else if (event.key === "ArrowUp") {
|
|
1424
|
+
event.preventDefault();
|
|
1425
|
+
setOpen(true);
|
|
1426
|
+
setActive((current) => Math.max(0, current - 1));
|
|
1427
|
+
} else if (event.key === "Enter" && open && displayed[active] !== void 0) {
|
|
1428
|
+
event.preventDefault();
|
|
1429
|
+
onSelect(displayed[active]);
|
|
1430
|
+
setOpen(false);
|
|
1431
|
+
} else if (event.key === "Escape") setOpen(false);
|
|
1432
|
+
};
|
|
1433
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1434
|
+
className: styles.voicePicker,
|
|
1435
|
+
onBlur: (event) => {
|
|
1436
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
|
|
1437
|
+
},
|
|
1438
|
+
children: [
|
|
1439
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1440
|
+
className: styles.label,
|
|
1441
|
+
htmlFor: inputId,
|
|
1442
|
+
children: label
|
|
1443
|
+
}),
|
|
1444
|
+
selected === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1445
|
+
className: styles.voiceSelected,
|
|
1446
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: selected.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1447
|
+
selected.providerName ?? selected.provider,
|
|
1448
|
+
" · ",
|
|
1449
|
+
selected.model
|
|
1450
|
+
] })]
|
|
1451
|
+
}),
|
|
1452
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1453
|
+
id: inputId,
|
|
1454
|
+
className: styles.input,
|
|
1455
|
+
type: "search",
|
|
1456
|
+
role: "combobox",
|
|
1457
|
+
"aria-autocomplete": "list",
|
|
1458
|
+
"aria-controls": listId,
|
|
1459
|
+
"aria-expanded": open,
|
|
1460
|
+
"aria-activedescendant": open && displayed[active] !== void 0 ? `${listId}-${String(active)}` : void 0,
|
|
1461
|
+
value: query,
|
|
1462
|
+
placeholder,
|
|
1463
|
+
disabled,
|
|
1464
|
+
onFocus: () => {
|
|
1465
|
+
setOpen(true);
|
|
1466
|
+
},
|
|
1467
|
+
onChange: (event) => {
|
|
1468
|
+
onQuery(event.target.value);
|
|
1469
|
+
setOpen(true);
|
|
1470
|
+
},
|
|
1471
|
+
onKeyDown: keyDown
|
|
1472
|
+
}),
|
|
1473
|
+
hint === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1474
|
+
className: styles.hint,
|
|
1475
|
+
children: hint
|
|
1476
|
+
}),
|
|
1477
|
+
!open ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1478
|
+
id: listId,
|
|
1479
|
+
className: styles.voiceMenu,
|
|
1480
|
+
role: "listbox",
|
|
1481
|
+
children: loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1482
|
+
className: styles.voiceNotice,
|
|
1483
|
+
role: "status",
|
|
1484
|
+
children: loadingLabel
|
|
1485
|
+
}) : displayed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1486
|
+
className: styles.voiceNotice,
|
|
1487
|
+
children: emptyLabel
|
|
1488
|
+
}) : displayed.map((voice, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1489
|
+
id: `${listId}-${String(index)}`,
|
|
1490
|
+
className: styles.voiceOption,
|
|
1491
|
+
type: "button",
|
|
1492
|
+
role: "option",
|
|
1493
|
+
"aria-selected": selected !== void 0 && voiceKey(voice) === voiceKey(selected),
|
|
1494
|
+
"data-active": index === active,
|
|
1495
|
+
onMouseDown: (event) => {
|
|
1496
|
+
event.preventDefault();
|
|
1497
|
+
},
|
|
1498
|
+
onMouseEnter: () => {
|
|
1499
|
+
setActive(index);
|
|
1500
|
+
},
|
|
1501
|
+
onClick: () => {
|
|
1502
|
+
onSelect(voice);
|
|
1503
|
+
setOpen(false);
|
|
1504
|
+
},
|
|
1505
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: voice.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
1506
|
+
voice.providerName ?? voice.provider,
|
|
1507
|
+
" · ",
|
|
1508
|
+
voice.model
|
|
1509
|
+
] })] }), voice.description === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: voice.description })]
|
|
1510
|
+
}, voiceKey(voice)))
|
|
1511
|
+
})
|
|
1512
|
+
]
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
function ModelPicker({ label, placeholder, emptyLabel, options, value, disabled, onSelect }) {
|
|
1516
|
+
const inputId = (0, react.useId)();
|
|
1517
|
+
const listId = (0, react.useId)();
|
|
1518
|
+
const [query, setQuery] = (0, react.useState)(value);
|
|
1519
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1520
|
+
const [active, setActive] = (0, react.useState)(-1);
|
|
1521
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
1522
|
+
const displayed = normalizedQuery.length === 0 || query === value ? options : options.filter((option) => option.toLocaleLowerCase().includes(normalizedQuery));
|
|
1523
|
+
(0, react.useEffect)(() => {
|
|
1524
|
+
setQuery(value);
|
|
1525
|
+
}, [value]);
|
|
1526
|
+
(0, react.useEffect)(() => {
|
|
1527
|
+
setActive(-1);
|
|
1528
|
+
}, [query, options]);
|
|
1529
|
+
const select = (model) => {
|
|
1530
|
+
setQuery(model);
|
|
1531
|
+
setOpen(false);
|
|
1532
|
+
setActive(-1);
|
|
1533
|
+
onSelect(model);
|
|
1534
|
+
};
|
|
1535
|
+
const keyDown = (event) => {
|
|
1536
|
+
if (event.key === "ArrowDown") {
|
|
1537
|
+
event.preventDefault();
|
|
1538
|
+
setOpen(true);
|
|
1539
|
+
setActive((current) => Math.min(displayed.length - 1, current + 1));
|
|
1540
|
+
} else if (event.key === "ArrowUp") {
|
|
1541
|
+
event.preventDefault();
|
|
1542
|
+
setOpen(true);
|
|
1543
|
+
setActive((current) => current < 0 ? displayed.length - 1 : Math.max(0, current - 1));
|
|
1544
|
+
} else if (event.key === "Enter" && open && displayed[active] !== void 0) {
|
|
1545
|
+
event.preventDefault();
|
|
1546
|
+
select(displayed[active]);
|
|
1547
|
+
} else if (event.key === "Escape") {
|
|
1548
|
+
event.stopPropagation();
|
|
1549
|
+
setQuery(value);
|
|
1550
|
+
setOpen(false);
|
|
1551
|
+
setActive(-1);
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1555
|
+
className: styles.voicePicker,
|
|
1556
|
+
onBlur: (event) => {
|
|
1557
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
1558
|
+
setQuery(value);
|
|
1559
|
+
setOpen(false);
|
|
1560
|
+
setActive(-1);
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1563
|
+
children: [
|
|
1564
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1565
|
+
className: styles.label,
|
|
1566
|
+
htmlFor: inputId,
|
|
1567
|
+
children: label
|
|
1568
|
+
}),
|
|
1569
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1570
|
+
id: inputId,
|
|
1571
|
+
className: styles.input,
|
|
1572
|
+
type: "search",
|
|
1573
|
+
role: "combobox",
|
|
1574
|
+
"aria-autocomplete": "list",
|
|
1575
|
+
"aria-controls": listId,
|
|
1576
|
+
"aria-expanded": open,
|
|
1577
|
+
"aria-activedescendant": open && displayed[active] !== void 0 ? `${listId}-${String(active)}` : void 0,
|
|
1578
|
+
value: query,
|
|
1579
|
+
placeholder,
|
|
1580
|
+
disabled,
|
|
1581
|
+
onFocus: () => {
|
|
1582
|
+
setOpen(true);
|
|
1583
|
+
},
|
|
1584
|
+
onChange: (event) => {
|
|
1585
|
+
setQuery(event.target.value);
|
|
1586
|
+
setOpen(true);
|
|
1587
|
+
},
|
|
1588
|
+
onKeyDown: keyDown
|
|
1589
|
+
}),
|
|
1590
|
+
!open ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1591
|
+
id: listId,
|
|
1592
|
+
className: styles.voiceMenu,
|
|
1593
|
+
role: "listbox",
|
|
1594
|
+
children: displayed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1595
|
+
className: styles.voiceNotice,
|
|
1596
|
+
children: emptyLabel
|
|
1597
|
+
}) : displayed.map((model, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1598
|
+
id: `${listId}-${String(index)}`,
|
|
1599
|
+
className: styles.voiceOption,
|
|
1600
|
+
type: "button",
|
|
1601
|
+
role: "option",
|
|
1602
|
+
"aria-selected": model === value,
|
|
1603
|
+
"data-active": index === active,
|
|
1604
|
+
onMouseDown: (event) => {
|
|
1605
|
+
event.preventDefault();
|
|
1606
|
+
},
|
|
1607
|
+
onMouseEnter: () => {
|
|
1608
|
+
setActive(index);
|
|
1609
|
+
},
|
|
1610
|
+
onClick: () => {
|
|
1611
|
+
select(model);
|
|
1612
|
+
},
|
|
1613
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: model }) })
|
|
1614
|
+
}, model))
|
|
1615
|
+
})
|
|
1616
|
+
]
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1205
1619
|
function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
1206
1620
|
const client = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
1207
1621
|
const settings = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
@@ -1218,6 +1632,8 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1218
1632
|
const [topUpUrl, setTopUpUrl] = (0, react.useState)(null);
|
|
1219
1633
|
const [languageDraft, setLanguageDraft] = (0, react.useState)("auto");
|
|
1220
1634
|
const [contextDraft, setContextDraft] = (0, react.useState)("");
|
|
1635
|
+
const [voiceSearch, setVoiceSearch] = (0, react.useState)("");
|
|
1636
|
+
const [modelVoiceSearch, setModelVoiceSearch] = (0, react.useState)("");
|
|
1221
1637
|
(0, react.useEffect)(() => {
|
|
1222
1638
|
controller.ensureMetadata();
|
|
1223
1639
|
}, [controller]);
|
|
@@ -1248,6 +1664,33 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1248
1664
|
const selectedProvider = providers.some((binding) => binding.provider === value?.provider) ? value?.provider ?? "" : providers.find((binding) => binding.isProviderDefault)?.provider ?? providers[0]?.provider ?? "";
|
|
1249
1665
|
const selectedBinding = providers.find((binding) => binding.provider === selectedProvider);
|
|
1250
1666
|
const models = (0, react.useMemo)(() => [...new Set(catalog.map((binding) => binding.model))].sort(), [catalog]);
|
|
1667
|
+
const ttsCatalog = client.catalog?.ttsBindings ?? [];
|
|
1668
|
+
const ttsEnabled = value?.ttsEnabled ?? true;
|
|
1669
|
+
const ttsLocale = value?.language !== void 0 && value.language !== "auto" ? value.language : getLocale();
|
|
1670
|
+
const localizedTts = preferredTtsSelection(ttsLocale);
|
|
1671
|
+
const hasSavedVoiceRoute = value?.ttsModel !== void 0 && value.ttsProvider !== void 0 && value.ttsVoice !== void 0;
|
|
1672
|
+
const ttsBinding = selectLocalizedTtsBinding(ttsCatalog, ttsLocale, { ...!hasSavedVoiceRoute ? {} : {
|
|
1673
|
+
model: value.ttsModel,
|
|
1674
|
+
provider: value.ttsProvider
|
|
1675
|
+
} });
|
|
1676
|
+
const selectedTtsModel = ttsBinding?.model ?? value?.ttsModel ?? "";
|
|
1677
|
+
const ttsProviders = ttsCatalog.filter((binding) => binding.model === selectedTtsModel);
|
|
1678
|
+
const selectedTtsProvider = ttsProviders.some((binding) => binding.provider === value?.ttsProvider) ? value?.ttsProvider ?? "" : ttsProviders.find((binding) => binding.isProviderDefault)?.provider ?? ttsProviders[0]?.provider ?? "";
|
|
1679
|
+
const selectedTtsBinding = ttsProviders.find((binding) => binding.provider === selectedTtsProvider) ?? ttsBinding;
|
|
1680
|
+
const ttsModels = (0, react.useMemo)(() => [...new Set(ttsCatalog.map((binding) => binding.model))].sort(), [ttsCatalog]);
|
|
1681
|
+
const selectedVoice = (hasSavedVoiceRoute ? value.ttsVoice : void 0) ?? (selectedTtsModel === localizedTts.model && selectedTtsProvider === localizedTts.provider ? localizedTts.voice : void 0) ?? selectedTtsBinding?.defaultVoice ?? "";
|
|
1682
|
+
const defaultVoiceOption = {
|
|
1683
|
+
id: selectedVoice,
|
|
1684
|
+
name: selectedVoice === localizedTts.voice ? localizedTts.name : selectedVoice,
|
|
1685
|
+
model: selectedTtsModel,
|
|
1686
|
+
provider: selectedTtsProvider
|
|
1687
|
+
};
|
|
1688
|
+
const globalVoiceResults = useVoiceOptions({}, voiceSearch, ttsEnabled);
|
|
1689
|
+
const scopedVoiceResults = useVoiceOptions({
|
|
1690
|
+
model: selectedTtsBinding?.aliases?.[0] ?? selectedTtsModel,
|
|
1691
|
+
provider: selectedTtsProvider
|
|
1692
|
+
}, modelVoiceSearch, ttsEnabled && selectedTtsModel.length > 0 && selectedTtsProvider.length > 0);
|
|
1693
|
+
const selectedVoiceOption = [...scopedVoiceResults.voices, ...globalVoiceResults.voices].find((voice) => voice.id === selectedVoice && voice.model === selectedTtsModel && voice.provider === selectedTtsProvider) ?? (selectedVoice.length === 0 ? void 0 : defaultVoiceOption);
|
|
1251
1694
|
const remaining = Math.max(0, Math.ceil((codeExpiry - now) / 1e3));
|
|
1252
1695
|
const connected = client.status?.credential.configured === true;
|
|
1253
1696
|
const writable = settings.writable;
|
|
@@ -1433,23 +1876,24 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1433
1876
|
})
|
|
1434
1877
|
] })
|
|
1435
1878
|
}),
|
|
1436
|
-
connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1879
|
+
connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1880
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1881
|
+
className: styles.card,
|
|
1882
|
+
children: [
|
|
1883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("settings") }),
|
|
1884
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1885
|
+
className: styles.grid,
|
|
1886
|
+
children: [
|
|
1887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1888
|
+
className: styles.modelFull,
|
|
1889
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
|
|
1890
|
+
label: t("model"),
|
|
1891
|
+
placeholder: t("modelSearchPlaceholder"),
|
|
1892
|
+
emptyLabel: t("modelNoResults"),
|
|
1893
|
+
options: models,
|
|
1449
1894
|
value: selectedModel,
|
|
1450
1895
|
disabled: !writable || models.length === 0,
|
|
1451
|
-
|
|
1452
|
-
const model = event.target.value;
|
|
1896
|
+
onSelect: (model) => {
|
|
1453
1897
|
const choices = catalog.filter((binding) => binding.model === model);
|
|
1454
1898
|
const provider = choices.find((binding) => binding.isProviderDefault)?.provider ?? choices[0]?.provider;
|
|
1455
1899
|
run("setting:model", async () => {
|
|
@@ -1457,137 +1901,278 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1457
1901
|
if (provider !== void 0) await scope.set("provider", provider);
|
|
1458
1902
|
await controller.ensureMetadata(true);
|
|
1459
1903
|
});
|
|
1904
|
+
}
|
|
1905
|
+
})
|
|
1906
|
+
}),
|
|
1907
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1908
|
+
label: t("provider"),
|
|
1909
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1910
|
+
className: styles.input,
|
|
1911
|
+
value: selectedProvider,
|
|
1912
|
+
disabled: !writable || providers.length === 0,
|
|
1913
|
+
onChange: (event) => {
|
|
1914
|
+
write("provider", event.target.value);
|
|
1460
1915
|
},
|
|
1461
|
-
children:
|
|
1462
|
-
value:
|
|
1463
|
-
children:
|
|
1464
|
-
},
|
|
1916
|
+
children: providers.map((binding) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1917
|
+
value: binding.provider,
|
|
1918
|
+
children: binding.provider
|
|
1919
|
+
}, binding.provider))
|
|
1465
1920
|
})
|
|
1921
|
+
}),
|
|
1922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
|
|
1923
|
+
label: t("language"),
|
|
1924
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1925
|
+
className: styles.input,
|
|
1926
|
+
list: "dsh-speech-languages",
|
|
1927
|
+
value: languageDraft,
|
|
1928
|
+
disabled: !writable,
|
|
1929
|
+
onChange: (event) => {
|
|
1930
|
+
setLanguageDraft(event.target.value);
|
|
1931
|
+
},
|
|
1932
|
+
onBlur: commitLanguage,
|
|
1933
|
+
onKeyDown: (event) => {
|
|
1934
|
+
if (event.key === "Enter") event.currentTarget.blur();
|
|
1935
|
+
}
|
|
1936
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
1937
|
+
id: "dsh-speech-languages",
|
|
1938
|
+
children: LANGUAGES.map(([id, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1939
|
+
value: id,
|
|
1940
|
+
children: id === "auto" ? t("auto") : label
|
|
1941
|
+
}, id))
|
|
1942
|
+
})]
|
|
1466
1943
|
})
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
value: languageDraft,
|
|
1944
|
+
]
|
|
1945
|
+
}),
|
|
1946
|
+
selectedBinding?.contextSupported === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
1947
|
+
className: styles.contextDetails,
|
|
1948
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
|
|
1949
|
+
className: styles.contextSummary,
|
|
1950
|
+
children: [
|
|
1951
|
+
t("context"),
|
|
1952
|
+
" ",
|
|
1953
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("optional") })
|
|
1954
|
+
]
|
|
1955
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1956
|
+
className: styles.contextBody,
|
|
1957
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1958
|
+
className: styles.hint,
|
|
1959
|
+
children: t("contextHint")
|
|
1960
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1961
|
+
className: styles.textarea,
|
|
1962
|
+
"aria-label": t("context"),
|
|
1963
|
+
maxLength: 4e3,
|
|
1964
|
+
value: contextDraft,
|
|
1489
1965
|
disabled: !writable,
|
|
1490
1966
|
onChange: (event) => {
|
|
1491
|
-
|
|
1967
|
+
setContextDraft(event.target.value);
|
|
1492
1968
|
},
|
|
1493
|
-
onBlur:
|
|
1494
|
-
onKeyDown: (event) => {
|
|
1495
|
-
if (event.key === "Enter") event.currentTarget.blur();
|
|
1496
|
-
}
|
|
1497
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
1498
|
-
id: "dsh-speech-languages",
|
|
1499
|
-
children: LANGUAGES.map(([id, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1500
|
-
value: id,
|
|
1501
|
-
children: id === "auto" ? t("auto") : label
|
|
1502
|
-
}, id))
|
|
1969
|
+
onBlur: commitContext
|
|
1503
1970
|
})]
|
|
1504
|
-
})
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("optional") })
|
|
1515
|
-
]
|
|
1516
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1517
|
-
className: styles.contextBody,
|
|
1518
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1971
|
+
})]
|
|
1972
|
+
}) : null
|
|
1973
|
+
]
|
|
1974
|
+
}),
|
|
1975
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1976
|
+
className: styles.card,
|
|
1977
|
+
children: [
|
|
1978
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1979
|
+
className: styles.cardTitle,
|
|
1980
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("spokenSummaries") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1519
1981
|
className: styles.hint,
|
|
1520
|
-
children: t("
|
|
1521
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1522
|
-
className: styles.
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1982
|
+
children: t("spokenSummariesHint")
|
|
1983
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1984
|
+
className: styles.summaryToggles,
|
|
1985
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1986
|
+
className: styles.switchLabel,
|
|
1987
|
+
children: [
|
|
1988
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1989
|
+
type: "checkbox",
|
|
1990
|
+
checked: ttsEnabled,
|
|
1991
|
+
disabled: !writable,
|
|
1992
|
+
onChange: (event) => {
|
|
1993
|
+
write("ttsEnabled", event.target.checked);
|
|
1994
|
+
}
|
|
1995
|
+
}),
|
|
1996
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1997
|
+
className: styles.switchTrack,
|
|
1998
|
+
"aria-hidden": "true",
|
|
1999
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
|
|
2000
|
+
}),
|
|
2001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("ttsEnabled") })
|
|
2002
|
+
]
|
|
2003
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2004
|
+
className: styles.switchLabel,
|
|
2005
|
+
children: [
|
|
2006
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2007
|
+
type: "checkbox",
|
|
2008
|
+
checked: value?.autoPlay ?? true,
|
|
2009
|
+
disabled: !writable || !ttsEnabled,
|
|
2010
|
+
onChange: (event) => {
|
|
2011
|
+
write("autoPlay", event.target.checked);
|
|
2012
|
+
}
|
|
2013
|
+
}),
|
|
2014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2015
|
+
className: styles.switchTrack,
|
|
2016
|
+
"aria-hidden": "true",
|
|
2017
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
|
|
2018
|
+
}),
|
|
2019
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("autoplayGlobal") })
|
|
2020
|
+
]
|
|
2021
|
+
})]
|
|
1531
2022
|
})]
|
|
1532
|
-
})
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
2023
|
+
}),
|
|
2024
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2025
|
+
className: styles.grid,
|
|
2026
|
+
children: [
|
|
2027
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2028
|
+
className: styles.modelFull,
|
|
2029
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoicePicker, {
|
|
2030
|
+
label: t("voiceSearch"),
|
|
2031
|
+
hint: t("voiceSearchHint"),
|
|
2032
|
+
placeholder: t("voiceSearchPlaceholder"),
|
|
2033
|
+
loadingLabel: t("loading"),
|
|
2034
|
+
emptyLabel: t("voiceNoResults"),
|
|
2035
|
+
query: voiceSearch,
|
|
2036
|
+
voices: globalVoiceResults.voices,
|
|
2037
|
+
selected: selectedVoiceOption,
|
|
2038
|
+
loading: globalVoiceResults.loading,
|
|
2039
|
+
disabled: !writable || !ttsEnabled,
|
|
2040
|
+
onQuery: setVoiceSearch,
|
|
2041
|
+
onSelect: (voice) => {
|
|
2042
|
+
if (voice.model === void 0 || voice.provider === void 0) return;
|
|
2043
|
+
setVoiceSearch(voice.name);
|
|
2044
|
+
setModelVoiceSearch("");
|
|
2045
|
+
run("setting:ttsVoice", async () => {
|
|
2046
|
+
await scope.set("ttsVoice", voice.id);
|
|
2047
|
+
await scope.set("ttsModel", voice.model);
|
|
2048
|
+
await scope.set("ttsProvider", voice.provider);
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
})
|
|
2052
|
+
}),
|
|
2053
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2054
|
+
className: styles.modelFull,
|
|
2055
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
|
|
2056
|
+
label: t("ttsModel"),
|
|
2057
|
+
placeholder: t("modelSearchPlaceholder"),
|
|
2058
|
+
emptyLabel: t("modelNoResults"),
|
|
2059
|
+
options: ttsModels,
|
|
2060
|
+
value: selectedTtsModel,
|
|
2061
|
+
disabled: !writable || !ttsEnabled || ttsModels.length === 0,
|
|
2062
|
+
onSelect: (model) => {
|
|
2063
|
+
const choices = ttsCatalog.filter((binding) => binding.model === model);
|
|
2064
|
+
const provider = choices.find((binding) => binding.isProviderDefault)?.provider ?? choices[0]?.provider;
|
|
2065
|
+
run("setting:ttsModel", async () => {
|
|
2066
|
+
await scope.set("ttsModel", model);
|
|
2067
|
+
if (provider !== void 0) await scope.set("ttsProvider", provider);
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
})
|
|
2071
|
+
}),
|
|
2072
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2073
|
+
label: t("ttsProvider"),
|
|
2074
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2075
|
+
className: styles.input,
|
|
2076
|
+
value: selectedTtsProvider,
|
|
2077
|
+
disabled: !writable || !ttsEnabled || ttsProviders.length === 0,
|
|
2078
|
+
onChange: (event) => {
|
|
2079
|
+
write("ttsProvider", event.target.value);
|
|
2080
|
+
},
|
|
2081
|
+
children: ttsProviders.map((binding) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2082
|
+
value: binding.provider,
|
|
2083
|
+
children: binding.provider
|
|
2084
|
+
}, binding.provider))
|
|
2085
|
+
})
|
|
2086
|
+
}),
|
|
2087
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2088
|
+
className: styles.modelFull,
|
|
2089
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoicePicker, {
|
|
2090
|
+
label: t("ttsVoice"),
|
|
2091
|
+
hint: t("modelVoiceSearchHint", { model: selectedTtsModel }),
|
|
2092
|
+
placeholder: t("modelVoiceSearchPlaceholder"),
|
|
2093
|
+
loadingLabel: t("loading"),
|
|
2094
|
+
emptyLabel: t("voiceNoResults"),
|
|
2095
|
+
query: modelVoiceSearch,
|
|
2096
|
+
voices: scopedVoiceResults.voices,
|
|
2097
|
+
selected: selectedVoiceOption,
|
|
2098
|
+
loading: scopedVoiceResults.loading,
|
|
2099
|
+
disabled: !writable || !ttsEnabled || selectedTtsBinding === void 0,
|
|
2100
|
+
onQuery: setModelVoiceSearch,
|
|
2101
|
+
onSelect: (voice) => {
|
|
2102
|
+
setModelVoiceSearch(voice.name);
|
|
2103
|
+
run("setting:ttsVoice", async () => {
|
|
2104
|
+
await scope.set("ttsVoice", voice.id);
|
|
2105
|
+
await scope.set("ttsModel", selectedTtsModel);
|
|
2106
|
+
await scope.set("ttsProvider", selectedTtsProvider);
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
})
|
|
1567
2110
|
})
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2111
|
+
]
|
|
2112
|
+
}),
|
|
2113
|
+
ttsCatalog.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2114
|
+
className: styles.muted,
|
|
2115
|
+
children: t("ttsUnavailable")
|
|
2116
|
+
}) : null
|
|
2117
|
+
]
|
|
2118
|
+
}),
|
|
2119
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2120
|
+
className: styles.card,
|
|
2121
|
+
children: [
|
|
2122
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("balance") }),
|
|
2123
|
+
client.status?.balance === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2124
|
+
className: styles.muted,
|
|
2125
|
+
children: client.status?.balanceError === void 0 ? t("loading") : t("balanceUnavailable")
|
|
2126
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
2127
|
+
className: styles.balanceValue,
|
|
2128
|
+
children: money(client.status.balance.paidUsd)
|
|
2129
|
+
}), client.status.balance.exhausted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2130
|
+
className: styles.danger,
|
|
2131
|
+
children: t("emptyBalance")
|
|
2132
|
+
}) : client.status.balance.low ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2133
|
+
className: styles.warning,
|
|
2134
|
+
children: t("lowBalance")
|
|
2135
|
+
}) : null] }),
|
|
2136
|
+
connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2137
|
+
className: styles.topUp,
|
|
2138
|
+
children: [
|
|
2139
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2140
|
+
label: t("amount"),
|
|
2141
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2142
|
+
className: styles.input,
|
|
2143
|
+
type: "number",
|
|
2144
|
+
min: 5,
|
|
2145
|
+
max: 1e3,
|
|
2146
|
+
step: 1,
|
|
2147
|
+
value: topUpAmount,
|
|
2148
|
+
onChange: (event) => {
|
|
2149
|
+
setTopUpAmount(Number(event.target.value));
|
|
2150
|
+
}
|
|
2151
|
+
})
|
|
2152
|
+
}),
|
|
2153
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2154
|
+
className: styles.primary,
|
|
2155
|
+
type: "button",
|
|
2156
|
+
disabled: busy !== null || topUpAmount < 5 || topUpAmount > 1e3,
|
|
2157
|
+
onClick: () => {
|
|
2158
|
+
run("top-up", async () => {
|
|
2159
|
+
setTopUpUrl((await speechApi.topUp(topUpAmount)).url);
|
|
2160
|
+
});
|
|
2161
|
+
},
|
|
2162
|
+
children: t("createLink")
|
|
2163
|
+
}),
|
|
2164
|
+
topUpUrl === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2165
|
+
className: styles.checkout,
|
|
2166
|
+
href: topUpUrl,
|
|
2167
|
+
target: "_blank",
|
|
2168
|
+
rel: "noreferrer",
|
|
2169
|
+
children: t("openCheckout")
|
|
2170
|
+
})
|
|
2171
|
+
]
|
|
2172
|
+
}) : null
|
|
2173
|
+
]
|
|
2174
|
+
})
|
|
2175
|
+
] }) : null,
|
|
1591
2176
|
message === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1592
2177
|
className: styles.good,
|
|
1593
2178
|
role: "status",
|
|
@@ -1602,6 +2187,813 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1602
2187
|
});
|
|
1603
2188
|
}
|
|
1604
2189
|
|
|
2190
|
+
//#endregion
|
|
2191
|
+
//#region src/client/spoken-controller.ts
|
|
2192
|
+
const EMPTY_PEAKS = Object.freeze(Array.from({ length: 48 }, (_, index) => .15 + .08 * Math.sin(index * .73) ** 2));
|
|
2193
|
+
function contentText(value) {
|
|
2194
|
+
if (!Array.isArray(value)) return "";
|
|
2195
|
+
return value.flatMap((block) => {
|
|
2196
|
+
if (block === null || typeof block !== "object") return [];
|
|
2197
|
+
const candidate = block;
|
|
2198
|
+
return candidate.type === "text" && typeof candidate.text === "string" ? [candidate.text] : [];
|
|
2199
|
+
}).join("\n").trim();
|
|
2200
|
+
}
|
|
2201
|
+
function boundSource(text, maximum) {
|
|
2202
|
+
if (text.length <= maximum) return text;
|
|
2203
|
+
const marker = "\n\n[...middle omitted for spoken-summary input...]\n\n";
|
|
2204
|
+
const remaining = maximum - 51;
|
|
2205
|
+
const beginning = Math.ceil(remaining / 2);
|
|
2206
|
+
return `${text.slice(0, beginning)}${marker}${text.slice(text.length - (remaining - beginning))}`;
|
|
2207
|
+
}
|
|
2208
|
+
/** Derive only the closing finalized assistant message for each completed turn. */
|
|
2209
|
+
function spokenSources(nodes, locale, requests = []) {
|
|
2210
|
+
const closings = /* @__PURE__ */ new Map();
|
|
2211
|
+
for (const node of nodes) {
|
|
2212
|
+
if (node.kind !== "assistant" || node.messageId === void 0 || node.interrupted === true) continue;
|
|
2213
|
+
const current = closings.get(node.turn);
|
|
2214
|
+
if (current === void 0 || node.seq > current.seq) closings.set(node.turn, node);
|
|
2215
|
+
}
|
|
2216
|
+
const result = [];
|
|
2217
|
+
for (const assistant of [...closings.values()].sort((a, b) => a.seq - b.seq)) {
|
|
2218
|
+
const answer = assistant.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n").trim();
|
|
2219
|
+
if (answer.length === 0 || assistant.messageId === void 0) continue;
|
|
2220
|
+
let request$1 = "";
|
|
2221
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
2222
|
+
const candidate = nodes[index];
|
|
2223
|
+
if (candidate === void 0 || candidate.seq >= assistant.seq) continue;
|
|
2224
|
+
if (candidate.kind === "user" || candidate.kind === "steering") {
|
|
2225
|
+
request$1 = contentText(candidate.content);
|
|
2226
|
+
if (request$1.length > 0) break;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const inspection = requests.find((request$2) => request$2.purpose === "assistant" && request$2.resultSeq === assistant.seq) ?? [...requests].reverse().find((request$2) => request$2.purpose === "assistant" && request$2.turn === assistant.turn && request$2.step === assistant.step && request$2.status === "complete");
|
|
2230
|
+
const recorded = assistant.requestConfig ?? assistant.provenance ?? inspection?.requestConfig ?? inspection?.provenance;
|
|
2231
|
+
const route = recorded === void 0 ? void 0 : {
|
|
2232
|
+
provider: recorded.provider,
|
|
2233
|
+
model: recorded.model,
|
|
2234
|
+
..."reasoningEffort" in recorded && typeof recorded.reasoningEffort === "string" ? { reasoningEffort: recorded.reasoningEffort } : {}
|
|
2235
|
+
};
|
|
2236
|
+
result.push({
|
|
2237
|
+
messageId: String(assistant.messageId),
|
|
2238
|
+
request: boundSource(request$1, MAX_SUMMARY_REQUEST_CHARACTERS),
|
|
2239
|
+
answer: boundSource(answer, MAX_SUMMARY_ANSWER_CHARACTERS),
|
|
2240
|
+
locale,
|
|
2241
|
+
...route === void 0 ? {} : { route }
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
return result;
|
|
2245
|
+
}
|
|
2246
|
+
const INTERACTION_CUES = {
|
|
2247
|
+
en: "I need some feedback to keep going.",
|
|
2248
|
+
zh: "我需要你的反馈才能继续。",
|
|
2249
|
+
ja: "続けるには、フィードバックが必要です。",
|
|
2250
|
+
ko: "계속하려면 피드백이 필요해요.",
|
|
2251
|
+
es: "Necesito tus comentarios para continuar.",
|
|
2252
|
+
fr: "J’ai besoin de votre avis pour continuer.",
|
|
2253
|
+
de: "Ich brauche Ihre Rückmeldung, um fortzufahren.",
|
|
2254
|
+
pt: "Preciso do seu feedback para continuar.",
|
|
2255
|
+
it: "Ho bisogno del tuo feedback per continuare.",
|
|
2256
|
+
ru: "Мне нужна ваша обратная связь, чтобы продолжить.",
|
|
2257
|
+
ar: "أحتاج إلى ملاحظاتك لكي أتابع.",
|
|
2258
|
+
hi: "आगे बढ़ने के लिए मुझे आपकी प्रतिक्रिया चाहिए।",
|
|
2259
|
+
id: "Saya perlu masukan Anda untuk melanjutkan.",
|
|
2260
|
+
vi: "Tôi cần phản hồi của bạn để tiếp tục."
|
|
2261
|
+
};
|
|
2262
|
+
function interactionCue(locale) {
|
|
2263
|
+
return INTERACTION_CUES[locale.toLowerCase().split("-")[0] ?? ""] ?? INTERACTION_CUES.en;
|
|
2264
|
+
}
|
|
2265
|
+
function revokeAudioUrl(url) {
|
|
2266
|
+
if (url?.startsWith("blob:") === true) URL.revokeObjectURL(url);
|
|
2267
|
+
}
|
|
2268
|
+
function browserAudio() {
|
|
2269
|
+
const audio = new Audio();
|
|
2270
|
+
audio.preload = "metadata";
|
|
2271
|
+
return audio;
|
|
2272
|
+
}
|
|
2273
|
+
var SpokenSummaryController = class {
|
|
2274
|
+
audio;
|
|
2275
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2276
|
+
states = /* @__PURE__ */ new Map();
|
|
2277
|
+
observed = /* @__PURE__ */ new Map();
|
|
2278
|
+
observedInteractions = /* @__PURE__ */ new Set();
|
|
2279
|
+
voiceCache = /* @__PURE__ */ new Map();
|
|
2280
|
+
activeMessageId;
|
|
2281
|
+
loadedMessageId;
|
|
2282
|
+
playbackGeneration = 0;
|
|
2283
|
+
snapshot = { messages: /* @__PURE__ */ new Map() };
|
|
2284
|
+
enabled = true;
|
|
2285
|
+
disposed = false;
|
|
2286
|
+
analyserContext;
|
|
2287
|
+
analyserSource;
|
|
2288
|
+
analyser;
|
|
2289
|
+
analyserData;
|
|
2290
|
+
analysisFrame;
|
|
2291
|
+
analysisTick = 0;
|
|
2292
|
+
pauseResolutionTimer;
|
|
2293
|
+
constructor(audioFactory = browserAudio) {
|
|
2294
|
+
this.audio = audioFactory();
|
|
2295
|
+
}
|
|
2296
|
+
subscribe = (listener) => {
|
|
2297
|
+
this.listeners.add(listener);
|
|
2298
|
+
return () => {
|
|
2299
|
+
this.listeners.delete(listener);
|
|
2300
|
+
};
|
|
2301
|
+
};
|
|
2302
|
+
getSnapshot = () => this.snapshot;
|
|
2303
|
+
setEnabled(enabled) {
|
|
2304
|
+
if (this.disposed || this.enabled === enabled) return;
|
|
2305
|
+
this.enabled = enabled;
|
|
2306
|
+
if (enabled) return;
|
|
2307
|
+
this.stopActive();
|
|
2308
|
+
this.clearAudioOwnership(true);
|
|
2309
|
+
for (const [messageId, state] of this.states) {
|
|
2310
|
+
state.abort?.abort();
|
|
2311
|
+
state.abort = void 0;
|
|
2312
|
+
state.requestGeneration += 1;
|
|
2313
|
+
revokeAudioUrl(state.audioUrl);
|
|
2314
|
+
if (state.ephemeral === true) {
|
|
2315
|
+
this.states.delete(messageId);
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
Object.assign(state, {
|
|
2319
|
+
phase: "idle",
|
|
2320
|
+
summary: void 0,
|
|
2321
|
+
audioUrl: void 0,
|
|
2322
|
+
error: void 0,
|
|
2323
|
+
duration: 0,
|
|
2324
|
+
progress: 0,
|
|
2325
|
+
peaks: EMPTY_PEAKS,
|
|
2326
|
+
ended: false
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
this.voiceCache.clear();
|
|
2330
|
+
this.publish();
|
|
2331
|
+
}
|
|
2332
|
+
mount(messageId) {
|
|
2333
|
+
const state = this.ensure(messageId);
|
|
2334
|
+
state.mounts += 1;
|
|
2335
|
+
state.releaseGeneration += 1;
|
|
2336
|
+
return () => {
|
|
2337
|
+
state.mounts = Math.max(0, state.mounts - 1);
|
|
2338
|
+
const releaseGeneration = ++state.releaseGeneration;
|
|
2339
|
+
queueMicrotask(() => {
|
|
2340
|
+
if (state.mounts === 0 && state.releaseGeneration === releaseGeneration) this.release(messageId);
|
|
2341
|
+
});
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
observeSession(sessionKey, sources, settings) {
|
|
2345
|
+
if (this.disposed) return;
|
|
2346
|
+
this.setEnabled(settings.ttsEnabled);
|
|
2347
|
+
const ids = new Set(sources.map((source) => source.messageId));
|
|
2348
|
+
const previous = this.observed.get(sessionKey);
|
|
2349
|
+
if (previous === void 0) {
|
|
2350
|
+
this.observed.set(sessionKey, ids);
|
|
2351
|
+
for (const id of ids) this.ensure(id);
|
|
2352
|
+
this.publish();
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
this.observed.set(sessionKey, new Set([...previous, ...ids]));
|
|
2356
|
+
for (const source of sources) {
|
|
2357
|
+
if (previous.has(source.messageId)) continue;
|
|
2358
|
+
previous.add(source.messageId);
|
|
2359
|
+
if (settings.ttsEnabled) this.prepare(source, settings, settings.autoPlay);
|
|
2360
|
+
else this.ensure(source.messageId);
|
|
2361
|
+
}
|
|
2362
|
+
if (!settings.ttsEnabled) this.publish();
|
|
2363
|
+
}
|
|
2364
|
+
observeInteractions(sessionKey, interactionKeys, locale, settings) {
|
|
2365
|
+
if (this.disposed) return;
|
|
2366
|
+
this.setEnabled(settings.ttsEnabled);
|
|
2367
|
+
for (const key of interactionKeys) {
|
|
2368
|
+
const identity = `${sessionKey}\n${key}`;
|
|
2369
|
+
if (this.observedInteractions.has(identity)) continue;
|
|
2370
|
+
this.observedInteractions.add(identity);
|
|
2371
|
+
if (settings.ttsEnabled && settings.autoPlay) this.prepareCue(`interaction:${identity}`, interactionCue(locale), settings);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
async prepare(source, settings, autoPlay = false) {
|
|
2375
|
+
if (!this.enabled || !settings.ttsEnabled) return;
|
|
2376
|
+
const state = this.ensure(source.messageId);
|
|
2377
|
+
if (state.phase === "preparing" || state.phase === "ready" || state.phase === "playing") {
|
|
2378
|
+
if (state.phase === "ready" && !autoPlay) await this.play(source.messageId, true);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
if (source.route === void 0) {
|
|
2382
|
+
this.fail(source.messageId, "The completed answer has no recorded LLM route.");
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
const binding = selectLocalizedTtsBinding(settings.bindings, source.locale, {
|
|
2386
|
+
...settings.ttsModel === void 0 ? {} : { model: settings.ttsModel },
|
|
2387
|
+
...settings.ttsProvider === void 0 ? {} : { provider: settings.ttsProvider }
|
|
2388
|
+
});
|
|
2389
|
+
if (binding === void 0) {
|
|
2390
|
+
this.fail(source.messageId, "No compatible synchronous text-to-speech model is available.");
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
const abort = new AbortController();
|
|
2394
|
+
state.abort?.abort();
|
|
2395
|
+
state.abort = abort;
|
|
2396
|
+
const requestGeneration = ++state.requestGeneration;
|
|
2397
|
+
this.patch(source.messageId, {
|
|
2398
|
+
phase: "preparing",
|
|
2399
|
+
error: void 0,
|
|
2400
|
+
progress: 0
|
|
2401
|
+
});
|
|
2402
|
+
try {
|
|
2403
|
+
const voice = await this.voiceFor(binding, settings.ttsVoice);
|
|
2404
|
+
if (voice === void 0) throw new Error("No compatible voice is available for this text-to-speech route.");
|
|
2405
|
+
const { summary } = await speechApi.summarize({
|
|
2406
|
+
request: source.request || "No preceding user prose was available.",
|
|
2407
|
+
answer: source.answer,
|
|
2408
|
+
locale: source.locale,
|
|
2409
|
+
route: source.route
|
|
2410
|
+
}, abort.signal);
|
|
2411
|
+
const prepared = await speechApi.prepareTts({
|
|
2412
|
+
text: summary,
|
|
2413
|
+
model: binding.model,
|
|
2414
|
+
provider: binding.provider,
|
|
2415
|
+
voice
|
|
2416
|
+
}, abort.signal);
|
|
2417
|
+
if (this.disposed || abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2418
|
+
state.abort = void 0;
|
|
2419
|
+
this.patch(source.messageId, {
|
|
2420
|
+
phase: "ready",
|
|
2421
|
+
summary,
|
|
2422
|
+
audioUrl: prepared.url,
|
|
2423
|
+
duration: 0,
|
|
2424
|
+
progress: 0,
|
|
2425
|
+
peaks: EMPTY_PEAKS,
|
|
2426
|
+
error: void 0,
|
|
2427
|
+
ended: false
|
|
2428
|
+
});
|
|
2429
|
+
if (autoPlay && this.activeMessageId === void 0) await this.play(source.messageId, false);
|
|
2430
|
+
} catch (error) {
|
|
2431
|
+
if (abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2432
|
+
state.abort = void 0;
|
|
2433
|
+
this.fail(source.messageId, error instanceof Error ? error.message : "Could not prepare the spoken summary.");
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
async prepareCue(messageId, text, settings) {
|
|
2437
|
+
if (!this.enabled || !settings.ttsEnabled) return;
|
|
2438
|
+
const state = this.ensure(messageId);
|
|
2439
|
+
state.ephemeral = true;
|
|
2440
|
+
const binding = selectLocalizedTtsBinding(settings.bindings, text === INTERACTION_CUES.zh ? "zh" : "en", {
|
|
2441
|
+
...settings.ttsModel === void 0 ? {} : { model: settings.ttsModel },
|
|
2442
|
+
...settings.ttsProvider === void 0 ? {} : { provider: settings.ttsProvider }
|
|
2443
|
+
});
|
|
2444
|
+
if (binding === void 0) {
|
|
2445
|
+
this.release(messageId);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
const abort = new AbortController();
|
|
2449
|
+
state.abort = abort;
|
|
2450
|
+
const requestGeneration = ++state.requestGeneration;
|
|
2451
|
+
try {
|
|
2452
|
+
const voice = await this.voiceFor(binding, settings.ttsVoice);
|
|
2453
|
+
if (voice === void 0) throw new Error("No compatible voice is available.");
|
|
2454
|
+
const prepared = await speechApi.prepareTts({
|
|
2455
|
+
text,
|
|
2456
|
+
model: binding.model,
|
|
2457
|
+
provider: binding.provider,
|
|
2458
|
+
voice
|
|
2459
|
+
}, abort.signal);
|
|
2460
|
+
if (this.disposed || abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2461
|
+
state.abort = void 0;
|
|
2462
|
+
this.patch(messageId, {
|
|
2463
|
+
phase: "ready",
|
|
2464
|
+
audioUrl: prepared.url,
|
|
2465
|
+
duration: 0,
|
|
2466
|
+
progress: 0,
|
|
2467
|
+
peaks: EMPTY_PEAKS,
|
|
2468
|
+
ended: false
|
|
2469
|
+
});
|
|
2470
|
+
if (this.activeMessageId !== void 0) {
|
|
2471
|
+
this.release(messageId);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
await this.play(messageId, false);
|
|
2475
|
+
if (this.activeMessageId !== messageId) this.release(messageId);
|
|
2476
|
+
} catch {
|
|
2477
|
+
if (!abort.signal.aborted && state.requestGeneration === requestGeneration) this.release(messageId);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
async play(messageId, explicit) {
|
|
2481
|
+
const state = this.states.get(messageId);
|
|
2482
|
+
if (state?.audioUrl === void 0 || this.disposed || !this.enabled) return;
|
|
2483
|
+
if (!explicit && this.activeMessageId !== void 0 && this.activeMessageId !== messageId) return;
|
|
2484
|
+
if (this.activeMessageId !== void 0 && this.activeMessageId !== messageId) this.stopActive();
|
|
2485
|
+
const generation = ++this.playbackGeneration;
|
|
2486
|
+
this.activeMessageId = messageId;
|
|
2487
|
+
this.clearPauseResolution();
|
|
2488
|
+
if (this.loadedMessageId !== messageId) {
|
|
2489
|
+
this.audio.pause();
|
|
2490
|
+
this.audio.src = state.audioUrl;
|
|
2491
|
+
this.loadedMessageId = messageId;
|
|
2492
|
+
this.audio.currentTime = state.progress > 0 && (state.duration <= 0 || state.progress < state.duration * .98) ? state.progress : 0;
|
|
2493
|
+
} else if (state.ended === true) this.audio.currentTime = 0;
|
|
2494
|
+
this.audio.onended = () => {
|
|
2495
|
+
this.clearPauseResolution();
|
|
2496
|
+
this.finishPlayback(generation, messageId, true);
|
|
2497
|
+
};
|
|
2498
|
+
this.audio.onerror = () => {
|
|
2499
|
+
if (!this.owns(generation, messageId)) return;
|
|
2500
|
+
this.fail(messageId, "The spoken summary could not be played.");
|
|
2501
|
+
this.clearAudioOwnership(true);
|
|
2502
|
+
};
|
|
2503
|
+
this.audio.onpause = () => {
|
|
2504
|
+
if (!this.owns(generation, messageId)) return;
|
|
2505
|
+
this.clearPauseResolution();
|
|
2506
|
+
this.pauseResolutionTimer = setTimeout(() => {
|
|
2507
|
+
this.pauseResolutionTimer = void 0;
|
|
2508
|
+
this.finishPlayback(generation, messageId, this.audio.ended);
|
|
2509
|
+
}, 80);
|
|
2510
|
+
};
|
|
2511
|
+
this.audio.ontimeupdate = () => {
|
|
2512
|
+
if (!this.owns(generation, messageId)) return;
|
|
2513
|
+
const duration = Number.isFinite(this.audio.duration) ? this.audio.duration : state.duration;
|
|
2514
|
+
this.patch(messageId, {
|
|
2515
|
+
progress: Math.max(0, this.audio.currentTime),
|
|
2516
|
+
duration: Math.max(0, duration)
|
|
2517
|
+
});
|
|
2518
|
+
};
|
|
2519
|
+
this.patch(messageId, {
|
|
2520
|
+
phase: "playing",
|
|
2521
|
+
ended: false
|
|
2522
|
+
});
|
|
2523
|
+
try {
|
|
2524
|
+
await this.audio.play();
|
|
2525
|
+
if (!this.owns(generation, messageId)) return;
|
|
2526
|
+
this.startAnalysis(messageId, generation);
|
|
2527
|
+
} catch {
|
|
2528
|
+
if (!this.owns(generation, messageId)) return;
|
|
2529
|
+
this.patch(messageId, { phase: "ready" });
|
|
2530
|
+
this.clearAudioOwnership(false);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
pause(messageId) {
|
|
2534
|
+
if (this.activeMessageId !== messageId) return;
|
|
2535
|
+
this.patch(messageId, {
|
|
2536
|
+
phase: "ready",
|
|
2537
|
+
progress: Math.max(0, this.audio.currentTime),
|
|
2538
|
+
ended: false
|
|
2539
|
+
});
|
|
2540
|
+
++this.playbackGeneration;
|
|
2541
|
+
this.audio.pause();
|
|
2542
|
+
this.clearAudioOwnership(false);
|
|
2543
|
+
}
|
|
2544
|
+
retry(source, settings) {
|
|
2545
|
+
if (!this.enabled || !settings.ttsEnabled) return;
|
|
2546
|
+
const state = this.ensure(source.messageId);
|
|
2547
|
+
if (this.loadedMessageId === source.messageId) this.clearAudioOwnership(true);
|
|
2548
|
+
state.abort?.abort();
|
|
2549
|
+
state.requestGeneration += 1;
|
|
2550
|
+
revokeAudioUrl(state.audioUrl);
|
|
2551
|
+
Object.assign(state, {
|
|
2552
|
+
phase: "idle",
|
|
2553
|
+
audioUrl: void 0,
|
|
2554
|
+
summary: void 0,
|
|
2555
|
+
error: void 0,
|
|
2556
|
+
progress: 0,
|
|
2557
|
+
duration: 0,
|
|
2558
|
+
peaks: EMPTY_PEAKS,
|
|
2559
|
+
ended: false
|
|
2560
|
+
});
|
|
2561
|
+
this.publish();
|
|
2562
|
+
this.prepare(source, settings, false).then(() => this.play(source.messageId, true));
|
|
2563
|
+
}
|
|
2564
|
+
dispose() {
|
|
2565
|
+
if (this.disposed) return;
|
|
2566
|
+
this.disposed = true;
|
|
2567
|
+
this.stopActive();
|
|
2568
|
+
this.clearAudioOwnership(true);
|
|
2569
|
+
for (const state of this.states.values()) {
|
|
2570
|
+
state.abort?.abort();
|
|
2571
|
+
revokeAudioUrl(state.audioUrl);
|
|
2572
|
+
}
|
|
2573
|
+
this.states.clear();
|
|
2574
|
+
this.observed.clear();
|
|
2575
|
+
this.observedInteractions.clear();
|
|
2576
|
+
this.voiceCache.clear();
|
|
2577
|
+
this.analyserContext?.close().catch(() => {});
|
|
2578
|
+
this.analyserContext = void 0;
|
|
2579
|
+
this.analyserSource = void 0;
|
|
2580
|
+
this.analyser = void 0;
|
|
2581
|
+
this.analyserData = void 0;
|
|
2582
|
+
this.listeners.clear();
|
|
2583
|
+
}
|
|
2584
|
+
async voiceFor(binding, preferred) {
|
|
2585
|
+
if (preferred !== void 0 && preferred.length > 0) return preferred;
|
|
2586
|
+
const key = `${binding.provider}\n${binding.model}`;
|
|
2587
|
+
let promise = this.voiceCache.get(key);
|
|
2588
|
+
if (promise === void 0) {
|
|
2589
|
+
promise = speechApi.voices({
|
|
2590
|
+
model: binding.aliases?.[0] ?? binding.model,
|
|
2591
|
+
provider: binding.provider
|
|
2592
|
+
}).then((result) => result.voices.map((voice) => voice.id)).catch((error) => {
|
|
2593
|
+
this.voiceCache.delete(key);
|
|
2594
|
+
throw error;
|
|
2595
|
+
});
|
|
2596
|
+
this.voiceCache.set(key, promise);
|
|
2597
|
+
}
|
|
2598
|
+
const voices = await promise;
|
|
2599
|
+
const localized = preferredTtsSelection(binding.model.startsWith("minimax/") ? "zh" : "en");
|
|
2600
|
+
return binding.model === localized.model && binding.provider === localized.provider && voices.includes(localized.voice) ? localized.voice : binding.defaultVoice !== void 0 ? binding.defaultVoice : voices[0];
|
|
2601
|
+
}
|
|
2602
|
+
startAnalysis(messageId, generation) {
|
|
2603
|
+
this.stopAnalysis();
|
|
2604
|
+
this.resetAnalyser();
|
|
2605
|
+
this.ensureAnalyser();
|
|
2606
|
+
if (typeof requestAnimationFrame === "undefined") return;
|
|
2607
|
+
const frame = (time) => {
|
|
2608
|
+
if (!this.owns(generation, messageId)) return;
|
|
2609
|
+
if (time - this.analysisTick >= 45) {
|
|
2610
|
+
this.analysisTick = time;
|
|
2611
|
+
this.patch(messageId, { peaks: this.readAmplitude(time) });
|
|
2612
|
+
}
|
|
2613
|
+
this.analysisFrame = requestAnimationFrame(frame);
|
|
2614
|
+
};
|
|
2615
|
+
this.analysisFrame = requestAnimationFrame(frame);
|
|
2616
|
+
}
|
|
2617
|
+
ensureAnalyser() {
|
|
2618
|
+
if (this.analyser !== void 0) {
|
|
2619
|
+
this.analyserContext?.resume().catch(() => {});
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
if (typeof AudioContext === "undefined" || this.audio.captureStream === void 0) return;
|
|
2623
|
+
try {
|
|
2624
|
+
const context = new AudioContext();
|
|
2625
|
+
const analyser = context.createAnalyser();
|
|
2626
|
+
analyser.fftSize = 128;
|
|
2627
|
+
analyser.smoothingTimeConstant = .62;
|
|
2628
|
+
const source = context.createMediaStreamSource(this.audio.captureStream());
|
|
2629
|
+
source.connect(analyser);
|
|
2630
|
+
this.analyserContext = context;
|
|
2631
|
+
this.analyserSource = source;
|
|
2632
|
+
this.analyser = analyser;
|
|
2633
|
+
this.analyserData = new Uint8Array(analyser.fftSize);
|
|
2634
|
+
context.resume().catch(() => {});
|
|
2635
|
+
} catch {
|
|
2636
|
+
this.analyserContext = void 0;
|
|
2637
|
+
this.analyserSource = void 0;
|
|
2638
|
+
this.analyser = void 0;
|
|
2639
|
+
this.analyserData = void 0;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
readAmplitude(time) {
|
|
2643
|
+
if (this.analyser !== void 0 && this.analyserData !== void 0) {
|
|
2644
|
+
this.analyser.getByteTimeDomainData(this.analyserData);
|
|
2645
|
+
let signalPeak = 0;
|
|
2646
|
+
const peaks = Array.from({ length: 48 }, (_, index) => {
|
|
2647
|
+
const start = Math.floor(index * this.analyserData.length / 48);
|
|
2648
|
+
const end = Math.max(start + 1, Math.floor((index + 1) * this.analyserData.length / 48));
|
|
2649
|
+
let amplitude = 0;
|
|
2650
|
+
for (let sample = start; sample < end; sample += 1) amplitude = Math.max(amplitude, Math.abs((this.analyserData[sample] ?? 128) - 128) / 128);
|
|
2651
|
+
signalPeak = Math.max(signalPeak, amplitude);
|
|
2652
|
+
return Math.min(1, .06 + Math.sqrt(amplitude) * 2.4);
|
|
2653
|
+
});
|
|
2654
|
+
if (signalPeak > .004) return peaks;
|
|
2655
|
+
}
|
|
2656
|
+
return this.activityPeaks(time);
|
|
2657
|
+
}
|
|
2658
|
+
activityPeaks(time) {
|
|
2659
|
+
return Array.from({ length: 48 }, (_, index) => {
|
|
2660
|
+
const carrier = .5 + .5 * Math.sin(time * .014 + index * 1.37);
|
|
2661
|
+
const envelope = .5 + .5 * Math.sin(time * .0037 + index * .29);
|
|
2662
|
+
return .1 + .52 * carrier * envelope;
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
stopAnalysis() {
|
|
2666
|
+
if (this.analysisFrame !== void 0 && typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(this.analysisFrame);
|
|
2667
|
+
this.analysisFrame = void 0;
|
|
2668
|
+
this.analysisTick = 0;
|
|
2669
|
+
}
|
|
2670
|
+
clearPauseResolution() {
|
|
2671
|
+
if (this.pauseResolutionTimer !== void 0) clearTimeout(this.pauseResolutionTimer);
|
|
2672
|
+
this.pauseResolutionTimer = void 0;
|
|
2673
|
+
}
|
|
2674
|
+
resetAnalyser() {
|
|
2675
|
+
this.analyserContext?.close().catch(() => {});
|
|
2676
|
+
this.analyserContext = void 0;
|
|
2677
|
+
this.analyserSource = void 0;
|
|
2678
|
+
this.analyser = void 0;
|
|
2679
|
+
this.analyserData = void 0;
|
|
2680
|
+
}
|
|
2681
|
+
ensure(messageId) {
|
|
2682
|
+
let state = this.states.get(messageId);
|
|
2683
|
+
if (state === void 0) {
|
|
2684
|
+
state = {
|
|
2685
|
+
phase: "idle",
|
|
2686
|
+
duration: 0,
|
|
2687
|
+
progress: 0,
|
|
2688
|
+
peaks: EMPTY_PEAKS,
|
|
2689
|
+
requestGeneration: 0,
|
|
2690
|
+
mounts: 0,
|
|
2691
|
+
releaseGeneration: 0
|
|
2692
|
+
};
|
|
2693
|
+
this.states.set(messageId, state);
|
|
2694
|
+
}
|
|
2695
|
+
return state;
|
|
2696
|
+
}
|
|
2697
|
+
patch(messageId, patch) {
|
|
2698
|
+
Object.assign(this.ensure(messageId), patch);
|
|
2699
|
+
this.publish();
|
|
2700
|
+
}
|
|
2701
|
+
fail(messageId, message) {
|
|
2702
|
+
this.patch(messageId, {
|
|
2703
|
+
phase: "error",
|
|
2704
|
+
error: message
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
owns(generation, messageId) {
|
|
2708
|
+
return this.playbackGeneration === generation && this.activeMessageId === messageId;
|
|
2709
|
+
}
|
|
2710
|
+
finishPlayback(generation, messageId, ended) {
|
|
2711
|
+
if (!this.owns(generation, messageId)) return;
|
|
2712
|
+
const state = this.states.get(messageId);
|
|
2713
|
+
const measuredDuration = Number.isFinite(this.audio.duration) ? Math.max(0, this.audio.duration) : state?.duration ?? 0;
|
|
2714
|
+
if (state !== void 0) Object.assign(state, {
|
|
2715
|
+
phase: "ready",
|
|
2716
|
+
duration: measuredDuration,
|
|
2717
|
+
progress: ended ? measuredDuration : this.audio.currentTime,
|
|
2718
|
+
ended
|
|
2719
|
+
});
|
|
2720
|
+
++this.playbackGeneration;
|
|
2721
|
+
if (state?.ephemeral === true) {
|
|
2722
|
+
this.clearAudioOwnership(true);
|
|
2723
|
+
revokeAudioUrl(state.audioUrl);
|
|
2724
|
+
this.states.delete(messageId);
|
|
2725
|
+
} else this.clearAudioOwnership(false);
|
|
2726
|
+
this.publish();
|
|
2727
|
+
}
|
|
2728
|
+
stopActive() {
|
|
2729
|
+
const messageId = this.activeMessageId;
|
|
2730
|
+
if (messageId === void 0) return;
|
|
2731
|
+
const state = this.states.get(messageId);
|
|
2732
|
+
if (state !== void 0 && state.phase === "playing") Object.assign(state, {
|
|
2733
|
+
phase: "ready",
|
|
2734
|
+
progress: Math.max(0, this.audio.currentTime),
|
|
2735
|
+
ended: false
|
|
2736
|
+
});
|
|
2737
|
+
++this.playbackGeneration;
|
|
2738
|
+
this.audio.pause();
|
|
2739
|
+
this.clearAudioOwnership(false);
|
|
2740
|
+
this.publish();
|
|
2741
|
+
}
|
|
2742
|
+
clearAudioOwnership(unload) {
|
|
2743
|
+
this.stopAnalysis();
|
|
2744
|
+
this.clearPauseResolution();
|
|
2745
|
+
this.activeMessageId = void 0;
|
|
2746
|
+
this.audio.onended = null;
|
|
2747
|
+
this.audio.onerror = null;
|
|
2748
|
+
this.audio.onpause = null;
|
|
2749
|
+
this.audio.ontimeupdate = null;
|
|
2750
|
+
if (unload) {
|
|
2751
|
+
this.loadedMessageId = void 0;
|
|
2752
|
+
this.audio.removeAttribute("src");
|
|
2753
|
+
this.audio.load();
|
|
2754
|
+
}
|
|
2755
|
+
this.publish();
|
|
2756
|
+
}
|
|
2757
|
+
release(messageId) {
|
|
2758
|
+
const state = this.states.get(messageId);
|
|
2759
|
+
if (state === void 0) return;
|
|
2760
|
+
if (this.activeMessageId === messageId) this.stopActive();
|
|
2761
|
+
if (this.loadedMessageId === messageId) this.clearAudioOwnership(true);
|
|
2762
|
+
state.abort?.abort();
|
|
2763
|
+
state.requestGeneration += 1;
|
|
2764
|
+
revokeAudioUrl(state.audioUrl);
|
|
2765
|
+
this.states.delete(messageId);
|
|
2766
|
+
this.publish();
|
|
2767
|
+
}
|
|
2768
|
+
publish() {
|
|
2769
|
+
this.snapshot = {
|
|
2770
|
+
messages: new Map([...this.states].map(([id, state]) => [id, {
|
|
2771
|
+
phase: state.phase,
|
|
2772
|
+
...state.summary === void 0 ? {} : { summary: state.summary },
|
|
2773
|
+
...state.audioUrl === void 0 ? {} : { audioUrl: state.audioUrl },
|
|
2774
|
+
duration: state.duration,
|
|
2775
|
+
progress: state.progress,
|
|
2776
|
+
peaks: state.peaks,
|
|
2777
|
+
...state.error === void 0 ? {} : { error: state.error },
|
|
2778
|
+
...state.ended === void 0 ? {} : { ended: state.ended }
|
|
2779
|
+
}])),
|
|
2780
|
+
...this.activeMessageId === void 0 ? {} : { activeMessageId: this.activeMessageId }
|
|
2781
|
+
};
|
|
2782
|
+
for (const listener of this.listeners) listener();
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
|
|
2786
|
+
//#endregion
|
|
2787
|
+
//#region src/client/SpokenSummary.tsx
|
|
2788
|
+
const EMPTY_REQUESTS = [];
|
|
2789
|
+
function preparationSettings(controller, settings, locale) {
|
|
2790
|
+
const fallback = preferredTtsSelection(locale);
|
|
2791
|
+
const selection = settings?.ttsModel !== void 0 && settings.ttsProvider !== void 0 && settings.ttsVoice !== void 0 ? {
|
|
2792
|
+
model: settings.ttsModel,
|
|
2793
|
+
provider: settings.ttsProvider,
|
|
2794
|
+
voice: settings.ttsVoice
|
|
2795
|
+
} : fallback;
|
|
2796
|
+
return {
|
|
2797
|
+
ttsEnabled: settings?.ttsEnabled ?? true,
|
|
2798
|
+
autoPlay: settings?.autoPlay ?? true,
|
|
2799
|
+
bindings: controller.getSnapshot().catalog?.ttsBindings ?? [],
|
|
2800
|
+
ttsModel: selection.model,
|
|
2801
|
+
ttsProvider: selection.provider,
|
|
2802
|
+
ttsVoice: selection.voice
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
function PlayIcon({ pause = false }) {
|
|
2806
|
+
return pause ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2807
|
+
viewBox: "0 0 20 20",
|
|
2808
|
+
"aria-hidden": "true",
|
|
2809
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 5h3v10H6zM11 5h3v10h-3z" })
|
|
2810
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2811
|
+
viewBox: "0 0 20 20",
|
|
2812
|
+
"aria-hidden": "true",
|
|
2813
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m7 4 9 6-9 6z" })
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
function Waveform({ peaks, preparing, playing }) {
|
|
2817
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2818
|
+
className: styles.summaryWaveform,
|
|
2819
|
+
"aria-hidden": "true",
|
|
2820
|
+
"data-preparing": preparing ? "true" : void 0,
|
|
2821
|
+
"data-playing": playing ? "true" : void 0,
|
|
2822
|
+
children: peaks.map((peak, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { "--dsh-speech-peak": String(peak) } }, index))
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
function SpokenSummaryTail({ controller, spoken, scope, getLocale, seq, useSession, t }) {
|
|
2826
|
+
const nodes = useSession((snapshot) => snapshot.nodes);
|
|
2827
|
+
const requests = useSession((snapshot) => {
|
|
2828
|
+
const views = snapshot.views;
|
|
2829
|
+
if (views?.get === void 0) return EMPTY_REQUESTS;
|
|
2830
|
+
return views.get("trajectory")?.requests ?? EMPTY_REQUESTS;
|
|
2831
|
+
});
|
|
2832
|
+
const client = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
2833
|
+
const scopeState = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
2834
|
+
const spokenState = (0, react.useSyncExternalStore)(spoken.subscribe, spoken.getSnapshot);
|
|
2835
|
+
const locale = getLocale();
|
|
2836
|
+
const sources = (0, react.useMemo)(() => spokenSources(nodes, locale, requests), [
|
|
2837
|
+
nodes,
|
|
2838
|
+
locale,
|
|
2839
|
+
requests
|
|
2840
|
+
]);
|
|
2841
|
+
const closing = nodes.find((node) => node.kind === "assistant" && node.seq === seq);
|
|
2842
|
+
const id = closing?.kind === "assistant" && closing.messageId !== void 0 ? String(closing.messageId) : `turn-tail:${String(seq)}`;
|
|
2843
|
+
const source = sources.find((candidate) => candidate.messageId === id);
|
|
2844
|
+
const settings = (0, react.useMemo)(() => preparationSettings(controller, scopeState.value, locale), [
|
|
2845
|
+
controller,
|
|
2846
|
+
scopeState.value,
|
|
2847
|
+
client.catalog,
|
|
2848
|
+
locale
|
|
2849
|
+
]);
|
|
2850
|
+
const state = spokenState.messages.get(id) ?? {
|
|
2851
|
+
phase: "idle",
|
|
2852
|
+
duration: 0,
|
|
2853
|
+
progress: 0,
|
|
2854
|
+
peaks: []
|
|
2855
|
+
};
|
|
2856
|
+
const latestAudioMessageId = [...sources].reverse().find((candidate) => spokenState.messages.get(candidate.messageId)?.audioUrl !== void 0)?.messageId;
|
|
2857
|
+
const latestSourceMessageId = sources.at(-1)?.messageId;
|
|
2858
|
+
const legacyAutoplayWasUsed = scopeState.user.autoPlay !== void 0;
|
|
2859
|
+
const autoplayInlineRevealed = scopeState.value?.autoplayInlineRevealed === true || legacyAutoplayWasUsed;
|
|
2860
|
+
const autoplayHostMessageId = latestAudioMessageId ?? (autoplayInlineRevealed ? latestSourceMessageId : void 0);
|
|
2861
|
+
(0, react.useEffect)(() => spoken.mount(id), [spoken, id]);
|
|
2862
|
+
(0, react.useEffect)(() => {
|
|
2863
|
+
if (autoplayHostMessageId !== id || scopeState.value?.autoplayInlineRevealed === true || !scopeState.writable) return;
|
|
2864
|
+
scope.set("autoplayInlineRevealed", true);
|
|
2865
|
+
}, [
|
|
2866
|
+
autoplayHostMessageId,
|
|
2867
|
+
id,
|
|
2868
|
+
scope,
|
|
2869
|
+
scopeState.value?.autoplayInlineRevealed,
|
|
2870
|
+
scopeState.writable
|
|
2871
|
+
]);
|
|
2872
|
+
if (!settings.ttsEnabled || source === void 0) return null;
|
|
2873
|
+
const preparing = state.phase === "preparing";
|
|
2874
|
+
const unavailable = source.route === void 0 || client.catalog !== void 0 && settings.bindings.length === 0;
|
|
2875
|
+
const replay = state.phase === "ready" && state.ended === true;
|
|
2876
|
+
const paused = state.phase === "ready" && state.progress > 0 && !replay;
|
|
2877
|
+
const label = unavailable ? t("summaryUnavailable") : state.phase === "playing" ? t("summaryPause") : preparing ? t("summaryPreparing") : state.phase === "error" ? t("summaryRetry") : replay ? t("summaryReplay") : paused ? t("summaryResume") : state.audioUrl === void 0 ? t("summaryGenerate") : t("summaryPlay");
|
|
2878
|
+
const activate = () => {
|
|
2879
|
+
if (unavailable) return;
|
|
2880
|
+
if (state.phase === "playing") {
|
|
2881
|
+
spoken.pause(id);
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
if (state.phase === "ready") {
|
|
2885
|
+
spoken.play(id, true);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (state.phase === "error") {
|
|
2889
|
+
spoken.retry(source, settings);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
spoken.prepare(source, settings, false).then(() => spoken.play(id, true));
|
|
2893
|
+
};
|
|
2894
|
+
const showLabel = state.phase === "idle" || state.phase === "error" || unavailable;
|
|
2895
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2896
|
+
className: styles.summaryPlayer,
|
|
2897
|
+
"data-phase": state.phase,
|
|
2898
|
+
children: [
|
|
2899
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2900
|
+
className: styles.summaryControl,
|
|
2901
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2902
|
+
type: "button",
|
|
2903
|
+
className: styles.summaryButton,
|
|
2904
|
+
disabled: preparing || unavailable,
|
|
2905
|
+
"aria-label": label,
|
|
2906
|
+
title: label,
|
|
2907
|
+
onClick: activate,
|
|
2908
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayIcon, { pause: state.phase === "playing" })
|
|
2909
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Waveform, {
|
|
2910
|
+
peaks: state.peaks.length === 48 ? state.peaks : Array.from({ length: 48 }, () => .12),
|
|
2911
|
+
preparing,
|
|
2912
|
+
playing: state.phase === "playing"
|
|
2913
|
+
})]
|
|
2914
|
+
}),
|
|
2915
|
+
showLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2916
|
+
className: styles.summaryLabel,
|
|
2917
|
+
children: label
|
|
2918
|
+
}) : null,
|
|
2919
|
+
autoplayHostMessageId !== id ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2920
|
+
type: "button",
|
|
2921
|
+
className: styles.autoplayToggle,
|
|
2922
|
+
role: "switch",
|
|
2923
|
+
"aria-checked": scopeState.value?.autoPlay ?? true,
|
|
2924
|
+
"aria-label": `${t("autoplayInline")}: ${scopeState.value?.autoPlay ?? true ? t("autoplayOn") : t("autoplayOff")}`,
|
|
2925
|
+
title: `${t("autoplayInline")}: ${scopeState.value?.autoPlay ?? true ? t("autoplayOn") : t("autoplayOff")}`,
|
|
2926
|
+
disabled: !scopeState.writable,
|
|
2927
|
+
onClick: () => {
|
|
2928
|
+
scope.set("autoPlay", !(scopeState.value?.autoPlay ?? true));
|
|
2929
|
+
},
|
|
2930
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2931
|
+
className: styles.switchTrack,
|
|
2932
|
+
"aria-hidden": "true",
|
|
2933
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
|
|
2934
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("autoplayInline") })]
|
|
2935
|
+
}),
|
|
2936
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2937
|
+
className: styles.srOnly,
|
|
2938
|
+
role: "status",
|
|
2939
|
+
"aria-live": "polite",
|
|
2940
|
+
children: state.phase === "error" ? state.error ?? t("summaryError") : label
|
|
2941
|
+
})
|
|
2942
|
+
]
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
function SpokenSessionObserver({ controller, spoken, scope, getLocale, sessionId, useSession }) {
|
|
2946
|
+
const state = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
2947
|
+
const client = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
2948
|
+
const pending = useSession((snapshot) => snapshot.pending);
|
|
2949
|
+
const nodes = useSession((snapshot) => snapshot.nodes);
|
|
2950
|
+
const requests = useSession((snapshot) => {
|
|
2951
|
+
const views = snapshot.views;
|
|
2952
|
+
if (views?.get === void 0) return EMPTY_REQUESTS;
|
|
2953
|
+
return views.get("trajectory")?.requests ?? EMPTY_REQUESTS;
|
|
2954
|
+
});
|
|
2955
|
+
const speechLocale = state.value?.language === void 0 || state.value.language === "auto" ? getLocale() : state.value.language;
|
|
2956
|
+
const settings = (0, react.useMemo)(() => preparationSettings(controller, state.value, speechLocale), [
|
|
2957
|
+
client.catalog,
|
|
2958
|
+
controller,
|
|
2959
|
+
speechLocale,
|
|
2960
|
+
state.value
|
|
2961
|
+
]);
|
|
2962
|
+
const sources = (0, react.useMemo)(() => spokenSources(nodes, speechLocale, requests), [
|
|
2963
|
+
nodes,
|
|
2964
|
+
requests,
|
|
2965
|
+
speechLocale
|
|
2966
|
+
]);
|
|
2967
|
+
(0, react.useEffect)(() => {
|
|
2968
|
+
controller.ensureMetadata();
|
|
2969
|
+
}, [controller]);
|
|
2970
|
+
(0, react.useEffect)(() => {
|
|
2971
|
+
spoken.setEnabled(settings.ttsEnabled);
|
|
2972
|
+
}, [settings.ttsEnabled, spoken]);
|
|
2973
|
+
(0, react.useEffect)(() => {
|
|
2974
|
+
if (client.catalog === void 0) return;
|
|
2975
|
+
spoken.observeSession(String(sessionId), sources, settings);
|
|
2976
|
+
}, [
|
|
2977
|
+
client.catalog,
|
|
2978
|
+
sessionId,
|
|
2979
|
+
settings,
|
|
2980
|
+
sources,
|
|
2981
|
+
spoken
|
|
2982
|
+
]);
|
|
2983
|
+
(0, react.useEffect)(() => {
|
|
2984
|
+
if (client.catalog === void 0) return;
|
|
2985
|
+
spoken.observeInteractions(String(sessionId), pending.map((item) => item.key), speechLocale, settings);
|
|
2986
|
+
}, [
|
|
2987
|
+
client.catalog,
|
|
2988
|
+
pending,
|
|
2989
|
+
sessionId,
|
|
2990
|
+
settings,
|
|
2991
|
+
speechLocale,
|
|
2992
|
+
spoken
|
|
2993
|
+
]);
|
|
2994
|
+
return null;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
1605
2997
|
//#endregion
|
|
1606
2998
|
//#region src/client/locales.ts
|
|
1607
2999
|
const en = {
|
|
@@ -1628,6 +3020,8 @@ const en = {
|
|
|
1628
3020
|
disconnect: "Disconnect",
|
|
1629
3021
|
settings: "Recognition",
|
|
1630
3022
|
model: "STT model",
|
|
3023
|
+
modelSearchPlaceholder: "Search or select a model",
|
|
3024
|
+
modelNoResults: "No matching models",
|
|
1631
3025
|
provider: "Provider",
|
|
1632
3026
|
language: "Language",
|
|
1633
3027
|
invalidLanguage: "Enter Auto or a valid language tag such as en, zh-CN, or ja.",
|
|
@@ -1652,6 +3046,7 @@ const en = {
|
|
|
1652
3046
|
saved: "Saved",
|
|
1653
3047
|
micStart: "Start voice input",
|
|
1654
3048
|
micStop: "Stop voice input",
|
|
3049
|
+
micSend: "Finish voice input and send",
|
|
1655
3050
|
micCancel: "Cancel voice input",
|
|
1656
3051
|
starting: "Preparing microphone…",
|
|
1657
3052
|
listening: "Listening…",
|
|
@@ -1664,7 +3059,34 @@ const en = {
|
|
|
1664
3059
|
disconnectedMic: "Connect AllModels in Settings → Speech",
|
|
1665
3060
|
emptyMic: "Top up your AllModels balance in Settings → Speech",
|
|
1666
3061
|
anotherMic: "The microphone is active in another session",
|
|
1667
|
-
liveModel: "{provider} · {model}"
|
|
3062
|
+
liveModel: "{provider} · {model}",
|
|
3063
|
+
spokenSummaries: "Spoken summaries",
|
|
3064
|
+
spokenSummariesHint: "Completed answers are summarized with their recorded LLM route, then spoken through AllModels. Nothing is added to the conversation.",
|
|
3065
|
+
ttsEnabled: "Text-to-speech summaries",
|
|
3066
|
+
ttsModel: "TTS model",
|
|
3067
|
+
ttsProvider: "TTS provider",
|
|
3068
|
+
ttsVoice: "Voice",
|
|
3069
|
+
voiceSearch: "Search AllModels voices",
|
|
3070
|
+
voiceSearchHint: "Optional. Choosing a voice automatically selects its compatible model and provider.",
|
|
3071
|
+
voiceSearchPlaceholder: "Search by name, style, accent, or description",
|
|
3072
|
+
modelVoiceSearchHint: "Search only voices compatible with {model}.",
|
|
3073
|
+
modelVoiceSearchPlaceholder: "Type to search voices for this model",
|
|
3074
|
+
voiceNoResults: "No matching voices",
|
|
3075
|
+
ttsUnavailable: "No compatible synchronous MP3 text-to-speech route is currently advertised.",
|
|
3076
|
+
autoplayGlobal: "Autoplay",
|
|
3077
|
+
autoplayShort: "Spoken summaries",
|
|
3078
|
+
autoplayInline: "Auto play",
|
|
3079
|
+
autoplayOn: "Autoplay on",
|
|
3080
|
+
autoplayOff: "Autoplay off",
|
|
3081
|
+
summaryPlay: "Play summary",
|
|
3082
|
+
summaryGenerate: "Generate and play summary",
|
|
3083
|
+
summaryPause: "Pause summary",
|
|
3084
|
+
summaryResume: "Resume summary",
|
|
3085
|
+
summaryReplay: "Replay summary",
|
|
3086
|
+
summaryRetry: "Retry spoken summary",
|
|
3087
|
+
summaryPreparing: "Preparing spoken summary…",
|
|
3088
|
+
summaryError: "Could not prepare the spoken summary.",
|
|
3089
|
+
summaryUnavailable: "Spoken summary unavailable for this answer"
|
|
1668
3090
|
};
|
|
1669
3091
|
const zh = {
|
|
1670
3092
|
nav: "语音",
|
|
@@ -1690,6 +3112,8 @@ const zh = {
|
|
|
1690
3112
|
disconnect: "断开连接",
|
|
1691
3113
|
settings: "语音识别",
|
|
1692
3114
|
model: "STT 模型",
|
|
3115
|
+
modelSearchPlaceholder: "搜索或选择模型",
|
|
3116
|
+
modelNoResults: "没有匹配的模型",
|
|
1693
3117
|
provider: "服务商",
|
|
1694
3118
|
language: "语言",
|
|
1695
3119
|
invalidLanguage: "请输入“自动”或有效的语言标签,例如 en、zh-CN 或 ja。",
|
|
@@ -1714,6 +3138,7 @@ const zh = {
|
|
|
1714
3138
|
saved: "已保存",
|
|
1715
3139
|
micStart: "开始语音输入",
|
|
1716
3140
|
micStop: "停止语音输入",
|
|
3141
|
+
micSend: "完成语音输入并发送",
|
|
1717
3142
|
micCancel: "取消语音输入",
|
|
1718
3143
|
starting: "正在准备麦克风…",
|
|
1719
3144
|
listening: "正在聆听…",
|
|
@@ -1726,7 +3151,34 @@ const zh = {
|
|
|
1726
3151
|
disconnectedMic: "请前往“设置 → 语音”连接 AllModels",
|
|
1727
3152
|
emptyMic: "请前往“设置 → 语音”为 AllModels 充值",
|
|
1728
3153
|
anotherMic: "麦克风正在另一个会话中使用",
|
|
1729
|
-
liveModel: "{provider} · {model}"
|
|
3154
|
+
liveModel: "{provider} · {model}",
|
|
3155
|
+
spokenSummaries: "语音摘要",
|
|
3156
|
+
spokenSummariesHint: "使用回答中记录的 LLM 路由生成简短摘要,再通过 AllModels 播放。不会向对话添加任何内容。",
|
|
3157
|
+
ttsEnabled: "文本转语音摘要",
|
|
3158
|
+
ttsModel: "TTS 模型",
|
|
3159
|
+
ttsProvider: "TTS 服务商",
|
|
3160
|
+
ttsVoice: "声音",
|
|
3161
|
+
voiceSearch: "搜索 AllModels 声音",
|
|
3162
|
+
voiceSearchHint: "可选。选择声音后会自动设置兼容的模型和服务商。",
|
|
3163
|
+
voiceSearchPlaceholder: "按名称、风格、口音或描述搜索",
|
|
3164
|
+
modelVoiceSearchHint: "仅搜索与 {model} 兼容的声音。",
|
|
3165
|
+
modelVoiceSearchPlaceholder: "输入关键词搜索此模型的声音",
|
|
3166
|
+
voiceNoResults: "没有匹配的声音",
|
|
3167
|
+
ttsUnavailable: "目前没有兼容的同步 MP3 文本转语音路由。",
|
|
3168
|
+
autoplayGlobal: "自动播放",
|
|
3169
|
+
autoplayShort: "语音摘要",
|
|
3170
|
+
autoplayInline: "自动播放",
|
|
3171
|
+
autoplayOn: "自动播放已开启",
|
|
3172
|
+
autoplayOff: "自动播放已关闭",
|
|
3173
|
+
summaryPlay: "播放摘要",
|
|
3174
|
+
summaryGenerate: "生成并播放摘要",
|
|
3175
|
+
summaryPause: "暂停摘要",
|
|
3176
|
+
summaryResume: "继续播放摘要",
|
|
3177
|
+
summaryReplay: "重新播放摘要",
|
|
3178
|
+
summaryRetry: "重试语音摘要",
|
|
3179
|
+
summaryPreparing: "正在准备语音摘要…",
|
|
3180
|
+
summaryError: "无法准备语音摘要。",
|
|
3181
|
+
summaryUnavailable: "此回答无法生成语音摘要"
|
|
1730
3182
|
};
|
|
1731
3183
|
|
|
1732
3184
|
//#endregion
|
|
@@ -1738,6 +3190,7 @@ const inject = [
|
|
|
1738
3190
|
];
|
|
1739
3191
|
function apply(ctx) {
|
|
1740
3192
|
const controller = new SpeechController();
|
|
3193
|
+
const spoken = new SpokenSummaryController();
|
|
1741
3194
|
const scope = ctx.settingsScope.bind({ namespace: "dsh-speech" });
|
|
1742
3195
|
const getLocale = () => ctx.locale.getLocale().active;
|
|
1743
3196
|
ctx.effect(() => ctx.locale.register("speech", {
|
|
@@ -1776,7 +3229,9 @@ function apply(ctx) {
|
|
|
1776
3229
|
}, "dsh-speech: settings navigation icon");
|
|
1777
3230
|
ctx.effect(() => () => {
|
|
1778
3231
|
controller.dispose();
|
|
1779
|
-
|
|
3232
|
+
spoken.dispose();
|
|
3233
|
+
}, "dsh-speech: browser controllers");
|
|
3234
|
+
controller.ensureMetadata();
|
|
1780
3235
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1781
3236
|
name: "settings.section",
|
|
1782
3237
|
id: "speech",
|
|
@@ -1822,11 +3277,39 @@ function apply(ctx) {
|
|
|
1822
3277
|
getLocale
|
|
1823
3278
|
})
|
|
1824
3279
|
}, SpeechInputDock);
|
|
3280
|
+
const spokenTail = scoped.slots.register({
|
|
3281
|
+
name: "conversation.chat.turnTail",
|
|
3282
|
+
priority: 20,
|
|
3283
|
+
select: (owner) => ({
|
|
3284
|
+
turn: owner.turn.turn,
|
|
3285
|
+
seq: owner.seq
|
|
3286
|
+
}),
|
|
3287
|
+
locale: "speech",
|
|
3288
|
+
inject: () => ({
|
|
3289
|
+
controller,
|
|
3290
|
+
spoken,
|
|
3291
|
+
scope,
|
|
3292
|
+
getLocale
|
|
3293
|
+
})
|
|
3294
|
+
}, SpokenSummaryTail);
|
|
3295
|
+
const spokenObserver = scoped.slots.register({
|
|
3296
|
+
name: "conversation.composer.dock",
|
|
3297
|
+
id: "spoken-summary-observer",
|
|
3298
|
+
order: 110,
|
|
3299
|
+
inject: () => ({
|
|
3300
|
+
controller,
|
|
3301
|
+
spoken,
|
|
3302
|
+
scope,
|
|
3303
|
+
getLocale
|
|
3304
|
+
})
|
|
3305
|
+
}, SpokenSessionObserver);
|
|
1825
3306
|
return () => {
|
|
1826
3307
|
controller.detachBlocks(blocks);
|
|
1827
3308
|
mic();
|
|
1828
3309
|
dock();
|
|
1829
3310
|
inputDock();
|
|
3311
|
+
spokenTail();
|
|
3312
|
+
spokenObserver();
|
|
1830
3313
|
};
|
|
1831
3314
|
});
|
|
1832
3315
|
}
|