From fb0863f660c19cbbe8796b0b53a69072d424a85b Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 2 Aug 2026 23:07:37 -0700 Subject: [PATCH] FN-8753: enable installed voice input in project settings Make Voice Input available only after its local model and runtime are ready. - Bundle the optional sherpa runtime with the published CLI. - Gate the project setting on model installation and stable runtime status codes. - Add localized recovery guidance, documentation, and coverage. Files changed: .changeset/fn-8753-voice-input-enable.md | 7 ++++ docs/dashboard-guide.md | 2 +- docs/settings-reference.md | 8 +++-- packages/cli/package.json | 3 ++ packages/cli/src/__tests__/package-config.test.ts | 16 +++++++++ .../settings/__tests__/VoiceInputSection.test.tsx | 36 ++++++++++++++++---- .../settings/sections/VoiceInputSection.tsx | 30 ++++++++++++----- packages/dashboard/package.json | 2 +- .../routes/__tests__/register-voice-routes.test.ts | 39 ++++++++++++++++++++++ .../dashboard/src/stt/__tests__/voice-stt.test.ts | 27 ++++++++++++--- packages/dashboard/src/stt/parakeet-service.ts | 26 ++++++++++----- packages/i18n/locales/en/app.json | 6 ++++ pnpm-lock.yaml | 6 +++- 13 files changed, 175 insertions(+), 33 deletions(-) Fusion-Task-Id: FN-8753 Fusion-Task-Lineage: db92b148-37f0-45d2-b616-ed2e3f2d54f6 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8753-voice-input-enable.md | 7 ++++ docs/dashboard-guide.md | 2 +- docs/settings-reference.md | 8 +++- packages/cli/package.json | 3 ++ .../cli/src/__tests__/package-config.test.ts | 16 ++++++++ .../__tests__/VoiceInputSection.test.tsx | 36 +++++++++++++---- .../settings/sections/VoiceInputSection.tsx | 30 ++++++++++---- packages/dashboard/package.json | 2 +- .../__tests__/register-voice-routes.test.ts | 39 +++++++++++++++++++ .../src/stt/__tests__/voice-stt.test.ts | 27 +++++++++++-- .../dashboard/src/stt/parakeet-service.ts | 26 ++++++++----- packages/i18n/locales/en/app.json | 6 +++ pnpm-lock.yaml | 6 ++- 13 files changed, 175 insertions(+), 33 deletions(-) create mode 100644 .changeset/fn-8753-voice-input-enable.md diff --git a/.changeset/fn-8753-voice-input-enable.md b/.changeset/fn-8753-voice-input-enable.md new file mode 100644 index 0000000000..c9e5937717 --- /dev/null +++ b/.changeset/fn-8753-voice-input-enable.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Let supported installations enable Voice Input after its model is ready. +category: fix +dev: Publishes sherpa-onnx-node as an optional runtime dependency and reports stable readiness codes. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 5f22045e41..8ce4b12c58 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -43,7 +43,7 @@ Settings form changes save automatically after a short pause. The footer no long ## Voice Input -**Settings → Voice Input** is visible in both Basic and Advanced settings. Voice mode is off by default; enabling it is an explicit project preference. The same section shows the locally managed Parakeet v3 model and lets an operator download or remove it. Its upstream `sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8.tar.bz2` archive is about 465 MB and Fusion verifies its pinned SHA-256 before installing it; an unpinned or mismatched download is refused. Download progress is polled only while the model is downloading. When sherpa-onnx is unavailable, Settings preserves any saved enabled preference but presents voice mode as backend-enforced disabled with an explanation. If status cannot be determined, the section fails closed: voice mode stays disabled and model actions are not shown until status is available. +**Settings → Voice Input** is visible in both Basic and Advanced settings. Voice mode is off by default; enabling it is an explicit project preference. The same section shows the locally managed Parakeet v3 model and lets an operator download or remove it. Its upstream `sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8.tar.bz2` archive is about 465 MB and Fusion verifies its pinned SHA-256 before installing it; an unpinned or mismatched download is refused. Download progress is polled only while the model is downloading. The toggle becomes interactive only when the model is installed and Fusion can load the optional `sherpa-onnx-node` runtime. If Settings reports a missing module, a platform runtime load failure, or an incompatible runtime, reinstall a supported Fusion package for the current platform and reopen Settings. When sherpa-onnx is unavailable, Settings preserves any saved enabled preference but presents voice mode as backend-enforced disabled with an explanation. If status cannot be determined, the section fails closed: voice mode stays disabled and model actions are not shown until status is available. ## Reset Settings diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 63d9d2f6f7..a798463912 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -51,8 +51,12 @@ See [Signals Connectors](./signals-connectors.md) for setup, signing, payload, a `voiceInput: { enabled?, model?, language? }`; values resolve per request with project precedence. `enabled` defaults to false and gates dictation only, so model status/download/delete remain available while disabled. `model` defaults to registry identifier `"parakeet-v3"` and `language` to `"en"`; -unsupported values are rejected and never become URLs or paths. The optional sherpa runtime and -user-scoped cache degrade to unavailable safely. Downloads are on demand and require a pinned SHA-256; +unsupported values are rejected and never become URLs or paths. An installed model is not by itself +sufficient: Project Settings enables the toggle only after the optional `sherpa-onnx-node` runtime +loads successfully. A missing module, platform-addon load failure, or incompatible runtime leaves +voice disabled with a recovery message; install or reinstall a supported Fusion package for the +current platform, then reopen Settings. The optional sherpa runtime and user-scoped cache degrade to +unavailable safely. Downloads are on demand and require a pinned SHA-256; unpinned assets refuse download. The default asset is upstream `sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8.tar.bz2` (~465 MB), verified against its pinned SHA-256 before installation. Status polling reports `queued`/`downloading`; deleting fences an in-flight download. Voice chunks alone allow 2 MiB JSON, diff --git a/packages/cli/package.json b/packages/cli/package.json index 76810faecd..a0074a96c4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -86,6 +86,9 @@ "ws": "^8.18.0", "playwright-core": "^1.60.0" }, + "optionalDependencies": { + "sherpa-onnx-node": "1.13.4" + }, "peerDependencies": { "typebox": "*" }, diff --git a/packages/cli/src/__tests__/package-config.test.ts b/packages/cli/src/__tests__/package-config.test.ts index ef3ce747d3..0023908c2e 100644 --- a/packages/cli/src/__tests__/package-config.test.ts +++ b/packages/cli/src/__tests__/package-config.test.ts @@ -177,6 +177,22 @@ describe("CLI package.json publishing config", () => { expect(publishedPkg.files).toContain("agent-browser.mjs"); }); + /* + FNXC:VoiceInput 2026-08-03-05:43: + FN-8753 requires the published CLI manifest to own the lazy sherpa native + addon. The private dashboard workspace dependency is not present after an + npm install, so retain the pinned optional dependency through prepack while + keeping private workspace tooling out of the consumer manifest. + */ + it("keeps the optional voice runtime in the published manifest", () => { + const publishedPkg = applyPrepackTransform(pkg); + + expect(pkg.optionalDependencies).toHaveProperty("sherpa-onnx-node", "1.13.4"); + expect(publishedPkg.optionalDependencies).toHaveProperty("sherpa-onnx-node", "1.13.4"); + expect(publishedPkg.devDependencies).not.toHaveProperty("@fusion/dashboard"); + expect(publishedPkg.dependencies).not.toHaveProperty("@fusion/dashboard"); + }); + /** * FNXC:Packaging 2026-06-13-16:36: * Standalone npm/pnpm installs may omit a package when the published manifest declares it as both a runtime dependency and an optional peer. Keep the pi runtime packages as plain dependencies so dist/bin.js and dist/extension.js can resolve their static imports outside the monorepo, while leaving typebox as the optional-peer control because Fusion does not import it at runtime. diff --git a/packages/dashboard/app/components/settings/__tests__/VoiceInputSection.test.tsx b/packages/dashboard/app/components/settings/__tests__/VoiceInputSection.test.tsx index 344d0d56f8..3399dc241c 100644 --- a/packages/dashboard/app/components/settings/__tests__/VoiceInputSection.test.tsx +++ b/packages/dashboard/app/components/settings/__tests__/VoiceInputSection.test.tsx @@ -21,12 +21,20 @@ function renderSection(status: unknown, formOverrides: Partial = {}) { afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); }); describe("VoiceInputSection", () => { - it("persists the opt-in toggle through the Settings form", async () => { - const { setForm, getForm } = renderSection(available("not-installed")); - await screen.findByText("Not installed"); - fireEvent.click(screen.getByLabelText("Enable voice input")); + it("persists the opt-in toggle through the project Settings form without clobbering model fields", async () => { + const { setForm, getForm } = renderSection(available("installed"), { voiceInput: { model: "parakeet-v3", language: "en" } }); + const toggle = await screen.findByLabelText("Enable voice input"); + expect(toggle).toBeEnabled(); + fireEvent.click(toggle); expect(setForm).toHaveBeenCalledOnce(); - expect(getForm().voiceInput?.enabled).toBe(true); + expect(getForm().voiceInput).toEqual({ model: "parakeet-v3", language: "en", enabled: true }); + }); + + it("keeps the toggle disabled until the locally managed model is installed", async () => { + renderSection(available("not-installed")); + const toggle = await screen.findByLabelText("Enable voice input"); + expect(toggle).toBeDisabled(); + expect(screen.getByTestId("voice-input-runtime-unavailable")).toHaveTextContent("Download the Parakeet model"); }); it.each([ @@ -47,15 +55,29 @@ describe("VoiceInputSection", () => { }); it("fails closed for unavailable runtime without rewriting a persisted preference", async () => { - const { setForm } = renderSection({ model: { status: "not-installed" }, runtime: { status: "unavailable" } }, { voiceInput: { enabled: true } }); + const { setForm } = renderSection({ model: { status: "installed" }, runtime: { status: "unavailable", unavailableReason: "runtime-module-missing" } }, { voiceInput: { enabled: true } }); const toggle = await screen.findByLabelText("Enable voice input"); expect(toggle).toBeDisabled(); expect(toggle).not.toBeChecked(); expect(toggle.closest("div[data-effective-enabled]")).toHaveAttribute("data-effective-enabled", "false"); - expect(screen.getByTestId("voice-input-runtime-unavailable")).toHaveTextContent("saved preference remains on"); + expect(screen.getByTestId("voice-input-runtime-unavailable")).toHaveTextContent("includes the optional voice runtime"); expect(setForm).not.toHaveBeenCalled(); }); + it("removes the unavailable alert when model management refreshes to an installed compatible runtime", async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(response({ model: { status: "not-installed" }, runtime: { status: "unavailable", unavailableReason: "runtime-module-missing" } })) + .mockResolvedValueOnce(response({})) + .mockResolvedValueOnce(response(available("installed"))); + vi.stubGlobal("fetch", fetchMock); + render(); + + await screen.findByTestId("voice-input-runtime-unavailable"); + fireEvent.click(screen.getByRole("button", { name: "Download" })); + await waitFor(() => expect(screen.getByLabelText("Enable voice input")).toBeEnabled()); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); + it.each([undefined, { nonsense: true }])("fails closed when status cannot be parsed", async (body) => { const { setForm } = renderSection(body); const toggle = await screen.findByLabelText("Enable voice input"); diff --git a/packages/dashboard/app/components/settings/sections/VoiceInputSection.tsx b/packages/dashboard/app/components/settings/sections/VoiceInputSection.tsx index 629031876e..95d1e79fa9 100644 --- a/packages/dashboard/app/components/settings/sections/VoiceInputSection.tsx +++ b/packages/dashboard/app/components/settings/sections/VoiceInputSection.tsx @@ -67,22 +67,36 @@ export function VoiceInputSection({ form, setForm }: SectionBaseProps) { return () => window.clearInterval(interval); }, [loadStatus, modelStatus]); + const downloading = modelStatus === "downloading" || modelStatus === "queued"; + const progress = status?.model.progress; + const statusLoading = status === null && !statusUnavailable; + const modelReady = modelStatus === "installed"; const runtimeUnavailable = status?.runtime.status === "unavailable"; - const unavailable = statusUnavailable || runtimeUnavailable; + const unavailable = statusLoading || statusUnavailable || !modelReady || runtimeUnavailable; const storedEnabled = form.voiceInput?.enabled === true; const effectiveEnabled = storedEnabled && !unavailable; + const runtimeReason = status?.runtime.unavailableReason; const unavailableMessage = statusUnavailable ? t("settings.voiceInput.statusUnavailable", "Voice runtime status could not be determined; voice mode stays disabled.") - : t("settings.voiceInput.runtimeUnavailable", storedEnabled - ? "Voice mode is inactive because the sherpa-onnx runtime is unavailable. Your saved preference remains on." - : "The sherpa-onnx runtime is unavailable, so voice mode stays disabled."); + : !modelReady + ? (downloading + ? t("settings.voiceInput.modelPreparing", "Voice input becomes available after the model installation finishes.") + : modelStatus === "error" + ? t("settings.voiceInput.modelFailed", "Fix or retry the model installation before enabling voice input.") + : t("settings.voiceInput.modelRequired", "Download the Parakeet model before enabling voice input.")) + : runtimeReason === "runtime-module-missing" + ? t("settings.voiceInput.runtimeModuleMissing", "Install a Fusion release that includes the optional voice runtime, then reopen Settings.") + : runtimeReason === "runtime-platform-load-failed" + ? t("settings.voiceInput.runtimePlatformLoadFailed", "Reinstall Fusion for this platform so the optional voice runtime can load.") + : runtimeReason === "runtime-incompatible" + ? t("settings.voiceInput.runtimeIncompatible", "Update or reinstall Fusion because the installed voice runtime is incompatible.") + : t("settings.voiceInput.runtimeUnavailable", storedEnabled + ? "Voice mode is inactive because the sherpa-onnx runtime is unavailable. Your saved preference remains on." + : "The sherpa-onnx runtime is unavailable, so voice mode stays disabled."); const performModelAction = async (path: string, method: "POST" | "DELETE") => { try { await api(path, { method }); } finally { await loadStatus(); } }; - const downloading = modelStatus === "downloading" || modelStatus === "queued"; - const progress = status?.model.progress; - return

