fix(subtitles): preserve opaque text beside ASS texture fragments

- Require structural texture evidence before suppressing same-font fragments
This commit is contained in:
2026-08-24 18:16:42 -07:00
parent b7358507b1
commit 0b963ef729
2 changed files with 27 additions and 4 deletions
@@ -1265,6 +1265,21 @@ test('parseSubtitleCues drops clipped repeated-glyph texture text', () => {
); );
}); });
test('parseSubtitleCues preserves opaque same-font text beside texture fragments', () => {
const content = [
'[Events]',
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
'Dialogue: 2,0:00:01.00,0:00:04.00,MarySigns,seed,0,0,0,,{\\pos(960,80)\\fnSerangkaian Pattern Regular\\clip(800,20,1120,140)}LLLLLLLLLLLLLLLLLLLLLLLL',
'Dialogue: 2,0:00:01.00,0:00:04.00,MarySigns,piece,0,0,0,,{\\pos(960,110)\\fnSerangkaian Pattern Regular\\clip(800,20,1120,140)}LLLL',
'Dialogue: 3,0:00:01.00,0:00:04.00,MarySigns,label,0,0,0,,{\\pos(960,150)\\fnSerangkaian Pattern Regular}Keep this label',
].join('\n');
assert.deepEqual(
parseSubtitleCues(content, 'test.ass').map((cue) => cue.text),
['Keep this label'],
);
});
test('parseSubtitleCues drops clipped repeated-glyph texture text without a font override', () => { test('parseSubtitleCues drops clipped repeated-glyph texture text without a font override', () => {
const content = [ const content = [
'[Events]', '[Events]',
+12 -4
View File
@@ -1038,6 +1038,14 @@ function isNearlyTransparentPositionedText(cue: AnnotatedSubtitleCue): boolean {
); );
} }
function hasAssTextureCandidateEvidence(cue: AnnotatedSubtitleCue): boolean {
return (
isClippedRepeatedGlyphFragment(cue) ||
isNearlyTransparentPositionedText(cue) ||
hasStaticOverride(cue, '2a')
);
}
function assFontTextureGroupKey(cue: AnnotatedSubtitleCue): string | null { function assFontTextureGroupKey(cue: AnnotatedSubtitleCue): string | null {
const font = staticFontOverride(cue); const font = staticFontOverride(cue);
return font === null ? null : `${cue.style}\0${cue.startTime}\0${cue.endTime}\0${font}`; return font === null ? null : `${cue.style}\0${cue.startTime}\0${cue.endTime}\0${font}`;
@@ -1050,9 +1058,9 @@ function assTextureTimingGroupKey(cue: AnnotatedSubtitleCue): string {
function removeAssFontTextureEvents(events: ParsedAssEvents): ParsedAssEvents { function removeAssFontTextureEvents(events: ParsedAssEvents): ParsedAssEvents {
const seeds = events.dialogue.filter(isAssTextureSeed); const seeds = events.dialogue.filter(isAssTextureSeed);
const seedSet = new Set(seeds); const seedSet = new Set(seeds);
// Short pieces can share the seeded font effect under another actor without carrying // Short pieces can share the seeded font effect under another actor. Matching the
// enough tags to identify themselves. The exact style, time, and font group catches // seed's style, timing, and font only narrows the candidates; each piece must still
// those pieces without inspecting their content. // carry structural texture evidence.
const textureGroups = new Set( const textureGroups = new Set(
seeds.map(assFontTextureGroupKey).filter((key): key is string => key !== null), seeds.map(assFontTextureGroupKey).filter((key): key is string => key !== null),
); );
@@ -1090,7 +1098,7 @@ function removeAssFontTextureEvents(events: ParsedAssEvents): ParsedAssEvents {
return false; return false;
} }
const key = assFontTextureGroupKey(cue); const key = assFontTextureGroupKey(cue);
if (key !== null && textureGroups.has(key)) { if (key !== null && textureGroups.has(key) && hasAssTextureCandidateEvidence(cue)) {
return false; return false;
} }
if (!isNearlyTransparentPositionedText(cue)) { if (!isNearlyTransparentPositionedText(cue)) {