neutral-karaoke
v0.0.1
Published
Word-by-word text reveal component synced with TTS events
Maintainers
Readme
neutral-karaoke
Word-by-word text reveal component that syncs with TTS (Text-to-Speech) events. Creates a "karaoke" effect where words are revealed as they are spoken.
Installation
npm install neutral-karaoke neutral-agent
# or
pnpm add neutral-karaoke neutral-agentPeer Dependencies
This package requires:
react>= 18react-dom>= 18neutral-agent(provides TTS state management viauseTtsStore)
CSS Requirements
The component uses these Tailwind CSS classes that must be available in your project:
| Class | Purpose |
| -------------------------- | ---------------------------- |
| text-muted-foreground/60 | Unrevealed (dimmed) word styling |
| text-foreground | Revealed word styling |
| transition-colors | Color transition animation |
| duration-75 | Transition timing (75ms) |
If you're not using Tailwind, you'll need to provide equivalent CSS for [data-reveal-word] elements.
Custom CSS Example (non-Tailwind)
[data-reveal-word] {
transition: color 75ms ease;
}
[data-reveal-word].text-muted-foreground\/60 {
color: rgba(113, 113, 122, 0.6); /* Dimmed color */
}
[data-reveal-word].text-foreground {
color: rgb(250, 250, 250); /* Revealed color */
}Usage
Basic Usage with RevealText Component
import { RevealText, useWordReveal } from "neutral-karaoke";
function MyMessage({ text, messageId }) {
const { revealedCount, isComplete } = useWordReveal(text, {
enabled: true,
revealMessageId: messageId,
useExternalCount: true, // Use TTS events for reveal timing
});
return (
<RevealText revealedCount={revealedCount} enabled={!isComplete}>
<YourMarkdownRenderer>{text}</YourMarkdownRenderer>
</RevealText>
);
}API Reference
useWordReveal(text, options)
Hook that manages word reveal state based on TTS events.
Parameters:
text: string- The text to revealoptions.enabled?: boolean- Enable/disable reveal effect (default:true)options.revealMessageId?: string- Message ID for this reveal sessionoptions.useExternalCount?: boolean- Use TTS word count for timing (default:false)
Returns:
{
revealedCount: number; // Number of words revealed
isComplete: boolean; // Whether all words are revealed
getRevealedText: () => string; // Get text up to revealed count
stopReveal: () => { text: string; rawPosition: number };
needsTruncation: () => boolean;
isTruncationNeeded: boolean;
}<RevealText>
Component that wraps content and applies reveal styling to words.
Props:
| Prop | Type | Default | Description |
| -------------- | ----------- | ------- | ----------------------------------------- |
| children | ReactNode | - | Content to reveal (typically markdown) |
| revealedCount| number | - | Number of words to show as revealed |
| enabled | boolean | true | Enable the reveal effect |
| className | string | - | Additional CSS classes |
Types
import type {
RevealState,
RevealTextProps,
WordRevealOptions,
WordRevealState,
} from "neutral-karaoke";WordRevealOptions
type WordRevealOptions = {
enabled?: boolean;
revealMessageId?: string;
useExternalCount?: boolean;
};WordRevealState
type WordRevealState = {
revealedCount: number;
isComplete: boolean;
getRevealedText: () => string;
stopReveal: () => { text: string; rawPosition: number };
needsTruncation: () => boolean;
isTruncationNeeded: boolean;
};RevealState
type RevealState = {
messageId: string;
isStreaming: boolean;
isRevealComplete: boolean;
getRevealedText: () => string;
stopReveal: () => { text: string; rawPosition: number };
needsTruncation: () => boolean;
};Integration with neutral-agent
This package is designed to work with neutral-agent's TTS system. The useTtsStore from that package provides:
spokenWordCount- Number of words spoken by TTSmessageId- Current message being spoken
When useExternalCount: true, the reveal timing is driven by TTS events, creating synchronized karaoke-style text reveal.
Example: Chat Message with Voice
import { useWordReveal, RevealText, type RevealState } from "neutral-karaoke";
function ChatMessage({ message, isVoiceActive, onRevealStateChange }) {
const {
revealedCount,
isComplete,
getRevealedText,
stopReveal,
needsTruncation,
} = useWordReveal(message.text, {
enabled: message.role === "assistant",
revealMessageId: message.id,
useExternalCount: isVoiceActive,
});
// Report reveal state to parent for interruption handling
useEffect(() => {
onRevealStateChange?.({
messageId: message.id,
isStreaming: false,
isRevealComplete: isComplete,
getRevealedText,
stopReveal,
needsTruncation,
});
}, [isComplete]);
return (
<RevealText revealedCount={revealedCount} enabled={!isComplete}>
<Markdown>{message.text}</Markdown>
</RevealText>
);
}How It Works
DOM Manipulation:
RevealTextuses aMutationObserverto detect new text content and wraps each word in a<span>with adata-word-indexattribute.CSS Transitions: Words are styled with
text-muted-foreground/60(dimmed) ortext-foreground(revealed) based on their index relative torevealedCount.TTS Sync:
useWordRevealsubscribes touseTtsStorefromneutral-agentto get the current spoken word count.Markdown Handling: The hook cleans markdown syntax (code blocks, list markers, etc.) when calculating word positions for accurate truncation.
Interruption Support:
stopReveal()freezes the reveal count and returns the truncated text with its position in the raw (unprocessed) text.
License
ISC
