mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-20 17:16:19 -07:00
feat(subtitles): use loaded subtitles to guide generation timing (#249)
This commit is contained in:
@@ -136,25 +136,28 @@ test('current mpv generation requires an identifiable selected audio track', asy
|
||||
}
|
||||
});
|
||||
|
||||
test('explicit local file leaves Japanese track selection to the shared generator', async () => {
|
||||
test('explicit playing file leaves audio selection to the generator but inspects subtitle references', async () => {
|
||||
const f = fixture();
|
||||
await runGenerateSubtitlesCommand(f.context, f.deps);
|
||||
assert.equal(f.generations[0]?.audioStreamIndex, undefined);
|
||||
assert.equal(
|
||||
f.commands.some((command) => command[1] === 'track-list'),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('launcher does not load generated subtitles after mpv switches files', async () => {
|
||||
const f = fixture(['generate-subs']);
|
||||
let pathRequests = 0;
|
||||
let changed = false;
|
||||
f.deps.mpvCommand = async (_socket, command) => {
|
||||
f.commands.push(command);
|
||||
if (command[1] === 'path')
|
||||
return ++pathRequests === 1 ? '/media/episode.mkv' : '/media/next.mkv';
|
||||
if (command[1] === 'path') return changed ? '/media/next.mkv' : '/media/episode.mkv';
|
||||
return [{ type: 'audio', selected: true, 'ff-index': 2 }];
|
||||
};
|
||||
f.deps.generate = async () => {
|
||||
changed = true;
|
||||
return '/media/episode.ja.srt';
|
||||
};
|
||||
await runGenerateSubtitlesCommand(f.context, f.deps);
|
||||
assert.equal(
|
||||
f.commands.some((command) => command[0] === 'sub-add'),
|
||||
@@ -163,6 +166,86 @@ test('launcher does not load generated subtitles after mpv switches files', asyn
|
||||
assert.match(f.output.join(''), /Saved Japanese subtitles/);
|
||||
});
|
||||
|
||||
test('current mpv generation rejects tracks when playback changes during capture', async () => {
|
||||
for (const nextMedia of ['/media/next.mkv', null]) {
|
||||
const f = fixture(['generate-subs']);
|
||||
let media: string | null = '/media/episode.mkv';
|
||||
let modelChecks = 0;
|
||||
f.deps.mpvCommand = async (_socket, command) => {
|
||||
if (command[1] === 'path') return media;
|
||||
if (command[1] === 'track-list') {
|
||||
media = nextMedia;
|
||||
return [{ type: 'audio', selected: true, 'ff-index': 99 }];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
f.deps.resolveModel = async () => {
|
||||
modelChecks += 1;
|
||||
return { kind: 'external', path: '/models/model.bin' };
|
||||
};
|
||||
await assert.rejects(
|
||||
runGenerateSubtitlesCommand(f.context, f.deps),
|
||||
/mpv media.*Run generate-subs again/,
|
||||
);
|
||||
assert.equal(f.generations.length, 0);
|
||||
assert.equal(modelChecks, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('explicit media or audio stream remains usable when the mpv snapshot changes', async () => {
|
||||
for (const options of [['/media/episode.mkv'], ['--audio-stream', '7']]) {
|
||||
const f = fixture(['generate-subs', ...options]);
|
||||
let media = '/media/episode.mkv';
|
||||
f.deps.mpvCommand = async (_socket, command) => {
|
||||
if (command[1] === 'path') return media;
|
||||
if (command[1] === 'track-list') {
|
||||
media = '/media/next.mkv';
|
||||
return [
|
||||
{ type: 'audio', selected: true, 'ff-index': 99 },
|
||||
{ type: 'sub', lang: 'eng', 'ff-index': 100 },
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
await runGenerateSubtitlesCommand(f.context, f.deps);
|
||||
assert.equal(f.generations[0]?.mediaPath, '/media/episode.mkv');
|
||||
assert.equal(
|
||||
f.generations[0]?.audioStreamIndex,
|
||||
options[0] === '--audio-stream' ? 7 : undefined,
|
||||
);
|
||||
assert.deepEqual(f.generations[0]?.references, []);
|
||||
}
|
||||
});
|
||||
|
||||
test('launcher captures loaded external references only for the matching media', async () => {
|
||||
for (const matching of [true, false]) {
|
||||
const f = fixture();
|
||||
f.deps.mpvCommand = async (_socket, command) => {
|
||||
if (command[1] === 'path') return matching ? '/media/episode.mkv' : '/media/other.mkv';
|
||||
if (command[1] === 'working-directory') return '/mpv';
|
||||
if (command[1] === 'track-list')
|
||||
return [
|
||||
{ type: 'sub', external: true, 'external-filename': 'episode.en.signs.ass' },
|
||||
{ type: 'sub', external: true, 'external-filename': 'episode.en.srt' },
|
||||
];
|
||||
return undefined;
|
||||
};
|
||||
await runGenerateSubtitlesCommand(f.context, f.deps);
|
||||
assert.deepEqual(
|
||||
f.generations[0]?.references,
|
||||
matching
|
||||
? [
|
||||
{
|
||||
label: 'episode.en.srt',
|
||||
delaySeconds: 0,
|
||||
source: { kind: 'external', path: '/mpv/episode.en.srt' },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('explicit managed model overrides external config and downloads before generation', async () => {
|
||||
const f = fixture([
|
||||
'generate-subs',
|
||||
@@ -191,6 +274,69 @@ test('explicit managed model overrides external config and downloads before gene
|
||||
assert.equal(f.generations.length, 1);
|
||||
});
|
||||
|
||||
test('audio and subtitle timing use the initial mpv snapshot across model setup', async () => {
|
||||
for (const changeDuring of ['resolve', 'download']) {
|
||||
const f = fixture(['generate-subs', '--download-model']);
|
||||
let changed = false;
|
||||
let trackReads = 0;
|
||||
f.deps.mpvCommand = async (_socket, command) => {
|
||||
if (command[1] === 'path') return '/media/episode.mkv';
|
||||
if (command[1] === 'track-list') {
|
||||
trackReads += 1;
|
||||
return [
|
||||
{ type: 'audio', selected: true, 'ff-index': changed ? 3 : 2 },
|
||||
{ type: 'sub', id: 1, title: 'English Full', lang: 'eng', 'ff-index': changed ? 5 : 4 },
|
||||
];
|
||||
}
|
||||
if (command[1] === 'sid') return 1;
|
||||
if (command[1] === 'sub-delay') return changed ? 9 : 1.5;
|
||||
return undefined;
|
||||
};
|
||||
f.deps.resolveModel = async () => {
|
||||
if (changeDuring === 'resolve') changed = true;
|
||||
return { kind: 'missing', path: '/models/model.bin' };
|
||||
};
|
||||
f.deps.downloadModel = async () => {
|
||||
changed = true;
|
||||
return '/models/model.bin';
|
||||
};
|
||||
await runGenerateSubtitlesCommand(f.context, f.deps);
|
||||
assert.equal(f.generations[0]?.audioStreamIndex, 2);
|
||||
assert.deepEqual(f.generations[0]?.references, [
|
||||
{
|
||||
label: 'English Full',
|
||||
delaySeconds: 1.5,
|
||||
source: { kind: 'embedded', streamIndex: 4 },
|
||||
},
|
||||
]);
|
||||
assert.equal(trackReads, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('explicit audio stream bypasses mpv audio selection while retaining subtitle references', async () => {
|
||||
const f = fixture(['generate-subs', '--audio-stream', '7']);
|
||||
f.deps.mpvCommand = async (_socket, command) => {
|
||||
if (command[1] === 'path') return '/media/episode.mkv';
|
||||
if (command[1] === 'track-list')
|
||||
return [
|
||||
{ type: 'audio', selected: true, external: true, 'ff-index': 0 },
|
||||
{ type: 'sub', id: 2, title: 'English Full', lang: 'eng', 'ff-index': 4 },
|
||||
];
|
||||
if (command[1] === 'secondary-sid') return 2;
|
||||
if (command[1] === 'secondary-sub-delay') return -0.5;
|
||||
return undefined;
|
||||
};
|
||||
await runGenerateSubtitlesCommand(f.context, f.deps);
|
||||
assert.equal(f.generations[0]?.audioStreamIndex, 7);
|
||||
assert.deepEqual(f.generations[0]?.references, [
|
||||
{
|
||||
label: 'English Full',
|
||||
delaySeconds: -0.5,
|
||||
source: { kind: 'embedded', streamIndex: 4 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('generation can run standalone and never loads subtitles into another video', async () => {
|
||||
for (const playing of [null, '/media/different.mkv']) {
|
||||
const f = fixture();
|
||||
|
||||
Reference in New Issue
Block a user