Spaces:
Runtime error
Runtime error
File size: 5,670 Bytes
e1222dc 66de9c8 e1222dc 66de9c8 e1222dc 66de9c8 e1222dc 66de9c8 e1222dc 66de9c8 e1222dc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 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<VideoVoicesResponse | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const [error, setError] = useState<string | null>(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<unknown>) {
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 (
<div className="stack">
<div className="row between wrap manage-top">
<p className="muted">Your voices read your audio overviews, and the free ones can narrate videos. Add your own, or pick from the library.</p>
<button className="btn btn-primary" onClick={onAdd}>Add voices</button>
</div>
{error && <p className="error-text">{error}</p>}
{!voices && !error && <div className="loading-center inline"><Loading label="Loading voices" /></div>}
{voices && (
<>
<section className="stack yv-panel">
<h3 className="yv-head">Your voices <span className="yv-count mono">{voices.saved.length}</span></h3>
{voices.saved.length === 0 ? (
<p className="muted">No voices yet. Add one, or pick from the library below.</p>
) : (
<div className="vw-voice-list vw-voice-list-tight vw-voice-list-four">
{voices.saved.map((v) => (
<div key={v.voice_id} className="vw-vrow">
<PlayButton on={playing === v.voice_id} disabled={v.source !== "notestack" && !v.preview_url}
label={v.name} onClick={() => playSaved(v)} />
<div className="vw-vpick static">
<strong>{v.name}</strong>
<span className="muted">
{v.source === "notestack" ? "Your voice"
: [cap(v.gender), cap(v.accent)].filter(Boolean).join(" • ")}
</span>
</div>
<button className="btn btn-small" disabled={busy === v.voice_id}
onClick={() => act(v.voice_id, () => videoVoicesApi.unsave(v.voice_id))}>
Remove
</button>
</div>
))}
</div>
)}
</section>
<section className="stack yv-panel">
<h3 className="yv-head">Voice library</h3>
{library.length === 0 ? (
<p className="muted">Every library voice is in your voices.</p>
) : (
<div className="vw-voice-list vw-voice-list-tight">
{library.map((v) => (
<div key={v.voice_id} className="vw-vrow">
<PlayButton on={playing === v.voice_id} disabled={!v.preview_url} label={v.name}
onClick={() => play(v.voice_id, v.preview_url)} />
<div className="vw-vpick static">
<strong>{v.name}</strong>
<span className="muted">
{[cap(v.gender), cap(v.accent), cap(v.age)].filter(Boolean).join(" • ")}
{v.description ? ` - ${v.description}` : ""}
</span>
</div>
<button className="btn btn-small btn-primary" disabled={busy === v.voice_id} onClick={() => add(v)}>
Add
</button>
</div>
))}
</div>
)}
</section>
</>
)}
</div>
);
}
|