pi-voicekit 0.1.3 → 0.2.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 +144 -41
- package/extensions/voice/config.ts +157 -16
- package/extensions/voice/onboarding.ts +2 -2
- package/extensions/voice/post-process-context.ts +185 -0
- package/extensions/voice/post-process-prompt.ts +95 -0
- package/extensions/voice/post-process.ts +219 -0
- package/extensions/voice/settings-panel.ts +267 -6
- package/extensions/voice/tts-local-models.ts +1 -1
- package/extensions/voice/tts-onboarding-overlay.ts +7 -3
- package/extensions/voice/tts-playback.ts +2 -2
- package/extensions/voice/ui-aura.ts +1 -1
- package/extensions/voice/ui-help-overlay.ts +2 -2
- package/extensions/voice/ui-locale-labels.ts +1 -1
- package/extensions/voice/ui-width.ts +1 -1
- package/extensions/voice.ts +683 -43
- package/package.json +6 -6
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import { matchesKey, Key, truncateToWidth } from "@earendil-works/pi-tui";
|
|
22
22
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
23
|
-
import type
|
|
23
|
+
import { saveGlobalVoiceFields, type VoiceConfig, type VoiceSettingsScope } from "./config";
|
|
24
|
+
import { polishModelOptions } from "./post-process";
|
|
24
25
|
import { LOCAL_MODELS, getLanguagesForLocalModel, type LocalModelInfo } from "./local";
|
|
25
26
|
import type { DeviceProfile, ModelFitness } from "./device";
|
|
26
27
|
import { getFreeDiskSpace, formatBytes, getModelsDir, scanHandyModels, importHandyModel } from "./model-download";
|
|
@@ -64,10 +65,13 @@ function buildTtsModelPickerRows(catalog: ReadonlyArray<TtsLocalModelInfo>): Pic
|
|
|
64
65
|
|
|
65
66
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
66
67
|
|
|
67
|
-
const TAB_IDS = ["general", "models", "downloaded", "speak", "device"] as const;
|
|
68
|
-
const TAB_LABELS = ["General", "Models", "Downloaded", "Speak", "Device"];
|
|
68
|
+
const TAB_IDS = ["general", "models", "downloaded", "speak", "device", "polish"] as const;
|
|
69
|
+
const TAB_LABELS = ["General", "Models", "Downloaded", "Speak", "Device", "Polish"];
|
|
69
70
|
type TabId = (typeof TAB_IDS)[number];
|
|
70
71
|
|
|
72
|
+
/** R30: the text shown when the field-level global writer refuses an unreadable settings file. */
|
|
73
|
+
const POLISH_WRITE_REFUSED = "The settings file could not be read — nothing was changed.";
|
|
74
|
+
|
|
71
75
|
export type PanelAction =
|
|
72
76
|
| { type: "download"; modelId: string }
|
|
73
77
|
| { type: "speak-test" }
|
|
@@ -87,6 +91,21 @@ export interface PanelDeps {
|
|
|
87
91
|
clearRecognizerCache: () => void;
|
|
88
92
|
resolveApiKey: () => string | undefined;
|
|
89
93
|
deepgramLanguages: { name: string; code: string; popular?: boolean }[];
|
|
94
|
+
/** Polish tab → Model row: the available models as canonical `provider/id` references. */
|
|
95
|
+
getPolishModels: () => { ref: string; label: string }[];
|
|
96
|
+
/**
|
|
97
|
+
* Polish tab → the two numeric rows: the scope this session's config was loaded
|
|
98
|
+
* from, so the write lands in the file the loader reads next time. `config.scope`
|
|
99
|
+
* is an in-memory field a project file can set itself, which would make the row
|
|
100
|
+
* look saved while a reload shows the old value.
|
|
101
|
+
*/
|
|
102
|
+
getPolishScope: () => VoiceSettingsScope;
|
|
103
|
+
/**
|
|
104
|
+
* Polish tab → Last dictation row: the latest polished dictation of this
|
|
105
|
+
* session, or undefined when there is none. `rawFullText`/`writtenText` are
|
|
106
|
+
* optional in the real history entry, so the row falls back to `text`.
|
|
107
|
+
*/
|
|
108
|
+
getLastDictation: () => { text: string; rawFullText?: string; writtenText?: string } | undefined;
|
|
90
109
|
/**
|
|
91
110
|
* Optional. If provided, panel renders use the host theme so colors track
|
|
92
111
|
* user theme choices (Catppuccin, Solarized, etc.). Without it, raw ANSI
|
|
@@ -122,7 +141,7 @@ export class VoiceSettingsPanel {
|
|
|
122
141
|
|
|
123
142
|
private tab = 0;
|
|
124
143
|
private row = 0;
|
|
125
|
-
private sub: "main" | "lang-picker" | "tts-model-picker" | "tts-voice-picker" = "main";
|
|
144
|
+
private sub: "main" | "lang-picker" | "tts-model-picker" | "tts-voice-picker" | "polish-model-picker" = "main";
|
|
126
145
|
|
|
127
146
|
// Models tab — grouped view
|
|
128
147
|
private modelSearch = "";
|
|
@@ -147,6 +166,14 @@ export class VoiceSettingsPanel {
|
|
|
147
166
|
private ttsVoiceSearch = "";
|
|
148
167
|
private ttsVoiceRow = 0;
|
|
149
168
|
|
|
169
|
+
// Polish model sub-picker (Polish tab → Model row). Rows come from
|
|
170
|
+
// `polishModelOptions` and are rebuilt on every open.
|
|
171
|
+
private polishModelChassis = new PickerChassis<{ label: string; value: string }>();
|
|
172
|
+
|
|
173
|
+
// R30: a failed field-level global write. The writer refuses to overwrite a settings
|
|
174
|
+
// file it cannot read; shown on the Polish tab until the next successful action.
|
|
175
|
+
private polishWriteError: string | null = null;
|
|
176
|
+
|
|
150
177
|
// Two-step delete on the Downloaded tab. When `x` is pressed, set the
|
|
151
178
|
// pending modelId + expiry timestamp; a second `x` within DELETE_CONFIRM_MS
|
|
152
179
|
// commits. Any other navigation cancels.
|
|
@@ -193,7 +220,7 @@ export class VoiceSettingsPanel {
|
|
|
193
220
|
// commands remain available (panel-less).
|
|
194
221
|
if (isPanelTooNarrow(width)) {
|
|
195
222
|
return [
|
|
196
|
-
` ${this.bold("pi-
|
|
223
|
+
` ${this.bold("pi-voicekit")}`,
|
|
197
224
|
` ${this.warning(`Terminal too narrow ${ICON.middot} resize to ${ICON.arrowRight} 60 cols`)}`,
|
|
198
225
|
` ${this.dim("Slash commands still work: /voice-speak, /voice-models, /voice-settings")}`,
|
|
199
226
|
];
|
|
@@ -212,7 +239,7 @@ export class VoiceSettingsPanel {
|
|
|
212
239
|
// backend/model/voice + active language. Width-tier aware:
|
|
213
240
|
// at "mid" (60-79) the row is trimmed; at "wide" (≥80) it
|
|
214
241
|
// shows the full picture.
|
|
215
|
-
lines.push(t(` ${this.bold("pi-
|
|
242
|
+
lines.push(t(` ${this.bold("pi-voicekit")} ${this.dim(this.p.formatDeviceSummary(device))}`));
|
|
216
243
|
lines.push(t(" " + this.renderStatusRow(widthTier(width) === "mid")));
|
|
217
244
|
lines.push(t(this.dim(" " + "─".repeat(Math.min(iw, 60)))));
|
|
218
245
|
|
|
@@ -233,6 +260,10 @@ export class VoiceSettingsPanel {
|
|
|
233
260
|
lines.push(...this.renderTtsVoicePicker(w, iw).map(t));
|
|
234
261
|
return lines;
|
|
235
262
|
}
|
|
263
|
+
if (this.sub === "polish-model-picker") {
|
|
264
|
+
lines.push(...this.renderPolishModelPicker(w, iw).map(t));
|
|
265
|
+
return lines;
|
|
266
|
+
}
|
|
236
267
|
|
|
237
268
|
// Tab content
|
|
238
269
|
const tabId = TAB_IDS[this.tab]!;
|
|
@@ -252,6 +283,9 @@ export class VoiceSettingsPanel {
|
|
|
252
283
|
case "device":
|
|
253
284
|
lines.push(...this.renderDevice(w, iw).map(t));
|
|
254
285
|
break;
|
|
286
|
+
case "polish":
|
|
287
|
+
lines.push(...this.renderPolish(w, iw).map(t));
|
|
288
|
+
break;
|
|
255
289
|
}
|
|
256
290
|
|
|
257
291
|
return lines;
|
|
@@ -270,6 +304,10 @@ export class VoiceSettingsPanel {
|
|
|
270
304
|
this.handleTtsVoiceInput(data);
|
|
271
305
|
return;
|
|
272
306
|
}
|
|
307
|
+
if (this.sub === "polish-model-picker") {
|
|
308
|
+
this.handlePolishModelInput(data);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
273
311
|
|
|
274
312
|
const tabId = TAB_IDS[this.tab]!;
|
|
275
313
|
|
|
@@ -827,6 +865,74 @@ export class VoiceSettingsPanel {
|
|
|
827
865
|
return lines;
|
|
828
866
|
}
|
|
829
867
|
|
|
868
|
+
// ─── Polish tab (post-processing) ─────────────────────────────────────
|
|
869
|
+
|
|
870
|
+
private renderPolish(_w: number, _iw: number): string[] {
|
|
871
|
+
const lines: string[] = [];
|
|
872
|
+
const { config } = this.p;
|
|
873
|
+
// Fallbacks mirror DEFAULT_CONFIG; the loader always fills both fields.
|
|
874
|
+
const turns = config.postProcessContextTurns ?? 2;
|
|
875
|
+
const timeoutMs = config.postProcessTimeoutMs ?? 8000;
|
|
876
|
+
const model = config.postProcessModel ?? "session";
|
|
877
|
+
const last = this.p.getLastDictation();
|
|
878
|
+
|
|
879
|
+
// Five rows, one per setting:
|
|
880
|
+
// 0: Enabled toggle (global-only)
|
|
881
|
+
// 1: Model picker (global-only)
|
|
882
|
+
// 2: Context turns (0-10)
|
|
883
|
+
// 3: Timeout ms (1000-30000, step 1000)
|
|
884
|
+
// 4: Last dictation — read-only raw/polished pair
|
|
885
|
+
// ←/→ switches tabs on every tab, so the two numeric rows adjust with ↵
|
|
886
|
+
// like the Speak tab's Speed row instead of stealing the arrow keys.
|
|
887
|
+
const rows: { label: string; value: string; hint?: string }[] = [
|
|
888
|
+
{
|
|
889
|
+
label: "Enabled",
|
|
890
|
+
value: config.postProcessEnabled !== false ? this.success("Enabled") : this.error("Disabled"),
|
|
891
|
+
hint: "toggle",
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
label: "Model",
|
|
895
|
+
value: model === "session" ? `Session model ${this.dim("(follow the chat)")}` : this.accent(model),
|
|
896
|
+
hint: "pick model ›",
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
label: "Context turns",
|
|
900
|
+
value: turns === 0 ? `0 ${this.dim("(no context sent)")}` : `${turns}`,
|
|
901
|
+
hint: "cycle",
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
label: "Timeout",
|
|
905
|
+
value: `${timeoutMs} ms`,
|
|
906
|
+
hint: "cycle",
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
label: "Last dictation",
|
|
910
|
+
value: last ? `${this.dim("RAW:")} ${last.rawFullText ?? last.text}` : this.dim("no polished dictation yet"),
|
|
911
|
+
},
|
|
912
|
+
];
|
|
913
|
+
|
|
914
|
+
// v7.2 — left-bar cursor + dim non-selected (HIG deference).
|
|
915
|
+
// 15 = "Last dictation" (14) plus the one-space gap before the value.
|
|
916
|
+
const labelW = 15;
|
|
917
|
+
for (let i = 0; i < rows.length; i++) {
|
|
918
|
+
const r = rows[i]!;
|
|
919
|
+
const isSelected = i === this.row;
|
|
920
|
+
const prefix = isSelected ? `${this.accent(ICON.cursorBar)} ` : ` `;
|
|
921
|
+
const label = isSelected ? r.label.padEnd(labelW) : this.dim(r.label.padEnd(labelW));
|
|
922
|
+
const hint = isSelected && r.hint ? this.dim(` [↵ ${r.hint}]`) : "";
|
|
923
|
+
lines.push(`${prefix}${label}${r.value}${hint}`);
|
|
924
|
+
// The same pair /voice-polish last prints, one line per half.
|
|
925
|
+
if (i === 4 && last) {
|
|
926
|
+
lines.push(` ${" ".repeat(labelW)}${this.dim("POLISHED:")} ${last.writtenText ?? last.text}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
lines.push("");
|
|
931
|
+
if (this.polishWriteError) lines.push(` ${this.error(this.polishWriteError)}`);
|
|
932
|
+
lines.push(this.dim(" ↵ change ←→/Tab tabs ↑↓ navigate esc close"));
|
|
933
|
+
return lines;
|
|
934
|
+
}
|
|
935
|
+
|
|
830
936
|
// ─── Language sub-picker ──────────────────────────────────────────────
|
|
831
937
|
|
|
832
938
|
private renderLangPicker(_w: number, _iw: number): string[] {
|
|
@@ -1003,6 +1109,50 @@ export class VoiceSettingsPanel {
|
|
|
1003
1109
|
return;
|
|
1004
1110
|
}
|
|
1005
1111
|
}
|
|
1112
|
+
} else if (tabId === "polish") {
|
|
1113
|
+
const { config } = this.p;
|
|
1114
|
+
switch (this.row) {
|
|
1115
|
+
case 0: {
|
|
1116
|
+
// `!== false` is how /voice-polish reads the flag; the default is on.
|
|
1117
|
+
const next = config.postProcessEnabled === false;
|
|
1118
|
+
// D7/R26: enablement is global-only — a scoped save strips it in a
|
|
1119
|
+
// project session and reports a success that silently reverts.
|
|
1120
|
+
// R30: the writer refuses an unreadable file; report it and leave the
|
|
1121
|
+
// in-memory value alone, so "nothing was changed" stays true.
|
|
1122
|
+
try {
|
|
1123
|
+
saveGlobalVoiceFields({ postProcessEnabled: next });
|
|
1124
|
+
} catch {
|
|
1125
|
+
this.polishWriteError = POLISH_WRITE_REFUSED;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
config.postProcessEnabled = next;
|
|
1129
|
+
this.polishWriteError = null;
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
case 1:
|
|
1133
|
+
this.openPolishModelPicker();
|
|
1134
|
+
break;
|
|
1135
|
+
case 2: {
|
|
1136
|
+
// 0-10, wrapping; the loader clamps anything hand-edited out of range.
|
|
1137
|
+
const current = config.postProcessContextTurns ?? 2;
|
|
1138
|
+
config.postProcessContextTurns = current >= 10 ? 0 : current + 1;
|
|
1139
|
+
this.savePolishNumbers();
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
case 3: {
|
|
1143
|
+
// Step 1000 but clamp to the documented maximum: the loader accepts any
|
|
1144
|
+
// integer in [1000, 30000], so a hand-edited value off the 1000 grid can
|
|
1145
|
+
// otherwise advance straight past the maximum and render a value the
|
|
1146
|
+
// next load silently clamps back.
|
|
1147
|
+
const current = config.postProcessTimeoutMs ?? 8000;
|
|
1148
|
+
config.postProcessTimeoutMs = current >= 30000 ? 1000 : Math.min(current + 1000, 30000);
|
|
1149
|
+
this.savePolishNumbers();
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
case 4:
|
|
1153
|
+
// Display-only row — the pair it shows has no action.
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1006
1156
|
}
|
|
1007
1157
|
}
|
|
1008
1158
|
|
|
@@ -1075,6 +1225,16 @@ export class VoiceSettingsPanel {
|
|
|
1075
1225
|
this.p.saveConfig(config, config.scope === "project" ? "project" : "global", cwd);
|
|
1076
1226
|
}
|
|
1077
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* Item 6: the polish numbers persist to the scope the config was loaded from — a
|
|
1230
|
+
* project file can set `config.scope` itself, and a write to the other file would
|
|
1231
|
+
* silently not be what the loader reads back. The other tabs keep using `save()`.
|
|
1232
|
+
*/
|
|
1233
|
+
private savePolishNumbers(): void {
|
|
1234
|
+
const { config, cwd } = this.p;
|
|
1235
|
+
this.p.saveConfig(config, this.p.getPolishScope(), cwd);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1078
1238
|
// ─── TTS Model picker ──────────────────────────────────────────────────
|
|
1079
1239
|
|
|
1080
1240
|
/** Lazy chassis getter — created on first model picker open. */
|
|
@@ -1386,6 +1546,105 @@ export class VoiceSettingsPanel {
|
|
|
1386
1546
|
}
|
|
1387
1547
|
}
|
|
1388
1548
|
|
|
1549
|
+
// ─── Polish model picker (Polish tab → Model row) ─────────────────────
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Rows come from `polishModelOptions`, so the session entry is always first
|
|
1553
|
+
* and every value is a reference the resolver accepts. Rebuilt on each open:
|
|
1554
|
+
* the list is cheap and can change between opens.
|
|
1555
|
+
*/
|
|
1556
|
+
private openPolishModelPicker(): void {
|
|
1557
|
+
const options = polishModelOptions(this.p.getPolishModels(), this.p.config.postProcessModel);
|
|
1558
|
+
const rows: PickerRow<{ label: string; value: string }>[] = options.map((option) => ({
|
|
1559
|
+
kind: "data",
|
|
1560
|
+
value: option,
|
|
1561
|
+
searchKey: `${option.label} ${option.value}`,
|
|
1562
|
+
}));
|
|
1563
|
+
this.polishModelChassis.setRows(rows);
|
|
1564
|
+
this.polishModelChassis.clearSearch();
|
|
1565
|
+
const active = this.p.config.postProcessModel ?? "session";
|
|
1566
|
+
const current = options.find((option) => option.value === active) ?? options[0];
|
|
1567
|
+
if (current) this.polishModelChassis.selectValue(current);
|
|
1568
|
+
this.sub = "polish-model-picker";
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
private renderPolishModelPicker(w: number, _iw: number): string[] {
|
|
1572
|
+
const lines: string[] = [];
|
|
1573
|
+
const chassis = this.polishModelChassis;
|
|
1574
|
+
|
|
1575
|
+
lines.push(` ${this.bold("Pick polish model")}`);
|
|
1576
|
+
const query = chassis.getQuery();
|
|
1577
|
+
lines.push(` ${this.dim("Search:")} ${query ? query : this.dim("type to filter…")}`);
|
|
1578
|
+
lines.push("");
|
|
1579
|
+
|
|
1580
|
+
const view = chassis.view({ maxVisible: 12, compact: w < 80 });
|
|
1581
|
+
if (view.kind === "empty") {
|
|
1582
|
+
lines.push(this.dim(` No matches for "${query}".`));
|
|
1583
|
+
lines.push("");
|
|
1584
|
+
lines.push(this.dim(" esc back bksp clear search"));
|
|
1585
|
+
return lines;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const selected = chassis.selected();
|
|
1589
|
+
for (const r of view.rows) {
|
|
1590
|
+
if (r.kind === "heading") continue;
|
|
1591
|
+
const option = r.value;
|
|
1592
|
+
const isSelected = option === selected;
|
|
1593
|
+
// v7.2 — accent left bar on the selected row, dim elsewhere.
|
|
1594
|
+
const prefix = isSelected ? `${this.accent(ICON.cursorBar)} ` : ` `;
|
|
1595
|
+
const label = isSelected ? this.accent(option.label) : this.dim(option.label);
|
|
1596
|
+
lines.push(`${prefix}${label}`);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (view.viewportStart > 0 || view.viewportEnd < view.totalSelectable) {
|
|
1600
|
+
lines.push(this.dim(` showing ${view.viewportStart + 1}–${view.viewportEnd} of ${view.totalSelectable}`));
|
|
1601
|
+
}
|
|
1602
|
+
lines.push("");
|
|
1603
|
+
lines.push(this.dim(" ↵ select esc back type to filter"));
|
|
1604
|
+
return lines;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
private handlePolishModelInput(data: string): void {
|
|
1608
|
+
const chassis = this.polishModelChassis;
|
|
1609
|
+
if (matchesKey(data, Key.escape)) {
|
|
1610
|
+
this.sub = "main";
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (matchesKey(data, Key.up)) {
|
|
1614
|
+
chassis.moveUp();
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (matchesKey(data, Key.down)) {
|
|
1618
|
+
chassis.moveDown();
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (matchesKey(data, Key.enter)) {
|
|
1622
|
+
const option = chassis.selected();
|
|
1623
|
+
if (!option) return;
|
|
1624
|
+
// D7/R26: the model choice is global-only — write it field by field to
|
|
1625
|
+
// the global file, like /voice-polish model does. R30: report a refusal
|
|
1626
|
+
// and keep the old value in memory.
|
|
1627
|
+
try {
|
|
1628
|
+
saveGlobalVoiceFields({ postProcessModel: option.value });
|
|
1629
|
+
} catch {
|
|
1630
|
+
this.polishWriteError = POLISH_WRITE_REFUSED;
|
|
1631
|
+
this.sub = "main";
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
this.p.config.postProcessModel = option.value;
|
|
1635
|
+
this.polishWriteError = null;
|
|
1636
|
+
this.sub = "main";
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (matchesKey(data, Key.backspace)) {
|
|
1640
|
+
chassis.backspaceSearch();
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
if (data.length === 1 && data >= " " && data <= "~") {
|
|
1644
|
+
chassis.appendSearchChar(data);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1389
1648
|
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
1390
1649
|
|
|
1391
1650
|
private getRowCount(tabId: TabId): number {
|
|
@@ -1401,6 +1660,8 @@ export class VoiceSettingsPanel {
|
|
|
1401
1660
|
}
|
|
1402
1661
|
case "speak":
|
|
1403
1662
|
return 6;
|
|
1663
|
+
case "polish":
|
|
1664
|
+
return 5;
|
|
1404
1665
|
case "device":
|
|
1405
1666
|
return 0;
|
|
1406
1667
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Visual design (no emoji per v7.1 hard constraint):
|
|
19
19
|
*
|
|
20
|
-
* ┌─ pi-
|
|
20
|
+
* ┌─ pi-voicekit TTS ─────────────────────────────────────────────┐
|
|
21
21
|
* │ │
|
|
22
22
|
* │ Voice output ready. │
|
|
23
23
|
* │ │
|
|
@@ -90,7 +90,11 @@ export class TtsOnboardingOverlay {
|
|
|
90
90
|
// v7.2: rounded corners for modal feel. Title sits inline on the
|
|
91
91
|
// top edge with thin spacing so the box reads as a "sheet"
|
|
92
92
|
// rather than a hard frame (HIG modal aesthetic).
|
|
93
|
-
|
|
93
|
+
// Title sits inline on the top edge; the pad is derived from the title's
|
|
94
|
+
// *visual* width so the frame stays exactly as wide as the body/bottom
|
|
95
|
+
// rows (brand length is data, not a magic constant).
|
|
96
|
+
const title = "pi-voicekit TTS";
|
|
97
|
+
const top = `${ICON.boxRoundedTL}${ICON.boxH.repeat(2)} ${bold(title)} ${ICON.boxH.repeat(Math.max(0, innerW - visualWidth(title) - 4))}${ICON.boxRoundedTR}`;
|
|
94
98
|
const bottom = `${ICON.boxRoundedBL}${ICON.boxH.repeat(innerW)}${ICON.boxRoundedBR}`;
|
|
95
99
|
const hr = `${ICON.boxV}${" ".repeat(innerW)}${ICON.boxV}`;
|
|
96
100
|
const row = (s: string): string => {
|
|
@@ -147,7 +151,7 @@ export class TtsOnboardingOverlay {
|
|
|
147
151
|
const dim = (s: string) => (t ? t.fg("dim", s) : s);
|
|
148
152
|
const accent = (s: string) => (t ? t.fg("accent", s) : s);
|
|
149
153
|
return [
|
|
150
|
-
` ${accent("pi-
|
|
154
|
+
` ${accent("pi-voicekit TTS")}`,
|
|
151
155
|
` ${dim("Voice output ready. Resize to ≥60 cols for the full hint.")}`,
|
|
152
156
|
` ${accent("[↵]")} ${dim("test")} ${accent("[m]")} ${dim("pick")} ${accent("[esc]")} ${dim("skip")}`,
|
|
153
157
|
];
|
|
@@ -210,7 +210,7 @@ export interface OpenPlaybackStreamOpts {
|
|
|
210
210
|
* Open a streaming playback sink. Returns `null` when no streaming-capable
|
|
211
211
|
* player is found on PATH — the caller should fall back to the file-based
|
|
212
212
|
* `play()` path. Player priority: `sox` (preferred — works on macOS via
|
|
213
|
-
* homebrew, Linux via apt/yum, ships with most pi-
|
|
213
|
+
* homebrew, Linux via apt/yum, ships with most pi-voicekit STT installs) →
|
|
214
214
|
* `paplay` (Linux PulseAudio) → null.
|
|
215
215
|
*
|
|
216
216
|
* Windows is intentionally unsupported here — PowerShell SoundPlayer
|
|
@@ -464,7 +464,7 @@ function pickStreamingPlayer(sampleRate: number): StreamingPlayerSpec | null {
|
|
|
464
464
|
if (process.platform === "linux" && binaryAvailable("paplay")) {
|
|
465
465
|
return {
|
|
466
466
|
cmd: "paplay",
|
|
467
|
-
args: ["--raw", `--rate=${sampleRate}`, "--format=s16le", "--channels=1", "--client-name=pi-
|
|
467
|
+
args: ["--raw", `--rate=${sampleRate}`, "--format=s16le", "--channels=1", "--client-name=pi-voicekit"],
|
|
468
468
|
};
|
|
469
469
|
}
|
|
470
470
|
// sox last-resort: cross-platform but has the macOS CoreAudio
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* "Auroral" visual language — v7.2 world-class polish.
|
|
3
3
|
*
|
|
4
|
-
* Three primitives that elevate pi-
|
|
4
|
+
* Three primitives that elevate pi-voicekit's visual identity from
|
|
5
5
|
* "minimal CLI" to "premium application":
|
|
6
6
|
*
|
|
7
7
|
* 1. Liquid Braille waveform — sub-cell vertical bars at 4-level
|
|
@@ -103,7 +103,7 @@ export class HelpOverlay {
|
|
|
103
103
|
const w = Math.max(60, Math.min(width - 2, 90));
|
|
104
104
|
const lines: string[] = [];
|
|
105
105
|
lines.push(
|
|
106
|
-
` ${bold("pi-
|
|
106
|
+
` ${bold("pi-voicekit")} ${dim(ICON.middot)} ${bold("Help")} ${dim(`${ICON.middot} press [esc] to close`)}`
|
|
107
107
|
);
|
|
108
108
|
lines.push(` ${dim(ICON.boxH.repeat(Math.min(w, 60)))}`);
|
|
109
109
|
for (const sec of HELP_SECTIONS) {
|
|
@@ -127,7 +127,7 @@ export class HelpOverlay {
|
|
|
127
127
|
const dim = (s: string) => (t ? t.fg("dim", s) : s);
|
|
128
128
|
const accent = (s: string) => (t ? t.fg("accent", s) : s);
|
|
129
129
|
const lines: string[] = [];
|
|
130
|
-
lines.push(` ${accent("pi-
|
|
130
|
+
lines.push(` ${accent("pi-voicekit Help")}`);
|
|
131
131
|
for (const sec of HELP_SECTIONS) {
|
|
132
132
|
lines.push("");
|
|
133
133
|
lines.push(` ${accent(sec.heading)}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Native-script + gender labels for the Voice picker (§8 of v7.1 plan).
|
|
3
3
|
*
|
|
4
|
-
* Hand-curated for the languages pi-
|
|
4
|
+
* Hand-curated for the languages pi-voicekit ships voices for, using the
|
|
5
5
|
* BCP-47 base-language tag (not script/region) as the lookup key. Three
|
|
6
6
|
* intentional omissions:
|
|
7
7
|
*
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* two halves of nothing.
|
|
9
9
|
*
|
|
10
10
|
* Hand-curated EAW Wide/Fullwidth ranges from Unicode 15.1 EastAsianWidth.txt
|
|
11
|
-
* — only the blocks pi-
|
|
11
|
+
* — only the blocks pi-voicekit actually ships labels for (CJK, Hangul,
|
|
12
12
|
* Hiragana/Katakana, fullwidth ASCII). Hindi/Devanagari and Arabic
|
|
13
13
|
* intentionally NOT covered: per the v7.1 plan their voices are rendered
|
|
14
14
|
* with romanized labels, so a precise width here is unnecessary and a
|