Spaces:
Sleeping
Sleeping
Download src/editor/useClips.ts from vikasbanjare/pulse: direct link, hf CLI and curl.
- Browser
- Download file 4.34 kB
-
https://huggingface.co/spaces/vikasbanjare/pulse/resolve/main/src/editor/useClips.ts
- Command line
-
hf download hf://spaces/vikasbanjare/pulse/src/editor/useClips.ts
-
curl -L -o useClips.ts https://huggingface.co/spaces/vikasbanjare/pulse/resolve/main/src/editor/useClips.ts
4.34 kB
| import { useEffect, useState } from 'react' | |
| import { useEditor } from '../store' | |
| import { api } from '../lib/api' | |
| import { clipWords, remapDoc, segsOf, segLen, type Seg } from '../lib/clips' | |
| const uid = () => Math.random().toString(36).slice(2, 10) | |
| /** Clip operations: every change re-renders the timeline video on the server, then edits are carried across. */ | |
| export function useClips() { | |
| const s = useEditor() | |
| const [busy, setBusy] = useState<string | null>(null) | |
| const [sel, setSel] = useState<string | null>(null) | |
| const segs = s.project ? segsOf(s.project) : [] | |
| const apply = async (next: Omit<Seg, 'start'>[], label: string) => { | |
| const st = useEditor.getState(); if (!st.project || busy) return | |
| const before = segsOf(st.project) | |
| setBusy(label) | |
| try { | |
| const m = await api.assemble(st.project.id, next) | |
| const now = useEditor.getState() | |
| const r = remapDoc(now, before, m.segments!, m.duration) | |
| // clip edits re-render the video, so they start a fresh undo history | |
| now.set({ project: { ...now.project!, ...m }, ...r, past: [], future: [], selected: null, capSel: false }) | |
| await addPendingWords() | |
| } catch (e) { alert(`Could not rebuild the video: ${e}`) } finally { setBusy(null) } | |
| } | |
| /** Words of clips whose transcription finished since they were added. */ | |
| const addPendingWords = async () => { | |
| const st = useEditor.getState(); const p = st.project; if (!p?.clips) return | |
| for (const c of p.clips) { | |
| if (st.clipWords.includes(c.id) || c.transcribeStatus !== 'done') continue | |
| const tr = await api.clipTranscript(p.id, c.id).catch(() => null); if (!tr) continue | |
| const cur = useEditor.getState() | |
| const add = clipWords(tr, c.id, segsOf(cur.project!)) | |
| cur.set({ words: [...cur.words, ...add].sort((a, b) => a.start - b.start), clipWords: [...cur.clipWords, c.id] }) | |
| } | |
| } | |
| // poll while a clip is still being transcribed | |
| const pending = s.project?.clips?.some(c => c.transcribeStatus === 'running') | |
| useEffect(() => { | |
| if (!pending || !s.project) return | |
| const id = setInterval(async () => { | |
| const m = await api.get(s.project!.id).catch(() => null); if (!m) return | |
| const cur = useEditor.getState(); cur.set({ project: { ...cur.project!, clips: m.clips } }) | |
| if (!m.clips?.some(c => c.transcribeStatus === 'running')) { clearInterval(id); addPendingWords() } | |
| }, 2500) | |
| return () => clearInterval(id) | |
| }, [pending, s.project?.id]) // eslint-disable-line react-hooks/exhaustive-deps | |
| const strip = (x: Seg[]) => x.map(({ start: _s, ...r }) => r) // eslint-disable-line @typescript-eslint/no-unused-vars | |
| const addFile = async (file: File) => { | |
| const p = useEditor.getState().project; if (!p) return | |
| setBusy('Uploading clip…') | |
| try { | |
| const c = await api.addClip(p.id, file) | |
| const cur = useEditor.getState(); cur.set({ project: { ...cur.project!, clips: [...(cur.project!.clips ?? []), c] } }) | |
| setBusy(null) | |
| await apply([...strip(segsOf(cur.project!)), { id: uid(), clip: c.id, in: 0, out: c.duration, transition: 'fade', tdur: 0.5 }], 'Adding clip…') | |
| } catch (e) { setBusy(null); alert(`Could not add that clip: ${e}`) } | |
| } | |
| const split = (t = useEditor.getState().time) => { | |
| const sg = segs.find(x => t > x.start + 0.2 && t < x.start + segLen(x) - 0.2); if (!sg) return | |
| const at = sg.in + (t - sg.start) | |
| const next = segs.flatMap(x => x.id !== sg.id ? [x] : [{ ...x, out: at }, { ...x, id: uid(), in: at, transition: 'none' }]) | |
| apply(strip(next), 'Splitting…') | |
| } | |
| const remove = (id: string) => { if (segs.length < 2) return; setSel(null); apply(strip(segs.filter(x => x.id !== id)), 'Removing clip…') } | |
| const move = (id: string, to: number) => { | |
| const from = segs.findIndex(x => x.id === id); if (from < 0 || from === to) return | |
| const next = segs.slice(); const [x] = next.splice(from, 1); next.splice(Math.min(to, next.length), 0, x) | |
| apply(strip(next), 'Reordering…') | |
| } | |
| const setTransition = (id: string, transition: string, tdur?: number) => | |
| apply(strip(segs.map(x => x.id === id ? { ...x, transition, tdur: tdur ?? x.tdur } : x)), 'Applying transition…') | |
| return { segs, busy, sel, setSel, addFile, split, remove, move, setTransition } | |
| } | |
| export type ClipOps = ReturnType<typeof useClips> | |