diff --git a/spec/edit_caption.png b/spec/edit_caption.png new file mode 100644 index 0000000000000000000000000000000000000000..2d5c62806c4bc8452fd0922f305a1aaed3ffb48e GIT binary patch literal 9540 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 + + + +
+
+ +
+
+

captions

+
+ mode = 'full'}>all + {#if currentCaptionIdx >= 0} + mode = 'edit'}>current + {/if} +
+
+ + {#if mode === 'edit' && currentCaptionIdx >= 0} +
+ + + +
+ {:else} +
+ {#each editCaptions as cap, i} +
+
+ updateCaption(i, 'start', e.currentTarget.value)} /> + - + updateCaption(i, 'end', e.currentTarget.value)} /> +
+ updateCaption(i, 'text', e.currentTarget.value)} /> + +
+ {/each} +
+ {/if} + + {#if saveResult} +

{saveResult}

+ {/if} + + +
+ import + export + apply + {#if auth.session && videoUri} + + {saving ? '...' : 'upload'} + + {/if} + close +
+
+
+
+
+ + diff --git a/src/lib/VideoPlayer.svelte b/src/lib/VideoPlayer.svelte index 256da61..e555288 100644 --- a/src/lib/VideoPlayer.svelte +++ b/src/lib/VideoPlayer.svelte @@ -3,7 +3,9 @@ import Hls from "hls.js"; import WavyBorder from "./WavyBorder.svelte"; import { playCroak } from "./croak"; - import { getModelStatus, getCaptionsEnabled, getCurrentCaption, toggleCaptionsDisplay, updateCaptionForTime, destroyCaptions, getCaptionCount, getDebugState, precomputeCaptions, getIsProcessing, getProcessProgress } from "./captions.svelte"; + import { getModelStatus, getCaptionsEnabled, getCurrentCaption, toggleCaptionsDisplay, updateCaptionForTime, destroyCaptions, getCaptionCount, getDebugState, precomputeCaptions, getIsProcessing, getProcessProgress, getAllCaptions, getProcessedRanges, loadExistingCaptions } from "./captions.svelte"; + import { fetchCaptionsForVideo } from "./caption-records"; + import CaptionEditor from "./CaptionEditor.svelte"; // HLS video source URL (m3u8 playlist) and AT URI for caption pre-computation let { src, atUri = '' }: { src: string; atUri?: string } = $props(); @@ -12,6 +14,15 @@ let hls: Hls | null = null; let errorMsg = $state(""); + // Caption editor + let showEditor = $state(false); + + export function openEditor() { showEditor = true; } + + function onCaptionUpdate(updated: { text: string; start: number; end: number }[]) { + console.log('[CC] Captions updated:', updated.length); + } + // CC debug overlay — toggle with Shift+D let ccDebug = $state(false); let debugInfo = $state({}); @@ -90,9 +101,20 @@ hls = new Hls({ enableWorker: true, lowLatencyMode: false }); hls.loadSource(src); hls.attachMedia(videoEl); - hls.on(Hls.Events.MANIFEST_PARSED, () => { + hls.on(Hls.Events.MANIFEST_PARSED, async () => { videoEl?.play().catch(() => {}); - if (atUri) precomputeCaptions(atUri); + if (atUri) { + // Check for existing captions first + console.log('[CC] Checking for existing captions...'); + const existing = await fetchCaptionsForVideo(atUri); + if (existing?.record?.captions?.length) { + loadExistingCaptions(existing.record.captions); + console.log(`[CC] Loaded ${existing.record.captions.length} existing captions from ${existing.did}`); + } else { + console.log('[CC] No existing captions found, starting Whisper...'); + precomputeCaptions(atUri); + } + } }); hls.on(Hls.Events.ERROR, (_event, data) => { console.error("HLS error:", data); @@ -283,7 +305,8 @@ } function onKeyDown(e: KeyboardEvent) { - if (e.code === "Space" && videoEl) { + const tag = (e.target as HTMLElement)?.tagName; + if (e.code === "Space" && videoEl && tag !== 'INPUT' && tag !== 'TEXTAREA') { e.preventDefault(); togglePlay(); } @@ -428,7 +451,8 @@ {#each (debugInfo.captions ?? []) as cap} {@const vt = debugInfo.videoTime ?? 0} {@const active = vt >= cap.start && vt <= cap.end} -
+ +
{ if (videoEl) { videoEl.currentTime = cap.start; currentTime = cap.start; } }}> {cap.start?.toFixed(1)}s-{cap.end?.toFixed(1)}s {cap.text} {#if active}NOW{/if} @@ -443,7 +467,16 @@ {/if}
-> + +{#if showEditor} + showEditor = false} + onUpdate={onCaptionUpdate} + /> +{/if}