voice-page-agent 0.1.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 +151 -0
- package/dist/index.cjs +603 -0
- package/dist/index.d.cts +151 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +596 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { defineComponent, ref, onMounted, onBeforeUnmount, h, inject, getCurrentInstance } from 'vue-demi';
|
|
2
|
+
|
|
3
|
+
// src/controller.ts
|
|
4
|
+
var DEFAULT_WAKE_WORD = "\u5E03\u4E01\u5E03\u4E01";
|
|
5
|
+
var DEFAULT_OPTIONS = {
|
|
6
|
+
wakeWord: [DEFAULT_WAKE_WORD],
|
|
7
|
+
enableHomophoneMatch: true,
|
|
8
|
+
wakeCooldownMs: 1400,
|
|
9
|
+
commandInitialTimeoutMs: 12e3,
|
|
10
|
+
commandSilenceTimeoutMs: 2600,
|
|
11
|
+
commandMaxWindowMs: 22e3,
|
|
12
|
+
recognitionLang: "zh-CN",
|
|
13
|
+
showAgentWhenWake: true,
|
|
14
|
+
autoStart: false
|
|
15
|
+
};
|
|
16
|
+
var HOMOPHONE_VARIANTS = {
|
|
17
|
+
\u5E03: ["\u8865", "\u4E0D", "\u6B65", "\u90E8"],
|
|
18
|
+
\u4E01: ["\u53EE", "\u9489", "\u76EF", "\u9876"],
|
|
19
|
+
\u5C0F: ["\u6653", "\u7B11", "\u6821", "\u7B71"],
|
|
20
|
+
\u73ED: ["\u822C", "\u6591", "\u534A"]
|
|
21
|
+
};
|
|
22
|
+
function normalizeText(text) {
|
|
23
|
+
return (text || "").toLowerCase().replace(/[\s,,.。!!??;;::、"'`~\-_/\\]/g, "");
|
|
24
|
+
}
|
|
25
|
+
function escapeRegExp(value) {
|
|
26
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
}
|
|
28
|
+
function levenshteinDistance(source, target) {
|
|
29
|
+
const a = Array.from(source);
|
|
30
|
+
const b = Array.from(target);
|
|
31
|
+
if (a.length === 0) return b.length;
|
|
32
|
+
if (b.length === 0) return a.length;
|
|
33
|
+
const dp = Array.from(
|
|
34
|
+
{ length: a.length + 1 },
|
|
35
|
+
() => Array.from({ length: b.length + 1 }, () => 0)
|
|
36
|
+
);
|
|
37
|
+
for (let i = 0; i <= a.length; i += 1) dp[i][0] = i;
|
|
38
|
+
for (let j = 0; j <= b.length; j += 1) dp[0][j] = j;
|
|
39
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
40
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
41
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
42
|
+
dp[i][j] = Math.min(
|
|
43
|
+
dp[i - 1][j] + 1,
|
|
44
|
+
dp[i][j - 1] + 1,
|
|
45
|
+
dp[i - 1][j - 1] + cost
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return dp[a.length][b.length];
|
|
50
|
+
}
|
|
51
|
+
function makeHomophoneRegex(words) {
|
|
52
|
+
const fuzzyWords = words.map((word) => {
|
|
53
|
+
const chars = Array.from(word);
|
|
54
|
+
return chars.map((char) => {
|
|
55
|
+
const variants = HOMOPHONE_VARIANTS[char] || [];
|
|
56
|
+
const options = [char, ...variants].map((item) => escapeRegExp(item));
|
|
57
|
+
return `(?:${options.join("|")})`;
|
|
58
|
+
}).join("");
|
|
59
|
+
});
|
|
60
|
+
return new RegExp(fuzzyWords.join("|"));
|
|
61
|
+
}
|
|
62
|
+
function buildWakeRegex(words) {
|
|
63
|
+
const items = words.map((word) => normalizeText(word)).filter(Boolean).sort((a, b) => b.length - a.length).map((word) => escapeRegExp(word));
|
|
64
|
+
const body = items.length ? items.join("|") : escapeRegExp(normalizeText(DEFAULT_WAKE_WORD));
|
|
65
|
+
return {
|
|
66
|
+
single: new RegExp(`(${body})`),
|
|
67
|
+
global: new RegExp(`(${body})`, "g")
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function resolveOptions(options) {
|
|
71
|
+
if (!(options == null ? void 0 : options.pageAgent) || typeof options.pageAgent !== "object") {
|
|
72
|
+
throw new Error("voice-page-agent: options.pageAgent is required");
|
|
73
|
+
}
|
|
74
|
+
const wakeWordInput = options.wakeWord;
|
|
75
|
+
const wakeWord = Array.isArray(wakeWordInput) ? wakeWordInput.filter(Boolean) : [wakeWordInput || DEFAULT_WAKE_WORD];
|
|
76
|
+
return {
|
|
77
|
+
...DEFAULT_OPTIONS,
|
|
78
|
+
...options,
|
|
79
|
+
pageAgent: options.pageAgent,
|
|
80
|
+
wakeWord
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var VoicePageAgentController = class {
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
86
|
+
this.state = {
|
|
87
|
+
status: "off",
|
|
88
|
+
message: "\u8BED\u97F3\u52A9\u624B\u672A\u5F00\u542F",
|
|
89
|
+
supported: false,
|
|
90
|
+
enabled: false,
|
|
91
|
+
micPermissionGranted: false
|
|
92
|
+
};
|
|
93
|
+
this.recognition = null;
|
|
94
|
+
this.initAgentPromise = null;
|
|
95
|
+
this.voiceEnabled = false;
|
|
96
|
+
this.disposed = false;
|
|
97
|
+
this.awaitingCommand = false;
|
|
98
|
+
this.commandFinalText = "";
|
|
99
|
+
this.commandInterimText = "";
|
|
100
|
+
this.commandTimer = null;
|
|
101
|
+
this.commandDeadlineTimer = null;
|
|
102
|
+
this.restartBusy = false;
|
|
103
|
+
this.lastWakeAt = 0;
|
|
104
|
+
this.options = resolveOptions(options);
|
|
105
|
+
this.wakeRegex = buildWakeRegex(this.options.wakeWord);
|
|
106
|
+
this.wakeHomophoneRegex = this.options.enableHomophoneMatch ? makeHomophoneRegex(this.options.wakeWord) : null;
|
|
107
|
+
this.patchState({
|
|
108
|
+
supported: this.hasSpeechSupport(),
|
|
109
|
+
status: this.hasSpeechSupport() ? "off" : "unsupported",
|
|
110
|
+
message: this.hasSpeechSupport() ? "\u8BED\u97F3\u52A9\u624B\u672A\u5F00\u542F" : "\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u8BED\u97F3\u8BC6\u522B\uFF0C\u5EFA\u8BAE\u4F7F\u7528 Chrome/Edge"
|
|
111
|
+
});
|
|
112
|
+
if (typeof window !== "undefined") {
|
|
113
|
+
const saved = window.localStorage.getItem("voice-page-agent-enabled");
|
|
114
|
+
if (saved === "1" || this.options.autoStart) {
|
|
115
|
+
void this.startWake();
|
|
116
|
+
}
|
|
117
|
+
void this.syncPermissionState();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
get snapshot() {
|
|
121
|
+
return { ...this.state };
|
|
122
|
+
}
|
|
123
|
+
onStateChange(listener) {
|
|
124
|
+
this.listeners.add(listener);
|
|
125
|
+
listener(this.snapshot);
|
|
126
|
+
return () => {
|
|
127
|
+
this.listeners.delete(listener);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async openAgent() {
|
|
131
|
+
return this.ensureAgent();
|
|
132
|
+
}
|
|
133
|
+
async startWake() {
|
|
134
|
+
if (this.disposed) return;
|
|
135
|
+
if (!this.hasSpeechSupport()) {
|
|
136
|
+
this.patchState({
|
|
137
|
+
status: "unsupported",
|
|
138
|
+
supported: false,
|
|
139
|
+
message: "\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u8BED\u97F3\u8BC6\u522B\uFF0C\u5EFA\u8BAE\u4F7F\u7528 Chrome/Edge"
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (this.voiceEnabled) {
|
|
144
|
+
this.patchState({
|
|
145
|
+
status: "waking",
|
|
146
|
+
enabled: true,
|
|
147
|
+
message: `\u8BED\u97F3\u5DF2\u5F00\u542F\uFF0C\u8BF7\u8BF4\u201C${this.options.wakeWord[0]}\u201D\u5524\u9192\u52A9\u624B`
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const granted = await this.requestMicrophonePermission();
|
|
152
|
+
if (!granted) {
|
|
153
|
+
this.patchState({
|
|
154
|
+
status: "error",
|
|
155
|
+
enabled: false,
|
|
156
|
+
micPermissionGranted: false,
|
|
157
|
+
message: "\u9EA6\u514B\u98CE\u6388\u6743\u5931\u8D25\uFF0C\u8BF7\u5141\u8BB8\u6D4F\u89C8\u5668\u4F7F\u7528\u9EA6\u514B\u98CE"
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const recognition = this.recognition || this.buildRecognition();
|
|
162
|
+
if (!recognition) {
|
|
163
|
+
this.patchState({
|
|
164
|
+
status: "error",
|
|
165
|
+
enabled: false,
|
|
166
|
+
message: "\u8BED\u97F3\u8BC6\u522B\u521D\u59CB\u5316\u5931\u8D25"
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.recognition = recognition;
|
|
171
|
+
this.voiceEnabled = true;
|
|
172
|
+
this.patchState({
|
|
173
|
+
status: "waking",
|
|
174
|
+
enabled: true,
|
|
175
|
+
micPermissionGranted: true,
|
|
176
|
+
message: `\u8BED\u97F3\u5DF2\u5F00\u542F\uFF0C\u8BF7\u8BF4\u201C${this.options.wakeWord[0]}\u201D\u5524\u9192\u52A9\u624B`
|
|
177
|
+
});
|
|
178
|
+
window.localStorage.setItem("voice-page-agent-enabled", "1");
|
|
179
|
+
try {
|
|
180
|
+
recognition.start();
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
stopWake() {
|
|
185
|
+
this.voiceEnabled = false;
|
|
186
|
+
this.awaitingCommand = false;
|
|
187
|
+
this.commandFinalText = "";
|
|
188
|
+
this.commandInterimText = "";
|
|
189
|
+
this.clearCommandTimer();
|
|
190
|
+
this.clearCommandDeadlineTimer();
|
|
191
|
+
this.patchState({
|
|
192
|
+
status: "off",
|
|
193
|
+
enabled: false,
|
|
194
|
+
message: "\u8BED\u97F3\u52A9\u624B\u5DF2\u5173\u95ED"
|
|
195
|
+
});
|
|
196
|
+
if (typeof window !== "undefined") {
|
|
197
|
+
window.localStorage.setItem("voice-page-agent-enabled", "0");
|
|
198
|
+
}
|
|
199
|
+
if (this.recognition) {
|
|
200
|
+
this.recognition.stop();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async runCommand(commandText) {
|
|
204
|
+
await this.executeVoiceCommand(commandText);
|
|
205
|
+
}
|
|
206
|
+
dispose() {
|
|
207
|
+
this.disposed = true;
|
|
208
|
+
this.stopWake();
|
|
209
|
+
if (this.recognition) {
|
|
210
|
+
this.recognition.abort();
|
|
211
|
+
this.recognition = null;
|
|
212
|
+
}
|
|
213
|
+
this.listeners.clear();
|
|
214
|
+
}
|
|
215
|
+
hasSpeechSupport() {
|
|
216
|
+
if (typeof window === "undefined") return false;
|
|
217
|
+
const runtimeWindow = window;
|
|
218
|
+
return Boolean(runtimeWindow.SpeechRecognition || runtimeWindow.webkitSpeechRecognition);
|
|
219
|
+
}
|
|
220
|
+
patchState(next) {
|
|
221
|
+
this.state = { ...this.state, ...next };
|
|
222
|
+
const snapshot = this.snapshot;
|
|
223
|
+
this.listeners.forEach((listener) => listener(snapshot));
|
|
224
|
+
}
|
|
225
|
+
clearCommandTimer() {
|
|
226
|
+
if (this.commandTimer !== null) {
|
|
227
|
+
window.clearTimeout(this.commandTimer);
|
|
228
|
+
this.commandTimer = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
clearCommandDeadlineTimer() {
|
|
232
|
+
if (this.commandDeadlineTimer !== null) {
|
|
233
|
+
window.clearTimeout(this.commandDeadlineTimer);
|
|
234
|
+
this.commandDeadlineTimer = null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
composeCommandText() {
|
|
238
|
+
return `${this.commandFinalText} ${this.commandInterimText}`.trim();
|
|
239
|
+
}
|
|
240
|
+
scheduleFlushCommand(ms) {
|
|
241
|
+
this.clearCommandTimer();
|
|
242
|
+
this.commandTimer = window.setTimeout(() => {
|
|
243
|
+
this.flushVoiceCommand();
|
|
244
|
+
}, ms);
|
|
245
|
+
}
|
|
246
|
+
scheduleCommandDeadline() {
|
|
247
|
+
this.clearCommandDeadlineTimer();
|
|
248
|
+
this.commandDeadlineTimer = window.setTimeout(() => {
|
|
249
|
+
this.flushVoiceCommand();
|
|
250
|
+
}, this.options.commandMaxWindowMs);
|
|
251
|
+
}
|
|
252
|
+
flushVoiceCommand() {
|
|
253
|
+
this.clearCommandTimer();
|
|
254
|
+
this.clearCommandDeadlineTimer();
|
|
255
|
+
const command = this.sanitizeVoiceCommand(this.composeCommandText());
|
|
256
|
+
this.awaitingCommand = false;
|
|
257
|
+
this.commandFinalText = "";
|
|
258
|
+
this.commandInterimText = "";
|
|
259
|
+
void this.executeVoiceCommand(command);
|
|
260
|
+
}
|
|
261
|
+
isWakePhraseHit(text) {
|
|
262
|
+
var _a;
|
|
263
|
+
const normalized = normalizeText(text);
|
|
264
|
+
if (!normalized) return false;
|
|
265
|
+
if (this.options.wakeWord.some((word) => normalized.includes(normalizeText(word)))) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (this.options.enableHomophoneMatch && ((_a = this.wakeHomophoneRegex) == null ? void 0 : _a.test(normalized))) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const candidates = this.options.wakeWord.map((word) => normalizeText(word));
|
|
272
|
+
for (const candidate of candidates) {
|
|
273
|
+
if (Math.abs(candidate.length - normalized.length) > 1) continue;
|
|
274
|
+
if (levenshteinDistance(normalized, candidate) <= 1) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
extractCommandAfterWake(text) {
|
|
281
|
+
const source = (text || "").trim();
|
|
282
|
+
if (!source) return "";
|
|
283
|
+
const match = source.match(this.wakeRegex.single);
|
|
284
|
+
if (!match || typeof match.index !== "number") return "";
|
|
285
|
+
const rest = source.slice(match.index + match[0].length);
|
|
286
|
+
return rest.replace(/^[\s,,.。!!??;;::、]+/, "").trim();
|
|
287
|
+
}
|
|
288
|
+
sanitizeVoiceCommand(text) {
|
|
289
|
+
return (text || "").replace(this.wakeRegex.global, "").replace(/^[\s,,.。!!??;;::、]+/, "").trim();
|
|
290
|
+
}
|
|
291
|
+
async requestMicrophonePermission() {
|
|
292
|
+
try {
|
|
293
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
294
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
295
|
+
return true;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async syncPermissionState() {
|
|
301
|
+
var _a;
|
|
302
|
+
if (!("permissions" in navigator) || !((_a = navigator.permissions) == null ? void 0 : _a.query)) return;
|
|
303
|
+
try {
|
|
304
|
+
const result = await navigator.permissions.query({
|
|
305
|
+
name: "microphone"
|
|
306
|
+
});
|
|
307
|
+
this.patchState({ micPermissionGranted: result.state === "granted" });
|
|
308
|
+
result.onchange = () => {
|
|
309
|
+
this.patchState({ micPermissionGranted: result.state === "granted" });
|
|
310
|
+
};
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async ensureAgent() {
|
|
315
|
+
if (typeof window === "undefined") return null;
|
|
316
|
+
const runtimeWindow = window;
|
|
317
|
+
if (runtimeWindow.pageAgent) {
|
|
318
|
+
runtimeWindow.pageAgent.panel.show();
|
|
319
|
+
runtimeWindow.pageAgent.panel.expand();
|
|
320
|
+
return runtimeWindow.pageAgent;
|
|
321
|
+
}
|
|
322
|
+
if (this.initAgentPromise) return this.initAgentPromise;
|
|
323
|
+
this.initAgentPromise = (async () => {
|
|
324
|
+
const mod = await import('page-agent');
|
|
325
|
+
const Agent = mod.PageAgent;
|
|
326
|
+
const agent = new Agent(this.options.pageAgent);
|
|
327
|
+
runtimeWindow.pageAgent = agent;
|
|
328
|
+
agent.panel.show();
|
|
329
|
+
agent.panel.expand();
|
|
330
|
+
return agent;
|
|
331
|
+
})().catch((err) => {
|
|
332
|
+
this.patchState({
|
|
333
|
+
status: "error",
|
|
334
|
+
message: err instanceof Error ? err.message : "Page Agent \u521D\u59CB\u5316\u5931\u8D25"
|
|
335
|
+
});
|
|
336
|
+
return null;
|
|
337
|
+
}).finally(() => {
|
|
338
|
+
this.initAgentPromise = null;
|
|
339
|
+
});
|
|
340
|
+
return this.initAgentPromise;
|
|
341
|
+
}
|
|
342
|
+
async executeVoiceCommand(rawCommand) {
|
|
343
|
+
const command = this.sanitizeVoiceCommand(rawCommand);
|
|
344
|
+
if (!command) {
|
|
345
|
+
this.patchState({
|
|
346
|
+
status: "waking",
|
|
347
|
+
message: `\u672A\u8BC6\u522B\u5230\u6307\u4EE4\uFF0C\u8BF7\u518D\u8BF4\u4E00\u6B21\u201C${this.options.wakeWord[0]}\u201D`
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.patchState({
|
|
352
|
+
status: "processing",
|
|
353
|
+
message: `\u5DF2\u8BC6\u522B\uFF1A${command}`
|
|
354
|
+
});
|
|
355
|
+
const agent = await this.ensureAgent();
|
|
356
|
+
if (!agent) return;
|
|
357
|
+
if (agent.status === "running") {
|
|
358
|
+
this.patchState({
|
|
359
|
+
status: "waking",
|
|
360
|
+
message: "\u52A9\u624B\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BF4"
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await agent.execute(command);
|
|
366
|
+
this.patchState({
|
|
367
|
+
status: "waking",
|
|
368
|
+
message: `\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u5B8C\u6210\uFF0C\u7EE7\u7EED\u7B49\u5F85\u201C${this.options.wakeWord[0]}\u201D`
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
this.patchState({
|
|
372
|
+
status: "error",
|
|
373
|
+
message: err instanceof Error ? err.message : "\u8BED\u97F3\u4EFB\u52A1\u6267\u884C\u5931\u8D25"
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
buildRecognition() {
|
|
378
|
+
const runtimeWindow = window;
|
|
379
|
+
const Ctor = runtimeWindow.SpeechRecognition || runtimeWindow.webkitSpeechRecognition;
|
|
380
|
+
if (!Ctor) return null;
|
|
381
|
+
const recognition = new Ctor();
|
|
382
|
+
recognition.lang = this.options.recognitionLang;
|
|
383
|
+
recognition.continuous = true;
|
|
384
|
+
recognition.interimResults = true;
|
|
385
|
+
recognition.maxAlternatives = 3;
|
|
386
|
+
recognition.onstart = () => {
|
|
387
|
+
if (!this.voiceEnabled) return;
|
|
388
|
+
this.patchState({
|
|
389
|
+
status: "waking",
|
|
390
|
+
message: `\u8BED\u97F3\u5DF2\u5F00\u542F\uFF0C\u8BF7\u8BF4\u201C${this.options.wakeWord[0]}\u201D\u5524\u9192\u52A9\u624B`
|
|
391
|
+
});
|
|
392
|
+
};
|
|
393
|
+
recognition.onresult = (event) => {
|
|
394
|
+
var _a;
|
|
395
|
+
for (let i = event.resultIndex; i < event.results.length; i += 1) {
|
|
396
|
+
const result = event.results[i];
|
|
397
|
+
const isFinal = Boolean(result == null ? void 0 : result.isFinal);
|
|
398
|
+
const transcript = String(((_a = result[0]) == null ? void 0 : _a.transcript) || "").trim();
|
|
399
|
+
if (!transcript) continue;
|
|
400
|
+
if (!this.awaitingCommand) {
|
|
401
|
+
if (!this.isWakePhraseHit(transcript)) continue;
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
if (now - this.lastWakeAt < this.options.wakeCooldownMs) continue;
|
|
404
|
+
this.lastWakeAt = now;
|
|
405
|
+
this.awaitingCommand = true;
|
|
406
|
+
this.commandFinalText = "";
|
|
407
|
+
this.commandInterimText = "";
|
|
408
|
+
this.patchState({
|
|
409
|
+
status: "listening_command",
|
|
410
|
+
message: "\u5DF2\u5524\u9192\uFF0C\u8BF7\u8BF4\u51FA\u4F60\u7684\u6307\u4EE4\uFF08\u53EF\u8FDE\u7EED\u8BF4\u66F4\u957F\u5185\u5BB9\uFF09"
|
|
411
|
+
});
|
|
412
|
+
if (this.options.showAgentWhenWake) {
|
|
413
|
+
void this.ensureAgent();
|
|
414
|
+
}
|
|
415
|
+
const inlineCommand = isFinal ? this.extractCommandAfterWake(transcript) : "";
|
|
416
|
+
if (inlineCommand) {
|
|
417
|
+
this.commandFinalText = inlineCommand;
|
|
418
|
+
this.scheduleFlushCommand(this.options.commandSilenceTimeoutMs);
|
|
419
|
+
} else {
|
|
420
|
+
this.scheduleFlushCommand(this.options.commandInitialTimeoutMs);
|
|
421
|
+
}
|
|
422
|
+
this.scheduleCommandDeadline();
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (isFinal) {
|
|
426
|
+
this.commandFinalText = `${this.commandFinalText} ${transcript}`.trim();
|
|
427
|
+
this.commandInterimText = "";
|
|
428
|
+
} else {
|
|
429
|
+
this.commandInterimText = transcript;
|
|
430
|
+
}
|
|
431
|
+
this.patchState({
|
|
432
|
+
status: "listening_command",
|
|
433
|
+
message: `\u6B63\u5728\u8BC6\u522B\uFF1A${this.composeCommandText() || "\u8BF7\u7EE7\u7EED\u8BF4\u8BDD..."}`
|
|
434
|
+
});
|
|
435
|
+
this.scheduleFlushCommand(this.options.commandSilenceTimeoutMs);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
recognition.onerror = (event) => {
|
|
439
|
+
const code = String(event.error || "");
|
|
440
|
+
if (code === "no-speech" || code === "aborted") return;
|
|
441
|
+
if (code === "not-allowed" || code === "service-not-allowed") {
|
|
442
|
+
this.voiceEnabled = false;
|
|
443
|
+
this.patchState({
|
|
444
|
+
status: "error",
|
|
445
|
+
enabled: false,
|
|
446
|
+
micPermissionGranted: false,
|
|
447
|
+
message: "\u9EA6\u514B\u98CE\u6743\u9650\u88AB\u62D2\u7EDD\uFF0C\u8BF7\u5728\u6D4F\u89C8\u5668\u8BBE\u7F6E\u4E2D\u5141\u8BB8\u9EA6\u514B\u98CE"
|
|
448
|
+
});
|
|
449
|
+
window.localStorage.setItem("voice-page-agent-enabled", "0");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this.patchState({
|
|
453
|
+
status: "error",
|
|
454
|
+
message: `\u8BED\u97F3\u8BC6\u522B\u5F02\u5E38\uFF1A${code || "\u672A\u77E5\u9519\u8BEF"}`
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
recognition.onend = () => {
|
|
458
|
+
if (!this.voiceEnabled || this.disposed) return;
|
|
459
|
+
if (this.restartBusy) return;
|
|
460
|
+
this.restartBusy = true;
|
|
461
|
+
window.setTimeout(() => {
|
|
462
|
+
this.restartBusy = false;
|
|
463
|
+
if (!this.voiceEnabled || this.disposed) return;
|
|
464
|
+
try {
|
|
465
|
+
recognition.start();
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
}, 320);
|
|
469
|
+
};
|
|
470
|
+
return recognition;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
function createVoicePageAgent(options) {
|
|
474
|
+
return new VoicePageAgentController(options);
|
|
475
|
+
}
|
|
476
|
+
var VOICE_PAGE_AGENT_KEY = "VOICE_PAGE_AGENT_INSTANCE";
|
|
477
|
+
var defaultController = null;
|
|
478
|
+
function attachToApp(target, controller) {
|
|
479
|
+
var _a, _b, _c, _d, _e;
|
|
480
|
+
const anyTarget = target;
|
|
481
|
+
if ("config" in anyTarget && ((_a = anyTarget.config) == null ? void 0 : _a.globalProperties)) {
|
|
482
|
+
anyTarget.config.globalProperties.$voicePageAgent = controller;
|
|
483
|
+
(_b = anyTarget.provide) == null ? void 0 : _b.call(anyTarget, VOICE_PAGE_AGENT_KEY, controller);
|
|
484
|
+
(_c = anyTarget.component) == null ? void 0 : _c.call(anyTarget, "VoicePageAgentButton", VoicePageAgentButton);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
anyTarget.prototype.$voicePageAgent = controller;
|
|
488
|
+
(_d = anyTarget.component) == null ? void 0 : _d.call(anyTarget, "VoicePageAgentButton", VoicePageAgentButton);
|
|
489
|
+
(_e = anyTarget.mixin) == null ? void 0 : _e.call(anyTarget, {
|
|
490
|
+
provide() {
|
|
491
|
+
return {
|
|
492
|
+
[VOICE_PAGE_AGENT_KEY]: controller
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function useVoicePageAgent() {
|
|
498
|
+
const injected = inject(VOICE_PAGE_AGENT_KEY, null);
|
|
499
|
+
if (injected) return injected;
|
|
500
|
+
const instance = getCurrentInstance();
|
|
501
|
+
const proxy = instance == null ? void 0 : instance.proxy;
|
|
502
|
+
if (proxy == null ? void 0 : proxy.$voicePageAgent) return proxy.$voicePageAgent;
|
|
503
|
+
if (defaultController) return defaultController;
|
|
504
|
+
throw new Error("voice-page-agent: controller not found, please install plugin first.");
|
|
505
|
+
}
|
|
506
|
+
var VoicePageAgentButton = defineComponent({
|
|
507
|
+
name: "VoicePageAgentButton",
|
|
508
|
+
props: {
|
|
509
|
+
showStatus: {
|
|
510
|
+
type: Boolean,
|
|
511
|
+
default: true
|
|
512
|
+
},
|
|
513
|
+
startText: {
|
|
514
|
+
type: String,
|
|
515
|
+
default: "\u5F00\u542F\u8BED\u97F3\u5524\u9192"
|
|
516
|
+
},
|
|
517
|
+
wakeOnText: {
|
|
518
|
+
type: String,
|
|
519
|
+
default: "\u8BED\u97F3\u5524\u9192\u4E2D"
|
|
520
|
+
},
|
|
521
|
+
openText: {
|
|
522
|
+
type: String,
|
|
523
|
+
default: "\u7F51\u9875\u52A9\u624B"
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
setup(props) {
|
|
527
|
+
const controller = useVoicePageAgent();
|
|
528
|
+
const state = ref(controller.snapshot);
|
|
529
|
+
let off = null;
|
|
530
|
+
onMounted(() => {
|
|
531
|
+
off = controller.onStateChange((next) => {
|
|
532
|
+
state.value = next;
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
onBeforeUnmount(() => {
|
|
536
|
+
off == null ? void 0 : off();
|
|
537
|
+
off = null;
|
|
538
|
+
});
|
|
539
|
+
const handleWakeClick = () => {
|
|
540
|
+
if (state.value.enabled) {
|
|
541
|
+
controller.stopWake();
|
|
542
|
+
} else {
|
|
543
|
+
void controller.startWake();
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const handleOpenClick = () => {
|
|
547
|
+
void controller.openAgent();
|
|
548
|
+
};
|
|
549
|
+
return () => h("div", { class: "voice-page-agent-root" }, [
|
|
550
|
+
h("div", { class: "voice-page-agent-actions" }, [
|
|
551
|
+
state.value.supported && !state.value.micPermissionGranted ? h(
|
|
552
|
+
"button",
|
|
553
|
+
{
|
|
554
|
+
type: "button",
|
|
555
|
+
class: "voice-page-agent-btn",
|
|
556
|
+
onClick: handleWakeClick
|
|
557
|
+
},
|
|
558
|
+
state.value.enabled ? props.wakeOnText : props.startText
|
|
559
|
+
) : null,
|
|
560
|
+
h(
|
|
561
|
+
"button",
|
|
562
|
+
{
|
|
563
|
+
type: "button",
|
|
564
|
+
class: "voice-page-agent-btn",
|
|
565
|
+
onClick: handleOpenClick
|
|
566
|
+
},
|
|
567
|
+
props.openText
|
|
568
|
+
)
|
|
569
|
+
]),
|
|
570
|
+
props.showStatus ? h("p", { class: "voice-page-agent-status" }, state.value.message) : null
|
|
571
|
+
]);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
function createVoicePageAgentPlugin(options) {
|
|
575
|
+
const controller = createVoicePageAgent(options);
|
|
576
|
+
defaultController = controller;
|
|
577
|
+
return {
|
|
578
|
+
install(app) {
|
|
579
|
+
attachToApp(app, controller);
|
|
580
|
+
},
|
|
581
|
+
controller
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
var VoicePageAgentVuePlugin = {
|
|
585
|
+
install(app, options) {
|
|
586
|
+
if (!options) {
|
|
587
|
+
throw new Error("voice-page-agent: install options is required.");
|
|
588
|
+
}
|
|
589
|
+
const plugin = createVoicePageAgentPlugin(options);
|
|
590
|
+
VoicePageAgentVuePlugin.controller = plugin.controller;
|
|
591
|
+
attachToApp(app, plugin.controller);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var plugin_default = VoicePageAgentVuePlugin;
|
|
595
|
+
|
|
596
|
+
export { VoicePageAgentButton, VoicePageAgentController, plugin_default as VoicePageAgentPlugin, createVoicePageAgent, createVoicePageAgentPlugin, useVoicePageAgent };
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voice-page-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Voice wake plugin for page-agent with Vue2/Vue3 compatibility.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"prepack": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"voice",
|
|
28
|
+
"wake-word",
|
|
29
|
+
"page-agent",
|
|
30
|
+
"vue2",
|
|
31
|
+
"vue3"
|
|
32
|
+
],
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"page-agent": ">=0.2.0",
|
|
35
|
+
"vue": "^2.6.14 || ^3.2.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"page-agent": {
|
|
39
|
+
"optional": false
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"vue-demi": "^0.14.10"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.1",
|
|
47
|
+
"tsup": "^8.4.0",
|
|
48
|
+
"typescript": "^5.7.2"
|
|
49
|
+
}
|
|
50
|
+
}
|