@allmodels/dsh-speech 0.1.2 → 0.1.4
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 +29 -4
- package/lib/client.js +1926 -139
- package/lib/index.d.ts +23 -2
- package/lib/index.js +670 -27
- package/package.json +6 -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,44 @@ 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_TTS_CHARACTERS = 4096;
|
|
307
|
+
const MAX_SUMMARY_REQUEST_CHARACTERS = 16e3;
|
|
308
|
+
const MAX_SUMMARY_ANSWER_CHARACTERS = 64e3;
|
|
309
|
+
const SUMMARY_PROMPT_VERSION = 1;
|
|
310
|
+
const DEFAULT_ENGLISH_TTS = {
|
|
311
|
+
model: "fish/s2.1-pro",
|
|
312
|
+
provider: "fish",
|
|
313
|
+
voice: "03397b4c4be74759b72533b663fbd001",
|
|
314
|
+
name: "Elon Musk (Noise reduction)"
|
|
315
|
+
};
|
|
316
|
+
const DEFAULT_CHINESE_TTS = {
|
|
317
|
+
model: "minimax/speech-2.8-hd",
|
|
318
|
+
provider: "minimax",
|
|
319
|
+
voice: "Chinese (Mandarin)_HK_Flight_Attendant",
|
|
320
|
+
name: "HK Flight Attendant"
|
|
321
|
+
};
|
|
322
|
+
function selectTtsBinding(bindings, preferred) {
|
|
323
|
+
if (preferred?.model !== void 0) {
|
|
324
|
+
const preferredModel = preferred.model;
|
|
325
|
+
const matchesModel = (binding) => binding.model === preferredModel || binding.canonical === preferredModel || !preferredModel.includes("/") && binding.model.endsWith(`/${preferredModel}`);
|
|
326
|
+
const exact = bindings.find((binding) => matchesModel(binding) && (preferred.provider === void 0 || binding.provider === preferred.provider));
|
|
327
|
+
if (exact !== void 0) return exact;
|
|
328
|
+
const sameModel = bindings.find(matchesModel);
|
|
329
|
+
if (sameModel !== void 0) return sameModel;
|
|
330
|
+
}
|
|
331
|
+
return bindings.find((binding) => binding.isProviderDefault) ?? bindings[0];
|
|
332
|
+
}
|
|
333
|
+
function preferredTtsSelection(locale) {
|
|
334
|
+
return locale.toLowerCase().startsWith("zh") ? DEFAULT_CHINESE_TTS : DEFAULT_ENGLISH_TTS;
|
|
335
|
+
}
|
|
336
|
+
function selectLocalizedTtsBinding(bindings, locale, preferred) {
|
|
337
|
+
if (preferred?.model !== void 0 || preferred?.provider !== void 0) return selectTtsBinding(bindings, preferred);
|
|
338
|
+
const localized = preferredTtsSelection(locale);
|
|
339
|
+
return selectTtsBinding(bindings, {
|
|
340
|
+
model: localized.model,
|
|
341
|
+
provider: localized.provider
|
|
342
|
+
});
|
|
343
|
+
}
|
|
268
344
|
function selectBinding(bindings, locale, preferred) {
|
|
269
345
|
if (preferred?.model !== void 0) {
|
|
270
346
|
const preferredModel = preferred.model;
|
|
@@ -696,7 +772,23 @@ const styles = {
|
|
|
696
772
|
srOnly: "dsh-speech-sr-only",
|
|
697
773
|
dock: "dsh-speech-dock",
|
|
698
774
|
dockError: "dsh-speech-dock-error",
|
|
699
|
-
dockDetail: "dsh-speech-dock-detail"
|
|
775
|
+
dockDetail: "dsh-speech-dock-detail",
|
|
776
|
+
switchLabel: "dsh-speech-switch-label",
|
|
777
|
+
switchTrack: "dsh-speech-switch-track",
|
|
778
|
+
summaryPlayer: "dsh-speech-summary-player",
|
|
779
|
+
summaryControl: "dsh-speech-summary-control",
|
|
780
|
+
summaryButton: "dsh-speech-summary-button",
|
|
781
|
+
summaryWaveform: "dsh-speech-summary-waveform",
|
|
782
|
+
summaryLabel: "dsh-speech-summary-label",
|
|
783
|
+
autoplayToggle: "dsh-speech-autoplay-toggle",
|
|
784
|
+
sidebarPlaying: "dsh-speech-sidebar-playing",
|
|
785
|
+
summaryToggles: "dsh-speech-summary-toggles",
|
|
786
|
+
summaryCache: "dsh-speech-summary-cache",
|
|
787
|
+
voicePicker: "dsh-speech-voice-picker",
|
|
788
|
+
voiceSelected: "dsh-speech-voice-selected",
|
|
789
|
+
voiceMenu: "dsh-speech-voice-menu",
|
|
790
|
+
voiceOption: "dsh-speech-voice-option",
|
|
791
|
+
voiceNotice: "dsh-speech-voice-notice"
|
|
700
792
|
};
|
|
701
793
|
const STYLE_TEXT = String.raw`
|
|
702
794
|
.dsh-speech-settings{display:grid;gap:16px;max-width:760px;padding:4px 2px 24px;color:var(--color-text,#e8e8e8)}
|
|
@@ -709,15 +801,22 @@ const STYLE_TEXT = String.raw`
|
|
|
709
801
|
.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)}
|
|
710
802
|
.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}
|
|
711
803
|
.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}
|
|
804
|
+
.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}
|
|
712
805
|
.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}
|
|
713
806
|
.dsh-speech-top-up{justify-content:flex-start;align-items:end;flex-wrap:wrap}.dsh-speech-top-up .dsh-speech-field{max-width:180px}
|
|
807
|
+
.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}
|
|
808
|
+
.dsh-speech-summary-cache{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-top:12px;border-top:1px solid var(--color-border,#343434)}.dsh-speech-summary-cache>div{display:grid;gap:4px}.dsh-speech-summary-cache>div>strong{font-size:13px}.dsh-speech-summary-cache>.dsh-speech-secondary{flex:none}
|
|
809
|
+
.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)}}
|
|
714
810
|
.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)}
|
|
715
811
|
: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)}}
|
|
716
812
|
.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}
|
|
717
813
|
[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}
|
|
814
|
+
.dsh-speech-sidebar-playing{width:16px;height:20px;display:inline-flex;align-items:center;justify-content:center;flex:none;margin-left:1px;color:var(--dsw-alias-state-business-primary,#679efe);pointer-events:none}.dsh-speech-sidebar-playing svg{width:13px;height:13px;fill:currentColor}[data-dsh-speech-playing-session]>.dsh-speech-sidebar-playing{animation:dsh-speech-sidebar-playing-in .16s ease-out}@keyframes dsh-speech-sidebar-playing-in{from{opacity:0;transform:scale(.72)}to{opacity:1;transform:scale(1)}}
|
|
718
815
|
.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)}
|
|
816
|
+
.dsh-speech-summary-player{width:100%}.dsh-speech-summary-player>.dsh-speech-autoplay-toggle{margin-left:auto}
|
|
817
|
+
:where(div):has(>.dsh-speech-summary-player){height:auto;flex-wrap:wrap}:where(div):has(>.dsh-speech-summary-player)>.dsh-speech-summary-player{order:-1;flex:0 0 100%;margin-bottom:4px}
|
|
719
818
|
@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}}
|
|
720
|
-
@media(prefers-reduced-motion:reduce){.dsh-speech-mic-tooltip{transition:none}.dsh-speech-recording-progress i{animation:none;border-right-color:currentColor}}
|
|
819
|
+
@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,.dsh-speech-sidebar-playing{animation:none}}
|
|
721
820
|
`;
|
|
722
821
|
|
|
723
822
|
//#endregion
|
|
@@ -1272,7 +1371,259 @@ function Field({ label, children, hint }) {
|
|
|
1272
1371
|
]
|
|
1273
1372
|
});
|
|
1274
1373
|
}
|
|
1275
|
-
function
|
|
1374
|
+
function voiceKey(voice) {
|
|
1375
|
+
return `${voice.provider ?? ""}\n${voice.model ?? ""}\n${voice.id}`;
|
|
1376
|
+
}
|
|
1377
|
+
function useVoiceOptions(filters, query, enabled = true) {
|
|
1378
|
+
const [voices, setVoices] = (0, react.useState)([]);
|
|
1379
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
1380
|
+
(0, react.useEffect)(() => {
|
|
1381
|
+
if (!enabled) {
|
|
1382
|
+
setVoices([]);
|
|
1383
|
+
setLoading(false);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const controller = new AbortController();
|
|
1387
|
+
const timer = setTimeout(() => {
|
|
1388
|
+
setLoading(true);
|
|
1389
|
+
speechApi.voices({
|
|
1390
|
+
...filters.model === void 0 ? {} : { model: filters.model },
|
|
1391
|
+
...filters.provider === void 0 ? {} : { provider: filters.provider },
|
|
1392
|
+
...query.trim().length === 0 ? {} : { q: query.trim() }
|
|
1393
|
+
}, controller.signal).then((result) => {
|
|
1394
|
+
setVoices(result.voices.filter((voice) => voice.model !== void 0 && voice.provider !== void 0));
|
|
1395
|
+
}).catch((cause) => {
|
|
1396
|
+
if (!(cause instanceof DOMException && cause.name === "AbortError")) setVoices([]);
|
|
1397
|
+
}).finally(() => {
|
|
1398
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
1399
|
+
});
|
|
1400
|
+
}, 250);
|
|
1401
|
+
return () => {
|
|
1402
|
+
clearTimeout(timer);
|
|
1403
|
+
controller.abort();
|
|
1404
|
+
};
|
|
1405
|
+
}, [
|
|
1406
|
+
enabled,
|
|
1407
|
+
filters.model,
|
|
1408
|
+
filters.provider,
|
|
1409
|
+
query
|
|
1410
|
+
]);
|
|
1411
|
+
return {
|
|
1412
|
+
voices,
|
|
1413
|
+
loading
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function VoicePicker({ label, hint, placeholder, loadingLabel, emptyLabel, query, voices, selected, loading, disabled, onQuery, onSelect }) {
|
|
1417
|
+
const inputId = (0, react.useId)();
|
|
1418
|
+
const listId = (0, react.useId)();
|
|
1419
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1420
|
+
const [active, setActive] = (0, react.useState)(0);
|
|
1421
|
+
const displayed = voices.slice(0, 24);
|
|
1422
|
+
(0, react.useEffect)(() => {
|
|
1423
|
+
setActive(0);
|
|
1424
|
+
}, [voices]);
|
|
1425
|
+
const keyDown = (event) => {
|
|
1426
|
+
if (event.key === "ArrowDown") {
|
|
1427
|
+
event.preventDefault();
|
|
1428
|
+
setOpen(true);
|
|
1429
|
+
setActive((current) => Math.min(displayed.length - 1, current + 1));
|
|
1430
|
+
} else if (event.key === "ArrowUp") {
|
|
1431
|
+
event.preventDefault();
|
|
1432
|
+
setOpen(true);
|
|
1433
|
+
setActive((current) => Math.max(0, current - 1));
|
|
1434
|
+
} else if (event.key === "Enter" && open && displayed[active] !== void 0) {
|
|
1435
|
+
event.preventDefault();
|
|
1436
|
+
onSelect(displayed[active]);
|
|
1437
|
+
setOpen(false);
|
|
1438
|
+
} else if (event.key === "Escape") setOpen(false);
|
|
1439
|
+
};
|
|
1440
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1441
|
+
className: styles.voicePicker,
|
|
1442
|
+
onBlur: (event) => {
|
|
1443
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
|
|
1444
|
+
},
|
|
1445
|
+
children: [
|
|
1446
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1447
|
+
className: styles.label,
|
|
1448
|
+
htmlFor: inputId,
|
|
1449
|
+
children: label
|
|
1450
|
+
}),
|
|
1451
|
+
selected === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1452
|
+
className: styles.voiceSelected,
|
|
1453
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: selected.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1454
|
+
selected.providerName ?? selected.provider,
|
|
1455
|
+
" · ",
|
|
1456
|
+
selected.model
|
|
1457
|
+
] })]
|
|
1458
|
+
}),
|
|
1459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1460
|
+
id: inputId,
|
|
1461
|
+
className: styles.input,
|
|
1462
|
+
type: "search",
|
|
1463
|
+
role: "combobox",
|
|
1464
|
+
"aria-autocomplete": "list",
|
|
1465
|
+
"aria-controls": listId,
|
|
1466
|
+
"aria-expanded": open,
|
|
1467
|
+
"aria-activedescendant": open && displayed[active] !== void 0 ? `${listId}-${String(active)}` : void 0,
|
|
1468
|
+
value: query,
|
|
1469
|
+
placeholder,
|
|
1470
|
+
disabled,
|
|
1471
|
+
onFocus: () => {
|
|
1472
|
+
setOpen(true);
|
|
1473
|
+
},
|
|
1474
|
+
onChange: (event) => {
|
|
1475
|
+
onQuery(event.target.value);
|
|
1476
|
+
setOpen(true);
|
|
1477
|
+
},
|
|
1478
|
+
onKeyDown: keyDown
|
|
1479
|
+
}),
|
|
1480
|
+
hint === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1481
|
+
className: styles.hint,
|
|
1482
|
+
children: hint
|
|
1483
|
+
}),
|
|
1484
|
+
!open ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1485
|
+
id: listId,
|
|
1486
|
+
className: styles.voiceMenu,
|
|
1487
|
+
role: "listbox",
|
|
1488
|
+
children: loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1489
|
+
className: styles.voiceNotice,
|
|
1490
|
+
role: "status",
|
|
1491
|
+
children: loadingLabel
|
|
1492
|
+
}) : displayed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1493
|
+
className: styles.voiceNotice,
|
|
1494
|
+
children: emptyLabel
|
|
1495
|
+
}) : displayed.map((voice, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1496
|
+
id: `${listId}-${String(index)}`,
|
|
1497
|
+
className: styles.voiceOption,
|
|
1498
|
+
type: "button",
|
|
1499
|
+
role: "option",
|
|
1500
|
+
"aria-selected": selected !== void 0 && voiceKey(voice) === voiceKey(selected),
|
|
1501
|
+
"data-active": index === active,
|
|
1502
|
+
onMouseDown: (event) => {
|
|
1503
|
+
event.preventDefault();
|
|
1504
|
+
},
|
|
1505
|
+
onMouseEnter: () => {
|
|
1506
|
+
setActive(index);
|
|
1507
|
+
},
|
|
1508
|
+
onClick: () => {
|
|
1509
|
+
onSelect(voice);
|
|
1510
|
+
setOpen(false);
|
|
1511
|
+
},
|
|
1512
|
+
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: [
|
|
1513
|
+
voice.providerName ?? voice.provider,
|
|
1514
|
+
" · ",
|
|
1515
|
+
voice.model
|
|
1516
|
+
] })] }), voice.description === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: voice.description })]
|
|
1517
|
+
}, voiceKey(voice)))
|
|
1518
|
+
})
|
|
1519
|
+
]
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
function ModelPicker({ label, placeholder, emptyLabel, options, value, disabled, onSelect }) {
|
|
1523
|
+
const inputId = (0, react.useId)();
|
|
1524
|
+
const listId = (0, react.useId)();
|
|
1525
|
+
const [query, setQuery] = (0, react.useState)(value);
|
|
1526
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1527
|
+
const [active, setActive] = (0, react.useState)(-1);
|
|
1528
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
1529
|
+
const displayed = normalizedQuery.length === 0 || query === value ? options : options.filter((option) => option.toLocaleLowerCase().includes(normalizedQuery));
|
|
1530
|
+
(0, react.useEffect)(() => {
|
|
1531
|
+
setQuery(value);
|
|
1532
|
+
}, [value]);
|
|
1533
|
+
(0, react.useEffect)(() => {
|
|
1534
|
+
setActive(-1);
|
|
1535
|
+
}, [query, options]);
|
|
1536
|
+
const select = (model) => {
|
|
1537
|
+
setQuery(model);
|
|
1538
|
+
setOpen(false);
|
|
1539
|
+
setActive(-1);
|
|
1540
|
+
onSelect(model);
|
|
1541
|
+
};
|
|
1542
|
+
const keyDown = (event) => {
|
|
1543
|
+
if (event.key === "ArrowDown") {
|
|
1544
|
+
event.preventDefault();
|
|
1545
|
+
setOpen(true);
|
|
1546
|
+
setActive((current) => Math.min(displayed.length - 1, current + 1));
|
|
1547
|
+
} else if (event.key === "ArrowUp") {
|
|
1548
|
+
event.preventDefault();
|
|
1549
|
+
setOpen(true);
|
|
1550
|
+
setActive((current) => current < 0 ? displayed.length - 1 : Math.max(0, current - 1));
|
|
1551
|
+
} else if (event.key === "Enter" && open && displayed[active] !== void 0) {
|
|
1552
|
+
event.preventDefault();
|
|
1553
|
+
select(displayed[active]);
|
|
1554
|
+
} else if (event.key === "Escape") {
|
|
1555
|
+
event.stopPropagation();
|
|
1556
|
+
setQuery(value);
|
|
1557
|
+
setOpen(false);
|
|
1558
|
+
setActive(-1);
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1562
|
+
className: styles.voicePicker,
|
|
1563
|
+
onBlur: (event) => {
|
|
1564
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
1565
|
+
setQuery(value);
|
|
1566
|
+
setOpen(false);
|
|
1567
|
+
setActive(-1);
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
children: [
|
|
1571
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1572
|
+
className: styles.label,
|
|
1573
|
+
htmlFor: inputId,
|
|
1574
|
+
children: label
|
|
1575
|
+
}),
|
|
1576
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1577
|
+
id: inputId,
|
|
1578
|
+
className: styles.input,
|
|
1579
|
+
type: "search",
|
|
1580
|
+
role: "combobox",
|
|
1581
|
+
"aria-autocomplete": "list",
|
|
1582
|
+
"aria-controls": listId,
|
|
1583
|
+
"aria-expanded": open,
|
|
1584
|
+
"aria-activedescendant": open && displayed[active] !== void 0 ? `${listId}-${String(active)}` : void 0,
|
|
1585
|
+
value: query,
|
|
1586
|
+
placeholder,
|
|
1587
|
+
disabled,
|
|
1588
|
+
onFocus: () => {
|
|
1589
|
+
setOpen(true);
|
|
1590
|
+
},
|
|
1591
|
+
onChange: (event) => {
|
|
1592
|
+
setQuery(event.target.value);
|
|
1593
|
+
setOpen(true);
|
|
1594
|
+
},
|
|
1595
|
+
onKeyDown: keyDown
|
|
1596
|
+
}),
|
|
1597
|
+
!open ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1598
|
+
id: listId,
|
|
1599
|
+
className: styles.voiceMenu,
|
|
1600
|
+
role: "listbox",
|
|
1601
|
+
children: displayed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1602
|
+
className: styles.voiceNotice,
|
|
1603
|
+
children: emptyLabel
|
|
1604
|
+
}) : displayed.map((model, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1605
|
+
id: `${listId}-${String(index)}`,
|
|
1606
|
+
className: styles.voiceOption,
|
|
1607
|
+
type: "button",
|
|
1608
|
+
role: "option",
|
|
1609
|
+
"aria-selected": model === value,
|
|
1610
|
+
"data-active": index === active,
|
|
1611
|
+
onMouseDown: (event) => {
|
|
1612
|
+
event.preventDefault();
|
|
1613
|
+
},
|
|
1614
|
+
onMouseEnter: () => {
|
|
1615
|
+
setActive(index);
|
|
1616
|
+
},
|
|
1617
|
+
onClick: () => {
|
|
1618
|
+
select(model);
|
|
1619
|
+
},
|
|
1620
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: model }) })
|
|
1621
|
+
}, model))
|
|
1622
|
+
})
|
|
1623
|
+
]
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
function SpeechSettings({ controller, scope, getLocale, summaryCache, t }) {
|
|
1276
1627
|
const client = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
1277
1628
|
const settings = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
1278
1629
|
const value = settings.value;
|
|
@@ -1288,6 +1639,8 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1288
1639
|
const [topUpUrl, setTopUpUrl] = (0, react.useState)(null);
|
|
1289
1640
|
const [languageDraft, setLanguageDraft] = (0, react.useState)("auto");
|
|
1290
1641
|
const [contextDraft, setContextDraft] = (0, react.useState)("");
|
|
1642
|
+
const [voiceSearch, setVoiceSearch] = (0, react.useState)("");
|
|
1643
|
+
const [modelVoiceSearch, setModelVoiceSearch] = (0, react.useState)("");
|
|
1291
1644
|
(0, react.useEffect)(() => {
|
|
1292
1645
|
controller.ensureMetadata();
|
|
1293
1646
|
}, [controller]);
|
|
@@ -1318,6 +1671,33 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1318
1671
|
const selectedProvider = providers.some((binding) => binding.provider === value?.provider) ? value?.provider ?? "" : providers.find((binding) => binding.isProviderDefault)?.provider ?? providers[0]?.provider ?? "";
|
|
1319
1672
|
const selectedBinding = providers.find((binding) => binding.provider === selectedProvider);
|
|
1320
1673
|
const models = (0, react.useMemo)(() => [...new Set(catalog.map((binding) => binding.model))].sort(), [catalog]);
|
|
1674
|
+
const ttsCatalog = client.catalog?.ttsBindings ?? [];
|
|
1675
|
+
const ttsEnabled = value?.ttsEnabled ?? true;
|
|
1676
|
+
const ttsLocale = value?.language !== void 0 && value.language !== "auto" ? value.language : getLocale();
|
|
1677
|
+
const localizedTts = preferredTtsSelection(ttsLocale);
|
|
1678
|
+
const hasSavedVoiceRoute = value?.ttsModel !== void 0 && value.ttsProvider !== void 0 && value.ttsVoice !== void 0;
|
|
1679
|
+
const ttsBinding = selectLocalizedTtsBinding(ttsCatalog, ttsLocale, { ...!hasSavedVoiceRoute ? {} : {
|
|
1680
|
+
model: value.ttsModel,
|
|
1681
|
+
provider: value.ttsProvider
|
|
1682
|
+
} });
|
|
1683
|
+
const selectedTtsModel = ttsBinding?.model ?? value?.ttsModel ?? "";
|
|
1684
|
+
const ttsProviders = ttsCatalog.filter((binding) => binding.model === selectedTtsModel);
|
|
1685
|
+
const selectedTtsProvider = ttsProviders.some((binding) => binding.provider === value?.ttsProvider) ? value?.ttsProvider ?? "" : ttsProviders.find((binding) => binding.isProviderDefault)?.provider ?? ttsProviders[0]?.provider ?? "";
|
|
1686
|
+
const selectedTtsBinding = ttsProviders.find((binding) => binding.provider === selectedTtsProvider) ?? ttsBinding;
|
|
1687
|
+
const ttsModels = (0, react.useMemo)(() => [...new Set(ttsCatalog.map((binding) => binding.model))].sort(), [ttsCatalog]);
|
|
1688
|
+
const selectedVoice = (hasSavedVoiceRoute ? value.ttsVoice : void 0) ?? (selectedTtsModel === localizedTts.model && selectedTtsProvider === localizedTts.provider ? localizedTts.voice : void 0) ?? selectedTtsBinding?.defaultVoice ?? "";
|
|
1689
|
+
const defaultVoiceOption = {
|
|
1690
|
+
id: selectedVoice,
|
|
1691
|
+
name: selectedVoice === localizedTts.voice ? localizedTts.name : selectedVoice,
|
|
1692
|
+
model: selectedTtsModel,
|
|
1693
|
+
provider: selectedTtsProvider
|
|
1694
|
+
};
|
|
1695
|
+
const globalVoiceResults = useVoiceOptions({}, voiceSearch, ttsEnabled);
|
|
1696
|
+
const scopedVoiceResults = useVoiceOptions({
|
|
1697
|
+
model: selectedTtsBinding?.aliases?.[0] ?? selectedTtsModel,
|
|
1698
|
+
provider: selectedTtsProvider
|
|
1699
|
+
}, modelVoiceSearch, ttsEnabled && selectedTtsModel.length > 0 && selectedTtsProvider.length > 0);
|
|
1700
|
+
const selectedVoiceOption = [...scopedVoiceResults.voices, ...globalVoiceResults.voices].find((voice) => voice.id === selectedVoice && voice.model === selectedTtsModel && voice.provider === selectedTtsProvider) ?? (selectedVoice.length === 0 ? void 0 : defaultVoiceOption);
|
|
1321
1701
|
const remaining = Math.max(0, Math.ceil((codeExpiry - now) / 1e3));
|
|
1322
1702
|
const connected = client.status?.credential.configured === true;
|
|
1323
1703
|
const writable = settings.writable;
|
|
@@ -1503,23 +1883,24 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1503
1883
|
})
|
|
1504
1884
|
] })
|
|
1505
1885
|
}),
|
|
1506
|
-
connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1886
|
+
connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1888
|
+
className: styles.card,
|
|
1889
|
+
children: [
|
|
1890
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("settings") }),
|
|
1891
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1892
|
+
className: styles.grid,
|
|
1893
|
+
children: [
|
|
1894
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1895
|
+
className: styles.modelFull,
|
|
1896
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
|
|
1897
|
+
label: t("model"),
|
|
1898
|
+
placeholder: t("modelSearchPlaceholder"),
|
|
1899
|
+
emptyLabel: t("modelNoResults"),
|
|
1900
|
+
options: models,
|
|
1519
1901
|
value: selectedModel,
|
|
1520
1902
|
disabled: !writable || models.length === 0,
|
|
1521
|
-
|
|
1522
|
-
const model = event.target.value;
|
|
1903
|
+
onSelect: (model) => {
|
|
1523
1904
|
const choices = catalog.filter((binding) => binding.model === model);
|
|
1524
1905
|
const provider = choices.find((binding) => binding.isProviderDefault)?.provider ?? choices[0]?.provider;
|
|
1525
1906
|
run("setting:model", async () => {
|
|
@@ -1527,137 +1908,296 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1527
1908
|
if (provider !== void 0) await scope.set("provider", provider);
|
|
1528
1909
|
await controller.ensureMetadata(true);
|
|
1529
1910
|
});
|
|
1911
|
+
}
|
|
1912
|
+
})
|
|
1913
|
+
}),
|
|
1914
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1915
|
+
label: t("provider"),
|
|
1916
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1917
|
+
className: styles.input,
|
|
1918
|
+
value: selectedProvider,
|
|
1919
|
+
disabled: !writable || providers.length === 0,
|
|
1920
|
+
onChange: (event) => {
|
|
1921
|
+
write("provider", event.target.value);
|
|
1530
1922
|
},
|
|
1531
|
-
children:
|
|
1532
|
-
value:
|
|
1533
|
-
children:
|
|
1534
|
-
},
|
|
1923
|
+
children: providers.map((binding) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1924
|
+
value: binding.provider,
|
|
1925
|
+
children: binding.provider
|
|
1926
|
+
}, binding.provider))
|
|
1535
1927
|
})
|
|
1928
|
+
}),
|
|
1929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
|
|
1930
|
+
label: t("language"),
|
|
1931
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1932
|
+
className: styles.input,
|
|
1933
|
+
list: "dsh-speech-languages",
|
|
1934
|
+
value: languageDraft,
|
|
1935
|
+
disabled: !writable,
|
|
1936
|
+
onChange: (event) => {
|
|
1937
|
+
setLanguageDraft(event.target.value);
|
|
1938
|
+
},
|
|
1939
|
+
onBlur: commitLanguage,
|
|
1940
|
+
onKeyDown: (event) => {
|
|
1941
|
+
if (event.key === "Enter") event.currentTarget.blur();
|
|
1942
|
+
}
|
|
1943
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
1944
|
+
id: "dsh-speech-languages",
|
|
1945
|
+
children: LANGUAGES.map(([id, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1946
|
+
value: id,
|
|
1947
|
+
children: id === "auto" ? t("auto") : label
|
|
1948
|
+
}, id))
|
|
1949
|
+
})]
|
|
1536
1950
|
})
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
value: languageDraft,
|
|
1951
|
+
]
|
|
1952
|
+
}),
|
|
1953
|
+
selectedBinding?.contextSupported === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
1954
|
+
className: styles.contextDetails,
|
|
1955
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
|
|
1956
|
+
className: styles.contextSummary,
|
|
1957
|
+
children: [
|
|
1958
|
+
t("context"),
|
|
1959
|
+
" ",
|
|
1960
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("optional") })
|
|
1961
|
+
]
|
|
1962
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1963
|
+
className: styles.contextBody,
|
|
1964
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1965
|
+
className: styles.hint,
|
|
1966
|
+
children: t("contextHint")
|
|
1967
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1968
|
+
className: styles.textarea,
|
|
1969
|
+
"aria-label": t("context"),
|
|
1970
|
+
maxLength: 4e3,
|
|
1971
|
+
value: contextDraft,
|
|
1559
1972
|
disabled: !writable,
|
|
1560
1973
|
onChange: (event) => {
|
|
1561
|
-
|
|
1974
|
+
setContextDraft(event.target.value);
|
|
1562
1975
|
},
|
|
1563
|
-
onBlur:
|
|
1564
|
-
onKeyDown: (event) => {
|
|
1565
|
-
if (event.key === "Enter") event.currentTarget.blur();
|
|
1566
|
-
}
|
|
1567
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
1568
|
-
id: "dsh-speech-languages",
|
|
1569
|
-
children: LANGUAGES.map(([id, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1570
|
-
value: id,
|
|
1571
|
-
children: id === "auto" ? t("auto") : label
|
|
1572
|
-
}, id))
|
|
1976
|
+
onBlur: commitContext
|
|
1573
1977
|
})]
|
|
1574
|
-
})
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("optional") })
|
|
1585
|
-
]
|
|
1586
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1587
|
-
className: styles.contextBody,
|
|
1588
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1978
|
+
})]
|
|
1979
|
+
}) : null
|
|
1980
|
+
]
|
|
1981
|
+
}),
|
|
1982
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1983
|
+
className: styles.card,
|
|
1984
|
+
children: [
|
|
1985
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1986
|
+
className: styles.cardTitle,
|
|
1987
|
+
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", {
|
|
1589
1988
|
className: styles.hint,
|
|
1590
|
-
children: t("
|
|
1591
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1592
|
-
className: styles.
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1989
|
+
children: t("spokenSummariesHint")
|
|
1990
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1991
|
+
className: styles.summaryToggles,
|
|
1992
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1993
|
+
className: styles.switchLabel,
|
|
1994
|
+
children: [
|
|
1995
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1996
|
+
type: "checkbox",
|
|
1997
|
+
checked: ttsEnabled,
|
|
1998
|
+
disabled: !writable,
|
|
1999
|
+
onChange: (event) => {
|
|
2000
|
+
write("ttsEnabled", event.target.checked);
|
|
2001
|
+
}
|
|
2002
|
+
}),
|
|
2003
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2004
|
+
className: styles.switchTrack,
|
|
2005
|
+
"aria-hidden": "true",
|
|
2006
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
|
|
2007
|
+
}),
|
|
2008
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("ttsEnabled") })
|
|
2009
|
+
]
|
|
2010
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2011
|
+
className: styles.switchLabel,
|
|
2012
|
+
children: [
|
|
2013
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2014
|
+
type: "checkbox",
|
|
2015
|
+
checked: value?.autoPlay ?? true,
|
|
2016
|
+
disabled: !writable || !ttsEnabled,
|
|
2017
|
+
onChange: (event) => {
|
|
2018
|
+
write("autoPlay", event.target.checked);
|
|
2019
|
+
}
|
|
2020
|
+
}),
|
|
2021
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2022
|
+
className: styles.switchTrack,
|
|
2023
|
+
"aria-hidden": "true",
|
|
2024
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
|
|
2025
|
+
}),
|
|
2026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("autoplayGlobal") })
|
|
2027
|
+
]
|
|
2028
|
+
})]
|
|
1601
2029
|
})]
|
|
1602
|
-
})
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
2030
|
+
}),
|
|
2031
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2032
|
+
className: styles.grid,
|
|
2033
|
+
children: [
|
|
2034
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2035
|
+
className: styles.modelFull,
|
|
2036
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoicePicker, {
|
|
2037
|
+
label: t("voiceSearch"),
|
|
2038
|
+
hint: t("voiceSearchHint"),
|
|
2039
|
+
placeholder: t("voiceSearchPlaceholder"),
|
|
2040
|
+
loadingLabel: t("loading"),
|
|
2041
|
+
emptyLabel: t("voiceNoResults"),
|
|
2042
|
+
query: voiceSearch,
|
|
2043
|
+
voices: globalVoiceResults.voices,
|
|
2044
|
+
selected: selectedVoiceOption,
|
|
2045
|
+
loading: globalVoiceResults.loading,
|
|
2046
|
+
disabled: !writable || !ttsEnabled,
|
|
2047
|
+
onQuery: setVoiceSearch,
|
|
2048
|
+
onSelect: (voice) => {
|
|
2049
|
+
if (voice.model === void 0 || voice.provider === void 0) return;
|
|
2050
|
+
setVoiceSearch(voice.name);
|
|
2051
|
+
setModelVoiceSearch("");
|
|
2052
|
+
run("setting:ttsVoice", async () => {
|
|
2053
|
+
await scope.set("ttsVoice", voice.id);
|
|
2054
|
+
await scope.set("ttsModel", voice.model);
|
|
2055
|
+
await scope.set("ttsProvider", voice.provider);
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
})
|
|
2059
|
+
}),
|
|
2060
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2061
|
+
className: styles.modelFull,
|
|
2062
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
|
|
2063
|
+
label: t("ttsModel"),
|
|
2064
|
+
placeholder: t("modelSearchPlaceholder"),
|
|
2065
|
+
emptyLabel: t("modelNoResults"),
|
|
2066
|
+
options: ttsModels,
|
|
2067
|
+
value: selectedTtsModel,
|
|
2068
|
+
disabled: !writable || !ttsEnabled || ttsModels.length === 0,
|
|
2069
|
+
onSelect: (model) => {
|
|
2070
|
+
const choices = ttsCatalog.filter((binding) => binding.model === model);
|
|
2071
|
+
const provider = choices.find((binding) => binding.isProviderDefault)?.provider ?? choices[0]?.provider;
|
|
2072
|
+
run("setting:ttsModel", async () => {
|
|
2073
|
+
await scope.set("ttsModel", model);
|
|
2074
|
+
if (provider !== void 0) await scope.set("ttsProvider", provider);
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
})
|
|
2078
|
+
}),
|
|
2079
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2080
|
+
label: t("ttsProvider"),
|
|
2081
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2082
|
+
className: styles.input,
|
|
2083
|
+
value: selectedTtsProvider,
|
|
2084
|
+
disabled: !writable || !ttsEnabled || ttsProviders.length === 0,
|
|
2085
|
+
onChange: (event) => {
|
|
2086
|
+
write("ttsProvider", event.target.value);
|
|
2087
|
+
},
|
|
2088
|
+
children: ttsProviders.map((binding) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2089
|
+
value: binding.provider,
|
|
2090
|
+
children: binding.provider
|
|
2091
|
+
}, binding.provider))
|
|
2092
|
+
})
|
|
2093
|
+
}),
|
|
2094
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2095
|
+
className: styles.modelFull,
|
|
2096
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoicePicker, {
|
|
2097
|
+
label: t("ttsVoice"),
|
|
2098
|
+
hint: t("modelVoiceSearchHint", { model: selectedTtsModel }),
|
|
2099
|
+
placeholder: t("modelVoiceSearchPlaceholder"),
|
|
2100
|
+
loadingLabel: t("loading"),
|
|
2101
|
+
emptyLabel: t("voiceNoResults"),
|
|
2102
|
+
query: modelVoiceSearch,
|
|
2103
|
+
voices: scopedVoiceResults.voices,
|
|
2104
|
+
selected: selectedVoiceOption,
|
|
2105
|
+
loading: scopedVoiceResults.loading,
|
|
2106
|
+
disabled: !writable || !ttsEnabled || selectedTtsBinding === void 0,
|
|
2107
|
+
onQuery: setModelVoiceSearch,
|
|
2108
|
+
onSelect: (voice) => {
|
|
2109
|
+
setModelVoiceSearch(voice.name);
|
|
2110
|
+
run("setting:ttsVoice", async () => {
|
|
2111
|
+
await scope.set("ttsVoice", voice.id);
|
|
2112
|
+
await scope.set("ttsModel", selectedTtsModel);
|
|
2113
|
+
await scope.set("ttsProvider", selectedTtsProvider);
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
})
|
|
1637
2117
|
})
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
2118
|
+
]
|
|
2119
|
+
}),
|
|
2120
|
+
ttsCatalog.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2121
|
+
className: styles.muted,
|
|
2122
|
+
children: t("ttsUnavailable")
|
|
2123
|
+
}) : null,
|
|
2124
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2125
|
+
className: styles.summaryCache,
|
|
2126
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("summaryCache") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2127
|
+
className: styles.hint,
|
|
2128
|
+
children: t("summaryCacheHint")
|
|
2129
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2130
|
+
className: styles.secondary,
|
|
1641
2131
|
type: "button",
|
|
1642
|
-
disabled: busy !== null
|
|
2132
|
+
disabled: busy !== null,
|
|
1643
2133
|
onClick: () => {
|
|
1644
|
-
run("
|
|
1645
|
-
|
|
2134
|
+
run("clear-summary-cache", async () => {
|
|
2135
|
+
await summaryCache.clear();
|
|
2136
|
+
setMessage(t("summaryCacheCleared"));
|
|
1646
2137
|
});
|
|
1647
2138
|
},
|
|
1648
|
-
children: t("
|
|
1649
|
-
})
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
2139
|
+
children: busy === "clear-summary-cache" ? t("loading") : t("clearSummaryCache")
|
|
2140
|
+
})]
|
|
2141
|
+
})
|
|
2142
|
+
]
|
|
2143
|
+
}),
|
|
2144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2145
|
+
className: styles.card,
|
|
2146
|
+
children: [
|
|
2147
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("balance") }),
|
|
2148
|
+
client.status?.balance === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2149
|
+
className: styles.muted,
|
|
2150
|
+
children: client.status?.balanceError === void 0 ? t("loading") : t("balanceUnavailable")
|
|
2151
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
2152
|
+
className: styles.balanceValue,
|
|
2153
|
+
children: money(client.status.balance.paidUsd)
|
|
2154
|
+
}), client.status.balance.exhausted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2155
|
+
className: styles.danger,
|
|
2156
|
+
children: t("emptyBalance")
|
|
2157
|
+
}) : client.status.balance.low ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2158
|
+
className: styles.warning,
|
|
2159
|
+
children: t("lowBalance")
|
|
2160
|
+
}) : null] }),
|
|
2161
|
+
connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2162
|
+
className: styles.topUp,
|
|
2163
|
+
children: [
|
|
2164
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2165
|
+
label: t("amount"),
|
|
2166
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2167
|
+
className: styles.input,
|
|
2168
|
+
type: "number",
|
|
2169
|
+
min: 5,
|
|
2170
|
+
max: 1e3,
|
|
2171
|
+
step: 1,
|
|
2172
|
+
value: topUpAmount,
|
|
2173
|
+
onChange: (event) => {
|
|
2174
|
+
setTopUpAmount(Number(event.target.value));
|
|
2175
|
+
}
|
|
2176
|
+
})
|
|
2177
|
+
}),
|
|
2178
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2179
|
+
className: styles.primary,
|
|
2180
|
+
type: "button",
|
|
2181
|
+
disabled: busy !== null || topUpAmount < 5 || topUpAmount > 1e3,
|
|
2182
|
+
onClick: () => {
|
|
2183
|
+
run("top-up", async () => {
|
|
2184
|
+
setTopUpUrl((await speechApi.topUp(topUpAmount)).url);
|
|
2185
|
+
});
|
|
2186
|
+
},
|
|
2187
|
+
children: t("createLink")
|
|
2188
|
+
}),
|
|
2189
|
+
topUpUrl === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2190
|
+
className: styles.checkout,
|
|
2191
|
+
href: topUpUrl,
|
|
2192
|
+
target: "_blank",
|
|
2193
|
+
rel: "noreferrer",
|
|
2194
|
+
children: t("openCheckout")
|
|
2195
|
+
})
|
|
2196
|
+
]
|
|
2197
|
+
}) : null
|
|
2198
|
+
]
|
|
2199
|
+
})
|
|
2200
|
+
] }) : null,
|
|
1661
2201
|
message === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1662
2202
|
className: styles.good,
|
|
1663
2203
|
role: "status",
|
|
@@ -1672,6 +2212,1152 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
1672
2212
|
});
|
|
1673
2213
|
}
|
|
1674
2214
|
|
|
2215
|
+
//#endregion
|
|
2216
|
+
//#region src/client/summary-cache.ts
|
|
2217
|
+
const SUMMARY_CACHE_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
2218
|
+
const SUMMARY_CACHE_MAX_ENTRIES = 500;
|
|
2219
|
+
const DATABASE_NAME = "dsh-speech-summary-cache";
|
|
2220
|
+
const DATABASE_VERSION = 1;
|
|
2221
|
+
const STORE_NAME = "summaries";
|
|
2222
|
+
var IndexedDbSummaryCacheStore = class {
|
|
2223
|
+
database;
|
|
2224
|
+
async get(key) {
|
|
2225
|
+
const database = await this.open();
|
|
2226
|
+
if (database === void 0) return void 0;
|
|
2227
|
+
return await new Promise((resolve) => {
|
|
2228
|
+
const request$1 = database.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(key);
|
|
2229
|
+
request$1.onsuccess = () => {
|
|
2230
|
+
resolve(request$1.result);
|
|
2231
|
+
};
|
|
2232
|
+
request$1.onerror = () => {
|
|
2233
|
+
resolve(void 0);
|
|
2234
|
+
};
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
async put(record) {
|
|
2238
|
+
const database = await this.open();
|
|
2239
|
+
if (database === void 0) return;
|
|
2240
|
+
await new Promise((resolve) => {
|
|
2241
|
+
const transaction = database.transaction(STORE_NAME, "readwrite");
|
|
2242
|
+
transaction.objectStore(STORE_NAME).put(record);
|
|
2243
|
+
transaction.oncomplete = () => {
|
|
2244
|
+
resolve();
|
|
2245
|
+
};
|
|
2246
|
+
transaction.onerror = () => {
|
|
2247
|
+
resolve();
|
|
2248
|
+
};
|
|
2249
|
+
transaction.onabort = () => {
|
|
2250
|
+
resolve();
|
|
2251
|
+
};
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
async delete(key) {
|
|
2255
|
+
const database = await this.open();
|
|
2256
|
+
if (database === void 0) return;
|
|
2257
|
+
await new Promise((resolve) => {
|
|
2258
|
+
const transaction = database.transaction(STORE_NAME, "readwrite");
|
|
2259
|
+
transaction.objectStore(STORE_NAME).delete(key);
|
|
2260
|
+
transaction.oncomplete = () => {
|
|
2261
|
+
resolve();
|
|
2262
|
+
};
|
|
2263
|
+
transaction.onerror = () => {
|
|
2264
|
+
resolve();
|
|
2265
|
+
};
|
|
2266
|
+
transaction.onabort = () => {
|
|
2267
|
+
resolve();
|
|
2268
|
+
};
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
async all() {
|
|
2272
|
+
const database = await this.open();
|
|
2273
|
+
if (database === void 0) return [];
|
|
2274
|
+
return await new Promise((resolve) => {
|
|
2275
|
+
const request$1 = database.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).getAll();
|
|
2276
|
+
request$1.onsuccess = () => {
|
|
2277
|
+
resolve(request$1.result);
|
|
2278
|
+
};
|
|
2279
|
+
request$1.onerror = () => {
|
|
2280
|
+
resolve([]);
|
|
2281
|
+
};
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
async clear() {
|
|
2285
|
+
const database = await this.open();
|
|
2286
|
+
if (database === void 0) return;
|
|
2287
|
+
await new Promise((resolve) => {
|
|
2288
|
+
const transaction = database.transaction(STORE_NAME, "readwrite");
|
|
2289
|
+
transaction.objectStore(STORE_NAME).clear();
|
|
2290
|
+
transaction.oncomplete = () => {
|
|
2291
|
+
resolve();
|
|
2292
|
+
};
|
|
2293
|
+
transaction.onerror = () => {
|
|
2294
|
+
resolve();
|
|
2295
|
+
};
|
|
2296
|
+
transaction.onabort = () => {
|
|
2297
|
+
resolve();
|
|
2298
|
+
};
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
close() {
|
|
2302
|
+
this.database?.then((database) => {
|
|
2303
|
+
database?.close();
|
|
2304
|
+
});
|
|
2305
|
+
this.database = void 0;
|
|
2306
|
+
}
|
|
2307
|
+
open() {
|
|
2308
|
+
if (this.database !== void 0) return this.database;
|
|
2309
|
+
if (typeof indexedDB === "undefined") return Promise.resolve(void 0);
|
|
2310
|
+
this.database = new Promise((resolve) => {
|
|
2311
|
+
let settled = false;
|
|
2312
|
+
const finish = (database) => {
|
|
2313
|
+
if (settled) {
|
|
2314
|
+
database?.close();
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
settled = true;
|
|
2318
|
+
resolve(database);
|
|
2319
|
+
};
|
|
2320
|
+
const request$1 = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
|
|
2321
|
+
request$1.onupgradeneeded = () => {
|
|
2322
|
+
if (!request$1.result.objectStoreNames.contains(STORE_NAME)) request$1.result.createObjectStore(STORE_NAME, { keyPath: "key" });
|
|
2323
|
+
};
|
|
2324
|
+
request$1.onsuccess = () => {
|
|
2325
|
+
finish(request$1.result);
|
|
2326
|
+
};
|
|
2327
|
+
request$1.onerror = () => {
|
|
2328
|
+
finish(void 0);
|
|
2329
|
+
};
|
|
2330
|
+
request$1.onblocked = () => {
|
|
2331
|
+
finish(void 0);
|
|
2332
|
+
};
|
|
2333
|
+
});
|
|
2334
|
+
return this.database;
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
async function summaryCacheKey(input) {
|
|
2338
|
+
if (globalThis.crypto?.subtle === void 0) return void 0;
|
|
2339
|
+
const canonical = JSON.stringify({
|
|
2340
|
+
version: SUMMARY_PROMPT_VERSION,
|
|
2341
|
+
request: input.request,
|
|
2342
|
+
answer: input.answer,
|
|
2343
|
+
locale: input.locale,
|
|
2344
|
+
route: {
|
|
2345
|
+
provider: input.route.provider,
|
|
2346
|
+
model: input.route.model,
|
|
2347
|
+
reasoningEffort: input.route.reasoningEffort ?? null
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical));
|
|
2351
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2352
|
+
}
|
|
2353
|
+
/** Browser-local, fail-open cache. It never stores session or message identity. */
|
|
2354
|
+
var SummaryCache = class {
|
|
2355
|
+
constructor(store = new IndexedDbSummaryCacheStore(), now = Date.now, ttlMs = SUMMARY_CACHE_TTL_MS, maximumEntries = SUMMARY_CACHE_MAX_ENTRIES) {
|
|
2356
|
+
this.store = store;
|
|
2357
|
+
this.now = now;
|
|
2358
|
+
this.ttlMs = ttlMs;
|
|
2359
|
+
this.maximumEntries = maximumEntries;
|
|
2360
|
+
}
|
|
2361
|
+
async get(input) {
|
|
2362
|
+
try {
|
|
2363
|
+
const key = await summaryCacheKey(input);
|
|
2364
|
+
if (key === void 0) return void 0;
|
|
2365
|
+
const record = await this.store.get(key);
|
|
2366
|
+
if (record === void 0) return void 0;
|
|
2367
|
+
const now = this.now();
|
|
2368
|
+
if (record.expiresAt <= now || record.summary.length === 0 || record.summary.length > MAX_TTS_CHARACTERS) {
|
|
2369
|
+
await this.store.delete(key);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
await this.store.put({
|
|
2373
|
+
...record,
|
|
2374
|
+
accessedAt: now
|
|
2375
|
+
});
|
|
2376
|
+
return record.summary;
|
|
2377
|
+
} catch {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
async set(input, summary) {
|
|
2382
|
+
if (summary.length === 0 || summary.length > MAX_TTS_CHARACTERS) return;
|
|
2383
|
+
try {
|
|
2384
|
+
const key = await summaryCacheKey(input);
|
|
2385
|
+
if (key === void 0) return;
|
|
2386
|
+
const now = this.now();
|
|
2387
|
+
await this.store.put({
|
|
2388
|
+
key,
|
|
2389
|
+
summary,
|
|
2390
|
+
expiresAt: now + this.ttlMs,
|
|
2391
|
+
accessedAt: now
|
|
2392
|
+
});
|
|
2393
|
+
await this.prune(now);
|
|
2394
|
+
} catch {}
|
|
2395
|
+
}
|
|
2396
|
+
async clear() {
|
|
2397
|
+
try {
|
|
2398
|
+
await this.store.clear();
|
|
2399
|
+
} catch {}
|
|
2400
|
+
}
|
|
2401
|
+
dispose() {
|
|
2402
|
+
this.store.close();
|
|
2403
|
+
}
|
|
2404
|
+
async prune(now) {
|
|
2405
|
+
const records = await this.store.all();
|
|
2406
|
+
const live = [];
|
|
2407
|
+
for (const record of records) if (record.expiresAt <= now) await this.store.delete(record.key);
|
|
2408
|
+
else live.push(record);
|
|
2409
|
+
live.sort((a, b) => b.accessedAt - a.accessedAt);
|
|
2410
|
+
for (const record of live.slice(this.maximumEntries)) await this.store.delete(record.key);
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
//#endregion
|
|
2415
|
+
//#region src/client/spoken-controller.ts
|
|
2416
|
+
const EMPTY_PEAKS = Object.freeze(Array.from({ length: 48 }, (_, index) => .15 + .08 * Math.sin(index * .73) ** 2));
|
|
2417
|
+
function contentText(value) {
|
|
2418
|
+
if (!Array.isArray(value)) return "";
|
|
2419
|
+
return value.flatMap((block) => {
|
|
2420
|
+
if (block === null || typeof block !== "object") return [];
|
|
2421
|
+
const candidate = block;
|
|
2422
|
+
return candidate.type === "text" && typeof candidate.text === "string" ? [candidate.text] : [];
|
|
2423
|
+
}).join("\n").trim();
|
|
2424
|
+
}
|
|
2425
|
+
function boundSource(text, maximum) {
|
|
2426
|
+
if (text.length <= maximum) return text;
|
|
2427
|
+
const marker = "\n\n[...middle omitted for spoken-summary input...]\n\n";
|
|
2428
|
+
const remaining = maximum - 51;
|
|
2429
|
+
const beginning = Math.ceil(remaining / 2);
|
|
2430
|
+
return `${text.slice(0, beginning)}${marker}${text.slice(text.length - (remaining - beginning))}`;
|
|
2431
|
+
}
|
|
2432
|
+
/** Derive only the closing finalized assistant message for each completed turn. */
|
|
2433
|
+
function spokenSources(nodes, locale, turnEnds, requests = []) {
|
|
2434
|
+
const closings = /* @__PURE__ */ new Map();
|
|
2435
|
+
for (const node of nodes) {
|
|
2436
|
+
if (node.kind !== "assistant" || node.messageId === void 0 || node.interrupted === true) continue;
|
|
2437
|
+
if (!turnEnds.has(node.turn)) continue;
|
|
2438
|
+
if (node.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n").trim().length === 0) continue;
|
|
2439
|
+
const current = closings.get(node.turn);
|
|
2440
|
+
if (current === void 0 || node.seq > current.seq) closings.set(node.turn, node);
|
|
2441
|
+
}
|
|
2442
|
+
const result = [];
|
|
2443
|
+
for (const assistant of [...closings.values()].sort((a, b) => a.seq - b.seq)) {
|
|
2444
|
+
const answer = assistant.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n").trim();
|
|
2445
|
+
if (answer.length === 0 || assistant.messageId === void 0) continue;
|
|
2446
|
+
let request$1 = "";
|
|
2447
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
2448
|
+
const candidate = nodes[index];
|
|
2449
|
+
if (candidate === void 0 || candidate.seq >= assistant.seq) continue;
|
|
2450
|
+
if (candidate.kind === "user" || candidate.kind === "steering") {
|
|
2451
|
+
request$1 = contentText(candidate.content);
|
|
2452
|
+
if (request$1.length > 0) break;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
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");
|
|
2456
|
+
const recorded = assistant.requestConfig ?? assistant.provenance ?? inspection?.requestConfig ?? inspection?.provenance;
|
|
2457
|
+
const route = recorded === void 0 ? void 0 : {
|
|
2458
|
+
provider: recorded.provider,
|
|
2459
|
+
model: recorded.model,
|
|
2460
|
+
..."reasoningEffort" in recorded && typeof recorded.reasoningEffort === "string" ? { reasoningEffort: recorded.reasoningEffort } : {}
|
|
2461
|
+
};
|
|
2462
|
+
result.push({
|
|
2463
|
+
messageId: String(assistant.messageId),
|
|
2464
|
+
seq: assistant.seq,
|
|
2465
|
+
request: boundSource(request$1, MAX_SUMMARY_REQUEST_CHARACTERS),
|
|
2466
|
+
answer: boundSource(answer, MAX_SUMMARY_ANSWER_CHARACTERS),
|
|
2467
|
+
locale,
|
|
2468
|
+
...route === void 0 ? {} : { route }
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
return result;
|
|
2472
|
+
}
|
|
2473
|
+
const INTERACTION_CUES = {
|
|
2474
|
+
en: "I need some feedback to keep going.",
|
|
2475
|
+
zh: "我需要你的反馈才能继续。",
|
|
2476
|
+
ja: "続けるには、フィードバックが必要です。",
|
|
2477
|
+
ko: "계속하려면 피드백이 필요해요.",
|
|
2478
|
+
es: "Necesito tus comentarios para continuar.",
|
|
2479
|
+
fr: "J’ai besoin de votre avis pour continuer.",
|
|
2480
|
+
de: "Ich brauche Ihre Rückmeldung, um fortzufahren.",
|
|
2481
|
+
pt: "Preciso do seu feedback para continuar.",
|
|
2482
|
+
it: "Ho bisogno del tuo feedback per continuare.",
|
|
2483
|
+
ru: "Мне нужна ваша обратная связь, чтобы продолжить.",
|
|
2484
|
+
ar: "أحتاج إلى ملاحظاتك لكي أتابع.",
|
|
2485
|
+
hi: "आगे बढ़ने के लिए मुझे आपकी प्रतिक्रिया चाहिए।",
|
|
2486
|
+
id: "Saya perlu masukan Anda untuk melanjutkan.",
|
|
2487
|
+
vi: "Tôi cần phản hồi của bạn để tiếp tục."
|
|
2488
|
+
};
|
|
2489
|
+
function interactionCue(locale) {
|
|
2490
|
+
return INTERACTION_CUES[locale.toLowerCase().split("-")[0] ?? ""] ?? INTERACTION_CUES.en;
|
|
2491
|
+
}
|
|
2492
|
+
function revokeAudioUrl(url) {
|
|
2493
|
+
if (url?.startsWith("blob:") === true) URL.revokeObjectURL(url);
|
|
2494
|
+
}
|
|
2495
|
+
function browserAudio() {
|
|
2496
|
+
const audio = new Audio();
|
|
2497
|
+
audio.preload = "metadata";
|
|
2498
|
+
return audio;
|
|
2499
|
+
}
|
|
2500
|
+
var SpokenSummaryController = class {
|
|
2501
|
+
audio;
|
|
2502
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2503
|
+
states = /* @__PURE__ */ new Map();
|
|
2504
|
+
observed = /* @__PURE__ */ new Map();
|
|
2505
|
+
messageSessions = /* @__PURE__ */ new Map();
|
|
2506
|
+
observedInteractions = /* @__PURE__ */ new Set();
|
|
2507
|
+
observedInteractionSessions = /* @__PURE__ */ new Set();
|
|
2508
|
+
autoplayConsumed = /* @__PURE__ */ new Set();
|
|
2509
|
+
voiceCache = /* @__PURE__ */ new Map();
|
|
2510
|
+
activeMessageId;
|
|
2511
|
+
activeSessionKey;
|
|
2512
|
+
loadedMessageId;
|
|
2513
|
+
playbackGeneration = 0;
|
|
2514
|
+
snapshot = { messages: /* @__PURE__ */ new Map() };
|
|
2515
|
+
enabled = true;
|
|
2516
|
+
disposed = false;
|
|
2517
|
+
analyserContext;
|
|
2518
|
+
analyserSource;
|
|
2519
|
+
analyser;
|
|
2520
|
+
analyserData;
|
|
2521
|
+
analysisFrame;
|
|
2522
|
+
analysisTick = 0;
|
|
2523
|
+
pauseResolutionTimer;
|
|
2524
|
+
constructor(audioFactory = browserAudio, summaryCache = new SummaryCache()) {
|
|
2525
|
+
this.summaryCache = summaryCache;
|
|
2526
|
+
this.audio = audioFactory();
|
|
2527
|
+
}
|
|
2528
|
+
subscribe = (listener) => {
|
|
2529
|
+
this.listeners.add(listener);
|
|
2530
|
+
return () => {
|
|
2531
|
+
this.listeners.delete(listener);
|
|
2532
|
+
};
|
|
2533
|
+
};
|
|
2534
|
+
getSnapshot = () => this.snapshot;
|
|
2535
|
+
setEnabled(enabled) {
|
|
2536
|
+
if (this.disposed || this.enabled === enabled) return;
|
|
2537
|
+
this.enabled = enabled;
|
|
2538
|
+
if (enabled) return;
|
|
2539
|
+
this.stopActive();
|
|
2540
|
+
this.clearAudioOwnership(true);
|
|
2541
|
+
for (const [messageId, state] of this.states) {
|
|
2542
|
+
state.abort?.abort();
|
|
2543
|
+
state.abort = void 0;
|
|
2544
|
+
state.requestGeneration += 1;
|
|
2545
|
+
revokeAudioUrl(state.audioUrl);
|
|
2546
|
+
if (state.ephemeral === true) {
|
|
2547
|
+
this.states.delete(messageId);
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
Object.assign(state, {
|
|
2551
|
+
phase: "idle",
|
|
2552
|
+
summary: void 0,
|
|
2553
|
+
audioUrl: void 0,
|
|
2554
|
+
error: void 0,
|
|
2555
|
+
duration: 0,
|
|
2556
|
+
progress: 0,
|
|
2557
|
+
peaks: EMPTY_PEAKS,
|
|
2558
|
+
ended: false
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
this.voiceCache.clear();
|
|
2562
|
+
this.publish();
|
|
2563
|
+
}
|
|
2564
|
+
mount(messageId) {
|
|
2565
|
+
const state = this.ensure(messageId);
|
|
2566
|
+
state.mounts += 1;
|
|
2567
|
+
state.releaseGeneration += 1;
|
|
2568
|
+
return () => {
|
|
2569
|
+
state.mounts = Math.max(0, state.mounts - 1);
|
|
2570
|
+
const releaseGeneration = ++state.releaseGeneration;
|
|
2571
|
+
queueMicrotask(() => {
|
|
2572
|
+
if (state.mounts === 0 && state.releaseGeneration === releaseGeneration) this.release(messageId);
|
|
2573
|
+
});
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
observeSession(sessionKey, sources, settings) {
|
|
2577
|
+
if (this.disposed) return;
|
|
2578
|
+
this.setEnabled(settings.ttsEnabled);
|
|
2579
|
+
const ids = new Set(sources.map((source) => source.messageId));
|
|
2580
|
+
const maxSeq = sources.reduce((maximum, source) => Math.max(maximum, source.seq), -1);
|
|
2581
|
+
for (const id of ids) this.messageSessions.set(id, sessionKey);
|
|
2582
|
+
const previous = this.observed.get(sessionKey);
|
|
2583
|
+
if (previous === void 0) {
|
|
2584
|
+
this.observed.set(sessionKey, {
|
|
2585
|
+
ids,
|
|
2586
|
+
maxSeq
|
|
2587
|
+
});
|
|
2588
|
+
for (const id of ids) this.ensure(id);
|
|
2589
|
+
this.publish();
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
const previousMaxSeq = previous.maxSeq;
|
|
2593
|
+
let changed = false;
|
|
2594
|
+
for (const source of sources) {
|
|
2595
|
+
if (previous.ids.has(source.messageId)) continue;
|
|
2596
|
+
previous.ids.add(source.messageId);
|
|
2597
|
+
changed = true;
|
|
2598
|
+
if (settings.ttsEnabled && source.seq > previousMaxSeq) this.prepare(source, settings, settings.autoPlay);
|
|
2599
|
+
else this.ensure(source.messageId);
|
|
2600
|
+
}
|
|
2601
|
+
previous.maxSeq = Math.max(previous.maxSeq, maxSeq);
|
|
2602
|
+
if (changed && (!settings.ttsEnabled || sources.some((source) => source.seq <= previousMaxSeq))) this.publish();
|
|
2603
|
+
}
|
|
2604
|
+
observeInteractions(sessionKey, interactionKeys, locale, settings) {
|
|
2605
|
+
if (this.disposed) return;
|
|
2606
|
+
this.setEnabled(settings.ttsEnabled);
|
|
2607
|
+
if (!this.observedInteractionSessions.has(sessionKey)) {
|
|
2608
|
+
this.observedInteractionSessions.add(sessionKey);
|
|
2609
|
+
for (const key of interactionKeys) this.observedInteractions.add(`${sessionKey}\n${key}`);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
for (const key of interactionKeys) {
|
|
2613
|
+
const identity = `${sessionKey}\n${key}`;
|
|
2614
|
+
if (this.observedInteractions.has(identity)) continue;
|
|
2615
|
+
this.observedInteractions.add(identity);
|
|
2616
|
+
const messageId = `interaction:${identity}`;
|
|
2617
|
+
this.messageSessions.set(messageId, sessionKey);
|
|
2618
|
+
if (settings.ttsEnabled && settings.autoPlay) this.prepareCue(messageId, interactionCue(locale), settings);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
async prepare(source, settings, autoPlay = false) {
|
|
2622
|
+
if (!this.enabled || !settings.ttsEnabled) return;
|
|
2623
|
+
const state = this.ensure(source.messageId);
|
|
2624
|
+
if (state.phase === "preparing" || state.phase === "ready" || state.phase === "playing") {
|
|
2625
|
+
if (state.phase === "ready" && !autoPlay) await this.play(source.messageId, true);
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
if (source.route === void 0) {
|
|
2629
|
+
this.fail(source.messageId, "The completed answer has no recorded LLM route.");
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const binding = selectLocalizedTtsBinding(settings.bindings, source.locale, {
|
|
2633
|
+
...settings.ttsModel === void 0 ? {} : { model: settings.ttsModel },
|
|
2634
|
+
...settings.ttsProvider === void 0 ? {} : { provider: settings.ttsProvider }
|
|
2635
|
+
});
|
|
2636
|
+
if (binding === void 0) {
|
|
2637
|
+
this.fail(source.messageId, "No compatible synchronous text-to-speech model is available.");
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
const abort = new AbortController();
|
|
2641
|
+
state.abort?.abort();
|
|
2642
|
+
state.abort = abort;
|
|
2643
|
+
const requestGeneration = ++state.requestGeneration;
|
|
2644
|
+
this.patch(source.messageId, {
|
|
2645
|
+
phase: "preparing",
|
|
2646
|
+
error: void 0,
|
|
2647
|
+
progress: 0
|
|
2648
|
+
});
|
|
2649
|
+
try {
|
|
2650
|
+
const voice = await this.voiceFor(binding, settings.ttsVoice);
|
|
2651
|
+
if (voice === void 0) throw new Error("No compatible voice is available for this text-to-speech route.");
|
|
2652
|
+
const summaryInput = {
|
|
2653
|
+
request: source.request || "No preceding user prose was available.",
|
|
2654
|
+
answer: source.answer,
|
|
2655
|
+
locale: source.locale,
|
|
2656
|
+
route: source.route
|
|
2657
|
+
};
|
|
2658
|
+
let summary = await this.summaryCache.get(summaryInput);
|
|
2659
|
+
if (this.disposed || abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2660
|
+
if (summary === void 0) {
|
|
2661
|
+
summary = (await speechApi.summarize(summaryInput, abort.signal)).summary;
|
|
2662
|
+
if (this.disposed || abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2663
|
+
await this.summaryCache.set(summaryInput, summary);
|
|
2664
|
+
}
|
|
2665
|
+
const prepared = await speechApi.prepareTts({
|
|
2666
|
+
text: summary,
|
|
2667
|
+
model: binding.model,
|
|
2668
|
+
provider: binding.provider,
|
|
2669
|
+
voice
|
|
2670
|
+
}, abort.signal);
|
|
2671
|
+
if (this.disposed || abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2672
|
+
state.abort = void 0;
|
|
2673
|
+
this.patch(source.messageId, {
|
|
2674
|
+
phase: "ready",
|
|
2675
|
+
summary,
|
|
2676
|
+
audioUrl: prepared.url,
|
|
2677
|
+
duration: 0,
|
|
2678
|
+
progress: 0,
|
|
2679
|
+
peaks: EMPTY_PEAKS,
|
|
2680
|
+
error: void 0,
|
|
2681
|
+
ended: false
|
|
2682
|
+
});
|
|
2683
|
+
if (autoPlay && this.activeMessageId === void 0 && !this.autoplayConsumed.has(source.messageId)) await this.play(source.messageId, false);
|
|
2684
|
+
} catch (error) {
|
|
2685
|
+
if (abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2686
|
+
state.abort = void 0;
|
|
2687
|
+
this.fail(source.messageId, error instanceof Error ? error.message : "Could not prepare the spoken summary.");
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
async prepareCue(messageId, text, settings) {
|
|
2691
|
+
if (!this.enabled || !settings.ttsEnabled) return;
|
|
2692
|
+
const state = this.ensure(messageId);
|
|
2693
|
+
state.ephemeral = true;
|
|
2694
|
+
const binding = selectLocalizedTtsBinding(settings.bindings, text === INTERACTION_CUES.zh ? "zh" : "en", {
|
|
2695
|
+
...settings.ttsModel === void 0 ? {} : { model: settings.ttsModel },
|
|
2696
|
+
...settings.ttsProvider === void 0 ? {} : { provider: settings.ttsProvider }
|
|
2697
|
+
});
|
|
2698
|
+
if (binding === void 0) {
|
|
2699
|
+
this.release(messageId);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
const abort = new AbortController();
|
|
2703
|
+
state.abort = abort;
|
|
2704
|
+
const requestGeneration = ++state.requestGeneration;
|
|
2705
|
+
try {
|
|
2706
|
+
const voice = await this.voiceFor(binding, settings.ttsVoice);
|
|
2707
|
+
if (voice === void 0) throw new Error("No compatible voice is available.");
|
|
2708
|
+
const prepared = await speechApi.prepareTts({
|
|
2709
|
+
text,
|
|
2710
|
+
model: binding.model,
|
|
2711
|
+
provider: binding.provider,
|
|
2712
|
+
voice
|
|
2713
|
+
}, abort.signal);
|
|
2714
|
+
if (this.disposed || abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2715
|
+
state.abort = void 0;
|
|
2716
|
+
this.patch(messageId, {
|
|
2717
|
+
phase: "ready",
|
|
2718
|
+
audioUrl: prepared.url,
|
|
2719
|
+
duration: 0,
|
|
2720
|
+
progress: 0,
|
|
2721
|
+
peaks: EMPTY_PEAKS,
|
|
2722
|
+
ended: false
|
|
2723
|
+
});
|
|
2724
|
+
if (this.activeMessageId !== void 0) {
|
|
2725
|
+
this.release(messageId);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
await this.play(messageId, false);
|
|
2729
|
+
if (this.activeMessageId !== messageId) this.release(messageId);
|
|
2730
|
+
} catch {
|
|
2731
|
+
if (!abort.signal.aborted && state.requestGeneration === requestGeneration) this.release(messageId);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
async play(messageId, explicit) {
|
|
2735
|
+
const state = this.states.get(messageId);
|
|
2736
|
+
if (state?.audioUrl === void 0 || this.disposed || !this.enabled) return;
|
|
2737
|
+
if (!explicit && this.activeMessageId !== void 0 && this.activeMessageId !== messageId) return;
|
|
2738
|
+
if (this.activeMessageId !== void 0 && this.activeMessageId !== messageId) this.stopActive();
|
|
2739
|
+
const generation = ++this.playbackGeneration;
|
|
2740
|
+
this.activeMessageId = messageId;
|
|
2741
|
+
this.clearPauseResolution();
|
|
2742
|
+
if (this.loadedMessageId !== messageId) {
|
|
2743
|
+
this.audio.pause();
|
|
2744
|
+
this.audio.src = state.audioUrl;
|
|
2745
|
+
this.loadedMessageId = messageId;
|
|
2746
|
+
this.audio.currentTime = state.progress > 0 && (state.duration <= 0 || state.progress < state.duration * .98) ? state.progress : 0;
|
|
2747
|
+
} else if (state.ended === true) this.audio.currentTime = 0;
|
|
2748
|
+
this.audio.onended = () => {
|
|
2749
|
+
this.clearPauseResolution();
|
|
2750
|
+
this.finishPlayback(generation, messageId, true);
|
|
2751
|
+
};
|
|
2752
|
+
this.audio.onerror = () => {
|
|
2753
|
+
if (!this.owns(generation, messageId)) return;
|
|
2754
|
+
this.fail(messageId, "The spoken summary could not be played.");
|
|
2755
|
+
this.clearAudioOwnership(true);
|
|
2756
|
+
};
|
|
2757
|
+
this.audio.onpause = () => {
|
|
2758
|
+
if (!this.owns(generation, messageId)) return;
|
|
2759
|
+
this.clearPauseResolution();
|
|
2760
|
+
this.pauseResolutionTimer = setTimeout(() => {
|
|
2761
|
+
this.pauseResolutionTimer = void 0;
|
|
2762
|
+
this.finishPlayback(generation, messageId, this.audio.ended);
|
|
2763
|
+
}, 80);
|
|
2764
|
+
};
|
|
2765
|
+
this.audio.ontimeupdate = () => {
|
|
2766
|
+
if (!this.owns(generation, messageId)) return;
|
|
2767
|
+
const duration = Number.isFinite(this.audio.duration) ? this.audio.duration : state.duration;
|
|
2768
|
+
this.patch(messageId, {
|
|
2769
|
+
progress: Math.max(0, this.audio.currentTime),
|
|
2770
|
+
duration: Math.max(0, duration)
|
|
2771
|
+
});
|
|
2772
|
+
};
|
|
2773
|
+
this.audio.onplaying = () => {
|
|
2774
|
+
if (!this.owns(generation, messageId)) return;
|
|
2775
|
+
this.autoplayConsumed.add(messageId);
|
|
2776
|
+
};
|
|
2777
|
+
this.patch(messageId, {
|
|
2778
|
+
phase: "playing",
|
|
2779
|
+
ended: false
|
|
2780
|
+
});
|
|
2781
|
+
try {
|
|
2782
|
+
await this.audio.play();
|
|
2783
|
+
if (!this.owns(generation, messageId)) return;
|
|
2784
|
+
this.autoplayConsumed.add(messageId);
|
|
2785
|
+
this.activeSessionKey = this.messageSessions.get(messageId);
|
|
2786
|
+
this.publish();
|
|
2787
|
+
this.startAnalysis(messageId, generation);
|
|
2788
|
+
} catch {
|
|
2789
|
+
if (!this.owns(generation, messageId)) return;
|
|
2790
|
+
this.patch(messageId, { phase: "ready" });
|
|
2791
|
+
this.clearAudioOwnership(false);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
pause(messageId) {
|
|
2795
|
+
if (this.activeMessageId !== messageId) return;
|
|
2796
|
+
this.patch(messageId, {
|
|
2797
|
+
phase: "ready",
|
|
2798
|
+
progress: Math.max(0, this.audio.currentTime),
|
|
2799
|
+
ended: false
|
|
2800
|
+
});
|
|
2801
|
+
++this.playbackGeneration;
|
|
2802
|
+
this.audio.pause();
|
|
2803
|
+
this.clearAudioOwnership(false);
|
|
2804
|
+
}
|
|
2805
|
+
retry(source, settings) {
|
|
2806
|
+
if (!this.enabled || !settings.ttsEnabled) return;
|
|
2807
|
+
const state = this.ensure(source.messageId);
|
|
2808
|
+
if (this.loadedMessageId === source.messageId) this.clearAudioOwnership(true);
|
|
2809
|
+
state.abort?.abort();
|
|
2810
|
+
state.requestGeneration += 1;
|
|
2811
|
+
revokeAudioUrl(state.audioUrl);
|
|
2812
|
+
Object.assign(state, {
|
|
2813
|
+
phase: "idle",
|
|
2814
|
+
audioUrl: void 0,
|
|
2815
|
+
summary: void 0,
|
|
2816
|
+
error: void 0,
|
|
2817
|
+
progress: 0,
|
|
2818
|
+
duration: 0,
|
|
2819
|
+
peaks: EMPTY_PEAKS,
|
|
2820
|
+
ended: false
|
|
2821
|
+
});
|
|
2822
|
+
this.publish();
|
|
2823
|
+
this.prepare(source, settings, false).then(() => this.play(source.messageId, true));
|
|
2824
|
+
}
|
|
2825
|
+
dispose() {
|
|
2826
|
+
if (this.disposed) return;
|
|
2827
|
+
this.disposed = true;
|
|
2828
|
+
this.stopActive();
|
|
2829
|
+
this.clearAudioOwnership(true);
|
|
2830
|
+
for (const state of this.states.values()) {
|
|
2831
|
+
state.abort?.abort();
|
|
2832
|
+
revokeAudioUrl(state.audioUrl);
|
|
2833
|
+
}
|
|
2834
|
+
this.states.clear();
|
|
2835
|
+
this.observed.clear();
|
|
2836
|
+
this.messageSessions.clear();
|
|
2837
|
+
this.observedInteractions.clear();
|
|
2838
|
+
this.observedInteractionSessions.clear();
|
|
2839
|
+
this.autoplayConsumed.clear();
|
|
2840
|
+
this.voiceCache.clear();
|
|
2841
|
+
this.analyserContext?.close().catch(() => {});
|
|
2842
|
+
this.analyserContext = void 0;
|
|
2843
|
+
this.analyserSource = void 0;
|
|
2844
|
+
this.analyser = void 0;
|
|
2845
|
+
this.analyserData = void 0;
|
|
2846
|
+
this.listeners.clear();
|
|
2847
|
+
}
|
|
2848
|
+
async voiceFor(binding, preferred) {
|
|
2849
|
+
if (preferred !== void 0 && preferred.length > 0) return preferred;
|
|
2850
|
+
const key = `${binding.provider}\n${binding.model}`;
|
|
2851
|
+
let promise = this.voiceCache.get(key);
|
|
2852
|
+
if (promise === void 0) {
|
|
2853
|
+
promise = speechApi.voices({
|
|
2854
|
+
model: binding.aliases?.[0] ?? binding.model,
|
|
2855
|
+
provider: binding.provider
|
|
2856
|
+
}).then((result) => result.voices.map((voice) => voice.id)).catch((error) => {
|
|
2857
|
+
this.voiceCache.delete(key);
|
|
2858
|
+
throw error;
|
|
2859
|
+
});
|
|
2860
|
+
this.voiceCache.set(key, promise);
|
|
2861
|
+
}
|
|
2862
|
+
const voices = await promise;
|
|
2863
|
+
const localized = preferredTtsSelection(binding.model.startsWith("minimax/") ? "zh" : "en");
|
|
2864
|
+
return binding.model === localized.model && binding.provider === localized.provider && voices.includes(localized.voice) ? localized.voice : binding.defaultVoice !== void 0 ? binding.defaultVoice : voices[0];
|
|
2865
|
+
}
|
|
2866
|
+
startAnalysis(messageId, generation) {
|
|
2867
|
+
this.stopAnalysis();
|
|
2868
|
+
this.resetAnalyser();
|
|
2869
|
+
this.ensureAnalyser();
|
|
2870
|
+
if (typeof requestAnimationFrame === "undefined") return;
|
|
2871
|
+
const frame = (time) => {
|
|
2872
|
+
if (!this.owns(generation, messageId)) return;
|
|
2873
|
+
if (time - this.analysisTick >= 45) {
|
|
2874
|
+
this.analysisTick = time;
|
|
2875
|
+
this.patch(messageId, { peaks: this.readAmplitude(time) });
|
|
2876
|
+
}
|
|
2877
|
+
this.analysisFrame = requestAnimationFrame(frame);
|
|
2878
|
+
};
|
|
2879
|
+
this.analysisFrame = requestAnimationFrame(frame);
|
|
2880
|
+
}
|
|
2881
|
+
ensureAnalyser() {
|
|
2882
|
+
if (this.analyser !== void 0) {
|
|
2883
|
+
this.analyserContext?.resume().catch(() => {});
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
if (typeof AudioContext === "undefined" || this.audio.captureStream === void 0) return;
|
|
2887
|
+
try {
|
|
2888
|
+
const context = new AudioContext();
|
|
2889
|
+
const analyser = context.createAnalyser();
|
|
2890
|
+
analyser.fftSize = 128;
|
|
2891
|
+
analyser.smoothingTimeConstant = .62;
|
|
2892
|
+
const source = context.createMediaStreamSource(this.audio.captureStream());
|
|
2893
|
+
source.connect(analyser);
|
|
2894
|
+
this.analyserContext = context;
|
|
2895
|
+
this.analyserSource = source;
|
|
2896
|
+
this.analyser = analyser;
|
|
2897
|
+
this.analyserData = new Uint8Array(analyser.fftSize);
|
|
2898
|
+
context.resume().catch(() => {});
|
|
2899
|
+
} catch {
|
|
2900
|
+
this.analyserContext = void 0;
|
|
2901
|
+
this.analyserSource = void 0;
|
|
2902
|
+
this.analyser = void 0;
|
|
2903
|
+
this.analyserData = void 0;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
readAmplitude(time) {
|
|
2907
|
+
if (this.analyser !== void 0 && this.analyserData !== void 0) {
|
|
2908
|
+
this.analyser.getByteTimeDomainData(this.analyserData);
|
|
2909
|
+
let signalPeak = 0;
|
|
2910
|
+
const peaks = Array.from({ length: 48 }, (_, index) => {
|
|
2911
|
+
const start = Math.floor(index * this.analyserData.length / 48);
|
|
2912
|
+
const end = Math.max(start + 1, Math.floor((index + 1) * this.analyserData.length / 48));
|
|
2913
|
+
let amplitude = 0;
|
|
2914
|
+
for (let sample = start; sample < end; sample += 1) amplitude = Math.max(amplitude, Math.abs((this.analyserData[sample] ?? 128) - 128) / 128);
|
|
2915
|
+
signalPeak = Math.max(signalPeak, amplitude);
|
|
2916
|
+
return Math.min(1, .06 + Math.sqrt(amplitude) * 2.4);
|
|
2917
|
+
});
|
|
2918
|
+
if (signalPeak > .004) return peaks;
|
|
2919
|
+
}
|
|
2920
|
+
return this.activityPeaks(time);
|
|
2921
|
+
}
|
|
2922
|
+
activityPeaks(time) {
|
|
2923
|
+
return Array.from({ length: 48 }, (_, index) => {
|
|
2924
|
+
const carrier = .5 + .5 * Math.sin(time * .014 + index * 1.37);
|
|
2925
|
+
const envelope = .5 + .5 * Math.sin(time * .0037 + index * .29);
|
|
2926
|
+
return .1 + .52 * carrier * envelope;
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
stopAnalysis() {
|
|
2930
|
+
if (this.analysisFrame !== void 0 && typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(this.analysisFrame);
|
|
2931
|
+
this.analysisFrame = void 0;
|
|
2932
|
+
this.analysisTick = 0;
|
|
2933
|
+
}
|
|
2934
|
+
clearPauseResolution() {
|
|
2935
|
+
if (this.pauseResolutionTimer !== void 0) clearTimeout(this.pauseResolutionTimer);
|
|
2936
|
+
this.pauseResolutionTimer = void 0;
|
|
2937
|
+
}
|
|
2938
|
+
resetAnalyser() {
|
|
2939
|
+
this.analyserContext?.close().catch(() => {});
|
|
2940
|
+
this.analyserContext = void 0;
|
|
2941
|
+
this.analyserSource = void 0;
|
|
2942
|
+
this.analyser = void 0;
|
|
2943
|
+
this.analyserData = void 0;
|
|
2944
|
+
}
|
|
2945
|
+
ensure(messageId) {
|
|
2946
|
+
let state = this.states.get(messageId);
|
|
2947
|
+
if (state === void 0) {
|
|
2948
|
+
state = {
|
|
2949
|
+
phase: "idle",
|
|
2950
|
+
duration: 0,
|
|
2951
|
+
progress: 0,
|
|
2952
|
+
peaks: EMPTY_PEAKS,
|
|
2953
|
+
requestGeneration: 0,
|
|
2954
|
+
mounts: 0,
|
|
2955
|
+
releaseGeneration: 0
|
|
2956
|
+
};
|
|
2957
|
+
this.states.set(messageId, state);
|
|
2958
|
+
}
|
|
2959
|
+
return state;
|
|
2960
|
+
}
|
|
2961
|
+
patch(messageId, patch) {
|
|
2962
|
+
Object.assign(this.ensure(messageId), patch);
|
|
2963
|
+
this.publish();
|
|
2964
|
+
}
|
|
2965
|
+
fail(messageId, message) {
|
|
2966
|
+
this.patch(messageId, {
|
|
2967
|
+
phase: "error",
|
|
2968
|
+
error: message
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
owns(generation, messageId) {
|
|
2972
|
+
return this.playbackGeneration === generation && this.activeMessageId === messageId;
|
|
2973
|
+
}
|
|
2974
|
+
finishPlayback(generation, messageId, ended) {
|
|
2975
|
+
if (!this.owns(generation, messageId)) return;
|
|
2976
|
+
const state = this.states.get(messageId);
|
|
2977
|
+
const measuredDuration = Number.isFinite(this.audio.duration) ? Math.max(0, this.audio.duration) : state?.duration ?? 0;
|
|
2978
|
+
if (state !== void 0) Object.assign(state, {
|
|
2979
|
+
phase: "ready",
|
|
2980
|
+
duration: measuredDuration,
|
|
2981
|
+
progress: ended ? measuredDuration : this.audio.currentTime,
|
|
2982
|
+
ended
|
|
2983
|
+
});
|
|
2984
|
+
++this.playbackGeneration;
|
|
2985
|
+
if (state?.ephemeral === true) {
|
|
2986
|
+
this.clearAudioOwnership(true);
|
|
2987
|
+
revokeAudioUrl(state.audioUrl);
|
|
2988
|
+
this.states.delete(messageId);
|
|
2989
|
+
} else this.clearAudioOwnership(false);
|
|
2990
|
+
this.publish();
|
|
2991
|
+
}
|
|
2992
|
+
stopActive() {
|
|
2993
|
+
const messageId = this.activeMessageId;
|
|
2994
|
+
if (messageId === void 0) return;
|
|
2995
|
+
const state = this.states.get(messageId);
|
|
2996
|
+
if (state !== void 0 && state.phase === "playing") Object.assign(state, {
|
|
2997
|
+
phase: "ready",
|
|
2998
|
+
progress: Math.max(0, this.audio.currentTime),
|
|
2999
|
+
ended: false
|
|
3000
|
+
});
|
|
3001
|
+
++this.playbackGeneration;
|
|
3002
|
+
this.audio.pause();
|
|
3003
|
+
this.clearAudioOwnership(false);
|
|
3004
|
+
this.publish();
|
|
3005
|
+
}
|
|
3006
|
+
clearAudioOwnership(unload) {
|
|
3007
|
+
this.stopAnalysis();
|
|
3008
|
+
this.clearPauseResolution();
|
|
3009
|
+
this.activeMessageId = void 0;
|
|
3010
|
+
this.activeSessionKey = void 0;
|
|
3011
|
+
this.audio.onended = null;
|
|
3012
|
+
this.audio.onplaying = null;
|
|
3013
|
+
this.audio.onerror = null;
|
|
3014
|
+
this.audio.onpause = null;
|
|
3015
|
+
this.audio.ontimeupdate = null;
|
|
3016
|
+
if (unload) {
|
|
3017
|
+
this.loadedMessageId = void 0;
|
|
3018
|
+
this.audio.removeAttribute("src");
|
|
3019
|
+
this.audio.load();
|
|
3020
|
+
}
|
|
3021
|
+
this.publish();
|
|
3022
|
+
}
|
|
3023
|
+
release(messageId) {
|
|
3024
|
+
const state = this.states.get(messageId);
|
|
3025
|
+
if (state === void 0) return;
|
|
3026
|
+
if (this.activeMessageId === messageId) this.stopActive();
|
|
3027
|
+
if (this.loadedMessageId === messageId) this.clearAudioOwnership(true);
|
|
3028
|
+
state.abort?.abort();
|
|
3029
|
+
state.requestGeneration += 1;
|
|
3030
|
+
revokeAudioUrl(state.audioUrl);
|
|
3031
|
+
this.states.delete(messageId);
|
|
3032
|
+
this.publish();
|
|
3033
|
+
}
|
|
3034
|
+
publish() {
|
|
3035
|
+
this.snapshot = {
|
|
3036
|
+
messages: new Map([...this.states].map(([id, state]) => [id, {
|
|
3037
|
+
phase: state.phase,
|
|
3038
|
+
...state.summary === void 0 ? {} : { summary: state.summary },
|
|
3039
|
+
...state.audioUrl === void 0 ? {} : { audioUrl: state.audioUrl },
|
|
3040
|
+
duration: state.duration,
|
|
3041
|
+
progress: state.progress,
|
|
3042
|
+
peaks: state.peaks,
|
|
3043
|
+
...state.error === void 0 ? {} : { error: state.error },
|
|
3044
|
+
...state.ended === void 0 ? {} : { ended: state.ended }
|
|
3045
|
+
}])),
|
|
3046
|
+
...this.activeMessageId === void 0 ? {} : { activeMessageId: this.activeMessageId },
|
|
3047
|
+
...this.activeSessionKey === void 0 ? {} : { activeSessionKey: this.activeSessionKey }
|
|
3048
|
+
};
|
|
3049
|
+
for (const listener of this.listeners) listener();
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
|
|
3053
|
+
//#endregion
|
|
3054
|
+
//#region src/client/SpokenSummary.tsx
|
|
3055
|
+
const EMPTY_REQUESTS = [];
|
|
3056
|
+
function preparationSettings(controller, settings, locale) {
|
|
3057
|
+
const fallback = preferredTtsSelection(locale);
|
|
3058
|
+
const selection = settings?.ttsModel !== void 0 && settings.ttsProvider !== void 0 && settings.ttsVoice !== void 0 ? {
|
|
3059
|
+
model: settings.ttsModel,
|
|
3060
|
+
provider: settings.ttsProvider,
|
|
3061
|
+
voice: settings.ttsVoice
|
|
3062
|
+
} : fallback;
|
|
3063
|
+
return {
|
|
3064
|
+
ttsEnabled: settings?.ttsEnabled ?? true,
|
|
3065
|
+
autoPlay: settings?.autoPlay ?? true,
|
|
3066
|
+
bindings: controller.getSnapshot().catalog?.ttsBindings ?? [],
|
|
3067
|
+
ttsModel: selection.model,
|
|
3068
|
+
ttsProvider: selection.provider,
|
|
3069
|
+
ttsVoice: selection.voice
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
function PlayIcon({ pause = false }) {
|
|
3073
|
+
return pause ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3074
|
+
viewBox: "0 0 20 20",
|
|
3075
|
+
"aria-hidden": "true",
|
|
3076
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 5h3v10H6zM11 5h3v10h-3z" })
|
|
3077
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3078
|
+
viewBox: "0 0 20 20",
|
|
3079
|
+
"aria-hidden": "true",
|
|
3080
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m7 4 9 6-9 6z" })
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
function Waveform({ peaks, preparing, playing }) {
|
|
3084
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3085
|
+
className: styles.summaryWaveform,
|
|
3086
|
+
"aria-hidden": "true",
|
|
3087
|
+
"data-preparing": preparing ? "true" : void 0,
|
|
3088
|
+
"data-playing": playing ? "true" : void 0,
|
|
3089
|
+
children: peaks.map((peak, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { "--dsh-speech-peak": String(peak) } }, index))
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
function SpokenSummaryTail({ controller, spoken, scope, getLocale, messageId, useSession, t }) {
|
|
3093
|
+
const nodes = useSession((snapshot) => snapshot.nodes);
|
|
3094
|
+
const turnEnds = useSession((snapshot) => snapshot.turnEnds);
|
|
3095
|
+
const requests = useSession((snapshot) => {
|
|
3096
|
+
const views = snapshot.views;
|
|
3097
|
+
if (views?.get === void 0) return EMPTY_REQUESTS;
|
|
3098
|
+
return views.get("trajectory")?.requests ?? EMPTY_REQUESTS;
|
|
3099
|
+
});
|
|
3100
|
+
const client = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
3101
|
+
const scopeState = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
3102
|
+
const spokenState = (0, react.useSyncExternalStore)(spoken.subscribe, spoken.getSnapshot);
|
|
3103
|
+
const locale = getLocale();
|
|
3104
|
+
const sources = (0, react.useMemo)(() => spokenSources(nodes, locale, turnEnds, requests), [
|
|
3105
|
+
nodes,
|
|
3106
|
+
locale,
|
|
3107
|
+
requests,
|
|
3108
|
+
turnEnds
|
|
3109
|
+
]);
|
|
3110
|
+
const id = String(messageId);
|
|
3111
|
+
const source = sources.find((candidate) => candidate.messageId === id);
|
|
3112
|
+
const settings = (0, react.useMemo)(() => preparationSettings(controller, scopeState.value, locale), [
|
|
3113
|
+
controller,
|
|
3114
|
+
scopeState.value,
|
|
3115
|
+
client.catalog,
|
|
3116
|
+
locale
|
|
3117
|
+
]);
|
|
3118
|
+
const state = spokenState.messages.get(id) ?? {
|
|
3119
|
+
phase: "idle",
|
|
3120
|
+
duration: 0,
|
|
3121
|
+
progress: 0,
|
|
3122
|
+
peaks: []
|
|
3123
|
+
};
|
|
3124
|
+
const latestAudioMessageId = [...sources].reverse().find((candidate) => spokenState.messages.get(candidate.messageId)?.audioUrl !== void 0)?.messageId;
|
|
3125
|
+
const latestSourceMessageId = sources.at(-1)?.messageId;
|
|
3126
|
+
const legacyAutoplayWasUsed = scopeState.user?.autoPlay !== void 0;
|
|
3127
|
+
const autoplayInlineRevealed = scopeState.value?.autoplayInlineRevealed === true || legacyAutoplayWasUsed;
|
|
3128
|
+
const autoplayHostMessageId = latestAudioMessageId ?? (autoplayInlineRevealed ? latestSourceMessageId : void 0);
|
|
3129
|
+
(0, react.useEffect)(() => spoken.mount(id), [spoken, id]);
|
|
3130
|
+
(0, react.useEffect)(() => {
|
|
3131
|
+
if (autoplayHostMessageId !== id || scopeState.value?.autoplayInlineRevealed === true || !scopeState.writable) return;
|
|
3132
|
+
scope.set("autoplayInlineRevealed", true);
|
|
3133
|
+
}, [
|
|
3134
|
+
autoplayHostMessageId,
|
|
3135
|
+
id,
|
|
3136
|
+
scope,
|
|
3137
|
+
scopeState.value?.autoplayInlineRevealed,
|
|
3138
|
+
scopeState.writable
|
|
3139
|
+
]);
|
|
3140
|
+
if (!settings.ttsEnabled || source === void 0) return null;
|
|
3141
|
+
const preparing = state.phase === "preparing";
|
|
3142
|
+
const unavailable = source.route === void 0 || client.catalog !== void 0 && settings.bindings.length === 0;
|
|
3143
|
+
const replay = state.phase === "ready" && state.ended === true;
|
|
3144
|
+
const paused = state.phase === "ready" && state.progress > 0 && !replay;
|
|
3145
|
+
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");
|
|
3146
|
+
const activate = () => {
|
|
3147
|
+
if (unavailable) return;
|
|
3148
|
+
if (state.phase === "playing") {
|
|
3149
|
+
spoken.pause(id);
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
if (state.phase === "ready") {
|
|
3153
|
+
spoken.play(id, true);
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
if (state.phase === "error") {
|
|
3157
|
+
spoken.retry(source, settings);
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
spoken.prepare(source, settings, false).then(() => spoken.play(id, true));
|
|
3161
|
+
};
|
|
3162
|
+
const showLabel = state.phase === "idle" || state.phase === "error" || unavailable;
|
|
3163
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3164
|
+
className: styles.summaryPlayer,
|
|
3165
|
+
"data-phase": state.phase,
|
|
3166
|
+
children: [
|
|
3167
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3168
|
+
className: styles.summaryControl,
|
|
3169
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3170
|
+
type: "button",
|
|
3171
|
+
className: styles.summaryButton,
|
|
3172
|
+
disabled: preparing || unavailable,
|
|
3173
|
+
"aria-label": label,
|
|
3174
|
+
title: label,
|
|
3175
|
+
onClick: activate,
|
|
3176
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayIcon, { pause: state.phase === "playing" })
|
|
3177
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Waveform, {
|
|
3178
|
+
peaks: state.peaks.length === 48 ? state.peaks : Array.from({ length: 48 }, () => .12),
|
|
3179
|
+
preparing,
|
|
3180
|
+
playing: state.phase === "playing"
|
|
3181
|
+
})]
|
|
3182
|
+
}),
|
|
3183
|
+
showLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3184
|
+
className: styles.summaryLabel,
|
|
3185
|
+
children: label
|
|
3186
|
+
}) : null,
|
|
3187
|
+
autoplayHostMessageId !== id ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3188
|
+
type: "button",
|
|
3189
|
+
className: styles.autoplayToggle,
|
|
3190
|
+
role: "switch",
|
|
3191
|
+
"aria-checked": scopeState.value?.autoPlay ?? true,
|
|
3192
|
+
"aria-label": `${t("autoplayInline")}: ${scopeState.value?.autoPlay ?? true ? t("autoplayOn") : t("autoplayOff")}`,
|
|
3193
|
+
title: `${t("autoplayInline")}: ${scopeState.value?.autoPlay ?? true ? t("autoplayOn") : t("autoplayOff")}`,
|
|
3194
|
+
disabled: !scopeState.writable,
|
|
3195
|
+
onClick: () => {
|
|
3196
|
+
scope.set("autoPlay", !(scopeState.value?.autoPlay ?? true));
|
|
3197
|
+
},
|
|
3198
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3199
|
+
className: styles.switchTrack,
|
|
3200
|
+
"aria-hidden": "true",
|
|
3201
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {})
|
|
3202
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("autoplayInline") })]
|
|
3203
|
+
}),
|
|
3204
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3205
|
+
className: styles.srOnly,
|
|
3206
|
+
role: "status",
|
|
3207
|
+
"aria-live": "polite",
|
|
3208
|
+
children: state.phase === "error" ? state.error ?? t("summaryError") : label
|
|
3209
|
+
})
|
|
3210
|
+
]
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
function SpokenSessionObserver({ controller, spoken, scope, getLocale, sidebarPlayback, sessionId, useSession }) {
|
|
3214
|
+
const state = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
3215
|
+
const client = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
3216
|
+
const openState = useSession((snapshot) => snapshot.openState);
|
|
3217
|
+
const pending = useSession((snapshot) => snapshot.pending);
|
|
3218
|
+
const nodes = useSession((snapshot) => snapshot.nodes);
|
|
3219
|
+
const turnEnds = useSession((snapshot) => snapshot.turnEnds);
|
|
3220
|
+
const requests = useSession((snapshot) => {
|
|
3221
|
+
const views = snapshot.views;
|
|
3222
|
+
if (views?.get === void 0) return EMPTY_REQUESTS;
|
|
3223
|
+
return views.get("trajectory")?.requests ?? EMPTY_REQUESTS;
|
|
3224
|
+
});
|
|
3225
|
+
const speechLocale = state.value?.language === void 0 || state.value.language === "auto" ? getLocale() : state.value.language;
|
|
3226
|
+
const settings = (0, react.useMemo)(() => preparationSettings(controller, state.value, speechLocale), [
|
|
3227
|
+
client.catalog,
|
|
3228
|
+
controller,
|
|
3229
|
+
speechLocale,
|
|
3230
|
+
state.value
|
|
3231
|
+
]);
|
|
3232
|
+
const sources = (0, react.useMemo)(() => spokenSources(nodes, speechLocale, turnEnds, requests), [
|
|
3233
|
+
nodes,
|
|
3234
|
+
requests,
|
|
3235
|
+
speechLocale,
|
|
3236
|
+
turnEnds
|
|
3237
|
+
]);
|
|
3238
|
+
(0, react.useEffect)(() => {
|
|
3239
|
+
controller.ensureMetadata();
|
|
3240
|
+
}, [controller]);
|
|
3241
|
+
(0, react.useEffect)(() => {
|
|
3242
|
+
sidebarPlayback.observeCurrentSession(String(sessionId));
|
|
3243
|
+
}, [sessionId, sidebarPlayback]);
|
|
3244
|
+
(0, react.useEffect)(() => {
|
|
3245
|
+
spoken.setEnabled(settings.ttsEnabled);
|
|
3246
|
+
}, [settings.ttsEnabled, spoken]);
|
|
3247
|
+
(0, react.useEffect)(() => {
|
|
3248
|
+
if (client.catalog === void 0 || openState !== "open") return;
|
|
3249
|
+
spoken.observeSession(String(sessionId), sources, settings);
|
|
3250
|
+
}, [
|
|
3251
|
+
client.catalog,
|
|
3252
|
+
openState,
|
|
3253
|
+
sessionId,
|
|
3254
|
+
settings,
|
|
3255
|
+
sources,
|
|
3256
|
+
spoken
|
|
3257
|
+
]);
|
|
3258
|
+
(0, react.useEffect)(() => {
|
|
3259
|
+
if (client.catalog === void 0 || openState !== "open") return;
|
|
3260
|
+
spoken.observeInteractions(String(sessionId), pending.map((item) => item.key), speechLocale, settings);
|
|
3261
|
+
}, [
|
|
3262
|
+
client.catalog,
|
|
3263
|
+
openState,
|
|
3264
|
+
pending,
|
|
3265
|
+
sessionId,
|
|
3266
|
+
settings,
|
|
3267
|
+
speechLocale,
|
|
3268
|
+
spoken
|
|
3269
|
+
]);
|
|
3270
|
+
return null;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
//#endregion
|
|
3274
|
+
//#region src/client/sidebar-playback-indicator.ts
|
|
3275
|
+
const INDICATOR_CLASS = "dsh-speech-sidebar-playing";
|
|
3276
|
+
const ROW_ATTRIBUTE = "data-dsh-speech-playing-session";
|
|
3277
|
+
function selectedSessionRow() {
|
|
3278
|
+
if (typeof document === "undefined") return void 0;
|
|
3279
|
+
return document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]") ?? void 0;
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3282
|
+
* Adds a memory-only playback marker to the Harness session row that owned
|
|
3283
|
+
* the selected conversation when audio began. Harness RC does not expose a
|
|
3284
|
+
* per-session-row slot, so this bridge relies only on its ARIA tree contract.
|
|
3285
|
+
*/
|
|
3286
|
+
var SidebarPlaybackIndicator = class {
|
|
3287
|
+
rows = /* @__PURE__ */ new Map();
|
|
3288
|
+
unsubscribe;
|
|
3289
|
+
observer;
|
|
3290
|
+
currentSessionKey;
|
|
3291
|
+
activeSessionKey;
|
|
3292
|
+
disposed = false;
|
|
3293
|
+
constructor(spoken) {
|
|
3294
|
+
this.spoken = spoken;
|
|
3295
|
+
this.unsubscribe = spoken.subscribe(() => {
|
|
3296
|
+
const activeSessionKey = spoken.getSnapshot().activeSessionKey;
|
|
3297
|
+
if (activeSessionKey === this.activeSessionKey) return;
|
|
3298
|
+
this.activeSessionKey = activeSessionKey;
|
|
3299
|
+
this.refresh();
|
|
3300
|
+
});
|
|
3301
|
+
this.activeSessionKey = spoken.getSnapshot().activeSessionKey;
|
|
3302
|
+
this.observer = typeof MutationObserver === "undefined" || typeof document === "undefined" ? void 0 : new MutationObserver(() => {
|
|
3303
|
+
this.refresh();
|
|
3304
|
+
});
|
|
3305
|
+
this.observer?.observe(document.body, {
|
|
3306
|
+
childList: true,
|
|
3307
|
+
subtree: true,
|
|
3308
|
+
attributes: true,
|
|
3309
|
+
attributeFilter: ["aria-selected"]
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
observeCurrentSession(sessionKey) {
|
|
3313
|
+
if (this.disposed) return;
|
|
3314
|
+
this.currentSessionKey = sessionKey;
|
|
3315
|
+
this.refresh();
|
|
3316
|
+
}
|
|
3317
|
+
dispose() {
|
|
3318
|
+
if (this.disposed) return;
|
|
3319
|
+
this.disposed = true;
|
|
3320
|
+
this.unsubscribe();
|
|
3321
|
+
this.observer?.disconnect();
|
|
3322
|
+
this.removeIndicators();
|
|
3323
|
+
this.rows.clear();
|
|
3324
|
+
}
|
|
3325
|
+
refresh() {
|
|
3326
|
+
if (this.disposed) return;
|
|
3327
|
+
const selected = selectedSessionRow();
|
|
3328
|
+
if (selected !== void 0 && this.currentSessionKey !== void 0) this.rows.set(this.currentSessionKey, selected);
|
|
3329
|
+
if (this.activeSessionKey === void 0) {
|
|
3330
|
+
this.removeIndicators();
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
let row = this.rows.get(this.activeSessionKey);
|
|
3334
|
+
if (row?.isConnected !== true) {
|
|
3335
|
+
this.rows.delete(this.activeSessionKey);
|
|
3336
|
+
row = this.currentSessionKey === this.activeSessionKey ? selected : void 0;
|
|
3337
|
+
if (row !== void 0) this.rows.set(this.activeSessionKey, row);
|
|
3338
|
+
}
|
|
3339
|
+
if (row === void 0) {
|
|
3340
|
+
this.removeIndicators();
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
if (document.querySelector(`.${INDICATOR_CLASS}`)?.parentElement === row) return;
|
|
3344
|
+
this.removeIndicators();
|
|
3345
|
+
row.setAttribute(ROW_ATTRIBUTE, "");
|
|
3346
|
+
const indicator = document.createElement("span");
|
|
3347
|
+
indicator.className = INDICATOR_CLASS;
|
|
3348
|
+
indicator.setAttribute("role", "img");
|
|
3349
|
+
indicator.setAttribute("aria-label", "Playing spoken summary");
|
|
3350
|
+
indicator.setAttribute("title", "Playing spoken summary");
|
|
3351
|
+
indicator.innerHTML = "<svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M5 3.25v9.5L12.5 8 5 3.25Z\"/></svg>";
|
|
3352
|
+
row.append(indicator);
|
|
3353
|
+
}
|
|
3354
|
+
removeIndicators() {
|
|
3355
|
+
if (typeof document === "undefined") return;
|
|
3356
|
+
for (const row of document.querySelectorAll(`[${ROW_ATTRIBUTE}]`)) row.removeAttribute(ROW_ATTRIBUTE);
|
|
3357
|
+
for (const indicator of document.querySelectorAll(`.${INDICATOR_CLASS}`)) indicator.remove();
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
|
|
1675
3361
|
//#endregion
|
|
1676
3362
|
//#region src/client/locales.ts
|
|
1677
3363
|
const en = {
|
|
@@ -1698,6 +3384,8 @@ const en = {
|
|
|
1698
3384
|
disconnect: "Disconnect",
|
|
1699
3385
|
settings: "Recognition",
|
|
1700
3386
|
model: "STT model",
|
|
3387
|
+
modelSearchPlaceholder: "Search or select a model",
|
|
3388
|
+
modelNoResults: "No matching models",
|
|
1701
3389
|
provider: "Provider",
|
|
1702
3390
|
language: "Language",
|
|
1703
3391
|
invalidLanguage: "Enter Auto or a valid language tag such as en, zh-CN, or ja.",
|
|
@@ -1735,7 +3423,38 @@ const en = {
|
|
|
1735
3423
|
disconnectedMic: "Connect AllModels in Settings → Speech",
|
|
1736
3424
|
emptyMic: "Top up your AllModels balance in Settings → Speech",
|
|
1737
3425
|
anotherMic: "The microphone is active in another session",
|
|
1738
|
-
liveModel: "{provider} · {model}"
|
|
3426
|
+
liveModel: "{provider} · {model}",
|
|
3427
|
+
spokenSummaries: "Spoken summaries",
|
|
3428
|
+
spokenSummariesHint: "Completed answers are summarized with their recorded LLM route, then spoken through AllModels. Nothing is added to the conversation.",
|
|
3429
|
+
ttsEnabled: "Text-to-speech summaries",
|
|
3430
|
+
ttsModel: "TTS model",
|
|
3431
|
+
ttsProvider: "TTS provider",
|
|
3432
|
+
ttsVoice: "Voice",
|
|
3433
|
+
voiceSearch: "Search AllModels voices",
|
|
3434
|
+
voiceSearchHint: "Optional. Choosing a voice automatically selects its compatible model and provider.",
|
|
3435
|
+
voiceSearchPlaceholder: "Search by name, style, accent, or description",
|
|
3436
|
+
modelVoiceSearchHint: "Search only voices compatible with {model}.",
|
|
3437
|
+
modelVoiceSearchPlaceholder: "Type to search voices for this model",
|
|
3438
|
+
voiceNoResults: "No matching voices",
|
|
3439
|
+
ttsUnavailable: "No compatible synchronous MP3 text-to-speech route is currently advertised.",
|
|
3440
|
+
summaryCache: "Summary cache",
|
|
3441
|
+
summaryCacheHint: "Summaries are stored only in this browser for up to 30 days. The cache is limited to 500 entries.",
|
|
3442
|
+
clearSummaryCache: "Clear cache",
|
|
3443
|
+
summaryCacheCleared: "Summary cache cleared",
|
|
3444
|
+
autoplayGlobal: "Autoplay",
|
|
3445
|
+
autoplayShort: "Spoken summaries",
|
|
3446
|
+
autoplayInline: "Auto play",
|
|
3447
|
+
autoplayOn: "Autoplay on",
|
|
3448
|
+
autoplayOff: "Autoplay off",
|
|
3449
|
+
summaryPlay: "Play summary",
|
|
3450
|
+
summaryGenerate: "Generate and play summary",
|
|
3451
|
+
summaryPause: "Pause summary",
|
|
3452
|
+
summaryResume: "Resume summary",
|
|
3453
|
+
summaryReplay: "Replay summary",
|
|
3454
|
+
summaryRetry: "Retry spoken summary",
|
|
3455
|
+
summaryPreparing: "Preparing spoken summary…",
|
|
3456
|
+
summaryError: "Could not prepare the spoken summary.",
|
|
3457
|
+
summaryUnavailable: "Spoken summary unavailable for this answer"
|
|
1739
3458
|
};
|
|
1740
3459
|
const zh = {
|
|
1741
3460
|
nav: "语音",
|
|
@@ -1761,6 +3480,8 @@ const zh = {
|
|
|
1761
3480
|
disconnect: "断开连接",
|
|
1762
3481
|
settings: "语音识别",
|
|
1763
3482
|
model: "STT 模型",
|
|
3483
|
+
modelSearchPlaceholder: "搜索或选择模型",
|
|
3484
|
+
modelNoResults: "没有匹配的模型",
|
|
1764
3485
|
provider: "服务商",
|
|
1765
3486
|
language: "语言",
|
|
1766
3487
|
invalidLanguage: "请输入“自动”或有效的语言标签,例如 en、zh-CN 或 ja。",
|
|
@@ -1798,7 +3519,38 @@ const zh = {
|
|
|
1798
3519
|
disconnectedMic: "请前往“设置 → 语音”连接 AllModels",
|
|
1799
3520
|
emptyMic: "请前往“设置 → 语音”为 AllModels 充值",
|
|
1800
3521
|
anotherMic: "麦克风正在另一个会话中使用",
|
|
1801
|
-
liveModel: "{provider} · {model}"
|
|
3522
|
+
liveModel: "{provider} · {model}",
|
|
3523
|
+
spokenSummaries: "语音摘要",
|
|
3524
|
+
spokenSummariesHint: "使用回答中记录的 LLM 路由生成简短摘要,再通过 AllModels 播放。不会向对话添加任何内容。",
|
|
3525
|
+
ttsEnabled: "文本转语音摘要",
|
|
3526
|
+
ttsModel: "TTS 模型",
|
|
3527
|
+
ttsProvider: "TTS 服务商",
|
|
3528
|
+
ttsVoice: "声音",
|
|
3529
|
+
voiceSearch: "搜索 AllModels 声音",
|
|
3530
|
+
voiceSearchHint: "可选。选择声音后会自动设置兼容的模型和服务商。",
|
|
3531
|
+
voiceSearchPlaceholder: "按名称、风格、口音或描述搜索",
|
|
3532
|
+
modelVoiceSearchHint: "仅搜索与 {model} 兼容的声音。",
|
|
3533
|
+
modelVoiceSearchPlaceholder: "输入关键词搜索此模型的声音",
|
|
3534
|
+
voiceNoResults: "没有匹配的声音",
|
|
3535
|
+
ttsUnavailable: "目前没有兼容的同步 MP3 文本转语音路由。",
|
|
3536
|
+
summaryCache: "摘要缓存",
|
|
3537
|
+
summaryCacheHint: "摘要仅在此浏览器中保存最多 30 天,缓存上限为 500 条。",
|
|
3538
|
+
clearSummaryCache: "清除缓存",
|
|
3539
|
+
summaryCacheCleared: "摘要缓存已清除",
|
|
3540
|
+
autoplayGlobal: "自动播放",
|
|
3541
|
+
autoplayShort: "语音摘要",
|
|
3542
|
+
autoplayInline: "自动播放",
|
|
3543
|
+
autoplayOn: "自动播放已开启",
|
|
3544
|
+
autoplayOff: "自动播放已关闭",
|
|
3545
|
+
summaryPlay: "播放摘要",
|
|
3546
|
+
summaryGenerate: "生成并播放摘要",
|
|
3547
|
+
summaryPause: "暂停摘要",
|
|
3548
|
+
summaryResume: "继续播放摘要",
|
|
3549
|
+
summaryReplay: "重新播放摘要",
|
|
3550
|
+
summaryRetry: "重试语音摘要",
|
|
3551
|
+
summaryPreparing: "正在准备语音摘要…",
|
|
3552
|
+
summaryError: "无法准备语音摘要。",
|
|
3553
|
+
summaryUnavailable: "此回答无法生成语音摘要"
|
|
1802
3554
|
};
|
|
1803
3555
|
|
|
1804
3556
|
//#endregion
|
|
@@ -1810,6 +3562,9 @@ const inject = [
|
|
|
1810
3562
|
];
|
|
1811
3563
|
function apply(ctx) {
|
|
1812
3564
|
const controller = new SpeechController();
|
|
3565
|
+
const summaryCache = new SummaryCache();
|
|
3566
|
+
const spoken = new SpokenSummaryController(void 0, summaryCache);
|
|
3567
|
+
const sidebarPlayback = new SidebarPlaybackIndicator(spoken);
|
|
1813
3568
|
const scope = ctx.settingsScope.bind({ namespace: "dsh-speech" });
|
|
1814
3569
|
const getLocale = () => ctx.locale.getLocale().active;
|
|
1815
3570
|
ctx.effect(() => ctx.locale.register("speech", {
|
|
@@ -1847,8 +3602,12 @@ function apply(ctx) {
|
|
|
1847
3602
|
};
|
|
1848
3603
|
}, "dsh-speech: settings navigation icon");
|
|
1849
3604
|
ctx.effect(() => () => {
|
|
3605
|
+
sidebarPlayback.dispose();
|
|
1850
3606
|
controller.dispose();
|
|
1851
|
-
|
|
3607
|
+
spoken.dispose();
|
|
3608
|
+
summaryCache.dispose();
|
|
3609
|
+
}, "dsh-speech: browser controllers");
|
|
3610
|
+
controller.ensureMetadata();
|
|
1852
3611
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1853
3612
|
name: "settings.section",
|
|
1854
3613
|
id: "speech",
|
|
@@ -1858,7 +3617,8 @@ function apply(ctx) {
|
|
|
1858
3617
|
inject: () => ({
|
|
1859
3618
|
controller,
|
|
1860
3619
|
scope,
|
|
1861
|
-
getLocale
|
|
3620
|
+
getLocale,
|
|
3621
|
+
summaryCache
|
|
1862
3622
|
})
|
|
1863
3623
|
}, SpeechSettings));
|
|
1864
3624
|
ctx.inject(["conversation"], (scoped) => {
|
|
@@ -1894,11 +3654,38 @@ function apply(ctx) {
|
|
|
1894
3654
|
getLocale
|
|
1895
3655
|
})
|
|
1896
3656
|
}, SpeechInputDock);
|
|
3657
|
+
const spokenAction = scoped.slots.register({
|
|
3658
|
+
name: "conversation.chat.assistant-actions",
|
|
3659
|
+
id: "spoken-summary",
|
|
3660
|
+
order: -100,
|
|
3661
|
+
locale: "speech",
|
|
3662
|
+
inject: () => ({
|
|
3663
|
+
controller,
|
|
3664
|
+
spoken,
|
|
3665
|
+
scope,
|
|
3666
|
+
getLocale,
|
|
3667
|
+
sidebarPlayback
|
|
3668
|
+
})
|
|
3669
|
+
}, SpokenSummaryTail);
|
|
3670
|
+
const spokenObserver = scoped.slots.register({
|
|
3671
|
+
name: "conversation.composer.dock",
|
|
3672
|
+
id: "spoken-summary-observer",
|
|
3673
|
+
order: 110,
|
|
3674
|
+
inject: () => ({
|
|
3675
|
+
controller,
|
|
3676
|
+
spoken,
|
|
3677
|
+
scope,
|
|
3678
|
+
getLocale,
|
|
3679
|
+
sidebarPlayback
|
|
3680
|
+
})
|
|
3681
|
+
}, SpokenSessionObserver);
|
|
1897
3682
|
return () => {
|
|
1898
3683
|
controller.detachBlocks(blocks);
|
|
1899
3684
|
mic();
|
|
1900
3685
|
dock();
|
|
1901
3686
|
inputDock();
|
|
3687
|
+
spokenAction();
|
|
3688
|
+
spokenObserver();
|
|
1902
3689
|
};
|
|
1903
3690
|
});
|
|
1904
3691
|
}
|