nexus/ui/src/components/TtsButton.tsx
Nexus Dev 847f316319 feat(34-01): create usePiperTts hook and TtsButton component with piper-tts-web
- Install @mintplex-labs/piper-tts-web as UI dependency
- Create usePiperTts hook with prewarm/speak/stop/status/progress (VOICE-01, VOICE-02)
- tts.stored() checks IndexedDB cache to skip re-download
- tts.download() with progress callback for visible download progress
- tts.predict() returns WAV blob URL for CPU-safe WASM synthesis
- Create TtsButton component showing download progress during prewarm
- TtsButton shows Volume2/VolumeX icons for idle/speaking states
2026-04-04 03:55:49 +00:00

62 lines
1.7 KiB
TypeScript

import { Volume2, VolumeX, Loader2 } from "lucide-react";
import { Button } from "./ui/button";
import type { TtsStatus } from "../hooks/usePiperTts";
interface TtsButtonProps {
status: TtsStatus;
progress: number;
onSpeak: () => void;
onStop: () => void;
onPrewarm: () => void;
disabled?: boolean;
}
export function TtsButton({ status, progress, onSpeak, onStop, onPrewarm, disabled }: TtsButtonProps) {
if (status === "downloading") {
return (
<Button variant="ghost" size="icon" className="h-8 w-8 relative" disabled title={`Downloading voice model: ${progress}%`}>
<Loader2 className="h-4 w-4 animate-spin" />
<span className="absolute -bottom-1 text-[10px] text-muted-foreground">{progress}%</span>
</Button>
);
}
if (status === "speaking") {
return (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-primary"
onClick={onStop}
aria-label="Stop speaking"
title="Stop speaking"
>
<VolumeX className="h-4 w-4" />
</Button>
);
}
// idle or error: clicking triggers prewarm then speak
// ready: clicking triggers speak directly
const handleClick = () => {
if (status === "ready") {
onSpeak();
} else {
onPrewarm();
}
};
return (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={handleClick}
disabled={disabled || status === "error"}
aria-label="Read aloud"
title={status === "error" ? "TTS unavailable" : status === "idle" ? "Download voice model and read aloud" : "Read aloud"}
>
<Volume2 className="h-4 w-4" />
</Button>
);
}