mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 03:16:46 -07:00
Fix startup autoplay and dictionary progress sequencing
- keep paused startup release retries aligned with the full gate window - restore dictionary sync progress and reuse merged zips on unchanged revisits - surface later dictionary OSD updates once tokenization is ready
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
id: TASK-143
|
id: TASK-143
|
||||||
title: Keep character dictionary auto-sync non-blocking during startup
|
title: Keep character dictionary auto-sync non-blocking during startup
|
||||||
status: Done
|
status: In Progress
|
||||||
assignee: []
|
assignee:
|
||||||
|
- codex
|
||||||
created_date: '2026-03-09 01:45'
|
created_date: '2026-03-09 01:45'
|
||||||
updated_date: '2026-03-18 05:28'
|
updated_date: '2026-03-20 09:22'
|
||||||
labels:
|
labels:
|
||||||
- dictionary
|
- dictionary
|
||||||
- startup
|
- startup
|
||||||
@@ -33,8 +34,20 @@ Keep character dictionary auto-sync running in parallel during startup without d
|
|||||||
- [x] #3 Regression coverage verifies auto-sync builds before the gate and only mutates Yomitan after the gate resolves.
|
- [x] #3 Regression coverage verifies auto-sync builds before the gate and only mutates Yomitan after the gate resolves.
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1. Add a regression test for startup autoplay release surviving delayed mpv readiness or late subtitle refresh after dictionary sync.
|
||||||
|
2. Harden the autoplay-ready release path so paused startup keeps retrying until mpv is actually released or media changes, without resuming user-paused playback later.
|
||||||
|
3. Keep the existing character-dictionary revisit fixes and paused-startup OSD fixes aligned with the autoplay change, then run targeted runtime tests and typecheck.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
Added a small current-media tokenization gate in main runtime. Media changes reset the gate, the first tokenization-ready event marks it ready, and auto-sync now waits on that gate only before Yomitan dictionary inspection/import/settings updates. Snapshot generation and merged ZIP build still run immediately in parallel.
|
Added a small current-media tokenization gate in main runtime. Media changes reset the gate, the first tokenization-ready event marks it ready, and auto-sync now waits on that gate only before Yomitan dictionary inspection/import/settings updates. Snapshot generation and merged ZIP build still run immediately in parallel.
|
||||||
|
|
||||||
|
2026-03-20: User reports startup remains paused after annotations/tokenization are visible and only resumes after character-dictionary generation/import finishes. Investigating autoplay-ready release regression vs dictionary sync completion refresh.
|
||||||
|
|
||||||
|
2026-03-20: Added startup autoplay retry-budget helper so paused startup retries cover the full plugin gate window instead of only ~2.8s. Verification: bun test src/main/runtime/startup-autoplay-release-policy.test.ts src/main/runtime/character-dictionary-auto-sync.test.ts src/main/runtime/startup-osd-sequencer.test.ts src/main/runtime/character-dictionary-auto-sync-completion.test.ts; bun run typecheck; bun run test:fast; bun run test:env; bun run build; bun run test:smoke:dist; runtime-compat verifier passed at .tmp/skill-verification/subminer-verify-20260320-022106-nM28Nk. Pending real installed-app/mpv validation.
|
||||||
<!-- SECTION:NOTES:END -->
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
id: TASK-212
|
||||||
|
title: Fix mac texthooker helper startup blocking mpv launch
|
||||||
|
status: In Progress
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-20 08:27'
|
||||||
|
updated_date: '2026-03-20 08:45'
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
- macos
|
||||||
|
- startup
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- /Users/sudacode/projects/japanese/SubMiner/src/core/services/startup.ts
|
||||||
|
- /Users/sudacode/projects/japanese/SubMiner/src/main.ts
|
||||||
|
- /Users/sudacode/projects/japanese/SubMiner/plugin/subminer/process.lua
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
`subminer` mpv auto-start on mac can stall before the video is usable because the helper process launched with `--texthooker` still runs heavy app-ready startup. Recent logs show the helper loading the Yomitan Chromium extension, emitting `Permission 'contextMenus' is unknown` warnings, then hitting Chromium runtime errors before SubMiner signals readiness back to the mpv plugin. The texthooker helper should take the minimal startup path needed to serve texthooker traffic without loading overlay/window-only startup work that can crash or delay readiness.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Launching SubMiner with `--texthooker` avoids heavy app-ready startup work that is not required for texthooker helper mode.
|
||||||
|
- [x] #2 A regression test covers texthooker helper startup so it fails if Yomitan extension loading is reintroduced on that path.
|
||||||
|
- [x] #3 The change preserves existing startup behavior for non-texthooker app launches.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Follow-up: user confirmed the root issue is the plugin auto-start ordering. Adjust mpv plugin sequencing so `--start` launches before any separate `--texthooker` helper, then verify plugin regressions still pass.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Fixed the mac mpv startup hang caused by the `--texthooker` helper taking the full app-ready path. `runAppReadyRuntime` now fast-paths texthooker-only mode through minimal startup (`reloadConfig` plus CLI handling) so it no longer loads Yomitan or first-run setup work before serving texthooker traffic. Added regression coverage in `src/core/services/app-ready.test.ts`, then verified with `bun test src/core/services/app-ready.test.ts src/core/services/startup.test.ts`, `bun test src/cli/args.test.ts src/main/early-single-instance.test.ts src/main/runtime/stats-cli-command.test.ts`, and `bun run typecheck`.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
id: TASK-213
|
||||||
|
title: Show character dictionary progress during paused startup waits
|
||||||
|
status: In Progress
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-20 08:59'
|
||||||
|
updated_date: '2026-03-20 09:22'
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
- ux
|
||||||
|
- dictionary
|
||||||
|
- startup
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- >-
|
||||||
|
/Users/sudacode/projects/japanese/SubMiner/src/main/runtime/startup-osd-sequencer.ts
|
||||||
|
- >-
|
||||||
|
/Users/sudacode/projects/japanese/SubMiner/src/main/runtime/character-dictionary-auto-sync-notifications.ts
|
||||||
|
- /Users/sudacode/projects/japanese/SubMiner/src/main.ts
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
During startup on mpv auto-start, character dictionary regeneration/update can be active while playback remains paused. The current startup OSD sequencer buffers dictionary progress behind annotation-loading OSD, which leaves the user with no visible dictionary-specific progress while the pause is active. Adjust the startup OSD sequencing so dictionary progress can surface once tokenization is ready during the paused startup window, without regressing later ready/failure handling.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 When tokenization is ready during startup, later character dictionary progress updates are shown on OSD even if annotation-loading state is still active.
|
||||||
|
- [ ] #2 Startup OSD completion/failure behavior for character dictionary sync remains coherent after the new progress ordering.
|
||||||
|
- [ ] #3 Regression coverage exercises the paused startup sequencing for dictionary progress.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
2026-03-20: Confirmed issue is broader than OSD-only. Paused-startup OSD fixes remain relevant, but current user report also points at a regression in non-blocking startup playback release (tracked in TASK-143).
|
||||||
|
|
||||||
|
2026-03-20: OSD sequencing fix remains in local patch alongside TASK-143 regression fix. Covered by startup-osd-sequencer tests; pending installed-app/mpv validation before task finalization.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
@@ -315,6 +315,7 @@ import {
|
|||||||
createFirstRunSetupService,
|
createFirstRunSetupService,
|
||||||
shouldAutoOpenFirstRunSetup,
|
shouldAutoOpenFirstRunSetup,
|
||||||
} from './main/runtime/first-run-setup-service';
|
} from './main/runtime/first-run-setup-service';
|
||||||
|
import { resolveAutoplayReadyMaxReleaseAttempts } from './main/runtime/startup-autoplay-release-policy';
|
||||||
import {
|
import {
|
||||||
buildFirstRunSetupHtml,
|
buildFirstRunSetupHtml,
|
||||||
createMaybeFocusExistingFirstRunSetupWindowHandler,
|
createMaybeFocusExistingFirstRunSetupWindowHandler,
|
||||||
@@ -1096,8 +1097,11 @@ function maybeSignalPluginAutoplayReady(
|
|||||||
|
|
||||||
// Fallback: repeatedly try to release pause for a short window in case startup
|
// Fallback: repeatedly try to release pause for a short window in case startup
|
||||||
// gate arming and tokenization-ready signal arrive out of order.
|
// gate arming and tokenization-ready signal arrive out of order.
|
||||||
const maxReleaseAttempts = options?.forceWhilePaused === true ? 14 : 3;
|
|
||||||
const releaseRetryDelayMs = 200;
|
const releaseRetryDelayMs = 200;
|
||||||
|
const maxReleaseAttempts = resolveAutoplayReadyMaxReleaseAttempts({
|
||||||
|
forceWhilePaused: options?.forceWhilePaused === true,
|
||||||
|
retryDelayMs: releaseRetryDelayMs,
|
||||||
|
});
|
||||||
const attemptRelease = (attempt: number): void => {
|
const attemptRelease = (attempt: number): void => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -150,6 +150,59 @@ test('auto sync skips rebuild/import on unchanged revisit when merged dictionary
|
|||||||
assert.deepEqual(imports, ['/tmp/merged.zip']);
|
assert.deepEqual(imports, ['/tmp/merged.zip']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auto sync does not emit updating progress for unchanged revisit when merged dictionary is current', async () => {
|
||||||
|
const userDataPath = makeTempDir();
|
||||||
|
let importedRevision: string | null = null;
|
||||||
|
let currentRun: string[] = [];
|
||||||
|
const phaseHistory: string[][] = [];
|
||||||
|
|
||||||
|
const runtime = createCharacterDictionaryAutoSyncRuntimeService({
|
||||||
|
userDataPath,
|
||||||
|
getConfig: () => ({
|
||||||
|
enabled: true,
|
||||||
|
maxLoaded: 3,
|
||||||
|
profileScope: 'all',
|
||||||
|
}),
|
||||||
|
getOrCreateCurrentSnapshot: async () => ({
|
||||||
|
mediaId: 7,
|
||||||
|
mediaTitle: 'Frieren',
|
||||||
|
entryCount: 100,
|
||||||
|
fromCache: true,
|
||||||
|
updatedAt: 1000,
|
||||||
|
}),
|
||||||
|
buildMergedDictionary: async () => ({
|
||||||
|
zipPath: '/tmp/merged.zip',
|
||||||
|
revision: 'rev-7',
|
||||||
|
dictionaryTitle: 'SubMiner Character Dictionary',
|
||||||
|
entryCount: 100,
|
||||||
|
}),
|
||||||
|
getYomitanDictionaryInfo: async () =>
|
||||||
|
importedRevision
|
||||||
|
? [{ title: 'SubMiner Character Dictionary', revision: importedRevision }]
|
||||||
|
: [],
|
||||||
|
importYomitanDictionary: async () => {
|
||||||
|
importedRevision = 'rev-7';
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteYomitanDictionary: async () => true,
|
||||||
|
upsertYomitanDictionarySettings: async () => false,
|
||||||
|
now: () => 1000,
|
||||||
|
onSyncStatus: (event) => {
|
||||||
|
currentRun.push(event.phase);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
currentRun = [];
|
||||||
|
await runtime.runSyncNow();
|
||||||
|
phaseHistory.push([...currentRun]);
|
||||||
|
currentRun = [];
|
||||||
|
await runtime.runSyncNow();
|
||||||
|
phaseHistory.push([...currentRun]);
|
||||||
|
|
||||||
|
assert.deepEqual(phaseHistory[0], ['building', 'importing', 'ready']);
|
||||||
|
assert.deepEqual(phaseHistory[1], ['ready']);
|
||||||
|
});
|
||||||
|
|
||||||
test('auto sync updates MRU order without rebuilding merged dictionary when membership is unchanged', async () => {
|
test('auto sync updates MRU order without rebuilding merged dictionary when membership is unchanged', async () => {
|
||||||
const userDataPath = makeTempDir();
|
const userDataPath = makeTempDir();
|
||||||
const sequence = [1, 2, 1];
|
const sequence = [1, 2, 1];
|
||||||
@@ -217,6 +270,63 @@ test('auto sync updates MRU order without rebuilding merged dictionary when memb
|
|||||||
assert.deepEqual(state.activeMediaIds, ['1 - Title 1', '2 - Title 2']);
|
assert.deepEqual(state.activeMediaIds, ['1 - Title 1', '2 - Title 2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auto sync reimports existing merged zip without rebuilding on unchanged revisit', async () => {
|
||||||
|
const userDataPath = makeTempDir();
|
||||||
|
const dictionariesDir = path.join(userDataPath, 'character-dictionaries');
|
||||||
|
fs.mkdirSync(dictionariesDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dictionariesDir, 'merged.zip'), 'cached-zip', 'utf8');
|
||||||
|
const mergedBuilds: number[][] = [];
|
||||||
|
const imports: string[] = [];
|
||||||
|
let importedRevision: string | null = null;
|
||||||
|
|
||||||
|
const runtime = createCharacterDictionaryAutoSyncRuntimeService({
|
||||||
|
userDataPath,
|
||||||
|
getConfig: () => ({
|
||||||
|
enabled: true,
|
||||||
|
maxLoaded: 3,
|
||||||
|
profileScope: 'all',
|
||||||
|
}),
|
||||||
|
getOrCreateCurrentSnapshot: async () => ({
|
||||||
|
mediaId: 7,
|
||||||
|
mediaTitle: 'Frieren',
|
||||||
|
entryCount: 100,
|
||||||
|
fromCache: true,
|
||||||
|
updatedAt: 1000,
|
||||||
|
}),
|
||||||
|
buildMergedDictionary: async (mediaIds) => {
|
||||||
|
mergedBuilds.push([...mediaIds]);
|
||||||
|
return {
|
||||||
|
zipPath: '/tmp/merged.zip',
|
||||||
|
revision: 'rev-7',
|
||||||
|
dictionaryTitle: 'SubMiner Character Dictionary',
|
||||||
|
entryCount: 100,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getYomitanDictionaryInfo: async () =>
|
||||||
|
importedRevision
|
||||||
|
? [{ title: 'SubMiner Character Dictionary', revision: importedRevision }]
|
||||||
|
: [],
|
||||||
|
importYomitanDictionary: async (zipPath) => {
|
||||||
|
imports.push(zipPath);
|
||||||
|
importedRevision = 'rev-7';
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteYomitanDictionary: async () => true,
|
||||||
|
upsertYomitanDictionarySettings: async () => true,
|
||||||
|
now: () => 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.runSyncNow();
|
||||||
|
importedRevision = null;
|
||||||
|
await runtime.runSyncNow();
|
||||||
|
|
||||||
|
assert.deepEqual(mergedBuilds, [[7]]);
|
||||||
|
assert.deepEqual(imports, [
|
||||||
|
'/tmp/merged.zip',
|
||||||
|
path.join(userDataPath, 'character-dictionaries', 'merged.zip'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('auto sync evicts least recently used media from merged set', async () => {
|
test('auto sync evicts least recently used media from merged set', async () => {
|
||||||
const userDataPath = makeTempDir();
|
const userDataPath = makeTempDir();
|
||||||
const sequence = [1, 2, 3, 4];
|
const sequence = [1, 2, 3, 4];
|
||||||
@@ -537,12 +647,6 @@ test('auto sync emits progress events for start import and completion', async ()
|
|||||||
mediaTitle: 'Rascal Does Not Dream of Bunny Girl Senpai',
|
mediaTitle: 'Rascal Does Not Dream of Bunny Girl Senpai',
|
||||||
message: 'Generating character dictionary for Rascal Does Not Dream of Bunny Girl Senpai...',
|
message: 'Generating character dictionary for Rascal Does Not Dream of Bunny Girl Senpai...',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
phase: 'syncing',
|
|
||||||
mediaId: 101291,
|
|
||||||
mediaTitle: 'Rascal Does Not Dream of Bunny Girl Senpai',
|
|
||||||
message: 'Updating character dictionary for Rascal Does Not Dream of Bunny Girl Senpai...',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
phase: 'building',
|
phase: 'building',
|
||||||
mediaId: 101291,
|
mediaId: 101291,
|
||||||
|
|||||||
@@ -275,12 +275,6 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
|||||||
});
|
});
|
||||||
currentMediaId = snapshot.mediaId;
|
currentMediaId = snapshot.mediaId;
|
||||||
currentMediaTitle = snapshot.mediaTitle;
|
currentMediaTitle = snapshot.mediaTitle;
|
||||||
deps.onSyncStatus?.({
|
|
||||||
phase: 'syncing',
|
|
||||||
mediaId: snapshot.mediaId,
|
|
||||||
mediaTitle: snapshot.mediaTitle,
|
|
||||||
message: buildSyncingMessage(snapshot.mediaTitle),
|
|
||||||
});
|
|
||||||
const state = readAutoSyncState(statePath);
|
const state = readAutoSyncState(statePath);
|
||||||
const nextActiveMediaIds = [
|
const nextActiveMediaIds = [
|
||||||
{
|
{
|
||||||
@@ -360,8 +354,18 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (merged === null) {
|
if (merged === null) {
|
||||||
|
const existingMergedZipPath = path.join(dictionariesDir, 'merged.zip');
|
||||||
|
if (fs.existsSync(existingMergedZipPath)) {
|
||||||
|
merged = {
|
||||||
|
zipPath: existingMergedZipPath,
|
||||||
|
revision,
|
||||||
|
dictionaryTitle,
|
||||||
|
entryCount: snapshot.entryCount,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
merged = await deps.buildMergedDictionary(nextActiveMediaIdValues);
|
merged = await deps.buildMergedDictionary(nextActiveMediaIdValues);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
deps.logInfo?.(`[dictionary:auto-sync] importing merged dictionary: ${merged.zipPath}`);
|
deps.logInfo?.(`[dictionary:auto-sync] importing merged dictionary: ${merged.zipPath}`);
|
||||||
const imported = await withOperationTimeout(
|
const imported = await withOperationTimeout(
|
||||||
`importYomitanDictionary(${path.basename(merged.zipPath)})`,
|
`importYomitanDictionary(${path.basename(merged.zipPath)})`,
|
||||||
|
|||||||
32
src/main/runtime/startup-autoplay-release-policy.test.ts
Normal file
32
src/main/runtime/startup-autoplay-release-policy.test.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS,
|
||||||
|
resolveAutoplayReadyMaxReleaseAttempts,
|
||||||
|
STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS,
|
||||||
|
} from './startup-autoplay-release-policy';
|
||||||
|
|
||||||
|
test('autoplay release keeps the short retry budget for normal playback signals', () => {
|
||||||
|
assert.equal(resolveAutoplayReadyMaxReleaseAttempts(), 3);
|
||||||
|
assert.equal(resolveAutoplayReadyMaxReleaseAttempts({ forceWhilePaused: false }), 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('autoplay release uses the full startup timeout window while paused', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveAutoplayReadyMaxReleaseAttempts({ forceWhilePaused: true }),
|
||||||
|
Math.ceil(
|
||||||
|
STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS / DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('autoplay release rounds up custom paused retry budgets to cover the timeout window', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveAutoplayReadyMaxReleaseAttempts({
|
||||||
|
forceWhilePaused: true,
|
||||||
|
retryDelayMs: 300,
|
||||||
|
startupTimeoutMs: 1_000,
|
||||||
|
}),
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
});
|
||||||
28
src/main/runtime/startup-autoplay-release-policy.ts
Normal file
28
src/main/runtime/startup-autoplay-release-policy.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS = 200;
|
||||||
|
const STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
export function resolveAutoplayReadyMaxReleaseAttempts(options?: {
|
||||||
|
forceWhilePaused?: boolean;
|
||||||
|
retryDelayMs?: number;
|
||||||
|
startupTimeoutMs?: number;
|
||||||
|
}): number {
|
||||||
|
if (options?.forceWhilePaused !== true) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryDelayMs = Math.max(
|
||||||
|
1,
|
||||||
|
Math.floor(options.retryDelayMs ?? DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS),
|
||||||
|
);
|
||||||
|
const startupTimeoutMs = Math.max(
|
||||||
|
retryDelayMs,
|
||||||
|
Math.floor(options.startupTimeoutMs ?? STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Math.max(3, Math.ceil(startupTimeoutMs / retryDelayMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DEFAULT_AUTOPLAY_RELEASE_RETRY_DELAY_MS,
|
||||||
|
STARTUP_AUTOPLAY_RELEASE_TIMEOUT_MS,
|
||||||
|
};
|
||||||
@@ -62,7 +62,10 @@ test('startup OSD buffers checking behind annotations and replaces it with later
|
|||||||
makeDictionaryEvent('generating', 'Generating character dictionary for Frieren...'),
|
makeDictionaryEvent('generating', 'Generating character dictionary for Frieren...'),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |']);
|
assert.deepEqual(osdMessages, [
|
||||||
|
'Loading subtitle annotations |',
|
||||||
|
'Generating character dictionary for Frieren...',
|
||||||
|
]);
|
||||||
|
|
||||||
sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded');
|
sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded');
|
||||||
|
|
||||||
@@ -154,3 +157,30 @@ test('startup OSD reset keeps tokenization ready after first warmup', () => {
|
|||||||
|
|
||||||
assert.deepEqual(osdMessages, ['Updating character dictionary for Frieren...']);
|
assert.deepEqual(osdMessages, ['Updating character dictionary for Frieren...']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('startup OSD shows later dictionary progress immediately once tokenization is ready', () => {
|
||||||
|
const osdMessages: string[] = [];
|
||||||
|
const sequencer = createStartupOsdSequencer({
|
||||||
|
showOsd: (message) => {
|
||||||
|
osdMessages.push(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sequencer.showAnnotationLoading('Loading subtitle annotations |');
|
||||||
|
sequencer.markTokenizationReady();
|
||||||
|
sequencer.notifyCharacterDictionaryStatus(
|
||||||
|
makeDictionaryEvent('generating', 'Generating character dictionary for Frieren...'),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(osdMessages, [
|
||||||
|
'Loading subtitle annotations |',
|
||||||
|
'Generating character dictionary for Frieren...',
|
||||||
|
]);
|
||||||
|
|
||||||
|
sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded');
|
||||||
|
|
||||||
|
assert.deepEqual(osdMessages, [
|
||||||
|
'Loading subtitle annotations |',
|
||||||
|
'Generating character dictionary for Frieren...',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (pendingDictionaryProgress) {
|
if (pendingDictionaryProgress) {
|
||||||
|
if (dictionaryProgressShown) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
deps.showOsd(pendingDictionaryProgress.message);
|
deps.showOsd(pendingDictionaryProgress.message);
|
||||||
dictionaryProgressShown = true;
|
dictionaryProgressShown = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -84,6 +87,9 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
|||||||
if (canShowDictionaryStatus()) {
|
if (canShowDictionaryStatus()) {
|
||||||
deps.showOsd(event.message);
|
deps.showOsd(event.message);
|
||||||
dictionaryProgressShown = true;
|
dictionaryProgressShown = true;
|
||||||
|
} else if (tokenizationReady) {
|
||||||
|
deps.showOsd(event.message);
|
||||||
|
dictionaryProgressShown = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user