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, 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 { getAuthTokenGetter, getApiUrl } from "../../api/client"; import { VoiceAvatarPlaceholder } from "../OnboardingWizard/PersonalizationStep/components/VoiceAvatarPlaceholder"; import { useVoicePreview } from "./useVoicePreview"; import { useVoiceFiltering } from "./useVoiceFiltering"; import { VoiceClonePanel } from "./VoiceClonePanel"; import { VoiceOption, VoiceAudioSettings, DEFAULT_AUDIO_SETTINGS, EMOTION_OPTIONS, VOICE_PREVIEW_MAP, CATEGORY_OPTIONS, PREDEFINED_VOICES, CategoryFilter, VoiceSelectorGenderFilter, } from "./voiceConstants"; interface VoiceSelectorProps { value: string; onChange: (voiceId: string) => void; disabled?: boolean; showVoiceClone?: boolean; compact?: boolean; audioSettings?: VoiceAudioSettings; onAudioSettingsChange?: (settings: VoiceAudioSettings) => void; } export 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 [showVoiceClonePanel, setShowVoiceClonePanel] = useState(false); const [voiceCreated, setVoiceCreated] = useState(false); 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 prevVoiceCloneIdRef = useRef(null); const { playingPreview, handlePreview, stopCurrentAudio } = useVoicePreview(); const isPreviewing = playingPreview !== null; const { voiceOptions, filteredVoices } = useVoiceFiltering({ showVoiceClone, voiceClone, value, genderFilter, categoryFilter, }); const fetchVoiceClone = useCallback(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); } }, []); 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 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); }, []); const handleRedoClone = useCallback(() => { setSelectOpen(false); setTimeout(() => { setRedoingClone(true); setShowVoiceClonePanel(true); setVoiceCreated(false); }, 150); }, []); const handleDoneWithVoice = useCallback(() => { fetchVoiceClone(); setShowVoiceClonePanel(false); setVoiceCreated(false); setRedoingClone(false); }, []); 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); setRedoingClone(false); } }, [showVoiceClonePanel]); 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 ? ( ) : 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;