mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 04:49:49 -07:00
fix(overlay): defer autoplay release for untokenized startup priming
- Autoplay-ready gate now ignores untokenized subtitle payloads while tokenization warmup is pending, so playback doesn't resume before the first tokenized cue - Wire isTokenizationReady dep through main.ts and update gate/wiring tests accordingly - Drop stale release/release-notes.md, add changelog entry under changes/
This commit is contained in:
@@ -1275,6 +1275,8 @@ const autoplayReadyGate = createAutoplayReadyGate({
|
||||
signalPluginAutoplayReady: () => {
|
||||
sendMpvCommandRuntime(appState.mpvClient, ['script-message', 'subminer-autoplay-ready']);
|
||||
},
|
||||
// Deferred: isTokenizationWarmupReady is assigned during composeMpvRuntimeHandlers below.
|
||||
isTokenizationReady: () => isTokenizationWarmupReady(),
|
||||
requestOverlayPointerRecovery: () => {
|
||||
if (process.platform !== 'darwin' || !overlayManager.getVisibleOverlayVisible()) {
|
||||
return;
|
||||
|
||||
@@ -278,7 +278,15 @@ test('startup autoplay release is tied to visible overlay measurement readiness'
|
||||
|
||||
assert.ok(gateBlock);
|
||||
assert.match(gateBlock, /isSignalTargetReady:\s*\(signal\) =>/);
|
||||
assert.doesNotMatch(gateBlock, /isTokenizationWarmupReady\(\)/);
|
||||
// Untokenized signals are filtered by the gate's isTokenizationReady dep, not
|
||||
// by the target-readiness predicate (which must stay warmup-free so warm and
|
||||
// tokenized releases are never deferred behind the global warmup flag).
|
||||
assert.match(gateBlock, /isTokenizationReady:\s*\(\) => isTokenizationWarmupReady\(\)/);
|
||||
const signalTargetReadyBlock = gateBlock.match(
|
||||
/isSignalTargetReady:\s*\(signal\) =>(?<body>[\s\S]*?)\n schedule:/,
|
||||
)?.groups?.body;
|
||||
assert.ok(signalTargetReadyBlock);
|
||||
assert.doesNotMatch(signalTargetReadyBlock, /isTokenizationWarmupReady\(\)/);
|
||||
assert.match(gateBlock, /isVisibleOverlayAutoplayTargetReady\(/);
|
||||
assert.match(gateBlock, /getLatestVisibleMeasurement:/);
|
||||
|
||||
|
||||
@@ -542,3 +542,79 @@ test('autoplay ready gate passes the pending subtitle signal to the readiness pr
|
||||
[['script-message', 'subminer-autoplay-ready']],
|
||||
);
|
||||
});
|
||||
|
||||
function createTokenizationGateHarness(deps: { isTokenizationReady: () => boolean }) {
|
||||
const commands: Array<Array<string | boolean>> = [];
|
||||
const gate = createAutoplayReadyGate({
|
||||
isAppOwnedFlowInFlight: () => false,
|
||||
getCurrentMediaPath: () => '/media/video.mkv',
|
||||
getCurrentVideoPath: () => null,
|
||||
getPlaybackPaused: () => true,
|
||||
getMpvClient: () =>
|
||||
({
|
||||
connected: true,
|
||||
requestProperty: async () => true,
|
||||
send: ({ command }: { command: Array<string | boolean> }) => {
|
||||
commands.push(command);
|
||||
},
|
||||
}) as never,
|
||||
signalPluginAutoplayReady: () => {
|
||||
commands.push(['script-message', 'subminer-autoplay-ready']);
|
||||
},
|
||||
isTokenizationReady: deps.isTokenizationReady,
|
||||
schedule: (callback) => {
|
||||
queueMicrotask(callback);
|
||||
return 1 as never;
|
||||
},
|
||||
logDebug: () => {},
|
||||
});
|
||||
return { gate, commands };
|
||||
}
|
||||
|
||||
test('autoplay ready gate ignores untokenized signals while tokenization warmup is pending', async () => {
|
||||
const { gate, commands } = createTokenizationGateHarness({
|
||||
isTokenizationReady: () => false,
|
||||
});
|
||||
|
||||
gate.maybeSignalPluginAutoplayReady({ text: '字幕', tokens: null }, { forceWhilePaused: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.deepEqual(commands, []);
|
||||
});
|
||||
|
||||
test('autoplay ready gate releases tokenized signals while tokenization warmup is pending', async () => {
|
||||
const { gate, commands } = createTokenizationGateHarness({
|
||||
isTokenizationReady: () => false,
|
||||
});
|
||||
|
||||
gate.maybeSignalPluginAutoplayReady(
|
||||
{ text: '字幕', tokens: [{ surface: '字幕' }] as never },
|
||||
{ forceWhilePaused: true },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.deepEqual(
|
||||
commands.filter((command) => command[0] === 'script-message'),
|
||||
[['script-message', 'subminer-autoplay-ready']],
|
||||
);
|
||||
});
|
||||
|
||||
test('autoplay ready gate releases untokenized signals once tokenization warmup is ready', async () => {
|
||||
let tokenizationReady = false;
|
||||
const { gate, commands } = createTokenizationGateHarness({
|
||||
isTokenizationReady: () => tokenizationReady,
|
||||
});
|
||||
|
||||
gate.maybeSignalPluginAutoplayReady({ text: '字幕', tokens: null }, { forceWhilePaused: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(commands, []);
|
||||
|
||||
tokenizationReady = true;
|
||||
gate.maybeSignalPluginAutoplayReady({ text: '字幕', tokens: null }, { forceWhilePaused: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.deepEqual(
|
||||
commands.filter((command) => command[0] === 'script-message'),
|
||||
[['script-message', 'subminer-autoplay-ready']],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export type AutoplayReadyGateDeps = {
|
||||
getPlaybackPaused: () => boolean | null;
|
||||
getMpvClient: () => MpvClientLike | null;
|
||||
signalPluginAutoplayReady: () => void;
|
||||
isTokenizationReady?: () => boolean;
|
||||
requestOverlayPointerRecovery?: () => void;
|
||||
onAutoplayReadyReleased?: (signal: AutoplayReadySignal) => void;
|
||||
isSignalTargetReady?: (signal: AutoplayReadySignal) => boolean;
|
||||
@@ -217,6 +218,15 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) {
|
||||
if (!payload.text.trim()) {
|
||||
return;
|
||||
}
|
||||
// Untokenized payloads (startup subtitle priming, tokenizer fallbacks) must
|
||||
// not release the startup pause gate while tokenization warmup is pending —
|
||||
// the tokenized delivery or the post-warmup release signals readiness later.
|
||||
if (payload.tokens === null && deps.isTokenizationReady && !deps.isTokenizationReady()) {
|
||||
deps.logDebug(
|
||||
'[autoplay-ready] ignored untokenized signal while tokenization warmup is pending',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
maybeReleaseAutoplayReadySignal({
|
||||
mediaPath: getSignalMediaPath(),
|
||||
|
||||
Reference in New Issue
Block a user