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<Blob>, 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) <noreply@anthropic.com>
This commit is contained in:
Nexus Dev 2026-04-11 10:35:01 +00:00
parent 43ca8d3047
commit 137bd3d0f6

View file

@ -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]);