pi-meta-oauth 0.4.4 → 0.5.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 +14 -3
- package/extensions/media/responses.ts +14 -8
- package/extensions/media.ts +321 -129
- package/extensions/meta.ts +87 -2
- package/extensions/voice/asr.ts +25 -1
- package/extensions/voice.ts +12 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
Meta Model API OAuth and Muse-style voice input for [pi](https://pi.dev).
|
|
10
10
|
|
|
11
|
-
- Use Muse Spark models through Pi's `openai-responses` provider
|
|
11
|
+
- Use Muse Spark models through Pi's `openai-responses` provider (not `/chat/completions` — Muse prompt cache is ~0% there)
|
|
12
|
+
- Send `prompt_cache_retention: "24h"` on Meta Responses requests unless the payload already set a retention
|
|
12
13
|
- Device authorization against `https://auth.meta.com`
|
|
13
14
|
- Model API-key minting through `POST https://api.meta.ai/muse-code/key`
|
|
14
15
|
- Dynamic Muse model catalog from `GET https://api.meta.ai/v1/models`
|
|
@@ -87,11 +88,13 @@ Fallback models use a 1,048,576-token context window, up to 256K output tokens,
|
|
|
87
88
|
|
|
88
89
|
| id | pricing (input/output/cached) $/M |
|
|
89
90
|
| --- | --- |
|
|
91
|
+
| `muse-spark-1.3` | 1.25 / 4.25 / 0.15 |
|
|
92
|
+
| `muse-spark-1.3-contributor` | 0.10 / 0.20 / 0.002 |
|
|
90
93
|
| `muse-spark-1.2` | 1.25 / 4.25 / 0.15 |
|
|
91
94
|
| `muse-spark-1.2-contributor` | 0.10 / 0.20 / 0.002 |
|
|
92
95
|
| `muse-spark-1.1` | 1.25 / 4.25 / 0.15 |
|
|
93
96
|
|
|
94
|
-
> **Contributor-model privacy:**
|
|
97
|
+
> **Contributor-model privacy:** discounted contributor models allow Meta to use prompts and completions for product improvement, including training future Meta models. Use a standard model such as `muse-spark-1.3` if you do not want the contributor terms. See [Meta's model documentation](https://dev.meta.ai/docs/models).
|
|
95
98
|
|
|
96
99
|
To scope Pi's model picker to Meta models:
|
|
97
100
|
|
|
@@ -122,11 +125,19 @@ catalog.
|
|
|
122
125
|
|
|
123
126
|
```bash
|
|
124
127
|
pi --list-models meta
|
|
125
|
-
pi -p --provider meta --model muse-spark-1.
|
|
128
|
+
pi -p --provider meta --model muse-spark-1.3 "Reply exactly: META_OK"
|
|
126
129
|
bun run typecheck
|
|
127
130
|
bun test
|
|
128
131
|
```
|
|
129
132
|
|
|
133
|
+
`bun test` is hermetic unless a Meta credential is already available. The live cache-hit probe makes real billable API calls when a credential resolves: two identical `/v1/responses` calls (asserting `cached_tokens` on the second), plus one 2s retry if that second call misses cache. The credential is resolved, in order, from `PI_META_LIVE_API_KEY`, `META_API_KEY`, `MODEL_API_KEY`, or the minted key from `~/.pi/agent/auth.json` after `/login meta` (skipped if expired). OAuth is enough — you do not need a separate key. Skipped when no valid credential exists (CI):
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bun test tests/meta-cache.test.ts
|
|
137
|
+
# or, if you are not logged in:
|
|
138
|
+
PI_META_LIVE_API_KEY='LLM|...' bun test tests/meta-cache.test.ts
|
|
139
|
+
```
|
|
140
|
+
|
|
130
141
|
## Publish to npm and pi.dev
|
|
131
142
|
|
|
132
143
|
The `pi-package` keyword makes the package discoverable at <https://pi.dev/packages>. Publishing is handled by `.github/workflows/publish.yml` when a `v*` tag is pushed. Each release publishes the canonical `pi-meta-oauth` package to npm and a scoped `@blockedpath/pi-meta-oauth` mirror to GitHub Packages.
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applyMetaResponsesCacheHints,
|
|
4
|
+
META_API_BASE_URL,
|
|
5
|
+
metaFallbackCost,
|
|
6
|
+
} from "../meta.ts";
|
|
3
7
|
|
|
4
8
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
5
9
|
return value !== null && typeof value === "object"
|
|
@@ -20,7 +24,11 @@ function tokenCount(...values: unknown[]): number | undefined {
|
|
|
20
24
|
return undefined;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
function responsesErrorDetail(
|
|
27
|
+
function responsesErrorDetail(
|
|
28
|
+
json: unknown,
|
|
29
|
+
text: string,
|
|
30
|
+
statusText: string,
|
|
31
|
+
): string {
|
|
24
32
|
const body = record(json);
|
|
25
33
|
if (body?.error !== undefined) {
|
|
26
34
|
return JSON.stringify(body.error).slice(0, 1000);
|
|
@@ -77,10 +85,8 @@ export function extractMetaResponseUsage(
|
|
|
77
85
|
const outputTokens = output ?? 0;
|
|
78
86
|
const reasoning = Math.min(
|
|
79
87
|
outputTokens,
|
|
80
|
-
tokenCount(
|
|
81
|
-
|
|
82
|
-
outputDetails?.reasoningTokens,
|
|
83
|
-
) ?? 0,
|
|
88
|
+
tokenCount(outputDetails?.reasoning_tokens, outputDetails?.reasoningTokens) ??
|
|
89
|
+
0,
|
|
84
90
|
);
|
|
85
91
|
const totalTokens =
|
|
86
92
|
tokenCount(usage.total_tokens, usage.totalTokens) ??
|
|
@@ -127,8 +133,7 @@ export function extractResponsesText(json: unknown): string {
|
|
|
127
133
|
if (Array.isArray(output)) {
|
|
128
134
|
const texts: string[] = [];
|
|
129
135
|
for (const item of output as Record<string, unknown>[]) {
|
|
130
|
-
if (!item || typeof item !== "object" || item.type !== "message")
|
|
131
|
-
continue;
|
|
136
|
+
if (!item || typeof item !== "object" || item.type !== "message") continue;
|
|
132
137
|
if (typeof item.content === "string" && item.content.trim())
|
|
133
138
|
texts.push(item.content);
|
|
134
139
|
if (Array.isArray(item.content)) {
|
|
@@ -160,6 +165,7 @@ export async function callMetaResponses(
|
|
|
160
165
|
signal?: AbortSignal,
|
|
161
166
|
): Promise<{ text: string; raw: unknown }> {
|
|
162
167
|
payload.store = false;
|
|
168
|
+
applyMetaResponsesCacheHints(payload);
|
|
163
169
|
const res = await fetch(`${META_API_BASE_URL}/responses`, {
|
|
164
170
|
method: "POST",
|
|
165
171
|
headers: {
|
package/extensions/media.ts
CHANGED
|
@@ -48,7 +48,10 @@ import {
|
|
|
48
48
|
} from "./media/responses.ts";
|
|
49
49
|
|
|
50
50
|
export { mediaInputFromSource } from "./media/mime.ts";
|
|
51
|
-
export {
|
|
51
|
+
export {
|
|
52
|
+
maybeUploadLargeInlineBlocks,
|
|
53
|
+
rewriteResponsesPayload,
|
|
54
|
+
} from "./media/payload.ts";
|
|
52
55
|
export {
|
|
53
56
|
extractMetaResponseUsage,
|
|
54
57
|
extractResponsesText,
|
|
@@ -205,13 +208,12 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
205
208
|
}
|
|
206
209
|
if (apiKey) {
|
|
207
210
|
try {
|
|
208
|
-
const { payload: uploadedPayload } =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
);
|
|
211
|
+
const { payload: uploadedPayload } = await maybeUploadLargeInlineBlocks(
|
|
212
|
+
payloadToCheck,
|
|
213
|
+
apiKey,
|
|
214
|
+
STORE_SAFE_INLINE_BYTES,
|
|
215
|
+
ctx.signal,
|
|
216
|
+
);
|
|
215
217
|
return uploadedPayload as unknown;
|
|
216
218
|
} catch {
|
|
217
219
|
const fallback = payloadToCheck as Record<string, unknown>;
|
|
@@ -296,7 +298,8 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
296
298
|
label: "Meta Video Describe",
|
|
297
299
|
description:
|
|
298
300
|
"Analyze an MP4 video with Muse Spark, including visuals and embedded speech. Accepts a local path, public HTTPS/data URL, or Meta file_id. Results are capped by max_chars; when truncated, the full text is saved to a temp file for continuation with read offset/limit.",
|
|
299
|
-
promptSnippet:
|
|
301
|
+
promptSnippet:
|
|
302
|
+
"Inspect video and embedded audio when the active model cannot",
|
|
300
303
|
promptGuidelines: [
|
|
301
304
|
"Use meta_describe_video when the task depends on an MP4 video the active model cannot inspect directly.",
|
|
302
305
|
"Give meta_describe_video a task-specific prompt that asks for the exact visual, transcript, timestamp, or defect evidence needed next.",
|
|
@@ -304,31 +307,55 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
304
307
|
],
|
|
305
308
|
parameters: Type.Object(
|
|
306
309
|
{
|
|
307
|
-
path: Type.Optional(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
310
|
+
path: Type.Optional(
|
|
311
|
+
Type.String({
|
|
312
|
+
description: "Local .mp4/.m4v path, relative to cwd or absolute",
|
|
313
|
+
}),
|
|
314
|
+
),
|
|
315
|
+
url: Type.Optional(
|
|
316
|
+
Type.String({ description: "Public HTTPS or data:video/mp4 URL" }),
|
|
317
|
+
),
|
|
318
|
+
file_id: Type.Optional(
|
|
319
|
+
Type.String({ description: "Existing Meta Files API id" }),
|
|
320
|
+
),
|
|
321
|
+
prompt: Type.String({
|
|
322
|
+
description: "What evidence to extract from the video",
|
|
323
|
+
}),
|
|
324
|
+
model: Type.Optional(
|
|
325
|
+
Type.String({ description: "Muse model id, default muse-spark-1.2" }),
|
|
326
|
+
),
|
|
327
|
+
max_output_tokens: Type.Optional(
|
|
328
|
+
Type.Integer({
|
|
329
|
+
minimum: MIN_MEDIA_MAX_OUTPUT_TOKENS,
|
|
330
|
+
maximum: MAX_MEDIA_MAX_OUTPUT_TOKENS,
|
|
331
|
+
description: `Muse generation budget, default ${DEFAULT_MEDIA_MAX_OUTPUT_TOKENS}`,
|
|
332
|
+
}),
|
|
333
|
+
),
|
|
334
|
+
max_chars: Type.Optional(
|
|
335
|
+
Type.Integer({
|
|
336
|
+
minimum: 1_000,
|
|
337
|
+
maximum: MAX_MEDIA_MAX_CHARS,
|
|
338
|
+
description: `Maximum result characters returned inline, default ${DEFAULT_MEDIA_MAX_CHARS}`,
|
|
339
|
+
}),
|
|
340
|
+
),
|
|
322
341
|
},
|
|
323
342
|
{ additionalProperties: false },
|
|
324
343
|
),
|
|
325
344
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
326
|
-
const { path, url, file_id, prompt, model, max_output_tokens, max_chars } =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
const { path, url, file_id, prompt, model, max_output_tokens, max_chars } =
|
|
346
|
+
params as {
|
|
347
|
+
path?: string;
|
|
348
|
+
url?: string;
|
|
349
|
+
file_id?: string;
|
|
350
|
+
prompt: string;
|
|
351
|
+
model?: string;
|
|
352
|
+
max_output_tokens?: number;
|
|
353
|
+
max_chars?: number;
|
|
354
|
+
};
|
|
330
355
|
if ([path, url, file_id].filter(Boolean).length !== 1) {
|
|
331
|
-
throw new Error(
|
|
356
|
+
throw new Error(
|
|
357
|
+
"meta_describe_video requires exactly one of path, url, or file_id",
|
|
358
|
+
);
|
|
332
359
|
}
|
|
333
360
|
try {
|
|
334
361
|
const apiKey = await safeGetMetaApiKey(ctx as ExtensionContext);
|
|
@@ -341,26 +368,51 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
341
368
|
const absolute = resolve(ctx.cwd, path as string);
|
|
342
369
|
if (!existsSync(absolute)) throw new Error(`File not found: ${absolute}`);
|
|
343
370
|
const stat = statSync(absolute);
|
|
344
|
-
if (stat.size > FILES_API_LIMIT_BYTES)
|
|
371
|
+
if (stat.size > FILES_API_LIMIT_BYTES)
|
|
372
|
+
throw new Error(`File too large (1 GiB limit): ${path}`);
|
|
345
373
|
rejectWrongMime("video", path as string, mimeForPath(absolute));
|
|
346
374
|
if (stat.size > STORE_SAFE_INLINE_BYTES) {
|
|
347
|
-
const upload = await uploadMetaFile(
|
|
375
|
+
const upload = await uploadMetaFile(
|
|
376
|
+
apiKey,
|
|
377
|
+
absolute,
|
|
378
|
+
automaticExpiry(),
|
|
379
|
+
signal,
|
|
380
|
+
);
|
|
348
381
|
videoBlock = { type: "input_file", file_id: upload.id };
|
|
349
382
|
} else {
|
|
350
|
-
videoBlock = mediaInputFromSource(
|
|
383
|
+
videoBlock = mediaInputFromSource(
|
|
384
|
+
dataUrlForFile(absolute).dataUrl,
|
|
385
|
+
basename(absolute),
|
|
386
|
+
"video/mp4",
|
|
387
|
+
);
|
|
351
388
|
}
|
|
352
389
|
}
|
|
353
390
|
const selectedModel = model ?? "muse-spark-1.2";
|
|
354
|
-
const { text, raw } = await callMetaResponses(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
391
|
+
const { text, raw } = await callMetaResponses(
|
|
392
|
+
apiKey,
|
|
393
|
+
{
|
|
394
|
+
model: selectedModel,
|
|
395
|
+
store: false,
|
|
396
|
+
input: [
|
|
397
|
+
{
|
|
398
|
+
type: "message",
|
|
399
|
+
role: "user",
|
|
400
|
+
content: [
|
|
401
|
+
{ type: "input_text", text: prompt },
|
|
402
|
+
videoBlock as Record<string, unknown>,
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
max_output_tokens: mediaMaxOutputTokens(max_output_tokens),
|
|
407
|
+
},
|
|
408
|
+
signal,
|
|
409
|
+
);
|
|
410
|
+
const output = await prepareMediaOutput({
|
|
411
|
+
text,
|
|
412
|
+
response: raw,
|
|
413
|
+
identity: toolCallId,
|
|
414
|
+
maxChars: max_chars,
|
|
415
|
+
});
|
|
364
416
|
const usage = extractMetaResponseUsage(raw, selectedModel);
|
|
365
417
|
return {
|
|
366
418
|
content: [{ type: "text", text: output.text }],
|
|
@@ -386,30 +438,52 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
386
438
|
parameters: Type.Object(
|
|
387
439
|
{
|
|
388
440
|
path: Type.Optional(Type.String({ description: "Local .wav/.mp3 path" })),
|
|
389
|
-
url: Type.Optional(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
description:
|
|
402
|
-
|
|
441
|
+
url: Type.Optional(
|
|
442
|
+
Type.String({ description: "Public HTTPS or data audio URL" }),
|
|
443
|
+
),
|
|
444
|
+
file_id: Type.Optional(
|
|
445
|
+
Type.String({ description: "Existing Meta Files API id" }),
|
|
446
|
+
),
|
|
447
|
+
prompt: Type.Optional(
|
|
448
|
+
Type.String({
|
|
449
|
+
description: "Transcription or audio-analysis instruction",
|
|
450
|
+
}),
|
|
451
|
+
),
|
|
452
|
+
model: Type.Optional(
|
|
453
|
+
Type.String({ description: "Muse model id, default muse-spark-1.2" }),
|
|
454
|
+
),
|
|
455
|
+
max_output_tokens: Type.Optional(
|
|
456
|
+
Type.Integer({
|
|
457
|
+
minimum: MIN_MEDIA_MAX_OUTPUT_TOKENS,
|
|
458
|
+
maximum: MAX_MEDIA_MAX_OUTPUT_TOKENS,
|
|
459
|
+
description: `Muse generation budget, default ${DEFAULT_MEDIA_MAX_OUTPUT_TOKENS}`,
|
|
460
|
+
}),
|
|
461
|
+
),
|
|
462
|
+
max_chars: Type.Optional(
|
|
463
|
+
Type.Integer({
|
|
464
|
+
minimum: 1_000,
|
|
465
|
+
maximum: MAX_MEDIA_MAX_CHARS,
|
|
466
|
+
description: `Maximum transcript characters returned inline, default ${DEFAULT_MEDIA_MAX_CHARS}`,
|
|
467
|
+
}),
|
|
468
|
+
),
|
|
403
469
|
},
|
|
404
470
|
{ additionalProperties: false },
|
|
405
471
|
),
|
|
406
472
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
407
|
-
const { path, url, file_id, prompt, model, max_output_tokens, max_chars } =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
473
|
+
const { path, url, file_id, prompt, model, max_output_tokens, max_chars } =
|
|
474
|
+
params as {
|
|
475
|
+
path?: string;
|
|
476
|
+
url?: string;
|
|
477
|
+
file_id?: string;
|
|
478
|
+
prompt?: string;
|
|
479
|
+
model?: string;
|
|
480
|
+
max_output_tokens?: number;
|
|
481
|
+
max_chars?: number;
|
|
482
|
+
};
|
|
411
483
|
if ([path, url, file_id].filter(Boolean).length !== 1) {
|
|
412
|
-
throw new Error(
|
|
484
|
+
throw new Error(
|
|
485
|
+
"meta_transcribe_audio requires exactly one of path, url, or file_id",
|
|
486
|
+
);
|
|
413
487
|
}
|
|
414
488
|
try {
|
|
415
489
|
const apiKey = await safeGetMetaApiKey(ctx as ExtensionContext);
|
|
@@ -422,27 +496,55 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
422
496
|
const absolute = resolve(ctx.cwd, path as string);
|
|
423
497
|
if (!existsSync(absolute)) throw new Error(`File not found: ${absolute}`);
|
|
424
498
|
const stat = statSync(absolute);
|
|
425
|
-
if (stat.size > FILES_API_LIMIT_BYTES)
|
|
499
|
+
if (stat.size > FILES_API_LIMIT_BYTES)
|
|
500
|
+
throw new Error("Audio too large (1 GiB limit)");
|
|
426
501
|
const mime = mimeForPath(absolute);
|
|
427
502
|
rejectWrongMime("audio", path as string, mime);
|
|
428
503
|
if (stat.size > STORE_SAFE_INLINE_BYTES) {
|
|
429
|
-
const upload = await uploadMetaFile(
|
|
504
|
+
const upload = await uploadMetaFile(
|
|
505
|
+
apiKey,
|
|
506
|
+
absolute,
|
|
507
|
+
automaticExpiry(),
|
|
508
|
+
signal,
|
|
509
|
+
);
|
|
430
510
|
audioBlock = { type: "input_file", file_id: upload.id };
|
|
431
511
|
} else {
|
|
432
|
-
audioBlock = mediaInputFromSource(
|
|
512
|
+
audioBlock = mediaInputFromSource(
|
|
513
|
+
dataUrlForFile(absolute).dataUrl,
|
|
514
|
+
basename(absolute),
|
|
515
|
+
mime,
|
|
516
|
+
);
|
|
433
517
|
}
|
|
434
518
|
}
|
|
435
519
|
const selectedModel = model ?? "muse-spark-1.2";
|
|
436
|
-
const { text, raw } = await callMetaResponses(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
520
|
+
const { text, raw } = await callMetaResponses(
|
|
521
|
+
apiKey,
|
|
522
|
+
{
|
|
523
|
+
model: selectedModel,
|
|
524
|
+
store: false,
|
|
525
|
+
input: [
|
|
526
|
+
{
|
|
527
|
+
type: "message",
|
|
528
|
+
role: "user",
|
|
529
|
+
content: [
|
|
530
|
+
{
|
|
531
|
+
type: "input_text",
|
|
532
|
+
text: prompt ?? "Transcribe this audio. Return only the transcript.",
|
|
533
|
+
},
|
|
534
|
+
audioBlock as Record<string, unknown>,
|
|
535
|
+
],
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
max_output_tokens: mediaMaxOutputTokens(max_output_tokens),
|
|
539
|
+
},
|
|
540
|
+
signal,
|
|
541
|
+
);
|
|
542
|
+
const output = await prepareMediaOutput({
|
|
543
|
+
text,
|
|
544
|
+
response: raw,
|
|
545
|
+
identity: toolCallId,
|
|
546
|
+
maxChars: max_chars,
|
|
547
|
+
});
|
|
446
548
|
const usage = extractMetaResponseUsage(raw, selectedModel);
|
|
447
549
|
return {
|
|
448
550
|
content: [{ type: "text", text: output.text }],
|
|
@@ -463,30 +565,42 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
463
565
|
parameters: Type.Object(
|
|
464
566
|
{
|
|
465
567
|
path: Type.String({ description: "Local file path to upload" }),
|
|
466
|
-
expires_after_seconds: Type.Optional(
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
568
|
+
expires_after_seconds: Type.Optional(
|
|
569
|
+
Type.Integer({
|
|
570
|
+
minimum: MIN_UPLOAD_EXPIRY_SECONDS,
|
|
571
|
+
maximum: MAX_UPLOAD_EXPIRY_SECONDS,
|
|
572
|
+
description: `Expiry in seconds, default ${EXPLICIT_UPLOAD_EXPIRY_SECONDS} (7 days)`,
|
|
573
|
+
}),
|
|
574
|
+
),
|
|
575
|
+
retain: Type.Optional(
|
|
576
|
+
Type.Boolean({
|
|
577
|
+
description: "Keep indefinitely instead of applying an expiry",
|
|
578
|
+
}),
|
|
579
|
+
),
|
|
472
580
|
},
|
|
473
581
|
{ additionalProperties: false },
|
|
474
582
|
),
|
|
475
583
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
476
584
|
const { path, expires_after_seconds, retain } = params as {
|
|
477
|
-
path: string;
|
|
585
|
+
path: string;
|
|
586
|
+
expires_after_seconds?: number;
|
|
587
|
+
retain?: boolean;
|
|
478
588
|
};
|
|
479
589
|
if (retain && expires_after_seconds !== undefined) {
|
|
480
|
-
throw new Error(
|
|
590
|
+
throw new Error(
|
|
591
|
+
"meta_upload_file accepts either retain=true or expires_after_seconds, not both",
|
|
592
|
+
);
|
|
481
593
|
}
|
|
482
594
|
try {
|
|
483
595
|
const apiKey = await safeGetMetaApiKey(ctx as ExtensionContext);
|
|
484
596
|
const absolute = resolve(ctx.cwd, path);
|
|
485
597
|
if (!existsSync(absolute)) throw new Error(`File not found: ${absolute}`);
|
|
486
|
-
const expiresAfter = retain
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
598
|
+
const expiresAfter = retain
|
|
599
|
+
? undefined
|
|
600
|
+
: {
|
|
601
|
+
anchor: "created_at" as const,
|
|
602
|
+
seconds: expires_after_seconds ?? EXPLICIT_UPLOAD_EXPIRY_SECONDS,
|
|
603
|
+
};
|
|
490
604
|
const result = await uploadMetaFile(apiKey, absolute, expiresAfter, signal);
|
|
491
605
|
const retention = retain
|
|
492
606
|
? "retained without expiry"
|
|
@@ -494,7 +608,12 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
494
608
|
? `expires_at ${result.expires_at}`
|
|
495
609
|
: `expires after ${expiresAfter?.seconds} seconds`;
|
|
496
610
|
return {
|
|
497
|
-
content: [
|
|
611
|
+
content: [
|
|
612
|
+
{
|
|
613
|
+
type: "text",
|
|
614
|
+
text: `Uploaded ${basename(absolute)} to ${result.id} (${result.bytes} bytes, ${retention}).`,
|
|
615
|
+
},
|
|
616
|
+
],
|
|
498
617
|
details: result,
|
|
499
618
|
};
|
|
500
619
|
} catch (error) {
|
|
@@ -508,7 +627,8 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
508
627
|
label: "Meta File Analyze",
|
|
509
628
|
description:
|
|
510
629
|
"Analyze one or more images, PDFs, videos, or audio files with Muse Spark. Use path/url/file_id for one source, or ordered sources for comparisons. Images and PDF pages share Meta's 50-image budget; PDFs include text from the first 100 pages and images from the first 50 pages. Truncated text is saved for continuation with read offset/limit.",
|
|
511
|
-
promptSnippet:
|
|
630
|
+
promptSnippet:
|
|
631
|
+
"Inspect images, PDFs, or mixed media when the active model cannot",
|
|
512
632
|
promptGuidelines: [
|
|
513
633
|
"Use meta_analyze_file when the task depends on an image, PDF, or generic media file the active model cannot inspect directly.",
|
|
514
634
|
"Give meta_analyze_file a task-specific prompt that asks for the exact visible text, layout, comparison, defect, or action evidence needed next.",
|
|
@@ -517,61 +637,134 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
517
637
|
parameters: Type.Object(
|
|
518
638
|
{
|
|
519
639
|
path: Type.Optional(Type.String({ description: "Single local file path" })),
|
|
520
|
-
url: Type.Optional(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
640
|
+
url: Type.Optional(
|
|
641
|
+
Type.String({ description: "Single public HTTPS or data URL" }),
|
|
642
|
+
),
|
|
643
|
+
file_id: Type.Optional(
|
|
644
|
+
Type.String({ description: "Single existing Meta file_id" }),
|
|
645
|
+
),
|
|
646
|
+
sources: Type.Optional(
|
|
647
|
+
Type.Array(
|
|
648
|
+
Type.Object(
|
|
649
|
+
{
|
|
650
|
+
source: Type.String({
|
|
651
|
+
description: "Local path, HTTPS/data URL, or Meta file_id",
|
|
652
|
+
}),
|
|
653
|
+
label: Type.Optional(
|
|
654
|
+
Type.String({
|
|
655
|
+
description: "Human-readable label such as Before or After",
|
|
656
|
+
}),
|
|
657
|
+
),
|
|
658
|
+
},
|
|
659
|
+
{ additionalProperties: false },
|
|
660
|
+
),
|
|
661
|
+
{
|
|
662
|
+
minItems: 1,
|
|
663
|
+
maxItems: MAX_ANALYSIS_SOURCES,
|
|
664
|
+
description: "Ordered sources for comparison or combined analysis",
|
|
665
|
+
},
|
|
666
|
+
),
|
|
667
|
+
),
|
|
530
668
|
prompt: Type.String({ description: "What evidence to extract or compare" }),
|
|
531
|
-
model: Type.Optional(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
669
|
+
model: Type.Optional(
|
|
670
|
+
Type.String({ description: "Muse model id, default muse-spark-1.2" }),
|
|
671
|
+
),
|
|
672
|
+
max_output_tokens: Type.Optional(
|
|
673
|
+
Type.Integer({
|
|
674
|
+
minimum: MIN_MEDIA_MAX_OUTPUT_TOKENS,
|
|
675
|
+
maximum: MAX_MEDIA_MAX_OUTPUT_TOKENS,
|
|
676
|
+
description: `Muse generation budget, default ${DEFAULT_MEDIA_MAX_OUTPUT_TOKENS}`,
|
|
677
|
+
}),
|
|
678
|
+
),
|
|
679
|
+
max_chars: Type.Optional(
|
|
680
|
+
Type.Integer({
|
|
681
|
+
minimum: 1_000,
|
|
682
|
+
maximum: MAX_MEDIA_MAX_CHARS,
|
|
683
|
+
description: `Maximum result characters returned inline, default ${DEFAULT_MEDIA_MAX_CHARS}`,
|
|
684
|
+
}),
|
|
685
|
+
),
|
|
542
686
|
},
|
|
543
687
|
{ additionalProperties: false },
|
|
544
688
|
),
|
|
545
689
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
546
|
-
const {
|
|
547
|
-
path
|
|
548
|
-
|
|
690
|
+
const {
|
|
691
|
+
path,
|
|
692
|
+
url,
|
|
693
|
+
file_id,
|
|
694
|
+
sources,
|
|
695
|
+
prompt,
|
|
696
|
+
model,
|
|
697
|
+
max_output_tokens,
|
|
698
|
+
max_chars,
|
|
699
|
+
} = params as {
|
|
700
|
+
path?: string;
|
|
701
|
+
url?: string;
|
|
702
|
+
file_id?: string;
|
|
703
|
+
sources?: AnalysisSource[];
|
|
704
|
+
prompt: string;
|
|
705
|
+
model?: string;
|
|
706
|
+
max_output_tokens?: number;
|
|
707
|
+
max_chars?: number;
|
|
549
708
|
};
|
|
550
|
-
const legacySources = [path, url, file_id].filter((value): value is string =>
|
|
551
|
-
|
|
552
|
-
|
|
709
|
+
const legacySources = [path, url, file_id].filter((value): value is string =>
|
|
710
|
+
Boolean(value),
|
|
711
|
+
);
|
|
712
|
+
if (
|
|
713
|
+
legacySources.length > 1 ||
|
|
714
|
+
(legacySources.length === 1 && sources) ||
|
|
715
|
+
(legacySources.length === 0 && !sources)
|
|
716
|
+
) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
"meta_analyze_file requires exactly one path/url/file_id or a sources array",
|
|
719
|
+
);
|
|
553
720
|
}
|
|
554
721
|
if (sources && sources.length > MAX_ANALYSIS_SOURCES) {
|
|
555
|
-
throw new Error(
|
|
722
|
+
throw new Error(
|
|
723
|
+
`meta_analyze_file supports at most ${MAX_ANALYSIS_SOURCES} sources`,
|
|
724
|
+
);
|
|
556
725
|
}
|
|
557
726
|
try {
|
|
558
727
|
const apiKey = await safeGetMetaApiKey(ctx as ExtensionContext);
|
|
559
|
-
const requestedSources: AnalysisSource[] = sources ?? [
|
|
560
|
-
|
|
728
|
+
const requestedSources: AnalysisSource[] = sources ?? [
|
|
729
|
+
{ source: legacySources[0] as string },
|
|
730
|
+
];
|
|
731
|
+
const content: ResponsesContentBlock[] = [
|
|
732
|
+
{ type: "input_text", text: prompt },
|
|
733
|
+
];
|
|
561
734
|
for (const [index, item] of requestedSources.entries()) {
|
|
562
735
|
if (requestedSources.length > 1 || item.label) {
|
|
563
|
-
content.push({
|
|
736
|
+
content.push({
|
|
737
|
+
type: "input_text",
|
|
738
|
+
text: `${sourceLabel(item, index)}:`,
|
|
739
|
+
});
|
|
564
740
|
}
|
|
565
|
-
content.push(
|
|
741
|
+
content.push(
|
|
742
|
+
await resolveGenericMediaSource(apiKey, ctx.cwd, item.source, signal),
|
|
743
|
+
);
|
|
566
744
|
}
|
|
567
745
|
const selectedModel = model ?? "muse-spark-1.2";
|
|
568
|
-
const { text, raw } = await callMetaResponses(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
746
|
+
const { text, raw } = await callMetaResponses(
|
|
747
|
+
apiKey,
|
|
748
|
+
{
|
|
749
|
+
model: selectedModel,
|
|
750
|
+
store: false,
|
|
751
|
+
input: [
|
|
752
|
+
{
|
|
753
|
+
type: "message",
|
|
754
|
+
role: "user",
|
|
755
|
+
content: content as Record<string, unknown>[],
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
max_output_tokens: mediaMaxOutputTokens(max_output_tokens),
|
|
759
|
+
},
|
|
760
|
+
signal,
|
|
761
|
+
);
|
|
762
|
+
const output = await prepareMediaOutput({
|
|
763
|
+
text,
|
|
764
|
+
response: raw,
|
|
765
|
+
identity: toolCallId,
|
|
766
|
+
maxChars: max_chars,
|
|
767
|
+
});
|
|
575
768
|
const usage = extractMetaResponseUsage(raw, selectedModel);
|
|
576
769
|
return {
|
|
577
770
|
content: [{ type: "text", text: output.text }],
|
|
@@ -737,8 +930,7 @@ export default function metaMedia(pi: ExtensionAPI): void {
|
|
|
737
930
|
const apiKey = await safeGetMetaApiKey(ctx as unknown as ExtensionContext);
|
|
738
931
|
let block: ResponsesContentBlock;
|
|
739
932
|
if (isFileId(source)) block = { type: "input_file", file_id: source };
|
|
740
|
-
else if (isRemoteMediaSource(source))
|
|
741
|
-
block = mediaInputFromSource(source);
|
|
933
|
+
else if (isRemoteMediaSource(source)) block = mediaInputFromSource(source);
|
|
742
934
|
else {
|
|
743
935
|
const abs = resolve(cwd, source);
|
|
744
936
|
if (!existsSync(abs)) throw new Error(`File not found: ${abs}`);
|
package/extensions/meta.ts
CHANGED
|
@@ -82,6 +82,56 @@ interface MetaCatalogModel {
|
|
|
82
82
|
// See media.ts for the before_provider_request rewrite that makes @video/@audio work today
|
|
83
83
|
// despite UserMessage.content being string | (TextContent | ImageContent)[] .
|
|
84
84
|
const FALLBACK_MODELS: MetaProviderModel[] = [
|
|
85
|
+
{
|
|
86
|
+
id: "muse-spark-1.3",
|
|
87
|
+
name: "Muse Spark 1.3",
|
|
88
|
+
reasoning: true,
|
|
89
|
+
thinkingLevelMap: {
|
|
90
|
+
off: null,
|
|
91
|
+
minimal: "minimal",
|
|
92
|
+
low: "low",
|
|
93
|
+
medium: "medium",
|
|
94
|
+
high: "high",
|
|
95
|
+
xhigh: "xhigh",
|
|
96
|
+
max: null,
|
|
97
|
+
},
|
|
98
|
+
// SAFETY: Meta supports video/audio inputs, while pi-ai 0.83/0.84 types only expose text/image.
|
|
99
|
+
input: [
|
|
100
|
+
"text",
|
|
101
|
+
"image",
|
|
102
|
+
"video",
|
|
103
|
+
"audio",
|
|
104
|
+
] as unknown as MetaProviderModel["input"],
|
|
105
|
+
cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
|
106
|
+
contextWindow: 1_048_576,
|
|
107
|
+
maxTokens: 256_000,
|
|
108
|
+
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "muse-spark-1.3-contributor",
|
|
112
|
+
name: "Muse Spark 1.3 Contributor",
|
|
113
|
+
reasoning: true,
|
|
114
|
+
thinkingLevelMap: {
|
|
115
|
+
off: null,
|
|
116
|
+
minimal: "minimal",
|
|
117
|
+
low: "low",
|
|
118
|
+
medium: "medium",
|
|
119
|
+
high: "high",
|
|
120
|
+
xhigh: "xhigh",
|
|
121
|
+
max: null,
|
|
122
|
+
},
|
|
123
|
+
// SAFETY: Meta supports video/audio inputs, while pi-ai 0.83/0.84 types only expose text/image.
|
|
124
|
+
input: [
|
|
125
|
+
"text",
|
|
126
|
+
"image",
|
|
127
|
+
"video",
|
|
128
|
+
"audio",
|
|
129
|
+
] as unknown as MetaProviderModel["input"],
|
|
130
|
+
cost: { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 },
|
|
131
|
+
contextWindow: 1_048_576,
|
|
132
|
+
maxTokens: 256_000,
|
|
133
|
+
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
134
|
+
},
|
|
85
135
|
{
|
|
86
136
|
id: "muse-spark-1.2",
|
|
87
137
|
name: "Muse Spark 1.2",
|
|
@@ -336,8 +386,7 @@ export async function refreshMetaToken(
|
|
|
336
386
|
);
|
|
337
387
|
// Pi 0.83 calls refreshToken(credentials). Pi 0.84 passes AbortSignal as
|
|
338
388
|
// the second argument. Tests inject a fetch mock in that slot.
|
|
339
|
-
const fetchImpl =
|
|
340
|
-
typeof fetchOrSignal === "function" ? fetchOrSignal : fetch;
|
|
389
|
+
const fetchImpl = typeof fetchOrSignal === "function" ? fetchOrSignal : fetch;
|
|
341
390
|
const signal = isAbortSignal(fetchOrSignal) ? fetchOrSignal : undefined;
|
|
342
391
|
if (signal?.aborted) {
|
|
343
392
|
throw new Error("Meta token refresh was cancelled");
|
|
@@ -582,6 +631,38 @@ export function metaFallbackCost(
|
|
|
582
631
|
return FALLBACK_MODELS.find((model) => model.id === modelId)?.cost;
|
|
583
632
|
}
|
|
584
633
|
|
|
634
|
+
/** Meta prompt-cache opt-in. Measured 0% hits on /chat/completions vs 93–99% on /responses with 24h. */
|
|
635
|
+
export const META_PROMPT_CACHE_RETENTION = "24h";
|
|
636
|
+
|
|
637
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
638
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
639
|
+
? (value as Record<string, unknown>)
|
|
640
|
+
: undefined;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Hermes-equivalent Responses hints for api.meta.ai:
|
|
645
|
+
* setdefault `prompt_cache_retention: 24h`, and drop `reasoning.effort: none`
|
|
646
|
+
* because Meta 400s on it.
|
|
647
|
+
*/
|
|
648
|
+
export function applyMetaResponsesCacheHints(payload: unknown): unknown {
|
|
649
|
+
const body = asRecord(payload);
|
|
650
|
+
if (!body) return payload;
|
|
651
|
+
if (body.prompt_cache_retention === undefined) {
|
|
652
|
+
body.prompt_cache_retention = META_PROMPT_CACHE_RETENTION;
|
|
653
|
+
}
|
|
654
|
+
const reasoning = asRecord(body.reasoning);
|
|
655
|
+
if (
|
|
656
|
+
reasoning &&
|
|
657
|
+
(reasoning.effort === "none" ||
|
|
658
|
+
reasoning.effort === undefined ||
|
|
659
|
+
reasoning.effort === null)
|
|
660
|
+
) {
|
|
661
|
+
delete body.reasoning;
|
|
662
|
+
}
|
|
663
|
+
return body;
|
|
664
|
+
}
|
|
665
|
+
|
|
585
666
|
export function createMetaProviderConfig(): ProviderConfig {
|
|
586
667
|
return {
|
|
587
668
|
name: "Meta Model API",
|
|
@@ -614,4 +695,8 @@ export default function metaOAuthProvider(pi: ExtensionAPI): void {
|
|
|
614
695
|
process.env["MODEL_API_KEY"] = process.env[META_ENV_VAR];
|
|
615
696
|
}
|
|
616
697
|
pi.registerProvider(META_PROVIDER_ID, createMetaProviderConfig());
|
|
698
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
699
|
+
if (ctx.model?.provider !== META_PROVIDER_ID) return undefined;
|
|
700
|
+
return applyMetaResponsesCacheHints(event.payload);
|
|
701
|
+
});
|
|
617
702
|
}
|
package/extensions/voice/asr.ts
CHANGED
|
@@ -53,6 +53,28 @@ export function formatAuthorization(apiKey: string): string {
|
|
|
53
53
|
return apiKey.startsWith("OAuth ") ? apiKey : `OAuth ${apiKey}`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export function asrModel(): string {
|
|
57
|
+
return (
|
|
58
|
+
process.env.PI_META_VOICE_ASR_MODEL ??
|
|
59
|
+
process.env.MUSE_VOICE_ASR_MODEL ??
|
|
60
|
+
DEFAULT_ASR_MODEL
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function asrHandshake(apiKey: string): {
|
|
65
|
+
mode: "DEFAULT";
|
|
66
|
+
authorization: { accessToken: string };
|
|
67
|
+
audioEncoding: "PCM_16KHZ";
|
|
68
|
+
model: string;
|
|
69
|
+
} {
|
|
70
|
+
return {
|
|
71
|
+
mode: "DEFAULT",
|
|
72
|
+
authorization: { accessToken: formatAuthorization(apiKey) },
|
|
73
|
+
audioEncoding: "PCM_16KHZ",
|
|
74
|
+
model: asrModel(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
export function asrEndpoint(sessionId: string): string {
|
|
57
79
|
const configured =
|
|
58
80
|
process.env.PI_META_VOICE_ASR_ENDPOINT ??
|
|
@@ -90,7 +112,9 @@ export async function socketMessageText(
|
|
|
90
112
|
return undefined;
|
|
91
113
|
}
|
|
92
114
|
|
|
93
|
-
export function parseJsonObject(
|
|
115
|
+
export function parseJsonObject(
|
|
116
|
+
text: string,
|
|
117
|
+
): Record<string, unknown> | undefined {
|
|
94
118
|
try {
|
|
95
119
|
const value = JSON.parse(text) as unknown;
|
|
96
120
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
package/extensions/voice.ts
CHANGED
|
@@ -11,9 +11,8 @@ import { homedir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import {
|
|
13
13
|
asrEndpoint,
|
|
14
|
+
asrHandshake,
|
|
14
15
|
chooseTranscript,
|
|
15
|
-
DEFAULT_ASR_MODEL,
|
|
16
|
-
formatAuthorization,
|
|
17
16
|
isNormalSocketClose,
|
|
18
17
|
parseJsonObject,
|
|
19
18
|
pcmAudioLevel,
|
|
@@ -25,7 +24,12 @@ import {
|
|
|
25
24
|
supportedPlatformLabel,
|
|
26
25
|
} from "./voice/helpers.ts";
|
|
27
26
|
|
|
28
|
-
export {
|
|
27
|
+
export {
|
|
28
|
+
asrEndpoint,
|
|
29
|
+
asrHandshake,
|
|
30
|
+
formatAuthorization,
|
|
31
|
+
pcmAudioLevel,
|
|
32
|
+
} from "./voice/asr.ts";
|
|
29
33
|
|
|
30
34
|
const AGENT_DIR = join(homedir(), ".pi/agent");
|
|
31
35
|
const SETTINGS_FILE = join(AGENT_DIR, "pi-meta-oauth-voice.json");
|
|
@@ -253,10 +257,7 @@ class MetaVoiceController {
|
|
|
253
257
|
this.audioLevel *= 0.78;
|
|
254
258
|
}, 120);
|
|
255
259
|
this.recordingLimitTimer = setTimeout(() => {
|
|
256
|
-
ctx.ui.notify(
|
|
257
|
-
"Voice recording reached the two-minute limit",
|
|
258
|
-
"warning",
|
|
259
|
-
);
|
|
260
|
+
ctx.ui.notify("Voice recording reached the two-minute limit", "warning");
|
|
260
261
|
this.stop(ctx);
|
|
261
262
|
}, MAX_RECORDING_MS);
|
|
262
263
|
} catch (error) {
|
|
@@ -294,17 +295,7 @@ class MetaVoiceController {
|
|
|
294
295
|
this.socket = socket;
|
|
295
296
|
socket.addEventListener("open", () => {
|
|
296
297
|
if (currentGeneration !== this.generation) return;
|
|
297
|
-
socket.send(
|
|
298
|
-
JSON.stringify({
|
|
299
|
-
mode: "DEFAULT",
|
|
300
|
-
authorization: { accessToken: formatAuthorization(apiKey) },
|
|
301
|
-
audioEncoding: "PCM_16KHZ",
|
|
302
|
-
model:
|
|
303
|
-
process.env.PI_META_VOICE_ASR_MODEL ??
|
|
304
|
-
process.env.MUSE_VOICE_ASR_MODEL ??
|
|
305
|
-
DEFAULT_ASR_MODEL,
|
|
306
|
-
}),
|
|
307
|
-
);
|
|
298
|
+
socket.send(JSON.stringify(asrHandshake(apiKey)));
|
|
308
299
|
});
|
|
309
300
|
socket.addEventListener("message", (event) => {
|
|
310
301
|
if (currentGeneration !== this.generation) return;
|
|
@@ -315,8 +306,7 @@ class MetaVoiceController {
|
|
|
315
306
|
this.fail(ctx, new Error("could not connect to Muse's Meta ASR service"));
|
|
316
307
|
});
|
|
317
308
|
socket.addEventListener("close", (event) => {
|
|
318
|
-
if (currentGeneration !== this.generation || this.phase === "idle")
|
|
319
|
-
return;
|
|
309
|
+
if (currentGeneration !== this.generation || this.phase === "idle") return;
|
|
320
310
|
if (
|
|
321
311
|
this.phase === "stopping" &&
|
|
322
312
|
this.transcript &&
|
|
@@ -328,8 +318,7 @@ class MetaVoiceController {
|
|
|
328
318
|
this.fail(
|
|
329
319
|
ctx,
|
|
330
320
|
new Error(
|
|
331
|
-
event.reason ||
|
|
332
|
-
`Meta ASR connection closed (${event.code || "unknown"})`,
|
|
321
|
+
event.reason || `Meta ASR connection closed (${event.code || "unknown"})`,
|
|
333
322
|
),
|
|
334
323
|
);
|
|
335
324
|
});
|
|
@@ -593,9 +582,7 @@ class MetaVoiceController {
|
|
|
593
582
|
this.generation += 1;
|
|
594
583
|
this.clearTimers();
|
|
595
584
|
this.closeResources();
|
|
596
|
-
ctx.ui.setEditorText(
|
|
597
|
-
combineEditorText(this.originalEditorText, transcript),
|
|
598
|
-
);
|
|
585
|
+
ctx.ui.setEditorText(combineEditorText(this.originalEditorText, transcript));
|
|
599
586
|
ctx.ui.setWidget("muse-voice", undefined);
|
|
600
587
|
this.renderIdleStatus(ctx);
|
|
601
588
|
if (transcript) {
|