Transcript improvements

This commit is contained in:
WardPearce
2025-06-05 21:44:04 +12:00
parent f04fcd9b06
commit d6acb11257
6 changed files with 34 additions and 118 deletions
+28 -117
View File
@@ -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<number, VTTCue[]>();
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);
}
</script>
@@ -260,4 +167,8 @@
.transcript-text {
margin-left: 1em;
}
.transcript-line .chip {
padding: 0 1.5em;
}
</style>
@@ -606,7 +606,7 @@
</nav>
</summary>
<div class="space"></div>
<div>
<div class="chapter-list">
<ul class="list">
{#each data.content.timestamps as timestamp}
<li
@@ -778,6 +778,11 @@
</dialog>
<style>
.chapter-list {
max-height: 300px;
overflow-x: scroll;
}
.video-actions {
display: flex;
align-items: center;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB