@tychenjiajun/pi-voice 0.4.0 → 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 +18 -6
- package/package.json +1 -1
- package/src/dictation-controller.ts +6 -2
- package/src/file-transcription.ts +8 -7
- package/src/onboarding.ts +19 -8
- package/src/openai-asr.ts +9 -43
- package/src/remote-asr.ts +3 -3
- package/src/runtime.ts +7 -7
- package/src/settings-menu.ts +54 -26
- package/src/settings.ts +79 -20
- package/src/transcription-service.ts +15 -13
- package/src/try-it.ts +8 -4
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Speech-to-text for Pi — on-device model or OpenAI-compatible remote ASR, with
|
|
|
4
4
|
|
|
5
5
|
Press a keyboard shortcut, talk, and the resulting transcript will be put directly into your chat.
|
|
6
6
|
|
|
7
|
-
> **Fork notice.** `@tychenjiajun/pi-voice` is a fork of [`@earendil-works/pi-voice`](https://github.com/earendil-works/pi-voice) that adds a remote ASR backend (any OpenAI-compatible `input_audio` chat-completions endpoint)
|
|
7
|
+
> **Fork notice.** `@tychenjiajun/pi-voice` is a fork of [`@earendil-works/pi-voice`](https://github.com/earendil-works/pi-voice) that adds a remote ASR backend (any OpenAI-compatible `input_audio` chat-completions endpoint) in an exclusive local-only or remote-only setup, plus compaction-style post-processing that refines a transcript with Pi's own model. It is published independently under a personal npm scope. See the [changelog](#changelog) for what changed.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -49,12 +49,13 @@ export OPENAI_API_KEY=sk-...
|
|
|
49
49
|
|
|
50
50
|
Behavior:
|
|
51
51
|
|
|
52
|
-
- **
|
|
52
|
+
- **Remote-only mode.** Remote settings store no local model: setup skips the model download step, and a failed remote request (network, quota, HTTP error, or an unset API key) surfaces as an error instead of being retried offline. Failures are logged to `~/.pi/agent/pi-voice.log`.
|
|
53
53
|
- **Batch-only.** Remote transcription happens when recording stops (the ASR request carries the whole clip), so there is no streaming preview.
|
|
54
|
-
- **16 kHz mono WAV** is uploaded as a base64 data URL.
|
|
55
|
-
- Switching
|
|
54
|
+
- **16 kHz mono WAV** is uploaded as a base64 WAV data URL.
|
|
55
|
+
- **Exclusive modes.** Switching **Transcription backend** to **Local model** asks you to pick a model first; switching to **Remote ASR** drops the local model from settings. The **Model** and **Transcription language** settings rows appear only in local mode.
|
|
56
|
+
- `transcribe_file` uses whichever mode is configured — in remote mode the file audio is sent to the endpoint instead of a local model.
|
|
56
57
|
|
|
57
|
-
Transcripts still go through the **Chinese output** script conversion preference; because a remote response carries no detected language, Han characters in the transcript are what trigger the conversion when the language is set to automatic.
|
|
58
|
+
Transcripts still go through the **Chinese output** script conversion preference; because a remote response carries no detected language, Han characters in the transcript are what trigger the conversion when the language is set to automatic. The **Chinese output** row stays reachable in remote mode, since the endpoint can always produce Chinese.
|
|
58
59
|
|
|
59
60
|
## Post-processing (compaction-style refinement)
|
|
60
61
|
|
|
@@ -111,10 +112,21 @@ export PI_VOICE_FFMPEG_PATH=/path/to/ffmpeg
|
|
|
111
112
|
|
|
112
113
|
The legacy `PI_TRANSCRIBE_FFMPEG_PATH` variable remains supported when `PI_VOICE_FFMPEG_PATH` is not set.
|
|
113
114
|
|
|
114
|
-
When FFmpeg is unavailable, `transcribe_file` reports platform-specific guidance to the agent. The agent should ask before running a package-manager command. Model setup is still explicit: run `/voice-settings` once in the interactive TUI to choose and,
|
|
115
|
+
When FFmpeg is unavailable, `transcribe_file` reports platform-specific guidance to the agent. The agent should ask before running a package-manager command. Model setup is still explicit: run `/voice-settings` once in the interactive TUI to choose a mode and, in local mode, confirm the download of a local model.
|
|
115
116
|
|
|
116
117
|
## Changelog
|
|
117
118
|
|
|
119
|
+
### 0.5.0
|
|
120
|
+
|
|
121
|
+
**Changed — Exclusive local / remote modes (no fallback)**
|
|
122
|
+
|
|
123
|
+
- Local and remote transcription are now mutually exclusive: remote settings store no local model, and selecting a local model drops any remote configuration. Settings files with both are normalized to remote-only on read.
|
|
124
|
+
- First-run setup in remote mode skips the model step entirely; no local model is downloaded (onboarding jumps from step 2 to step 4, the Try it pane).
|
|
125
|
+
- Removed the local fallback for remote ASR. Network, quota, HTTP, and unset-API-key failures now surface directly and are logged to `~/.pi/agent/pi-voice.log`.
|
|
126
|
+
- The settings menu hides the **Model** and **Transcription language** rows in remote mode and always shows **Chinese output** there; switching the backend to local asks you to pick a model first, and switching to remote clears the local model.
|
|
127
|
+
- Try it shows a **Local model** row in remote mode; choosing a model from it switches setup to local mode.
|
|
128
|
+
- `transcribe_file` works in either mode and reports the remote model name in its details when remote.
|
|
129
|
+
|
|
118
130
|
### 0.4.0
|
|
119
131
|
|
|
120
132
|
**Added — Backend choice in first-run setup**
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { CAPTURE_SAMPLE_RATE } from "./audio-constants.js";
|
|
2
2
|
import { describeError, log, logStep, sinceKeyPress } from "./log.js";
|
|
3
3
|
import { PcmChunker } from "./pcm-chunker.js";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
transcriptionModelLabel,
|
|
6
|
+
type MicrophoneSetting,
|
|
7
|
+
type TranscribeSettings,
|
|
8
|
+
} from "./settings.js";
|
|
5
9
|
import type { DictationReservation, TranscriptionService } from "./transcription-service.js";
|
|
6
10
|
|
|
7
11
|
/** What a recording captured. `interruption` is why it ended early, if it did. */
|
|
@@ -90,7 +94,7 @@ export class DictationController {
|
|
|
90
94
|
/** Reserves the model for a take; on failure the state says why. */
|
|
91
95
|
private reserve(settings: TranscribeSettings): Take | undefined {
|
|
92
96
|
this.readiness = "loading";
|
|
93
|
-
logStep("reserving model", settings
|
|
97
|
+
logStep("reserving model", transcriptionModelLabel(settings));
|
|
94
98
|
const reservedAt = this.now();
|
|
95
99
|
let reservation: DictationReservation;
|
|
96
100
|
try {
|
|
@@ -12,7 +12,7 @@ import { basename, join, resolve } from "node:path";
|
|
|
12
12
|
import { Type } from "typebox";
|
|
13
13
|
import { AsyncLimiter } from "./async-limiter.js";
|
|
14
14
|
import { polishTranscript } from "./polish.js";
|
|
15
|
-
import type
|
|
15
|
+
import { transcriptionModelLabel, type TranscribeSettings } from "./settings.js";
|
|
16
16
|
import type { TranscriptionService } from "./transcription-service.js";
|
|
17
17
|
|
|
18
18
|
const CONTEXT_LINE_LENGTH = 1_000;
|
|
@@ -84,7 +84,7 @@ export function registerFileTranscriptionTool(
|
|
|
84
84
|
pi.registerTool({
|
|
85
85
|
name: "transcribe_file",
|
|
86
86
|
label: "Transcribe File",
|
|
87
|
-
description: `Transcribe speech from a local audio or video file using Pi Voice's configured transcription backend (local model
|
|
87
|
+
description: `Transcribe speech from a local audio or video file using Pi Voice's configured transcription backend (local model or remote ASR endpoint), then applies Pi Voice's compaction-style post-processing when it is enabled. Requires the ffmpeg executable on PATH (or PI_VOICE_FFMPEG_PATH). Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)}; a complete transcript is saved to a temporary file when needed.`,
|
|
88
88
|
promptSnippet: "Transcribe speech from local audio or video files",
|
|
89
89
|
promptGuidelines: [
|
|
90
90
|
"Use transcribe_file when speech in a local audio or video file needs to be read, analyzed, or transcribed.",
|
|
@@ -118,10 +118,11 @@ export function registerFileTranscriptionTool(
|
|
|
118
118
|
|
|
119
119
|
const configured = await options.getSettings();
|
|
120
120
|
const service = await options.getService();
|
|
121
|
+
const modelLabel = transcriptionModelLabel(configured);
|
|
121
122
|
if (fileOperations.saturated) {
|
|
122
123
|
onUpdate?.({
|
|
123
124
|
content: [{ type: "text", text: "Waiting for file transcription capacity…" }],
|
|
124
|
-
details: { inputPath, modelId:
|
|
125
|
+
details: { inputPath, modelId: modelLabel, seconds: 0 },
|
|
125
126
|
});
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -129,13 +130,13 @@ export function registerFileTranscriptionTool(
|
|
|
129
130
|
if (fileDecoders.saturated) {
|
|
130
131
|
onUpdate?.({
|
|
131
132
|
content: [{ type: "text", text: `Waiting to decode ${basename(inputPath)}…` }],
|
|
132
|
-
details: { inputPath, modelId:
|
|
133
|
+
details: { inputPath, modelId: modelLabel, seconds: 0 },
|
|
133
134
|
});
|
|
134
135
|
}
|
|
135
136
|
const audio = await fileDecoders.run(async () => {
|
|
136
137
|
onUpdate?.({
|
|
137
138
|
content: [{ type: "text", text: `Decoding ${basename(inputPath)} with FFmpeg…` }],
|
|
138
|
-
details: { inputPath, modelId:
|
|
139
|
+
details: { inputPath, modelId: modelLabel, seconds: 0 },
|
|
139
140
|
});
|
|
140
141
|
const { decodeFileAudio } = await import("./file-audio.js");
|
|
141
142
|
return decodeFileAudio(inputPath, operationSignal);
|
|
@@ -150,7 +151,7 @@ export function registerFileTranscriptionTool(
|
|
|
150
151
|
],
|
|
151
152
|
details: {
|
|
152
153
|
inputPath,
|
|
153
|
-
modelId:
|
|
154
|
+
modelId: modelLabel,
|
|
154
155
|
seconds: audio.seconds,
|
|
155
156
|
},
|
|
156
157
|
});
|
|
@@ -170,7 +171,7 @@ export function registerFileTranscriptionTool(
|
|
|
170
171
|
: rawTranscript;
|
|
171
172
|
const details: FileTranscriptionDetails = {
|
|
172
173
|
inputPath,
|
|
173
|
-
modelId:
|
|
174
|
+
modelId: modelLabel,
|
|
174
175
|
seconds: audio.seconds,
|
|
175
176
|
};
|
|
176
177
|
|
package/src/onboarding.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { tryVoice } from "./try-it.js";
|
|
|
20
20
|
import {
|
|
21
21
|
DEFAULT_MICROPHONE,
|
|
22
22
|
settingsForModel,
|
|
23
|
+
settingsRemoteOnly,
|
|
23
24
|
writeSettings,
|
|
24
25
|
type BackendSettings,
|
|
25
26
|
type ChineseOutput,
|
|
@@ -96,7 +97,7 @@ export async function runModelSelection(
|
|
|
96
97
|
}),
|
|
97
98
|
(settings) => {
|
|
98
99
|
configured = settings;
|
|
99
|
-
currentModelId = settings.model
|
|
100
|
+
currentModelId = settings.model?.id;
|
|
100
101
|
},
|
|
101
102
|
);
|
|
102
103
|
|
|
@@ -237,7 +238,7 @@ export async function changeOnboardingModel(
|
|
|
237
238
|
expanded: true,
|
|
238
239
|
onboardingStep: 3,
|
|
239
240
|
})
|
|
240
|
-
: await chooseCatalogModel(ctx, languages, chosen?.model
|
|
241
|
+
: await chooseCatalogModel(ctx, languages, chosen?.model?.id ?? current.model?.id, {
|
|
241
242
|
postActivation: "advance",
|
|
242
243
|
onActivate: activation.activate,
|
|
243
244
|
cancelLabel: "back",
|
|
@@ -274,7 +275,7 @@ export type SetupMode = "local" | "remote";
|
|
|
274
275
|
|
|
275
276
|
/**
|
|
276
277
|
* First screen of first-run setup: on-device model, or a remote ASR endpoint.
|
|
277
|
-
*
|
|
278
|
+
* The modes are exclusive: remote setup downloads no local model.
|
|
278
279
|
*/
|
|
279
280
|
async function chooseSetupMode(ctx: ExtensionContext): Promise<SetupMode | undefined> {
|
|
280
281
|
const choices: SingleSelectChoice<SetupMode>[] = [
|
|
@@ -287,7 +288,7 @@ async function chooseSetupMode(ctx: ExtensionContext): Promise<SetupMode | undef
|
|
|
287
288
|
value: "remote",
|
|
288
289
|
label: "Remote ASR (OpenAI-compatible)",
|
|
289
290
|
description:
|
|
290
|
-
"Send audio to an OpenAI-compatible ASR endpoint;
|
|
291
|
+
"Send audio to an OpenAI-compatible ASR endpoint; no local model is downloaded",
|
|
291
292
|
},
|
|
292
293
|
];
|
|
293
294
|
return ctx.ui.custom<SetupMode | undefined>((tui, theme, keybindings, done) =>
|
|
@@ -323,7 +324,6 @@ export async function runOnboarding(
|
|
|
323
324
|
if (!backend) return undefined;
|
|
324
325
|
warnMissingApiKey(ctx, backend.apiKeyEnv);
|
|
325
326
|
}
|
|
326
|
-
const usingRemote = backend !== undefined;
|
|
327
327
|
|
|
328
328
|
let languages = defaultSpokenLanguages();
|
|
329
329
|
// Navigation cannot undo a completed settings commit. Keep it across a
|
|
@@ -336,6 +336,18 @@ export async function runOnboarding(
|
|
|
336
336
|
});
|
|
337
337
|
if (!chosen?.confirmed) return configured;
|
|
338
338
|
languages = chosen.languages;
|
|
339
|
+
|
|
340
|
+
// Remote mode is remote-only: no model step, settings are complete once
|
|
341
|
+
// the endpoint and languages are chosen.
|
|
342
|
+
if (backend?.type === "openai-asr") {
|
|
343
|
+
const settings = settingsRemoteOnly({ backend, shortcut, preferredLanguages: languages });
|
|
344
|
+
if (!(await saveOnboardingSettings(ctx, settings))) return configured;
|
|
345
|
+
configured = settings;
|
|
346
|
+
return finishOnboarding(ctx, configured, (current) =>
|
|
347
|
+
changeOnboardingModel(ctx, current),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
339
351
|
// The picks are judged on the benchmark rig; the Try it step measures
|
|
340
352
|
// the real wait on this machine.
|
|
341
353
|
const picks = recommendModels(CATALOG_MODELS, languages);
|
|
@@ -355,7 +367,6 @@ export async function runOnboarding(
|
|
|
355
367
|
activate,
|
|
356
368
|
{
|
|
357
369
|
onboardingStep: 3,
|
|
358
|
-
...(usingRemote ? { title: "Choose the local fallback model" } : {}),
|
|
359
370
|
},
|
|
360
371
|
);
|
|
361
372
|
await waitForCommits();
|
|
@@ -368,12 +379,12 @@ export async function runOnboarding(
|
|
|
368
379
|
continue;
|
|
369
380
|
}
|
|
370
381
|
|
|
371
|
-
const selection = await chooseCatalogModel(ctx, languages, configured?.model
|
|
382
|
+
const selection = await chooseCatalogModel(ctx, languages, configured?.model?.id, {
|
|
372
383
|
postActivation: "advance",
|
|
373
384
|
onActivate: activate,
|
|
374
385
|
cancelLabel: "back",
|
|
375
386
|
onboardingStep: 3,
|
|
376
|
-
title:
|
|
387
|
+
title: "Browse all models",
|
|
377
388
|
});
|
|
378
389
|
await waitForCommits();
|
|
379
390
|
if (selection?.type === "complete" && configured) {
|
package/src/openai-asr.ts
CHANGED
|
@@ -5,18 +5,11 @@ import { describeError, writeLog } from "./log.js";
|
|
|
5
5
|
import type { BackendSettings, ChineseOutput } from "./settings.js";
|
|
6
6
|
import type {
|
|
7
7
|
DictationStream,
|
|
8
|
-
TranscribeCppBackend,
|
|
9
8
|
TranscriptionOptions,
|
|
10
9
|
} from "./transcription.js";
|
|
11
10
|
|
|
12
11
|
type OpenAIAsrSettings = Extract<BackendSettings, { type: "openai-asr" }>;
|
|
13
12
|
|
|
14
|
-
/** The slice of the local backend this module needs for offline fallback. */
|
|
15
|
-
export type LocalFallback = Pick<
|
|
16
|
-
TranscribeCppBackend,
|
|
17
|
-
"prepare" | "transcribe" | "dispose"
|
|
18
|
-
>;
|
|
19
|
-
|
|
20
13
|
export type AsrRequest = {
|
|
21
14
|
/** `data:audio/wav;base64,…` of the whole recording. */
|
|
22
15
|
dataUrl: string;
|
|
@@ -136,8 +129,6 @@ export async function finishRemoteTranscript(
|
|
|
136
129
|
}
|
|
137
130
|
|
|
138
131
|
export type OpenAIAsrBackendDeps = {
|
|
139
|
-
/** Creates the local model used when a remote call fails. */
|
|
140
|
-
createFallback?: () => Promise<LocalFallback>;
|
|
141
132
|
/** Overrides the network transport (tests). */
|
|
142
133
|
transport?: AsrTransport;
|
|
143
134
|
};
|
|
@@ -145,12 +136,11 @@ export type OpenAIAsrBackendDeps = {
|
|
|
145
136
|
/**
|
|
146
137
|
* Batch-only remote backend: `startStream` resolves to undefined so the
|
|
147
138
|
* service records the whole clip and transcribes it after stop, exactly like
|
|
148
|
-
* a non-streaming local model. Remote failures
|
|
149
|
-
*
|
|
139
|
+
* a non-streaming local model. Remote failures surface to the caller; there
|
|
140
|
+
* is no local fallback.
|
|
150
141
|
*/
|
|
151
142
|
export class OpenAIAsrBackend {
|
|
152
143
|
private client: { transport: AsrTransport; dispose: () => void } | undefined;
|
|
153
|
-
private fallback: Promise<LocalFallback> | undefined;
|
|
154
144
|
private disposed = false;
|
|
155
145
|
private readonly injected: AsrTransport | undefined;
|
|
156
146
|
|
|
@@ -164,8 +154,8 @@ export class OpenAIAsrBackend {
|
|
|
164
154
|
async prepare(): Promise<void> {
|
|
165
155
|
if (this.disposed) throw new Error("Remote ASR backend has been disposed");
|
|
166
156
|
// The transport is created lazily in transcribe() so that a missing API
|
|
167
|
-
// key
|
|
168
|
-
//
|
|
157
|
+
// key surfaces per recording instead of failing prepare() and blocking
|
|
158
|
+
// every subsequent transcription.
|
|
169
159
|
}
|
|
170
160
|
|
|
171
161
|
/** Batch-only: the dictation service drops live chunks and transcribes on stop. */
|
|
@@ -184,9 +174,9 @@ export class OpenAIAsrBackend {
|
|
|
184
174
|
if (!transport) {
|
|
185
175
|
if (this.disposed) throw new Error("Remote ASR backend has been disposed");
|
|
186
176
|
// Lazily build the transport inside the try so a missing API key or
|
|
187
|
-
// any construction failure
|
|
188
|
-
// cached; a failure is not, so a later call retries once
|
|
189
|
-
// fixed.
|
|
177
|
+
// any construction failure is reported like a request failure.
|
|
178
|
+
// Success is cached; a failure is not, so a later call retries once
|
|
179
|
+
// the env is fixed.
|
|
190
180
|
const client = this.client ?? createOpenAITransport(this.config);
|
|
191
181
|
this.client = client;
|
|
192
182
|
transport = client.transport;
|
|
@@ -205,20 +195,12 @@ export class OpenAIAsrBackend {
|
|
|
205
195
|
return await finishRemoteTranscript(text, options);
|
|
206
196
|
} catch (error) {
|
|
207
197
|
if (signal?.aborted || this.disposed) throw error;
|
|
208
|
-
if (!this.deps.createFallback) throw error;
|
|
209
198
|
writeLog(
|
|
210
199
|
"warn",
|
|
211
200
|
"voice",
|
|
212
|
-
`remote ASR failed (${describeError(error)})
|
|
201
|
+
`remote ASR failed (${describeError(error)})`,
|
|
213
202
|
);
|
|
214
|
-
|
|
215
|
-
const local = await this.fallbackBackend();
|
|
216
|
-
return await local.transcribe(pcm, options);
|
|
217
|
-
} catch (fallbackError) {
|
|
218
|
-
throw new Error(
|
|
219
|
-
`Remote ASR failed (${describeError(error)}) and the local fallback failed (${describeError(fallbackError)})`,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
203
|
+
throw error;
|
|
222
204
|
}
|
|
223
205
|
}
|
|
224
206
|
|
|
@@ -227,21 +209,5 @@ export class OpenAIAsrBackend {
|
|
|
227
209
|
this.disposed = true;
|
|
228
210
|
this.client?.dispose();
|
|
229
211
|
this.client = undefined;
|
|
230
|
-
if (this.fallback) {
|
|
231
|
-
const local = await this.fallback.catch(() => undefined);
|
|
232
|
-
await local?.dispose().catch(() => undefined);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private fallbackBackend(): Promise<LocalFallback> {
|
|
237
|
-
if (!this.fallback) {
|
|
238
|
-
// A rejected fallback is not cached: a later attempt can re-create the
|
|
239
|
-
// local model instead of re-throwing a stale failure.
|
|
240
|
-
this.fallback = this.deps.createFallback!().catch((error) => {
|
|
241
|
-
this.fallback = undefined;
|
|
242
|
-
throw error;
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
return this.fallback;
|
|
246
212
|
}
|
|
247
213
|
}
|
package/src/remote-asr.ts
CHANGED
|
@@ -26,7 +26,7 @@ export async function chooseBackendType(
|
|
|
26
26
|
value: "openai-asr",
|
|
27
27
|
label: "Remote ASR (OpenAI-compatible)",
|
|
28
28
|
description:
|
|
29
|
-
"Upload audio to any OpenAI-compatible chat-completions ASR endpoint;
|
|
29
|
+
"Upload audio to any OpenAI-compatible chat-completions ASR endpoint; no local model is used",
|
|
30
30
|
},
|
|
31
31
|
];
|
|
32
32
|
return ctx.ui.custom<BackendType | undefined>((tui, theme, keybindings, done) =>
|
|
@@ -74,11 +74,11 @@ function remoteBackendsEqual(
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/** Warn when the named variable is unset, so remote calls
|
|
77
|
+
/** Warn when the named variable is unset, so remote calls fail until it is set. */
|
|
78
78
|
export function warnMissingApiKey(ctx: ExtensionContext, apiKeyEnv: string): void {
|
|
79
79
|
if (process.env[apiKeyEnv]) return;
|
|
80
80
|
ctx.ui.notify(
|
|
81
|
-
`${apiKeyEnv} is not set in Pi's environment; remote transcription will
|
|
81
|
+
`${apiKeyEnv} is not set in Pi's environment; remote transcription will fail until it is set`,
|
|
82
82
|
"warning",
|
|
83
83
|
);
|
|
84
84
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -145,7 +145,7 @@ export function createPiVoiceRuntime(
|
|
|
145
145
|
preferredLanguages: previous.preferredLanguages,
|
|
146
146
|
transcriptionLanguage: previous.transcriptionLanguage,
|
|
147
147
|
chineseOutput: previous.chineseOutput,
|
|
148
|
-
currentModelId: previous.model
|
|
148
|
+
currentModelId: previous.model?.id,
|
|
149
149
|
microphone: previous.microphone,
|
|
150
150
|
backend: previous.backend,
|
|
151
151
|
polishEnabled: previous.polishEnabled,
|
|
@@ -165,12 +165,12 @@ export function createPiVoiceRuntime(
|
|
|
165
165
|
ctx.ui.notify(settingsReadWarning, "warning");
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
if (settings && existsSync(settings.model.path)) {
|
|
168
|
+
if (settings && (!settings.model || existsSync(settings.model.path))) {
|
|
169
169
|
return { configured: settings, completedFirstRun: false };
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
const previous = settings;
|
|
173
|
-
if (settings) {
|
|
173
|
+
if (settings?.model) {
|
|
174
174
|
ctx.ui.notify(
|
|
175
175
|
`Configured model file is missing: ${settings.model.path}. Choose a model again; nothing will be downloaded without confirmation.`,
|
|
176
176
|
"warning",
|
|
@@ -189,15 +189,15 @@ export function createPiVoiceRuntime(
|
|
|
189
189
|
await loadSettingsOnce();
|
|
190
190
|
if (settingsReadWarning && !settings) {
|
|
191
191
|
throw new Error(
|
|
192
|
-
`${settingsReadWarning} Ask the user to run /voice-settings in Pi's interactive TUI to set up Pi Voice (a local model
|
|
192
|
+
`${settingsReadWarning} Ask the user to run /voice-settings in Pi's interactive TUI to set up Pi Voice (a local model or a remote ASR endpoint), then retry transcribe_file.`,
|
|
193
193
|
);
|
|
194
194
|
}
|
|
195
195
|
if (!settings) {
|
|
196
196
|
throw new Error(
|
|
197
|
-
"Pi Voice is not configured. Ask the user to run /voice-settings in Pi's interactive TUI once to set up Pi Voice (choose and download a local model,
|
|
197
|
+
"Pi Voice is not configured. Ask the user to run /voice-settings in Pi's interactive TUI once to set up Pi Voice (choose and download a local model, or configure a remote ASR endpoint), then retry transcribe_file.",
|
|
198
198
|
);
|
|
199
199
|
}
|
|
200
|
-
if (!existsSync(settings.model.path)) {
|
|
200
|
+
if (settings.model && !existsSync(settings.model.path)) {
|
|
201
201
|
throw new Error(
|
|
202
202
|
`The configured transcription model is missing: ${settings.model.path}. Ask the user to run /voice-settings and choose a model again, then retry transcribe_file.`,
|
|
203
203
|
);
|
|
@@ -408,7 +408,7 @@ export function createPiVoiceRuntime(
|
|
|
408
408
|
const { clearTranscribeWidget, showTranscribeStatus } = await loadVisualizer();
|
|
409
409
|
logStep("loading settings");
|
|
410
410
|
await loadSettingsOnce();
|
|
411
|
-
if (settings && existsSync(settings.model.path)) {
|
|
411
|
+
if (settings && (!settings.model || existsSync(settings.model.path))) {
|
|
412
412
|
showTranscribeStatus(ctx, "Starting microphone…");
|
|
413
413
|
} else {
|
|
414
414
|
// Setup panes replace only the editor, so a status line set here or by
|
package/src/settings-menu.ts
CHANGED
|
@@ -100,7 +100,9 @@ export function settingsHomeChoices(
|
|
|
100
100
|
configured: TranscribeSettings,
|
|
101
101
|
permission: Permission,
|
|
102
102
|
): SettingsHomeChoice[] {
|
|
103
|
-
|
|
103
|
+
// Local mode always carries a model; remote mode never does.
|
|
104
|
+
const model = configured.model ? getCatalogModel(configured.model.id) : undefined;
|
|
105
|
+
const remote = configured.backend.type === "openai-asr";
|
|
104
106
|
const choices: SettingsHomeChoice[] = [
|
|
105
107
|
{
|
|
106
108
|
value: "preferred-languages",
|
|
@@ -108,18 +110,24 @@ export function settingsHomeChoices(
|
|
|
108
110
|
summary: preferredLanguagesSummary(configured.preferredLanguages),
|
|
109
111
|
description: "Languages you speak, used to rank and recommend transcription models",
|
|
110
112
|
},
|
|
111
|
-
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
if (model) {
|
|
116
|
+
choices.push({
|
|
112
117
|
value: "model",
|
|
113
118
|
label: "Model",
|
|
114
119
|
summary: model.name,
|
|
115
120
|
description: "Switch between downloaded models, or download a new one",
|
|
116
|
-
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
choices.push(
|
|
117
125
|
{
|
|
118
126
|
value: "backend",
|
|
119
127
|
label: "Transcription backend",
|
|
120
128
|
summary: backendSummary(configured.backend),
|
|
121
129
|
description:
|
|
122
|
-
"Local model
|
|
130
|
+
"Local model or remote ASR endpoint; the two modes are exclusive",
|
|
123
131
|
},
|
|
124
132
|
{
|
|
125
133
|
value: "post-processing",
|
|
@@ -128,24 +136,25 @@ export function settingsHomeChoices(
|
|
|
128
136
|
description:
|
|
129
137
|
"Refine transcripts with Pi's current model after ASR, like context compaction",
|
|
130
138
|
},
|
|
131
|
-
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (model) {
|
|
142
|
+
choices.push({
|
|
132
143
|
value: "transcription-language",
|
|
133
144
|
label: "Transcription language",
|
|
134
145
|
summary: transcriptionLanguageSummary(configured.transcriptionLanguage, model),
|
|
135
|
-
description:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
: "Language expected in recordings, or automatic detection when supported",
|
|
139
|
-
},
|
|
140
|
-
];
|
|
146
|
+
description: "Language expected in recordings, or automatic detection when supported",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
141
149
|
|
|
142
150
|
// Chinese script conversion applies when the configured language is Chinese,
|
|
143
151
|
// when a preferred language is Chinese, or when a detecting model can pick
|
|
144
|
-
// Chinese on its own.
|
|
145
|
-
//
|
|
152
|
+
// Chinese on its own. Remote mode can always produce Chinese, so its row is
|
|
153
|
+
// never gated.
|
|
146
154
|
const detectsChinese =
|
|
147
|
-
model
|
|
155
|
+
model?.capabilities.languageDetection && model.languages.some(isChineseLanguage);
|
|
148
156
|
if (
|
|
157
|
+
remote ||
|
|
149
158
|
isChineseLanguage(configured.transcriptionLanguage) ||
|
|
150
159
|
configured.preferredLanguages.some(isChineseLanguage) ||
|
|
151
160
|
detectsChinese
|
|
@@ -264,7 +273,8 @@ async function chooseTranscriptionLanguage(
|
|
|
264
273
|
ctx: ExtensionContext,
|
|
265
274
|
configured: TranscribeSettings,
|
|
266
275
|
): Promise<TranscriptionLanguage | undefined> {
|
|
267
|
-
const model = getCatalogModel(configured.model.id)
|
|
276
|
+
const model = configured.model ? getCatalogModel(configured.model.id) : undefined;
|
|
277
|
+
if (!model) return undefined;
|
|
268
278
|
return ctx.ui.custom<TranscriptionLanguage | undefined>(
|
|
269
279
|
(tui, theme, keybindings, done) =>
|
|
270
280
|
createTranscriptionLanguagePicker(
|
|
@@ -297,12 +307,26 @@ async function editBackend(
|
|
|
297
307
|
if (!type) return false;
|
|
298
308
|
if (type === "transcribe-cpp") {
|
|
299
309
|
if (configured.backend.type === "transcribe-cpp") return false;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
// Remote-only settings carry no local model; pick one first. Every commit
|
|
311
|
+
// writes local-mode settings, so cancelling keeps the remote setup.
|
|
312
|
+
const updated = await runModelSelection(ctx, {
|
|
313
|
+
shortcut: configured.shortcut,
|
|
314
|
+
preferredLanguages: configured.preferredLanguages,
|
|
315
|
+
transcriptionLanguage: configured.transcriptionLanguage,
|
|
316
|
+
chineseOutput: configured.chineseOutput,
|
|
317
|
+
microphone: configured.microphone,
|
|
318
|
+
backend: { type: "transcribe-cpp" },
|
|
319
|
+
polishEnabled: configured.polishEnabled,
|
|
320
|
+
polishModel: configured.polishModel,
|
|
321
|
+
postActivation: "stay",
|
|
322
|
+
});
|
|
323
|
+
if (!updated) {
|
|
324
|
+
ctx.ui.notify("Local mode needs a local model; keeping remote ASR", "info");
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
Object.assign(configured, updated);
|
|
328
|
+
ctx.ui.notify("Using the local model", "info");
|
|
329
|
+
return true;
|
|
306
330
|
}
|
|
307
331
|
|
|
308
332
|
const current =
|
|
@@ -313,8 +337,8 @@ async function editBackend(
|
|
|
313
337
|
const saved = await saveUpdatedSettings(
|
|
314
338
|
ctx,
|
|
315
339
|
configured,
|
|
316
|
-
{ ...configured, backend },
|
|
317
|
-
`Remote ASR configured (${backend.model})`,
|
|
340
|
+
{ ...configured, backend, model: undefined },
|
|
341
|
+
`Remote ASR configured (${backend.model}); the local model is no longer used`,
|
|
318
342
|
);
|
|
319
343
|
if (saved) warnMissingApiKey(ctx, backend.apiKeyEnv);
|
|
320
344
|
return saved;
|
|
@@ -514,7 +538,7 @@ export async function showSettingsMenu(
|
|
|
514
538
|
preferredLanguages: configured.preferredLanguages,
|
|
515
539
|
transcriptionLanguage: configured.transcriptionLanguage,
|
|
516
540
|
chineseOutput: configured.chineseOutput,
|
|
517
|
-
currentModelId: configured.model
|
|
541
|
+
currentModelId: configured.model?.id,
|
|
518
542
|
microphone: configured.microphone,
|
|
519
543
|
backend: configured.backend,
|
|
520
544
|
polishEnabled: configured.polishEnabled,
|
|
@@ -550,8 +574,12 @@ export async function showSettingsMenu(
|
|
|
550
574
|
) {
|
|
551
575
|
continue;
|
|
552
576
|
}
|
|
553
|
-
const model =
|
|
554
|
-
|
|
577
|
+
const model = configured.model
|
|
578
|
+
? getCatalogModel(configured.model.id)
|
|
579
|
+
: undefined;
|
|
580
|
+
const summary = model
|
|
581
|
+
? transcriptionLanguageSummary(transcriptionLanguage, model)
|
|
582
|
+
: transcriptionLanguage;
|
|
555
583
|
await saveUpdatedSettings(
|
|
556
584
|
ctx,
|
|
557
585
|
configured,
|
package/src/settings.ts
CHANGED
|
@@ -21,8 +21,8 @@ export type ChineseOutput = "simplified" | "traditional-taiwan" | "traditional-h
|
|
|
21
21
|
/**
|
|
22
22
|
* Where recordings are transcribed. `transcribe-cpp` runs a local catalog
|
|
23
23
|
* model; `openai-asr` uploads to any OpenAI-compatible chat-completions ASR
|
|
24
|
-
* endpoint via the `input_audio` content block. The
|
|
25
|
-
*
|
|
24
|
+
* endpoint via the `input_audio` content block. The two are exclusive: remote
|
|
25
|
+
* mode stores no local model, and local mode has no remote configuration.
|
|
26
26
|
*/
|
|
27
27
|
export type BackendSettings =
|
|
28
28
|
| { type: "transcribe-cpp" }
|
|
@@ -57,7 +57,11 @@ export type TranscribeSettings = {
|
|
|
57
57
|
polishEnabled?: boolean;
|
|
58
58
|
/** Optional post-processing model override; absent uses the agent's current model. */
|
|
59
59
|
polishModel?: PolishModelRef;
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The local catalog model. Present only in local mode (backend
|
|
62
|
+
* `transcribe-cpp`); remote mode stores no local model.
|
|
63
|
+
*/
|
|
64
|
+
model?: {
|
|
61
65
|
source: "catalog";
|
|
62
66
|
id: string;
|
|
63
67
|
path: string;
|
|
@@ -164,6 +168,17 @@ function normalizePolishModel(value: unknown): PolishModelRef | undefined {
|
|
|
164
168
|
return { provider: value.provider.trim(), id: value.id.trim() };
|
|
165
169
|
}
|
|
166
170
|
|
|
171
|
+
function validateModelValue(
|
|
172
|
+
value: unknown,
|
|
173
|
+
): { id: string; path: string; catalogModel: CatalogModel } | undefined {
|
|
174
|
+
if (!isObject(value) || value.source !== "catalog") return undefined;
|
|
175
|
+
if (typeof value.id !== "string") return undefined;
|
|
176
|
+
const catalogModel = getCatalogModel(value.id);
|
|
177
|
+
if (!catalogModel) return undefined;
|
|
178
|
+
if (typeof value.path !== "string" || value.path.length === 0) return undefined;
|
|
179
|
+
return { id: value.id, path: value.path, catalogModel };
|
|
180
|
+
}
|
|
181
|
+
|
|
167
182
|
function validateSettings(value: unknown): TranscribeSettings | undefined {
|
|
168
183
|
if (!isObject(value) || value.version !== SETTINGS_VERSION) return undefined;
|
|
169
184
|
const backend = validateBackend(value.backend);
|
|
@@ -171,36 +186,43 @@ function validateSettings(value: unknown): TranscribeSettings | undefined {
|
|
|
171
186
|
const shortcut =
|
|
172
187
|
typeof value.shortcut === "string" ? normalizeShortcut(value.shortcut) : undefined;
|
|
173
188
|
if (!shortcut) return undefined;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
189
|
+
|
|
190
|
+
const selected = validateModelValue(value.model);
|
|
191
|
+
// Local mode needs a model; remote mode drops any leftover local model so
|
|
192
|
+
// settings are ever either local-only or remote-only, never both.
|
|
193
|
+
if (backend.type === "transcribe-cpp" && !selected) return undefined;
|
|
179
194
|
|
|
180
195
|
const preferredLanguages = normalizeLanguages(value.preferredLanguages);
|
|
181
196
|
const microphone = validateMicrophone(value.microphone);
|
|
182
197
|
if (!preferredLanguages || !microphone) return undefined;
|
|
183
198
|
const polishModel = normalizePolishModel(value.polishModel);
|
|
184
199
|
|
|
200
|
+
// The remote endpoint detects the language itself; the stored language only
|
|
201
|
+
// triggers Chinese script conversion, so any previous value is kept as-is.
|
|
202
|
+
const transcriptionLanguage =
|
|
203
|
+
backend.type === "openai-asr"
|
|
204
|
+
? typeof value.transcriptionLanguage === "string" && value.transcriptionLanguage
|
|
205
|
+
? value.transcriptionLanguage
|
|
206
|
+
: "auto"
|
|
207
|
+
: transcriptionLanguageForModel(
|
|
208
|
+
value.transcriptionLanguage,
|
|
209
|
+
selected!.catalogModel,
|
|
210
|
+
preferredLanguages,
|
|
211
|
+
);
|
|
212
|
+
|
|
185
213
|
return {
|
|
186
214
|
version: SETTINGS_VERSION,
|
|
187
215
|
backend,
|
|
188
216
|
shortcut,
|
|
189
217
|
preferredLanguages,
|
|
190
|
-
transcriptionLanguage
|
|
191
|
-
value.transcriptionLanguage,
|
|
192
|
-
model,
|
|
193
|
-
preferredLanguages,
|
|
194
|
-
),
|
|
218
|
+
transcriptionLanguage,
|
|
195
219
|
chineseOutput: validateChineseOutput(value.chineseOutput),
|
|
196
220
|
microphone,
|
|
197
221
|
polishEnabled: value.polishEnabled === true,
|
|
198
222
|
...(polishModel ? { polishModel } : {}),
|
|
199
|
-
|
|
200
|
-
source: "catalog",
|
|
201
|
-
|
|
202
|
-
path: value.model.path,
|
|
203
|
-
},
|
|
223
|
+
...(backend.type === "transcribe-cpp"
|
|
224
|
+
? { model: { source: "catalog" as const, id: selected!.id, path: selected!.path } }
|
|
225
|
+
: {}),
|
|
204
226
|
};
|
|
205
227
|
}
|
|
206
228
|
|
|
@@ -262,7 +284,10 @@ type ModelSettingsOptions = {
|
|
|
262
284
|
transcriptionLanguage?: TranscriptionLanguage;
|
|
263
285
|
chineseOutput?: ChineseOutput;
|
|
264
286
|
microphone?: MicrophoneSetting;
|
|
265
|
-
/**
|
|
287
|
+
/**
|
|
288
|
+
* Preserved across model re-selection when local; picking a model always
|
|
289
|
+
* yields local-only settings, since the backends are exclusive.
|
|
290
|
+
*/
|
|
266
291
|
backend?: BackendSettings;
|
|
267
292
|
/** Preserved across model re-selection; defaults to disabled. */
|
|
268
293
|
polishEnabled?: boolean;
|
|
@@ -282,7 +307,11 @@ export function settingsForModel(
|
|
|
282
307
|
];
|
|
283
308
|
return {
|
|
284
309
|
version: SETTINGS_VERSION,
|
|
285
|
-
|
|
310
|
+
// Exclusive modes: a selected local model drops any remote configuration.
|
|
311
|
+
backend:
|
|
312
|
+
options.backend && options.backend.type === "transcribe-cpp"
|
|
313
|
+
? options.backend
|
|
314
|
+
: { type: "transcribe-cpp" },
|
|
286
315
|
shortcut: options.shortcut ?? DEFAULT_SHORTCUT,
|
|
287
316
|
preferredLanguages,
|
|
288
317
|
transcriptionLanguage: transcriptionLanguageForModel(
|
|
@@ -297,3 +326,33 @@ export function settingsForModel(
|
|
|
297
326
|
model: { source: "catalog", id: modelId, path: modelPath },
|
|
298
327
|
};
|
|
299
328
|
}
|
|
329
|
+
|
|
330
|
+
type RemoteSettingsOptions = Omit<ModelSettingsOptions, "backend"> & {
|
|
331
|
+
backend: Extract<BackendSettings, { type: "openai-asr" }>;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/** Remote-only settings: no local model is stored, so the endpoint is the sole backend. */
|
|
335
|
+
export function settingsRemoteOnly(options: RemoteSettingsOptions): TranscribeSettings {
|
|
336
|
+
const preferredLanguages = [
|
|
337
|
+
...new Set((options.preferredLanguages ?? ["en"]).map(languageIdentity)),
|
|
338
|
+
];
|
|
339
|
+
return {
|
|
340
|
+
version: SETTINGS_VERSION,
|
|
341
|
+
backend: options.backend,
|
|
342
|
+
shortcut: options.shortcut ?? DEFAULT_SHORTCUT,
|
|
343
|
+
preferredLanguages,
|
|
344
|
+
// The endpoint detects the language itself; "auto" keeps the stored value
|
|
345
|
+
// from triggering Chinese conversion against a language that is not there.
|
|
346
|
+
transcriptionLanguage: options.transcriptionLanguage ?? "auto",
|
|
347
|
+
chineseOutput: options.chineseOutput ?? defaultChineseOutput(),
|
|
348
|
+
microphone: { ...(options.microphone ?? DEFAULT_MICROPHONE) },
|
|
349
|
+
polishEnabled: options.polishEnabled ?? false,
|
|
350
|
+
...(options.polishModel ? { polishModel: options.polishModel } : {}),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** The configured transcription source: local model id, or the remote ASR model name. */
|
|
355
|
+
export function transcriptionModelLabel(settings: TranscribeSettings): string {
|
|
356
|
+
if (settings.model) return settings.model.id;
|
|
357
|
+
return settings.backend.type === "openai-asr" ? settings.backend.model : "none";
|
|
358
|
+
}
|
|
@@ -59,13 +59,22 @@ type BackendFactory = (
|
|
|
59
59
|
settings: TranscribeSettings,
|
|
60
60
|
) => ReusableBackend | Promise<ReusableBackend>;
|
|
61
61
|
|
|
62
|
-
/** Local backends key on the model file; remote
|
|
62
|
+
/** Local backends key on the model file; remote keys on endpoint identity. */
|
|
63
|
+
function requireLocalModel(settings: TranscribeSettings): { path: string } {
|
|
64
|
+
if (!settings.model) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"No local model is configured. Run /voice-settings and choose a local model, or switch the transcription backend to remote ASR.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return settings.model;
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
function backendKeyFor(settings: TranscribeSettings): string {
|
|
64
73
|
if (settings.backend.type === "openai-asr") {
|
|
65
74
|
const { baseUrl, model } = settings.backend;
|
|
66
|
-
return `openai-asr:${baseUrl}|${model}
|
|
75
|
+
return `openai-asr:${baseUrl}|${model}`;
|
|
67
76
|
}
|
|
68
|
-
return settings.
|
|
77
|
+
return requireLocalModel(settings).path;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
function abortError(signal: AbortSignal): Error {
|
|
@@ -104,19 +113,12 @@ export class TranscriptionService {
|
|
|
104
113
|
|
|
105
114
|
constructor(
|
|
106
115
|
private readonly createBackend: BackendFactory = async (_key, settings) => {
|
|
107
|
-
const { TranscribeCppBackend } = await import("./transcription.js");
|
|
108
116
|
if (settings.backend.type === "openai-asr") {
|
|
109
117
|
const { OpenAIAsrBackend } = await import("./openai-asr.js");
|
|
110
|
-
|
|
111
|
-
return new OpenAIAsrBackend(settings.backend, {
|
|
112
|
-
createFallback: async () => {
|
|
113
|
-
const local = new TranscribeCppBackend(modelPath);
|
|
114
|
-
await local.prepare();
|
|
115
|
-
return local;
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
+
return new OpenAIAsrBackend(settings.backend);
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
+
const { TranscribeCppBackend } = await import("./transcription.js");
|
|
121
|
+
return new TranscribeCppBackend(requireLocalModel(settings).path);
|
|
120
122
|
},
|
|
121
123
|
) {}
|
|
122
124
|
|
package/src/try-it.ts
CHANGED
|
@@ -116,7 +116,10 @@ export class TryItPane implements Component {
|
|
|
116
116
|
|
|
117
117
|
const state = this.dictation.state;
|
|
118
118
|
const shortcut = displayShortcut(this.settings.shortcut);
|
|
119
|
-
const
|
|
119
|
+
const localModel = this.settings.model;
|
|
120
|
+
const modelName = localModel
|
|
121
|
+
? getCatalogModel(localModel.id)?.name ?? localModel.id
|
|
122
|
+
: "Not installed (remote mode)";
|
|
120
123
|
const title = "Try it";
|
|
121
124
|
const fg = (color: Parameters<UiTheme["fg"]>[0], text: string) => this.theme.fg(color, text);
|
|
122
125
|
const text = (value: string) => new Text(value, PANEL_PADDING, 0).render(width);
|
|
@@ -132,7 +135,8 @@ export class TryItPane implements Component {
|
|
|
132
135
|
});
|
|
133
136
|
} else if (
|
|
134
137
|
(state.phase === "idle" || state.phase === "ready") &&
|
|
135
|
-
this.dictation.modelState === "loading"
|
|
138
|
+
this.dictation.modelState === "loading" &&
|
|
139
|
+
localModel
|
|
136
140
|
) {
|
|
137
141
|
activity = fg("muted", `Loading ${modelName}… You can start recording now.`);
|
|
138
142
|
} else if (state.phase === "transcribing") {
|
|
@@ -147,7 +151,7 @@ export class TryItPane implements Component {
|
|
|
147
151
|
activity = fg("muted", formatTranscriptionSummary(speechSeconds, transcribeSeconds));
|
|
148
152
|
if (interruption) {
|
|
149
153
|
details = fg("warning", `The microphone stopped after ${speechSeconds.toFixed(1)}s: ${interruption.message}`);
|
|
150
|
-
} else if (needsFasterModel(speechSeconds, transcribeSeconds)) {
|
|
154
|
+
} else if (localModel && needsFasterModel(speechSeconds, transcribeSeconds)) {
|
|
151
155
|
details = fg("warning", `Slow on this machine? Press ${this.keys.keyText("voice.tryIt.model")} to try another model.`);
|
|
152
156
|
}
|
|
153
157
|
} else if (state.phase === "error") {
|
|
@@ -209,7 +213,7 @@ export class TryItPane implements Component {
|
|
|
209
213
|
...render(setting("Shortcut", shortcut, this.keys.keyText("voice.tryIt.shortcut"), compact)),
|
|
210
214
|
...render(setting("Microphone", microphoneSummary(this.settings.microphone), this.keys.keyText("voice.tryIt.microphone"), compact)),
|
|
211
215
|
...render(setting(
|
|
212
|
-
|
|
216
|
+
localModel ? "Model" : "Local model",
|
|
213
217
|
modelName,
|
|
214
218
|
this.keys.keyText("voice.tryIt.model"),
|
|
215
219
|
compact,
|