fix(subtitles): preserve authored spaces in fragmented ASS karaoke

- Only trim indentation before slicing ASS event fields so a trailing authored space at a fragment's end is kept, preventing words from being joined together when fragmented karaoke lines are reconstructed
- Add regression test covering event-boundary word spacing
This commit is contained in:
2026-08-22 23:32:35 -07:00
parent 3aea42e6f8
commit 03ea903927
3 changed files with 43 additions and 4 deletions
+7 -3
View File
@@ -853,6 +853,10 @@ function parseAnnotatedAssEvents(content: string): ParsedAssEvents {
for (const line of lines) {
const trimmed = line.trim();
// Event text can end in an authored space. Fragmented karaoke commonly uses that
// space to retain word boundaries when its separately positioned events are joined
// back into a line, so only remove indentation before slicing the event fields.
const eventLine = line.trimStart();
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
inEventsSection = trimmed.toLowerCase() === '[events]';
@@ -883,9 +887,9 @@ function parseAnnotatedAssEvents(content: string): ParsedAssEvents {
continue;
}
const eventPrefix = trimmed.startsWith(ASS_DIALOGUE_PREFIX)
const eventPrefix = eventLine.startsWith(ASS_DIALOGUE_PREFIX)
? ASS_DIALOGUE_PREFIX
: trimmed.startsWith(ASS_COMMENT_PREFIX)
: eventLine.startsWith(ASS_COMMENT_PREFIX)
? ASS_COMMENT_PREFIX
: null;
if (!eventPrefix) {
@@ -896,7 +900,7 @@ function parseAnnotatedAssEvents(content: string): ParsedAssEvents {
continue;
}
const fields = trimmed.slice(eventPrefix.length).split(',');
const fields = eventLine.slice(eventPrefix.length).split(',');
if (
fieldIndex.start >= fields.length ||
fieldIndex.end >= fields.length ||
@@ -181,6 +181,41 @@ test('ASS fragment karaoke stays separated by style with authored word spacing',
assert.equal(findActiveSubtitleText(parseSubtitleCues(ass, 'ending.ass'), 8.5), 'Iwanttogo');
});
test('ASS fragment karaoke preserves word spaces authored at event boundaries', () => {
const fragments = [
'The ',
'shoot',
'ing ',
'stars ',
'arc',
'ing ',
'across ',
'the ',
'sky ',
'I ',
'wish ',
'upon,',
];
const events: string[] = [];
for (const layer of [0, 1]) {
fragments.forEach((fragment, index) => {
events.push(
`Dialogue: ${layer},0:00:01.00,0:00:04.00,op_english,,0,0,0,,{\\pos(${100 + index * 40},110)\\t(0,200,\\fscx110)}${fragment}`,
);
});
}
const ass = [
'[Events]',
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
...events,
].join('\n');
assert.equal(
findActiveSubtitleText(parseSubtitleCues(ass, 'bravern-s01e10.ass'), 2),
'The shooting stars arcing across the sky I wish upon,',
);
});
test('findActiveSubtitleText keeps a complete reconstructed line over entrance fragments', () => {
const current = {
startTime: 1,