dsh-agent-voice 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -39,7 +39,6 @@ window.__ModuleLoader__.load({
39
39
  function AgentVoice(props) {
40
40
  const [voices, setVoices] = React.useState(FALLBACK_VOICES);
41
41
  const [status, setStatus] = React.useState("");
42
- const [lastPlayed, setLastPlayed] = React.useState(null);
43
42
 
44
43
  const action = React.useMemo(() => useAction(props.action), [props.action]);
45
44
  const cfgSnap = useSnapshot(props.config);
@@ -51,20 +50,21 @@ window.__ModuleLoader__.load({
51
50
  const announceAttention = cfg.announceAttention !== false;
52
51
  const volume = cfg.volume ?? 1.0;
53
52
 
54
- // Live audio playback: watch the status namespace for a new announcement.
53
+ // Live status feed: show the last announcement (playback happens in the
54
+ // always-on overlay player, so it works on every tab).
55
55
  const statusSnap = useSnapshot(props.status);
56
56
  const statusJson = (statusSnap && statusSnap.value && statusSnap.value.json) || null;
57
+ const [lastText, setLastText] = React.useState(null);
58
+ const lastIdRef = React.useRef(null);
57
59
  React.useEffect(() => {
58
60
  if (!statusJson) return;
59
61
  let p = null;
60
62
  try { p = JSON.parse(statusJson); } catch { return; }
61
- if (!p || !p.dataUrl || !p.id) return;
62
- if (p.id === lastPlayed) return;
63
- setLastPlayed(p.id);
64
- const audio = new Audio(p.dataUrl);
65
- audio.volume = Math.max(0, Math.min(1, Number(p.volume ?? 1)));
66
- audio.play().catch(() => {});
67
- }, [statusJson, lastPlayed]);
63
+ if (!p || !p.id) return;
64
+ if (p.id === lastIdRef.current) return;
65
+ lastIdRef.current = p.id;
66
+ setLastText(p.text || "");
67
+ }, [statusJson]);
68
68
 
69
69
  React.useEffect(() => {
70
70
  action({ action: "voices" }).then((r) => {
@@ -123,7 +123,10 @@ window.__ModuleLoader__.load({
123
123
  React.createElement("div", { className: "av-row" },
124
124
  React.createElement("button", { className: "av-btn av-primary", onClick: test }, "▶ Vyzkoušet hlas"),
125
125
  React.createElement("button", { className: "av-btn", onClick: attention }, "🔔 Výzva k pozornosti"),
126
- status ? React.createElement("span", { className: "av-badge" }, status) : null)),
126
+ status ? React.createElement("span", { className: "av-badge" }, status) : null),
127
+ lastText ? React.createElement("div", { className: "av-row" },
128
+ React.createElement("span", { className: "av-tag" }, "Naposledy hlášeno:"),
129
+ React.createElement("span", { className: "av-sub" }, lastText)) : null),
127
130
 
128
131
  React.createElement("p", { className: "av-sub" },
129
132
  "Doporučené použití: agent zavolá dsh_agent_voice action='speak' po dokončení úkolu a action='attention', " +
@@ -139,6 +142,61 @@ window.__ModuleLoader__.load({
139
142
  const action = ctx.settingsScope.bind({ namespace: "agent-voice-action" });
140
143
  const config = ctx.settingsScope.bind({ namespace: "agent-voice" });
141
144
  const status = ctx.settingsScope.bind({ namespace: "agent-voice-status" });
145
+
146
+ // Global always-on audio player: plays announcements on EVERY tab, not
147
+ // only when the Agent Voice tab is active. Lives in the root-scoped
148
+ // overlay seat, so it stays mounted across view switches.
149
+ function Player(props) {
150
+ const statusSnap = useSnapshot(props.status);
151
+ const statusJson = (statusSnap && statusSnap.value && statusSnap.value.json) || null;
152
+ const cfgSnap = useSnapshot(props.config);
153
+ const cfg = (cfgSnap && cfgSnap.value) || {};
154
+ const [lastPlayed, setLastPlayed] = React.useState(null);
155
+ const [toast, setToast] = React.useState(null);
156
+ const toastTimer = React.useRef(null);
157
+
158
+ React.useEffect(() => {
159
+ if (!statusJson) return;
160
+ let p = null;
161
+ try { p = JSON.parse(statusJson); } catch { return; }
162
+ if (!p || !p.dataUrl || !p.id) return;
163
+ if (p.id === lastPlayed) return;
164
+ setLastPlayed(p.id);
165
+ const audio = new Audio(p.dataUrl);
166
+ audio.volume = Math.max(0, Math.min(1, Number(p.volume ?? 1)));
167
+ const kindLabel = p.kind === "attention" ? "🔔 Pozor: " : (p.kind === "task" ? "✅ " : "🔊 ");
168
+ setToast(kindLabel + p.text);
169
+ if (toastTimer.current) clearTimeout(toastTimer.current);
170
+ toastTimer.current = setTimeout(() => setToast(null), 6000);
171
+ audio.play().catch(() => {});
172
+ }, [statusJson, lastPlayed]);
173
+
174
+ // Disabled = silent (config read from the same source the host uses).
175
+ if (cfg.enabled === false) return null;
176
+
177
+ return React.createElement("div", {
178
+ style: {
179
+ position: "fixed", bottom: 16, right: 16, zIndex: 9999,
180
+ background: "var(--dsw-alias-bg-overlay)",
181
+ border: "1px solid var(--dsw-alias-border-l2)",
182
+ borderRadius: 12, padding: "10px 14px",
183
+ boxShadow: "0 8px 30px rgba(0,0,0,.35)",
184
+ color: "var(--dsw-alias-label-primary)",
185
+ font: "13px -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif",
186
+ maxWidth: 320, display: toast ? "block" : "none",
187
+ },
188
+ }, toast);
189
+ }
190
+
191
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
192
+ name: "shell.overlay",
193
+ id: "agent-voice-player",
194
+ order: 100,
195
+ label: () => "🔊 Agent Voice přehrávač",
196
+ inject: () => ({ config, status }),
197
+ children: {},
198
+ }, Player));
199
+
142
200
  ctx.slots.inject("conversation.view", () => ctx.slots.register({
143
201
  name: "conversation.view",
144
202
  id: "studio-agent-voice",
package/lib/index.js CHANGED
@@ -111,6 +111,7 @@ function apply(ctx) {
111
111
  }), { base: {} });
112
112
 
113
113
  async function synthesize(text, voice, language) {
114
+ await ensureWorker();
114
115
  const res = await postWorker("/tts", { text, voice, language });
115
116
  const bytes = await readFile(res.audio);
116
117
  return "data:audio/wav;base64," + Buffer.from(bytes).toString("base64");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agent-voice",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "DeepSeek Harness plugin: gives the agent a voice — it reads completed tasks and attention requests aloud (never reasoning or interim reports) via local Piper / XTTS v2, with a settings panel and a curated set of voices.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -1,3 +1,4 @@
1
1
  [2026-08-31 22:08:19] agent-voice worker listening on http://127.0.0.1:7863
2
2
  [2026-08-31 22:08:23] error: 'PiperVoice' object has no attribute 'synthesize_stream_raw'
3
3
  [2026-08-31 22:08:46] agent-voice worker listening on http://127.0.0.1:7863
4
+ [2026-09-01 13:40:42] agent-voice worker listening on http://127.0.0.1:7863