@allmodels/dsh-speech 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/client.js +415 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,13 +156,13 @@ After all jobs for a `main` push pass, the Release workflow checks out the exact
|
|
|
156
156
|
|
|
157
157
|
## Harness compatibility monitoring
|
|
158
158
|
|
|
159
|
-
GitHub Actions checks npm once per day for the most recently published `@deepseek-ai/dsh` version. It exits without installing browsers when
|
|
159
|
+
GitHub Actions checks npm once per day for the most recently published stable or `-rc` `@deepseek-ai/dsh` version. Alpha, beta, canary, development, and other prerelease channels are ignored. It exits without installing browsers when the newest eligible version is already tracked in `dsh-compatibility.json`, or when the same version already has an open compatibility issue or update pull request.
|
|
160
160
|
|
|
161
161
|
For a new version, the workflow updates the Harness peer/development dependency pins in an isolated checkout, installs that dependency set, runs the static and unit gates, packs the plugin into a disposable profile, and runs the desktop/mobile Chromium suite. It never receives the live AllModels or DeepSeek credentials.
|
|
162
162
|
|
|
163
163
|
- A failure creates one `dsh-compatibility` issue for that Harness version with a link to the failed run. Later daily checks do not duplicate it.
|
|
164
164
|
- A pass opens a reviewable pull request updating the tested version, peer dependencies, development dependencies, README, and lockfile. Merging remains manual.
|
|
165
|
-
- A manual run can target an explicit version or force a retest. When a forced retest fixes an open compatibility issue, the successful update pull request closes it.
|
|
165
|
+
- A manual run can target an explicit stable or `-rc` version or force a retest. Other prerelease channels are rejected by the same release policy used by the daily scheduler. When a forced retest fixes an open compatibility issue, the successful update pull request closes it.
|
|
166
166
|
|
|
167
167
|
Workflow-created pull requests require the repository’s **Settings → Actions → General → Allow GitHub Actions to create and approve pull requests** option. The compatibility workflow requests only the exact `contents`, `pull-requests`, and `issues` write permissions needed by its update job and contains no approval operation.
|
|
168
168
|
|
package/lib/client.js
CHANGED
|
@@ -303,8 +303,10 @@ function writePreferredMicrophone(deviceId) {
|
|
|
303
303
|
//#region src/shared.ts
|
|
304
304
|
const CATALOG_TTL_MS = 300 * 1e3;
|
|
305
305
|
const AUDIO_FORMAT = "pcm_16000";
|
|
306
|
+
const MAX_TTS_CHARACTERS = 4096;
|
|
306
307
|
const MAX_SUMMARY_REQUEST_CHARACTERS = 16e3;
|
|
307
308
|
const MAX_SUMMARY_ANSWER_CHARACTERS = 64e3;
|
|
309
|
+
const SUMMARY_PROMPT_VERSION = 1;
|
|
308
310
|
const DEFAULT_ENGLISH_TTS = {
|
|
309
311
|
model: "fish/s2.1-pro",
|
|
310
312
|
provider: "fish",
|
|
@@ -779,7 +781,9 @@ const styles = {
|
|
|
779
781
|
summaryWaveform: "dsh-speech-summary-waveform",
|
|
780
782
|
summaryLabel: "dsh-speech-summary-label",
|
|
781
783
|
autoplayToggle: "dsh-speech-autoplay-toggle",
|
|
784
|
+
sidebarPlaying: "dsh-speech-sidebar-playing",
|
|
782
785
|
summaryToggles: "dsh-speech-summary-toggles",
|
|
786
|
+
summaryCache: "dsh-speech-summary-cache",
|
|
783
787
|
voicePicker: "dsh-speech-voice-picker",
|
|
784
788
|
voiceSelected: "dsh-speech-voice-selected",
|
|
785
789
|
voiceMenu: "dsh-speech-voice-menu",
|
|
@@ -801,15 +805,18 @@ const STYLE_TEXT = String.raw`
|
|
|
801
805
|
.dsh-speech-primary,.dsh-speech-secondary,.dsh-speech-checkout{border:1px solid transparent;border-radius:8px;padding:8px 12px;font:inherit;font-size:13px;font-weight:600;cursor:pointer;text-decoration:none;text-align:center}.dsh-speech-primary{background:#e8e8e8;color:#151515}.dsh-speech-secondary,.dsh-speech-checkout{border-color:var(--color-border,#4a4a4a);background:transparent;color:inherit}.dsh-speech-primary:disabled,.dsh-speech-secondary:disabled{cursor:default;opacity:.45}.dsh-speech-good{color:#65c88a}.dsh-speech-warning{margin:0;color:#efb85c}.dsh-speech-danger,.dsh-speech-dock-error{margin:0;color:#f07878}
|
|
802
806
|
.dsh-speech-top-up{justify-content:flex-start;align-items:end;flex-wrap:wrap}.dsh-speech-top-up .dsh-speech-field{max-width:180px}
|
|
803
807
|
.dsh-speech-summary-toggles{display:flex;align-items:center;justify-content:flex-end;gap:18px;flex-wrap:wrap}.dsh-speech-switch-label{position:relative;display:flex;align-items:center;gap:8px;color:var(--color-text-secondary,#aaa);font-size:12px;cursor:pointer;white-space:nowrap}.dsh-speech-switch-label input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.dsh-speech-switch-track{width:32px;height:18px;display:inline-flex;align-items:center;flex:none;box-sizing:border-box;padding:2px;border:1px solid var(--color-border,#555);border-radius:999px;background:var(--color-background,#242424);transition:background .15s ease,border-color .15s ease}.dsh-speech-switch-track i{width:12px;height:12px;display:block;border-radius:50%;background:var(--color-text-secondary,#aaa);transform:translateX(0);transition:transform .15s ease,background .15s ease}.dsh-speech-switch-label input:checked+.dsh-speech-switch-track,.dsh-speech-autoplay-toggle[aria-checked="true"] .dsh-speech-switch-track{border-color:var(--dsw-alias-brand-primary,#68a8ff);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#68a8ff) 32%,transparent)}.dsh-speech-switch-label input:checked+.dsh-speech-switch-track i,.dsh-speech-autoplay-toggle[aria-checked="true"] .dsh-speech-switch-track i{background:#fff;transform:translateX(14px)}.dsh-speech-switch-label:focus-within .dsh-speech-switch-track{outline:2px solid color-mix(in srgb,#68a8ff 60%,transparent);outline-offset:2px}.dsh-speech-switch-label:has(input:disabled){cursor:default;opacity:.5}
|
|
808
|
+
.dsh-speech-summary-cache{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-top:12px;border-top:1px solid var(--color-border,#343434)}.dsh-speech-summary-cache>div{display:grid;gap:4px}.dsh-speech-summary-cache>div>strong{font-size:13px}.dsh-speech-summary-cache>.dsh-speech-secondary{flex:none}
|
|
804
809
|
.dsh-speech-summary-player{width:min(397px,100%);height:34px;display:flex;align-items:center;gap:9px;margin-left:-2px;color:var(--color-text-secondary,#9b9b9b)}.dsh-speech-summary-control{width:194px;height:34px;display:flex;align-items:center;gap:5px;box-sizing:border-box;padding:4px 12px 4px 7px;border:1px solid var(--color-border,#343434);border-radius:999px;background:var(--color-background-secondary,rgba(255,255,255,.025))}.dsh-speech-summary-button{width:24px;height:24px;display:inline-grid;place-items:center;flex:none;padding:0;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer}.dsh-speech-summary-button:hover:not(:disabled),.dsh-speech-summary-button:focus-visible{outline:0;background:var(--color-background-hover,rgba(255,255,255,.08));color:var(--color-text,#eee)}.dsh-speech-summary-button:disabled{cursor:wait;opacity:.6}.dsh-speech-summary-button svg{width:15px;height:15px;fill:currentColor}.dsh-speech-summary-waveform{width:144px;height:22px;display:flex;align-items:center;gap:1px;flex:none;overflow:hidden}.dsh-speech-summary-waveform i{width:2px;height:calc(3px + var(--dsh-speech-peak)*16px);display:block;flex:none;border-radius:2px;background:currentColor;opacity:.28;transition:height 45ms linear,opacity .12s ease,color .12s ease}.dsh-speech-summary-waveform[data-playing="true"] i{color:var(--dsw-alias-brand-primary,#68a8ff);opacity:.82}.dsh-speech-summary-waveform[data-preparing="true"] i{animation:dsh-speech-wave-pulse 1s ease-in-out infinite}.dsh-speech-summary-waveform[data-preparing="true"] i:nth-child(4n+2){animation-delay:.12s}.dsh-speech-summary-waveform[data-preparing="true"] i:nth-child(4n+3){animation-delay:.24s}.dsh-speech-summary-waveform[data-preparing="true"] i:nth-child(4n){animation-delay:.36s}.dsh-speech-summary-label{min-width:0;max-width:190px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.dsh-speech-autoplay-toggle{height:28px;display:inline-flex;align-items:center;gap:7px;padding:0 8px;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--color-text-secondary,#aaa);font:inherit;font-size:11px;cursor:pointer}.dsh-speech-autoplay-toggle .dsh-speech-switch-track{width:28px;height:16px}.dsh-speech-autoplay-toggle .dsh-speech-switch-track i{width:10px;height:10px}.dsh-speech-autoplay-toggle[aria-checked="true"] .dsh-speech-switch-track i{transform:translateX(12px)}.dsh-speech-autoplay-toggle:hover:not(:disabled),.dsh-speech-autoplay-toggle:focus-visible{outline:0;border-color:var(--color-border,#444);background:var(--color-background-hover,rgba(255,255,255,.06));color:var(--color-text,#eee)}.dsh-speech-autoplay-toggle:disabled{opacity:.45;cursor:default}@keyframes dsh-speech-wave-pulse{0%,100%{opacity:.2;transform:scaleY(.55)}50%{opacity:.78;transform:scaleY(1)}}
|
|
805
810
|
.dsh-speech-mic-wrap{position:relative;display:inline-flex;align-items:center;gap:5px;order:1}[data-slot="conversation.input.right"]:has(>.dsh-speech-mic-wrap)~button:last-child{order:2}.dsh-speech-mic{width:30px;height:30px;display:inline-grid;place-items:center;padding:0;border:0;border-radius:7px;background:transparent;color:var(--color-text-secondary,#a8a8a8);cursor:pointer}.dsh-speech-mic:hover:not(:disabled),.dsh-speech-mic:focus-visible{background:var(--color-background-hover,rgba(255,255,255,.08));color:var(--color-text,#eee)}.dsh-speech-mic[aria-disabled="true"]{opacity:.48}.dsh-speech-mic:disabled{cursor:default}.dsh-speech-mic svg{width:18px;height:18px;fill:currentColor}.dsh-speech-mic-active{color:#ef6666;background:rgba(239,102,102,.12)}.dsh-speech-mic-tooltip{position:absolute;right:0;bottom:calc(100% + 8px);z-index:40;width:max-content;max-width:260px;padding:6px 9px;border:1px solid var(--color-border,#343434);border-radius:7px;background:var(--color-background-elevated,#202020);color:var(--color-text,#eee);font-size:12px;line-height:1.35;box-shadow:0 6px 18px rgba(0,0,0,.24);opacity:0;visibility:hidden;transform:translateY(2px);pointer-events:none;transition:opacity .12s ease,transform .12s ease,visibility .12s}.dsh-speech-mic-wrap:hover .dsh-speech-mic-tooltip,.dsh-speech-mic-wrap:focus-within .dsh-speech-mic-tooltip,.dsh-speech-mic-wrap[data-explanation-open="true"] .dsh-speech-mic-tooltip{opacity:1;visibility:visible;transform:translateY(0)}
|
|
806
811
|
:where(div):has(>:where(div)>[data-slot="conversation.input.right"]>.dsh-speech-recording-takeover){position:relative}:where(div):has(>:where(div)>[data-slot="conversation.input.right"]>.dsh-speech-recording-takeover)>*{visibility:hidden}.dsh-speech-recording-takeover{position:absolute;inset:0;z-index:8;visibility:visible;display:flex;align-items:center;gap:10px;padding:4px 8px;box-sizing:border-box;color:var(--color-text-secondary,#a8a8a8)}.dsh-speech-recording-track{height:30px;min-width:0;flex:1;display:flex;align-items:center}.dsh-speech-recording-canvas{display:block;width:100%;height:30px;color:var(--color-text-secondary,#a8a8a8)}.dsh-speech-recording-cancel,.dsh-speech-recording-stop{flex:none;border-radius:999px}.dsh-speech-recording-cancel{color:var(--color-text-secondary,#a8a8a8);background:transparent}.dsh-speech-recording-cancel:hover:not(:disabled),.dsh-speech-recording-cancel:focus-visible{background:var(--color-background-hover,rgba(255,255,255,.08));color:var(--color-text,#eee)}.dsh-speech-recording-actions{display:flex;align-items:center;gap:6px;flex:none}.dsh-speech-recording-stop{color:#ef6666;background:rgba(239,102,102,.12)}.dsh-speech-recording-stop:hover:not(:disabled),.dsh-speech-recording-stop:focus-visible{color:#ff7777;background:rgba(239,102,102,.2)}.dsh-speech-recording-send{width:34px;height:34px;display:inline-grid;place-items:center;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-button-info-fill,#679efe);color:#fff;cursor:pointer}.dsh-speech-recording-send:hover:not(:disabled),.dsh-speech-recording-send:focus-visible{outline:0;background:var(--dsw-alias-button-info-hover,#4176e6)}.dsh-speech-recording-send:disabled{cursor:default;opacity:.4}.dsh-speech-recording-send svg{width:16px;height:16px}.dsh-speech-recording-progress{display:flex;align-items:center;justify-content:flex-end;gap:8px;width:100%;color:var(--color-text-secondary,#a8a8a8);font-size:12px}.dsh-speech-recording-takeover[data-phase="starting"] .dsh-speech-recording-progress{justify-content:center}.dsh-speech-recording-progress i{width:13px;height:13px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:dsh-speech-spin .8s linear infinite}.dsh-speech-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@keyframes dsh-speech-spin{to{transform:rotate(360deg)}}
|
|
807
812
|
.dsh-speech-device-marker{display:none}[data-slot="conversation.composer.dock"]>:has(>.dsh-speech-device-dock){overflow:visible}.dsh-speech-device-fallback{min-height:22px;display:flex;align-items:center;justify-content:flex-start;padding:0 8px;overflow:visible}.dsh-speech-device-fallback .dsh-speech-device-dock{margin-left:0}.dsh-speech-device-separator{margin-left:4px;color:var(--dsw-alias-label-tertiary,var(--color-text-secondary,#888))}.dsh-speech-device-dock{position:relative;display:inline-flex;vertical-align:middle;margin-left:5px;color:var(--dsw-alias-label-secondary,var(--color-text-secondary,#a8a8a8));font:inherit}.dsh-speech-device-trigger{max-width:126px;height:20px;display:inline-flex;align-items:center;gap:4px;padding:0 5px;border:0;border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:11px;line-height:20px;cursor:pointer}.dsh-speech-device-trigger:hover,.dsh-speech-device-trigger:focus-visible,.dsh-speech-device-trigger[aria-expanded="true"]{outline:0;background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));color:var(--dsw-alias-label-primary,var(--color-text,#eee))}.dsh-speech-device-trigger:disabled{cursor:wait;opacity:.6}.dsh-speech-device-dock[data-error="true"] .dsh-speech-device-trigger{color:var(--dsw-alias-state-error-primary,#f07878)}.dsh-speech-device-icon,.dsh-speech-device-chevron{display:inline-flex;flex:none}.dsh-speech-device-icon svg{width:12px;height:12px}.dsh-speech-device-chevron svg{width:10px;height:10px}.dsh-speech-device-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-speech-device-menu{position:absolute;right:0;bottom:calc(100% + 6px);z-index:80;min-width:240px;max-width:min(320px,calc(100vw - 32px));display:grid;box-sizing:border-box;padding:4px;border:1px solid var(--dsw-alias-border-l2-darkmode-thin,rgba(255,255,255,.06));border-radius:12px;background:var(--dsw-alias-bg-layer-3,#353638);color:var(--dsw-alias-label-primary,#f9fafb);box-shadow:0 0 1px rgba(0,0,0,.2),0 0 4px rgba(0,0,0,.02),0 12px 32px rgba(0,0,0,.08);font:13px/20px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.dsh-speech-device-menu-item{width:100%;min-width:0;height:38px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 10px;border:0;border-radius:10px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsh-speech-device-menu-item:hover,.dsh-speech-device-menu-item:focus-visible{outline:0;background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08))}.dsh-speech-device-menu-item>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-speech-device-check{width:14px;height:14px;display:inline-flex;flex:none;color:var(--dsw-alias-brand-primary,var(--dsw-alias-label-primary,#f9fafb))}.dsh-speech-device-check svg{width:14px;height:14px}.dsh-speech-device-dock[data-variant="hero"]{margin-left:0;color:var(--dsw-alias-label-primary,var(--color-text,#f9fafb));font:500 13px/20px -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif}.dsh-speech-device-dock[data-variant="hero"] .dsh-speech-device-trigger{max-width:220px;height:28px;padding:0 8px;gap:4px;border-radius:16px;font:inherit;line-height:20px}.dsh-speech-device-dock[data-variant="hero"] .dsh-speech-device-icon svg{width:16px;height:16px}.dsh-speech-device-dock[data-variant="hero"] .dsh-speech-device-chevron svg{width:14px;height:14px}.dsh-speech-device-dock[data-variant="hero"] .dsh-speech-device-menu{left:0;right:auto;top:calc(100% + 6px);bottom:auto;min-width:218px}
|
|
808
813
|
[data-dsh-speech-nav]>svg{display:none}[data-dsh-speech-nav]::before{content:"";width:16px;height:16px;flex:none;background:currentColor;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='7' y='2' width='6' height='11' rx='3' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v3M7 18h6' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E") center/contain no-repeat}
|
|
814
|
+
.dsh-speech-sidebar-playing{width:16px;height:20px;display:inline-flex;align-items:center;justify-content:center;flex:none;margin-left:1px;color:var(--dsw-alias-state-business-primary,#679efe);pointer-events:none}.dsh-speech-sidebar-playing svg{width:13px;height:13px;fill:currentColor}[data-dsh-speech-playing-session]>.dsh-speech-sidebar-playing{animation:dsh-speech-sidebar-playing-in .16s ease-out}@keyframes dsh-speech-sidebar-playing-in{from{opacity:0;transform:scale(.72)}to{opacity:1;transform:scale(1)}}
|
|
809
815
|
.dsh-speech-dock,.dsh-speech-dock-error{min-height:22px;display:flex;align-items:center;gap:9px;padding:4px 8px 0;font-size:12px}.dsh-speech-dock-detail{color:var(--color-text-secondary,#8d8d8d)}
|
|
810
816
|
.dsh-speech-summary-player{width:100%}.dsh-speech-summary-player>.dsh-speech-autoplay-toggle{margin-left:auto}
|
|
817
|
+
:where(div):has(>.dsh-speech-summary-player){height:auto;flex-wrap:wrap}:where(div):has(>.dsh-speech-summary-player)>.dsh-speech-summary-player{order:-1;flex:0 0 100%;margin-bottom:4px}
|
|
811
818
|
@media(max-width:680px){.dsh-speech-columns,.dsh-speech-grid,.dsh-speech-balance-grid{grid-template-columns:1fr}.dsh-speech-header{align-items:flex-start}.dsh-speech-device-separator,.dsh-speech-device-dock{display:none}}
|
|
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}}
|
|
819
|
+
@media(prefers-reduced-motion:reduce){.dsh-speech-mic-tooltip{transition:none}.dsh-speech-recording-progress i{animation:none;border-right-color:currentColor}.dsh-speech-summary-waveform i{transition:none}.dsh-speech-summary-waveform[data-preparing="true"] i,.dsh-speech-sidebar-playing{animation:none}}
|
|
813
820
|
`;
|
|
814
821
|
|
|
815
822
|
//#endregion
|
|
@@ -1616,7 +1623,7 @@ function ModelPicker({ label, placeholder, emptyLabel, options, value, disabled,
|
|
|
1616
1623
|
]
|
|
1617
1624
|
});
|
|
1618
1625
|
}
|
|
1619
|
-
function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
1626
|
+
function SpeechSettings({ controller, scope, getLocale, summaryCache, t }) {
|
|
1620
1627
|
const client = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
1621
1628
|
const settings = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
1622
1629
|
const value = settings.value;
|
|
@@ -2113,7 +2120,25 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
2113
2120
|
ttsCatalog.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2114
2121
|
className: styles.muted,
|
|
2115
2122
|
children: t("ttsUnavailable")
|
|
2116
|
-
}) : null
|
|
2123
|
+
}) : null,
|
|
2124
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2125
|
+
className: styles.summaryCache,
|
|
2126
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("summaryCache") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2127
|
+
className: styles.hint,
|
|
2128
|
+
children: t("summaryCacheHint")
|
|
2129
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2130
|
+
className: styles.secondary,
|
|
2131
|
+
type: "button",
|
|
2132
|
+
disabled: busy !== null,
|
|
2133
|
+
onClick: () => {
|
|
2134
|
+
run("clear-summary-cache", async () => {
|
|
2135
|
+
await summaryCache.clear();
|
|
2136
|
+
setMessage(t("summaryCacheCleared"));
|
|
2137
|
+
});
|
|
2138
|
+
},
|
|
2139
|
+
children: busy === "clear-summary-cache" ? t("loading") : t("clearSummaryCache")
|
|
2140
|
+
})]
|
|
2141
|
+
})
|
|
2117
2142
|
]
|
|
2118
2143
|
}),
|
|
2119
2144
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
@@ -2187,6 +2212,205 @@ function SpeechSettings({ controller, scope, getLocale, t }) {
|
|
|
2187
2212
|
});
|
|
2188
2213
|
}
|
|
2189
2214
|
|
|
2215
|
+
//#endregion
|
|
2216
|
+
//#region src/client/summary-cache.ts
|
|
2217
|
+
const SUMMARY_CACHE_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
2218
|
+
const SUMMARY_CACHE_MAX_ENTRIES = 500;
|
|
2219
|
+
const DATABASE_NAME = "dsh-speech-summary-cache";
|
|
2220
|
+
const DATABASE_VERSION = 1;
|
|
2221
|
+
const STORE_NAME = "summaries";
|
|
2222
|
+
var IndexedDbSummaryCacheStore = class {
|
|
2223
|
+
database;
|
|
2224
|
+
async get(key) {
|
|
2225
|
+
const database = await this.open();
|
|
2226
|
+
if (database === void 0) return void 0;
|
|
2227
|
+
return await new Promise((resolve) => {
|
|
2228
|
+
const request$1 = database.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(key);
|
|
2229
|
+
request$1.onsuccess = () => {
|
|
2230
|
+
resolve(request$1.result);
|
|
2231
|
+
};
|
|
2232
|
+
request$1.onerror = () => {
|
|
2233
|
+
resolve(void 0);
|
|
2234
|
+
};
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
async put(record) {
|
|
2238
|
+
const database = await this.open();
|
|
2239
|
+
if (database === void 0) return;
|
|
2240
|
+
await new Promise((resolve) => {
|
|
2241
|
+
const transaction = database.transaction(STORE_NAME, "readwrite");
|
|
2242
|
+
transaction.objectStore(STORE_NAME).put(record);
|
|
2243
|
+
transaction.oncomplete = () => {
|
|
2244
|
+
resolve();
|
|
2245
|
+
};
|
|
2246
|
+
transaction.onerror = () => {
|
|
2247
|
+
resolve();
|
|
2248
|
+
};
|
|
2249
|
+
transaction.onabort = () => {
|
|
2250
|
+
resolve();
|
|
2251
|
+
};
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
async delete(key) {
|
|
2255
|
+
const database = await this.open();
|
|
2256
|
+
if (database === void 0) return;
|
|
2257
|
+
await new Promise((resolve) => {
|
|
2258
|
+
const transaction = database.transaction(STORE_NAME, "readwrite");
|
|
2259
|
+
transaction.objectStore(STORE_NAME).delete(key);
|
|
2260
|
+
transaction.oncomplete = () => {
|
|
2261
|
+
resolve();
|
|
2262
|
+
};
|
|
2263
|
+
transaction.onerror = () => {
|
|
2264
|
+
resolve();
|
|
2265
|
+
};
|
|
2266
|
+
transaction.onabort = () => {
|
|
2267
|
+
resolve();
|
|
2268
|
+
};
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
async all() {
|
|
2272
|
+
const database = await this.open();
|
|
2273
|
+
if (database === void 0) return [];
|
|
2274
|
+
return await new Promise((resolve) => {
|
|
2275
|
+
const request$1 = database.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).getAll();
|
|
2276
|
+
request$1.onsuccess = () => {
|
|
2277
|
+
resolve(request$1.result);
|
|
2278
|
+
};
|
|
2279
|
+
request$1.onerror = () => {
|
|
2280
|
+
resolve([]);
|
|
2281
|
+
};
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
async clear() {
|
|
2285
|
+
const database = await this.open();
|
|
2286
|
+
if (database === void 0) return;
|
|
2287
|
+
await new Promise((resolve) => {
|
|
2288
|
+
const transaction = database.transaction(STORE_NAME, "readwrite");
|
|
2289
|
+
transaction.objectStore(STORE_NAME).clear();
|
|
2290
|
+
transaction.oncomplete = () => {
|
|
2291
|
+
resolve();
|
|
2292
|
+
};
|
|
2293
|
+
transaction.onerror = () => {
|
|
2294
|
+
resolve();
|
|
2295
|
+
};
|
|
2296
|
+
transaction.onabort = () => {
|
|
2297
|
+
resolve();
|
|
2298
|
+
};
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
close() {
|
|
2302
|
+
this.database?.then((database) => {
|
|
2303
|
+
database?.close();
|
|
2304
|
+
});
|
|
2305
|
+
this.database = void 0;
|
|
2306
|
+
}
|
|
2307
|
+
open() {
|
|
2308
|
+
if (this.database !== void 0) return this.database;
|
|
2309
|
+
if (typeof indexedDB === "undefined") return Promise.resolve(void 0);
|
|
2310
|
+
this.database = new Promise((resolve) => {
|
|
2311
|
+
let settled = false;
|
|
2312
|
+
const finish = (database) => {
|
|
2313
|
+
if (settled) {
|
|
2314
|
+
database?.close();
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
settled = true;
|
|
2318
|
+
resolve(database);
|
|
2319
|
+
};
|
|
2320
|
+
const request$1 = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
|
|
2321
|
+
request$1.onupgradeneeded = () => {
|
|
2322
|
+
if (!request$1.result.objectStoreNames.contains(STORE_NAME)) request$1.result.createObjectStore(STORE_NAME, { keyPath: "key" });
|
|
2323
|
+
};
|
|
2324
|
+
request$1.onsuccess = () => {
|
|
2325
|
+
finish(request$1.result);
|
|
2326
|
+
};
|
|
2327
|
+
request$1.onerror = () => {
|
|
2328
|
+
finish(void 0);
|
|
2329
|
+
};
|
|
2330
|
+
request$1.onblocked = () => {
|
|
2331
|
+
finish(void 0);
|
|
2332
|
+
};
|
|
2333
|
+
});
|
|
2334
|
+
return this.database;
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
async function summaryCacheKey(input) {
|
|
2338
|
+
if (globalThis.crypto?.subtle === void 0) return void 0;
|
|
2339
|
+
const canonical = JSON.stringify({
|
|
2340
|
+
version: SUMMARY_PROMPT_VERSION,
|
|
2341
|
+
request: input.request,
|
|
2342
|
+
answer: input.answer,
|
|
2343
|
+
locale: input.locale,
|
|
2344
|
+
route: {
|
|
2345
|
+
provider: input.route.provider,
|
|
2346
|
+
model: input.route.model,
|
|
2347
|
+
reasoningEffort: input.route.reasoningEffort ?? null
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical));
|
|
2351
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2352
|
+
}
|
|
2353
|
+
/** Browser-local, fail-open cache. It never stores session or message identity. */
|
|
2354
|
+
var SummaryCache = class {
|
|
2355
|
+
constructor(store = new IndexedDbSummaryCacheStore(), now = Date.now, ttlMs = SUMMARY_CACHE_TTL_MS, maximumEntries = SUMMARY_CACHE_MAX_ENTRIES) {
|
|
2356
|
+
this.store = store;
|
|
2357
|
+
this.now = now;
|
|
2358
|
+
this.ttlMs = ttlMs;
|
|
2359
|
+
this.maximumEntries = maximumEntries;
|
|
2360
|
+
}
|
|
2361
|
+
async get(input) {
|
|
2362
|
+
try {
|
|
2363
|
+
const key = await summaryCacheKey(input);
|
|
2364
|
+
if (key === void 0) return void 0;
|
|
2365
|
+
const record = await this.store.get(key);
|
|
2366
|
+
if (record === void 0) return void 0;
|
|
2367
|
+
const now = this.now();
|
|
2368
|
+
if (record.expiresAt <= now || record.summary.length === 0 || record.summary.length > MAX_TTS_CHARACTERS) {
|
|
2369
|
+
await this.store.delete(key);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
await this.store.put({
|
|
2373
|
+
...record,
|
|
2374
|
+
accessedAt: now
|
|
2375
|
+
});
|
|
2376
|
+
return record.summary;
|
|
2377
|
+
} catch {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
async set(input, summary) {
|
|
2382
|
+
if (summary.length === 0 || summary.length > MAX_TTS_CHARACTERS) return;
|
|
2383
|
+
try {
|
|
2384
|
+
const key = await summaryCacheKey(input);
|
|
2385
|
+
if (key === void 0) return;
|
|
2386
|
+
const now = this.now();
|
|
2387
|
+
await this.store.put({
|
|
2388
|
+
key,
|
|
2389
|
+
summary,
|
|
2390
|
+
expiresAt: now + this.ttlMs,
|
|
2391
|
+
accessedAt: now
|
|
2392
|
+
});
|
|
2393
|
+
await this.prune(now);
|
|
2394
|
+
} catch {}
|
|
2395
|
+
}
|
|
2396
|
+
async clear() {
|
|
2397
|
+
try {
|
|
2398
|
+
await this.store.clear();
|
|
2399
|
+
} catch {}
|
|
2400
|
+
}
|
|
2401
|
+
dispose() {
|
|
2402
|
+
this.store.close();
|
|
2403
|
+
}
|
|
2404
|
+
async prune(now) {
|
|
2405
|
+
const records = await this.store.all();
|
|
2406
|
+
const live = [];
|
|
2407
|
+
for (const record of records) if (record.expiresAt <= now) await this.store.delete(record.key);
|
|
2408
|
+
else live.push(record);
|
|
2409
|
+
live.sort((a, b) => b.accessedAt - a.accessedAt);
|
|
2410
|
+
for (const record of live.slice(this.maximumEntries)) await this.store.delete(record.key);
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2190
2414
|
//#endregion
|
|
2191
2415
|
//#region src/client/spoken-controller.ts
|
|
2192
2416
|
const EMPTY_PEAKS = Object.freeze(Array.from({ length: 48 }, (_, index) => .15 + .08 * Math.sin(index * .73) ** 2));
|
|
@@ -2206,10 +2430,12 @@ function boundSource(text, maximum) {
|
|
|
2206
2430
|
return `${text.slice(0, beginning)}${marker}${text.slice(text.length - (remaining - beginning))}`;
|
|
2207
2431
|
}
|
|
2208
2432
|
/** Derive only the closing finalized assistant message for each completed turn. */
|
|
2209
|
-
function spokenSources(nodes, locale, requests = []) {
|
|
2433
|
+
function spokenSources(nodes, locale, turnEnds, requests = []) {
|
|
2210
2434
|
const closings = /* @__PURE__ */ new Map();
|
|
2211
2435
|
for (const node of nodes) {
|
|
2212
2436
|
if (node.kind !== "assistant" || node.messageId === void 0 || node.interrupted === true) continue;
|
|
2437
|
+
if (!turnEnds.has(node.turn)) continue;
|
|
2438
|
+
if (node.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n").trim().length === 0) continue;
|
|
2213
2439
|
const current = closings.get(node.turn);
|
|
2214
2440
|
if (current === void 0 || node.seq > current.seq) closings.set(node.turn, node);
|
|
2215
2441
|
}
|
|
@@ -2235,6 +2461,7 @@ function spokenSources(nodes, locale, requests = []) {
|
|
|
2235
2461
|
};
|
|
2236
2462
|
result.push({
|
|
2237
2463
|
messageId: String(assistant.messageId),
|
|
2464
|
+
seq: assistant.seq,
|
|
2238
2465
|
request: boundSource(request$1, MAX_SUMMARY_REQUEST_CHARACTERS),
|
|
2239
2466
|
answer: boundSource(answer, MAX_SUMMARY_ANSWER_CHARACTERS),
|
|
2240
2467
|
locale,
|
|
@@ -2275,9 +2502,13 @@ var SpokenSummaryController = class {
|
|
|
2275
2502
|
listeners = /* @__PURE__ */ new Set();
|
|
2276
2503
|
states = /* @__PURE__ */ new Map();
|
|
2277
2504
|
observed = /* @__PURE__ */ new Map();
|
|
2505
|
+
messageSessions = /* @__PURE__ */ new Map();
|
|
2278
2506
|
observedInteractions = /* @__PURE__ */ new Set();
|
|
2507
|
+
observedInteractionSessions = /* @__PURE__ */ new Set();
|
|
2508
|
+
autoplayConsumed = /* @__PURE__ */ new Set();
|
|
2279
2509
|
voiceCache = /* @__PURE__ */ new Map();
|
|
2280
2510
|
activeMessageId;
|
|
2511
|
+
activeSessionKey;
|
|
2281
2512
|
loadedMessageId;
|
|
2282
2513
|
playbackGeneration = 0;
|
|
2283
2514
|
snapshot = { messages: /* @__PURE__ */ new Map() };
|
|
@@ -2290,7 +2521,8 @@ var SpokenSummaryController = class {
|
|
|
2290
2521
|
analysisFrame;
|
|
2291
2522
|
analysisTick = 0;
|
|
2292
2523
|
pauseResolutionTimer;
|
|
2293
|
-
constructor(audioFactory = browserAudio) {
|
|
2524
|
+
constructor(audioFactory = browserAudio, summaryCache = new SummaryCache()) {
|
|
2525
|
+
this.summaryCache = summaryCache;
|
|
2294
2526
|
this.audio = audioFactory();
|
|
2295
2527
|
}
|
|
2296
2528
|
subscribe = (listener) => {
|
|
@@ -2345,30 +2577,45 @@ var SpokenSummaryController = class {
|
|
|
2345
2577
|
if (this.disposed) return;
|
|
2346
2578
|
this.setEnabled(settings.ttsEnabled);
|
|
2347
2579
|
const ids = new Set(sources.map((source) => source.messageId));
|
|
2580
|
+
const maxSeq = sources.reduce((maximum, source) => Math.max(maximum, source.seq), -1);
|
|
2581
|
+
for (const id of ids) this.messageSessions.set(id, sessionKey);
|
|
2348
2582
|
const previous = this.observed.get(sessionKey);
|
|
2349
2583
|
if (previous === void 0) {
|
|
2350
|
-
this.observed.set(sessionKey,
|
|
2584
|
+
this.observed.set(sessionKey, {
|
|
2585
|
+
ids,
|
|
2586
|
+
maxSeq
|
|
2587
|
+
});
|
|
2351
2588
|
for (const id of ids) this.ensure(id);
|
|
2352
2589
|
this.publish();
|
|
2353
2590
|
return;
|
|
2354
2591
|
}
|
|
2355
|
-
|
|
2592
|
+
const previousMaxSeq = previous.maxSeq;
|
|
2593
|
+
let changed = false;
|
|
2356
2594
|
for (const source of sources) {
|
|
2357
|
-
if (previous.has(source.messageId)) continue;
|
|
2358
|
-
previous.add(source.messageId);
|
|
2359
|
-
|
|
2595
|
+
if (previous.ids.has(source.messageId)) continue;
|
|
2596
|
+
previous.ids.add(source.messageId);
|
|
2597
|
+
changed = true;
|
|
2598
|
+
if (settings.ttsEnabled && source.seq > previousMaxSeq) this.prepare(source, settings, settings.autoPlay);
|
|
2360
2599
|
else this.ensure(source.messageId);
|
|
2361
2600
|
}
|
|
2362
|
-
|
|
2601
|
+
previous.maxSeq = Math.max(previous.maxSeq, maxSeq);
|
|
2602
|
+
if (changed && (!settings.ttsEnabled || sources.some((source) => source.seq <= previousMaxSeq))) this.publish();
|
|
2363
2603
|
}
|
|
2364
2604
|
observeInteractions(sessionKey, interactionKeys, locale, settings) {
|
|
2365
2605
|
if (this.disposed) return;
|
|
2366
2606
|
this.setEnabled(settings.ttsEnabled);
|
|
2607
|
+
if (!this.observedInteractionSessions.has(sessionKey)) {
|
|
2608
|
+
this.observedInteractionSessions.add(sessionKey);
|
|
2609
|
+
for (const key of interactionKeys) this.observedInteractions.add(`${sessionKey}\n${key}`);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2367
2612
|
for (const key of interactionKeys) {
|
|
2368
2613
|
const identity = `${sessionKey}\n${key}`;
|
|
2369
2614
|
if (this.observedInteractions.has(identity)) continue;
|
|
2370
2615
|
this.observedInteractions.add(identity);
|
|
2371
|
-
|
|
2616
|
+
const messageId = `interaction:${identity}`;
|
|
2617
|
+
this.messageSessions.set(messageId, sessionKey);
|
|
2618
|
+
if (settings.ttsEnabled && settings.autoPlay) this.prepareCue(messageId, interactionCue(locale), settings);
|
|
2372
2619
|
}
|
|
2373
2620
|
}
|
|
2374
2621
|
async prepare(source, settings, autoPlay = false) {
|
|
@@ -2402,12 +2649,19 @@ var SpokenSummaryController = class {
|
|
|
2402
2649
|
try {
|
|
2403
2650
|
const voice = await this.voiceFor(binding, settings.ttsVoice);
|
|
2404
2651
|
if (voice === void 0) throw new Error("No compatible voice is available for this text-to-speech route.");
|
|
2405
|
-
const
|
|
2652
|
+
const summaryInput = {
|
|
2406
2653
|
request: source.request || "No preceding user prose was available.",
|
|
2407
2654
|
answer: source.answer,
|
|
2408
2655
|
locale: source.locale,
|
|
2409
2656
|
route: source.route
|
|
2410
|
-
}
|
|
2657
|
+
};
|
|
2658
|
+
let summary = await this.summaryCache.get(summaryInput);
|
|
2659
|
+
if (this.disposed || abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2660
|
+
if (summary === void 0) {
|
|
2661
|
+
summary = (await speechApi.summarize(summaryInput, abort.signal)).summary;
|
|
2662
|
+
if (this.disposed || abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2663
|
+
await this.summaryCache.set(summaryInput, summary);
|
|
2664
|
+
}
|
|
2411
2665
|
const prepared = await speechApi.prepareTts({
|
|
2412
2666
|
text: summary,
|
|
2413
2667
|
model: binding.model,
|
|
@@ -2426,7 +2680,7 @@ var SpokenSummaryController = class {
|
|
|
2426
2680
|
error: void 0,
|
|
2427
2681
|
ended: false
|
|
2428
2682
|
});
|
|
2429
|
-
if (autoPlay && this.activeMessageId === void 0) await this.play(source.messageId, false);
|
|
2683
|
+
if (autoPlay && this.activeMessageId === void 0 && !this.autoplayConsumed.has(source.messageId)) await this.play(source.messageId, false);
|
|
2430
2684
|
} catch (error) {
|
|
2431
2685
|
if (abort.signal.aborted || state.requestGeneration !== requestGeneration) return;
|
|
2432
2686
|
state.abort = void 0;
|
|
@@ -2516,6 +2770,10 @@ var SpokenSummaryController = class {
|
|
|
2516
2770
|
duration: Math.max(0, duration)
|
|
2517
2771
|
});
|
|
2518
2772
|
};
|
|
2773
|
+
this.audio.onplaying = () => {
|
|
2774
|
+
if (!this.owns(generation, messageId)) return;
|
|
2775
|
+
this.autoplayConsumed.add(messageId);
|
|
2776
|
+
};
|
|
2519
2777
|
this.patch(messageId, {
|
|
2520
2778
|
phase: "playing",
|
|
2521
2779
|
ended: false
|
|
@@ -2523,6 +2781,9 @@ var SpokenSummaryController = class {
|
|
|
2523
2781
|
try {
|
|
2524
2782
|
await this.audio.play();
|
|
2525
2783
|
if (!this.owns(generation, messageId)) return;
|
|
2784
|
+
this.autoplayConsumed.add(messageId);
|
|
2785
|
+
this.activeSessionKey = this.messageSessions.get(messageId);
|
|
2786
|
+
this.publish();
|
|
2526
2787
|
this.startAnalysis(messageId, generation);
|
|
2527
2788
|
} catch {
|
|
2528
2789
|
if (!this.owns(generation, messageId)) return;
|
|
@@ -2572,7 +2833,10 @@ var SpokenSummaryController = class {
|
|
|
2572
2833
|
}
|
|
2573
2834
|
this.states.clear();
|
|
2574
2835
|
this.observed.clear();
|
|
2836
|
+
this.messageSessions.clear();
|
|
2575
2837
|
this.observedInteractions.clear();
|
|
2838
|
+
this.observedInteractionSessions.clear();
|
|
2839
|
+
this.autoplayConsumed.clear();
|
|
2576
2840
|
this.voiceCache.clear();
|
|
2577
2841
|
this.analyserContext?.close().catch(() => {});
|
|
2578
2842
|
this.analyserContext = void 0;
|
|
@@ -2743,7 +3007,9 @@ var SpokenSummaryController = class {
|
|
|
2743
3007
|
this.stopAnalysis();
|
|
2744
3008
|
this.clearPauseResolution();
|
|
2745
3009
|
this.activeMessageId = void 0;
|
|
3010
|
+
this.activeSessionKey = void 0;
|
|
2746
3011
|
this.audio.onended = null;
|
|
3012
|
+
this.audio.onplaying = null;
|
|
2747
3013
|
this.audio.onerror = null;
|
|
2748
3014
|
this.audio.onpause = null;
|
|
2749
3015
|
this.audio.ontimeupdate = null;
|
|
@@ -2777,7 +3043,8 @@ var SpokenSummaryController = class {
|
|
|
2777
3043
|
...state.error === void 0 ? {} : { error: state.error },
|
|
2778
3044
|
...state.ended === void 0 ? {} : { ended: state.ended }
|
|
2779
3045
|
}])),
|
|
2780
|
-
...this.activeMessageId === void 0 ? {} : { activeMessageId: this.activeMessageId }
|
|
3046
|
+
...this.activeMessageId === void 0 ? {} : { activeMessageId: this.activeMessageId },
|
|
3047
|
+
...this.activeSessionKey === void 0 ? {} : { activeSessionKey: this.activeSessionKey }
|
|
2781
3048
|
};
|
|
2782
3049
|
for (const listener of this.listeners) listener();
|
|
2783
3050
|
}
|
|
@@ -2822,8 +3089,9 @@ function Waveform({ peaks, preparing, playing }) {
|
|
|
2822
3089
|
children: peaks.map((peak, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { "--dsh-speech-peak": String(peak) } }, index))
|
|
2823
3090
|
});
|
|
2824
3091
|
}
|
|
2825
|
-
function SpokenSummaryTail({ controller, spoken, scope, getLocale,
|
|
3092
|
+
function SpokenSummaryTail({ controller, spoken, scope, getLocale, messageId, useSession, t }) {
|
|
2826
3093
|
const nodes = useSession((snapshot) => snapshot.nodes);
|
|
3094
|
+
const turnEnds = useSession((snapshot) => snapshot.turnEnds);
|
|
2827
3095
|
const requests = useSession((snapshot) => {
|
|
2828
3096
|
const views = snapshot.views;
|
|
2829
3097
|
if (views?.get === void 0) return EMPTY_REQUESTS;
|
|
@@ -2833,13 +3101,13 @@ function SpokenSummaryTail({ controller, spoken, scope, getLocale, seq, useSessi
|
|
|
2833
3101
|
const scopeState = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
2834
3102
|
const spokenState = (0, react.useSyncExternalStore)(spoken.subscribe, spoken.getSnapshot);
|
|
2835
3103
|
const locale = getLocale();
|
|
2836
|
-
const sources = (0, react.useMemo)(() => spokenSources(nodes, locale, requests), [
|
|
3104
|
+
const sources = (0, react.useMemo)(() => spokenSources(nodes, locale, turnEnds, requests), [
|
|
2837
3105
|
nodes,
|
|
2838
3106
|
locale,
|
|
2839
|
-
requests
|
|
3107
|
+
requests,
|
|
3108
|
+
turnEnds
|
|
2840
3109
|
]);
|
|
2841
|
-
const
|
|
2842
|
-
const id = closing?.kind === "assistant" && closing.messageId !== void 0 ? String(closing.messageId) : `turn-tail:${String(seq)}`;
|
|
3110
|
+
const id = String(messageId);
|
|
2843
3111
|
const source = sources.find((candidate) => candidate.messageId === id);
|
|
2844
3112
|
const settings = (0, react.useMemo)(() => preparationSettings(controller, scopeState.value, locale), [
|
|
2845
3113
|
controller,
|
|
@@ -2855,7 +3123,7 @@ function SpokenSummaryTail({ controller, spoken, scope, getLocale, seq, useSessi
|
|
|
2855
3123
|
};
|
|
2856
3124
|
const latestAudioMessageId = [...sources].reverse().find((candidate) => spokenState.messages.get(candidate.messageId)?.audioUrl !== void 0)?.messageId;
|
|
2857
3125
|
const latestSourceMessageId = sources.at(-1)?.messageId;
|
|
2858
|
-
const legacyAutoplayWasUsed = scopeState.user
|
|
3126
|
+
const legacyAutoplayWasUsed = scopeState.user?.autoPlay !== void 0;
|
|
2859
3127
|
const autoplayInlineRevealed = scopeState.value?.autoplayInlineRevealed === true || legacyAutoplayWasUsed;
|
|
2860
3128
|
const autoplayHostMessageId = latestAudioMessageId ?? (autoplayInlineRevealed ? latestSourceMessageId : void 0);
|
|
2861
3129
|
(0, react.useEffect)(() => spoken.mount(id), [spoken, id]);
|
|
@@ -2942,11 +3210,13 @@ function SpokenSummaryTail({ controller, spoken, scope, getLocale, seq, useSessi
|
|
|
2942
3210
|
]
|
|
2943
3211
|
});
|
|
2944
3212
|
}
|
|
2945
|
-
function SpokenSessionObserver({ controller, spoken, scope, getLocale, sessionId, useSession }) {
|
|
3213
|
+
function SpokenSessionObserver({ controller, spoken, scope, getLocale, sidebarPlayback, sessionId, useSession }) {
|
|
2946
3214
|
const state = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
2947
3215
|
const client = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
3216
|
+
const openState = useSession((snapshot) => snapshot.openState);
|
|
2948
3217
|
const pending = useSession((snapshot) => snapshot.pending);
|
|
2949
3218
|
const nodes = useSession((snapshot) => snapshot.nodes);
|
|
3219
|
+
const turnEnds = useSession((snapshot) => snapshot.turnEnds);
|
|
2950
3220
|
const requests = useSession((snapshot) => {
|
|
2951
3221
|
const views = snapshot.views;
|
|
2952
3222
|
if (views?.get === void 0) return EMPTY_REQUESTS;
|
|
@@ -2959,32 +3229,38 @@ function SpokenSessionObserver({ controller, spoken, scope, getLocale, sessionId
|
|
|
2959
3229
|
speechLocale,
|
|
2960
3230
|
state.value
|
|
2961
3231
|
]);
|
|
2962
|
-
const sources = (0, react.useMemo)(() => spokenSources(nodes, speechLocale, requests), [
|
|
3232
|
+
const sources = (0, react.useMemo)(() => spokenSources(nodes, speechLocale, turnEnds, requests), [
|
|
2963
3233
|
nodes,
|
|
2964
3234
|
requests,
|
|
2965
|
-
speechLocale
|
|
3235
|
+
speechLocale,
|
|
3236
|
+
turnEnds
|
|
2966
3237
|
]);
|
|
2967
3238
|
(0, react.useEffect)(() => {
|
|
2968
3239
|
controller.ensureMetadata();
|
|
2969
3240
|
}, [controller]);
|
|
3241
|
+
(0, react.useEffect)(() => {
|
|
3242
|
+
sidebarPlayback.observeCurrentSession(String(sessionId));
|
|
3243
|
+
}, [sessionId, sidebarPlayback]);
|
|
2970
3244
|
(0, react.useEffect)(() => {
|
|
2971
3245
|
spoken.setEnabled(settings.ttsEnabled);
|
|
2972
3246
|
}, [settings.ttsEnabled, spoken]);
|
|
2973
3247
|
(0, react.useEffect)(() => {
|
|
2974
|
-
if (client.catalog === void 0) return;
|
|
3248
|
+
if (client.catalog === void 0 || openState !== "open") return;
|
|
2975
3249
|
spoken.observeSession(String(sessionId), sources, settings);
|
|
2976
3250
|
}, [
|
|
2977
3251
|
client.catalog,
|
|
3252
|
+
openState,
|
|
2978
3253
|
sessionId,
|
|
2979
3254
|
settings,
|
|
2980
3255
|
sources,
|
|
2981
3256
|
spoken
|
|
2982
3257
|
]);
|
|
2983
3258
|
(0, react.useEffect)(() => {
|
|
2984
|
-
if (client.catalog === void 0) return;
|
|
3259
|
+
if (client.catalog === void 0 || openState !== "open") return;
|
|
2985
3260
|
spoken.observeInteractions(String(sessionId), pending.map((item) => item.key), speechLocale, settings);
|
|
2986
3261
|
}, [
|
|
2987
3262
|
client.catalog,
|
|
3263
|
+
openState,
|
|
2988
3264
|
pending,
|
|
2989
3265
|
sessionId,
|
|
2990
3266
|
settings,
|
|
@@ -2994,6 +3270,94 @@ function SpokenSessionObserver({ controller, spoken, scope, getLocale, sessionId
|
|
|
2994
3270
|
return null;
|
|
2995
3271
|
}
|
|
2996
3272
|
|
|
3273
|
+
//#endregion
|
|
3274
|
+
//#region src/client/sidebar-playback-indicator.ts
|
|
3275
|
+
const INDICATOR_CLASS = "dsh-speech-sidebar-playing";
|
|
3276
|
+
const ROW_ATTRIBUTE = "data-dsh-speech-playing-session";
|
|
3277
|
+
function selectedSessionRow() {
|
|
3278
|
+
if (typeof document === "undefined") return void 0;
|
|
3279
|
+
return document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]") ?? void 0;
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3282
|
+
* Adds a memory-only playback marker to the Harness session row that owned
|
|
3283
|
+
* the selected conversation when audio began. Harness RC does not expose a
|
|
3284
|
+
* per-session-row slot, so this bridge relies only on its ARIA tree contract.
|
|
3285
|
+
*/
|
|
3286
|
+
var SidebarPlaybackIndicator = class {
|
|
3287
|
+
rows = /* @__PURE__ */ new Map();
|
|
3288
|
+
unsubscribe;
|
|
3289
|
+
observer;
|
|
3290
|
+
currentSessionKey;
|
|
3291
|
+
activeSessionKey;
|
|
3292
|
+
disposed = false;
|
|
3293
|
+
constructor(spoken) {
|
|
3294
|
+
this.spoken = spoken;
|
|
3295
|
+
this.unsubscribe = spoken.subscribe(() => {
|
|
3296
|
+
const activeSessionKey = spoken.getSnapshot().activeSessionKey;
|
|
3297
|
+
if (activeSessionKey === this.activeSessionKey) return;
|
|
3298
|
+
this.activeSessionKey = activeSessionKey;
|
|
3299
|
+
this.refresh();
|
|
3300
|
+
});
|
|
3301
|
+
this.activeSessionKey = spoken.getSnapshot().activeSessionKey;
|
|
3302
|
+
this.observer = typeof MutationObserver === "undefined" || typeof document === "undefined" ? void 0 : new MutationObserver(() => {
|
|
3303
|
+
this.refresh();
|
|
3304
|
+
});
|
|
3305
|
+
this.observer?.observe(document.body, {
|
|
3306
|
+
childList: true,
|
|
3307
|
+
subtree: true,
|
|
3308
|
+
attributes: true,
|
|
3309
|
+
attributeFilter: ["aria-selected"]
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
observeCurrentSession(sessionKey) {
|
|
3313
|
+
if (this.disposed) return;
|
|
3314
|
+
this.currentSessionKey = sessionKey;
|
|
3315
|
+
this.refresh();
|
|
3316
|
+
}
|
|
3317
|
+
dispose() {
|
|
3318
|
+
if (this.disposed) return;
|
|
3319
|
+
this.disposed = true;
|
|
3320
|
+
this.unsubscribe();
|
|
3321
|
+
this.observer?.disconnect();
|
|
3322
|
+
this.removeIndicators();
|
|
3323
|
+
this.rows.clear();
|
|
3324
|
+
}
|
|
3325
|
+
refresh() {
|
|
3326
|
+
if (this.disposed) return;
|
|
3327
|
+
const selected = selectedSessionRow();
|
|
3328
|
+
if (selected !== void 0 && this.currentSessionKey !== void 0) this.rows.set(this.currentSessionKey, selected);
|
|
3329
|
+
if (this.activeSessionKey === void 0) {
|
|
3330
|
+
this.removeIndicators();
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
let row = this.rows.get(this.activeSessionKey);
|
|
3334
|
+
if (row?.isConnected !== true) {
|
|
3335
|
+
this.rows.delete(this.activeSessionKey);
|
|
3336
|
+
row = this.currentSessionKey === this.activeSessionKey ? selected : void 0;
|
|
3337
|
+
if (row !== void 0) this.rows.set(this.activeSessionKey, row);
|
|
3338
|
+
}
|
|
3339
|
+
if (row === void 0) {
|
|
3340
|
+
this.removeIndicators();
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
if (document.querySelector(`.${INDICATOR_CLASS}`)?.parentElement === row) return;
|
|
3344
|
+
this.removeIndicators();
|
|
3345
|
+
row.setAttribute(ROW_ATTRIBUTE, "");
|
|
3346
|
+
const indicator = document.createElement("span");
|
|
3347
|
+
indicator.className = INDICATOR_CLASS;
|
|
3348
|
+
indicator.setAttribute("role", "img");
|
|
3349
|
+
indicator.setAttribute("aria-label", "Playing spoken summary");
|
|
3350
|
+
indicator.setAttribute("title", "Playing spoken summary");
|
|
3351
|
+
indicator.innerHTML = "<svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M5 3.25v9.5L12.5 8 5 3.25Z\"/></svg>";
|
|
3352
|
+
row.append(indicator);
|
|
3353
|
+
}
|
|
3354
|
+
removeIndicators() {
|
|
3355
|
+
if (typeof document === "undefined") return;
|
|
3356
|
+
for (const row of document.querySelectorAll(`[${ROW_ATTRIBUTE}]`)) row.removeAttribute(ROW_ATTRIBUTE);
|
|
3357
|
+
for (const indicator of document.querySelectorAll(`.${INDICATOR_CLASS}`)) indicator.remove();
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
|
|
2997
3361
|
//#endregion
|
|
2998
3362
|
//#region src/client/locales.ts
|
|
2999
3363
|
const en = {
|
|
@@ -3073,6 +3437,10 @@ const en = {
|
|
|
3073
3437
|
modelVoiceSearchPlaceholder: "Type to search voices for this model",
|
|
3074
3438
|
voiceNoResults: "No matching voices",
|
|
3075
3439
|
ttsUnavailable: "No compatible synchronous MP3 text-to-speech route is currently advertised.",
|
|
3440
|
+
summaryCache: "Summary cache",
|
|
3441
|
+
summaryCacheHint: "Summaries are stored only in this browser for up to 30 days. The cache is limited to 500 entries.",
|
|
3442
|
+
clearSummaryCache: "Clear cache",
|
|
3443
|
+
summaryCacheCleared: "Summary cache cleared",
|
|
3076
3444
|
autoplayGlobal: "Autoplay",
|
|
3077
3445
|
autoplayShort: "Spoken summaries",
|
|
3078
3446
|
autoplayInline: "Auto play",
|
|
@@ -3165,6 +3533,10 @@ const zh = {
|
|
|
3165
3533
|
modelVoiceSearchPlaceholder: "输入关键词搜索此模型的声音",
|
|
3166
3534
|
voiceNoResults: "没有匹配的声音",
|
|
3167
3535
|
ttsUnavailable: "目前没有兼容的同步 MP3 文本转语音路由。",
|
|
3536
|
+
summaryCache: "摘要缓存",
|
|
3537
|
+
summaryCacheHint: "摘要仅在此浏览器中保存最多 30 天,缓存上限为 500 条。",
|
|
3538
|
+
clearSummaryCache: "清除缓存",
|
|
3539
|
+
summaryCacheCleared: "摘要缓存已清除",
|
|
3168
3540
|
autoplayGlobal: "自动播放",
|
|
3169
3541
|
autoplayShort: "语音摘要",
|
|
3170
3542
|
autoplayInline: "自动播放",
|
|
@@ -3190,7 +3562,9 @@ const inject = [
|
|
|
3190
3562
|
];
|
|
3191
3563
|
function apply(ctx) {
|
|
3192
3564
|
const controller = new SpeechController();
|
|
3193
|
-
const
|
|
3565
|
+
const summaryCache = new SummaryCache();
|
|
3566
|
+
const spoken = new SpokenSummaryController(void 0, summaryCache);
|
|
3567
|
+
const sidebarPlayback = new SidebarPlaybackIndicator(spoken);
|
|
3194
3568
|
const scope = ctx.settingsScope.bind({ namespace: "dsh-speech" });
|
|
3195
3569
|
const getLocale = () => ctx.locale.getLocale().active;
|
|
3196
3570
|
ctx.effect(() => ctx.locale.register("speech", {
|
|
@@ -3228,8 +3602,10 @@ function apply(ctx) {
|
|
|
3228
3602
|
};
|
|
3229
3603
|
}, "dsh-speech: settings navigation icon");
|
|
3230
3604
|
ctx.effect(() => () => {
|
|
3605
|
+
sidebarPlayback.dispose();
|
|
3231
3606
|
controller.dispose();
|
|
3232
3607
|
spoken.dispose();
|
|
3608
|
+
summaryCache.dispose();
|
|
3233
3609
|
}, "dsh-speech: browser controllers");
|
|
3234
3610
|
controller.ensureMetadata();
|
|
3235
3611
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
@@ -3241,7 +3617,8 @@ function apply(ctx) {
|
|
|
3241
3617
|
inject: () => ({
|
|
3242
3618
|
controller,
|
|
3243
3619
|
scope,
|
|
3244
|
-
getLocale
|
|
3620
|
+
getLocale,
|
|
3621
|
+
summaryCache
|
|
3245
3622
|
})
|
|
3246
3623
|
}, SpeechSettings));
|
|
3247
3624
|
ctx.inject(["conversation"], (scoped) => {
|
|
@@ -3277,19 +3654,17 @@ function apply(ctx) {
|
|
|
3277
3654
|
getLocale
|
|
3278
3655
|
})
|
|
3279
3656
|
}, SpeechInputDock);
|
|
3280
|
-
const
|
|
3281
|
-
name: "conversation.chat.
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
turn: owner.turn.turn,
|
|
3285
|
-
seq: owner.seq
|
|
3286
|
-
}),
|
|
3657
|
+
const spokenAction = scoped.slots.register({
|
|
3658
|
+
name: "conversation.chat.assistant-actions",
|
|
3659
|
+
id: "spoken-summary",
|
|
3660
|
+
order: -100,
|
|
3287
3661
|
locale: "speech",
|
|
3288
3662
|
inject: () => ({
|
|
3289
3663
|
controller,
|
|
3290
3664
|
spoken,
|
|
3291
3665
|
scope,
|
|
3292
|
-
getLocale
|
|
3666
|
+
getLocale,
|
|
3667
|
+
sidebarPlayback
|
|
3293
3668
|
})
|
|
3294
3669
|
}, SpokenSummaryTail);
|
|
3295
3670
|
const spokenObserver = scoped.slots.register({
|
|
@@ -3300,7 +3675,8 @@ function apply(ctx) {
|
|
|
3300
3675
|
controller,
|
|
3301
3676
|
spoken,
|
|
3302
3677
|
scope,
|
|
3303
|
-
getLocale
|
|
3678
|
+
getLocale,
|
|
3679
|
+
sidebarPlayback
|
|
3304
3680
|
})
|
|
3305
3681
|
}, SpokenSessionObserver);
|
|
3306
3682
|
return () => {
|
|
@@ -3308,7 +3684,7 @@ function apply(ctx) {
|
|
|
3308
3684
|
mic();
|
|
3309
3685
|
dock();
|
|
3310
3686
|
inputDock();
|
|
3311
|
-
|
|
3687
|
+
spokenAction();
|
|
3312
3688
|
spokenObserver();
|
|
3313
3689
|
};
|
|
3314
3690
|
});
|