mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
fix: start animated AVIF motion at sentence start, not audio padding
- Removed leading padding from AVIF clip start so pre-sentence frames are not shown - Duration now extends by trailing padding only - Added regression test verifying -ss and -t args against a stub ffmpeg
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: anki
|
||||||
|
|
||||||
|
- Kept animated AVIF card images from showing pre-sentence motion on multi-line mines without delaying motion after sentence audio starts.
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
import { buildAnimatedImageVideoFilter } from './media-generator';
|
import { buildAnimatedImageVideoFilter, MediaGenerator } from './media-generator';
|
||||||
|
|
||||||
test('buildAnimatedImageVideoFilter prepends a cloned first frame when lead-in is provided', () => {
|
test('buildAnimatedImageVideoFilter prepends a cloned first frame when lead-in is provided', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -13,3 +16,55 @@ test('buildAnimatedImageVideoFilter prepends a cloned first frame when lead-in i
|
|||||||
'tpad=start_duration=1.25:start_mode=clone,fps=10,scale=w=640:h=-2',
|
'tpad=start_duration=1.25:start_mode=clone,fps=10,scale=w=640:h=-2',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('generateAnimatedImage starts motion with sentence audio instead of delaying for audio padding', async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-media-generator-test-'));
|
||||||
|
const binDir = path.join(root, 'bin');
|
||||||
|
const tempDir = path.join(root, 'media');
|
||||||
|
const argsPath = path.join(root, 'ffmpeg-args.txt');
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
const ffmpegPath = path.join(binDir, 'ffmpeg');
|
||||||
|
fs.writeFileSync(
|
||||||
|
ffmpegPath,
|
||||||
|
[
|
||||||
|
'#!/bin/sh',
|
||||||
|
'if [ "$1" = "-hide_banner" ] && [ "$2" = "-encoders" ]; then',
|
||||||
|
' echo " V..... libaom-av1"',
|
||||||
|
' exit 0',
|
||||||
|
'fi',
|
||||||
|
'printf "%s\\n" "$@" > "$SUBMINER_TEST_FFMPEG_ARGS"',
|
||||||
|
'out=""',
|
||||||
|
'for arg in "$@"; do out="$arg"; done',
|
||||||
|
'printf avif > "$out"',
|
||||||
|
].join('\n'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
fs.chmodSync(ffmpegPath, 0o755);
|
||||||
|
|
||||||
|
const originalPath = process.env.PATH;
|
||||||
|
const originalArgsPath = process.env.SUBMINER_TEST_FFMPEG_ARGS;
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ''}`;
|
||||||
|
process.env.SUBMINER_TEST_FFMPEG_ARGS = argsPath;
|
||||||
|
const generator = new MediaGenerator(tempDir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await generator.generateAnimatedImage('/video.mp4', 10, 12, 0.5, {
|
||||||
|
fps: 10,
|
||||||
|
maxWidth: 640,
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = fs.readFileSync(argsPath, 'utf8').trim().split('\n');
|
||||||
|
assert.equal(args[args.indexOf('-ss') + 1], '10');
|
||||||
|
assert.equal(args[args.indexOf('-t') + 1], '2.5');
|
||||||
|
assert.equal(args[args.indexOf('-vf') + 1], 'fps=10,scale=w=640:h=-2');
|
||||||
|
} finally {
|
||||||
|
generator.cleanup();
|
||||||
|
process.env.PATH = originalPath;
|
||||||
|
if (originalArgsPath === undefined) {
|
||||||
|
delete process.env.SUBMINER_TEST_FFMPEG_ARGS;
|
||||||
|
} else {
|
||||||
|
process.env.SUBMINER_TEST_FFMPEG_ARGS = originalArgsPath;
|
||||||
|
}
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -319,9 +319,11 @@ export class MediaGenerator {
|
|||||||
leadingStillDuration?: number;
|
leadingStillDuration?: number;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
const start = Math.max(0, startTime - padding);
|
|
||||||
const duration = endTime - startTime + 2 * padding;
|
|
||||||
const { fps = 10, maxWidth = 640, maxHeight, crf = 35, leadingStillDuration = 0 } = options;
|
const { fps = 10, maxWidth = 640, maxHeight, crf = 35, leadingStillDuration = 0 } = options;
|
||||||
|
const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
||||||
|
const start = Math.max(0, startTime);
|
||||||
|
const duration = endTime - startTime + safePadding;
|
||||||
|
const totalLeadingStillDuration = Math.max(0, leadingStillDuration);
|
||||||
|
|
||||||
const clampedCrf = Math.max(0, Math.min(63, crf));
|
const clampedCrf = Math.max(0, Math.min(63, crf));
|
||||||
|
|
||||||
@@ -359,7 +361,7 @@ export class MediaGenerator {
|
|||||||
fps,
|
fps,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
leadingStillDuration,
|
leadingStillDuration: totalLeadingStillDuration,
|
||||||
}),
|
}),
|
||||||
...encoderArgs,
|
...encoderArgs,
|
||||||
'-y',
|
'-y',
|
||||||
|
|||||||
Reference in New Issue
Block a user