import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { Box, Typography, Select, MenuItem, FormControl, InputLabel, Stack, Button, Chip, CircularProgress, Tooltip, alpha, IconButton, ListItemIcon, ListItemText, Collapse, FormControlLabel, Checkbox, Divider, Dialog, DialogTitle, DialogContent, DialogActions, Slider, } from "@mui/material"; import { Mic, PlayArrow, Pause, HelpOutline, AutoAwesome, CheckCircle, ExpandLess, ExpandMore, RestartAlt, VolumeUp, Tune, Close, Male, Female, Category, } from "@mui/icons-material"; import { getLatestVoiceClone, VoiceCloneResponse } from "../../api/brandAssets"; import { VoiceAvatarPlaceholder } from "../OnboardingWizard/PersonalizationStep/components/VoiceAvatarPlaceholder"; export type VoiceOption = { id: string; name: string; personality?: string; isCustom?: boolean; previewUrl?: string; gender?: "male" | "female"; category?: string; }; export type VoiceAudioSettings = { speed: number; volume: number; pitch: number; emotion: string; }; const DEFAULT_AUDIO_SETTINGS: VoiceAudioSettings = { speed: 1.0, volume: 1.0, pitch: 0, emotion: "neutral", }; const EMOTION_OPTIONS = ["neutral", "happy", "sad", "angry", "fearful", "disgusted", "surprised"]; type GenderFilter = "all" | "male" | "female"; type CategoryFilter = string; interface VoiceSelectorProps { value: string; onChange: (voiceId: string) => void; disabled?: boolean; showVoiceClone?: boolean; compact?: boolean; audioSettings?: VoiceAudioSettings; onAudioSettingsChange?: (settings: VoiceAudioSettings) => void; } const VOICE_SAMPLE_BASE = "/assets/voice-samples"; const VOICE_PREVIEW_MAP: Record = { Wise_Woman: `${VOICE_SAMPLE_BASE}/wise_woman.mp3`, Friendly_Person: `${VOICE_SAMPLE_BASE}/friendly_person.mp3`, Inspirational_girl: `${VOICE_SAMPLE_BASE}/inspirational_girl.mp3`, Deep_Voice_Man: `${VOICE_SAMPLE_BASE}/deep_voice_man.mp3`, Calm_Woman: `${VOICE_SAMPLE_BASE}/calm_woman.mp3`, Casual_Guy: `${VOICE_SAMPLE_BASE}/casual_guy.mp3`, Lively_Girl: `${VOICE_SAMPLE_BASE}/lively_girl.mp3`, Patient_Man: `${VOICE_SAMPLE_BASE}/patient_man.mp3`, Young_Knight: `${VOICE_SAMPLE_BASE}/young_knight.mp3`, Determined_Man: `${VOICE_SAMPLE_BASE}/determined_man.mp3`, Lovely_Girl: `${VOICE_SAMPLE_BASE}/lovely_girl.mp3`, Decent_Boy: `${VOICE_SAMPLE_BASE}/decent_boy.mp3`, Imposing_Manner: `${VOICE_SAMPLE_BASE}/imposing_manner.mp3`, Elegant_Man: `${VOICE_SAMPLE_BASE}/elegant_man.mp3`, Abbess: `${VOICE_SAMPLE_BASE}/abbess.mp3`, Sweet_Girl_2: `${VOICE_SAMPLE_BASE}/sweet_girl.mp3`, Exuberant_Girl: `${VOICE_SAMPLE_BASE}/exuberant_girl.mp3`, }; const CATEGORY_OPTIONS: { value: CategoryFilter; label: string }[] = [ { value: "all", label: "All" }, { value: "educational", label: "Educational" }, { value: "marketing", label: "Marketing" }, { value: "professional", label: "Professional" }, { value: "creative", label: "Creative" }, { value: "calming", label: "Calming" }, { value: "motivational", label: "Motivational" }, ]; const PREDEFINED_VOICES: VoiceOption[] = [ { id: "Wise_Woman", name: "Wise Woman", personality: "Authoritative, trustworthy female voice - perfect for educational content", previewUrl: VOICE_PREVIEW_MAP.Wise_Woman, gender: "female", category: "educational" }, { id: "Friendly_Person", name: "Friendly Person", personality: "Warm, approachable voice - great for welcoming introductions", previewUrl: VOICE_PREVIEW_MAP.Friendly_Person, category: "marketing" }, { id: "Inspirational_girl", name: "Inspirational Girl", personality: "Motivational, uplifting female voice - ideal for inspiration", previewUrl: VOICE_PREVIEW_MAP.Inspirational_girl, gender: "female", category: "motivational" }, { id: "Deep_Voice_Man", name: "Deep Voice Man", personality: "Powerful, commanding male voice - excellent for serious topics", previewUrl: VOICE_PREVIEW_MAP.Deep_Voice_Man, gender: "male", category: "professional" }, { id: "Calm_Woman", name: "Calm Woman", personality: "Soothing, composed female voice - perfect for meditation or sensitive topics", previewUrl: VOICE_PREVIEW_MAP.Calm_Woman, gender: "female", category: "calming" }, { id: "Casual_Guy", name: "Casual Guy", personality: "Relaxed, conversational male voice - great for vlogs and tutorials", previewUrl: VOICE_PREVIEW_MAP.Casual_Guy, gender: "male", category: "marketing" }, { id: "Lively_Girl", name: "Lively Girl", personality: "Energetic, enthusiastic female voice - ideal for exciting announcements", previewUrl: VOICE_PREVIEW_MAP.Lively_Girl, gender: "female", category: "marketing" }, { id: "Patient_Man", name: "Patient Man", personality: "Gentle, understanding male voice - perfect for explanations", previewUrl: VOICE_PREVIEW_MAP.Patient_Man, gender: "male", category: "educational" }, { id: "Young_Knight", name: "Young Knight", personality: "Brave, confident male voice - great for adventure and gaming", previewUrl: VOICE_PREVIEW_MAP.Young_Knight, gender: "male", category: "creative" }, { id: "Determined_Man", name: "Determined Man", personality: "Strong, resolute male voice - excellent for motivational speeches", previewUrl: VOICE_PREVIEW_MAP.Determined_Man, gender: "male", category: "motivational" }, { id: "Lovely_Girl", name: "Lovely Girl", personality: "Sweet, charming female voice - ideal for storytelling", previewUrl: VOICE_PREVIEW_MAP.Lovely_Girl, gender: "female", category: "creative" }, { id: "Decent_Boy", name: "Decent Boy", personality: "Honest, sincere male voice - perfect for testimonials", previewUrl: VOICE_PREVIEW_MAP.Decent_Boy, gender: "male", category: "marketing" }, { id: "Imposing_Manner", name: "Imposing Manner", personality: "Formal, dignified male voice - great for corporate content", previewUrl: VOICE_PREVIEW_MAP.Imposing_Manner, gender: "male", category: "professional" }, { id: "Elegant_Man", name: "Elegant Man", personality: "Refined, sophisticated male voice - ideal for luxury content", previewUrl: VOICE_PREVIEW_MAP.Elegant_Man, gender: "male", category: "professional" }, { id: "Abbess", name: "Abbess", personality: "Spiritual, serene female voice - perfect for meditation", previewUrl: VOICE_PREVIEW_MAP.Abbess, gender: "female", category: "calming" }, { id: "Sweet_Girl_2", name: "Sweet Girl 2", personality: "Gentle, melodic female voice - excellent for children's content", previewUrl: VOICE_PREVIEW_MAP.Sweet_Girl_2, gender: "female", category: "creative" }, { id: "Exuberant_Girl", name: "Exuberant Girl", personality: "Joyful, expressive female voice - ideal for celebrations", previewUrl: VOICE_PREVIEW_MAP.Exuberant_Girl, gender: "female", category: "creative" }, ]; const VOICE_CLONE_ID = "MY_VOICE_CLONE"; export const VoiceSelector: React.FC = ({ value, onChange, disabled = false, showVoiceClone = true, compact = false, audioSettings: externalAudioSettings, onAudioSettingsChange, }) => { const [voiceClone, setVoiceClone] = useState(null); const [loadingVoiceClone, setLoadingVoiceClone] = useState(false); const [playingPreview, setPlayingPreview] = useState(null); const [showVoiceClonePanel, setShowVoiceClonePanel] = useState(false); const [voiceCreated, setVoiceCreated] = useState(false); const [useCreatedVoice, setUseCreatedVoice] = useState(true); const [redoingClone, setRedoingClone] = useState(false); const [selectOpen, setSelectOpen] = useState(false); const [tuneModalOpen, setTuneModalOpen] = useState(false); const [tuneForVoice, setTuneForVoice] = useState(null); const [localAudioSettings, setLocalAudioSettings] = useState( externalAudioSettings || { ...DEFAULT_AUDIO_SETTINGS } ); const [genderFilter, setGenderFilter] = useState("all"); const [categoryFilter, setCategoryFilter] = useState("all"); const audioRef = useRef(null); const prevVoiceCloneIdRef = useRef(null); const fetchVoiceClone = async () => { try { setLoadingVoiceClone(true); const result = await getLatestVoiceClone(); setVoiceClone(result); return result; } catch (error) { console.error("Failed to fetch voice clone:", error); return null; } finally { setLoadingVoiceClone(false); } }; const voiceOptions = useMemo(() => { const options: VoiceOption[] = [...PREDEFINED_VOICES]; if (showVoiceClone && voiceClone?.success && voiceClone.custom_voice_id) { options.unshift({ id: VOICE_CLONE_ID, name: voiceClone.voice_name || voiceClone.custom_voice_id || "My Voice Clone", personality: "Your own voice - cloned from audio sample", isCustom: true, previewUrl: voiceClone.preview_audio_url, }); } return options; }, [showVoiceClone, voiceClone]); const filteredVoices = useMemo(() => { const filtered = PREDEFINED_VOICES.filter(v => { if (genderFilter !== "all" && v.gender !== genderFilter) return false; if (categoryFilter !== "all" && v.category !== categoryFilter) return false; return true; }); if (value && value !== VOICE_CLONE_ID && !filtered.some(v => v.id === value)) { const selected = PREDEFINED_VOICES.find(v => v.id === value); if (selected) filtered.unshift(selected); } return filtered; }, [genderFilter, categoryFilter, value]); useEffect(() => { if (!showVoiceClone) return; fetchVoiceClone(); }, [showVoiceClone]); useEffect(() => { if (voiceClone?.success && voiceClone.custom_voice_id) { const cloneId = voiceClone.custom_voice_id; if (prevVoiceCloneIdRef.current !== cloneId) { prevVoiceCloneIdRef.current = cloneId; if (!value || value === "Wise_Woman") { onChange(cloneId); } } } }, [voiceClone]); const stopCurrentAudio = useCallback(() => { if (audioRef.current) { audioRef.current.pause(); audioRef.current.currentTime = 0; audioRef.current.onended = null; audioRef.current.onerror = null; audioRef.current = null; } }, []); const handlePreview = useCallback((voice: VoiceOption) => { if (!voice.previewUrl) return; if (playingPreview === voice.id) { stopCurrentAudio(); setPlayingPreview(null); return; } stopCurrentAudio(); setPlayingPreview(voice.id); const audio = new Audio(voice.previewUrl); audioRef.current = audio; audio.onerror = () => { console.error("Failed to load voice preview audio:", voice.previewUrl); if (audioRef.current === audio) { audioRef.current = null; } setPlayingPreview(null); }; audio.onended = () => { if (audioRef.current === audio) { audioRef.current = null; } setPlayingPreview(null); }; audio.play().catch((err) => { console.error("Failed to play voice preview:", err); if (audioRef.current === audio) { audioRef.current = null; } setPlayingPreview(null); }); }, [playingPreview, stopCurrentAudio]); useEffect(() => { return () => { stopCurrentAudio(); }; }, [stopCurrentAudio]); const handleChange = (newValue: string) => { if (newValue === VOICE_CLONE_ID && voiceClone?.success) { onChange(voiceClone.custom_voice_id || VOICE_CLONE_ID); } else { onChange(newValue); } }; const isVoiceCloneSelected = value === VOICE_CLONE_ID || (voiceClone?.success && voiceClone.custom_voice_id && value === voiceClone.custom_voice_id); const selectValue = useMemo(() => { if (isVoiceCloneSelected) return VOICE_CLONE_ID; if (value && voiceOptions.some(v => v.id === value)) return value; return voiceOptions.length > 0 ? voiceOptions[0].id : ""; }, [value, isVoiceCloneSelected, voiceOptions]); const selectedVoice = useMemo(() => { if (isVoiceCloneSelected) { return voiceOptions.find(v => v.id === VOICE_CLONE_ID); } return voiceOptions.find(v => v.id === value) || voiceOptions[0]; }, [value, isVoiceCloneSelected, voiceOptions]); const handleVoiceSet = useCallback(() => { setVoiceCreated(true); setUseCreatedVoice(true); }, []); const handleRedoClone = useCallback(() => { setSelectOpen(false); setTimeout(() => { setRedoingClone(true); setShowVoiceClonePanel(true); setVoiceCreated(false); setUseCreatedVoice(true); }, 150); }, []); const handleDoneWithVoice = useCallback(() => { if (useCreatedVoice) { fetchVoiceClone(); } setShowVoiceClonePanel(false); setVoiceCreated(false); setRedoingClone(false); }, [useCreatedVoice]); const handleCancelRedo = useCallback(() => { setShowVoiceClonePanel(false); setRedoingClone(false); setVoiceCreated(false); }, []); const handleTogglePanel = useCallback(() => { if (showVoiceClonePanel) { setShowVoiceClonePanel(false); setVoiceCreated(false); setRedoingClone(false); } else { setShowVoiceClonePanel(true); setVoiceCreated(false); setUseCreatedVoice(true); setRedoingClone(false); } }, [showVoiceClonePanel]); const isPreviewing = playingPreview !== null; useEffect(() => { if (externalAudioSettings) { setLocalAudioSettings(externalAudioSettings); } }, [externalAudioSettings]); const hasCustomSettings = externalAudioSettings && ( externalAudioSettings.speed !== 1.0 || externalAudioSettings.volume !== 1.0 || externalAudioSettings.pitch !== 0 || externalAudioSettings.emotion !== "neutral" ); const handleApplyTune = useCallback(() => { if (onAudioSettingsChange) { onAudioSettingsChange(localAudioSettings); } setTuneModalOpen(false); }, [localAudioSettings, onAudioSettingsChange]); const handleOpenTune = useCallback((voiceId: string) => { setTuneForVoice(voiceId); setLocalAudioSettings(externalAudioSettings || { ...DEFAULT_AUDIO_SETTINGS }); setTuneModalOpen(true); }, [externalAudioSettings]); // Gradient style for Tune icon button const tuneButtonSx = useMemo(() => ({ textTransform: 'none' as const, fontSize: "0.68rem", fontWeight: 600, minWidth: 60, py: 0.3, borderRadius: 1.5, background: "linear-gradient(135deg, rgba(249, 115, 22, 0.08) 0%, rgba(236, 72, 153, 0.08) 100%)", color: "#e17055", borderColor: "rgba(249, 115, 22, 0.3)", "&:hover": { background: "linear-gradient(135deg, rgba(249, 115, 22, 0.18) 0%, rgba(236, 72, 153, 0.18) 100%)", borderColor: "#f97316", }, }), []); if (compact) { return ( Voice ); } return ( 4 Voice Selection {selectedVoice && ( } label={`Active: ${selectedVoice.isCustom ? "My Voice Clone" : selectedVoice.name}`} size="small" sx={{ background: selectedVoice.isCustom ? "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)" : "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)", color: selectedVoice.isCustom ? "#10b981" : "#6366f1", border: `1px solid ${selectedVoice.isCustom ? "rgba(16, 185, 129, 0.25)" : "rgba(99, 102, 241, 0.2)"}`, fontWeight: 600, fontSize: "0.7rem", height: 22, "& .MuiChip-icon": { color: selectedVoice.isCustom ? "#10b981" : "#6366f1" }, }} /> )} { setLocalAudioSettings(externalAudioSettings || { ...DEFAULT_AUDIO_SETTINGS }); setTuneForVoice(null); setTuneModalOpen(true); }} sx={{ color: hasCustomSettings ? "#667eea" : "#94a3b8", background: hasCustomSettings ? "rgba(102, 126, 234, 0.1)" : "transparent", "&:hover": { color: "#667eea", background: "rgba(102, 126, 234, 0.08)" }, }} > {showVoiceClone && loadingVoiceClone && ( )} {selectedVoice?.personality && ( {playingPreview && ( )} {selectedVoice.isCustom ? "My Voice Clone" : selectedVoice.name} {selectedVoice.personality} {playingPreview && ( Playing... )} )} {(showVoiceClone && !voiceClone?.success) || redoingClone ? ( {voiceCreated && ( {redoingClone ? "Voice Clone Updated!" : "Voice Clone Created Successfully!"} {redoingClone ? "Your voice clone has been updated." : "Your custom voice clone is ready. Would you like to use this voice for your podcast?"} setUseCreatedVoice(e.target.checked)} sx={{ color: "#10b981", "&.Mui-checked": { color: "#10b981" }, }} /> } label={ Use this voice for my podcast } /> )} ) : null} {/* Voice Fine-tune Modal */} setTuneModalOpen(false)} maxWidth="sm" fullWidth PaperProps={{ sx: { borderRadius: 3, background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", color: "white", }, }} > Fine-tune Voice setTuneModalOpen(false)} size="small" sx={{ color: "rgba(255,255,255,0.7)" }}> {tuneForVoice && ( Adjusting settings for: {voiceOptions.find(v => v.id === tuneForVoice)?.name || "selected voice"} )} {!tuneForVoice && ( Adjust speed, pitch, volume and emotion for your selected voice. )} Speaking Speed setLocalAudioSettings(s => ({ ...s, speed: v as number }))} sx={{ color: "#4ade80" }} /> 0.5 = Slow • 1.0 = Normal • 2.0 = Fast Volume setLocalAudioSettings(s => ({ ...s, volume: v as number }))} sx={{ color: "#fbbf24" }} /> 0.1 = Very soft • 1.0 = Normal • 10.0 = Very loud Pitch 0 ? `+${localAudioSettings.pitch}` : localAudioSettings.pitch} size="small" sx={{ bgcolor: "rgba(255,255,255,0.15)", color: "white", fontWeight: 600, fontSize: "0.7rem", height: 20 }} /> setLocalAudioSettings(s => ({ ...s, pitch: v as number }))} sx={{ color: "#f87171" }} /> -12 = Very deep • 0 = Normal • +12 = Very high Emotion {EMOTION_OPTIONS.map((em) => ( setLocalAudioSettings(s => ({ ...s, emotion: em }))} sx={{ bgcolor: localAudioSettings.emotion === em ? "rgba(255,255,255,0.25)" : "rgba(255,255,255,0.08)", color: "white", fontWeight: localAudioSettings.emotion === em ? 700 : 400, border: localAudioSettings.emotion === em ? "1px solid rgba(255,255,255,0.5)" : "1px solid rgba(255,255,255,0.1)", cursor: "pointer", "&:hover": { bgcolor: "rgba(255,255,255,0.2)" }, }} /> ))} ); }; export default VoiceSelector;