diff --git a/materialious/src/lib/components/Transcript.svelte b/materialious/src/lib/components/Transcript.svelte index cb534415..bcffad51 100644 --- a/materialious/src/lib/components/Transcript.svelte +++ b/materialious/src/lib/components/Transcript.svelte @@ -40,118 +40,6 @@ } }); - export function dedupeAndMergeOverlappingCues(cues: VTTCue[], timeOffset = -1): VTTCue[] { - const merged: VTTCue[] = []; - - for (const cue of cues) { - const trimmedText = cue.text.trim(); - if (!trimmedText) { - continue; - } - - let wasMerged = false; - - for (let i = 0; i < merged.length; i++) { - const existing = merged[i]; - - const timeGap = Math.abs(cue.startTime - existing.endTime); - const isTimeAdjacent = timeGap <= 1.5; - - if (isTimeAdjacent && areCuesTextSimilar(existing.text, cue.text)) { - const mergedText = mergeCueText(existing.text, cue.text); - - const newStart = Math.max(0, Math.min(existing.startTime, cue.startTime) + timeOffset); - const newEnd = Math.max(existing.endTime, cue.endTime) + timeOffset; - - const mergedCue = new VTTCue(newStart, newEnd, mergedText); - copyCueMeta(mergedCue, cue); - - merged[i] = mergedCue; - wasMerged = true; - break; - } - } - - if (!wasMerged) { - const offsetCue = new VTTCue( - Math.max(0, cue.startTime + timeOffset), - cue.endTime + timeOffset, - cue.text - ); - copyCueMeta(offsetCue, cue); - merged.push(offsetCue); - } - } - - // Ensure no overlapping cues (each cue ends before the next starts) - for (let i = 0; i < merged.length - 1; i++) { - const current = merged[i]; - const next = merged[i + 1]; - if (current.endTime > next.startTime) { - current.endTime = Math.max(current.startTime, next.startTime - 0.01); // 10ms gap - } - } - - return merged; - } - - function areCuesTextSimilar(a: string, b: string): boolean { - if (a === b) return true; - - // Normalize and compare overlap ratio - const cleanA = normalizeText(a); - const cleanB = normalizeText(b); - - const overlap = getTextOverlap(cleanA, cleanB); - const minLength = Math.min(cleanA.length, cleanB.length); - - // Consider similar if 50% or more overlap - return overlap / minLength >= 0.5; - } - - function getTextOverlap(a: string, b: string): number { - const maxOverlap = Math.min(a.length, b.length); - for (let i = maxOverlap; i > 0; i--) { - if (a.endsWith(b.slice(0, i)) || b.endsWith(a.slice(0, i))) { - return i; - } - } - return 0; - } - - function normalizeText(text: string): string { - return text - .trim() - .replace(/\s+/g, ' ') - .replace(/[.,?!"'”“]/g, '') - .toLowerCase(); - } - - function mergeCueText(a: string, b: string): string { - // Avoid duplicating overlap - const overlapLen = getTextOverlap(a, b); - if (a.endsWith(b.slice(0, overlapLen))) { - return a + b.slice(overlapLen); - } - if (b.endsWith(a.slice(0, overlapLen))) { - return b + a.slice(overlapLen); - } - return `${a} ${b}`; - } - - function copyCueMeta(target: VTTCue, source: VTTCue): void { - target.region = source.region; - target.vertical = source.vertical; - target.snapToLines = source.snapToLines; - target.line = source.line; - target.lineAlign = source.lineAlign; - target.position = source.position; - target.positionAlign = source.positionAlign; - target.size = source.size; - target.align = source.align; - target.style = source.style; - } - async function loadTranscript() { if (!url) { transcript = null; @@ -160,11 +48,31 @@ isLoading = true; transcript = null; - const resp = await fetch(`${!video.fallbackPatch ? get(instanceStore) : ''}${url}`); transcript = await parseText(await resp.text(), { strict: false }); - transcriptCues = dedupeAndMergeOverlappingCues(transcript.cues); + // Group cues by Math.ceil(startTime) + const startTimeMap = new Map(); + for (const cue of transcript.cues) { + const roundedTime = Math.ceil(cue.startTime); + if (!startTimeMap.has(roundedTime)) { + startTimeMap.set(roundedTime, []); + } + startTimeMap.get(roundedTime)!.push(cue); + } + + // Keep only the second cue if multiple exist for the same rounded time + transcriptCues = []; + for (const [, cues] of startTimeMap.entries()) { + if (cues.length === 1) { + transcriptCues.push(cues[0]); + } else if (cues.length >= 2) { + transcriptCues.push(cues[1]); // Keep the second one + } + } + + transcript.cues = transcriptCues; + isLoading = false; } @@ -172,7 +80,7 @@ if (!transcript) return; if (search.trim() === '') { - transcriptCues = dedupeAndMergeOverlappingCues(transcript.cues); + transcriptCues = transcript.cues; return; } @@ -180,8 +88,7 @@ keys: ['text'] }); - const results = fuse.search(search).map((item) => item.item); - transcriptCues = dedupeAndMergeOverlappingCues(results); + transcriptCues = fuse.search(search).map((item) => item.item); } @@ -260,4 +167,8 @@ .transcript-text { margin-left: 1em; } + + .transcript-line .chip { + padding: 0 1.5em; + } diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 947c9412..da05c36d 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -606,7 +606,7 @@
-
+
    {#each data.content.timestamps as timestamp}