From 137bd3d0f684ba4f055044b125f4898e31602a69 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Sat, 11 Apr 2026 10:35:01 +0000 Subject: [PATCH] fix(nexus): unblank assistant page on piper-tts import error The usePiperTts hook imported a non-existent 'tts' namespace from @mintplex-labs/piper-tts-web@1.0.4. The package exports named functions (stored, download, predict, etc.) at the top level, not under a tts namespace. The failing named-import threw at module-link time, which crashed the lazy chunk for PersonalAssistant.tsx and left /NEX/assistant blank with only a React error boundary fallback. Two fixes in one file: 1. Import as namespace: import * as tts from "@mintplex-labs/piper-tts-web" ESM namespace imports synthesize a 'tts' object whose members are the package's named exports, so the existing tts.stored() / tts.download() / tts.predict() call sites bind to real functions without touching the hook body. 2. Wrap predict() Blob result in URL.createObjectURL() before passing to new Audio(). predict() returns Promise, not a URL string, and Audio() cannot accept a Blob directly. Added a shared cleanup callback that revokes the object URL on onended/onerror and in the catch path so we don't leak blob URLs on every speak invocation. Bug 1 was the page-blanking crash at module load. Bug 2 was a latent runtime crash behind the speak button click handler, surfaced while the file was already being edited. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/hooks/usePiperTts.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ui/src/hooks/usePiperTts.ts b/ui/src/hooks/usePiperTts.ts index b2d67488..4baf985d 100644 --- a/ui/src/hooks/usePiperTts.ts +++ b/ui/src/hooks/usePiperTts.ts @@ -1,5 +1,5 @@ import { useState, useCallback, useRef } from "react"; -import { tts } from "@mintplex-labs/piper-tts-web"; +import * as tts from "@mintplex-labs/piper-tts-web"; const DEFAULT_VOICE = "en_US-hfc_female-medium"; @@ -36,20 +36,25 @@ export function usePiperTts() { audioRef.current = null; } setStatus("speaking"); + let objectUrl: string | null = null; try { const wav = await tts.predict({ text, voiceId: DEFAULT_VOICE }); - const audio = new Audio(wav); + objectUrl = URL.createObjectURL(wav); + const audio = new Audio(objectUrl); audioRef.current = audio; - audio.onended = () => { - audioRef.current = null; - setStatus("ready"); - }; - audio.onerror = () => { + const cleanup = () => { audioRef.current = null; + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + objectUrl = null; + } setStatus("ready"); }; + audio.onended = cleanup; + audio.onerror = cleanup; await audio.play(); } catch { + if (objectUrl) URL.revokeObjectURL(objectUrl); setStatus("ready"); } }, [status]);