/* GenVozTab — v6 */ // ── VoicePicker modal ───────────────────────────────────────────────────── const _VP_PALETTES = [ ["#7c5cff","#3a1fd1"],["#f87171","#b91c1c"],["#60a5fa","#1d4ed8"], ["#4ade80","#15803d"],["#fbbf24","#b45309"],["#a78bfa","#6d28d9"], ["#fb923c","#c2410c"],["#34d399","#065f46"],["#f472b6","#9d174d"], ]; const _VP_LANG = { spanish:"ES", español:"ES", english:"EN", inglés:"EN", portuguese:"PT", português:"PT", french:"FR", français:"FR", german:"DE", deutsch:"DE", japanese:"JP", japonés:"JP", chinese:"ZH", chino:"ZH", korean:"KR", coreano:"KR", italian:"IT", italiano:"IT", }; function vpGrad(name) { let h = 0; for (let i = 0; i < (name || "").length; i++) h = (h * 31 + (name || "").charCodeAt(i)) & 0x7fffffff; const [c1, c2] = _VP_PALETTES[h % _VP_PALETTES.length]; return `linear-gradient(135deg, ${c1} 0%, ${c2} 100%)`; } function vpLangs(tags) { return [...new Set((tags || []).map(t => _VP_LANG[(t || "").toLowerCase().trim()]).filter(Boolean))]; } function VoicePicker({ onSelect, onClose }) { const [tab, setTab] = React.useState("biblioteca"); const [presetVoices, setPresetVoices] = React.useState([]); const [cloneVoices, setCloneVoices] = React.useState([]); const [loading, setLoading] = React.useState(true); const [search, setSearch] = React.useState(""); const [playingId, setPlayingId] = React.useState(null); const audioRef = React.useRef(null); React.useEffect(() => { (async () => { setLoading(true); try { const [pr, cr] = await Promise.all([ fetch("/api/public-voices", { credentials: "same-origin" }).then(r => r.json()), fetch("/api/voice/clones", { credentials: "same-origin" }).then(r => r.json()).catch(() => ({})), ]); setPresetVoices(pr.voices || []); const raw = cr.voice_list || cr.data || cr.voices || cr.clones || []; setCloneVoices(raw.map(c => ({ id: c.voice_id || c.id, voice_id: c.voice_id || c.id, voice_name: c.voice_name || c.name || "Clon sin nombre", description: [c.gender, c.language].filter(Boolean).join(" · "), cover_url: null, sample_url: c.demo_audio || c.sample_url || null, tags: [c.language, c.gender].filter(Boolean), is_clone: true, }))); } catch (e) { console.error(e); } finally { setLoading(false); } })(); }, []); function playPreview(url, id) { if (audioRef.current) audioRef.current.pause(); if (playingId === id) { setPlayingId(null); return; } const a = new Audio(url); audioRef.current = a; a.play().catch(() => {}); setPlayingId(id); a.onended = () => setPlayingId(null); } function handleSelect(v) { if (audioRef.current) audioRef.current.pause(); onSelect(v); } const rawList = tab === "biblioteca" ? presetVoices : cloneVoices; const displayList = search.trim() ? rawList.filter(v => { const q = search.toLowerCase(); return (v.voice_name || "").toLowerCase().includes(q) || (v.description || "").toLowerCase().includes(q) || (v.tags || []).some(t => (t || "").toLowerCase().includes(q)); }) : rawList; return (
{ if (e.target === e.currentTarget) onClose(); }}>
{/* ── Header ── */}
Seleccionar Voz
setSearch(e.target.value)} autoFocus/>
{/* ── Body ── */}
{loading && (
Cargando voces…
)} {!loading && displayList.length === 0 && (
{tab === "clones" ? "No tienes voces clonadas aún" : "No se encontraron voces"}
)} {!loading && displayList.length > 0 && (
{displayList.map(v => { const vid = v.voice_id || v.id; const langs = vpLangs(v.tags); const bg = vpGrad(v.voice_name); const playing = playingId === vid; return (
handleSelect(v)} style={{ overflow: "hidden", cursor: "pointer", display: "flex", flexDirection: "column", transition: "transform 0.12s, box-shadow 0.12s" }} onMouseEnter={e => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "0 8px 28px rgba(0,0,0,0.3)"; }} onMouseLeave={e => { e.currentTarget.style.transform = ""; e.currentTarget.style.boxShadow = ""; }}> {/* Cover */}
{v.cover_url ? {v.voice_name} :
} {v.sample_url && ( )} {v.is_clone && ( TÚ )}
{/* Body */}
{v.voice_name} {langs.map(l => ( {l} ))}
{v.description && (
{v.description}
)}
); })}
)}
); } // ── Toggle helper ───────────────────────────────────────────────────────── function GvToggle({ value, onChange }) { return ( ); } // ── Section label ───────────────────────────────────────────────────────── function GvLabel({ children }) { return (
{children}
); } function GvDivider() { return
; } // ── GenVozTab ───────────────────────────────────────────────────────────── function GenVozTab({ onNavigate }) { const [text, setText] = React.useState(""); const [engine, setEngine] = React.useState("minimax"); const [voiceId, setVoiceId] = React.useState(""); const [voiceName, setVoiceName] = React.useState(""); const [voiceCover, setVoiceCover] = React.useState(null); const [model, setModel] = React.useState("speech-2.8-hd"); const [speed, setSpeed] = React.useState(0.95); const [pitch, setPitch] = React.useState(0); const [vol, setVol] = React.useState(1.0); const [langBoost, setLangBoost] = React.useState("Spanish"); const [loudness, setLoudness] = React.useState(false); const [exportTranscript, setExportTranscript] = React.useState(true); const [pickerOpen, setPickerOpen] = React.useState(false); const [generating, setGenerating] = React.useState(false); const [audioUrl, setAudioUrl] = React.useState(null); const [error, setError] = React.useState(null); const pollRef = React.useRef(null); const MAX_CHARS = 1455849; const MODELS = { minimax: [ { value: "speech-2.8-hd", label: "speech-2.8-hd — Alta calidad" }, { value: "speech-2.6-hd", label: "speech-2.6-hd — Equilibrado" }, { value: "speech-01-turbo", label: "speech-01-turbo — Rápido" }, ], elevenlabs: [ { value: "eleven_multilingual_v2", label: "Multilingual v2 — Mejor calidad" }, { value: "eleven_turbo_v2_5", label: "Turbo v2.5 — Rápido" }, { value: "eleven_flash_v2_5", label: "Flash v2.5 — Más rápido" }, ], }; const LANGS = ["Auto", "Spanish", "English", "Chinese", "French", "German", "Japanese", "Korean", "Portuguese", "Italian"]; React.useEffect(() => { setModel(engine === "minimax" ? "speech-2.8-hd" : "eleven_multilingual_v2"); setVoiceId(""); setVoiceName(""); setVoiceCover(null); setAudioUrl(null); setError(null); }, [engine]); function handleVoiceSelect(v) { setVoiceId(v.voice_id || v.id || ""); setVoiceName(v.voice_name || ""); setVoiceCover(v.cover_url || v.avatar_url || null); setPickerOpen(false); } function stopPolling() { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } } React.useEffect(() => () => stopPolling(), []); function handleReset() { setSpeed(0.95); setPitch(0); setVol(1.0); setLoudness(false); setExportTranscript(true); } async function handleGenerate() { if (!text.trim() || !voiceId || generating) return; setGenerating(true); setError(null); setAudioUrl(null); stopPolling(); try { const r = await fetch("/api/voice/tts", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "same-origin", body: JSON.stringify({ text: text.trim(), voice_id: voiceId, voice_name: voiceName, model, speed, pitch: Math.round(pitch), vol, language_boost: langBoost, engine }), }); const d = await r.json(); if (!r.ok) { setError(d.detail || "Error al iniciar generación"); setGenerating(false); return; } const tid = d.task_id || d.id; if (!tid) { setError("No se recibió task_id"); setGenerating(false); return; } let tries = 0; pollRef.current = setInterval(async () => { tries++; if (tries > 60) { stopPolling(); setGenerating(false); setError("Tiempo de espera agotado"); return; } try { const pr = await fetch(`/api/voice/task/${tid}`, { credentials: "same-origin" }); const pd = await pr.json(); const done = ["done", "Success", "success", "completed"].includes(pd.status); const failed = ["error", "failed"].includes(pd.status); if (done) { stopPolling(); setGenerating(false); const url = (pd.metadata && pd.metadata.audio_url) || pd.audio_url || (pd.data && pd.data.audio_url) || null; if (url) setAudioUrl(url); else setError("Audio generado pero URL no encontrada"); } else if (failed) { stopPolling(); setGenerating(false); setError(pd.error_message || pd.message || "Error en la generación"); } } catch (_) {} }, 3000); } catch (e) { setError(e.message || "Error de red"); setGenerating(false); } } function handleDrop(e) { e.preventDefault(); const file = e.dataTransfer.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => setText(ev.target.result.slice(0, MAX_CHARS)); reader.readAsText(file); } const canGenerate = text.trim().length > 0 && voiceId && !generating; const currentModels = MODELS[engine] || MODELS.minimax; // topbar ≈ 64px, page padding top 28 + bottom 48 = 76px → 140px total const panelH = "calc(100vh - 140px)"; const cardBox = { background: "var(--bg-card)", border: "1px solid var(--border)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-card)", overflow: "hidden", display: "flex", flexDirection: "column", }; return (
{/* ── RIGHT: Controls box ── */}
{/* Header */}
Generar Voz
{/* Scrollable body */}
{/* Engine */} Motor TTS
{/* Voice */} Voz {/* Language */} Idioma {/* Model */} Modelo {/* Sliders */} Parámetros {[ { label: "Velocidad", value: speed, display: speed.toFixed(2), min: 0.5, max: 2, step: 0.05, set: setSpeed, marks: ["Lenta", "Rápida"] }, { label: "Tono", value: pitch, display: pitch >= 0 ? `+${pitch}` : String(pitch), min: -12, max: 12, step: 1, set: v => setPitch(parseInt(v)), marks: null }, { label: "Volumen", value: vol, display: vol.toFixed(2), min: 0.1, max: 2, step: 0.05, set: setVol, marks: null }, ].map(s => (
{s.label} {s.display}
s.set(parseFloat(e.target.value))} style={{ width: "100%", accentColor: "var(--accent)", display: "block", margin: 0 }}/> {s.marks && (
{s.marks.map(m => {m})}
)}
))}
{/* Toggles */} Opciones
Normalización de loudness Beta
Exportar transcripción +15%
{/* Warning */}
La velocidad puede ser lenta con texto corto y rápida con texto largo.
{/* ── LEFT: Textarea box ── */}