mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
128 lines
4.0 KiB
TypeScript
128 lines
4.0 KiB
TypeScript
import { mkdtemp, rm } from "node:fs/promises";
|
|||
|
|
import { tmpdir } from "node:os";
|
||
|
|
import { join } from "node:path";
|
||
|
|
import { probeMedia, runFfmpeg } from "@snapotter/media-engine";
|
||
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||
|
|
import { runAiToolJob } from "../../../apps/api/src/jobs/ai-handlers.js";
|
||
|
|
import type { ToolJobData } from "../../../apps/api/src/jobs/types.js";
|
||
|
|
import type { ToolProcessCtx } from "../../../apps/api/src/routes/tool-factory.js";
|
||
|
|
|
||
|
|
const aiMocks = vi.hoisted(() => ({
|
||
|
|
removeBackground: vi.fn(),
|
||
|
|
transcribeAudio: vi.fn(),
|
||
|
|
}));
|
||
|
|
const mediaMocks = vi.hoisted(() => ({
|
||
|
|
probeMedia: vi.fn(),
|
||
|
|
runFfmpeg: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("@snapotter/ai", () => aiMocks);
|
||
|
|
vi.mock("@snapotter/media-engine", () => mediaMocks);
|
||
|
|
|
||
|
|
import { removeBackground, transcribeAudio } from "@snapotter/ai";
|
||
|
|
import "../../../apps/api/src/routes/tools/auto-subtitles.js";
|
||
|
|
import "../../../apps/api/src/routes/tools/background-replace.js";
|
||
|
|
import "../../../apps/api/src/routes/tools/blur-background.js";
|
||
|
|
import "../../../apps/api/src/routes/tools/transcribe-audio.js";
|
||
|
|
|
||
|
|
const INPUT = Buffer.from("fixture bytes");
|
||
|
|
const STOP = new Error("stop after dependency capture");
|
||
|
|
|
||
|
|
function job(toolId: string, settings: unknown, filename: string): ToolJobData {
|
||
|
|
return {
|
||
|
|
jobId: `job-${toolId}`,
|
||
|
|
toolId,
|
||
|
|
userId: null,
|
||
|
|
pool: "ai",
|
||
|
|
inputRefs: [`uploads/job-${toolId}/${filename}`],
|
||
|
|
filename,
|
||
|
|
settings,
|
||
|
|
kind: "ai-tool",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("installed AI job cancellation propagation", () => {
|
||
|
|
let scratchDir: string;
|
||
|
|
let controller: AbortController;
|
||
|
|
let ctx: ToolProcessCtx;
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
scratchDir = await mkdtemp(join(tmpdir(), "snapotter-installed-ai-cancel-"));
|
||
|
|
controller = new AbortController();
|
||
|
|
ctx = { signal: controller.signal, scratchDir, report: vi.fn() };
|
||
|
|
vi.mocked(probeMedia).mockResolvedValue({
|
||
|
|
container: "mov,mp4",
|
||
|
|
durationS: 7,
|
||
|
|
bitrateKbps: 64,
|
||
|
|
streams: [{ type: "audio", codec: "aac" }],
|
||
|
|
});
|
||
|
|
vi.mocked(runFfmpeg).mockResolvedValue("");
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
await rm(scratchDir, { recursive: true, force: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
it("passes ctx.signal from transcribe-audio to the transcription package", async () => {
|
||
|
|
vi.mocked(transcribeAudio).mockRejectedValue(STOP);
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
runAiToolJob(
|
||
|
|
job("transcribe-audio", { language: "auto", outputFormat: "txt" }, "speech.wav"),
|
||
|
|
INPUT,
|
||
|
|
ctx,
|
||
|
|
),
|
||
|
|
).rejects.toBe(STOP);
|
||
|
|
|
||
|
|
expect(transcribeAudio).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining("speech.wav"),
|
||
|
|
{ language: "auto", signal: controller.signal },
|
||
|
|
expect.any(Function),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("passes ctx.signal through every auto-subtitles subprocess boundary", async () => {
|
||
|
|
vi.mocked(transcribeAudio).mockRejectedValue(STOP);
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
runAiToolJob(
|
||
|
|
job("auto-subtitles", { language: "auto", format: "srt" }, "speech.mp4"),
|
||
|
|
INPUT,
|
||
|
|
ctx,
|
||
|
|
),
|
||
|
|
).rejects.toBe(STOP);
|
||
|
|
|
||
|
|
expect(probeMedia).toHaveBeenCalledWith(expect.stringContaining("speech.mp4"), {
|
||
|
|
signal: controller.signal,
|
||
|
|
});
|
||
|
|
expect(runFfmpeg).toHaveBeenCalledWith(
|
||
|
|
expect.arrayContaining(["-i", expect.stringContaining("speech.mp4")]),
|
||
|
|
{ timeoutMs: 10 * 60_000, signal: controller.signal },
|
||
|
|
);
|
||
|
|
expect(transcribeAudio).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining("audio-16k.wav"),
|
||
|
|
{ language: "auto", signal: controller.signal },
|
||
|
|
expect.any(Function),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.each([
|
||
|
|
["blur-background", { intensity: 75, feather: 3, format: "webp" }],
|
||
|
|
["background-replace", { color: "#ff0000" }],
|
||
|
|
])("passes ctx.signal from %s to background removal", async (toolId, settings) => {
|
||
|
|
vi.mocked(removeBackground).mockRejectedValue(STOP);
|
||
|
|
|
||
|
|
await expect(runAiToolJob(job(toolId, settings, "portrait.jpg"), INPUT, ctx)).rejects.toBe(
|
||
|
|
STOP,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(removeBackground).toHaveBeenCalledWith(
|
||
|
|
INPUT,
|
||
|
|
scratchDir,
|
||
|
|
{ signal: controller.signal },
|
||
|
|
expect.any(Function),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|