Overview
The Voice Library provides access to both pre-built system voices and your organization’s custom cloned voices. Voices can be filtered by category, language, and search terms.
Voice Types
Resonance supports two types of voices:
System Voices Pre-trained voices provided by Resonance
Curated for quality
No creation required
Available to all users
Cannot be deleted
Custom Voices Voices cloned from your audio samples
Organization-specific
Private to your org
Can be edited and deleted
Require voice cloning
Voice Schema
Each voice in the library has the following structure:
interface Voice {
id : string ; // Unique identifier (CUID)
name : string ; // Display name
description ?: string ; // Optional description
category : VoiceCategory ; // Voice category
language : string ; // BCP 47 language code (e.g., "en-US")
variant : "SYSTEM" | "CUSTOM" ;
orgId ?: string ; // Organization ID (custom voices only)
r2ObjectKey ?: string ; // Storage key for audio sample
createdAt : Date ;
updatedAt : Date ;
}
Voice Categories
Voices are organized into 12 categories:
Content Creation
Business
Entertainment
Specialty
AUDIOBOOK : Long-form narration for books
NARRATIVE : Storytelling and narrative content
PODCAST : Podcast hosting and episodes
CORPORATE : Professional business presentations
ADVERTISING : Marketing and promotional content
CUSTOMER_SERVICE : Support and service interactions
VOICEOVER : Professional voiceover work
CHARACTERS : Character voices for games/animation
CONVERSATIONAL : Natural dialogue
MEDITATION : Calm and soothing guidance
MOTIVATIONAL : Energetic and inspiring content
GENERAL : Multipurpose (default)
Category Labels
The UI displays human-readable labels:
export const VOICE_CATEGORY_LABELS : Record < VoiceCategory , string > = {
AUDIOBOOK: "Audiobook" ,
CONVERSATIONAL: "Conversational" ,
CUSTOMER_SERVICE: "Customer Service" ,
GENERAL: "General" ,
NARRATIVE: "Narrative" ,
CHARACTERS: "Characters" ,
MEDITATION: "Meditation" ,
MOTIVATIONAL: "Motivational" ,
PODCAST: "Podcast" ,
ADVERTISING: "Advertising" ,
VOICEOVER: "Voiceover" ,
CORPORATE: "Corporate" ,
};
Fetching Voices
The Voice Library API returns both custom and system voices:
tRPC Query
import { trpc } from '@/trpc/client' ;
// Fetch all voices
const { data } = trpc . voices . getAll . useQuery ();
const customVoices = data . custom ; // Your organization's voices
const systemVoices = data . system ; // Pre-built voices
With Search Filter
const { data } = trpc . voices . getAll . useQuery ({
query: "conversational" , // Search term
});
The search filter matches against:
Voice name (case-insensitive)
Voice description (case-insensitive)
Response Structure
type GetAllVoicesResponse = {
custom : Voice []; // Sorted by createdAt DESC
system : Voice []; // Sorted by name ASC
};
Voice Scoping
Voices are scoped based on variant:
System Voices
Available to all organizations
variant: "SYSTEM"
orgId: null
Queried with WHERE variant = 'SYSTEM'
Custom Voices
Private to creating organization
variant: "CUSTOM"
orgId: <organization-id>
Queried with WHERE variant = 'CUSTOM' AND orgId = <current-org>
Access Control
The API enforces organization-level access:
// Voice must be either:
// 1. A system voice, OR
// 2. A custom voice belonging to the requesting organization
const voice = await prisma . voice . findUnique ({
where: {
id: voiceId ,
OR: [
{ variant: "SYSTEM" },
{ variant: "CUSTOM" , orgId: currentOrgId },
],
},
});
Managing Custom Voices
Delete Voice
Only custom voices can be deleted:
const deleteMutation = trpc . voices . delete . useMutation ();
await deleteMutation . mutateAsync ({
id: "clx1234567890" ,
});
Deletion Process
Verify ownership
Ensure voice is custom and belongs to your organization
Delete database record
Remove voice from database
Delete audio file
Remove audio from R2 storage (fire-and-forget)
Update references
Existing generations using this voice remain intact with voiceName snapshot
Deleting a voice does not delete past generations. The generation history preserves the voice name at time of creation.
Error Handling
try {
await deleteMutation . mutateAsync ({ id: voiceId });
toast . success ( "Voice deleted" );
} catch ( error ) {
// Will throw NOT_FOUND if:
// - Voice doesn't exist
// - Voice is a system voice
// - Voice belongs to different organization
toast . error ( "Failed to delete voice" );
}
Voice Selection UI
The voice selector combines both types:
import { useTTSVoicesContext } from '@/features/text-to-speech/contexts/tts-voices-context' ;
function VoiceSelector () {
const { customVoices , systemVoices , allVoices } = useTTSVoicesContext ();
return (
<>
{ customVoices . length > 0 && (
< VoiceSection title = "Your Voices" >
{ customVoices . map ( voice => (
< VoiceCard key = {voice. id } voice = { voice } />
))}
</ VoiceSection >
)}
< VoiceSection title = "System Voices" >
{ systemVoices . map ( voice => (
< VoiceCard key = {voice. id } voice = { voice } />
</ VoiceSection >
</>
</>
);
}
Voice Preview
Voices include audio samples for preview:
// Access voice audio sample
const audioUrl = `/api/voices/ ${ voice . id } ` ;
// The API route validates access and returns audio from R2
Only voices with r2ObjectKey can be previewed. Voices without audio samples cannot be used for generation.
Language Support
Voices support any BCP 47 language tag:
import locales from 'locale-codes' ;
// Available languages (with regions)
const LANGUAGE_OPTIONS = locales . all
. filter (( l ) => l . tag && l . tag . includes ( "-" ) && l . name )
. map (( l ) => ({
value: l . tag , // e.g., "en-US", "es-MX"
label: l . location // e.g., "English (United States)"
? ` ${ l . name } ( ${ l . location } )`
: l . name ,
}));
Common Languages
en-US - English (United States)
en-GB - English (United Kingdom)
es-ES - Spanish (Spain)
es-MX - Spanish (Mexico)
fr-FR - French (France)
de-DE - German (Germany)
it-IT - Italian (Italy)
pt-BR - Portuguese (Brazil)
ja-JP - Japanese (Japan)
zh-CN - Chinese (Simplified)
ko-KR - Korean (South Korea)
Voice Avatar
Each voice has a generated avatar based on its ID:
import { VoiceAvatar } from '@/components/voice-avatar/voice-avatar' ;
< VoiceAvatar
seed = {voice.id ?? voice. name }
name = {voice. name }
className = "size-8"
/>
Avatars are deterministically generated from the voice ID, providing visual consistency.
Complete Example
import { useSuspenseQuery } from '@tanstack/react-query' ;
import { useTRPC } from '@/trpc/client' ;
function VoiceLibraryView () {
const trpc = useTRPC ();
const [ searchQuery , setSearchQuery ] = useState ( "" );
const { data } = useSuspenseQuery (
trpc . voices . getAll . queryOptions ({
query: searchQuery || undefined ,
})
);
const { custom : customVoices , system : systemVoices } = data ;
return (
< div >
< SearchBar
value = { searchQuery }
onChange = { setSearchQuery }
placeholder = "Search voices..."
/>
{ customVoices . length > 0 && (
< VoiceSection title = "Your Voices" >
{ customVoices . map ( voice => (
< VoiceCard
key = {voice. id }
voice = { voice }
onDelete = { async () => {
await trpc . voices . delete . mutate ({ id : voice . id });
}}
/>
))}
</ VoiceSection >
)}
< VoiceSection title = "System Voices" >
{ systemVoices . map ( voice => (
< VoiceCard key = {voice. id } voice = { voice } />
))}
</ VoiceSection >
</ div >
);
}