mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 06:22:44 -08:00
feat: make startup warmups configurable with low-power mode
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
id: TASK-74
|
||||||
|
title: 'Startup warmups: configurable warmup vs defer with low-power mode'
|
||||||
|
status: In Progress
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-27 21:05'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- src/types.ts
|
||||||
|
- src/config/definitions/defaults-core.ts
|
||||||
|
- src/config/definitions/options-core.ts
|
||||||
|
- src/config/definitions/template-sections.ts
|
||||||
|
- src/config/resolve/core-domains.ts
|
||||||
|
- src/main/runtime/startup-warmups.ts
|
||||||
|
- src/main/runtime/startup-warmups-main-deps.ts
|
||||||
|
- src/main/runtime/composers/mpv-runtime-composer.ts
|
||||||
|
- src/main.ts
|
||||||
|
- src/config/config.test.ts
|
||||||
|
- src/main/runtime/startup-warmups.test.ts
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Add startup warmup controls to allow per-integration warmup or deferred first-use loading.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- New config section `startupWarmups` with toggles for `mecab`, `yomitanExtension`, `subtitleDictionaries`, and `jellyfinRemoteSession`.
|
||||||
|
- New `startupWarmups.lowPowerMode` policy: defer everything except Yomitan extension.
|
||||||
|
- Keep default behavior as full warmup.
|
||||||
|
- Ensure deferred integrations lazy-load on first real usage path.
|
||||||
|
- Add test coverage for config parsing/defaults and warmup scheduling behavior.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Implemented:
|
||||||
|
- Added `startupWarmups` to config types/defaults/options/template/resolve.
|
||||||
|
- Warmup scheduler now uses per-integration gating functions.
|
||||||
|
- Low-power mode now defers MeCab, subtitle dictionaries, and Jellyfin remote session warmups while still warming Yomitan extension.
|
||||||
|
- Tokenization path guarantees lazy first-use init for deferred dependencies (Yomitan extension, MeCab when missing, subtitle dictionaries).
|
||||||
|
- Added/updated tests across config and runtime warmup modules.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `bun run test:config:src`
|
||||||
|
- `bun run test:core:src`
|
||||||
|
- `tsc --noEmit`
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -23,6 +23,11 @@ test('loads defaults when config is missing', () => {
|
|||||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||||
assert.equal(config.jellyfin.autoAnnounce, false);
|
assert.equal(config.jellyfin.autoAnnounce, false);
|
||||||
assert.equal(config.jellyfin.remoteControlDeviceName, 'SubMiner');
|
assert.equal(config.jellyfin.remoteControlDeviceName, 'SubMiner');
|
||||||
|
assert.equal(config.startupWarmups.lowPowerMode, false);
|
||||||
|
assert.equal(config.startupWarmups.mecab, true);
|
||||||
|
assert.equal(config.startupWarmups.yomitanExtension, true);
|
||||||
|
assert.equal(config.startupWarmups.subtitleDictionaries, true);
|
||||||
|
assert.equal(config.startupWarmups.jellyfinRemoteSession, true);
|
||||||
assert.equal(config.discordPresence.enabled, false);
|
assert.equal(config.discordPresence.enabled, false);
|
||||||
assert.equal(config.discordPresence.updateIntervalMs, 3_000);
|
assert.equal(config.discordPresence.updateIntervalMs, 3_000);
|
||||||
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
||||||
@@ -294,6 +299,72 @@ test('parses jellyfin.enabled and remoteControlEnabled disabled combinations', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses startup warmup toggles and low-power mode', () => {
|
||||||
|
const dir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"startupWarmups": {
|
||||||
|
"lowPowerMode": true,
|
||||||
|
"mecab": false,
|
||||||
|
"yomitanExtension": true,
|
||||||
|
"subtitleDictionaries": false,
|
||||||
|
"jellyfinRemoteSession": false
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = new ConfigService(dir);
|
||||||
|
const config = service.getConfig();
|
||||||
|
assert.equal(config.startupWarmups.lowPowerMode, true);
|
||||||
|
assert.equal(config.startupWarmups.mecab, false);
|
||||||
|
assert.equal(config.startupWarmups.yomitanExtension, true);
|
||||||
|
assert.equal(config.startupWarmups.subtitleDictionaries, false);
|
||||||
|
assert.equal(config.startupWarmups.jellyfinRemoteSession, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid startup warmup values warn and keep defaults', () => {
|
||||||
|
const dir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"startupWarmups": {
|
||||||
|
"lowPowerMode": "yes",
|
||||||
|
"mecab": 1,
|
||||||
|
"yomitanExtension": null,
|
||||||
|
"subtitleDictionaries": "no",
|
||||||
|
"jellyfinRemoteSession": []
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = new ConfigService(dir);
|
||||||
|
const config = service.getConfig();
|
||||||
|
const warnings = service.getWarnings();
|
||||||
|
|
||||||
|
assert.equal(config.startupWarmups.lowPowerMode, DEFAULT_CONFIG.startupWarmups.lowPowerMode);
|
||||||
|
assert.equal(config.startupWarmups.mecab, DEFAULT_CONFIG.startupWarmups.mecab);
|
||||||
|
assert.equal(
|
||||||
|
config.startupWarmups.yomitanExtension,
|
||||||
|
DEFAULT_CONFIG.startupWarmups.yomitanExtension,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
config.startupWarmups.subtitleDictionaries,
|
||||||
|
DEFAULT_CONFIG.startupWarmups.subtitleDictionaries,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
config.startupWarmups.jellyfinRemoteSession,
|
||||||
|
DEFAULT_CONFIG.startupWarmups.jellyfinRemoteSession,
|
||||||
|
);
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.lowPowerMode'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.mecab'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.yomitanExtension'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.subtitleDictionaries'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.jellyfinRemoteSession'));
|
||||||
|
});
|
||||||
|
|
||||||
test('parses discordPresence fields and warns for invalid types', () => {
|
test('parses discordPresence fields and warns for invalid types', () => {
|
||||||
const dir = makeTempDir();
|
const dir = makeTempDir();
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -1135,6 +1206,7 @@ test('template generator includes known keys', () => {
|
|||||||
assert.match(output, /"logging":/);
|
assert.match(output, /"logging":/);
|
||||||
assert.match(output, /"websocket":/);
|
assert.match(output, /"websocket":/);
|
||||||
assert.match(output, /"discordPresence":/);
|
assert.match(output, /"discordPresence":/);
|
||||||
|
assert.match(output, /"startupWarmups":/);
|
||||||
assert.match(output, /"youtubeSubgen":/);
|
assert.match(output, /"youtubeSubgen":/);
|
||||||
assert.match(output, /"preserveLineBreaks": false/);
|
assert.match(output, /"preserveLineBreaks": false/);
|
||||||
assert.match(output, /"nPlusOne"\s*:\s*\{/);
|
assert.match(output, /"nPlusOne"\s*:\s*\{/);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const {
|
|||||||
shortcuts,
|
shortcuts,
|
||||||
secondarySub,
|
secondarySub,
|
||||||
subsync,
|
subsync,
|
||||||
|
startupWarmups,
|
||||||
auto_start_overlay,
|
auto_start_overlay,
|
||||||
} = CORE_DEFAULT_CONFIG;
|
} = CORE_DEFAULT_CONFIG;
|
||||||
const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } =
|
const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } =
|
||||||
@@ -44,6 +45,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
|||||||
shortcuts,
|
shortcuts,
|
||||||
secondarySub,
|
secondarySub,
|
||||||
subsync,
|
subsync,
|
||||||
|
startupWarmups,
|
||||||
subtitleStyle,
|
subtitleStyle,
|
||||||
auto_start_overlay,
|
auto_start_overlay,
|
||||||
jimaku,
|
jimaku,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
|||||||
| 'shortcuts'
|
| 'shortcuts'
|
||||||
| 'secondarySub'
|
| 'secondarySub'
|
||||||
| 'subsync'
|
| 'subsync'
|
||||||
|
| 'startupWarmups'
|
||||||
| 'auto_start_overlay'
|
| 'auto_start_overlay'
|
||||||
> = {
|
> = {
|
||||||
subtitlePosition: { yPercent: 10 },
|
subtitlePosition: { yPercent: 10 },
|
||||||
@@ -50,5 +51,12 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
|||||||
ffsubsync_path: '',
|
ffsubsync_path: '',
|
||||||
ffmpeg_path: '',
|
ffmpeg_path: '',
|
||||||
},
|
},
|
||||||
|
startupWarmups: {
|
||||||
|
lowPowerMode: false,
|
||||||
|
mecab: true,
|
||||||
|
yomitanExtension: true,
|
||||||
|
subtitleDictionaries: true,
|
||||||
|
jellyfinRemoteSession: true,
|
||||||
|
},
|
||||||
auto_start_overlay: false,
|
auto_start_overlay: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ test('config option registry includes critical paths and has unique entries', ()
|
|||||||
|
|
||||||
for (const requiredPath of [
|
for (const requiredPath of [
|
||||||
'logging.level',
|
'logging.level',
|
||||||
|
'startupWarmups.lowPowerMode',
|
||||||
'subtitleStyle.enableJlpt',
|
'subtitleStyle.enableJlpt',
|
||||||
'ankiConnect.enabled',
|
'ankiConnect.enabled',
|
||||||
'immersionTracking.enabled',
|
'immersionTracking.enabled',
|
||||||
@@ -31,6 +32,7 @@ test('config template sections include expected domains and unique keys', () =>
|
|||||||
const keys = CONFIG_TEMPLATE_SECTIONS.map((section) => section.key);
|
const keys = CONFIG_TEMPLATE_SECTIONS.map((section) => section.key);
|
||||||
const requiredKeys: (typeof keys)[number][] = [
|
const requiredKeys: (typeof keys)[number][] = [
|
||||||
'websocket',
|
'websocket',
|
||||||
|
'startupWarmups',
|
||||||
'subtitleStyle',
|
'subtitleStyle',
|
||||||
'ankiConnect',
|
'ankiConnect',
|
||||||
'immersionTracking',
|
'immersionTracking',
|
||||||
|
|||||||
@@ -32,6 +32,36 @@ export function buildCoreConfigOptionRegistry(
|
|||||||
defaultValue: defaultConfig.subsync.defaultMode,
|
defaultValue: defaultConfig.subsync.defaultMode,
|
||||||
description: 'Subsync default mode.',
|
description: 'Subsync default mode.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'startupWarmups.lowPowerMode',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.startupWarmups.lowPowerMode,
|
||||||
|
description: 'Defer startup warmups except Yomitan extension.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'startupWarmups.mecab',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.startupWarmups.mecab,
|
||||||
|
description: 'Warm up MeCab tokenizer at startup.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'startupWarmups.yomitanExtension',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.startupWarmups.yomitanExtension,
|
||||||
|
description: 'Warm up Yomitan extension at startup.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'startupWarmups.subtitleDictionaries',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.startupWarmups.subtitleDictionaries,
|
||||||
|
description: 'Warm up subtitle dictionaries at startup.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'startupWarmups.jellyfinRemoteSession',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.startupWarmups.jellyfinRemoteSession,
|
||||||
|
description: 'Warm up Jellyfin remote session at startup.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'shortcuts.multiCopyTimeoutMs',
|
path: 'shortcuts.multiCopyTimeoutMs',
|
||||||
kind: 'number',
|
kind: 'number',
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
|||||||
description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'],
|
description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'],
|
||||||
key: 'logging',
|
key: 'logging',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Startup Warmups',
|
||||||
|
description: [
|
||||||
|
'Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.',
|
||||||
|
'Disable individual warmups to defer load until first real usage.',
|
||||||
|
'lowPowerMode defers all warmups except Yomitan extension.',
|
||||||
|
],
|
||||||
|
key: 'startupWarmups',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Keyboard Shortcuts',
|
title: 'Keyboard Shortcuts',
|
||||||
description: ['Overlay keyboard shortcuts. Set a shortcut to null to disable.'],
|
description: ['Overlay keyboard shortcuts. Set a shortcut to null to disable.'],
|
||||||
|
|||||||
@@ -74,6 +74,30 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isObject(src.startupWarmups)) {
|
||||||
|
const startupWarmupBooleanKeys = [
|
||||||
|
'lowPowerMode',
|
||||||
|
'mecab',
|
||||||
|
'yomitanExtension',
|
||||||
|
'subtitleDictionaries',
|
||||||
|
'jellyfinRemoteSession',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const key of startupWarmupBooleanKeys) {
|
||||||
|
const value = asBoolean(src.startupWarmups[key]);
|
||||||
|
if (value !== undefined) {
|
||||||
|
resolved.startupWarmups[key] = value as (typeof resolved.startupWarmups)[typeof key];
|
||||||
|
} else if (src.startupWarmups[key] !== undefined) {
|
||||||
|
warn(
|
||||||
|
`startupWarmups.${key}`,
|
||||||
|
src.startupWarmups[key],
|
||||||
|
resolved.startupWarmups[key],
|
||||||
|
'Expected boolean.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isObject(src.shortcuts)) {
|
if (isObject(src.shortcuts)) {
|
||||||
const shortcutKeys = [
|
const shortcutKeys = [
|
||||||
'toggleVisibleOverlayGlobal',
|
'toggleVisibleOverlayGlobal',
|
||||||
|
|||||||
27
src/main.ts
27
src/main.ts
@@ -1936,10 +1936,6 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR
|
|||||||
logInfo: (message) => appLogger.logInfo(message),
|
logInfo: (message) => appLogger.logInfo(message),
|
||||||
logWarning: (message) => appLogger.logWarning(message),
|
logWarning: (message) => appLogger.logWarning(message),
|
||||||
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
||||||
showConfigWarningsDialog:
|
|
||||||
process.platform === 'darwin'
|
|
||||||
? (title, details) => dialog.showErrorBox(title, details)
|
|
||||||
: undefined,
|
|
||||||
startConfigHotReload: () => configHotReloadRuntime.start(),
|
startConfigHotReload: () => configHotReloadRuntime.start(),
|
||||||
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
|
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
|
||||||
failHandlers: {
|
failHandlers: {
|
||||||
@@ -2250,6 +2246,7 @@ const {
|
|||||||
getKnownWordMatchMode: () =>
|
getKnownWordMatchMode: () =>
|
||||||
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
||||||
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
||||||
|
getNPlusOneEnabled: () => getResolvedConfig().ankiConnect.nPlusOne.highlightEnabled,
|
||||||
getMinSentenceWordsForNPlusOne: () =>
|
getMinSentenceWordsForNPlusOne: () =>
|
||||||
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
||||||
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
||||||
@@ -2290,6 +2287,28 @@ const {
|
|||||||
},
|
},
|
||||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||||
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
|
||||||
|
shouldWarmupMecab: () => {
|
||||||
|
const startupWarmups = getResolvedConfig().startupWarmups;
|
||||||
|
if (startupWarmups.lowPowerMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return startupWarmups.mecab;
|
||||||
|
},
|
||||||
|
shouldWarmupYomitanExtension: () => getResolvedConfig().startupWarmups.yomitanExtension,
|
||||||
|
shouldWarmupSubtitleDictionaries: () => {
|
||||||
|
const startupWarmups = getResolvedConfig().startupWarmups;
|
||||||
|
if (startupWarmups.lowPowerMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return startupWarmups.subtitleDictionaries;
|
||||||
|
},
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => {
|
||||||
|
const startupWarmups = getResolvedConfig().startupWarmups;
|
||||||
|
if (startupWarmups.lowPowerMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return startupWarmups.jellyfinRemoteSession;
|
||||||
|
},
|
||||||
shouldAutoConnectJellyfinRemote: () => {
|
shouldAutoConnectJellyfinRemote: () => {
|
||||||
const jellyfin = getResolvedConfig().jellyfin;
|
const jellyfin = getResolvedConfig().jellyfin;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
|||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
let started = false;
|
let started = false;
|
||||||
let metrics = BASE_METRICS;
|
let metrics = BASE_METRICS;
|
||||||
|
let mecabTokenizer: { id: string } | null = null;
|
||||||
|
|
||||||
class FakeMpvClient {
|
class FakeMpvClient {
|
||||||
connected = false;
|
connected = false;
|
||||||
@@ -140,9 +141,15 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
|||||||
return { text };
|
return { text };
|
||||||
},
|
},
|
||||||
createMecabTokenizerAndCheckMainDeps: {
|
createMecabTokenizerAndCheckMainDeps: {
|
||||||
getMecabTokenizer: () => ({ id: 'mecab' }),
|
getMecabTokenizer: () => mecabTokenizer,
|
||||||
setMecabTokenizer: () => {},
|
setMecabTokenizer: (next) => {
|
||||||
createMecabTokenizer: () => ({ id: 'mecab' }),
|
mecabTokenizer = next as { id: string };
|
||||||
|
calls.push('set-mecab');
|
||||||
|
},
|
||||||
|
createMecabTokenizer: () => {
|
||||||
|
calls.push('create-mecab');
|
||||||
|
return { id: 'mecab' };
|
||||||
|
},
|
||||||
checkAvailability: async () => {
|
checkAvailability: async () => {
|
||||||
calls.push('check-mecab');
|
calls.push('check-mecab');
|
||||||
},
|
},
|
||||||
@@ -176,6 +183,10 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
|||||||
ensureYomitanExtensionLoaded: async () => {
|
ensureYomitanExtensionLoaded: async () => {
|
||||||
calls.push('warmup-yomitan');
|
calls.push('warmup-yomitan');
|
||||||
},
|
},
|
||||||
|
shouldWarmupMecab: () => true,
|
||||||
|
shouldWarmupYomitanExtension: () => true,
|
||||||
|
shouldWarmupSubtitleDictionaries: () => true,
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => true,
|
||||||
shouldAutoConnectJellyfinRemote: () => false,
|
shouldAutoConnectJellyfinRemote: () => false,
|
||||||
startJellyfinRemoteSession: async () => {
|
startJellyfinRemoteSession: async () => {
|
||||||
calls.push('warmup-jellyfin');
|
calls.push('warmup-jellyfin');
|
||||||
@@ -212,9 +223,12 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
|||||||
assert.ok(calls.includes('broadcast-metrics'));
|
assert.ok(calls.includes('broadcast-metrics'));
|
||||||
assert.ok(calls.includes('create-tokenizer-runtime-deps'));
|
assert.ok(calls.includes('create-tokenizer-runtime-deps'));
|
||||||
assert.ok(calls.includes('tokenize:subtitle text'));
|
assert.ok(calls.includes('tokenize:subtitle text'));
|
||||||
|
assert.ok(calls.includes('create-mecab'));
|
||||||
|
assert.ok(calls.includes('set-mecab'));
|
||||||
assert.ok(calls.includes('check-mecab'));
|
assert.ok(calls.includes('check-mecab'));
|
||||||
assert.ok(calls.includes('prewarm-jlpt'));
|
assert.ok(calls.includes('prewarm-jlpt'));
|
||||||
assert.ok(calls.includes('prewarm-frequency'));
|
assert.ok(calls.includes('prewarm-frequency'));
|
||||||
assert.ok(calls.includes('set-started:true'));
|
assert.ok(calls.includes('set-started:true'));
|
||||||
assert.ok(calls.includes('warmup-yomitan'));
|
assert.ok(calls.includes('warmup-yomitan'));
|
||||||
|
assert.ok(calls.indexOf('create-mecab') < calls.indexOf('set-started:true'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ export function composeMpvRuntimeHandlers<
|
|||||||
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
||||||
);
|
);
|
||||||
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
||||||
|
await options.warmups.startBackgroundWarmupsMainDeps.ensureYomitanExtensionLoaded();
|
||||||
|
if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) {
|
||||||
|
await createMecabTokenizerAndCheck().catch(() => {});
|
||||||
|
}
|
||||||
await prewarmSubtitleDictionaries();
|
await prewarmSubtitleDictionaries();
|
||||||
return options.tokenizer.tokenizeSubtitle(
|
return options.tokenizer.tokenizeSubtitle(
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ test('startup warmups main deps builders map callbacks', async () => {
|
|||||||
prewarmSubtitleDictionaries: async () => {
|
prewarmSubtitleDictionaries: async () => {
|
||||||
calls.push('dict');
|
calls.push('dict');
|
||||||
},
|
},
|
||||||
|
shouldWarmupMecab: () => false,
|
||||||
|
shouldWarmupYomitanExtension: () => true,
|
||||||
|
shouldWarmupSubtitleDictionaries: () => false,
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => true,
|
||||||
shouldAutoConnectJellyfinRemote: () => true,
|
shouldAutoConnectJellyfinRemote: () => true,
|
||||||
startJellyfinRemoteSession: async () => {
|
startJellyfinRemoteSession: async () => {
|
||||||
calls.push('jellyfin');
|
calls.push('jellyfin');
|
||||||
@@ -48,6 +52,10 @@ test('startup warmups main deps builders map callbacks', async () => {
|
|||||||
await start.createMecabTokenizerAndCheck();
|
await start.createMecabTokenizerAndCheck();
|
||||||
await start.ensureYomitanExtensionLoaded();
|
await start.ensureYomitanExtensionLoaded();
|
||||||
await start.prewarmSubtitleDictionaries();
|
await start.prewarmSubtitleDictionaries();
|
||||||
|
assert.equal(start.shouldWarmupMecab(), false);
|
||||||
|
assert.equal(start.shouldWarmupYomitanExtension(), true);
|
||||||
|
assert.equal(start.shouldWarmupSubtitleDictionaries(), false);
|
||||||
|
assert.equal(start.shouldWarmupJellyfinRemoteSession(), true);
|
||||||
assert.equal(start.shouldAutoConnectJellyfinRemote(), true);
|
assert.equal(start.shouldAutoConnectJellyfinRemote(), true);
|
||||||
await start.startJellyfinRemoteSession();
|
await start.startJellyfinRemoteSession();
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export function createBuildStartBackgroundWarmupsMainDepsHandler(deps: StartBack
|
|||||||
createMecabTokenizerAndCheck: () => deps.createMecabTokenizerAndCheck(),
|
createMecabTokenizerAndCheck: () => deps.createMecabTokenizerAndCheck(),
|
||||||
ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(),
|
ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(),
|
||||||
prewarmSubtitleDictionaries: () => deps.prewarmSubtitleDictionaries(),
|
prewarmSubtitleDictionaries: () => deps.prewarmSubtitleDictionaries(),
|
||||||
|
shouldWarmupMecab: () => deps.shouldWarmupMecab(),
|
||||||
|
shouldWarmupYomitanExtension: () => deps.shouldWarmupYomitanExtension(),
|
||||||
|
shouldWarmupSubtitleDictionaries: () => deps.shouldWarmupSubtitleDictionaries(),
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => deps.shouldWarmupJellyfinRemoteSession(),
|
||||||
shouldAutoConnectJellyfinRemote: () => deps.shouldAutoConnectJellyfinRemote(),
|
shouldAutoConnectJellyfinRemote: () => deps.shouldAutoConnectJellyfinRemote(),
|
||||||
startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(),
|
startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ test('startBackgroundWarmups no-ops when already started', () => {
|
|||||||
createMecabTokenizerAndCheck: async () => {},
|
createMecabTokenizerAndCheck: async () => {},
|
||||||
ensureYomitanExtensionLoaded: async () => {},
|
ensureYomitanExtensionLoaded: async () => {},
|
||||||
prewarmSubtitleDictionaries: async () => {},
|
prewarmSubtitleDictionaries: async () => {},
|
||||||
|
shouldWarmupMecab: () => true,
|
||||||
|
shouldWarmupYomitanExtension: () => true,
|
||||||
|
shouldWarmupSubtitleDictionaries: () => true,
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => true,
|
||||||
shouldAutoConnectJellyfinRemote: () => false,
|
shouldAutoConnectJellyfinRemote: () => false,
|
||||||
startJellyfinRemoteSession: async () => {},
|
startJellyfinRemoteSession: async () => {},
|
||||||
});
|
});
|
||||||
@@ -49,7 +53,7 @@ test('startBackgroundWarmups no-ops when already started', () => {
|
|||||||
assert.equal(launches, 0);
|
assert.equal(launches, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.enabled is false', () => {
|
test('startBackgroundWarmups respects per-integration warmup toggles', () => {
|
||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
let started = false;
|
let started = false;
|
||||||
const startWarmups = createStartBackgroundWarmupsHandler({
|
const startWarmups = createStartBackgroundWarmupsHandler({
|
||||||
@@ -64,9 +68,13 @@ test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.ena
|
|||||||
createMecabTokenizerAndCheck: async () => {},
|
createMecabTokenizerAndCheck: async () => {},
|
||||||
ensureYomitanExtensionLoaded: async () => {},
|
ensureYomitanExtensionLoaded: async () => {},
|
||||||
prewarmSubtitleDictionaries: async () => {},
|
prewarmSubtitleDictionaries: async () => {},
|
||||||
|
shouldWarmupMecab: () => false,
|
||||||
|
shouldWarmupYomitanExtension: () => true,
|
||||||
|
shouldWarmupSubtitleDictionaries: () => false,
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => false,
|
||||||
shouldAutoConnectJellyfinRemote: () =>
|
shouldAutoConnectJellyfinRemote: () =>
|
||||||
shouldAutoConnectJellyfinRemote({
|
shouldAutoConnectJellyfinRemote({
|
||||||
enabled: false,
|
enabled: true,
|
||||||
remoteControlEnabled: true,
|
remoteControlEnabled: true,
|
||||||
remoteControlAutoConnect: true,
|
remoteControlAutoConnect: true,
|
||||||
}),
|
}),
|
||||||
@@ -75,7 +83,7 @@ test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.ena
|
|||||||
|
|
||||||
startWarmups();
|
startWarmups();
|
||||||
assert.equal(started, true);
|
assert.equal(started, true);
|
||||||
assert.deepEqual(labels, ['mecab', 'yomitan-extension', 'subtitle-dictionaries']);
|
assert.deepEqual(labels, ['yomitan-extension']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags are enabled', () => {
|
test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags are enabled', () => {
|
||||||
@@ -93,6 +101,10 @@ test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags a
|
|||||||
createMecabTokenizerAndCheck: async () => {},
|
createMecabTokenizerAndCheck: async () => {},
|
||||||
ensureYomitanExtensionLoaded: async () => {},
|
ensureYomitanExtensionLoaded: async () => {},
|
||||||
prewarmSubtitleDictionaries: async () => {},
|
prewarmSubtitleDictionaries: async () => {},
|
||||||
|
shouldWarmupMecab: () => true,
|
||||||
|
shouldWarmupYomitanExtension: () => true,
|
||||||
|
shouldWarmupSubtitleDictionaries: () => true,
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => true,
|
||||||
shouldAutoConnectJellyfinRemote: () =>
|
shouldAutoConnectJellyfinRemote: () =>
|
||||||
shouldAutoConnectJellyfinRemote({
|
shouldAutoConnectJellyfinRemote({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -111,3 +123,36 @@ test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags a
|
|||||||
'jellyfin-remote-session',
|
'jellyfin-remote-session',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('startBackgroundWarmups skips jellyfin warmup when warmup is deferred', () => {
|
||||||
|
const labels: string[] = [];
|
||||||
|
let started = false;
|
||||||
|
const startWarmups = createStartBackgroundWarmupsHandler({
|
||||||
|
getStarted: () => started,
|
||||||
|
setStarted: (value) => {
|
||||||
|
started = value;
|
||||||
|
},
|
||||||
|
isTexthookerOnlyMode: () => false,
|
||||||
|
launchTask: (label) => {
|
||||||
|
labels.push(label);
|
||||||
|
},
|
||||||
|
createMecabTokenizerAndCheck: async () => {},
|
||||||
|
ensureYomitanExtensionLoaded: async () => {},
|
||||||
|
prewarmSubtitleDictionaries: async () => {},
|
||||||
|
shouldWarmupMecab: () => false,
|
||||||
|
shouldWarmupYomitanExtension: () => true,
|
||||||
|
shouldWarmupSubtitleDictionaries: () => false,
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => false,
|
||||||
|
shouldAutoConnectJellyfinRemote: () =>
|
||||||
|
shouldAutoConnectJellyfinRemote({
|
||||||
|
enabled: true,
|
||||||
|
remoteControlEnabled: true,
|
||||||
|
remoteControlAutoConnect: true,
|
||||||
|
}),
|
||||||
|
startJellyfinRemoteSession: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
startWarmups();
|
||||||
|
assert.equal(started, true);
|
||||||
|
assert.deepEqual(labels, ['yomitan-extension']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ export function createStartBackgroundWarmupsHandler(deps: {
|
|||||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||||
ensureYomitanExtensionLoaded: () => Promise<void>;
|
ensureYomitanExtensionLoaded: () => Promise<void>;
|
||||||
prewarmSubtitleDictionaries: () => Promise<void>;
|
prewarmSubtitleDictionaries: () => Promise<void>;
|
||||||
|
shouldWarmupMecab: () => boolean;
|
||||||
|
shouldWarmupYomitanExtension: () => boolean;
|
||||||
|
shouldWarmupSubtitleDictionaries: () => boolean;
|
||||||
|
shouldWarmupJellyfinRemoteSession: () => boolean;
|
||||||
shouldAutoConnectJellyfinRemote: () => boolean;
|
shouldAutoConnectJellyfinRemote: () => boolean;
|
||||||
startJellyfinRemoteSession: () => Promise<void>;
|
startJellyfinRemoteSession: () => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
@@ -32,16 +36,22 @@ export function createStartBackgroundWarmupsHandler(deps: {
|
|||||||
if (deps.isTexthookerOnlyMode()) return;
|
if (deps.isTexthookerOnlyMode()) return;
|
||||||
|
|
||||||
deps.setStarted(true);
|
deps.setStarted(true);
|
||||||
|
if (deps.shouldWarmupMecab()) {
|
||||||
deps.launchTask('mecab', async () => {
|
deps.launchTask('mecab', async () => {
|
||||||
await deps.createMecabTokenizerAndCheck();
|
await deps.createMecabTokenizerAndCheck();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (deps.shouldWarmupYomitanExtension()) {
|
||||||
deps.launchTask('yomitan-extension', async () => {
|
deps.launchTask('yomitan-extension', async () => {
|
||||||
await deps.ensureYomitanExtensionLoaded();
|
await deps.ensureYomitanExtensionLoaded();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (deps.shouldWarmupSubtitleDictionaries()) {
|
||||||
deps.launchTask('subtitle-dictionaries', async () => {
|
deps.launchTask('subtitle-dictionaries', async () => {
|
||||||
await deps.prewarmSubtitleDictionaries();
|
await deps.prewarmSubtitleDictionaries();
|
||||||
});
|
});
|
||||||
if (deps.shouldAutoConnectJellyfinRemote()) {
|
}
|
||||||
|
if (deps.shouldWarmupJellyfinRemoteSession() && deps.shouldAutoConnectJellyfinRemote()) {
|
||||||
deps.launchTask('jellyfin-remote-session', async () => {
|
deps.launchTask('jellyfin-remote-session', async () => {
|
||||||
await deps.startJellyfinRemoteSession();
|
await deps.startJellyfinRemoteSession();
|
||||||
});
|
});
|
||||||
|
|||||||
16
src/types.ts
16
src/types.ts
@@ -99,6 +99,14 @@ export interface SubsyncConfig {
|
|||||||
ffmpeg_path?: string;
|
ffmpeg_path?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StartupWarmupsConfig {
|
||||||
|
lowPowerMode?: boolean;
|
||||||
|
mecab?: boolean;
|
||||||
|
yomitanExtension?: boolean;
|
||||||
|
subtitleDictionaries?: boolean;
|
||||||
|
jellyfinRemoteSession?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WebSocketConfig {
|
export interface WebSocketConfig {
|
||||||
enabled?: boolean | 'auto';
|
enabled?: boolean | 'auto';
|
||||||
port?: number;
|
port?: number;
|
||||||
@@ -417,6 +425,7 @@ export interface Config {
|
|||||||
shortcuts?: ShortcutsConfig;
|
shortcuts?: ShortcutsConfig;
|
||||||
secondarySub?: SecondarySubConfig;
|
secondarySub?: SecondarySubConfig;
|
||||||
subsync?: SubsyncConfig;
|
subsync?: SubsyncConfig;
|
||||||
|
startupWarmups?: StartupWarmupsConfig;
|
||||||
subtitleStyle?: SubtitleStyleConfig;
|
subtitleStyle?: SubtitleStyleConfig;
|
||||||
auto_start_overlay?: boolean;
|
auto_start_overlay?: boolean;
|
||||||
jimaku?: JimakuConfig;
|
jimaku?: JimakuConfig;
|
||||||
@@ -513,6 +522,13 @@ export interface ResolvedConfig {
|
|||||||
shortcuts: Required<ShortcutsConfig>;
|
shortcuts: Required<ShortcutsConfig>;
|
||||||
secondarySub: Required<SecondarySubConfig>;
|
secondarySub: Required<SecondarySubConfig>;
|
||||||
subsync: Required<SubsyncConfig>;
|
subsync: Required<SubsyncConfig>;
|
||||||
|
startupWarmups: {
|
||||||
|
lowPowerMode: boolean;
|
||||||
|
mecab: boolean;
|
||||||
|
yomitanExtension: boolean;
|
||||||
|
subtitleDictionaries: boolean;
|
||||||
|
jellyfinRemoteSession: boolean;
|
||||||
|
};
|
||||||
subtitleStyle: Required<Omit<SubtitleStyleConfig, 'secondary' | 'frequencyDictionary'>> & {
|
subtitleStyle: Required<Omit<SubtitleStyleConfig, 'secondary' | 'frequencyDictionary'>> & {
|
||||||
secondary: Required<NonNullable<SubtitleStyleConfig['secondary']>>;
|
secondary: Required<NonNullable<SubtitleStyleConfig['secondary']>>;
|
||||||
frequencyDictionary: {
|
frequencyDictionary: {
|
||||||
|
|||||||
Reference in New Issue
Block a user