fix(subtitles): preserve lyric selection and deduplicate secondary text

- Seek sidebar selections past overlapping karaoke exit spans
- Collapse duplicate long ASS lines and omit reconstructed sign grids
This commit is contained in:
2026-08-21 01:28:00 -07:00
parent 9445aef004
commit 88bb3edfa4
16 changed files with 190 additions and 11 deletions
+6 -2
View File
@@ -240,14 +240,15 @@ test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback
const snapshot: SubtitleSidebarSnapshot = {
cues: [
{ startTime: 1, endTime: 2, text: 'first' },
{ startTime: 1, endTime: 3.4, text: 'first' },
{ startTime: 3, endTime: 4, text: 'second' },
],
currentSubtitle: {
text: 'second',
startTime: 3,
startTime: 3.5,
endTime: 4,
},
currentTimeSec: 3.5,
config: {
enabled: true,
autoOpen: false,
@@ -361,6 +362,9 @@ test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback
modal.seekToCue(snapshot.cues[0]!);
assert.deepEqual(mpvCommands.at(-1), ['seek', 1.08, 'absolute+exact']);
modal.seekToCue(snapshot.cues[1]!);
assert.deepEqual(mpvCommands.at(-1), ['seek', 3.48, 'absolute+exact']);
modal.closeSubtitleSidebarModal();
assert.deepEqual(visibilityChanges, [true, false]);
assert.deepEqual(modalNotifications, ['open:subtitle-sidebar', 'close:subtitle-sidebar']);
+6 -2
View File
@@ -4,7 +4,7 @@ import type {
SubtitleMiningContext,
SubtitleSidebarSnapshot,
} from '../../types';
import { subtitleCueSeekTime } from '../../core/services/subtitle-cue-navigation.js';
import { subtitleCueListSeekTime } from '../../core/services/subtitle-cue-navigation.js';
import type { ModalStateReader, RendererContext } from '../context';
import { syncOverlayMouseIgnoreState } from '../overlay-mouse-ignore.js';
import {
@@ -392,7 +392,11 @@ export function createSubtitleSidebarModal(
}
function seekToCue(cue: SubtitleCue): void {
window.electronAPI.sendMpvCommand(['seek', subtitleCueSeekTime(cue), 'absolute+exact']);
window.electronAPI.sendMpvCommand([
'seek',
subtitleCueListSeekTime(ctx.state.subtitleSidebarCues, cue),
'absolute+exact',
]);
}
function getCueRowLabel(cue: SubtitleCue): string {
+9
View File
@@ -1454,6 +1454,15 @@ test('prepareSecondarySubtitleLines preserves repeated short dialogue without la
assert.deepEqual(prepareSecondarySubtitleLines(dialogue.join('\\N')), dialogue);
});
test('prepareSecondarySubtitleLines collapses punctuation variants of a full-sentence fallback', () => {
const dialogue = 'A question veiled as an insult!';
const positionedSign = 'A question veiled as an insult';
assert.deepEqual(prepareSecondarySubtitleLines([dialogue, positionedSign].join('\\N')), [
dialogue,
]);
});
test('prepareSecondarySubtitleLines preserves short simultaneous dialogue without repeats', () => {
const dialogue = ['Wait', 'Go!', 'No!', 'Run!'];
+13 -1
View File
@@ -6,6 +6,7 @@ import type {
SubtitleRendererStyleConfig,
} from '../types';
import { assToPlainText, normalizePlainSubtitleText } from '../core/services/ass-text.js';
import { flattenedSecondarySubtitleLineIdentity } from '../core/services/secondary-subtitle-line-identity.js';
import type { RendererContext } from './context';
import { PRIMARY_SUB_VISIBLE_ON_YOMITAN_POPUP_CLASS } from './yomitan-popup.js';
@@ -665,6 +666,17 @@ function isKaraokeLikeLineSet(lines: string[]): boolean {
return median <= KARAOKE_MAX_MEDIAN_LINE_LENGTH;
}
function collapseFullLineFallbackCopies(lines: string[]): string[] {
const seen = new Set<string>();
return lines.filter((line) => {
const identity = flattenedSecondarySubtitleLineIdentity(line);
if (!identity) return true;
if (seen.has(identity)) return false;
seen.add(identity);
return true;
});
}
export function prepareSecondarySubtitleLines(text: string): string[] {
// The one display-side ASS decode: secondary text also reaches the overlay from
// websocket clients that forward their source line untouched, so unlike the primary
@@ -678,7 +690,7 @@ export function prepareSecondarySubtitleLines(text: string): string[] {
.map((line) => line.trim())
.filter((line) => line.length > 0);
if (!isKaraokeLikeLineSet(lines)) {
return lines;
return collapseFullLineFallbackCopies(lines);
}
const seen = new Set<string>();