mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-30 00:15:27 -07:00
fix(subtitles): recover positioned ASS word spacing and drop control debris (#217)
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import type { SubtitleCue } from '../../types/subtitle';
|
||||
import { flattenedSecondarySubtitleLineIdentity } from '../../core/services/secondary-subtitle-line-identity';
|
||||
import {
|
||||
removeAssControlDebrisLines,
|
||||
removeLiveGlyphFragmentLines,
|
||||
} from '../../core/services/ass-text';
|
||||
|
||||
type SecondarySubtitleMpvClient = {
|
||||
connected?: boolean;
|
||||
@@ -23,6 +27,11 @@ type SecondarySubtitleSourceInput = {
|
||||
|
||||
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 {
|
||||
const number = typeof value === 'number' ? value : Number(value);
|
||||
return Number.isFinite(number) ? number : fallback;
|
||||
@@ -82,7 +91,26 @@ export function findActiveSubtitleText(cues: readonly SubtitleCue[], timeSeconds
|
||||
(cue) =>
|
||||
cue.source === 'canonical-ass' && cue.startTime <= timeSeconds && cue.endTime > timeSeconds,
|
||||
);
|
||||
const selectedCanonical = new Set<SubtitleCue>(authoredCanonical);
|
||||
const enteringCanonical = cues.filter(
|
||||
(cue) =>
|
||||
cue.source === 'canonical-ass' &&
|
||||
(cue.animationStartTime ?? cue.startTime) <= timeSeconds &&
|
||||
cue.startTime > timeSeconds &&
|
||||
(cue.animationEndTime ?? cue.endTime) > timeSeconds,
|
||||
);
|
||||
const nextAuthoredStart = enteringCanonical.reduce(
|
||||
(earliest, cue) => Math.min(earliest, cue.startTime),
|
||||
Infinity,
|
||||
);
|
||||
// Generated lyrics can begin drawing before their canonical Comment timing. Once that
|
||||
// entrance starts, replace a preceding lyric that ends before the new authored span;
|
||||
// genuinely concurrent subtitles that continue through the new span stay selected.
|
||||
const selectedCanonical = new Set<SubtitleCue>([
|
||||
...authoredCanonical.filter(
|
||||
(cue) => enteringCanonical.length === 0 || cue.endTime > nextAuthoredStart,
|
||||
),
|
||||
...enteringCanonical,
|
||||
]);
|
||||
if (selectedCanonical.size === 0) {
|
||||
const animatedCanonical = cues.filter(
|
||||
(cue) =>
|
||||
@@ -153,15 +181,17 @@ export function findActiveSubtitleText(cues: readonly SubtitleCue[], timeSeconds
|
||||
activeCues.sort(compareAuthoredSubtitleOrder);
|
||||
|
||||
for (const { cue } of activeCues) {
|
||||
const text = cue.text.trim();
|
||||
const compactText = text.replace(/\s+/gu, '');
|
||||
if (!compactText || seenExact.has(compactText)) continue;
|
||||
seenExact.add(compactText);
|
||||
for (const line of cue.text.split('\n')) {
|
||||
const text = line.trim();
|
||||
const compactText = text.normalize('NFKC').replace(/\s+/gu, '');
|
||||
if (!compactText || seenExact.has(compactText)) continue;
|
||||
seenExact.add(compactText);
|
||||
|
||||
const flattenedIdentity = flattenedSecondarySubtitleLineIdentity(text);
|
||||
if (flattenedIdentity && seenFlattened.has(flattenedIdentity)) continue;
|
||||
if (flattenedIdentity) seenFlattened.add(flattenedIdentity);
|
||||
activeText.push(text);
|
||||
const flattenedIdentity = flattenedSecondarySubtitleLineIdentity(text);
|
||||
if (flattenedIdentity && seenFlattened.has(flattenedIdentity)) continue;
|
||||
if (flattenedIdentity) seenFlattened.add(flattenedIdentity);
|
||||
activeText.push(text);
|
||||
}
|
||||
}
|
||||
return activeText.join('\n');
|
||||
}
|
||||
@@ -182,6 +212,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
||||
let parsedCues: SubtitleCue[] | null = null;
|
||||
let parsedSourceKey: string | null = null;
|
||||
let parsedTrackIdentity: string | null = null;
|
||||
let activeSourceUsesAssSyntax = false;
|
||||
let secondaryDelaySeconds = 0;
|
||||
let lastLiveText = '';
|
||||
let lastBroadcastText: string | null = null;
|
||||
@@ -211,6 +242,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
||||
const generation = ++refreshGeneration;
|
||||
const client = deps.getMpvClient();
|
||||
if (!client?.connected) {
|
||||
activeSourceUsesAssSyntax = false;
|
||||
useLiveFallback();
|
||||
return;
|
||||
}
|
||||
@@ -227,6 +259,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
||||
|
||||
const videoPath = typeof videoPathRaw === 'string' ? videoPathRaw.trim() : '';
|
||||
if (!videoPath || secondarySid === null || secondarySid === 'no') {
|
||||
activeSourceUsesAssSyntax = false;
|
||||
useLiveFallback();
|
||||
return;
|
||||
}
|
||||
@@ -248,11 +281,14 @@ export function createSecondarySubtitleTrackController(deps: {
|
||||
});
|
||||
if (generation !== refreshGeneration) return;
|
||||
if (!resolvedSource) {
|
||||
activeSourceUsesAssSyntax = false;
|
||||
deps.logDebug?.('[secondary-subtitle-track] selected source is not readable');
|
||||
useLiveFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
activeSourceUsesAssSyntax = sourceUsesAssSyntax(resolvedSource.path);
|
||||
|
||||
if (resolvedSource.sourceKey === parsedSourceKey && parsedCues) {
|
||||
parsedTrackIdentity = selectedTrackIdentity;
|
||||
publish(resolveAtTime(deps.getCurrentTimePos()));
|
||||
@@ -274,6 +310,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
||||
publish(resolveAtTime(deps.getCurrentTimePos()));
|
||||
} catch (error) {
|
||||
if (generation !== refreshGeneration) return;
|
||||
activeSourceUsesAssSyntax = false;
|
||||
deps.logWarn?.('[secondary-subtitle-track] failed to parse selected source', error);
|
||||
useLiveFallback();
|
||||
} finally {
|
||||
@@ -296,6 +333,7 @@ export function createSecondarySubtitleTrackController(deps: {
|
||||
parsedCues = null;
|
||||
parsedSourceKey = null;
|
||||
parsedTrackIdentity = null;
|
||||
activeSourceUsesAssSyntax = false;
|
||||
secondaryDelaySeconds = 0;
|
||||
lastLiveText = '';
|
||||
publish('');
|
||||
@@ -305,7 +343,9 @@ export function createSecondarySubtitleTrackController(deps: {
|
||||
refresh,
|
||||
scheduleRefresh,
|
||||
handleLiveText(text: string): void {
|
||||
lastLiveText = text;
|
||||
lastLiveText = removeLiveGlyphFragmentLines(
|
||||
activeSourceUsesAssSyntax ? removeAssControlDebrisLines(text) : text,
|
||||
);
|
||||
publish(resolveAtTime(deps.getCurrentTimePos()));
|
||||
},
|
||||
handleTimePos(timeSeconds: number): void {
|
||||
|
||||
Reference in New Issue
Block a user