import { useCallback, useEffect, useState } from "react"; import { videoVoicesApi, voiceApi } from "../../api/endpoints"; import type { VideoLibraryVoice, VideoSavedVoice, VideoVoicesResponse } from "../../api/types"; import { errorMessage, Loading } from "../ui"; import { PlayButton, useAudio } from "../video/parts"; const cap = (s?: string | null) => (s ? s[0].toUpperCase() + s.slice(1) : ""); /** Play a voice: library voices have a sample link; the workspace's own (Notestack) voices have none, so the Voice * page speaks a line in them. */ export function useVoicePlayer(onError: (msg: string) => void) { const { playing, play, stop } = useAudio(); const playNotestack = async (voiceId: string) => { if (playing === voiceId) return stop(); const res = await voiceApi.preview(voiceId).catch((e) => { onError(errorMessage(e)); return null; }); if (res?.url) play(voiceId, res.url); }; const playSaved = (v: VideoSavedVoice) => v.source === "notestack" ? playNotestack(v.voice_id) : play(v.voice_id, v.preview_url); return { playing, play, playNotestack, playSaved }; } /** The Voice page's Speaking voices: the workspace's voices (one list for audio overviews and videos, as many as * wanted) and, below, the built-in library to add from. New voices (Add voices) are saved here by the server. */ export function YourVoices({ reloadKey = 0, onAdd }: { /** Bumped by the Voice page after a voice was made or cloned, so the list reloads. */ reloadKey?: number; /** Opens the Add voices modal (build, describe or clone). */ onAdd: () => void; }) { const [voices, setVoices] = useState(null); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const { playing, play, playSaved } = useVoicePlayer(setError); const load = useCallback(() => videoVoicesApi.list().then(setVoices).catch((e) => setError(errorMessage(e))), []); useEffect(() => { load(); }, [load, reloadKey]); async function act(voiceId: string, fn: () => Promise) { setBusy(voiceId); setError(null); try { await fn(); await load(); } catch (e) { setError(errorMessage(e)); } finally { setBusy(null); } } const add = (v: VideoLibraryVoice) => act(v.voice_id, () => videoVoicesApi.save(v.voice_id)); const library = voices?.library.filter((v) => !v.saved) ?? []; return (

Your voices read your audio overviews, and the free ones can narrate videos. Add your own, or pick from the library.

{error &&

{error}

} {!voices && !error &&
} {voices && ( <>

Your voices {voices.saved.length}

{voices.saved.length === 0 ? (

No voices yet. Add one, or pick from the library below.

) : (
{voices.saved.map((v) => (
playSaved(v)} />
{v.name} {v.source === "notestack" ? "Your voice" : [cap(v.gender), cap(v.accent)].filter(Boolean).join(" • ")}
))}
)}

Voice library

{library.length === 0 ? (

Every library voice is in your voices.

) : (
{library.map((v) => (
play(v.voice_id, v.preview_url)} />
{v.name} {[cap(v.gender), cap(v.accent), cap(v.age)].filter(Boolean).join(" • ")} {v.description ? ` - ${v.description}` : ""}
))}
)}
)}
); }