@hyperdreamer/pi-webui 1.12.4 → 1.13.0
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/dist/client/assets/{CodeViewer-DEObG1xd.js → CodeViewer-B1Rl2AYu.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-BVKzRval.js → UnifiedDiffViewer--UWVARrK.js} +1 -1
- package/dist/client/assets/{index-BWZzZZw_.js → index-DXa2f-o6.js} +632 -530
- package/dist/client/index.html +1 -1
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -1
- package/dist/server/app.js +6 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +4 -1
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/tts/hostSpeech.js +7 -0
- package/dist/server/tts/hostSpeech.js.map +1 -0
- package/dist/server/tts/hostSpeechService.js +160 -0
- package/dist/server/tts/hostSpeechService.js.map +1 -0
- package/dist/server/tts/speechDispatcherAdapter.js +446 -0
- package/dist/server/tts/speechDispatcherAdapter.js.map +1 -0
- package/dist/server/tts/ssipProtocol.js +113 -0
- package/dist/server/tts/ssipProtocol.js.map +1 -0
- package/dist/server/tts/ttsRoutes.js +116 -0
- package/dist/server/tts/ttsRoutes.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +30 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/hostSpeech.js +28 -0
- package/dist/shared/hostSpeech.js.map +1 -0
- package/docs/config.md +35 -0
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { isHostSpeechRunId, truncateHostSpeechText } from "../../shared/hostSpeech.js";
|
|
2
|
+
import { HostSpeechUnavailableError } from "./hostSpeech.js";
|
|
3
|
+
const DEFAULT_PREFIX = "/api";
|
|
4
|
+
const SPEAK_BODY_KEYS = new Set(["runId", "text", "voice", "rate"]);
|
|
5
|
+
const UNAVAILABLE_MESSAGE = "Host speech is unavailable.";
|
|
6
|
+
const UNEXPECTED_FAILURE_MESSAGE = "Host speech failed. Try again.";
|
|
7
|
+
export function registerTtsRoutes(app, speech, prefix = DEFAULT_PREFIX) {
|
|
8
|
+
app.get(`${prefix}/tts`, async (_request, reply) => {
|
|
9
|
+
try {
|
|
10
|
+
return await speech.status();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
return mapSpeechError(reply, error);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
app.post(`${prefix}/tts/speak`, async (request, reply) => {
|
|
17
|
+
const input = parseSpeakBody(request.body);
|
|
18
|
+
if (input === undefined)
|
|
19
|
+
return reply.code(400).send({ error: "Invalid speak request" });
|
|
20
|
+
// The speak response stays open until the run reaches a terminal state, so a
|
|
21
|
+
// client disconnect must cancel the matching run instead of leaving it
|
|
22
|
+
// speaking. `reply.raw` emits "close" both for a client disconnect (with
|
|
23
|
+
// `writableEnded` false) and after a normal response write; only the former
|
|
24
|
+
// should stop the run, and only once. The flag lives on an object so the
|
|
25
|
+
// event callback's mutation stays visible to the handler. The listener is
|
|
26
|
+
// installed before the first await so an abort during the status lookup
|
|
27
|
+
// still cancels the run instead of letting it start speaking later.
|
|
28
|
+
const settled = { value: false };
|
|
29
|
+
const onClose = () => {
|
|
30
|
+
if (settled.value || reply.raw.writableEnded)
|
|
31
|
+
return;
|
|
32
|
+
settled.value = true;
|
|
33
|
+
void speech.stop(input.runId).catch(() => undefined);
|
|
34
|
+
};
|
|
35
|
+
reply.raw.on("close", onClose);
|
|
36
|
+
const isSettled = () => settled.value;
|
|
37
|
+
try {
|
|
38
|
+
const status = await speech.status();
|
|
39
|
+
if (isSettled())
|
|
40
|
+
return undefined;
|
|
41
|
+
if (!status.available)
|
|
42
|
+
return await reply.code(503).send({ error: status.reason ?? UNAVAILABLE_MESSAGE });
|
|
43
|
+
if (input.voice !== undefined && !status.voices.some((voice) => voice.name === input.voice)) {
|
|
44
|
+
return await reply.code(400).send({ error: `Unknown speech voice: ${input.voice}` });
|
|
45
|
+
}
|
|
46
|
+
const result = await speech.speak(input);
|
|
47
|
+
if (isSettled())
|
|
48
|
+
return undefined;
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isSettled())
|
|
53
|
+
return undefined;
|
|
54
|
+
return await mapSpeechError(reply, error);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
reply.raw.removeListener("close", onClose);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
app.post(`${prefix}/tts/stop`, async (request, reply) => {
|
|
61
|
+
const runId = parseStopBody(request.body);
|
|
62
|
+
if (runId === undefined)
|
|
63
|
+
return reply.code(400).send({ error: "Invalid stop request" });
|
|
64
|
+
try {
|
|
65
|
+
const result = await speech.stop(runId);
|
|
66
|
+
const response = { runId, stopped: result !== undefined };
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return mapSpeechError(reply, error);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function parseSpeakBody(body) {
|
|
75
|
+
if (!isRecord(body))
|
|
76
|
+
return undefined;
|
|
77
|
+
for (const key of Object.keys(body)) {
|
|
78
|
+
if (!SPEAK_BODY_KEYS.has(key))
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const { runId, text, voice, rate } = body;
|
|
82
|
+
if (typeof runId !== "string" || !isHostSpeechRunId(runId))
|
|
83
|
+
return undefined;
|
|
84
|
+
if (typeof text !== "string")
|
|
85
|
+
return undefined;
|
|
86
|
+
const truncatedText = truncateHostSpeechText(text);
|
|
87
|
+
if (truncatedText === "")
|
|
88
|
+
return undefined;
|
|
89
|
+
if (voice !== undefined && (typeof voice !== "string" || voice === "" || /[\r\n]/u.test(voice)))
|
|
90
|
+
return undefined;
|
|
91
|
+
if (typeof rate !== "number" || !Number.isInteger(rate) || rate < -100 || rate > 100)
|
|
92
|
+
return undefined;
|
|
93
|
+
return { runId, text: truncatedText, ...(voice === undefined ? {} : { voice }), rate };
|
|
94
|
+
}
|
|
95
|
+
function parseStopBody(body) {
|
|
96
|
+
if (!isRecord(body))
|
|
97
|
+
return undefined;
|
|
98
|
+
for (const key of Object.keys(body)) {
|
|
99
|
+
if (key !== "runId")
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const { runId } = body;
|
|
103
|
+
if (typeof runId !== "string" || !isHostSpeechRunId(runId))
|
|
104
|
+
return undefined;
|
|
105
|
+
return runId;
|
|
106
|
+
}
|
|
107
|
+
function isRecord(value) {
|
|
108
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109
|
+
}
|
|
110
|
+
function mapSpeechError(reply, error) {
|
|
111
|
+
if (error instanceof HostSpeechUnavailableError) {
|
|
112
|
+
return reply.code(503).send({ error: error.message });
|
|
113
|
+
}
|
|
114
|
+
return reply.code(500).send({ error: UNEXPECTED_FAILURE_MESSAGE });
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=ttsRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ttsRoutes.js","sourceRoot":"","sources":["../../../src/server/tts/ttsRoutes.ts"],"names":[],"mappings":"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"}
|
|
@@ -198,6 +198,34 @@ export interface PiWebUiAgentConfig {
|
|
|
198
198
|
/** Pi-compatible profile directory containing auth.json, models.json, settings.json, and sessions/. */
|
|
199
199
|
dir?: string;
|
|
200
200
|
}
|
|
201
|
+
export interface PiWebUiTtsConfig {
|
|
202
|
+
voice?: string;
|
|
203
|
+
rate?: number;
|
|
204
|
+
}
|
|
205
|
+
export interface HostSpeechVoice {
|
|
206
|
+
name: string;
|
|
207
|
+
language: string;
|
|
208
|
+
variant?: string;
|
|
209
|
+
}
|
|
210
|
+
export interface HostSpeechStatus {
|
|
211
|
+
available: boolean;
|
|
212
|
+
reason?: string;
|
|
213
|
+
voices: HostSpeechVoice[];
|
|
214
|
+
}
|
|
215
|
+
export interface HostSpeechSpeakRequest {
|
|
216
|
+
runId: string;
|
|
217
|
+
text: string;
|
|
218
|
+
voice?: string;
|
|
219
|
+
rate: number;
|
|
220
|
+
}
|
|
221
|
+
export interface HostSpeechTerminalResult {
|
|
222
|
+
runId: string;
|
|
223
|
+
outcome: "ended" | "canceled";
|
|
224
|
+
}
|
|
225
|
+
export interface HostSpeechStopResponse {
|
|
226
|
+
runId: string;
|
|
227
|
+
stopped: boolean;
|
|
228
|
+
}
|
|
201
229
|
export interface PiWebUiConfigValues {
|
|
202
230
|
host?: string;
|
|
203
231
|
port?: number;
|
|
@@ -225,6 +253,8 @@ export interface PiWebUiConfigValues {
|
|
|
225
253
|
subsessions?: boolean;
|
|
226
254
|
/** Desired Pi-compatible agent profile and companion CLI (Pi by default). */
|
|
227
255
|
agent?: PiWebUiAgentConfig;
|
|
256
|
+
/** Host speech configuration for manual text-to-speech playback. */
|
|
257
|
+
tts?: PiWebUiTtsConfig;
|
|
228
258
|
}
|
|
229
259
|
export type PiWebUiPluginScope = "bundled" | "local" | "user" | "project";
|
|
230
260
|
export interface PiWebUiPluginInfo {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiTypes.js","sourceRoot":"","sources":["../../src/shared/apiTypes.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,sBAAsB,EAAE,yBAAyB;IACjD,qBAAqB,EAAE,wBAAwB;IAC/C,eAAe,EAAE,kBAAkB;IACnC,cAAc,EAAE,iBAAiB;IACjC,kBAAkB,EAAE,qBAAqB;IACzC,sBAAsB,EAAE,yBAAyB;IACjD,oBAAoB,EAAE,uBAAuB;IAC7C,sBAAsB,EAAE,yBAAyB;IACjD,qBAAqB,EAAE,wBAAwB;IAC/C,cAAc,EAAE,iBAAiB;IACjC,iBAAiB,EAAE,oBAAoB;IACvC,wBAAwB,EAAE,2BAA2B;IACrD,gBAAgB,EAAE,mBAAmB;IACrC,uBAAuB,EAAE,0BAA0B;IACnD,kBAAkB,EAAE,uBAAuB;IAC3C,iBAAiB,EAAE,qBAAqB;IACxC,oBAAoB,EAAE,wBAAwB;IAC9C,mBAAmB,EAAE,sBAAsB;IAC3C,2BAA2B,EAAE,8BAA8B;IAC3D,mCAAmC,EAAE,sCAAsC;IAC3E,eAAe,EAAE,kBAAkB;IACnC,sBAAsB,EAAE,yBAAyB;CACzC,CAAC;AAsDX,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAQvG,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,aAAa,EAAE,SAAS,CAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"apiTypes.js","sourceRoot":"","sources":["../../src/shared/apiTypes.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,sBAAsB,EAAE,yBAAyB;IACjD,qBAAqB,EAAE,wBAAwB;IAC/C,eAAe,EAAE,kBAAkB;IACnC,cAAc,EAAE,iBAAiB;IACjC,kBAAkB,EAAE,qBAAqB;IACzC,sBAAsB,EAAE,yBAAyB;IACjD,oBAAoB,EAAE,uBAAuB;IAC7C,sBAAsB,EAAE,yBAAyB;IACjD,qBAAqB,EAAE,wBAAwB;IAC/C,cAAc,EAAE,iBAAiB;IACjC,iBAAiB,EAAE,oBAAoB;IACvC,wBAAwB,EAAE,2BAA2B;IACrD,gBAAgB,EAAE,mBAAmB;IACrC,uBAAuB,EAAE,0BAA0B;IACnD,kBAAkB,EAAE,uBAAuB;IAC3C,iBAAiB,EAAE,qBAAqB;IACxC,oBAAoB,EAAE,wBAAwB;IAC9C,mBAAmB,EAAE,sBAAsB;IAC3C,2BAA2B,EAAE,8BAA8B;IAC3D,mCAAmC,EAAE,sCAAsC;IAC3E,eAAe,EAAE,kBAAkB;IACnC,sBAAsB,EAAE,yBAAyB;CACzC,CAAC;AAsDX,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAQvG,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,aAAa,EAAE,SAAS,CAAU,CAAC;AA4cvE,gEAAgE;AAChE,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAwEvC,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAC3C,MAAM,CAAC,MAAM,qCAAqC,GAAG,GAAG,CAAC;AACzD,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,IAAI,CAAC;AACxD,MAAM,CAAC,MAAM,sCAAsC,GAAG,EAAE,GAAG,IAAI,CAAC;AAkBhE,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC;AAC1C,MAAM,CAAC,MAAM,oCAAoC,GAAG,GAAG,CAAC;AACxD,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,GAAG,IAAI,CAAC;AACvD,MAAM,CAAC,MAAM,oCAAoC,GAAG,GAAG,CAAC;AACxD,MAAM,CAAC,MAAM,sCAAsC,GAAG,EAAE,CAAC;AAqCzD,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAC9C,MAAM,CAAC,MAAM,kCAAkC,GAAG,CAAC,GAAG,IAAI,CAAC;AAiwB3D,MAAM,CAAC,MAAM,2CAA2C,GAAG,MAAM,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const HOST_SPEECH_MAX_TEXT_CHARS = 4_000;
|
|
2
|
+
export const HOST_SPEECH_MAX_RUN_ID_CHARS = 128;
|
|
3
|
+
export function effectivePiWebUiTtsConfig(config) {
|
|
4
|
+
return {
|
|
5
|
+
...(config?.voice !== undefined ? { voice: config.voice } : {}),
|
|
6
|
+
rate: config?.rate ?? 0,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function truncateHostSpeechText(text) {
|
|
10
|
+
// Normalize CRLF/CR to LF, remove NUL and control chars (Cc) except LF (\n) and Tab (\t)
|
|
11
|
+
const normalized = text
|
|
12
|
+
.replace(/\r\n|\r/gu, "\n")
|
|
13
|
+
.replace(/(?!\t|\n)\p{Cc}/gu, "");
|
|
14
|
+
let sliced = normalized.slice(0, HOST_SPEECH_MAX_TEXT_CHARS);
|
|
15
|
+
if (sliced.length === HOST_SPEECH_MAX_TEXT_CHARS) {
|
|
16
|
+
const last = sliced.charCodeAt(sliced.length - 1);
|
|
17
|
+
const next = normalized.charCodeAt(HOST_SPEECH_MAX_TEXT_CHARS);
|
|
18
|
+
if (last >= 0xD800 && last <= 0xDBFF && next >= 0xDC00 && next <= 0xDFFF) {
|
|
19
|
+
sliced = sliced.slice(0, -1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return sliced.trimEnd();
|
|
23
|
+
}
|
|
24
|
+
const RUN_ID_REGEX = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
25
|
+
export function isHostSpeechRunId(value) {
|
|
26
|
+
return RUN_ID_REGEX.test(value);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=hostSpeech.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hostSpeech.js","sourceRoot":"","sources":["../../src/shared/hostSpeech.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAChD,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,CAAC;AAEhD,MAAM,UAAU,yBAAyB,CAAC,MAAoC;IAI5E,OAAO;QACL,GAAG,CAAC,MAAM,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC;KACxB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,yFAAyF;IACzF,MAAM,UAAU,GAAG,IAAI;SACpB,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC;SAC1B,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;IACpC,IAAI,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,0BAA0B,CAAC,CAAC;IAC7D,IAAI,MAAM,CAAC,MAAM,KAAK,0BAA0B,EAAE,CAAC;QACjD,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,CAAC,0BAA0B,CAAC,CAAC;QAC/D,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YACzE,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC;AAC1B,CAAC;AAED,MAAM,YAAY,GAAG,sCAAsC,CAAC;AAE5D,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,OAAO,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC"}
|
package/docs/config.md
CHANGED
|
@@ -42,6 +42,7 @@ Process restarts depend on the key:
|
|
|
42
42
|
- `agent.command` / `agent.dir` / `spawnSessions` / `subsessions`: restart the session daemon on that machine.
|
|
43
43
|
- `modelTiers`: saved settings apply immediately in **Settings → Model tiers**; validates all six ladder rows atomically.
|
|
44
44
|
- `utilityModels`: saved settings apply immediately in **Settings → Utility models**; existing sessions use updated values on their next utility operation.
|
|
45
|
+
- `tts`: saved voice/rate settings apply to the next utterance; no service restart required.
|
|
45
46
|
- `pathAccess`: applies on the next request; existing file views may need a browser refresh.
|
|
46
47
|
- `uploads.defaultFolder`: applies to newly opened Files upload dialogs and new direct drag/drop batches after config/workspace refresh.
|
|
47
48
|
- `plugins`: reload the browser tab after changing PI WEBUI plugin enablement.
|
|
@@ -84,6 +85,10 @@ Process restarts depend on the key:
|
|
|
84
85
|
"id": "claude-sonnet"
|
|
85
86
|
}
|
|
86
87
|
},
|
|
88
|
+
"tts": {
|
|
89
|
+
"voice": "en-US-Test",
|
|
90
|
+
"rate": 20
|
|
91
|
+
},
|
|
87
92
|
"spawnSessions": true,
|
|
88
93
|
"subsessions": false,
|
|
89
94
|
"plugins": {
|
|
@@ -139,6 +144,7 @@ Rows with JSON key `—` are runtime-only environment variables, not config-file
|
|
|
139
144
|
| Tracked subsessions (beta) | `subsessions` | `PI_WEBUI_SUBSESSIONS` | Global/session daemon | Not supported locally; also requires `spawnSessions` | Restart session daemon on that machine |
|
|
140
145
|
| Model tier routing ladder | `modelTiers` | — | Global | Not supported locally | Saved settings apply immediately on save; requires remote peer capability `settings.modelTiers` |
|
|
141
146
|
| Utility model routing | `utilityModels` | — | Global | Not supported locally | Saved settings apply immediately on the next utility operation; requires remote peer capability `settings.utilityModels` |
|
|
147
|
+
| Local gateway text to speech | `tts` | — | Global | Not supported locally | Next utterance after settings save; no service restart |
|
|
142
148
|
| Plugin enablement/settings | `plugins.<id>.enabled`, `plugins.<id>.settings` | — | Global | Not core local config; plugins may read their own project files | Reload browser tab |
|
|
143
149
|
| Keyboard shortcuts | `shortcuts.<actionId>` | — | Global | Not supported locally | Applies after settings save/config refresh |
|
|
144
150
|
| Project config version | `version` | — | Project | Project-local only; must be `1` when present | Next project-config read |
|
|
@@ -281,6 +287,35 @@ Sessions can also use a per-session model policy from the composer:
|
|
|
281
287
|
- **Starting and persistence:** The selected policy is carried into root-session creation from both the first-prompt and **New Session** paths, persists with that session, and remains selected after a failed creation so a retry uses the same choice.
|
|
282
288
|
- **Scope and installation:** This release does not add `/tier-*` commands, and editing the tier ladder later does not automatically remap an existing Tiered session. Installing this change requires one manual `pi-webui-sessiond.service` restart; ordinary UI/API autoreload does not load session-daemon changes.
|
|
283
289
|
|
|
290
|
+
### Local gateway text-to-speech
|
|
291
|
+
|
|
292
|
+
PI WEBUI can read assistant replies aloud through the operating-system speech service on the machine running the local gateway. The browser is only the control surface: it sends the controls, and audio is audible on the gateway host, not in the browser. The capability is opt-in and local-gateway-only — there is no text-to-speech for remote machines or remote sessions, no browser-native synthesis or browser audio, no audio-file generation, and no online provider account, API key, or engine picker.
|
|
293
|
+
|
|
294
|
+
On Linux, the gateway host must run the Speech Dispatcher service, and the PI WEBUI web/API process must be able to reach its local socket. If the service is missing or unreachable, the **Listen to assistant reply** action and the settings card stay visible but disabled with the availability reason. PI WEBUI treats speech as an opaque OS capability: Speech Dispatcher output modules may use network-backed services, so playback is not guaranteed to work offline, and PI WEBUI does not report whether the backend is offline or network-backed.
|
|
295
|
+
|
|
296
|
+
The **Text to speech** card in **Settings → General** appears only while the local gateway is selected:
|
|
297
|
+
|
|
298
|
+
- **OS voice** selects an installed Speech Dispatcher voice or **System default**.
|
|
299
|
+
- **Speech rate** is an integer from `-100` to `100`; `0` is the system's normal rate.
|
|
300
|
+
- Eligible assistant replies show a **Listen to assistant reply** icon action that starts speech immediately; the same position becomes **Stop reading assistant reply** while that utterance is active. Stop affects only the utterance PI WEBUI started.
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"tts": {
|
|
305
|
+
"voice": "en-US-Test",
|
|
306
|
+
"rate": 20
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The `tts` key is a gateway-only setting in `$PI_WEBUI_CONFIG` or `~/.config/pi-webui/config.json`. It is not a selected-machine key and never applies to remote machines. Omitting the whole object or any field means the system default voice and rate `0`. The object accepts only `voice` (a nonempty string) and `rate` (an integer from `-100` to `100`); unknown keys are rejected. A saved named voice that the OS speech service no longer reports stays configured but is not used: playback falls back to the system default and the settings card marks the saved voice unavailable until you choose another voice. Saving settings does not alter an utterance already in progress; the next utterance uses the saved values, with no service restart required.
|
|
312
|
+
|
|
313
|
+
Operational notes:
|
|
314
|
+
|
|
315
|
+
- `SPEECHD_ADDRESS` (Unix hosts only) overrides the Speech Dispatcher socket and must be `unix:` or `unix_socket:` followed by an absolute path; otherwise PI WEBUI uses the standard runtime/cache socket path.
|
|
316
|
+
- PI WEBUI speaks at Speech Dispatcher's normal `text` priority. Its speech can cancel lower-priority `notification` or `progress` speech from other Speech Dispatcher clients, and higher-priority speech from another client (such as a screen reader) can cancel PI WEBUI's utterance. That external cancellation returns the message action to Listen and is not an error. PI WEBUI never issues a global Speech Dispatcher stop/cancel that would affect other clients' speech.
|
|
317
|
+
- PI WEBUI has no authentication layer: any client that can reach the gateway HTTP surface can trigger audible speech on the host and enumerate its installed voices. Keep the gateway on a trusted network, VPN, tunnel, or behind an authenticated reverse proxy; see [Remote access](https://pi-webui.dev/install#remote-access) and the [reverse proxy deployment example](https://pi-webui.dev/install#reverse-proxy-prefix).
|
|
318
|
+
|
|
284
319
|
### Session daemon tools
|
|
285
320
|
|
|
286
321
|
`spawnSessions` controls whether agents receive the `spawn_session` tool. It defaults to `true`; set it to `false` if you do not want an agent to start independent PI WEBUI sessions.
|
package/package.json
CHANGED