Spaces:
Sleeping
Sleeping
| import { useState, useRef, useCallback } from 'react'; | |
| import { useAppStore } from '../store'; | |
| import { generateSpeech, audioUrlWithCacheBust } from '../api/generate'; | |
| import { playBlobAudio, playPing } from '../utils/media'; | |
| import { probeAudioDuration } from '../utils/format'; | |
| import { CLONE_MAX_SECONDS, PRESETS } from '../utils/constants'; | |
| import { toast } from 'react-hot-toast'; | |
| /** | |
| * Encapsulates TTS generation logic, streaming response handling, | |
| * audio ingestion (with trim gate), and preset/tag helpers. | |
| */ | |
| export default function useTTS({ selectedProfile, setSelectedProfile, loadHistory }) { | |
| const text = useAppStore(s => s.text); | |
| const setText = useAppStore(s => s.setText); | |
| const language = useAppStore(s => s.language); | |
| const instruct = useAppStore(s => s.instruct); | |
| const refText = useAppStore(s => s.refText); | |
| const speed = useAppStore(s => s.speed); | |
| const steps = useAppStore(s => s.steps); | |
| const cfg = useAppStore(s => s.cfg); | |
| const denoise = useAppStore(s => s.denoise); | |
| const tShift = useAppStore(s => s.tShift); | |
| const posTemp = useAppStore(s => s.posTemp); | |
| const classTemp = useAppStore(s => s.classTemp); | |
| const layerPenalty = useAppStore(s => s.layerPenalty); | |
| const postprocess = useAppStore(s => s.postprocess); | |
| const duration = useAppStore(s => s.duration); | |
| const vdStates = useAppStore(s => s.vdStates); | |
| const mode = useAppStore(s => s.mode); | |
| const setSidebarTab = useAppStore(s => s.setSidebarTab); | |
| const [refAudio, setRefAudio] = useState(null); | |
| const [pendingTrimFile, setPendingTrimFile] = useState(null); | |
| const [isGenerating, setIsGenerating] = useState(false); | |
| const [generationTime, setGenerationTime] = useState(0); | |
| const timerRef = useRef(null); | |
| const textAreaRef = useRef(null); | |
| const ingestRefAudio = useCallback(async (file) => { | |
| if (!file) { setRefAudio(null); return; } | |
| const dur = await probeAudioDuration(file); | |
| if (dur && dur > CLONE_MAX_SECONDS) { | |
| setPendingTrimFile(file); | |
| setSelectedProfile(null); | |
| toast(`Audio is ${dur.toFixed(1)}s — trim to ≤${CLONE_MAX_SECONDS}s for best cloning`); | |
| return; | |
| } | |
| setRefAudio(file); | |
| setSelectedProfile(null); | |
| }, [setSelectedProfile]); | |
| const insertTag = useCallback((tag) => { | |
| if (!textAreaRef.current) return; | |
| const start = textAreaRef.current.selectionStart; | |
| const end = textAreaRef.current.selectionEnd; | |
| setText(text.substring(0, start) + tag + text.substring(end)); | |
| setTimeout(() => { textAreaRef.current.focus(); textAreaRef.current.setSelectionRange(start + tag.length, start + tag.length); }, 0); | |
| }, [text, setText]); | |
| const applyPreset = useCallback((preset) => { | |
| useAppStore.getState().setVdStates(preset.attrs); | |
| if (preset.tags && !text.includes(preset.tags.trim())) insertTag(preset.tags); | |
| }, [text, insertTag]); | |
| const handleGenerate = useCallback(async () => { | |
| if (!text.trim()) return toast.error("Please enter text"); | |
| if (mode === 'clone' && !refAudio && !selectedProfile) return toast.error("Upload an audio or select a voice profile"); | |
| setIsGenerating(true); | |
| setGenerationTime(0); | |
| const st = Date.now(); | |
| timerRef.current = setInterval(() => { | |
| const elapsed = ((Date.now() - st) / 1000).toFixed(1); | |
| setGenerationTime(prev => { | |
| const suffix = /\(\d+%\)$/.exec(String(prev))?.[0]; | |
| return suffix ? `${elapsed} ${suffix}` : elapsed; | |
| }); | |
| }, 100); | |
| try { | |
| const formData = new FormData(); | |
| formData.append("text", text); | |
| if (language !== 'Auto') formData.append("language", language); | |
| formData.append("num_step", steps); | |
| formData.append("guidance_scale", cfg); | |
| formData.append("speed", speed); | |
| formData.append("denoise", denoise); | |
| formData.append("t_shift", tShift); | |
| formData.append("position_temperature", posTemp); | |
| formData.append("class_temperature", classTemp); | |
| formData.append("layer_penalty_factor", layerPenalty); | |
| formData.append("postprocess_output", postprocess); | |
| if (duration) formData.append("duration", parseFloat(duration)); | |
| if (mode === 'clone') { | |
| if (selectedProfile) { | |
| formData.append("profile_id", selectedProfile); | |
| } else if (refAudio) { | |
| const arrBuf = await refAudio.arrayBuffer(); | |
| const safeBlob = new Blob([arrBuf], { type: refAudio.type }); | |
| formData.append("ref_audio", safeBlob, refAudio.name || "audio.wav"); | |
| formData.append("ref_text", refText); | |
| } | |
| if (instruct) formData.append("instruct", instruct); | |
| } else { | |
| const designSeed = Math.floor(Math.random() * 2147483647); | |
| formData.append("seed", designSeed); | |
| const parts = Object.values(vdStates).filter(v => v !== 'Auto'); | |
| const finalInstruct = parts.join(', '); | |
| if (finalInstruct) formData.append("instruct", finalInstruct); | |
| if (selectedProfile) { | |
| formData.append("profile_id", selectedProfile); | |
| } | |
| } | |
| const response = await generateSpeech(formData); | |
| const reader = response.body.getReader(); | |
| const chunks = []; | |
| let receivedLength = 0; | |
| const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10); | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| chunks.push(value); | |
| receivedLength += value.length; | |
| if (contentLength > 0) { | |
| const pct = Math.round((receivedLength / contentLength) * 100); | |
| setGenerationTime(prev => `${prev.toString().split(' ')[0]} (${pct}%)`); | |
| } | |
| } | |
| const blob = new Blob(chunks, { type: 'audio/wav' }); | |
| try { await playBlobAudio(blob); } catch (e) {} | |
| await loadHistory(); | |
| setSidebarTab('history'); | |
| playPing(); | |
| } catch (err) { | |
| toast.error("Error: " + err.message); | |
| } finally { | |
| clearInterval(timerRef.current); | |
| setIsGenerating(false); | |
| } | |
| }, [text, mode, selectedProfile, refAudio, refText, language, instruct, steps, cfg, speed, denoise, tShift, posTemp, classTemp, layerPenalty, postprocess, duration, vdStates, loadHistory, setSidebarTab]); | |
| return { | |
| refAudio, setRefAudio, | |
| pendingTrimFile, setPendingTrimFile, | |
| isGenerating, generationTime, | |
| textAreaRef, | |
| ingestRefAudio, | |
| insertTag, applyPreset, | |
| handleGenerate, | |
| }; | |
| } | |