mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-26 00:15:27 -07:00
fix(subtitles): suppress overlay duplicates and reset ASS cleanup
- Suppress overlapping decorative karaoke and shadow layer copies - Clear stale ASS-only fallback sanitization after source refresh failures
This commit is contained in:
@@ -1558,17 +1558,23 @@ test('parseSubtitleCues suppresses a karaoke highlight sweep without publishing
|
||||
`Dialogue: ${layer},0:00:01.00,0:00:04.00,ED Romaji,,0,0,0,fx,{\\pos(${x},60)\\t(${index * 2},${index * 2 + 100},\\fscx120)}${fragment}`,
|
||||
),
|
||||
),
|
||||
...[40, 41].flatMap((layer) =>
|
||||
sweepFragments.map(
|
||||
([fragment, x, start, end]) =>
|
||||
`Dialogue: ${layer},${start},${end},ED Romaji2,,0,0,0,fx,{\\an5\\pos(${x},60)\\t(150,290,\\1a&HFF&)}${fragment}`,
|
||||
...sweepFragments.flatMap(([fragment, x, start, end]) =>
|
||||
[
|
||||
[40, x, 60],
|
||||
[41, x + 4, 64],
|
||||
].map(
|
||||
([layer, copyX, copyY]) =>
|
||||
`Dialogue: ${layer},${start},${end},ED Romaji2,,0,0,0,fx,{\\an5\\pos(${copyX},${copyY})\\t(150,290,\\1a&HFF&)}${fragment}`,
|
||||
),
|
||||
),
|
||||
'Dialogue: 42,0:00:01.20,0:00:01.30,ED Romaji2,,0,0,0,fx,{\\fnWebdings\\pos(900,50)\\t(0,100,\\fscx120)}a',
|
||||
'Dialogue: 42,0:00:04.00,0:00:04.20,ED Romaji2,,0,0,0,fx,{\\fnWebdings\\pos(900,50)\\t(0,100,\\fscx120)}z',
|
||||
].join('\n');
|
||||
|
||||
const cues = parseSubtitleCues(content, 'test.ass');
|
||||
assert.equal(cues.length, 1);
|
||||
assert.equal(cues.length, 2);
|
||||
assert.equal(cues[0]?.text.replace(/\s+/gu, ''), 'tosouomo');
|
||||
assert.equal(cues[1]?.text, 'z');
|
||||
});
|
||||
|
||||
test('parseSubtitleCues collapses drop-shadow layer copies offset by a few pixels', () => {
|
||||
|
||||
@@ -809,15 +809,21 @@ interface FragmentInterval {
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
/** Event time ranges with layer copies (same text, placement, and timing) collapsed. */
|
||||
/** Event time ranges with repeated same-text, same-time layer copies collapsed. */
|
||||
function distinctFragmentIntervals(events: readonly AnnotatedSubtitleCue[]): FragmentInterval[] {
|
||||
const intervals: FragmentInterval[] = [];
|
||||
const seen = new Set<string>();
|
||||
const previousEvents: AnnotatedSubtitleCue[] = [];
|
||||
for (const event of events) {
|
||||
const anchors = [...fragmentPlacementAnchors(event)].sort().join('|');
|
||||
const key = `${compactCueMatchText(event)}\0${anchors}\0${event.startTime}\0${event.endTime}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const compactText = compactCueMatchText(event);
|
||||
const isLayerCopy = previousEvents.some(
|
||||
(previous) =>
|
||||
compactCueMatchText(previous) === compactText &&
|
||||
previous.startTime === event.startTime &&
|
||||
previous.endTime === event.endTime &&
|
||||
isRepeatedFragmentCopy(previous, event),
|
||||
);
|
||||
previousEvents.push(event);
|
||||
if (isLayerCopy) continue;
|
||||
intervals.push({ startTime: event.startTime, endTime: event.endTime });
|
||||
}
|
||||
return intervals.sort((a, b) => a.startTime - b.startTime || a.endTime - b.endTime);
|
||||
@@ -1018,6 +1024,13 @@ function recoverFragmentOnlyAssLines(dialogue: AnnotatedSubtitleCue[]): Annotate
|
||||
const lineEvents = decorative.size ? events.filter((event) => !decorative.has(event)) : events;
|
||||
if (isProgressiveHighlightSweepGroup(lineEvents)) {
|
||||
lineEvents.forEach((event) => suppressed.add(event));
|
||||
const spanStart = Math.min(...lineEvents.map((event) => event.startTime));
|
||||
const spanEnd = Math.max(...lineEvents.map((event) => event.endTime));
|
||||
for (const overlay of decorative) {
|
||||
if (overlay.startTime < spanEnd && overlay.endTime > spanStart) {
|
||||
suppressed.add(overlay);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const cluster of clusterAssFragmentEvents(lineEvents)) {
|
||||
|
||||
Reference in New Issue
Block a user