import { useEffect, useMemo, useRef, useState } from "react"; import { activateModel, generate, generateDialog, getActiveModel, listModels, streamActiveEvents, type ModelInfo, } from "@/lib/api"; import { addHistory, type HistoryRecord, type VoiceRecord } from "@/lib/idb"; import DeviceBadge from "@/components/DeviceBadge"; import DialogComposer, { type DialogSubmit } from "@/components/DialogComposer"; import HistoryList from "@/components/HistoryList"; import LoadingBanner from "@/components/LoadingBanner"; import MadeBy from "@/components/MadeBy"; import ModelPicker from "@/components/ModelPicker"; import ModeToggle, { type Mode } from "@/components/ModeToggle"; import ParamsPanel from "@/components/ParamsPanel"; import ProgressBar from "@/components/ProgressBar"; import TagBar from "@/components/TagBar"; import VoiceComposer from "@/components/VoiceComposer"; import VoiceLibrary from "@/components/VoiceLibrary"; import { cn } from "@/lib/utils"; function SectionHeader({ num, title, hint }: { num: string; title: string; hint?: string }) { return (
{num}

{title}

{hint &&

{hint}

}
); } export default function Studio() { const [mode, setMode] = useState("single"); const [models, setModels] = useState([]); const [activeId, setActiveId] = useState(null); const [dialogEngineId, setDialogEngineId] = useState("chatterbox-en"); const [loadingModel, setLoadingModel] = useState(false); const [tab, setTab] = useState<"voices" | "history">("voices"); const [text, setText] = useState(""); const [language, setLanguage] = useState(undefined); const [params, setParams] = useState>({}); const [selectedVoice, setSelectedVoice] = useState(); const [outputUrl, setOutputUrl] = useState(null); const [historyKey, setHistoryKey] = useState(0); const [libraryKey, setLibraryKey] = useState(0); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); const textRef = useRef(null); useEffect(() => { listModels().then((m) => { setModels(m); if (m[0]) { setActiveId((cur) => cur ?? m[0].id); setDialogEngineId((cur) => cur || m[0].id); } }); getActiveModel().then((s) => setActiveId((cur) => cur ?? s.id)); }, []); useEffect(() => { const close = streamActiveEvents((evt) => { if (evt.status === "loading") setLoadingModel(true); if (evt.status === "loaded" || evt.status === "error") setLoadingModel(false); if (evt.status === "loaded" && evt.id) setActiveId(evt.id); if (evt.status === "error" && evt.error) setErr(evt.error); }); return close; }, []); const active = useMemo( () => models.find((m) => m.id === activeId), [models, activeId], ); useEffect(() => { setParams( Object.fromEntries((active?.params ?? []).map((p) => [p.name, p.default])), ); setLanguage(active?.languages[0]?.code); }, [active?.id]); async function pickModel(id: string) { setLoadingModel(true); setErr(null); try { await activateModel(id); setActiveId(id); } catch (e) { setErr((e as Error).message); } finally { setLoadingModel(false); } } async function onGenerate(reuse?: HistoryRecord) { if (!active) return; if (active.supports_voice_clone && !selectedVoice && !reuse?.voiceId) { setErr("Pick or record a reference voice first."); return; } setErr(null); setBusy(true); try { const refBlob = selectedVoice?.blob; const inputText = reuse?.text ?? text; const inputLang = reuse?.language ?? language; const inputParams = reuse?.params ?? params; const result = await generate({ modelId: active.id, text: inputText, language: inputLang, params: inputParams, reference: refBlob, }); setOutputUrl((u) => { if (u) URL.revokeObjectURL(u); return URL.createObjectURL(result.blob); }); await addHistory({ text: inputText, modelId: active.id, voiceId: selectedVoice?.id, language: inputLang, params: inputParams, audioBlob: result.blob, kind: "single", seedUsed: result.seedUsed ?? undefined, }); setHistoryKey((k) => k + 1); } catch (e) { setErr((e as Error).message); } finally { setBusy(false); } } async function onDialogSubmit(input: DialogSubmit) { setErr(null); setBusy(true); try { const result = await generateDialog({ engineId: input.engineId, text: input.text, language: input.language, params: input.params, speakers: input.speakers.map((s) => ({ letter: s.letter, reference: s.voice.blob, })), }); setOutputUrl((u) => { if (u) URL.revokeObjectURL(u); return URL.createObjectURL(result.blob); }); await addHistory({ text: input.text, modelId: input.engineId, language: input.language, params: input.params, audioBlob: result.blob, kind: "dialog", seedUsed: result.seedUsed ?? undefined, speakers: input.speakers.map((s) => ({ letter: s.letter, voiceId: s.voice.id! })), }); setHistoryKey((k) => k + 1); } catch (e) { setErr((e as Error).message); } finally { setBusy(false); } } return (
Chatterbox voice studio · v0.2
{mode === "single" && ( )}
{err && (
error {err}
)}
{mode === "single" ? ( <>
setLibraryKey((k) => k + 1)} />
{active?.languages && active.languages.length > 1 && (
)}