Files
audiobook/frontend/src/components/VoiceAssignmentSection.tsx
Paul Bailey 5c7b339bdf Add all 54 Kokoro TTS voices and 9 language support
- Expanded from 8 to 54 voices across 9 languages
- American English: 21 voices (11 female, 10 male)
- British English: 8 voices (4 female, 4 male)
- Spanish: 3 voices (1 female, 2 male)
- French: 1 voice (1 female)
- Hindi: 4 voices (2 female, 2 male)
- Italian: 2 voices (1 female, 1 male)
- Japanese: 5 voices (4 female, 1 male)
- Portuguese: 3 voices (1 female, 2 male)
- Chinese: 7 voices (4 female, 4 male)
- Updated backend to support all language codes (a,b,e,f,h,i,j,p,z)
- Added comprehensive voice documentation
2025-12-09 11:14:28 +00:00

250 lines
11 KiB
TypeScript

import type { Role } from '../api/tts';
interface VoiceAssignmentSectionProps {
roles: Role[];
voiceAssignments: Map<string, string>;
onVoiceChange: (roleId: string, voiceId: string) => void;
language: string;
onLanguageChange: (lang: string) => void;
speed: number;
onSpeedChange: (speed: number) => void;
onGenerate: () => void;
isGenerating: boolean;
allRolesAssigned: boolean;
}
interface VoiceOption {
id: string;
label: string;
gender: 'male' | 'female';
accent: string;
}
interface LanguageOption {
code: string;
label: string;
}
// All available Kokoro voice options (54 voices across 9 languages)
const KOKORO_VOICES: VoiceOption[] = [
// American English - Female (11 voices)
{ id: 'af_alloy', label: 'Alloy (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_aoede', label: 'Aoede (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_bella', label: 'Bella (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_heart', label: 'Heart (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_jessica', label: 'Jessica (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_kore', label: 'Kore (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_nicole', label: 'Nicole (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_nova', label: 'Nova (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_river', label: 'River (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_sarah', label: 'Sarah (Female, American)', gender: 'female', accent: 'American' },
{ id: 'af_sky', label: 'Sky (Female, American)', gender: 'female', accent: 'American' },
// American English - Male (10 voices)
{ id: 'am_adam', label: 'Adam (Male, American)', gender: 'male', accent: 'American' },
{ id: 'am_echo', label: 'Echo (Male, American)', gender: 'male', accent: 'American' },
{ id: 'am_eric', label: 'Eric (Male, American)', gender: 'male', accent: 'American' },
{ id: 'am_fenrir', label: 'Fenrir (Male, American)', gender: 'male', accent: 'American' },
{ id: 'am_liam', label: 'Liam (Male, American)', gender: 'male', accent: 'American' },
{ id: 'am_michael', label: 'Michael (Male, American)', gender: 'male', accent: 'American' },
{ id: 'am_onyx', label: 'Onyx (Male, American)', gender: 'male', accent: 'American' },
{ id: 'am_puck', label: 'Puck (Male, American)', gender: 'male', accent: 'American' },
{ id: 'am_santa', label: 'Santa (Male, American)', gender: 'male', accent: 'American' },
// British English - Female (4 voices)
{ id: 'bf_alice', label: 'Alice (Female, British)', gender: 'female', accent: 'British' },
{ id: 'bf_emma', label: 'Emma (Female, British)', gender: 'female', accent: 'British' },
{ id: 'bf_isabella', label: 'Isabella (Female, British)', gender: 'female', accent: 'British' },
{ id: 'bf_lily', label: 'Lily (Female, British)', gender: 'female', accent: 'British' },
// British English - Male (4 voices)
{ id: 'bm_daniel', label: 'Daniel (Male, British)', gender: 'male', accent: 'British' },
{ id: 'bm_fable', label: 'Fable (Male, British)', gender: 'male', accent: 'British' },
{ id: 'bm_george', label: 'George (Male, British)', gender: 'male', accent: 'British' },
{ id: 'bm_lewis', label: 'Lewis (Male, British)', gender: 'male', accent: 'British' },
// Spanish - Female (1 voice)
{ id: 'ef_dora', label: 'Dora (Female, Spanish)', gender: 'female', accent: 'Spanish' },
// Spanish - Male (2 voices)
{ id: 'em_alex', label: 'Alex (Male, Spanish)', gender: 'male', accent: 'Spanish' },
{ id: 'em_santa', label: 'Santa (Male, Spanish)', gender: 'male', accent: 'Spanish' },
// French - Female (1 voice)
{ id: 'ff_siwis', label: 'Siwis (Female, French)', gender: 'female', accent: 'French' },
// Hindi - Female (2 voices)
{ id: 'hf_alpha', label: 'Alpha (Female, Hindi)', gender: 'female', accent: 'Hindi' },
{ id: 'hf_beta', label: 'Beta (Female, Hindi)', gender: 'female', accent: 'Hindi' },
// Hindi - Male (2 voices)
{ id: 'hm_omega', label: 'Omega (Male, Hindi)', gender: 'male', accent: 'Hindi' },
{ id: 'hm_psi', label: 'Psi (Male, Hindi)', gender: 'male', accent: 'Hindi' },
// Italian - Female (1 voice)
{ id: 'if_sara', label: 'Sara (Female, Italian)', gender: 'female', accent: 'Italian' },
// Italian - Male (1 voice)
{ id: 'im_nicola', label: 'Nicola (Male, Italian)', gender: 'male', accent: 'Italian' },
// Japanese - Female (4 voices)
{ id: 'jf_alpha', label: 'Alpha (Female, Japanese)', gender: 'female', accent: 'Japanese' },
{ id: 'jf_gongitsune', label: 'Gongitsune (Female, Japanese)', gender: 'female', accent: 'Japanese' },
{ id: 'jf_nezumi', label: 'Nezumi (Female, Japanese)', gender: 'female', accent: 'Japanese' },
{ id: 'jf_tebukuro', label: 'Tebukuro (Female, Japanese)', gender: 'female', accent: 'Japanese' },
// Japanese - Male (1 voice)
{ id: 'jm_kumo', label: 'Kumo (Male, Japanese)', gender: 'male', accent: 'Japanese' },
// Portuguese - Female (1 voice)
{ id: 'pf_dora', label: 'Dora (Female, Portuguese)', gender: 'female', accent: 'Portuguese' },
// Portuguese - Male (2 voices)
{ id: 'pm_alex', label: 'Alex (Male, Portuguese)', gender: 'male', accent: 'Portuguese' },
{ id: 'pm_santa', label: 'Santa (Male, Portuguese)', gender: 'male', accent: 'Portuguese' },
// Chinese/Mandarin - Female (4 voices)
{ id: 'zf_xiaobei', label: 'Xiaobei (Female, Chinese)', gender: 'female', accent: 'Chinese' },
{ id: 'zf_xiaoni', label: 'Xiaoni (Female, Chinese)', gender: 'female', accent: 'Chinese' },
{ id: 'zf_xiaoxiao', label: 'Xiaoxiao (Female, Chinese)', gender: 'female', accent: 'Chinese' },
{ id: 'zf_xiaoyi', label: 'Xiaoyi (Female, Chinese)', gender: 'female', accent: 'Chinese' },
// Chinese/Mandarin - Male (4 voices)
{ id: 'zm_yunjian', label: 'Yunjian (Male, Chinese)', gender: 'male', accent: 'Chinese' },
{ id: 'zm_yunxi', label: 'Yunxi (Male, Chinese)', gender: 'male', accent: 'Chinese' },
{ id: 'zm_yunxia', label: 'Yunxia (Male, Chinese)', gender: 'male', accent: 'Chinese' },
{ id: 'zm_yunyang', label: 'Yunyang (Male, Chinese)', gender: 'male', accent: 'Chinese' },
];
// Language options - all supported by Kokoro TTS
const LANGUAGES: LanguageOption[] = [
{ code: 'a', label: 'American English' },
{ code: 'b', label: 'British English' },
{ code: 'e', label: 'Spanish (Español)' },
{ code: 'f', label: 'French (Français)' },
{ code: 'h', label: 'Hindi (हिन्दी)' },
{ code: 'i', label: 'Italian (Italiano)' },
{ code: 'j', label: 'Japanese (日本語)' },
{ code: 'p', label: 'Portuguese (Português)' },
{ code: 'z', label: 'Chinese (中文)' },
];
export default function VoiceAssignmentSection({
roles,
voiceAssignments,
onVoiceChange,
language,
onLanguageChange,
speed,
onSpeedChange,
onGenerate,
isGenerating,
allRolesAssigned,
}: VoiceAssignmentSectionProps) {
const handleVoiceSelect = (roleId: string, voiceId: string) => {
onVoiceChange(roleId, voiceId);
};
const handleLanguageSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
onLanguageChange(e.target.value);
};
const handleSpeedChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newSpeed = parseFloat(e.target.value);
onSpeedChange(newSpeed);
};
const isGenerateDisabled = !allRolesAssigned || isGenerating;
return (
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6">
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Assign voices to roles</h2>
{/* Role-to-Voice Mapping Section */}
<div className="mb-6 sm:mb-8">
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Voice Assignments</h3>
<div className="space-y-3 sm:space-y-4">
{roles.map((role) => (
<div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<label className="font-medium text-gray-700 text-sm sm:text-base sm:w-40">
{role.label}:
</label>
<select
value={voiceAssignments.get(role.id) || ''}
onChange={(e) => handleVoiceSelect(role.id, e.target.value)}
className="flex-1 p-3 sm:p-2 min-h-[44px] border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-sm sm:text-base touch-manipulation"
>
<option value="">Select a voice...</option>
{KOKORO_VOICES.map((voice) => (
<option key={voice.id} value={voice.id}>
{voice.label}
</option>
))}
</select>
</div>
))}
</div>
</div>
{/* Language Selector */}
<div className="mb-6">
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Language</h3>
<select
value={language}
onChange={handleLanguageSelect}
className="w-full p-3 sm:p-2 min-h-[44px] border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-sm sm:text-base touch-manipulation"
>
{LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.label}
</option>
))}
</select>
</div>
{/* Speed Slider */}
<div className="mb-6 sm:mb-8">
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Playback Speed</h3>
<div className="flex items-center gap-2 sm:gap-4">
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12">0.5x</span>
<input
type="range"
min="0.5"
max="1.5"
step="0.1"
value={speed}
onChange={handleSpeedChange}
className="flex-1 h-8 sm:h-auto touch-manipulation"
/>
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12">1.5x</span>
<span className="font-semibold text-gray-800 w-12 sm:w-16 text-center text-sm sm:text-base">
{speed.toFixed(1)}x
</span>
</div>
</div>
{/* Generate Button */}
<div className="flex justify-center">
<button
onClick={onGenerate}
disabled={isGenerateDisabled}
className={`w-full sm:w-auto px-8 py-3 min-h-[44px] rounded-lg font-semibold text-white transition-colors touch-manipulation ${
isGenerateDisabled
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-600 hover:bg-green-700 active:bg-green-800 cursor-pointer'
}`}
>
{isGenerating ? 'Generating...' : 'Generate Audiobook'}
</button>
</div>
{/* Validation Message */}
{!allRolesAssigned && !isGenerating && (
<div className="mt-4 p-3 bg-yellow-100 border border-yellow-400 text-yellow-800 rounded text-center text-sm sm:text-base">
Please assign voices to all roles before generating the audiobook.
</div>
)}
</div>
);
}