Spaces:
Runtime error
Runtime error
Refactor VideoTemplateEditor component: remove AI editing features and related state management, streamline template loading, and update styles for improved layout. Enhance CSS for responsive design and visual consistency across various components, including dropdowns, sync stats, and quiz elements. Remove deprecated premium badge styles and adjust layout for chat components.
66de9c8 Download frontend/src/components/voice/YourVoices.tsx from firebird-technologies/NoteStack-Test: direct link, hf CLI and curl.
- Browser
- Download file 5.67 kB
-
https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/frontend/src/components/voice/YourVoices.tsx
- Command line
-
hf download hf://spaces/firebird-technologies/NoteStack-Test/frontend/src/components/voice/YourVoices.tsx
-
curl -L -o YourVoices.tsx https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/frontend/src/components/voice/YourVoices.tsx
5.67 kB
| 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> | |
| ); | |
| } | |