mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
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:
1 parent
848a6c4021
commit
fa3051f9cb
28 files changed
+740
-11
No files matched your search
@@ -20,6 +20,8 @@ type ConfigHotReloadAppliedDeps = {
|
||||
invalidateTokenizationCache?: () => void;
|
||||
refreshSubtitlePrefetch?: () => void;
|
||||
refreshCurrentSubtitle?: () => void;
|
||||
/** Re-reads the live line and re-parses the active cue list through the new filters. */
|
||||
refreshSubtitleFilter?: () => void;
|
||||
setLogLevel?: (level: ResolvedConfig['logging']['level']) => void;
|
||||
setLogRotation?: (rotation: ResolvedConfig['logging']['rotation']) => void;
|
||||
setLogFileToggles?: (files: ResolvedConfig['logging']['files']) => void;
|
||||
@@ -180,6 +182,10 @@ export function createConfigHotReloadAppliedHandler(deps: ConfigHotReloadApplied
|
||||
deps.applyAnkiRuntimeConfigPatch(ankiPatch);
|
||||
}
|
||||
|
||||
if (diff.hotReloadFields.includes('subtitleFilter')) {
|
||||
deps.refreshSubtitleFilter?.();
|
||||
}
|
||||
|
||||
if (hasAnnotationRuntimeHotReload(diff)) {
|
||||
deps.invalidateTokenizationCache?.();
|
||||
deps.refreshSubtitlePrefetch?.();
|
||||
|
||||
@@ -252,7 +252,7 @@ test('subtitle prefetch init logs a warning when the source parses to zero cues'
|
||||
assert.match(warnings[0]!, /\[subtitle-prefetch\].*0 cues.*\/tmp\/broken\.ass/);
|
||||
});
|
||||
|
||||
function createTrackingController(loadContent: () => string) {
|
||||
function createTrackingController(loadContent: () => string, getParseKey?: () => string) {
|
||||
let currentService: SubtitlePrefetchService | null = null;
|
||||
const events: string[] = [];
|
||||
const cueUpdates: Array<string | null> = [];
|
||||
@@ -265,6 +265,7 @@ function createTrackingController(loadContent: () => string) {
|
||||
},
|
||||
loadSubtitleSourceText: async () => loadContent(),
|
||||
parseSubtitleCues: (content): SubtitleCue[] => [{ startTime: 0, endTime: 1, text: content }],
|
||||
getParseKey,
|
||||
createSubtitlePrefetchService: () => {
|
||||
const id = ++serviceCount;
|
||||
return {
|
||||
@@ -308,6 +309,20 @@ test('re-initializing a source whose content changed restarts the service', asyn
|
||||
assert.deepEqual(events, ['start:1', 'stop:1', 'start:2']);
|
||||
});
|
||||
|
||||
test('re-initializing an unchanged source re-parses when the parse key changed', async () => {
|
||||
let parseKey = 'filters-off';
|
||||
const { controller, events } = createTrackingController(
|
||||
() => 'content',
|
||||
() => parseKey,
|
||||
);
|
||||
|
||||
await controller.initSubtitlePrefetch('track-1.srt', 0);
|
||||
parseKey = 'filters-on';
|
||||
await controller.initSubtitlePrefetch('track-1.srt', 0);
|
||||
|
||||
assert.deepEqual(events, ['start:1', 'stop:1', 'start:2']);
|
||||
});
|
||||
|
||||
test('subtitle prefetch init warms overlap lines without publishing them as cues', async () => {
|
||||
let prefetchedTexts: string[] = [];
|
||||
let publishedTexts: string[] = [];
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface SubtitlePrefetchInitControllerDeps {
|
||||
setCurrentService: (service: SubtitlePrefetchService | null) => void;
|
||||
loadSubtitleSourceText: (source: string) => Promise<string>;
|
||||
parseSubtitleCues: (content: string, filename: string) => SubtitleCue[];
|
||||
/** Changes whenever parseSubtitleCues would produce different cues for the same content. */
|
||||
getParseKey?: () => string;
|
||||
createSubtitlePrefetchService: (deps: SubtitlePrefetchServiceDeps) => SubtitlePrefetchService;
|
||||
tokenizeSubtitle: (text: string) => Promise<SubtitleData | null>;
|
||||
preCacheTokenization: (text: string, data: SubtitleData) => void;
|
||||
@@ -37,7 +39,12 @@ export function createSubtitlePrefetchInitController(
|
||||
// What the running service was built from. Startup fires several inits for one source
|
||||
// (Jellyfin preload seed, sid change, each track-list update); restarting on each would
|
||||
// discard the in-flight tokenization and re-queue the priority window behind it.
|
||||
let activeSource: { key: string; content: string; cues: SubtitleCue[] } | null = null;
|
||||
let activeSource: {
|
||||
key: string;
|
||||
content: string;
|
||||
parseKey: string;
|
||||
cues: SubtitleCue[];
|
||||
} | null = null;
|
||||
|
||||
const stopCurrentService = (): void => {
|
||||
deps.getCurrentService()?.stop();
|
||||
@@ -67,9 +74,11 @@ export function createSubtitlePrefetchInitController(
|
||||
if (revision !== initRevision) {
|
||||
return;
|
||||
}
|
||||
const parseKey = deps.getParseKey?.() ?? '';
|
||||
if (
|
||||
activeSource?.key === sourceKey &&
|
||||
activeSource.content === content &&
|
||||
activeSource.parseKey === parseKey &&
|
||||
deps.getCurrentService()
|
||||
) {
|
||||
// A track change clears the published cues synchronously, so republish them.
|
||||
@@ -106,7 +115,7 @@ export function createSubtitlePrefetchInitController(
|
||||
}
|
||||
|
||||
deps.setCurrentService(nextService);
|
||||
activeSource = { key: sourceKey, content, cues };
|
||||
activeSource = { key: sourceKey, content, parseKey, cues };
|
||||
deps.onParsedSubtitleCuesChanged?.(cues, sourceKey);
|
||||
nextService.start(currentTimePos);
|
||||
deps.logInfo(
|
||||
|
||||
Reference in new issue
Block a user