import { useCallback, useEffect, useRef, useState } from "react"; import { voiceApi } from "../api/endpoints"; import type { Job, VoiceDesignInput, VoiceDesignPreview, VoiceState } from "../api/types"; import { YourVoices } from "../components/voice/YourVoices"; import { ConfirmButton, errorMessage, JobProgress, Loading, Modal, Tabs } from "../components/ui"; import { useJob } from "../hooks/useJob"; import { useUpgrade } from "../hooks/useUpgrade"; /** Plays one URL at a time across the page. */ function usePlayer() { const audio = useRef(null); const [playing, setPlaying] = useState(null); const play = useCallback((url: string, id: string) => { audio.current?.pause(); if (playing === id) { setPlaying(null); return; } const a = new Audio(url); audio.current = a; a.onended = () => setPlaying(null); void a.play(); setPlaying(id); }, [playing]); useEffect(() => () => audio.current?.pause(), []); return { play, playing }; } function fmtSeconds(s: number) { return `${Math.floor(s / 60)}:${String(Math.round(s % 60)).padStart(2, "0")}`; } type GenMode = "options" | "prompt" | "clone"; const GEN_OPTIONS: { key: keyof Omit; label: string; values: string[] }[] = [ { key: "gender", label: "Gender", values: ["female", "male", "neutral"] }, { key: "age", label: "Age", values: ["young", "middle-aged", "older"] }, { key: "persona", label: "Personality", values: ["warm", "professional", "calm", "friendly", "confident", "energetic"] }, { key: "pace", label: "Pace", values: ["slow", "measured", "brisk"] }, { key: "accent", label: "Accent", values: ["American", "British", "Australian", "Irish", "Scottish", "Indian", "Canadian", "South African"] }, ]; const PROMPT_IDEAS = [ "A calm, low British voice for late night essays", "An upbeat young American podcaster who smiles while talking", "A thoughtful older narrator with a soft Irish lilt", ]; /** Make a new voice: build it from options, describe it, or clone your own. Inside the Manage voices modals. */ function GenerateVoicePanel({ state, onDone, onSaved, onClone, }: { state: VoiceState; /** Saved: the caller closes or switches away. */ onDone: () => void; onSaved: (s: VoiceState, voiceId: string) => void; onClone: () => void; }) { const [mode, setMode] = useState("prompt"); // Describe it first const [opts, setOpts] = useState({ gender: "female", age: "middle-aged", persona: "warm", pace: "measured", accent: "American" }); const [prompt, setPrompt] = useState(""); const [result, setResult] = useState<{ description: string; previews: VoiceDesignPreview[] } | null>(null); const [picked, setPicked] = useState(null); const [name, setName] = useState(""); const [busy, setBusy] = useState<"generate" | "save" | null>(null); const [error, setError] = useState(null); const { play, playing } = usePlayer(); const { openUpgrade } = useUpgrade(); const generate = async () => { setBusy("generate"); setError(null); setResult(null); setPicked(null); try { const res = await voiceApi.design(mode === "prompt" ? { prompt } : opts); setResult(res); setPicked(res.previews[0]?.generated_voice_id ?? null); } catch (e) { setError(errorMessage(e)); } finally { setBusy(null); } }; const save = async () => { if (!result || !picked) return; setBusy("save"); setError(null); try { const saved = await voiceApi.saveDesign({ generated_voice_id: picked, name: name.trim(), description: result.description }); onSaved(saved, saved.voice_id); onDone(); } catch (e) { setError(errorMessage(e)); } finally { setBusy(null); } }; return ( <>
tabs={[ { id: "prompt", label: "Describe it" }, { id: "clone", label: "Clone my voice" }, { id: "options", label: "Build from options" }, ]} value={mode} onChange={(m) => { setMode(m); setResult(null); setError(null); }} /> {mode === "options" && (
{GEN_OPTIONS.map((g) => (
{g.label}
{g.values.map((v) => ( ))}
))}
)} {mode === "prompt" && ( <>