ai-chatbot/tests/routes/video-audio-separation-tools.test.ts

119 lines
4.0 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
defaultVideoCanvasAudioSeparationConfig,
getVideoAudioSplitResultCopy,
hasMp4AudioTrack,
hasVideoAudioTrack,
hasWebmAudioTrack,
normalizeVideoCanvasAudioSeparationConfig,
separateVideoAudioChannels,
} from "../../lib/video-canvas/video-audio-separation";
function mp4Box(type: string, payload: Uint8Array) {
const output = new Uint8Array(8 + payload.length);
new DataView(output.buffer).setUint32(0, output.length);
Array.from(type).forEach((character, index) => {
output[4 + index] = character.charCodeAt(0);
});
output.set(payload, 8);
return output;
}
function joinBytes(...parts: Uint8Array[]) {
const output = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
let offset = 0;
for (const part of parts) {
output.set(part, offset);
offset += part.length;
}
return output;
}
function ebmlElement(id: number[], payload: Uint8Array) {
if (payload.length >= 127) throw new Error("Test EBML payload is too large");
return joinBytes(new Uint8Array(id), new Uint8Array([0x80 | payload.length]), payload);
}
test.describe("video audio separation tools", () => {
test("normalizes configurable local separation limits", () => {
expect(normalizeVideoCanvasAudioSeparationConfig({
backgroundGain: 9,
frameRate: 100,
model: " center-channel-custom ",
voiceGain: -1,
})).toMatchObject({
backgroundGain: 4,
frameRate: 60,
model: "center-channel-custom",
voiceGain: 0,
});
});
test("extracts center and side stereo signals for local fallback modes", () => {
const left = new Float32Array([1, 0.5]);
const right = new Float32Array([1, -0.5]);
const voice = separateVideoAudioChannels(
[left, right],
"voice",
defaultVideoCanvasAudioSeparationConfig,
);
const background = separateVideoAudioChannels(
[left, right],
"background",
defaultVideoCanvasAudioSeparationConfig,
);
expect(Array.from(voice[0])).toEqual([1, 0]);
expect(Array.from(voice[1])).toEqual([1, 0]);
expect(background[0][0]).toBe(0);
expect(background[0][1]).toBeCloseTo(0.6);
expect(background[1][1]).toBeCloseTo(-0.6);
});
test("matches competitor result suffixes", () => {
expect(getVideoAudioSplitResultCopy("audio-video").audioSuffix).toBe("音轨");
expect(getVideoAudioSplitResultCopy("voice").audioSuffix).toBe("人声");
expect(getVideoAudioSplitResultCopy("background").audioSuffix).toBe("背景音");
});
test("reads the structured MP4 audio handler instead of guessing by filename", () => {
const handlerPayload = new Uint8Array(12);
Array.from("soun").forEach((character, index) => {
handlerPayload[8 + index] = character.charCodeAt(0);
});
const audioFile = mp4Box("moov", mp4Box("trak", mp4Box("mdia", mp4Box("hdlr", handlerPayload))));
const videoHandler = new Uint8Array(handlerPayload);
Array.from("vide").forEach((character, index) => {
videoHandler[8 + index] = character.charCodeAt(0);
});
const videoOnlyFile = mp4Box("moov", mp4Box("trak", mp4Box("mdia", mp4Box("hdlr", videoHandler))));
expect(hasMp4AudioTrack(audioFile.buffer)).toBe(true);
expect(hasMp4AudioTrack(videoOnlyFile.buffer)).toBe(false);
});
test("reads structured WebM TrackType elements for generated video results", () => {
const audioEntry = ebmlElement(
[0xae],
ebmlElement([0x83], new Uint8Array([2])),
);
const videoEntry = ebmlElement(
[0xae],
ebmlElement([0x83], new Uint8Array([1])),
);
const audioWebm = ebmlElement(
[0x18, 0x53, 0x80, 0x67],
ebmlElement([0x16, 0x54, 0xae, 0x6b], audioEntry),
);
const videoOnlyWebm = ebmlElement(
[0x18, 0x53, 0x80, 0x67],
ebmlElement([0x16, 0x54, 0xae, 0x6b], videoEntry),
);
expect(hasWebmAudioTrack(audioWebm.buffer)).toBe(true);
expect(hasWebmAudioTrack(videoOnlyWebm.buffer)).toBe(false);
expect(hasVideoAudioTrack(audioWebm.buffer)).toBe(true);
});
});