@flowingspring/dsh-voco 0.2.9 → 0.3.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/README.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +17 -17
- package/lib/plugins/voice-assistant.js +67 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ The plugin uses `XingChenAGI/XingChenASR-V3.2-Ultra` for cloud speech recognitio
|
|
|
37
37
|
- Speaks a short contextual acknowledgement before starting delegated Agent work.
|
|
38
38
|
- Keeps full task reports in the task UI while speaking a concise, purpose-written result.
|
|
39
39
|
- Supports interruption, browser navigation, reconnects, and restored conversation history.
|
|
40
|
+
- Pure voice sessions get a concise title from the first meaningful voice request, with the transcription as a fallback when the title model is unavailable.
|
|
40
41
|
- Ships the server and browser surfaces as one public npm package.
|
|
41
42
|
|
|
42
43
|
## Configuration
|
package/README.zh.md
CHANGED
package/lib/client.js
CHANGED
|
@@ -18,11 +18,11 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
document.head.appendChild(tag);
|
|
19
19
|
}
|
|
20
20
|
var VoiceControl_module_css_default = {
|
|
21
|
-
"
|
|
21
|
+
"interruptButton": "TFZsua_interruptButton",
|
|
22
|
+
"active": "TFZsua_active",
|
|
22
23
|
"voice-pulse": "TFZsua_voice-pulse",
|
|
23
24
|
"button": "TFZsua_button",
|
|
24
|
-
"
|
|
25
|
-
"interruptButton": "TFZsua_interruptButton"
|
|
25
|
+
"controls": "TFZsua_controls"
|
|
26
26
|
};
|
|
27
27
|
//#endregion
|
|
28
28
|
//#region ../ui-voice/src/client/VoiceControl.tsx
|
|
@@ -141,22 +141,22 @@ window.__ModuleLoader__.load({
|
|
|
141
141
|
document.head.appendChild(tag);
|
|
142
142
|
}
|
|
143
143
|
var VoiceNodeViews_module_css_default = {
|
|
144
|
-
"taskInput": "Kt2smW_taskInput",
|
|
145
|
-
"miniWave": "Kt2smW_miniWave",
|
|
146
|
-
"taskSummary": "Kt2smW_taskSummary",
|
|
147
|
-
"meta": "Kt2smW_meta",
|
|
148
|
-
"bubble": "Kt2smW_bubble",
|
|
149
|
-
"taskStatus": "Kt2smW_taskStatus",
|
|
150
|
-
"taskActions": "Kt2smW_taskActions",
|
|
151
144
|
"taskLink": "Kt2smW_taskLink",
|
|
152
|
-
"
|
|
153
|
-
"taskDot": "Kt2smW_taskDot",
|
|
145
|
+
"taskCard": "Kt2smW_taskCard",
|
|
154
146
|
"utterance": "Kt2smW_utterance",
|
|
147
|
+
"taskDot": "Kt2smW_taskDot",
|
|
148
|
+
"bubble": "Kt2smW_bubble",
|
|
155
149
|
"voiceBadge": "Kt2smW_voiceBadge",
|
|
156
|
-
"
|
|
157
|
-
"taskCancel": "Kt2smW_taskCancel",
|
|
150
|
+
"taskActions": "Kt2smW_taskActions",
|
|
158
151
|
"taskChevron": "Kt2smW_taskChevron",
|
|
152
|
+
"taskSummary": "Kt2smW_taskSummary",
|
|
153
|
+
"miniWave": "Kt2smW_miniWave",
|
|
154
|
+
"meta": "Kt2smW_meta",
|
|
155
|
+
"taskDetails": "Kt2smW_taskDetails",
|
|
159
156
|
"taskUpdate": "Kt2smW_taskUpdate",
|
|
157
|
+
"taskCancel": "Kt2smW_taskCancel",
|
|
158
|
+
"taskInput": "Kt2smW_taskInput",
|
|
159
|
+
"taskStatus": "Kt2smW_taskStatus",
|
|
160
160
|
"taskTitle": "Kt2smW_taskTitle"
|
|
161
161
|
};
|
|
162
162
|
//#endregion
|
|
@@ -285,11 +285,11 @@ window.__ModuleLoader__.load({
|
|
|
285
285
|
document.head.appendChild(tag);
|
|
286
286
|
}
|
|
287
287
|
var VoiceOverlay_module_css_default = {
|
|
288
|
-
"wave": "GPPB2G_wave",
|
|
289
288
|
"root": "GPPB2G_root",
|
|
289
|
+
"status": "GPPB2G_status",
|
|
290
|
+
"wave": "GPPB2G_wave",
|
|
290
291
|
"voice-wave": "GPPB2G_voice-wave",
|
|
291
|
-
"stop": "GPPB2G_stop"
|
|
292
|
-
"status": "GPPB2G_status"
|
|
292
|
+
"stop": "GPPB2G_stop"
|
|
293
293
|
};
|
|
294
294
|
//#endregion
|
|
295
295
|
//#region ../ui-voice/src/client/VoiceOverlay.tsx
|
|
@@ -159,6 +159,13 @@ function optionalWorkspaceMemory(ctx) {
|
|
|
159
159
|
return;
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
+
function optionalSessionTitle(ctx) {
|
|
163
|
+
try {
|
|
164
|
+
return ctx.get("sessionTitle");
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
162
169
|
function workspaceMemoryReference(memory) {
|
|
163
170
|
const sections = [];
|
|
164
171
|
if (memory.summary.trim() !== "") sections.push(`稳定摘要:\n${memory.summary.trim()}`);
|
|
@@ -193,6 +200,55 @@ const REWRITE_EVENT_INSTRUCTIONS = {
|
|
|
193
200
|
warning: "这是警告信息。简洁说明风险、影响以及用户需要知道的事项。",
|
|
194
201
|
error: "这是失败信息。明确说明没有完成、主要原因以及可行的下一步。"
|
|
195
202
|
};
|
|
203
|
+
const VOICE_TITLE_SYSTEM_PROMPT = `你是语音会话标题生成器。根据用户第一条有实际内容的语音请求,生成一个简短、准确、便于在会话列表中识别的中文标题。
|
|
204
|
+
|
|
205
|
+
只输出标题本身,不要解释,不要引号,不要 Markdown,不要句末标点,不要使用“语音会话”“新会话”等泛化名称,不超过 12 个汉字。不要加入文件大小、行数、路径等执行细节。`;
|
|
206
|
+
function voiceTitleCandidate(value) {
|
|
207
|
+
const normalized = value.replace(/```[\s\S]*?```/gu, "").replace(/^\s*(?:标题|title)\s*[::]\s*/iu, "").replace(/["'「」『』]/gu, "").split(/\r?\n/u, 1)[0].replace(/[。!?!?;;,,::]+$/u, "").replace(/\s+/gu, " ").trim();
|
|
208
|
+
return Array.from(normalized).slice(0, 12).join("");
|
|
209
|
+
}
|
|
210
|
+
function isMeaningfulVoiceTitleInput(value) {
|
|
211
|
+
const normalized = value.replace(/\s+/gu, "").trim();
|
|
212
|
+
if (Array.from(normalized).length < 4) return false;
|
|
213
|
+
return !/^(?:你好|您好|嗨|哈喽|喂|在吗|有人吗|测试|谢谢|好的)[,。!?!?、]*$/u.test(normalized);
|
|
214
|
+
}
|
|
215
|
+
async function generateVoiceSessionTitle(ctx, session, input, titleService) {
|
|
216
|
+
let candidate = "";
|
|
217
|
+
try {
|
|
218
|
+
const llm = ctx.get("llm");
|
|
219
|
+
if (llm !== void 0) {
|
|
220
|
+
const selection = ctx.agentDefaultModel.currentSelection();
|
|
221
|
+
const message = createUserMessage({
|
|
222
|
+
content: [{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: `用户第一条有效语音请求:${input}`
|
|
225
|
+
}],
|
|
226
|
+
source: {
|
|
227
|
+
kind: "plugin",
|
|
228
|
+
plugin: "voice-assistant"
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
let output = "";
|
|
232
|
+
for await (const chunk of llm.stream({
|
|
233
|
+
provider: selection.provider,
|
|
234
|
+
model: selection.model,
|
|
235
|
+
...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort },
|
|
236
|
+
messages: [message],
|
|
237
|
+
system: VOICE_TITLE_SYSTEM_PROMPT
|
|
238
|
+
})) if (chunk.type === "text-delta") output += chunk.text;
|
|
239
|
+
candidate = voiceTitleCandidate(output);
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
243
|
+
}
|
|
244
|
+
if (candidate === "") candidate = voiceTitleCandidate(input);
|
|
245
|
+
if (candidate === "" || titleService.get(session) !== void 0) return;
|
|
246
|
+
try {
|
|
247
|
+
titleService.rename(session, candidate);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
196
252
|
/** Install the driver. @param ctx - composed Agent and voice context. @param config - driver copy and queue bounds. */
|
|
197
253
|
function apply(ctx, config = {}) {
|
|
198
254
|
const bindings = /* @__PURE__ */ new Map();
|
|
@@ -223,6 +279,7 @@ function apply(ctx, config = {}) {
|
|
|
223
279
|
interactionMode: void 0,
|
|
224
280
|
voiceAttached: false,
|
|
225
281
|
voiceTurnMarked: false,
|
|
282
|
+
voiceTitleRequested: false,
|
|
226
283
|
active: void 0,
|
|
227
284
|
continuousTaskAgent: void 0,
|
|
228
285
|
lastTerminalTaskId: void 0,
|
|
@@ -716,13 +773,22 @@ function apply(ctx, config = {}) {
|
|
|
716
773
|
};
|
|
717
774
|
const endUtterance = (binding, utteranceId, role, state, finalText, responseId) => {
|
|
718
775
|
const utterance = beginUtterance(binding, utteranceId, role, responseId);
|
|
719
|
-
requireSourceSession(binding)
|
|
776
|
+
const sourceSession = requireSourceSession(binding);
|
|
777
|
+
sourceSession.append("voice/utterance-end", {
|
|
720
778
|
utteranceId,
|
|
721
779
|
role,
|
|
722
780
|
text: finalText ?? utterance.text,
|
|
723
781
|
state,
|
|
724
782
|
...responseId === void 0 ? {} : { responseId }
|
|
725
783
|
});
|
|
784
|
+
const utteranceText = finalText ?? utterance.text;
|
|
785
|
+
if (role === "user" && state === "completed" && isMeaningfulVoiceTitleInput(utteranceText)) {
|
|
786
|
+
const titleService = optionalSessionTitle(ctx);
|
|
787
|
+
if (titleService !== void 0 && !binding.voiceTitleRequested && titleService.get(sourceSession) === void 0) {
|
|
788
|
+
binding.voiceTitleRequested = true;
|
|
789
|
+
generateVoiceSessionTitle(ctx, sourceSession, utteranceText, titleService);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
726
792
|
binding.utterances.delete(utteranceId);
|
|
727
793
|
};
|
|
728
794
|
const interruptAssistantUtterances = (binding, responseId) => {
|
package/package.json
CHANGED