mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-24 12:15:27 -07:00
fix(subtitles): only strip ASS control debris for ASS sources
- Skip control-debris stripping for primary/secondary live text when cues or source aren't ASS/SSA, so SRT lines that merely resemble ASS override tags survive - Track active source's ASS-ness in the secondary controller and gate handleLiveText's cleanup on it
This commit is contained in:
@@ -218,16 +218,35 @@ test('resolvePrimarySubtitleText uses fragment grids only to account for live si
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('resolvePrimarySubtitleText drops malformed ASS control debris from live text', () => {
|
test('resolvePrimarySubtitleText drops malformed ASS control debris from live text', () => {
|
||||||
|
const cues = parseSubtitleCues(
|
||||||
|
[
|
||||||
|
'[Events]',
|
||||||
|
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
|
||||||
|
'Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0,0,0,,Visible line',
|
||||||
|
].join('\n'),
|
||||||
|
'test.ass',
|
||||||
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolvePrimarySubtitleText({
|
resolvePrimarySubtitleText({
|
||||||
liveText: 'Visible line\n\\\n{\\fr0',
|
liveText: 'Visible line\n\\\n{\\fr0',
|
||||||
currentTimeSec: 2,
|
currentTimeSec: 2,
|
||||||
cues: null,
|
cues,
|
||||||
}),
|
}),
|
||||||
'Visible line',
|
'Visible line',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolvePrimarySubtitleText preserves SRT text that resembles ASS control debris', () => {
|
||||||
|
const liveText = 'Visible line\n\\\n{\\fr0';
|
||||||
|
const cues = parseSubtitleCues(
|
||||||
|
['1', '00:00:01,000 --> 00:00:03,000', liveText].join('\n'),
|
||||||
|
'test.srt',
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(resolvePrimarySubtitleText({ liveText, currentTimeSec: 2, cues }), liveText);
|
||||||
|
});
|
||||||
|
|
||||||
test('resolvePrimarySubtitleText keeps a fresh line starting just after the animation ended', () => {
|
test('resolvePrimarySubtitleText keeps a fresh line starting just after the animation ended', () => {
|
||||||
const text = resolvePrimarySubtitleText({
|
const text = resolvePrimarySubtitleText({
|
||||||
liveText: '次のセリフ',
|
liveText: '次のセリフ',
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ export interface ResolvedPrimarySubtitle {
|
|||||||
cues: SubtitleCue[];
|
cues: SubtitleCue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cuesUseAssSyntax(cues: readonly SubtitleCue[] | null | undefined): boolean {
|
||||||
|
return (cues ?? []).some(
|
||||||
|
(cue) =>
|
||||||
|
cue.source === 'canonical-ass' ||
|
||||||
|
cue.source === 'reconstructed-ass' ||
|
||||||
|
cue.assLayout !== undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function animationSpan(cue: SubtitleCue): { start: number; end: number } {
|
function animationSpan(cue: SubtitleCue): { start: number; end: number } {
|
||||||
return {
|
return {
|
||||||
start: cue.animationStartTime ?? cue.startTime,
|
start: cue.animationStartTime ?? cue.startTime,
|
||||||
@@ -235,7 +244,9 @@ export function resolvePrimarySubtitleText(options: {
|
|||||||
currentTimeSec: number;
|
currentTimeSec: number;
|
||||||
cues: readonly SubtitleCue[] | null | undefined;
|
cues: readonly SubtitleCue[] | null | undefined;
|
||||||
}): string {
|
}): string {
|
||||||
const liveText = removeAssControlDebrisLines(options.liveText);
|
const liveText = cuesUseAssSyntax(options.cues)
|
||||||
|
? removeAssControlDebrisLines(options.liveText)
|
||||||
|
: options.liveText;
|
||||||
if (!liveText.trim()) {
|
if (!liveText.trim()) {
|
||||||
return liveText;
|
return liveText;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,23 +406,60 @@ test('secondary track controller falls back to live mpv text without a readable
|
|||||||
assert.deepEqual(broadcasts, ['live fallback']);
|
assert.deepEqual(broadcasts, ['live fallback']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('secondary live fallback drops malformed ASS control debris', () => {
|
test('secondary ASS live fallback drops malformed control debris', async () => {
|
||||||
const broadcasts: string[] = [];
|
const broadcasts: string[] = [];
|
||||||
const controller = createSecondarySubtitleTrackController({
|
const controller = createSecondarySubtitleTrackController({
|
||||||
getMpvClient: () => null,
|
getMpvClient: () => ({
|
||||||
|
connected: true,
|
||||||
|
requestProperty: async (name) => {
|
||||||
|
if (name === 'secondary-sid') return 2;
|
||||||
|
if (name === 'track-list') return [{ type: 'sub', id: 2 }];
|
||||||
|
if (name === 'path') return '/media/video.mkv';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
getCurrentTimePos: () => 2,
|
getCurrentTimePos: () => 2,
|
||||||
resolveSubtitleSource: async () => null,
|
resolveSubtitleSource: async () => ({ path: '/subs/english.ass', sourceKey: 'english' }),
|
||||||
loadSubtitleSourceText: async () => '',
|
loadSubtitleSourceText: async () => '',
|
||||||
parseSubtitleCues: () => [],
|
parseSubtitleCues: () => [],
|
||||||
setCurrentSecondaryText: () => {},
|
setCurrentSecondaryText: () => {},
|
||||||
broadcastSecondaryText: (text) => broadcasts.push(text),
|
broadcastSecondaryText: (text) => broadcasts.push(text),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await controller.refresh();
|
||||||
|
broadcasts.length = 0;
|
||||||
controller.handleLiveText('Visible line\n\\\n{\\fr0');
|
controller.handleLiveText('Visible line\n\\\n{\\fr0');
|
||||||
|
|
||||||
assert.deepEqual(broadcasts, ['Visible line']);
|
assert.deepEqual(broadcasts, ['Visible line']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('secondary SRT live fallback preserves text that resembles ASS control debris', async () => {
|
||||||
|
const broadcasts: string[] = [];
|
||||||
|
const controller = createSecondarySubtitleTrackController({
|
||||||
|
getMpvClient: () => ({
|
||||||
|
connected: true,
|
||||||
|
requestProperty: async (name) => {
|
||||||
|
if (name === 'secondary-sid') return 2;
|
||||||
|
if (name === 'track-list') return [{ type: 'sub', id: 2 }];
|
||||||
|
if (name === 'path') return '/media/video.mkv';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getCurrentTimePos: () => 2,
|
||||||
|
resolveSubtitleSource: async () => ({ path: '/subs/english.srt', sourceKey: 'english' }),
|
||||||
|
loadSubtitleSourceText: async () => '',
|
||||||
|
parseSubtitleCues: () => [],
|
||||||
|
setCurrentSecondaryText: () => {},
|
||||||
|
broadcastSecondaryText: (text) => broadcasts.push(text),
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.refresh();
|
||||||
|
broadcasts.length = 0;
|
||||||
|
controller.handleLiveText('Visible line\n\\\n{\\fr0');
|
||||||
|
|
||||||
|
assert.deepEqual(broadcasts, ['Visible line\n\\\n{\\fr0']);
|
||||||
|
});
|
||||||
|
|
||||||
test('secondary track controller reuses parsed cues for an unchanged embedded track', async () => {
|
test('secondary track controller reuses parsed cues for an unchanged embedded track', async () => {
|
||||||
let resolveCalls = 0;
|
let resolveCalls = 0;
|
||||||
let parseCalls = 0;
|
let parseCalls = 0;
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ type SecondarySubtitleSourceInput = {
|
|||||||
|
|
||||||
const DEFAULT_REFRESH_DELAY_MS = 500;
|
const DEFAULT_REFRESH_DELAY_MS = 500;
|
||||||
|
|
||||||
|
function sourceUsesAssSyntax(source: string): boolean {
|
||||||
|
const sourceWithoutQuery = source.split(/[?#]/u, 1)[0] ?? '';
|
||||||
|
return /\.(?:ass|ssa)$/iu.test(sourceWithoutQuery);
|
||||||
|
}
|
||||||
|
|
||||||
function finiteNumber(value: unknown, fallback = 0): number {
|
function finiteNumber(value: unknown, fallback = 0): number {
|
||||||
const number = typeof value === 'number' ? value : Number(value);
|
const number = typeof value === 'number' ? value : Number(value);
|
||||||
return Number.isFinite(number) ? number : fallback;
|
return Number.isFinite(number) ? number : fallback;
|
||||||
@@ -185,6 +190,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
|||||||
let parsedCues: SubtitleCue[] | null = null;
|
let parsedCues: SubtitleCue[] | null = null;
|
||||||
let parsedSourceKey: string | null = null;
|
let parsedSourceKey: string | null = null;
|
||||||
let parsedTrackIdentity: string | null = null;
|
let parsedTrackIdentity: string | null = null;
|
||||||
|
let activeSourceUsesAssSyntax = false;
|
||||||
let secondaryDelaySeconds = 0;
|
let secondaryDelaySeconds = 0;
|
||||||
let lastLiveText = '';
|
let lastLiveText = '';
|
||||||
let lastBroadcastText: string | null = null;
|
let lastBroadcastText: string | null = null;
|
||||||
@@ -230,6 +236,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
|||||||
|
|
||||||
const videoPath = typeof videoPathRaw === 'string' ? videoPathRaw.trim() : '';
|
const videoPath = typeof videoPathRaw === 'string' ? videoPathRaw.trim() : '';
|
||||||
if (!videoPath || secondarySid === null || secondarySid === 'no') {
|
if (!videoPath || secondarySid === null || secondarySid === 'no') {
|
||||||
|
activeSourceUsesAssSyntax = false;
|
||||||
useLiveFallback();
|
useLiveFallback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -251,11 +258,14 @@ export function createSecondarySubtitleTrackController(deps: {
|
|||||||
});
|
});
|
||||||
if (generation !== refreshGeneration) return;
|
if (generation !== refreshGeneration) return;
|
||||||
if (!resolvedSource) {
|
if (!resolvedSource) {
|
||||||
|
activeSourceUsesAssSyntax = false;
|
||||||
deps.logDebug?.('[secondary-subtitle-track] selected source is not readable');
|
deps.logDebug?.('[secondary-subtitle-track] selected source is not readable');
|
||||||
useLiveFallback();
|
useLiveFallback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeSourceUsesAssSyntax = sourceUsesAssSyntax(resolvedSource.path);
|
||||||
|
|
||||||
if (resolvedSource.sourceKey === parsedSourceKey && parsedCues) {
|
if (resolvedSource.sourceKey === parsedSourceKey && parsedCues) {
|
||||||
parsedTrackIdentity = selectedTrackIdentity;
|
parsedTrackIdentity = selectedTrackIdentity;
|
||||||
publish(resolveAtTime(deps.getCurrentTimePos()));
|
publish(resolveAtTime(deps.getCurrentTimePos()));
|
||||||
@@ -299,6 +309,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
|||||||
parsedCues = null;
|
parsedCues = null;
|
||||||
parsedSourceKey = null;
|
parsedSourceKey = null;
|
||||||
parsedTrackIdentity = null;
|
parsedTrackIdentity = null;
|
||||||
|
activeSourceUsesAssSyntax = false;
|
||||||
secondaryDelaySeconds = 0;
|
secondaryDelaySeconds = 0;
|
||||||
lastLiveText = '';
|
lastLiveText = '';
|
||||||
publish('');
|
publish('');
|
||||||
@@ -308,7 +319,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
|||||||
refresh,
|
refresh,
|
||||||
scheduleRefresh,
|
scheduleRefresh,
|
||||||
handleLiveText(text: string): void {
|
handleLiveText(text: string): void {
|
||||||
lastLiveText = removeAssControlDebrisLines(text);
|
lastLiveText = activeSourceUsesAssSyntax ? removeAssControlDebrisLines(text) : text;
|
||||||
publish(resolveAtTime(deps.getCurrentTimePos()));
|
publish(resolveAtTime(deps.getCurrentTimePos()));
|
||||||
},
|
},
|
||||||
handleTimePos(timeSeconds: number): void {
|
handleTimePos(timeSeconds: number): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user