mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(audio): refuse trim windows shorter than one codec frame (#679)
trim-audio stream-copies, and a window below one frame ships a container with zero audio frames as a 200 success. Floor the window at 0.1s in the schema, and refuse after endS clamps to the real duration if the window drops under the floor. Fixes #671.
This commit is contained in:
@@ -5,12 +5,19 @@ import { audioContentType, formatFfmpegSeconds, runMediaTool } from "../../lib/m
|
||||
import { InputValidationError } from "../../modality/contract.js";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
// Stream copy cannot cut below one codec frame (mp3 ~26ms, flac up to ~93ms);
|
||||
// smaller windows produce a container with zero audio frames.
|
||||
const MIN_TRIM_WINDOW_S = 0.1;
|
||||
|
||||
const settingsSchema = z
|
||||
.object({
|
||||
startS: z.number().finite().min(0).default(0),
|
||||
endS: z.number().finite().min(0.000001),
|
||||
})
|
||||
.refine((s) => s.endS > s.startS, { message: "End must be after start" });
|
||||
.refine((s) => s.endS > s.startS, { message: "End must be after start" })
|
||||
.refine((s) => s.endS - s.startS >= MIN_TRIM_WINDOW_S, {
|
||||
message: `Trim window must be at least ${MIN_TRIM_WINDOW_S} seconds`,
|
||||
});
|
||||
|
||||
export function registerTrimAudio(app: FastifyInstance) {
|
||||
createToolRoute(app, {
|
||||
@@ -34,6 +41,11 @@ export function registerTrimAudio(app: FastifyInstance) {
|
||||
throw new InputValidationError("Start is beyond the end of the audio");
|
||||
}
|
||||
const endS = Math.min(settings.endS, info.durationS);
|
||||
if (endS - settings.startS < MIN_TRIM_WINDOW_S) {
|
||||
throw new InputValidationError(
|
||||
`Trim window is shorter than ${MIN_TRIM_WINDOW_S} seconds after clamping to the audio duration`,
|
||||
);
|
||||
}
|
||||
// Fast seek with stream-copy for audio
|
||||
return [
|
||||
"-ss",
|
||||
|
||||
@@ -67,4 +67,17 @@ describe.skipIf(!ffmpegAvailable())("trim-audio (requires ffmpeg)", () => {
|
||||
const res = await runTool({ startS: 0.5, endS: 0.2 });
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it("rejects a sub-frame trim window instead of returning an empty file", async () => {
|
||||
const res = await runTool({ startS: 0, endS: 0.000001 });
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it("rejects when clamping endS to the duration leaves a sub-frame window", async () => {
|
||||
// tiny.mp3 is ~1.0-1.045s depending on the ffprobe build; endS clamps to
|
||||
// the duration, leaving well under 0.1s either way.
|
||||
const res = await runTool({ startS: 0.98, endS: 9 });
|
||||
expect(res.statusCode).toBe(422);
|
||||
expect(JSON.parse(res.body).details).toMatch(/Trim window/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,6 +41,14 @@ describe("media time settings schemas", () => {
|
||||
expect(parses("trim-video", { startS: 0, endS: Number.MIN_VALUE })).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects trim-audio windows shorter than the stream-copy floor", () => {
|
||||
// Stream copy cannot cut below one codec frame (mp3 ~26ms, flac up to
|
||||
// ~93ms), so windows under 0.1s ship a file with zero audio frames.
|
||||
expect(parses("trim-audio", { startS: 0, endS: 0.000001 })).toBe(false);
|
||||
expect(parses("trim-audio", { startS: 5, endS: 5.05 })).toBe(false);
|
||||
expect(parses("trim-audio", { startS: 0, endS: 0.5 })).toBe(true);
|
||||
});
|
||||
|
||||
it("requires at least one representable fade duration", () => {
|
||||
expect(parses("fade-audio", { fadeInS: Number.MIN_VALUE, fadeOutS: 0 })).toBe(false);
|
||||
expect(parses("fade-audio", { fadeInS: 0, fadeOutS: 0.000001 })).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user