dsh-agent-voice 0.1.5 → 0.1.6
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 +21 -19
- package/lib/index.js +20 -46
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -186,30 +186,32 @@ window.__ModuleLoader__.load({
|
|
|
186
186
|
}, []);
|
|
187
187
|
|
|
188
188
|
function playAnnouncement(p) {
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
})
|
|
189
|
+
// The WAV is fetched one-shot via the action round-trip (base64),
|
|
190
|
+
// then played. Settings never carries the audio bytes.
|
|
191
|
+
const doPlay = (dataUrl) => {
|
|
192
|
+
const audio = new Audio(dataUrl);
|
|
193
|
+
audio.volume = Math.max(0, Math.min(1, Number(p.volume ?? 1)));
|
|
194
|
+
const kindLabel = p.kind === "attention" ? "🔔 Pozor: " : (p.kind === "task" ? "✅ " : "🔊 ");
|
|
195
|
+
setToast(kindLabel + p.text);
|
|
196
|
+
if (toastTimer.current) clearTimeout(toastTimer.current);
|
|
197
|
+
toastTimer.current = setTimeout(() => setToast(null), 6000);
|
|
198
|
+
audio.play().catch(() => {
|
|
199
|
+
// Autoplay still blocked: keep the toast and retry on next gesture.
|
|
200
|
+
pendingRef.current = p;
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
if (p.dataUrl) { doPlay(p.dataUrl); return; }
|
|
204
|
+
// One-shot poll for the audio bytes by announcement id.
|
|
205
|
+
action({ action: "poll", id: p.id }).then((r) => {
|
|
206
|
+
if (r && r.ok && r.result && r.result.found && r.result.dataUrl) doPlay(r.result.dataUrl);
|
|
207
|
+
}).catch(() => {});
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
React.useEffect(() => {
|
|
209
211
|
if (!statusJson) return;
|
|
210
212
|
let p = null;
|
|
211
213
|
try { p = JSON.parse(statusJson); } catch { return; }
|
|
212
|
-
if (!p ||
|
|
214
|
+
if (!p || !p.id) return;
|
|
213
215
|
if (p.id === lastPlayed) return;
|
|
214
216
|
setLastPlayed(p.id);
|
|
215
217
|
playAnnouncement(p);
|
|
@@ -237,7 +239,7 @@ window.__ModuleLoader__.load({
|
|
|
237
239
|
id: "agent-voice-player",
|
|
238
240
|
order: 100,
|
|
239
241
|
label: () => "🔊 Agent Voice přehrávač",
|
|
240
|
-
inject: () => ({ config, status }),
|
|
242
|
+
inject: () => ({ action, config, status }),
|
|
241
243
|
children: {},
|
|
242
244
|
}, Player));
|
|
243
245
|
|
package/lib/index.js
CHANGED
|
@@ -48,6 +48,10 @@ const DEFAULTS = {
|
|
|
48
48
|
|
|
49
49
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
50
50
|
|
|
51
|
+
// One-shot audio cache keyed by announcement id: the client polls action=poll
|
|
52
|
+
// to fetch the WAV bytes (base64) for a given id, then the entry expires.
|
|
53
|
+
const audioCache = new Map();
|
|
54
|
+
|
|
51
55
|
let workerProcess = null;
|
|
52
56
|
|
|
53
57
|
async function ensureWorker() {
|
|
@@ -97,30 +101,6 @@ async function postWorker(path, body) {
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
function apply(ctx) {
|
|
100
|
-
// Serve generated audio to the client over HTTP (keeps WAVs out of the
|
|
101
|
-
// settings document entirely).
|
|
102
|
-
const ws = ctx.get("webServer");
|
|
103
|
-
if (ws) {
|
|
104
|
-
ws.register({
|
|
105
|
-
kind: "prefix",
|
|
106
|
-
path: "/dsh-agent-voice/audio/",
|
|
107
|
-
handler: async (req, res) => {
|
|
108
|
-
const name = decodeURIComponent(req.url.split("/").pop() || "");
|
|
109
|
-
if (!/^av-\d+-[a-z0-9]+\.wav$/.test(name)) {
|
|
110
|
-
res.writeHead(404).end("not found");
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const data = await readFile(join(AUDIO_DIR, name));
|
|
115
|
-
res.writeHead(200, { "Content-Type": "audio/wav", "Content-Length": data.length });
|
|
116
|
-
res.end(data);
|
|
117
|
-
} catch {
|
|
118
|
-
res.writeHead(404).end("not found");
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
104
|
let source = () => ({ ...DEFAULTS });
|
|
125
105
|
installSettingsSection(ctx, NS, SCHEMA, DEFAULTS, {
|
|
126
106
|
setSource: (getCurrent) => { source = () => ({ ...DEFAULTS, ...(getCurrent() ?? {}) }); },
|
|
@@ -135,24 +115,20 @@ function apply(ctx) {
|
|
|
135
115
|
response: z.string(),
|
|
136
116
|
}), { base: {} });
|
|
137
117
|
|
|
138
|
-
// Audio
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
118
|
+
// Audio bytes travel to the client via the ACTION round-trip (one-shot,
|
|
119
|
+
// never persisted): the client calls action=poll with an announcement id and
|
|
120
|
+
// the host returns the base64 WAV. The STATUS namespace carries only small
|
|
121
|
+
// metadata (id + text), so the shared settings document stays tiny.
|
|
142
122
|
async function synthesize(text, voice, language) {
|
|
143
123
|
await ensureWorker();
|
|
144
124
|
const res = await postWorker("/tts", { text, voice, language });
|
|
145
|
-
await
|
|
146
|
-
const name = `av-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.wav`;
|
|
147
|
-
const file = join(AUDIO_DIR, name);
|
|
148
|
-
await writeFile(file, await readFile(res.audio));
|
|
149
|
-
return file;
|
|
125
|
+
return await readFile(res.audio);
|
|
150
126
|
}
|
|
151
127
|
|
|
152
128
|
async function announce(kind, text) {
|
|
153
129
|
const cfg = source();
|
|
154
130
|
if (!cfg.enabled) return { ok: false, skipped: "disabled" };
|
|
155
|
-
const
|
|
131
|
+
const wav = await synthesize(text, cfg.voice, cfg.language);
|
|
156
132
|
const id = String(Date.now()) + "-" + Math.random().toString(36).slice(2);
|
|
157
133
|
const payload = {
|
|
158
134
|
id,
|
|
@@ -160,24 +136,14 @@ function apply(ctx) {
|
|
|
160
136
|
text,
|
|
161
137
|
voice: cfg.voice,
|
|
162
138
|
volume: cfg.volume,
|
|
163
|
-
audioPath: file, // the client fetches /dsh-agent-voice/audio/<name>
|
|
164
139
|
at: Date.now(),
|
|
165
140
|
};
|
|
166
141
|
await ctx.settings.replace(STATUS_NS, { json: JSON.stringify(payload) });
|
|
167
|
-
|
|
142
|
+
audioCache.set(id, wav);
|
|
143
|
+
setTimeout(() => audioCache.delete(id), 60000).unref?.();
|
|
168
144
|
return { ok: true, kind, text };
|
|
169
145
|
}
|
|
170
146
|
|
|
171
|
-
async function cleanupOldAudio() {
|
|
172
|
-
const { readdir, rm } = await import("node:fs/promises");
|
|
173
|
-
let files = [];
|
|
174
|
-
try { files = (await readdir(AUDIO_DIR)).filter((f) => f.endsWith(".wav")).sort(); } catch { return; }
|
|
175
|
-
while (files.length > 10) {
|
|
176
|
-
const old = files.shift();
|
|
177
|
-
try { await rm(join(AUDIO_DIR, old), { force: true }); } catch {}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
147
|
async function runAction(req) {
|
|
182
148
|
const cfg = source();
|
|
183
149
|
const a = req.action;
|
|
@@ -214,6 +180,14 @@ function apply(ctx) {
|
|
|
214
180
|
const text = req.text || "Ahoj, tady agent. Test hlasu proběhl úspěšně.";
|
|
215
181
|
return announce("test", text);
|
|
216
182
|
}
|
|
183
|
+
if (a === "poll") {
|
|
184
|
+
// One-shot audio fetch: the client asks for the WAV of an announcement
|
|
185
|
+
// id and receives it as base64 (never persisted to settings).
|
|
186
|
+
if (!req.id) throw new Error("poll requires id");
|
|
187
|
+
const wav = audioCache.get(req.id);
|
|
188
|
+
if (!wav) return { id: req.id, found: false };
|
|
189
|
+
return { id: req.id, found: true, dataUrl: "data:audio/wav;base64," + Buffer.from(wav).toString("base64") };
|
|
190
|
+
}
|
|
217
191
|
throw new Error("unknown action: " + a);
|
|
218
192
|
}
|
|
219
193
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-voice",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|