@juicesharp/rpiv-voice 1.7.0 → 1.8.1
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 +7 -0
- package/config/voice-config.ts +16 -41
- package/package.json +2 -1
- package/state/state-reducer.ts +11 -3
- package/state/voice-session.ts +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to `@juicesharp/rpiv-voice` are documented here.
|
|
|
5
5
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.8.1] - 2026-05-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Voice settings now persist to disk before applying in memory, preventing contradictory success/failure notifications on write failure.
|
|
12
|
+
|
|
13
|
+
## [1.8.0] - 2026-05-16
|
|
14
|
+
|
|
8
15
|
## [1.7.0] - 2026-05-15
|
|
9
16
|
|
|
10
17
|
## [1.6.1] - 2026-05-14
|
package/config/voice-config.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* voice-config —
|
|
3
|
-
*
|
|
2
|
+
* voice-config — persistence of optional rpiv-voice settings at
|
|
3
|
+
* `~/.config/rpiv-voice/voice.json`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Load is crash-resistant: malformed JSON or missing file resolves to an
|
|
6
|
+
* empty config (warning emitted via `rpiv-config.loadJsonConfig`).
|
|
7
|
+
*
|
|
8
|
+
* Save returns a `boolean`; the caller (voice-session shell) notifies the
|
|
9
|
+
* user on failure so the UI never lies about persistence ("saved" while the
|
|
10
|
+
* disk write actually failed). chmod to 0600 is best-effort and never gates
|
|
11
|
+
* the return — see `rpiv-config.saveJsonConfig` for the full contract.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
|
-
import {
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
14
|
+
import { configPath, loadJsonConfig, saveJsonConfig } from "@juicesharp/rpiv-config";
|
|
13
15
|
|
|
14
16
|
// ── Filesystem layout ────────────────────────────────────────────────────────
|
|
15
|
-
const
|
|
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;
|
|
17
|
+
const CONFIG_PATH = configPath("rpiv-voice", "voice.json");
|
|
23
18
|
|
|
24
19
|
// ── Module-level singleton key (cleared by test/setup beforeEach) ────────────
|
|
25
20
|
const VOICE_STATE_KEY = Symbol.for("rpiv-voice");
|
|
@@ -32,9 +27,7 @@ export interface VoiceConfig {
|
|
|
32
27
|
/**
|
|
33
28
|
* The hallucination filter defaults to ENABLED. We only persist the off-state
|
|
34
29
|
* to keep voice.json minimal, which means "field absent" must be read as
|
|
35
|
-
* "enabled".
|
|
36
|
-
* persisted config, pipeline runtime options, and the in-flight settings
|
|
37
|
-
* draft all decode the absence the same way.
|
|
30
|
+
* "enabled".
|
|
38
31
|
*/
|
|
39
32
|
export function isHallucinationFilterEnabled(config: { hallucinationFilterEnabled?: boolean }): boolean {
|
|
40
33
|
return config.hallucinationFilterEnabled !== false;
|
|
@@ -42,36 +35,18 @@ export function isHallucinationFilterEnabled(config: { hallucinationFilterEnable
|
|
|
42
35
|
|
|
43
36
|
/**
|
|
44
37
|
* The equalizer defaults to DISABLED. Mirror of the hallucination-filter
|
|
45
|
-
* decoding rule but with the inverted polarity
|
|
46
|
-
* persisted, "field absent" reads as "disabled".
|
|
38
|
+
* decoding rule but with the inverted polarity.
|
|
47
39
|
*/
|
|
48
40
|
export function isEqualizerEnabled(config: { equalizerEnabled?: boolean }): boolean {
|
|
49
41
|
return config.equalizerEnabled === true;
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
export function loadVoiceConfig(): VoiceConfig {
|
|
53
|
-
|
|
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
|
-
}
|
|
45
|
+
return loadJsonConfig<VoiceConfig>(CONFIG_PATH);
|
|
61
46
|
}
|
|
62
47
|
|
|
63
|
-
export function saveVoiceConfig(config: VoiceConfig):
|
|
64
|
-
|
|
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
|
-
}
|
|
48
|
+
export function saveVoiceConfig(config: VoiceConfig): boolean {
|
|
49
|
+
return saveJsonConfig(CONFIG_PATH, config);
|
|
75
50
|
}
|
|
76
51
|
|
|
77
52
|
export function __resetState(): void {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juicesharp/rpiv-voice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Pi extension. Voice dictation via /voice — local on-device STT with sherpa-onnx Whisper (base multilingual int8), microphone capture via decibri.",
|
|
6
6
|
"keywords": [
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
]
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
+
"@juicesharp/rpiv-config": "^1.8.1",
|
|
81
82
|
"sherpa-onnx-node": "^1.13.0",
|
|
82
83
|
"decibri": "^3.4.0"
|
|
83
84
|
},
|
package/state/state-reducer.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type Effect =
|
|
|
16
16
|
| { kind: "stop_mic" }
|
|
17
17
|
| { kind: "set_pipeline_paused"; paused: boolean }
|
|
18
18
|
| { kind: "set_hallucination_filter"; enabled: boolean }
|
|
19
|
-
| { kind: "save_config"; config: VoiceConfig }
|
|
19
|
+
| { kind: "save_config"; config: VoiceConfig; successMessage?: string }
|
|
20
20
|
| { kind: "done"; result: VoiceResult };
|
|
21
21
|
|
|
22
22
|
export interface VoiceResult {
|
|
@@ -145,13 +145,21 @@ function stepFocus(current: SettingsFieldKey, delta: 1 | -1): SettingsFieldKey {
|
|
|
145
145
|
return order[next] ?? current;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Success notify is attached to the save_config effect (rather than a separate
|
|
149
|
+
// notify effect) so the imperative shell can fire it ONLY when persistence
|
|
150
|
+
// succeeds. A plain two-effect list ran both notifies unconditionally on
|
|
151
|
+
// failure because `return` inside runEffect()'s switch case exits the method,
|
|
152
|
+
// not the outer effect loop — review I1 caught this.
|
|
148
153
|
const settingsSave: Handler<"settings_save"> = (state, _action, _ctx) => {
|
|
149
154
|
const config = configFromDraft(state.settingsDraft);
|
|
150
155
|
return {
|
|
151
156
|
state,
|
|
152
157
|
effects: [
|
|
153
|
-
{
|
|
154
|
-
|
|
158
|
+
{
|
|
159
|
+
kind: "save_config",
|
|
160
|
+
config,
|
|
161
|
+
successMessage: t("notify.settings_saved", "Voice settings saved"),
|
|
162
|
+
},
|
|
155
163
|
],
|
|
156
164
|
};
|
|
157
165
|
};
|
package/state/voice-session.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { TranscriptView } from "../view/components/transcript-view.js";
|
|
|
11
11
|
import { OverlayView } from "../view/overlay-view.js";
|
|
12
12
|
import { VoiceOverlayPropsAdapter } from "../view/props-adapter.js";
|
|
13
13
|
import { DictationScreenStrategy, SettingsScreenStrategy } from "../view/screen-content-strategy.js";
|
|
14
|
+
import { t } from "./i18n-bridge.js";
|
|
14
15
|
import { routeKey, type VoiceAction } from "./key-router.js";
|
|
15
16
|
import {
|
|
16
17
|
selectEqualizerFieldProps,
|
|
@@ -158,7 +159,20 @@ export class VoiceSession {
|
|
|
158
159
|
this.deps.setHallucinationFilterEnabled(effect.enabled);
|
|
159
160
|
return;
|
|
160
161
|
case "save_config":
|
|
161
|
-
saveVoiceConfig(effect.config)
|
|
162
|
+
if (saveVoiceConfig(effect.config)) {
|
|
163
|
+
// Success notify only fires if the reducer asked for one (Ctrl-S
|
|
164
|
+
// path); the implicit close_settings save omits the message and
|
|
165
|
+
// stays silent. Conditional fire-on-success keeps the user out
|
|
166
|
+
// of the contradictory "Failed … / Saved" state the review caught.
|
|
167
|
+
if (effect.successMessage) {
|
|
168
|
+
this.deps.notify(effect.successMessage, "info");
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
this.deps.notify(
|
|
172
|
+
t("notify.settings_save_failed", "Failed to save voice settings — change not persisted"),
|
|
173
|
+
"error",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
162
176
|
return;
|
|
163
177
|
case "done":
|
|
164
178
|
this.done(effect.result);
|