pi-meta-oauth 0.4.2 → 0.4.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/README.md +40 -3
- package/extensions/media/files.ts +131 -0
- package/extensions/media/limits.ts +12 -0
- package/extensions/media/mime.ts +178 -0
- package/extensions/media/payload.ts +303 -0
- package/extensions/media/responses.ts +186 -0
- package/extensions/media-output.ts +127 -0
- package/extensions/media.ts +352 -1245
- package/extensions/meta.ts +181 -42
- package/extensions/voice/asr.ts +107 -0
- package/extensions/voice/helpers.ts +243 -0
- package/extensions/voice/macos-audio.swift +6 -16
- package/extensions/voice.ts +27 -333
- package/package.json +1 -1
|
@@ -73,22 +73,12 @@ private final class MicrophoneSession {
|
|
|
73
73
|
}
|
|
74
74
|
let tapBufferSize = AVAudioFrameCount(inputFormat.sampleRate * 0.1)
|
|
75
75
|
do {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
block: tap
|
|
83
|
-
)
|
|
84
|
-
} else {
|
|
85
|
-
inputNode.installTap(
|
|
86
|
-
onBus: 0,
|
|
87
|
-
bufferSize: tapBufferSize,
|
|
88
|
-
format: inputFormat,
|
|
89
|
-
block: tap
|
|
90
|
-
)
|
|
91
|
-
}
|
|
76
|
+
inputNode.installTap(
|
|
77
|
+
onBus: 0,
|
|
78
|
+
bufferSize: tapBufferSize,
|
|
79
|
+
format: inputFormat,
|
|
80
|
+
block: tap
|
|
81
|
+
)
|
|
92
82
|
audioEngine.prepare()
|
|
93
83
|
try audioEngine.start()
|
|
94
84
|
} catch {
|
package/extensions/voice.ts
CHANGED
|
@@ -6,33 +6,29 @@ import { META_PROVIDER_ID } from "./meta.ts";
|
|
|
6
6
|
import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
9
12
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
asrEndpoint,
|
|
14
|
+
chooseTranscript,
|
|
15
|
+
DEFAULT_ASR_MODEL,
|
|
16
|
+
formatAuthorization,
|
|
17
|
+
isNormalSocketClose,
|
|
18
|
+
parseJsonObject,
|
|
19
|
+
pcmAudioLevel,
|
|
20
|
+
socketMessageText,
|
|
21
|
+
} from "./voice/asr.ts";
|
|
22
|
+
import {
|
|
23
|
+
ensureHelper,
|
|
24
|
+
isSupportedPlatform,
|
|
25
|
+
supportedPlatformLabel,
|
|
26
|
+
} from "./voice/helpers.ts";
|
|
27
|
+
|
|
28
|
+
export { asrEndpoint, formatAuthorization, pcmAudioLevel } from "./voice/asr.ts";
|
|
19
29
|
|
|
20
30
|
const AGENT_DIR = join(homedir(), ".pi/agent");
|
|
21
31
|
const SETTINGS_FILE = join(AGENT_DIR, "pi-meta-oauth-voice.json");
|
|
22
|
-
const EXTENSION_DIR = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const HELPER_DIR = join(EXTENSION_DIR, "voice");
|
|
24
|
-
const HELPER_SOURCE = join(HELPER_DIR, "macos-audio.swift");
|
|
25
|
-
const HELPER_PLIST = join(HELPER_DIR, "Info.plist");
|
|
26
|
-
const HELPER_ENTITLEMENTS = join(HELPER_DIR, "Entitlements.plist");
|
|
27
|
-
const HELPER_BINARY = join(AGENT_DIR, "bin/pi-meta-oauth-voice-v1");
|
|
28
|
-
// Windows helpers
|
|
29
|
-
const WINDOWS_HELPER_SOURCE = join(HELPER_DIR, "windows-audio.cs");
|
|
30
|
-
const WINDOWS_HELPER_PS1 = join(HELPER_DIR, "windows-audio.ps1");
|
|
31
|
-
const WINDOWS_HELPER_BINARY = join(AGENT_DIR, "bin/pi-meta-oauth-voice-v1.exe");
|
|
32
|
-
|
|
33
|
-
const DEFAULT_ASR_ENDPOINT =
|
|
34
|
-
"wss://shortwave.facebook.com/voyager/v1/asr/duplex";
|
|
35
|
-
const DEFAULT_ASR_MODEL = "prod_tbh";
|
|
36
32
|
const AUDIO_FRAME_BYTES = 3_200;
|
|
37
33
|
const ASR_PREBUFFER_FRAMES = 6;
|
|
38
34
|
const MAX_QUEUED_AUDIO_BYTES = 2 * 1024 * 1024;
|
|
@@ -43,30 +39,11 @@ const FINAL_TRANSCRIPT_TIMEOUT_MS = 6_000;
|
|
|
43
39
|
type Settings = { enabled: boolean };
|
|
44
40
|
type VoicePhase = "idle" | "preparing" | "recording" | "stopping";
|
|
45
41
|
type HelperEvent = Record<string, unknown> & { type?: string };
|
|
46
|
-
|
|
47
42
|
type AsrTranscript = {
|
|
48
43
|
transcript?: unknown;
|
|
49
44
|
final?: unknown;
|
|
50
45
|
};
|
|
51
46
|
|
|
52
|
-
function isMacOS(): boolean {
|
|
53
|
-
return platform() === "darwin";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function isWindows(): boolean {
|
|
57
|
-
return platform() === "win32";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function isSupportedPlatform(): boolean {
|
|
61
|
-
return isMacOS() || isWindows();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function supportedPlatformLabel(): string {
|
|
65
|
-
if (isMacOS()) return "macOS";
|
|
66
|
-
if (isWindows()) return "Windows";
|
|
67
|
-
return `${platform()}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
47
|
function loadSettings(): Settings {
|
|
71
48
|
if (existsSync(SETTINGS_FILE)) {
|
|
72
49
|
try {
|
|
@@ -92,295 +69,6 @@ function combineEditorText(original: string, transcript: string): string {
|
|
|
92
69
|
return `${original.replace(/\s+$/, "")} ${transcript.trim()}`;
|
|
93
70
|
}
|
|
94
71
|
|
|
95
|
-
export function pcmAudioLevel(chunk: Buffer): number {
|
|
96
|
-
if (chunk.length < 2) return 0;
|
|
97
|
-
let sumSquares = 0;
|
|
98
|
-
let samples = 0;
|
|
99
|
-
for (let offset = 0; offset + 1 < chunk.length; offset += 4) {
|
|
100
|
-
const normalized = chunk.readInt16LE(offset) / 32_768;
|
|
101
|
-
sumSquares += normalized * normalized;
|
|
102
|
-
samples += 1;
|
|
103
|
-
}
|
|
104
|
-
if (samples === 0) return 0;
|
|
105
|
-
const rms = Math.sqrt(sumSquares / samples);
|
|
106
|
-
return Math.min(1, Math.log10(1 + rms * 90));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function hypothesisWords(value: string): string[] {
|
|
110
|
-
return value
|
|
111
|
-
.trim()
|
|
112
|
-
.split(/\s+/)
|
|
113
|
-
.map((word) => word.toLocaleLowerCase().replace(/[^\p{L}\p{N}'’]/gu, ""))
|
|
114
|
-
.filter(Boolean);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function chooseTranscript(previous: string, incoming: string): string {
|
|
118
|
-
const next = incoming.trim();
|
|
119
|
-
if (!next) return previous;
|
|
120
|
-
if (!previous.trim()) return next;
|
|
121
|
-
|
|
122
|
-
const previousWords = hypothesisWords(previous);
|
|
123
|
-
const nextWords = hypothesisWords(next);
|
|
124
|
-
const sharedLength = Math.min(previousWords.length, nextWords.length);
|
|
125
|
-
let commonPrefix = 0;
|
|
126
|
-
while (
|
|
127
|
-
commonPrefix < sharedLength &&
|
|
128
|
-
previousWords[commonPrefix] === nextWords[commonPrefix]
|
|
129
|
-
) {
|
|
130
|
-
commonPrefix += 1;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (commonPrefix === sharedLength || commonPrefix >= 2) {
|
|
134
|
-
const isRegression =
|
|
135
|
-
nextWords.length < previousWords.length &&
|
|
136
|
-
nextWords.every((word, index) => previousWords[index] === word);
|
|
137
|
-
return isRegression ? previous : next;
|
|
138
|
-
}
|
|
139
|
-
return next.length >= previous.length ? next : previous;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function formatAuthorization(apiKey: string): string {
|
|
143
|
-
return apiKey.startsWith("OAuth ") ? apiKey : `OAuth ${apiKey}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function asrEndpoint(sessionId: string): string {
|
|
147
|
-
const configured =
|
|
148
|
-
process.env.PI_META_VOICE_ASR_ENDPOINT ??
|
|
149
|
-
process.env.MUSE_VOICE_ASR_ENDPOINT ??
|
|
150
|
-
DEFAULT_ASR_ENDPOINT;
|
|
151
|
-
let endpoint: URL;
|
|
152
|
-
try {
|
|
153
|
-
endpoint = new URL(configured);
|
|
154
|
-
} catch {
|
|
155
|
-
throw new Error("Voice ASR endpoint is not a valid URL");
|
|
156
|
-
}
|
|
157
|
-
const localInsecureEndpoint =
|
|
158
|
-
endpoint.protocol === "ws:" &&
|
|
159
|
-
(endpoint.hostname === "localhost" || endpoint.hostname === "127.0.0.1");
|
|
160
|
-
if (endpoint.protocol !== "wss:" && !localInsecureEndpoint) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
"Voice ASR endpoint must use wss:// (or ws://localhost for testing)",
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
endpoint.searchParams.set("sessionId", sessionId);
|
|
166
|
-
return endpoint.toString();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function socketMessageText(data: unknown): Promise<string | undefined> {
|
|
170
|
-
if (typeof data === "string") return data;
|
|
171
|
-
if (data instanceof Blob) return data.text();
|
|
172
|
-
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
|
|
173
|
-
if (ArrayBuffer.isView(data)) {
|
|
174
|
-
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString(
|
|
175
|
-
"utf8",
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function parseJsonObject(text: string): Record<string, unknown> | undefined {
|
|
182
|
-
try {
|
|
183
|
-
const value = JSON.parse(text) as unknown;
|
|
184
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
185
|
-
return value as Record<string, unknown>;
|
|
186
|
-
}
|
|
187
|
-
} catch {
|
|
188
|
-
// Ignore non-JSON ASR messages.
|
|
189
|
-
}
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function runProcess(command: string, args: string[]): Promise<void> {
|
|
194
|
-
await new Promise<void>((resolve, reject) => {
|
|
195
|
-
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
196
|
-
let stdout = "";
|
|
197
|
-
let stderr = "";
|
|
198
|
-
child.stdout?.on("data", (chunk: Buffer) => {
|
|
199
|
-
stdout = `${stdout}${chunk.toString("utf8")}`.slice(-8_000);
|
|
200
|
-
});
|
|
201
|
-
child.stderr?.on("data", (chunk: Buffer) => {
|
|
202
|
-
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-8_000);
|
|
203
|
-
});
|
|
204
|
-
child.once("error", reject);
|
|
205
|
-
child.once("close", (code) => {
|
|
206
|
-
if (code === 0) {
|
|
207
|
-
resolve();
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
reject(
|
|
211
|
-
new Error(
|
|
212
|
-
(
|
|
213
|
-
stderr ||
|
|
214
|
-
stdout ||
|
|
215
|
-
`${command} exited with code ${code ?? "unknown"}`
|
|
216
|
-
).trim(),
|
|
217
|
-
),
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function helperNeedsBuild(): boolean {
|
|
224
|
-
if (!existsSync(HELPER_BINARY)) return true;
|
|
225
|
-
const binaryTime = statSync(HELPER_BINARY).mtimeMs;
|
|
226
|
-
return [HELPER_SOURCE, HELPER_PLIST, HELPER_ENTITLEMENTS].some(
|
|
227
|
-
(path) => !existsSync(path) || statSync(path).mtimeMs > binaryTime,
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function windowsHelperNeedsBuild(): boolean {
|
|
232
|
-
if (!existsSync(WINDOWS_HELPER_BINARY)) return true;
|
|
233
|
-
if (!existsSync(WINDOWS_HELPER_SOURCE)) return false;
|
|
234
|
-
const binaryTime = statSync(WINDOWS_HELPER_BINARY).mtimeMs;
|
|
235
|
-
return statSync(WINDOWS_HELPER_SOURCE).mtimeMs > binaryTime;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function ensureMacOSHelper(): Promise<string> {
|
|
239
|
-
if (!isMacOS()) {
|
|
240
|
-
throw new Error(
|
|
241
|
-
"Muse-style voice input is currently available only on macOS and Windows",
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
if (
|
|
245
|
-
!existsSync(HELPER_SOURCE) ||
|
|
246
|
-
!existsSync(HELPER_PLIST) ||
|
|
247
|
-
!existsSync(HELPER_ENTITLEMENTS)
|
|
248
|
-
) {
|
|
249
|
-
throw new Error("The macOS microphone helper sources are incomplete");
|
|
250
|
-
}
|
|
251
|
-
if (!helperNeedsBuild()) return HELPER_BINARY;
|
|
252
|
-
|
|
253
|
-
mkdirSync(join(AGENT_DIR, "bin"), { recursive: true });
|
|
254
|
-
await runProcess("/usr/bin/xcrun", [
|
|
255
|
-
"swiftc",
|
|
256
|
-
HELPER_SOURCE,
|
|
257
|
-
"-o",
|
|
258
|
-
HELPER_BINARY,
|
|
259
|
-
"-framework",
|
|
260
|
-
"AVFoundation",
|
|
261
|
-
"-Xlinker",
|
|
262
|
-
"-sectcreate",
|
|
263
|
-
"-Xlinker",
|
|
264
|
-
"__TEXT",
|
|
265
|
-
"-Xlinker",
|
|
266
|
-
"__info_plist",
|
|
267
|
-
"-Xlinker",
|
|
268
|
-
HELPER_PLIST,
|
|
269
|
-
]);
|
|
270
|
-
await runProcess("/usr/bin/codesign", [
|
|
271
|
-
"--force",
|
|
272
|
-
"--sign",
|
|
273
|
-
"-",
|
|
274
|
-
"--entitlements",
|
|
275
|
-
HELPER_ENTITLEMENTS,
|
|
276
|
-
HELPER_BINARY,
|
|
277
|
-
]);
|
|
278
|
-
return HELPER_BINARY;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function tryCompileWindowsHelper(): Promise<boolean> {
|
|
282
|
-
if (!existsSync(WINDOWS_HELPER_SOURCE)) return false;
|
|
283
|
-
mkdirSync(join(AGENT_DIR, "bin"), { recursive: true });
|
|
284
|
-
|
|
285
|
-
const candidates: string[] = [
|
|
286
|
-
"csc",
|
|
287
|
-
"csc.exe",
|
|
288
|
-
"C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\csc.exe",
|
|
289
|
-
"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe",
|
|
290
|
-
];
|
|
291
|
-
// Also try where-found csc via PATH expansion if on Windows
|
|
292
|
-
for (const c of candidates) {
|
|
293
|
-
try {
|
|
294
|
-
await runProcess(c, [
|
|
295
|
-
"/nologo",
|
|
296
|
-
"/target:exe",
|
|
297
|
-
`/out:${WINDOWS_HELPER_BINARY}`,
|
|
298
|
-
WINDOWS_HELPER_SOURCE,
|
|
299
|
-
]);
|
|
300
|
-
if (existsSync(WINDOWS_HELPER_BINARY)) return true;
|
|
301
|
-
} catch {
|
|
302
|
-
// try next candidate
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function findPowerShellCommand(): string {
|
|
309
|
-
// Prefer pwsh (PowerShell 7) if present, otherwise Windows PowerShell
|
|
310
|
-
// We check filesystem first to avoid spawning during hot path
|
|
311
|
-
if (existsSync("C:\\Program Files\\PowerShell\\7\\pwsh.exe"))
|
|
312
|
-
return "C:\\Program Files\\PowerShell\\7\\pwsh.exe";
|
|
313
|
-
if (existsSync("C:\\Program Files\\PowerShell\\7\\pwsh")) return "pwsh";
|
|
314
|
-
// Windows PowerShell is inbox in System32
|
|
315
|
-
if (
|
|
316
|
-
existsSync("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
|
|
317
|
-
)
|
|
318
|
-
return "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
319
|
-
return "powershell.exe";
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function ensureWindowsHelper(): Promise<{
|
|
323
|
-
command: string;
|
|
324
|
-
args: string[];
|
|
325
|
-
}> {
|
|
326
|
-
if (!isWindows()) {
|
|
327
|
-
throw new Error(
|
|
328
|
-
"Muse-style voice input is currently available only on macOS and Windows",
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
if (!existsSync(WINDOWS_HELPER_SOURCE) && !existsSync(WINDOWS_HELPER_PS1)) {
|
|
332
|
-
throw new Error("The Windows microphone helper sources are incomplete");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// If compiled exe exists and is fresh, use it
|
|
336
|
-
if (existsSync(WINDOWS_HELPER_BINARY) && !windowsHelperNeedsBuild()) {
|
|
337
|
-
return { command: WINDOWS_HELPER_BINARY, args: [] };
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Try to compile exe for better performance
|
|
341
|
-
if (existsSync(WINDOWS_HELPER_SOURCE)) {
|
|
342
|
-
if (!windowsHelperNeedsBuild() && existsSync(WINDOWS_HELPER_BINARY)) {
|
|
343
|
-
return { command: WINDOWS_HELPER_BINARY, args: [] };
|
|
344
|
-
}
|
|
345
|
-
const compiled = await tryCompileWindowsHelper();
|
|
346
|
-
if (compiled && existsSync(WINDOWS_HELPER_BINARY)) {
|
|
347
|
-
return { command: WINDOWS_HELPER_BINARY, args: [] };
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Fallback to PowerShell script (no compilation needed)
|
|
352
|
-
if (existsSync(WINDOWS_HELPER_PS1)) {
|
|
353
|
-
const ps = findPowerShellCommand();
|
|
354
|
-
return {
|
|
355
|
-
command: ps,
|
|
356
|
-
args: [
|
|
357
|
-
"-NoProfile",
|
|
358
|
-
"-ExecutionPolicy",
|
|
359
|
-
"Bypass",
|
|
360
|
-
"-File",
|
|
361
|
-
WINDOWS_HELPER_PS1,
|
|
362
|
-
],
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
throw new Error(
|
|
367
|
-
"Windows voice helper could not be prepared — no compiled binary and no PowerShell fallback found",
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async function ensureHelper(): Promise<{ command: string; args: string[] }> {
|
|
372
|
-
if (isMacOS()) {
|
|
373
|
-
const bin = await ensureMacOSHelper();
|
|
374
|
-
return { command: bin, args: [] };
|
|
375
|
-
}
|
|
376
|
-
if (isWindows()) {
|
|
377
|
-
return ensureWindowsHelper();
|
|
378
|
-
}
|
|
379
|
-
throw new Error(
|
|
380
|
-
`Muse-style voice input is currently available only on macOS and Windows (current: ${supportedPlatformLabel()})`,
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
72
|
class MetaVoiceController {
|
|
385
73
|
private readonly pi: ExtensionAPI;
|
|
386
74
|
private readonly settings = loadSettings();
|
|
@@ -620,7 +308,7 @@ class MetaVoiceController {
|
|
|
620
308
|
});
|
|
621
309
|
socket.addEventListener("message", (event) => {
|
|
622
310
|
if (currentGeneration !== this.generation) return;
|
|
623
|
-
void this.handleSocketMessage(ctx, event.data);
|
|
311
|
+
void this.handleSocketMessage(ctx, event.data, currentGeneration);
|
|
624
312
|
});
|
|
625
313
|
socket.addEventListener("error", () => {
|
|
626
314
|
if (currentGeneration !== this.generation) return;
|
|
@@ -629,7 +317,11 @@ class MetaVoiceController {
|
|
|
629
317
|
socket.addEventListener("close", (event) => {
|
|
630
318
|
if (currentGeneration !== this.generation || this.phase === "idle")
|
|
631
319
|
return;
|
|
632
|
-
if (
|
|
320
|
+
if (
|
|
321
|
+
this.phase === "stopping" &&
|
|
322
|
+
this.transcript &&
|
|
323
|
+
isNormalSocketClose(event.code)
|
|
324
|
+
) {
|
|
633
325
|
this.complete(ctx);
|
|
634
326
|
return;
|
|
635
327
|
}
|
|
@@ -646,8 +338,10 @@ class MetaVoiceController {
|
|
|
646
338
|
private async handleSocketMessage(
|
|
647
339
|
ctx: ExtensionContext,
|
|
648
340
|
data: unknown,
|
|
341
|
+
currentGeneration: number,
|
|
649
342
|
): Promise<void> {
|
|
650
343
|
const text = await socketMessageText(data);
|
|
344
|
+
if (currentGeneration !== this.generation) return;
|
|
651
345
|
if (text === undefined) return;
|
|
652
346
|
const payload = parseJsonObject(text);
|
|
653
347
|
if (!payload) return;
|