@juicesharp/rpiv-voice 1.4.2
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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/audio/error-log.ts +37 -0
- package/audio/hallucination-filter.ts +71 -0
- package/audio/mic-source.ts +38 -0
- package/audio/model-download.ts +268 -0
- package/audio/pcm.ts +45 -0
- package/audio/sherpa-onnx-node.d.ts +55 -0
- package/audio/stt-engine.ts +117 -0
- package/command/pipeline-runner.ts +238 -0
- package/command/splash-runner.ts +72 -0
- package/command/voice-command.ts +251 -0
- package/config/voice-config.ts +80 -0
- package/docs/cover.png +0 -0
- package/docs/cover.svg +173 -0
- package/docs/equalizer.svg +86 -0
- package/docs/overlay.jpg +0 -0
- package/docs/overlay.png +0 -0
- package/docs/vertical-cover.png +0 -0
- package/docs/vertical-cover.svg +239 -0
- package/index.ts +66 -0
- package/locales/de.json +39 -0
- package/locales/en.json +42 -0
- package/locales/es.json +39 -0
- package/locales/fr.json +39 -0
- package/locales/pt-BR.json +39 -0
- package/locales/pt.json +39 -0
- package/locales/ru.json +39 -0
- package/locales/uk.json +39 -0
- package/package.json +94 -0
- package/state/i18n-bridge.ts +51 -0
- package/state/key-router.ts +46 -0
- package/state/screen-intent.ts +27 -0
- package/state/selectors/contract.ts +13 -0
- package/state/selectors/derivations.ts +9 -0
- package/state/selectors/focus.ts +6 -0
- package/state/selectors/projections.ts +112 -0
- package/state/state-reducer.ts +197 -0
- package/state/state.ts +48 -0
- package/state/status-intent.ts +23 -0
- package/state/voice-session.ts +176 -0
- package/view/component-binding.ts +24 -0
- package/view/components/equalizer-view.ts +237 -0
- package/view/components/settings-field-view.ts +77 -0
- package/view/components/settings-form-view.ts +26 -0
- package/view/components/splash-view.ts +98 -0
- package/view/components/status-bar-view.ts +112 -0
- package/view/components/transcript-view.ts +50 -0
- package/view/overlay-view.ts +82 -0
- package/view/props-adapter.ts +29 -0
- package/view/screen-content-strategy.ts +58 -0
- package/view/stateful-view.ts +7 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createMic, type DecibriLike } from "../audio/mic-source.js";
|
|
3
|
+
import {
|
|
4
|
+
assertModelIntact,
|
|
5
|
+
ensureModelDownloaded,
|
|
6
|
+
getModelPaths,
|
|
7
|
+
isModelDownloaded,
|
|
8
|
+
ModelInstallError,
|
|
9
|
+
removeModelInstall,
|
|
10
|
+
} from "../audio/model-download.js";
|
|
11
|
+
import { createSttEngine, type SttEngine } from "../audio/stt-engine.js";
|
|
12
|
+
import { isHallucinationFilterEnabled, loadVoiceConfig } from "../config/voice-config.js";
|
|
13
|
+
import { getActiveLocale, t } from "../state/i18n-bridge.js";
|
|
14
|
+
import type { VoiceResult } from "../state/state-reducer.js";
|
|
15
|
+
import { VoiceSession } from "../state/voice-session.js";
|
|
16
|
+
import type { SplashPhase } from "../view/components/splash-view.js";
|
|
17
|
+
import { STATUS_BAR_PULSE_FRAME_INTERVAL_MS } from "../view/components/status-bar-view.js";
|
|
18
|
+
import { startDictationPipeline } from "./pipeline-runner.js";
|
|
19
|
+
import { runWithSplash } from "./splash-runner.js";
|
|
20
|
+
|
|
21
|
+
export const VOICE_COMMAND_NAME = "voice";
|
|
22
|
+
|
|
23
|
+
// Locales the bundled Whisper base multilingual model recognizes well. Mapped
|
|
24
|
+
// from i18n locale codes; entries not in this set fall back to Whisper's
|
|
25
|
+
// auto-detect (the multilingual model handles ~99 languages, but this keeps
|
|
26
|
+
// us aligned to locales we actively translate to and have tested).
|
|
27
|
+
const WHISPER_SUPPORTED_LANGUAGES: ReadonlySet<string> = new Set([
|
|
28
|
+
"de",
|
|
29
|
+
"en",
|
|
30
|
+
"es",
|
|
31
|
+
"fr",
|
|
32
|
+
"it",
|
|
33
|
+
"ja",
|
|
34
|
+
"pt",
|
|
35
|
+
"ru",
|
|
36
|
+
"uk",
|
|
37
|
+
"zh",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// IETF locale tags have an optional region subtag after the first hyphen
|
|
41
|
+
// (e.g. "pt-BR" → "pt"). Whisper's language hint expects just the base.
|
|
42
|
+
const LOCALE_REGION_SEPARATOR = "-";
|
|
43
|
+
const LOCALE_BASE_INDEX = 0;
|
|
44
|
+
|
|
45
|
+
function baseLanguage(locale: string): string {
|
|
46
|
+
return locale.split(LOCALE_REGION_SEPARATOR)[LOCALE_BASE_INDEX] ?? locale;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isWhisperSupported(language: string): boolean {
|
|
50
|
+
return WHISPER_SUPPORTED_LANGUAGES.has(language);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function whisperLanguageForLocale(locale: string | undefined): string | undefined {
|
|
54
|
+
if (!locale) return undefined;
|
|
55
|
+
const base = baseLanguage(locale);
|
|
56
|
+
return isWhisperSupported(base) ? base : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const SPLASH_INITIAL_ENGINE: SplashPhase = { kind: "loading_engine" };
|
|
60
|
+
function splashInitialDownload(): SplashPhase {
|
|
61
|
+
return { kind: "downloading", message: t("splash.preparing", "Preparing model…") };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type PreflightStage = "download" | "extract" | "verify" | "stale_install" | "engine" | "mic";
|
|
65
|
+
|
|
66
|
+
class PreflightError extends Error {
|
|
67
|
+
constructor(
|
|
68
|
+
public readonly stage: PreflightStage,
|
|
69
|
+
cause: unknown,
|
|
70
|
+
) {
|
|
71
|
+
super(`preflight failed at ${stage}`, { cause: cause as Error });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Preflight {
|
|
76
|
+
sttEngine: SttEngine;
|
|
77
|
+
mic: DecibriLike;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function registerVoiceCommand(pi: ExtensionAPI): void {
|
|
81
|
+
pi.registerCommand(VOICE_COMMAND_NAME, {
|
|
82
|
+
description: t("command.description", "Dictate text with your voice — local STT, no cloud"),
|
|
83
|
+
handler: (_args: string, ctx: ExtensionCommandContext) => handleVoiceCommand(ctx),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleVoiceCommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
88
|
+
if (!ctx.hasUI) {
|
|
89
|
+
ctx.ui.notify(t("error.requires_interactive", "/voice requires interactive mode"), "error");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const preflight = await runPreflight(ctx);
|
|
94
|
+
if (!preflight) return;
|
|
95
|
+
|
|
96
|
+
const result = await runDictationSession(ctx, preflight.sttEngine, preflight.mic);
|
|
97
|
+
if (result.intent === "commit" && result.transcript) {
|
|
98
|
+
ctx.ui.pasteToEditor(result.transcript);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runPreflight(ctx: ExtensionCommandContext): Promise<Preflight | null> {
|
|
103
|
+
try {
|
|
104
|
+
return await runWithSplash<Preflight>(
|
|
105
|
+
ctx,
|
|
106
|
+
{ initialPhase: isModelDownloaded() ? SPLASH_INITIAL_ENGINE : splashInitialDownload() },
|
|
107
|
+
async (controller) => {
|
|
108
|
+
if (!isModelDownloaded()) {
|
|
109
|
+
try {
|
|
110
|
+
await ensureModelDownloaded((p) => {
|
|
111
|
+
const message = p.message ?? "";
|
|
112
|
+
if (p.phase === "downloading")
|
|
113
|
+
controller.setPhase({
|
|
114
|
+
kind: "downloading",
|
|
115
|
+
message,
|
|
116
|
+
percent: p.percent,
|
|
117
|
+
bytesReceived: p.bytesReceived,
|
|
118
|
+
totalBytes: p.totalBytes,
|
|
119
|
+
});
|
|
120
|
+
else if (p.phase === "extracting") controller.setPhase({ kind: "extracting", message });
|
|
121
|
+
else if (p.phase === "verifying") controller.setPhase({ kind: "verifying", message });
|
|
122
|
+
});
|
|
123
|
+
} catch (e) {
|
|
124
|
+
const stage = e instanceof ModelInstallError ? e.stage : "download";
|
|
125
|
+
throw new PreflightError(stage, e);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
controller.setPhase({ kind: "loading_engine" });
|
|
130
|
+
let sttEngine: SttEngine;
|
|
131
|
+
try {
|
|
132
|
+
// The sentinel proves a *prior* run finished cleanly — it does not
|
|
133
|
+
// guarantee the .onnx files are still present and valid. Re-verify
|
|
134
|
+
// here so a tampered/partially-deleted install gets caught with a
|
|
135
|
+
// clear message + auto-recovery instead of an opaque native crash
|
|
136
|
+
// deep inside sherpa-onnx.
|
|
137
|
+
try {
|
|
138
|
+
assertModelIntact();
|
|
139
|
+
} catch (e) {
|
|
140
|
+
removeModelInstall();
|
|
141
|
+
throw new PreflightError("stale_install", e);
|
|
142
|
+
}
|
|
143
|
+
const paths = getModelPaths();
|
|
144
|
+
// Pre-set Whisper's language hint from the active i18n locale when we
|
|
145
|
+
// have a confident mapping; otherwise fall back to Whisper's built-in
|
|
146
|
+
// per-utterance auto-detect. Pre-setting trades a small amount of
|
|
147
|
+
// flexibility for noticeably better accuracy in the user's primary
|
|
148
|
+
// language and avoids the first-utterance detection latency.
|
|
149
|
+
sttEngine = await createSttEngine({
|
|
150
|
+
encoderPath: paths.encoderPath,
|
|
151
|
+
decoderPath: paths.decoderPath,
|
|
152
|
+
tokensPath: paths.tokensPath,
|
|
153
|
+
language: whisperLanguageForLocale(getActiveLocale()),
|
|
154
|
+
});
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// Preserve the inner stage tag (e.g. "stale_install") instead of
|
|
157
|
+
// flattening every failure in this block to "engine" — the user-
|
|
158
|
+
// facing copy in preflightUserMessage diverges per stage.
|
|
159
|
+
if (e instanceof PreflightError) throw e;
|
|
160
|
+
throw new PreflightError("engine", e);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
controller.setPhase({ kind: "initializing_mic" });
|
|
164
|
+
let mic: DecibriLike;
|
|
165
|
+
try {
|
|
166
|
+
mic = await createMic();
|
|
167
|
+
} catch (e) {
|
|
168
|
+
sttEngine.release();
|
|
169
|
+
throw new PreflightError("mic", e);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { sttEngine, mic };
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
if (e instanceof PreflightError) {
|
|
177
|
+
ctx.ui.notify(preflightUserMessage(e.stage), "error");
|
|
178
|
+
} else {
|
|
179
|
+
ctx.ui.notify(t("error.engine_load_failed", "Failed to load STT model."), "error");
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function preflightUserMessage(stage: PreflightStage): string {
|
|
186
|
+
switch (stage) {
|
|
187
|
+
case "download":
|
|
188
|
+
return t("error.model_download_failed", "Failed to download STT model. Check your internet connection.");
|
|
189
|
+
case "extract":
|
|
190
|
+
return t("error.model_extract_failed", "Downloaded STT model archive is corrupt. Please retry.");
|
|
191
|
+
case "verify":
|
|
192
|
+
return t("error.model_verify_failed", "STT model files are incomplete after download. Please retry.");
|
|
193
|
+
case "stale_install":
|
|
194
|
+
return t(
|
|
195
|
+
"error.model_stale_install",
|
|
196
|
+
"STT model files were removed or corrupted. They will be redownloaded on next launch.",
|
|
197
|
+
);
|
|
198
|
+
case "engine":
|
|
199
|
+
return t("error.engine_load_failed", "Failed to load STT model.");
|
|
200
|
+
case "mic":
|
|
201
|
+
return t(
|
|
202
|
+
"error.mic_unavailable",
|
|
203
|
+
"Microphone unavailable. Check that an input device is connected and that Pi has microphone permission.",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runDictationSession(
|
|
209
|
+
ctx: ExtensionCommandContext,
|
|
210
|
+
sttEngine: SttEngine,
|
|
211
|
+
mic: DecibriLike,
|
|
212
|
+
): Promise<VoiceResult> {
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const persistedConfig = loadVoiceConfig();
|
|
215
|
+
|
|
216
|
+
let pipelineHandle:
|
|
217
|
+
| {
|
|
218
|
+
setPaused: (v: boolean) => void;
|
|
219
|
+
setHallucinationFilterEnabled: (v: boolean) => void;
|
|
220
|
+
stop: () => void;
|
|
221
|
+
}
|
|
222
|
+
| undefined;
|
|
223
|
+
let pulseTick: ReturnType<typeof setInterval> | undefined;
|
|
224
|
+
|
|
225
|
+
const result = await ctx.ui.custom<VoiceResult>((tui, theme, _kb, done) => {
|
|
226
|
+
const session = new VoiceSession({
|
|
227
|
+
tui,
|
|
228
|
+
theme,
|
|
229
|
+
persistedConfig,
|
|
230
|
+
deps: {
|
|
231
|
+
pasteToEditor: (text) => ctx.ui.pasteToEditor(text),
|
|
232
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
233
|
+
abort: () => controller.abort(),
|
|
234
|
+
stopMic: () => pipelineHandle?.stop(),
|
|
235
|
+
setPipelinePaused: (paused) => pipelineHandle?.setPaused(paused),
|
|
236
|
+
setHallucinationFilterEnabled: (enabled) => pipelineHandle?.setHallucinationFilterEnabled(enabled),
|
|
237
|
+
},
|
|
238
|
+
done,
|
|
239
|
+
});
|
|
240
|
+
pipelineHandle = startDictationPipeline(mic, sttEngine, session, controller.signal, {
|
|
241
|
+
hallucinationFilterEnabled: isHallucinationFilterEnabled(persistedConfig),
|
|
242
|
+
});
|
|
243
|
+
pulseTick = setInterval(() => session.tickPulse(), STATUS_BAR_PULSE_FRAME_INTERVAL_MS);
|
|
244
|
+
return session.component;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (pulseTick) clearInterval(pulseTick);
|
|
248
|
+
if (!controller.signal.aborted) controller.abort();
|
|
249
|
+
sttEngine.release();
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* voice-config — best-effort persistence of optional rpiv-voice settings
|
|
3
|
+
* (custom model path, named mic device) at `~/.config/rpiv-voice/voice.json`.
|
|
4
|
+
*
|
|
5
|
+
* Both load and save are crash-resistant: malformed JSON or filesystem errors
|
|
6
|
+
* resolve to "no config". The file is created with 0600 perms because it may
|
|
7
|
+
* one day hold device IDs that the user doesn't want world-readable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
|
|
14
|
+
// ── Filesystem layout ────────────────────────────────────────────────────────
|
|
15
|
+
const CONFIG_DIR_NAME = "rpiv-voice";
|
|
16
|
+
const CONFIG_FILE_NAME = "voice.json";
|
|
17
|
+
const CONFIG_DIR = join(homedir(), ".config", CONFIG_DIR_NAME);
|
|
18
|
+
const CONFIG_PATH = join(CONFIG_DIR, CONFIG_FILE_NAME);
|
|
19
|
+
|
|
20
|
+
// ── Permissions & formatting ─────────────────────────────────────────────────
|
|
21
|
+
const CONFIG_FILE_MODE = 0o600;
|
|
22
|
+
const JSON_INDENT = 2;
|
|
23
|
+
|
|
24
|
+
// ── Module-level singleton key (cleared by test/setup beforeEach) ────────────
|
|
25
|
+
const VOICE_STATE_KEY = Symbol.for("rpiv-voice");
|
|
26
|
+
|
|
27
|
+
export interface VoiceConfig {
|
|
28
|
+
readonly hallucinationFilterEnabled?: boolean;
|
|
29
|
+
readonly equalizerEnabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The hallucination filter defaults to ENABLED. We only persist the off-state
|
|
34
|
+
* to keep voice.json minimal, which means "field absent" must be read as
|
|
35
|
+
* "enabled". Centralizing this rule here keeps the three readers in sync —
|
|
36
|
+
* persisted config, pipeline runtime options, and the in-flight settings
|
|
37
|
+
* draft all decode the absence the same way.
|
|
38
|
+
*/
|
|
39
|
+
export function isHallucinationFilterEnabled(config: { hallucinationFilterEnabled?: boolean }): boolean {
|
|
40
|
+
return config.hallucinationFilterEnabled !== false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The equalizer defaults to DISABLED. Mirror of the hallucination-filter
|
|
45
|
+
* decoding rule but with the inverted polarity: only the on-state is
|
|
46
|
+
* persisted, "field absent" reads as "disabled".
|
|
47
|
+
*/
|
|
48
|
+
export function isEqualizerEnabled(config: { equalizerEnabled?: boolean }): boolean {
|
|
49
|
+
return config.equalizerEnabled === true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function loadVoiceConfig(): VoiceConfig {
|
|
53
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as unknown;
|
|
56
|
+
if (parsed === null || typeof parsed !== "object") return {};
|
|
57
|
+
return parsed as VoiceConfig;
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function saveVoiceConfig(config: VoiceConfig): void {
|
|
64
|
+
try {
|
|
65
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
66
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, JSON_INDENT)}\n`, "utf-8");
|
|
67
|
+
} catch {
|
|
68
|
+
// best-effort
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
chmodSync(CONFIG_PATH, CONFIG_FILE_MODE);
|
|
72
|
+
} catch {
|
|
73
|
+
// best-effort — perms are advisory; the user's umask still wins.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function __resetState(): void {
|
|
78
|
+
const g = globalThis as unknown as { [k: symbol]: unknown };
|
|
79
|
+
delete g[VOICE_STATE_KEY];
|
|
80
|
+
}
|
package/docs/cover.png
ADDED
|
Binary file
|
package/docs/cover.svg
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 800" role="img" aria-label="rpiv-voice — Local voice dictation overlay for Pi" preserveAspectRatio="xMidYMid meet">
|
|
2
|
+
<title>rpiv-voice — Local voice dictation overlay for Pi</title>
|
|
3
|
+
<defs>
|
|
4
|
+
<style>
|
|
5
|
+
.mono { font-family: 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', ui-monospace, Menlo, Consolas, monospace; }
|
|
6
|
+
.ink { fill: #F4EFE6; }
|
|
7
|
+
.muted { fill: #8C8678; }
|
|
8
|
+
.dim { fill: #4F4A41; }
|
|
9
|
+
.fade { fill: #6A645A; }
|
|
10
|
+
.accent { fill: #FF7A3D; }
|
|
11
|
+
.mint { fill: #6FD0B8; }
|
|
12
|
+
.rec { fill: #E5594E; }
|
|
13
|
+
</style>
|
|
14
|
+
<filter id="grain" x="0" y="0" width="100%" height="100%">
|
|
15
|
+
<feTurbulence type="fractalNoise" baseFrequency="1.4" numOctaves="2" seed="31"/>
|
|
16
|
+
<feColorMatrix values="0 0 0 0 0.96 0 0 0 0 0.94 0 0 0 0 0.88 0 0 0 0.04 0"/>
|
|
17
|
+
</filter>
|
|
18
|
+
<radialGradient id="vignette" cx="50%" cy="50%" r="70%">
|
|
19
|
+
<stop offset="55%" stop-color="#070605" stop-opacity="0"/>
|
|
20
|
+
<stop offset="100%" stop-color="#000" stop-opacity="0.7"/>
|
|
21
|
+
</radialGradient>
|
|
22
|
+
<linearGradient id="titleGrad" x1="0" y1="0" x2="0" y2="1">
|
|
23
|
+
<stop offset="0%" stop-color="#1C1813"/>
|
|
24
|
+
<stop offset="100%" stop-color="#14110D"/>
|
|
25
|
+
</linearGradient>
|
|
26
|
+
<filter id="winShadow" x="-10%" y="-10%" width="120%" height="130%">
|
|
27
|
+
<feDropShadow dx="0" dy="14" stdDeviation="22" flood-color="#000" flood-opacity="0.6"/>
|
|
28
|
+
</filter>
|
|
29
|
+
<clipPath id="wclip"><rect x="64" y="56" width="1152" height="688" rx="12"/></clipPath>
|
|
30
|
+
<linearGradient id="eqFade" x1="0" x2="1" y1="0" y2="0">
|
|
31
|
+
<stop offset="0%" stop-color="#6FD0B8" stop-opacity="0.05"/>
|
|
32
|
+
<stop offset="50%" stop-color="#6FD0B8" stop-opacity="0.95"/>
|
|
33
|
+
<stop offset="100%" stop-color="#6FD0B8" stop-opacity="0.05"/>
|
|
34
|
+
</linearGradient>
|
|
35
|
+
<radialGradient id="recPulse" cx="50%" cy="50%" r="50%">
|
|
36
|
+
<stop offset="0%" stop-color="#E5594E" stop-opacity="0.45"/>
|
|
37
|
+
<stop offset="100%" stop-color="#E5594E" stop-opacity="0"/>
|
|
38
|
+
</radialGradient>
|
|
39
|
+
</defs>
|
|
40
|
+
|
|
41
|
+
<rect width="1280" height="800" fill="#070605"/>
|
|
42
|
+
<rect width="1280" height="800" filter="url(#grain)" opacity="0.45"/>
|
|
43
|
+
<rect width="1280" height="800" fill="url(#vignette)"/>
|
|
44
|
+
|
|
45
|
+
<rect x="64" y="56" width="1152" height="688" rx="12" fill="#0E0D0B" filter="url(#winShadow)"/>
|
|
46
|
+
|
|
47
|
+
<g clip-path="url(#wclip)">
|
|
48
|
+
<rect x="64" y="56" width="1152" height="44" fill="url(#titleGrad)"/>
|
|
49
|
+
<line x1="64" y1="100" x2="1216" y2="100" stroke="#2A2620"/>
|
|
50
|
+
<line x1="64" y1="57" x2="1216" y2="57" stroke="#3A352D" stroke-opacity="0.6"/>
|
|
51
|
+
<circle cx="92" cy="78" r="7" fill="#FF5F57"/>
|
|
52
|
+
<circle cx="120" cy="78" r="7" fill="#FFBD2E"/>
|
|
53
|
+
<circle cx="148" cy="78" r="7" fill="#28C840"/>
|
|
54
|
+
<text class="mono muted" x="640" y="84" font-size="14" letter-spacing="1.4" text-anchor="middle">~/rpiv-mono — pi — rpiv-voice</text>
|
|
55
|
+
|
|
56
|
+
<!-- Section label -->
|
|
57
|
+
<text class="mono dim" x="120" y="156" font-size="12" letter-spacing="2.4">/VOICE — LIVE TRANSCRIPT</text>
|
|
58
|
+
|
|
59
|
+
<!-- Live transcript: committed lines + dim rolling partial -->
|
|
60
|
+
<text class="mono ink" x="120" y="218" font-size="32">Refactor the splash runner so download progress shows</text>
|
|
61
|
+
<text class="mono ink" x="120" y="262" font-size="32">percent and bytes on a single line, then keep the same</text>
|
|
62
|
+
<text class="mono ink" x="120" y="306" font-size="32">format for extracting and verifying.</text>
|
|
63
|
+
<text class="mono fade" x="120" y="350" font-size="32">Add a unit test that asserts the whisper hallucination…</text>
|
|
64
|
+
|
|
65
|
+
<!-- Mint divider (matches rpiv-voice overlay) -->
|
|
66
|
+
<line x1="120" y1="392" x2="1160" y2="392" stroke="#6FD0B8" stroke-opacity="0.65" stroke-width="1.25"/>
|
|
67
|
+
|
|
68
|
+
<!-- Equalizer strip — live VAD waveform feel -->
|
|
69
|
+
<g transform="translate(120 416)" fill="url(#eqFade)">
|
|
70
|
+
<rect x="0" y="36" rx="3" width="10" height="6"/>
|
|
71
|
+
<rect x="16" y="32" rx="3" width="10" height="14"/>
|
|
72
|
+
<rect x="32" y="20" rx="3" width="10" height="38"/>
|
|
73
|
+
<rect x="48" y="26" rx="3" width="10" height="26"/>
|
|
74
|
+
<rect x="64" y="14" rx="3" width="10" height="50"/>
|
|
75
|
+
<rect x="80" y="22" rx="3" width="10" height="34"/>
|
|
76
|
+
<rect x="96" y="8" rx="3" width="10" height="62"/>
|
|
77
|
+
<rect x="112" y="18" rx="3" width="10" height="42"/>
|
|
78
|
+
<rect x="128" y="4" rx="3" width="10" height="70"/>
|
|
79
|
+
<rect x="144" y="14" rx="3" width="10" height="50"/>
|
|
80
|
+
<rect x="160" y="22" rx="3" width="10" height="34"/>
|
|
81
|
+
<rect x="176" y="10" rx="3" width="10" height="58"/>
|
|
82
|
+
<rect x="192" y="20" rx="3" width="10" height="38"/>
|
|
83
|
+
<rect x="208" y="6" rx="3" width="10" height="66"/>
|
|
84
|
+
<rect x="224" y="16" rx="3" width="10" height="46"/>
|
|
85
|
+
<rect x="240" y="0" rx="3" width="10" height="78"/>
|
|
86
|
+
<rect x="256" y="12" rx="3" width="10" height="54"/>
|
|
87
|
+
<rect x="272" y="22" rx="3" width="10" height="34"/>
|
|
88
|
+
<rect x="288" y="8" rx="3" width="10" height="62"/>
|
|
89
|
+
<rect x="304" y="18" rx="3" width="10" height="42"/>
|
|
90
|
+
<rect x="320" y="2" rx="3" width="10" height="74"/>
|
|
91
|
+
<rect x="336" y="14" rx="3" width="10" height="50"/>
|
|
92
|
+
<rect x="352" y="24" rx="3" width="10" height="30"/>
|
|
93
|
+
<rect x="368" y="10" rx="3" width="10" height="58"/>
|
|
94
|
+
<rect x="384" y="20" rx="3" width="10" height="38"/>
|
|
95
|
+
<rect x="400" y="6" rx="3" width="10" height="66"/>
|
|
96
|
+
<rect x="416" y="16" rx="3" width="10" height="46"/>
|
|
97
|
+
<rect x="432" y="22" rx="3" width="10" height="34"/>
|
|
98
|
+
<rect x="448" y="12" rx="3" width="10" height="54"/>
|
|
99
|
+
<rect x="464" y="26" rx="3" width="10" height="26"/>
|
|
100
|
+
<rect x="480" y="18" rx="3" width="10" height="42"/>
|
|
101
|
+
<rect x="496" y="10" rx="3" width="10" height="58"/>
|
|
102
|
+
<rect x="512" y="22" rx="3" width="10" height="34"/>
|
|
103
|
+
<rect x="528" y="14" rx="3" width="10" height="50"/>
|
|
104
|
+
<rect x="544" y="28" rx="3" width="10" height="22"/>
|
|
105
|
+
<rect x="560" y="20" rx="3" width="10" height="38"/>
|
|
106
|
+
<rect x="576" y="12" rx="3" width="10" height="54"/>
|
|
107
|
+
<rect x="592" y="24" rx="3" width="10" height="30"/>
|
|
108
|
+
<rect x="608" y="18" rx="3" width="10" height="42"/>
|
|
109
|
+
<rect x="624" y="30" rx="3" width="10" height="18"/>
|
|
110
|
+
<rect x="640" y="22" rx="3" width="10" height="34"/>
|
|
111
|
+
<rect x="656" y="14" rx="3" width="10" height="50"/>
|
|
112
|
+
<rect x="672" y="26" rx="3" width="10" height="26"/>
|
|
113
|
+
<rect x="688" y="34" rx="3" width="10" height="10"/>
|
|
114
|
+
<rect x="704" y="20" rx="3" width="10" height="38"/>
|
|
115
|
+
<rect x="720" y="32" rx="3" width="10" height="14"/>
|
|
116
|
+
<rect x="736" y="24" rx="3" width="10" height="30"/>
|
|
117
|
+
<rect x="752" y="36" rx="3" width="10" height="6"/>
|
|
118
|
+
<rect x="768" y="28" rx="3" width="10" height="22"/>
|
|
119
|
+
<rect x="784" y="38" rx="3" width="10" height="4"/>
|
|
120
|
+
<rect x="800" y="34" rx="3" width="10" height="10"/>
|
|
121
|
+
<rect x="816" y="38" rx="3" width="10" height="4"/>
|
|
122
|
+
<rect x="832" y="36" rx="3" width="10" height="6"/>
|
|
123
|
+
<rect x="848" y="38" rx="3" width="10" height="4"/>
|
|
124
|
+
<rect x="864" y="38" rx="3" width="10" height="4"/>
|
|
125
|
+
<rect x="880" y="38" rx="3" width="10" height="4"/>
|
|
126
|
+
<rect x="896" y="38" rx="3" width="10" height="4"/>
|
|
127
|
+
<rect x="912" y="38" rx="3" width="10" height="4"/>
|
|
128
|
+
<rect x="928" y="38" rx="3" width="10" height="4"/>
|
|
129
|
+
<rect x="944" y="38" rx="3" width="10" height="4"/>
|
|
130
|
+
<rect x="960" y="38" rx="3" width="10" height="4"/>
|
|
131
|
+
<rect x="976" y="38" rx="3" width="10" height="4"/>
|
|
132
|
+
<rect x="992" y="38" rx="3" width="10" height="4"/>
|
|
133
|
+
<rect x="1008" y="38" rx="3" width="10" height="4"/>
|
|
134
|
+
<rect x="1024" y="38" rx="3" width="10" height="4"/>
|
|
135
|
+
</g>
|
|
136
|
+
|
|
137
|
+
<!-- Status bar row: red glyph, timer, footer hints -->
|
|
138
|
+
<circle cx="138" cy="568" r="20" fill="url(#recPulse)"/>
|
|
139
|
+
<text class="rec mono" x="118" y="576" font-size="26">●</text>
|
|
140
|
+
<text class="mono" font-size="20" y="574">
|
|
141
|
+
<tspan x="158" class="muted" font-weight="700">0:14</tspan>
|
|
142
|
+
<tspan dx="14" class="dim">·</tspan>
|
|
143
|
+
<tspan dx="14" class="ink" font-weight="700">Enter</tspan>
|
|
144
|
+
<tspan dx="8" class="muted">to paste</tspan>
|
|
145
|
+
<tspan dx="14" class="dim">·</tspan>
|
|
146
|
+
<tspan dx="14" class="ink" font-weight="700">Space</tspan>
|
|
147
|
+
<tspan dx="8" class="muted">to pause</tspan>
|
|
148
|
+
<tspan dx="14" class="dim">·</tspan>
|
|
149
|
+
<tspan dx="14" class="ink" font-weight="700">Tab</tspan>
|
|
150
|
+
<tspan dx="8" class="muted">for settings</tspan>
|
|
151
|
+
<tspan dx="14" class="dim">·</tspan>
|
|
152
|
+
<tspan dx="14" class="ink" font-weight="700">Esc</tspan>
|
|
153
|
+
<tspan dx="8" class="muted">to cancel</tspan>
|
|
154
|
+
</text>
|
|
155
|
+
|
|
156
|
+
<text class="mono muted" x="120" y="640" font-size="13" letter-spacing="2.4">100% ON-DEVICE · WHISPER MULTILINGUAL · ~99 LANGUAGES · NO CLOUD · NO API KEYS</text>
|
|
157
|
+
|
|
158
|
+
<!-- Footer chrome -->
|
|
159
|
+
<rect x="64" y="708" width="1152" height="36" fill="url(#titleGrad)"/>
|
|
160
|
+
<line x1="64" y1="708" x2="1216" y2="708" stroke="#2A2620"/>
|
|
161
|
+
<text class="mono" font-size="14" y="731" letter-spacing="1">
|
|
162
|
+
<tspan x="92" class="accent" font-weight="700">▌ rpiv-voice</tspan>
|
|
163
|
+
<tspan dx="14" class="dim">│</tspan>
|
|
164
|
+
<tspan dx="14" class="mint">on-device STT</tspan>
|
|
165
|
+
<tspan dx="14" class="dim">│</tspan>
|
|
166
|
+
<tspan dx="14" class="muted">whisper-base int8</tspan>
|
|
167
|
+
<tspan dx="14" class="dim">│</tspan>
|
|
168
|
+
<tspan dx="14" class="muted">npm:@juicesharp/rpiv-voice</tspan>
|
|
169
|
+
</text>
|
|
170
|
+
</g>
|
|
171
|
+
|
|
172
|
+
<rect x="64" y="56" width="1152" height="688" rx="12" fill="none" stroke="#3A352D" stroke-width="1"/>
|
|
173
|
+
</svg>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1028 80" role="img" aria-label="rpiv-voice live audio equalizer" preserveAspectRatio="xMidYMid meet">
|
|
2
|
+
<title>rpiv-voice live audio equalizer</title>
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="eqFade" x1="0" x2="1" y1="0" y2="0">
|
|
5
|
+
<stop offset="0%" stop-color="#6FD0B8" stop-opacity="0.05"/>
|
|
6
|
+
<stop offset="50%" stop-color="#6FD0B8" stop-opacity="0.95"/>
|
|
7
|
+
<stop offset="100%" stop-color="#6FD0B8" stop-opacity="0.05"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<g fill="url(#eqFade)">
|
|
11
|
+
<rect x="0" y="39" width="6" height="2" rx="3"/>
|
|
12
|
+
<rect x="14" y="39" width="6" height="2" rx="3"/>
|
|
13
|
+
<rect x="28" y="39" width="6" height="2" rx="3"/>
|
|
14
|
+
<rect x="42" y="39" width="6" height="2" rx="3"/>
|
|
15
|
+
<rect x="56" y="39" width="6" height="2" rx="3"/>
|
|
16
|
+
<rect x="70" y="37" width="6" height="6" rx="3"/>
|
|
17
|
+
<rect x="84" y="36" width="6" height="8" rx="3"/>
|
|
18
|
+
<rect x="98" y="33" width="6" height="14" rx="3"/>
|
|
19
|
+
<rect x="112" y="29" width="6" height="22" rx="3"/>
|
|
20
|
+
<rect x="126" y="30" width="6" height="20" rx="3"/>
|
|
21
|
+
<rect x="140" y="27" width="6" height="26" rx="3"/>
|
|
22
|
+
<rect x="154" y="28" width="6" height="24" rx="3"/>
|
|
23
|
+
<rect x="168" y="18" width="6" height="44" rx="3"/>
|
|
24
|
+
<rect x="182" y="19" width="6" height="42" rx="3"/>
|
|
25
|
+
<rect x="196" y="20" width="6" height="40" rx="3"/>
|
|
26
|
+
<rect x="210" y="17" width="6" height="46" rx="3"/>
|
|
27
|
+
<rect x="224" y="12" width="6" height="56" rx="3"/>
|
|
28
|
+
<rect x="238" y="8" width="6" height="64" rx="3"/>
|
|
29
|
+
<rect x="252" y="16" width="6" height="48" rx="3"/>
|
|
30
|
+
<rect x="266" y="13" width="6" height="54" rx="3"/>
|
|
31
|
+
<rect x="280" y="11" width="6" height="58" rx="3"/>
|
|
32
|
+
<rect x="294" y="7" width="6" height="66" rx="3"/>
|
|
33
|
+
<rect x="308" y="13" width="6" height="54" rx="3"/>
|
|
34
|
+
<rect x="322" y="11" width="6" height="58" rx="3"/>
|
|
35
|
+
<rect x="336" y="5" width="6" height="70" rx="3"/>
|
|
36
|
+
<rect x="350" y="19" width="6" height="42" rx="3"/>
|
|
37
|
+
<rect x="364" y="9" width="6" height="62" rx="3"/>
|
|
38
|
+
<rect x="378" y="16" width="6" height="48" rx="3"/>
|
|
39
|
+
<rect x="392" y="16" width="6" height="48" rx="3"/>
|
|
40
|
+
<rect x="406" y="9" width="6" height="62" rx="3"/>
|
|
41
|
+
<rect x="420" y="18" width="6" height="44" rx="3"/>
|
|
42
|
+
<rect x="434" y="17" width="6" height="46" rx="3"/>
|
|
43
|
+
<rect x="448" y="16" width="6" height="48" rx="3"/>
|
|
44
|
+
<rect x="462" y="9" width="6" height="62" rx="3"/>
|
|
45
|
+
<rect x="476" y="10" width="6" height="60" rx="3"/>
|
|
46
|
+
<rect x="490" y="12" width="6" height="56" rx="3"/>
|
|
47
|
+
<rect x="504" y="10" width="6" height="60" rx="3"/>
|
|
48
|
+
<rect x="518" y="20" width="6" height="40" rx="3"/>
|
|
49
|
+
<rect x="532" y="9" width="6" height="62" rx="3"/>
|
|
50
|
+
<rect x="546" y="11" width="6" height="58" rx="3"/>
|
|
51
|
+
<rect x="560" y="16" width="6" height="48" rx="3"/>
|
|
52
|
+
<rect x="574" y="14" width="6" height="52" rx="3"/>
|
|
53
|
+
<rect x="588" y="11" width="6" height="58" rx="3"/>
|
|
54
|
+
<rect x="602" y="16" width="6" height="48" rx="3"/>
|
|
55
|
+
<rect x="616" y="13" width="6" height="54" rx="3"/>
|
|
56
|
+
<rect x="630" y="13" width="6" height="54" rx="3"/>
|
|
57
|
+
<rect x="644" y="5" width="6" height="70" rx="3"/>
|
|
58
|
+
<rect x="658" y="8" width="6" height="64" rx="3"/>
|
|
59
|
+
<rect x="672" y="6" width="6" height="68" rx="3"/>
|
|
60
|
+
<rect x="686" y="7" width="6" height="66" rx="3"/>
|
|
61
|
+
<rect x="700" y="12" width="6" height="56" rx="3"/>
|
|
62
|
+
<rect x="714" y="16" width="6" height="48" rx="3"/>
|
|
63
|
+
<rect x="728" y="5" width="6" height="70" rx="3"/>
|
|
64
|
+
<rect x="742" y="14" width="6" height="52" rx="3"/>
|
|
65
|
+
<rect x="756" y="15" width="6" height="50" rx="3"/>
|
|
66
|
+
<rect x="770" y="18" width="6" height="44" rx="3"/>
|
|
67
|
+
<rect x="784" y="18" width="6" height="44" rx="3"/>
|
|
68
|
+
<rect x="798" y="13" width="6" height="54" rx="3"/>
|
|
69
|
+
<rect x="812" y="21" width="6" height="38" rx="3"/>
|
|
70
|
+
<rect x="826" y="15" width="6" height="50" rx="3"/>
|
|
71
|
+
<rect x="840" y="21" width="6" height="38" rx="3"/>
|
|
72
|
+
<rect x="854" y="20" width="6" height="40" rx="3"/>
|
|
73
|
+
<rect x="868" y="26" width="6" height="28" rx="3"/>
|
|
74
|
+
<rect x="882" y="26" width="6" height="28" rx="3"/>
|
|
75
|
+
<rect x="896" y="30" width="6" height="20" rx="3"/>
|
|
76
|
+
<rect x="910" y="32" width="6" height="16" rx="3"/>
|
|
77
|
+
<rect x="924" y="33" width="6" height="14" rx="3"/>
|
|
78
|
+
<rect x="938" y="35" width="6" height="10" rx="3"/>
|
|
79
|
+
<rect x="952" y="37" width="6" height="6" rx="3"/>
|
|
80
|
+
<rect x="966" y="39" width="6" height="2" rx="3"/>
|
|
81
|
+
<rect x="980" y="39" width="6" height="2" rx="3"/>
|
|
82
|
+
<rect x="994" y="39" width="6" height="2" rx="3"/>
|
|
83
|
+
<rect x="1008" y="39" width="6" height="2" rx="3"/>
|
|
84
|
+
<rect x="1022" y="39" width="6" height="2" rx="3"/>
|
|
85
|
+
</g>
|
|
86
|
+
</svg>
|
package/docs/overlay.jpg
ADDED
|
Binary file
|
package/docs/overlay.png
ADDED
|
Binary file
|
|
Binary file
|