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