feat(subtitles): add regex subtitle filters with built-in presets (#318)

- Add `subtitleFilter` config with four presets: parentheses, brackets, speaker prefix and music symbols. Custom regex rules run after the presets.
- Filter primary text where it enters the app (live `sub-text` and parsed cues), so the overlay, sidebar, mining and stats all get the same filtered text.
- Changes hot-reload: the current line is read again and the active track is parsed again, using the filter config as the prefetch parse key.
- Add a custom rule editor in Settings under Behavior → Subtitle Filters that checks patterns as you type.
- Update the config example, the configuration docs and the architecture domains doc, and add a changelog fragment.
This commit is contained in:
sudacode authored and GitHub committed 2026-11-03 00:33:58 -08:00
1 parent 848a6c4021
commit fa3051f9cb
28 files changed
+740 -11

No files matched your search

+11 -2
View File
@@ -9,6 +9,7 @@ import {
tokenizeSubtitle as tokenizeSubtitleCore,
} from '../../core/services';
import { parseSubtitleCues } from '../../core/services/subtitle-cue-parser';
import { filterSubtitleCues } from '../../shared/subtitle-text-filter';
import { resolveAniSkipMetadataForFile } from '../runtime/aniskip-metadata';
import { createAniSkipRuntime } from '../runtime/aniskip-runtime';
import { resolveCurrentSubtitleForRenderer } from '../runtime/current-subtitle-snapshot';
@@ -452,13 +453,21 @@ export function createPlaybackAssembly(deps: PlaybackAssemblyDeps) {
},
});
// Primary cues get the same subtitleFilter pass as live mpv text, so the two still match.
const parsePrimarySubtitleCues = (content: string, filename: string) =>
filterSubtitleCues(
parseSubtitleCues(content, filename),
configService.getConfig().subtitleFilter,
);
const subtitlePrefetchInitController = createSubtitlePrefetchInitController({
getCurrentService: () => subtitlePrefetchService,
setCurrentService: (service) => {
subtitlePrefetchService = service;
},
loadSubtitleSourceText,
parseSubtitleCues: (content, filename) => parseSubtitleCues(content, filename),
parseSubtitleCues: parsePrimarySubtitleCues,
getParseKey: () => JSON.stringify(configService.getConfig().subtitleFilter),
createSubtitlePrefetchService: (serviceDeps) => createSubtitlePrefetchService(serviceDeps),
tokenizeSubtitle: async (text) =>
tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : null,
@@ -1126,7 +1135,7 @@ export function createPlaybackAssembly(deps: PlaybackAssemblyDeps) {
}
const content = await loadSubtitleSourceText(resolvedSource.path);
const cues = parseSubtitleCues(content, resolvedSource.path);
const cues = parsePrimarySubtitleCues(content, resolvedSource.path);
appState.activeParsedSubtitleCues = cues;
appState.activeParsedSubtitleSource = resolvedSource.sourceKey;
appState.activeParsedSubtitleMediaPath = videoPath || null;