{t("settings.voiceInput.title", "Voice Input")}

@@ -98,7 +112,7 @@ export function VoiceInputSection({ form, setForm }: SectionBaseProps) { onChange={(enabled) => setForm((current) => ({ ...current, voiceInput: { ...(current.voiceInput ?? {}), enabled: enabled === true } }))} />
- {unavailable &&

{unavailableMessage}

} + {!statusLoading && unavailable &&

{unavailableMessage}

} { await Promise.all(servers.splice(0).map((server) => new Promise((resolve) => server.close(() => resolve())))); }); describe("voice route authorization split", () => { + it("reports a loadable installed runtime and honors the project voice override", async () => { + const app = express(); + const router = express.Router(); + app.use(router); + const manager = { + getState: async () => ({ status: "installed" as const, installedPath: "/model" }), + peekState: () => ({ status: "installed" as const, installedPath: "/model" }), + scheduleDownload: () => ({ accepted: true as const, state: { status: "installed" as const } }), + remove: async () => {}, + download: async () => ({ status: "installed" as const }), + subscribe: () => () => {}, + }; + const service = { + getRuntimeStatus: async () => ({ status: "available" as const }), + createSession: async () => ({ acceptChunk: () => ({ partial: "ok" }), finish: () => ({ text: "ok" }), close: () => {} }), + }; + createRegisterVoiceRoutes({ manager, service })({ + router, + getScopedStore: async () => ({ + getSettings: async () => ({ voiceInput: { enabled: true, language: "en" } }), + getGlobalSettingsStore: () => ({ getSettings: async () => ({ voiceInput: { enabled: false, model: "parakeet-v3" } }) }), + }), + getProjectIdFromRequest: () => "project-voice-override", + } as unknown as ApiRoutesContext); + const server = app.listen(0); servers.push(server); + await new Promise((resolve) => server.once("listening", resolve)); + const port = (server.address() as AddressInfo).port; + + const response = await fetch(`http://127.0.0.1:${port}/voice/status`); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + enabled: true, + modelId: "parakeet-v3", + language: "en", + model: { status: "installed" }, + runtime: { status: "available" }, + }); + }); + it("allows lifecycle inspection while dictation is disabled", async () => { const request = await harness(false); expect((await request("/voice/status")).status).toBe(200); diff --git a/packages/dashboard/src/stt/__tests__/voice-stt.test.ts b/packages/dashboard/src/stt/__tests__/voice-stt.test.ts index 2ffcb6b46f..42d1b5398a 100644 --- a/packages/dashboard/src/stt/__tests__/voice-stt.test.ts +++ b/packages/dashboard/src/stt/__tests__/voice-stt.test.ts @@ -47,16 +47,35 @@ describe("voice STT graceful degradation", () => { expect(resolveVoiceLanguage(undefined)).toEqual({ language: "en" }); expect(resolveVoiceLanguage("fr")).toEqual({ unsupported: "fr" }); }); + it("reports an uninstalled model without attempting to load the native binding", async () => { + const loadBinding = vi.fn(); + const manager = { getState: async () => ({ status: "not-installed" as const }) } as ReturnType; + const service = createParakeetService({ manager, loadBinding }); + + await expect(service.getRuntimeStatus()).resolves.toEqual({ status: "unavailable", unavailableReason: "model-not-installed" }); + expect(loadBinding).not.toHaveBeenCalled(); + }); + it("reports a missing native binding as unavailable without throwing", async () => { - const manager = createVoiceModelManager({ cacheDir: "/unused" }); - const service = createParakeetService({ manager, loadBinding: async () => { throw new Error("ERR_MODULE_NOT_FOUND"); } }); - await expect(service.getRuntimeStatus()).resolves.toMatchObject({ status: "unavailable" }); + const manager = { getState: async () => ({ status: "installed" as const, installedPath: "/model" }) } as ReturnType; + const missingModule = Object.assign(new Error("not exposed to operators"), { code: "ERR_MODULE_NOT_FOUND" }); + const service = createParakeetService({ manager, loadBinding: async () => { throw missingModule; } }); + + await expect(service.getRuntimeStatus()).resolves.toEqual({ status: "unavailable", unavailableReason: "runtime-module-missing" }); + }); + + it("reports a platform addon load failure without leaking the loader error", async () => { + const manager = { getState: async () => ({ status: "installed" as const, installedPath: "/model" }) } as ReturnType; + const platformFailure = Object.assign(new Error("dlopen /private/operator/path"), { code: "ERR_DLOPEN_FAILED" }); + const service = createParakeetService({ manager, loadBinding: async () => { throw platformFailure; } }); + + await expect(service.getRuntimeStatus()).resolves.toEqual({ status: "unavailable", unavailableReason: "runtime-platform-load-failed" }); }); it("reports a loaded but incompatible native binding as unavailable", async () => { const manager = { getState: async () => ({ status: "installed" as const, installedPath: "/model" }) } as ReturnType; const service = createParakeetService({ manager, loadBinding: async () => ({}) }); - await expect(service.getRuntimeStatus()).resolves.toEqual({ status: "unavailable", unavailableReason: "OfflineRecognizer unavailable" }); + await expect(service.getRuntimeStatus()).resolves.toEqual({ status: "unavailable", unavailableReason: "runtime-incompatible" }); }); it("uses sherpa's OfflineRecognizer and stream API for incremental decoding", async () => { diff --git a/packages/dashboard/src/stt/parakeet-service.ts b/packages/dashboard/src/stt/parakeet-service.ts index 46478a8fb2..f39013825a 100644 --- a/packages/dashboard/src/stt/parakeet-service.ts +++ b/packages/dashboard/src/stt/parakeet-service.ts @@ -2,7 +2,8 @@ import type { VoiceModelManager } from "./model-manager.js"; import { resolveVoiceLanguage, type VoiceModelId, type VoiceRuntimeStatus } from "./types.js"; export class VoiceInputError extends Error { constructor(public readonly code: "unsupported-language" | "invalid-audio" | "unavailable", message: string) { super(message); } } -export interface ParakeetService { getRuntimeStatus(): Promise<{ status: VoiceRuntimeStatus; unavailableReason?: string }>; createSession(options: { modelId: VoiceModelId; language: string }): Promise; } +export type VoiceRuntimeUnavailableReason = "model-not-installed" | "runtime-module-missing" | "runtime-platform-load-failed" | "runtime-incompatible"; +export interface ParakeetService { getRuntimeStatus(): Promise<{ status: VoiceRuntimeStatus; unavailableReason?: VoiceRuntimeUnavailableReason }>; createSession(options: { modelId: VoiceModelId; language: string }): Promise; } export interface ParakeetSession { acceptChunk(pcm: Int16Array | Buffer, options: { final: boolean }): { partial?: string; text?: string; final?: true }; finish(): { text: string }; close(): void; } interface SherpaStream { acceptWaveform(options: { sampleRate: number; samples: Float32Array }): void; free?(): void; close?(): void; } interface SherpaRecognizer { createStream(): SherpaStream; getResult(stream: SherpaStream): { text?: string }; decode(stream: SherpaStream): void; free?(): void; close?(): void; } @@ -10,24 +11,31 @@ interface SherpaOfflineRecognizerConstructor { new(config: { modelConfig: { tran interface SherpaBinding { OfflineRecognizer?: SherpaOfflineRecognizerConstructor; } export interface ParakeetServiceOptions { manager: VoiceModelManager; loadBinding?: () => Promise; } +function runtimeUnavailableReason(error: unknown): VoiceRuntimeUnavailableReason { + const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : ""; + if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return "runtime-module-missing"; + return "runtime-platform-load-failed"; +} + /** - * FNXC:VoiceInput 2026-07-21-17:20: - * Voice is opt-in and the sherpa addon is an optional, lazy runtime. Its fixed input is 16 kHz, - * mono signed-16-bit little-endian PCM. Resolved model/language values are checked before use; - * missing addon or model reports unavailable rather than preventing dashboard or engine boot. + * FNXC:VoiceInput 2026-08-03-05:45: + * FN-8753 keeps the sherpa addon lazy and fail-closed, but converts native import + * failures into stable operator-safe codes. Raw loader errors can disclose paths + * and differ by platform; Settings needs to distinguish an absent module, a + * platform addon failure, and an incompatible export without exposing either. */ export function createParakeetService(options: ParakeetServiceOptions): ParakeetService { let bindingPromise: Promise | undefined; const binding = () => bindingPromise ??= (options.loadBinding ? options.loadBinding() : new Function("specifier", "return import(specifier)")("sherpa-onnx-node") as Promise); - const getRuntimeStatus = async () => { + const getRuntimeStatus = async (): Promise<{ status: VoiceRuntimeStatus; unavailableReason?: VoiceRuntimeUnavailableReason }> => { const model = await options.manager.getState(); - if (model.status !== "installed" || !model.installedPath) return { status: "unavailable" as const, unavailableReason: model.errorReason ?? model.status }; + if (model.status !== "installed" || !model.installedPath) return { status: "unavailable" as const, unavailableReason: "model-not-installed" }; try { // A module resolving is not sufficient: a platform-mismatched or incompatible addon // can load without exporting the recognizer API required for transcription. - if (!(await binding()).OfflineRecognizer) return { status: "unavailable" as const, unavailableReason: "OfflineRecognizer unavailable" }; + if (!(await binding()).OfflineRecognizer) return { status: "unavailable" as const, unavailableReason: "runtime-incompatible" }; return { status: "available" as const }; - } catch (error) { return { status: "unavailable" as const, unavailableReason: error instanceof Error ? error.message : "runtime-unavailable" }; } + } catch (error) { return { status: "unavailable" as const, unavailableReason: runtimeUnavailableReason(error) }; } }; return { getRuntimeStatus, diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 0c09458cdf..b64191e68a 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -6924,6 +6924,12 @@ "modelActionsHelp": "Download or remove the Parakeet v3 speech model.", "statusUnavailable": "Voice runtime status could not be determined; voice mode stays disabled.", "runtimeUnavailable": "Voice runtime unavailable", + "modelRequired": "Download the Parakeet model before enabling voice input.", + "modelPreparing": "Voice input becomes available after the model installation finishes.", + "modelFailed": "Fix or retry the model installation before enabling voice input.", + "runtimeModuleMissing": "Install a Fusion release that includes the optional voice runtime, then reopen Settings.", + "runtimePlatformLoadFailed": "Reinstall Fusion for this platform so the optional voice runtime can load.", + "runtimeIncompatible": "Update or reinstall Fusion because the installed voice runtime is incompatible.", "unknown": "Status unavailable", "downloading": "Downloading model…", "downloadingProgress": "Downloading: {{progress}}%", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8552722e19..23ccf9ccf7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,10 @@ importers: yaml: specifier: ^2.8.3 version: 2.9.0 + optionalDependencies: + sherpa-onnx-node: + specifier: 1.13.4 + version: 1.13.4 packages/cli-alias: dependencies: @@ -457,7 +461,7 @@ importers: version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@25.9.5)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) optionalDependencies: sherpa-onnx-node: - specifier: ^1.13.4 + specifier: 1.13.4 version: 1.13.4 packages/desktop: