@tychenjiajun/pi-voice 0.2.0 → 0.4.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 +54 -6
- package/package.json +2 -2
- package/src/chinese.ts +15 -0
- package/src/file-transcription.ts +13 -3
- package/src/log.ts +1 -1
- package/src/onboarding.ts +72 -5
- package/src/openai-asr.ts +4 -4
- package/src/polish.ts +165 -0
- package/src/remote-asr.ts +212 -0
- package/src/runtime.ts +17 -8
- package/src/settings-menu.ts +149 -174
- package/src/settings.ts +26 -0
- package/src/try-it.ts +8 -3
- package/src/ui-components.ts +8 -2
package/src/settings-menu.ts
CHANGED
|
@@ -2,11 +2,10 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
5
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import { microphonePermission, type Permission } from "./audio.js";
|
|
7
7
|
import { displayLanguage, getCatalogModel } from "./catalog.js";
|
|
8
8
|
import { chineseOutputSummary, isChineseLanguage } from "./chinese.js";
|
|
9
|
-
import { VoiceKeys } from "./keybindings.js";
|
|
10
9
|
import {
|
|
11
10
|
chooseLanguages,
|
|
12
11
|
createTranscriptionLanguagePicker,
|
|
@@ -18,20 +17,20 @@ import {
|
|
|
18
17
|
microphoneSummary,
|
|
19
18
|
} from "./microphone-picker.js";
|
|
20
19
|
import { runModelSelection } from "./onboarding.js";
|
|
20
|
+
import { chooseBackendType, promptRemoteBackend, warnMissingApiKey } from "./remote-asr.js";
|
|
21
21
|
import {
|
|
22
22
|
writeSettings,
|
|
23
23
|
type BackendSettings,
|
|
24
24
|
type ChineseOutput,
|
|
25
25
|
type MicrophoneSetting,
|
|
26
|
+
type PolishModelRef,
|
|
26
27
|
type TranscribeSettings,
|
|
27
28
|
type TranscriptionLanguage,
|
|
28
29
|
} from "./settings.js";
|
|
29
30
|
import { displayShortcut } from "./shortcut-core.js";
|
|
30
31
|
import { createShortcutPicker } from "./shortcuts.js";
|
|
31
32
|
import {
|
|
32
|
-
LIST_PADDING,
|
|
33
33
|
padToWidth,
|
|
34
|
-
panelBorder,
|
|
35
34
|
SingleSelectPicker,
|
|
36
35
|
type SingleSelectChoice,
|
|
37
36
|
} from "./ui-components.js";
|
|
@@ -42,16 +41,17 @@ const SETTINGS_LABEL_WIDTH = 25;
|
|
|
42
41
|
// Text padding (2), cursor gutter (2), label column, and its gap (2).
|
|
43
42
|
const SETTINGS_ROW_OVERHEAD = SETTINGS_LABEL_WIDTH + 6;
|
|
44
43
|
|
|
45
|
-
type SettingsAction =
|
|
44
|
+
export type SettingsAction =
|
|
46
45
|
| "preferred-languages"
|
|
47
46
|
| "model"
|
|
48
47
|
| "backend"
|
|
49
48
|
| "transcription-language"
|
|
50
49
|
| "chinese-output"
|
|
50
|
+
| "post-processing"
|
|
51
51
|
| "microphone"
|
|
52
52
|
| "shortcut";
|
|
53
53
|
|
|
54
|
-
type SettingsHomeChoice = SingleSelectChoice<SettingsAction> & {
|
|
54
|
+
export type SettingsHomeChoice = SingleSelectChoice<SettingsAction> & {
|
|
55
55
|
summary: string;
|
|
56
56
|
/** Render the summary in the error color. */
|
|
57
57
|
alert?: boolean;
|
|
@@ -91,7 +91,12 @@ function backendSummary(backend: BackendSettings): string {
|
|
|
91
91
|
return backend.type === "openai-asr" ? `Remote · ${backend.model}` : "Local model";
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function
|
|
94
|
+
function polishSummary(polishEnabled: boolean | undefined, model: PolishModelRef | undefined): string {
|
|
95
|
+
if (!polishEnabled) return "Off";
|
|
96
|
+
return model ? `${model.provider}/${model.id}` : "Pi's current model";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function settingsHomeChoices(
|
|
95
100
|
configured: TranscribeSettings,
|
|
96
101
|
permission: Permission,
|
|
97
102
|
): SettingsHomeChoice[] {
|
|
@@ -116,17 +121,34 @@ function settingsHomeChoices(
|
|
|
116
121
|
description:
|
|
117
122
|
"Local model, or an OpenAI-compatible remote ASR endpoint (local model stays as fallback)",
|
|
118
123
|
},
|
|
124
|
+
{
|
|
125
|
+
value: "post-processing",
|
|
126
|
+
label: "Post-processing",
|
|
127
|
+
summary: polishSummary(configured.polishEnabled, configured.polishModel),
|
|
128
|
+
description:
|
|
129
|
+
"Refine transcripts with Pi's current model after ASR, like context compaction",
|
|
130
|
+
},
|
|
119
131
|
{
|
|
120
132
|
value: "transcription-language",
|
|
121
133
|
label: "Transcription language",
|
|
122
134
|
summary: transcriptionLanguageSummary(configured.transcriptionLanguage, model),
|
|
123
|
-
description:
|
|
135
|
+
description:
|
|
136
|
+
configured.backend.type === "openai-asr"
|
|
137
|
+
? "Used by the local fallback and Chinese output conversion; the remote endpoint detects the language itself"
|
|
138
|
+
: "Language expected in recordings, or automatic detection when supported",
|
|
124
139
|
},
|
|
125
140
|
];
|
|
126
141
|
|
|
142
|
+
// Chinese script conversion applies when the configured language is Chinese,
|
|
143
|
+
// when a preferred language is Chinese, or when a detecting model can pick
|
|
144
|
+
// Chinese on its own. Gating only on the first two leaves the row unreachable
|
|
145
|
+
// for anyone relying on auto-detection.
|
|
146
|
+
const detectsChinese =
|
|
147
|
+
model.capabilities.languageDetection && model.languages.some(isChineseLanguage);
|
|
127
148
|
if (
|
|
128
149
|
isChineseLanguage(configured.transcriptionLanguage) ||
|
|
129
|
-
configured.preferredLanguages.some(isChineseLanguage)
|
|
150
|
+
configured.preferredLanguages.some(isChineseLanguage) ||
|
|
151
|
+
detectsChinese
|
|
130
152
|
) {
|
|
131
153
|
choices.push({
|
|
132
154
|
value: "chinese-output",
|
|
@@ -266,212 +288,158 @@ async function chooseShortcut(
|
|
|
266
288
|
);
|
|
267
289
|
}
|
|
268
290
|
|
|
269
|
-
/**
|
|
270
|
-
|
|
271
|
-
* (the caller decides via validate); Escape backs out of the whole flow.
|
|
272
|
-
*/
|
|
273
|
-
function promptText(
|
|
291
|
+
/** Backend row flow: pick the type, then collect remote settings step by step. */
|
|
292
|
+
async function editBackend(
|
|
274
293
|
ctx: ExtensionContext,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const container = new Container();
|
|
289
|
-
|
|
290
|
-
const rebuild = (): void => {
|
|
291
|
-
container.clear();
|
|
292
|
-
container.addChild(panelBorder(theme));
|
|
293
|
-
container.addChild(new Spacer(1));
|
|
294
|
-
container.addChild(new Text(theme.fg("accent", theme.bold(options.title)), 1, 0));
|
|
295
|
-
if (options.description) {
|
|
296
|
-
container.addChild(new Text(theme.fg("muted", options.description), 1, 0));
|
|
297
|
-
}
|
|
298
|
-
container.addChild(new Spacer(1));
|
|
299
|
-
if (error) {
|
|
300
|
-
container.addChild(new Text(theme.fg("error", error), 1, 0));
|
|
301
|
-
container.addChild(new Spacer(1));
|
|
302
|
-
}
|
|
303
|
-
if (!input.getValue() && options.placeholder) {
|
|
304
|
-
container.addChild(new Text(theme.fg("dim", options.placeholder), 2, 0));
|
|
305
|
-
}
|
|
306
|
-
const inputBox = new Box(LIST_PADDING, 0);
|
|
307
|
-
inputBox.addChild(input);
|
|
308
|
-
container.addChild(inputBox);
|
|
309
|
-
container.addChild(new Spacer(1));
|
|
310
|
-
container.addChild(
|
|
311
|
-
new Text(
|
|
312
|
-
`${keys.hint("tui.input.submit", "save")} ${keys.hint("tui.select.cancel", "back")}`,
|
|
313
|
-
1,
|
|
314
|
-
0,
|
|
315
|
-
),
|
|
316
|
-
);
|
|
317
|
-
container.addChild(new Spacer(1));
|
|
318
|
-
container.addChild(panelBorder(theme));
|
|
319
|
-
};
|
|
320
|
-
rebuild();
|
|
321
|
-
|
|
322
|
-
input.onSubmit = (value) => {
|
|
323
|
-
const problem = options.validate?.(value.trim());
|
|
324
|
-
if (problem) {
|
|
325
|
-
error = problem;
|
|
326
|
-
rebuild();
|
|
327
|
-
tui.requestRender();
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
done(value.trim());
|
|
331
|
-
};
|
|
332
|
-
input.onEscape = () => done(undefined);
|
|
294
|
+
configured: TranscribeSettings,
|
|
295
|
+
): Promise<boolean> {
|
|
296
|
+
const type = await chooseBackendType(ctx, configured.backend);
|
|
297
|
+
if (!type) return false;
|
|
298
|
+
if (type === "transcribe-cpp") {
|
|
299
|
+
if (configured.backend.type === "transcribe-cpp") return false;
|
|
300
|
+
return saveUpdatedSettings(
|
|
301
|
+
ctx,
|
|
302
|
+
configured,
|
|
303
|
+
{ ...configured, backend: { type: "transcribe-cpp" } },
|
|
304
|
+
"Using the local model",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
333
307
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
input.handleInput(data);
|
|
348
|
-
},
|
|
349
|
-
invalidate(): void {
|
|
350
|
-
container.invalidate();
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
});
|
|
308
|
+
const current =
|
|
309
|
+
configured.backend.type === "openai-asr" ? configured.backend : undefined;
|
|
310
|
+
const backend = await promptRemoteBackend(ctx, current);
|
|
311
|
+
if (!backend) return false;
|
|
312
|
+
|
|
313
|
+
const saved = await saveUpdatedSettings(
|
|
314
|
+
ctx,
|
|
315
|
+
configured,
|
|
316
|
+
{ ...configured, backend },
|
|
317
|
+
`Remote ASR configured (${backend.model})`,
|
|
318
|
+
);
|
|
319
|
+
if (saved) warnMissingApiKey(ctx, backend.apiKeyEnv);
|
|
320
|
+
return saved;
|
|
354
321
|
}
|
|
355
322
|
|
|
356
|
-
async function
|
|
323
|
+
async function choosePolishEnabled(
|
|
357
324
|
ctx: ExtensionContext,
|
|
358
|
-
current:
|
|
359
|
-
): Promise<
|
|
360
|
-
const choices: SingleSelectChoice<"
|
|
325
|
+
current: boolean,
|
|
326
|
+
): Promise<boolean | undefined> {
|
|
327
|
+
const choices: SingleSelectChoice<"off" | "on">[] = [
|
|
361
328
|
{
|
|
362
|
-
value: "
|
|
363
|
-
label: "
|
|
364
|
-
description: "
|
|
329
|
+
value: "off",
|
|
330
|
+
label: "Off",
|
|
331
|
+
description: "Insert transcripts exactly as ASR output them",
|
|
365
332
|
},
|
|
366
333
|
{
|
|
367
|
-
value: "
|
|
368
|
-
label: "
|
|
334
|
+
value: "on",
|
|
335
|
+
label: "On",
|
|
369
336
|
description:
|
|
370
|
-
"
|
|
337
|
+
"Refine transcripts after ASR with a Pi model and its provider auth, like context compaction; keeps the raw transcript on failure",
|
|
371
338
|
},
|
|
372
339
|
];
|
|
373
|
-
|
|
340
|
+
const chosen = await ctx.ui.custom<"off" | "on" | undefined>(
|
|
374
341
|
(tui, theme, keybindings, done) =>
|
|
375
342
|
new SingleSelectPicker(
|
|
376
343
|
tui,
|
|
377
344
|
theme,
|
|
378
345
|
keybindings,
|
|
379
346
|
choices,
|
|
380
|
-
current
|
|
381
|
-
{ title: "
|
|
347
|
+
current ? "on" : "off",
|
|
348
|
+
{ title: "Post-processing", cancelLabel: "back" },
|
|
382
349
|
done,
|
|
383
350
|
),
|
|
384
351
|
);
|
|
352
|
+
return chosen === undefined ? undefined : chosen === "on";
|
|
385
353
|
}
|
|
386
354
|
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return undefined;
|
|
355
|
+
function polishModelKey(model: PolishModelRef): string {
|
|
356
|
+
return `${model.provider}/${model.id}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function samePolishModel(
|
|
360
|
+
left: PolishModelRef | undefined,
|
|
361
|
+
right: PolishModelRef | undefined,
|
|
362
|
+
): boolean {
|
|
363
|
+
if (!left || !right) return left === right;
|
|
364
|
+
return left.provider === right.provider && left.id === right.id;
|
|
398
365
|
}
|
|
399
366
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
367
|
+
/** Pick Pi's current model (default) or any model with configured auth. */
|
|
368
|
+
async function choosePolishModel(
|
|
369
|
+
ctx: ExtensionContext,
|
|
370
|
+
current: PolishModelRef | undefined,
|
|
371
|
+
): Promise<"current" | PolishModelRef | undefined> {
|
|
372
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
373
|
+
const models = available.length > 0 ? available : ctx.modelRegistry.getAll();
|
|
374
|
+
const choices: SingleSelectChoice<string>[] = [
|
|
375
|
+
{
|
|
376
|
+
value: "current",
|
|
377
|
+
label: "Pi's current model",
|
|
378
|
+
description: "Use whichever model the agent is using (default)",
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
const seen = new Set<string>();
|
|
382
|
+
for (const model of models) {
|
|
383
|
+
const key = `${model.provider}/${model.id}`;
|
|
384
|
+
if (key === "current" || seen.has(key)) continue;
|
|
385
|
+
seen.add(key);
|
|
386
|
+
choices.push({ value: key, label: key, description: model.name });
|
|
404
387
|
}
|
|
405
|
-
|
|
388
|
+
if (current && !seen.has(polishModelKey(current))) {
|
|
389
|
+
choices.push({
|
|
390
|
+
value: polishModelKey(current),
|
|
391
|
+
label: polishModelKey(current),
|
|
392
|
+
description: "Configured post-processing model (not currently available)",
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const chosen = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) =>
|
|
396
|
+
new SingleSelectPicker(
|
|
397
|
+
tui,
|
|
398
|
+
theme,
|
|
399
|
+
keybindings,
|
|
400
|
+
choices,
|
|
401
|
+
current ? polishModelKey(current) : "current",
|
|
402
|
+
{ title: "Post-processing model", cancelLabel: "back" },
|
|
403
|
+
done,
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
if (chosen === undefined) return undefined;
|
|
407
|
+
if (chosen === "current") return "current";
|
|
408
|
+
const slash = chosen.indexOf("/");
|
|
409
|
+
return { provider: chosen.slice(0, slash), id: chosen.slice(slash + 1) };
|
|
406
410
|
}
|
|
407
411
|
|
|
408
|
-
/**
|
|
409
|
-
async function
|
|
412
|
+
/** Post-processing row flow: toggle, then choose Pi's current model or an override. */
|
|
413
|
+
async function editPostProcessing(
|
|
410
414
|
ctx: ExtensionContext,
|
|
411
415
|
configured: TranscribeSettings,
|
|
412
416
|
): Promise<boolean> {
|
|
413
|
-
const
|
|
414
|
-
if (
|
|
415
|
-
if (
|
|
416
|
-
if (configured.
|
|
417
|
+
const enabled = await choosePolishEnabled(ctx, configured.polishEnabled ?? false);
|
|
418
|
+
if (enabled === undefined) return false;
|
|
419
|
+
if (!enabled) {
|
|
420
|
+
if (!configured.polishEnabled) return false;
|
|
417
421
|
return saveUpdatedSettings(
|
|
418
422
|
ctx,
|
|
419
423
|
configured,
|
|
420
|
-
{ ...configured,
|
|
421
|
-
"
|
|
424
|
+
{ ...configured, polishEnabled: false },
|
|
425
|
+
"Post-processing disabled",
|
|
422
426
|
);
|
|
423
427
|
}
|
|
424
428
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
description: "OpenAI-compatible endpoint root, without the /chat/completions path",
|
|
430
|
-
value: current?.baseUrl ?? "",
|
|
431
|
-
placeholder: "https://api.example.com/v1",
|
|
432
|
-
validate: validateBaseUrl,
|
|
433
|
-
});
|
|
434
|
-
if (baseUrl === undefined) return false;
|
|
435
|
-
|
|
436
|
-
const model = await promptText(ctx, {
|
|
437
|
-
title: "ASR model",
|
|
438
|
-
value: current?.model ?? "",
|
|
439
|
-
placeholder: "whisper-1",
|
|
440
|
-
validate: (value) => (value ? undefined : "Model name is required"),
|
|
441
|
-
});
|
|
442
|
-
if (model === undefined) return false;
|
|
443
|
-
|
|
444
|
-
const apiKeyEnv = await promptText(ctx, {
|
|
445
|
-
title: "API key environment variable",
|
|
446
|
-
description: "The key itself is read from this variable at runtime; it is never stored in settings",
|
|
447
|
-
value: current?.apiKeyEnv ?? "",
|
|
448
|
-
placeholder: "OPENAI_API_KEY",
|
|
449
|
-
validate: validateEnvName,
|
|
450
|
-
});
|
|
451
|
-
if (apiKeyEnv === undefined) return false;
|
|
452
|
-
|
|
453
|
-
const backend: BackendSettings = { type: "openai-asr", baseUrl, model, apiKeyEnv };
|
|
454
|
-
if (
|
|
455
|
-
current &&
|
|
456
|
-
current.baseUrl === baseUrl &&
|
|
457
|
-
current.model === model &&
|
|
458
|
-
current.apiKeyEnv === apiKeyEnv
|
|
459
|
-
) {
|
|
429
|
+
const selection = await choosePolishModel(ctx, configured.polishModel);
|
|
430
|
+
if (selection === undefined) return false;
|
|
431
|
+
const polishModel = selection === "current" ? undefined : selection;
|
|
432
|
+
if (configured.polishEnabled && samePolishModel(configured.polishModel, polishModel)) {
|
|
460
433
|
return false;
|
|
461
434
|
}
|
|
462
|
-
|
|
435
|
+
return saveUpdatedSettings(
|
|
463
436
|
ctx,
|
|
464
437
|
configured,
|
|
465
|
-
{ ...configured,
|
|
466
|
-
|
|
438
|
+
{ ...configured, polishEnabled: true, polishModel },
|
|
439
|
+
polishModel
|
|
440
|
+
? `Post-processing enabled (${polishModelKey(polishModel)})`
|
|
441
|
+
: "Post-processing enabled (Pi's current model)",
|
|
467
442
|
);
|
|
468
|
-
if (saved && !process.env[apiKeyEnv]) {
|
|
469
|
-
ctx.ui.notify(
|
|
470
|
-
`${apiKeyEnv} is not set in Pi's environment; remote transcription will fall back to the local model until it is set`,
|
|
471
|
-
"warning",
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
return saved;
|
|
475
443
|
}
|
|
476
444
|
|
|
477
445
|
export async function openMacOSMicrophoneSettings(
|
|
@@ -549,6 +517,8 @@ export async function showSettingsMenu(
|
|
|
549
517
|
currentModelId: configured.model.id,
|
|
550
518
|
microphone: configured.microphone,
|
|
551
519
|
backend: configured.backend,
|
|
520
|
+
polishEnabled: configured.polishEnabled,
|
|
521
|
+
polishModel: configured.polishModel,
|
|
552
522
|
postActivation: "stay",
|
|
553
523
|
onPreferredLanguagesChange: async (preferredLanguages) => {
|
|
554
524
|
if (languagesEqual(preferredLanguages, configured.preferredLanguages)) return;
|
|
@@ -567,6 +537,11 @@ export async function showSettingsMenu(
|
|
|
567
537
|
continue;
|
|
568
538
|
}
|
|
569
539
|
|
|
540
|
+
if (action === "post-processing") {
|
|
541
|
+
await editPostProcessing(ctx, configured);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
570
545
|
if (action === "transcription-language") {
|
|
571
546
|
const transcriptionLanguage = await chooseTranscriptionLanguage(ctx, configured);
|
|
572
547
|
if (
|
package/src/settings.ts
CHANGED
|
@@ -28,6 +28,12 @@ export type BackendSettings =
|
|
|
28
28
|
| { type: "transcribe-cpp" }
|
|
29
29
|
| { type: "openai-asr"; baseUrl: string; model: string; apiKeyEnv: string };
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Explicit model for post-processing, resolved through Pi's model registry and
|
|
33
|
+
* its provider auth. Absent means the agent's current model.
|
|
34
|
+
*/
|
|
35
|
+
export type PolishModelRef = { provider: string; id: string };
|
|
36
|
+
|
|
31
37
|
function defaultChineseOutput(): ChineseOutput {
|
|
32
38
|
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
33
39
|
const subtags = locale.toLowerCase().split("-");
|
|
@@ -47,6 +53,10 @@ export type TranscribeSettings = {
|
|
|
47
53
|
transcriptionLanguage: TranscriptionLanguage;
|
|
48
54
|
chineseOutput: ChineseOutput;
|
|
49
55
|
microphone: MicrophoneSetting;
|
|
56
|
+
/** Optional post-processing with Pi's current model; absent means disabled (backward compatible). */
|
|
57
|
+
polishEnabled?: boolean;
|
|
58
|
+
/** Optional post-processing model override; absent uses the agent's current model. */
|
|
59
|
+
polishModel?: PolishModelRef;
|
|
50
60
|
model: {
|
|
51
61
|
source: "catalog";
|
|
52
62
|
id: string;
|
|
@@ -147,6 +157,13 @@ function validateBackend(value: unknown): BackendSettings | undefined {
|
|
|
147
157
|
return undefined;
|
|
148
158
|
}
|
|
149
159
|
|
|
160
|
+
function normalizePolishModel(value: unknown): PolishModelRef | undefined {
|
|
161
|
+
if (!isObject(value)) return undefined;
|
|
162
|
+
if (typeof value.provider !== "string" || !value.provider.trim()) return undefined;
|
|
163
|
+
if (typeof value.id !== "string" || !value.id.trim()) return undefined;
|
|
164
|
+
return { provider: value.provider.trim(), id: value.id.trim() };
|
|
165
|
+
}
|
|
166
|
+
|
|
150
167
|
function validateSettings(value: unknown): TranscribeSettings | undefined {
|
|
151
168
|
if (!isObject(value) || value.version !== SETTINGS_VERSION) return undefined;
|
|
152
169
|
const backend = validateBackend(value.backend);
|
|
@@ -163,6 +180,7 @@ function validateSettings(value: unknown): TranscribeSettings | undefined {
|
|
|
163
180
|
const preferredLanguages = normalizeLanguages(value.preferredLanguages);
|
|
164
181
|
const microphone = validateMicrophone(value.microphone);
|
|
165
182
|
if (!preferredLanguages || !microphone) return undefined;
|
|
183
|
+
const polishModel = normalizePolishModel(value.polishModel);
|
|
166
184
|
|
|
167
185
|
return {
|
|
168
186
|
version: SETTINGS_VERSION,
|
|
@@ -176,6 +194,8 @@ function validateSettings(value: unknown): TranscribeSettings | undefined {
|
|
|
176
194
|
),
|
|
177
195
|
chineseOutput: validateChineseOutput(value.chineseOutput),
|
|
178
196
|
microphone,
|
|
197
|
+
polishEnabled: value.polishEnabled === true,
|
|
198
|
+
...(polishModel ? { polishModel } : {}),
|
|
179
199
|
model: {
|
|
180
200
|
source: "catalog",
|
|
181
201
|
id: value.model.id,
|
|
@@ -244,6 +264,10 @@ type ModelSettingsOptions = {
|
|
|
244
264
|
microphone?: MicrophoneSetting;
|
|
245
265
|
/** Preserved across model re-selection; defaults to the local backend. */
|
|
246
266
|
backend?: BackendSettings;
|
|
267
|
+
/** Preserved across model re-selection; defaults to disabled. */
|
|
268
|
+
polishEnabled?: boolean;
|
|
269
|
+
/** Preserved across model re-selection; absent means the agent's current model. */
|
|
270
|
+
polishModel?: PolishModelRef;
|
|
247
271
|
};
|
|
248
272
|
|
|
249
273
|
export function settingsForModel(
|
|
@@ -268,6 +292,8 @@ export function settingsForModel(
|
|
|
268
292
|
),
|
|
269
293
|
chineseOutput: options.chineseOutput ?? defaultChineseOutput(),
|
|
270
294
|
microphone: { ...(options.microphone ?? DEFAULT_MICROPHONE) },
|
|
295
|
+
polishEnabled: options.polishEnabled ?? false,
|
|
296
|
+
...(options.polishModel ? { polishModel: options.polishModel } : {}),
|
|
271
297
|
model: { source: "catalog", id: modelId, path: modelPath },
|
|
272
298
|
};
|
|
273
299
|
}
|
package/src/try-it.ts
CHANGED
|
@@ -189,7 +189,7 @@ export class TryItPane implements Component {
|
|
|
189
189
|
const topChrome = (compact: boolean): string[] => [
|
|
190
190
|
...panelBorder(this.theme).render(width),
|
|
191
191
|
...(compact ? [] : [""]),
|
|
192
|
-
...onboardingHeader(this.theme, title,
|
|
192
|
+
...onboardingHeader(this.theme, title, 4).render(width),
|
|
193
193
|
...(compact ? [] : [""]),
|
|
194
194
|
...(compact ? [line(instructions(true))] : text(instructions(false))),
|
|
195
195
|
...(compact ? [] : [""]),
|
|
@@ -208,7 +208,12 @@ export class TryItPane implements Component {
|
|
|
208
208
|
...permission,
|
|
209
209
|
...render(setting("Shortcut", shortcut, this.keys.keyText("voice.tryIt.shortcut"), compact)),
|
|
210
210
|
...render(setting("Microphone", microphoneSummary(this.settings.microphone), this.keys.keyText("voice.tryIt.microphone"), compact)),
|
|
211
|
-
...render(setting(
|
|
211
|
+
...render(setting(
|
|
212
|
+
this.settings.backend.type === "openai-asr" ? "Fallback" : "Model",
|
|
213
|
+
modelName,
|
|
214
|
+
this.keys.keyText("voice.tryIt.model"),
|
|
215
|
+
compact,
|
|
216
|
+
)),
|
|
212
217
|
...(compact ? [] : [""]),
|
|
213
218
|
...text(hints),
|
|
214
219
|
...(compact ? [] : [""]),
|
|
@@ -230,7 +235,7 @@ export class TryItPane implements Component {
|
|
|
230
235
|
if (top.length + bottom.length + frameRows + 1 > budget) {
|
|
231
236
|
// Tiny terminals: drop settings, but keep the task and current actions visible.
|
|
232
237
|
top = [
|
|
233
|
-
...onboardingHeader(this.theme, title,
|
|
238
|
+
...onboardingHeader(this.theme, title, 4).render(width),
|
|
234
239
|
line(instructions(true)),
|
|
235
240
|
];
|
|
236
241
|
bottom = [...text(hints), ...panelBorder(this.theme).render(width)];
|
package/src/ui-components.ts
CHANGED
|
@@ -37,7 +37,7 @@ export function onboardingHeader(
|
|
|
37
37
|
theme: UiTheme,
|
|
38
38
|
title: string,
|
|
39
39
|
step: number,
|
|
40
|
-
total =
|
|
40
|
+
total = 4,
|
|
41
41
|
): Component {
|
|
42
42
|
const context = `Pi Voice setup · ${step} of ${total}`;
|
|
43
43
|
const compactContext = `${step} of ${total}`;
|
|
@@ -217,6 +217,8 @@ export class SingleSelectPicker<T extends string> extends Container implements F
|
|
|
217
217
|
searchable?: boolean;
|
|
218
218
|
maximumVisible?: number;
|
|
219
219
|
cancelLabel?: string;
|
|
220
|
+
/** Adds setup context and progress to the title. */
|
|
221
|
+
onboardingStep?: number;
|
|
220
222
|
/** Extra footer legend, appended after the ✓ current marker. */
|
|
221
223
|
legend?: string;
|
|
222
224
|
/** Custom row body after the cursor and ✓ markers; handles its own active styling. */
|
|
@@ -245,7 +247,11 @@ export class SingleSelectPicker<T extends string> extends Container implements F
|
|
|
245
247
|
|
|
246
248
|
this.addChild(panelBorder(theme));
|
|
247
249
|
this.addChild(new Spacer(1));
|
|
248
|
-
|
|
250
|
+
if (typeof options.onboardingStep === "number") {
|
|
251
|
+
this.addChild(onboardingHeader(theme, options.title, options.onboardingStep));
|
|
252
|
+
} else {
|
|
253
|
+
this.addChild(this.titleText);
|
|
254
|
+
}
|
|
249
255
|
if (this.subtitleText) this.addChild(this.subtitleText);
|
|
250
256
|
this.addChild(new Spacer(1));
|
|
251
257
|
if (options.searchable) {
|