ai-chatbot/scripts/qa/smart-edit-main-chain.cjs

215 lines
7.1 KiB
JavaScript

// Real local Next/auth/database/storage/task/model/render chain. Requires an isolated account.json and speech.mp4 in SMART_EDIT_QA_DIR. Never target production.
const fs = require("fs"),
crypto = require("crypto"),
path = require("path"),
assert = require("assert/strict");
const q = path.resolve(process.env.SMART_EDIT_QA_DIR || ".qa/smart-edit/main-chain"),
base = process.env.SMART_EDIT_QA_URL || "http://127.0.0.1:3025";
assert(
["127.0.0.1", "localhost"].includes(new URL(base).hostname),
"Use an isolated local server only",
);
fs.mkdirSync(q, { recursive: true });
let cookies = {};
let report = [];
async function request(p, o = {}) {
const r = await fetch(base + p, {
...o,
headers: {
...o.headers,
Cookie: Object.entries(cookies)
.map(([k, v]) => k + "=" + v)
.join("; "),
},
redirect: "manual",
signal: AbortSignal.timeout(180000),
});
for (const c of r.headers.getSetCookie()) {
let t = c.split(";")[0],
i = t.indexOf("=");
cookies[t.slice(0, i)] = t.slice(i + 1);
}
let raw = await r.text();
let data;
try {
data = JSON.parse(raw);
} catch {
data = raw.slice(0, 300);
}
return { status: r.status, data };
}
const post = (p, b) =>
request(p, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(b),
});
(async () => {
const a = JSON.parse(fs.readFileSync(path.join(q, "account.json")));
let csrf = await request("/api/auth/csrf");
assert.equal(csrf.status, 200, JSON.stringify(csrf));
await request("/api/auth/callback/credentials", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
},
body: new URLSearchParams({
csrfToken: csrf.data.csrfToken,
email: a.email,
password: a.password,
callbackUrl: base + "/video-canvas",
}),
});
let session = await request("/api/auth/session");
assert.equal(session.data.user?.id, a.id, "real login");
fs.writeFileSync(path.join(q, "cookies.json"), JSON.stringify(cookies), { mode: 0o600 });
console.log("LOGIN_OK");
let cap = await request("/api/canvas/smart-edit/capabilities");
assert.equal(cap.data.capabilities.asr, true);
assert.equal(cap.data.capabilities.separation, true);
console.log("CAPABILITIES_OK");
let project = await post("/api/canvas", {
title: "智能剪辑本地全链路验收0918",
workspaceType: "video",
});
assert.equal(project.status, 200, JSON.stringify(project));
let id = project.data.data.id;
assert(
Math.abs(Date.now() - new Date(project.data.data.createdAt).getTime()) < 60000,
"new canvas timestamp must agree with task timestamps",
);
fs.writeFileSync(path.join(q, "project-id.txt"), id);
let form = new FormData();
form.append(
"file",
new Blob([fs.readFileSync(path.join(q, "speech.mp4"))], { type: "video/mp4" }),
"测试中文视频.mp4",
);
let uploaded = await request("/api/canvas/media/upload", { method: "POST", body: form });
assert.equal(uploaded.status, 200, JSON.stringify(uploaded));
let asset = {
id: "qa-video",
name: "中文实测视频",
url: uploaded.data.data.url,
subtitles: [],
};
console.log("UPLOAD_OK");
const node = {
id: "qa-edit",
title: "智能剪辑全链路验收",
x: 200,
y: 200,
width: 440,
height: 350,
kind: "text",
inputType: "text",
draftValues: { action: "SMART_EDIT_WORKFLOW", nodeType: "smart-edit" },
};
async function task(params, target = node) {
let req = {
requestId: crypto.randomUUID(),
projectId: id,
kind: "smart-edit",
node: target,
params,
edges: [],
};
let start = Date.now();
let r = await post("/api/canvas/generation-tasks", req);
assert.equal(r.status, 202, JSON.stringify(r));
let repeat = await post("/api/canvas/generation-tasks", req);
assert.equal(repeat.data.task.id, r.data.task.id, "idempotent task");
while (Date.now() - start < 900000) {
await new Promise((r) => setTimeout(r, 1500));
let all = await request("/api/canvas/generation-tasks?projectId=" + id);
assert.equal(all.status, 200);
let t = all.data.tasks.find((t) => t.requestId === req.requestId);
if (t && ["succeeded", "failed", "interrupted"].includes(t.status)) {
console.log(
params.operation,
t.status,
t.error || "",
((Date.now() - start) / 1000).toFixed(1) + "s",
);
report.push({
operation: params.operation,
status: t.status,
seconds: (Date.now() - start) / 1000,
});
assert.equal(t.status, "succeeded", t.error);
fs.writeFileSync(
path.join(q, params.operation + "-task.json"),
JSON.stringify(t, null, 2),
);
return t.result;
}
}
throw Error("timeout " + params.operation);
}
let result = await task({
operation: "plan",
mode: "sequence",
assets: [asset],
targetDuration: 300,
semantic: false,
resolution: "480p",
});
let plan = result.plans[0];
result = await task({ operation: "separate", plan, clipId: plan.clips[0].id });
plan = result.plans[0];
assert.equal(plan.audioClips.length, 2);
assert.equal(plan.clips[0].audioDetached, true);
assert.equal(plan.assets.length, 3);
result = await task({ operation: "transcribe", plan });
plan = result.plans[0];
assert(plan.captions.some((s) => s.text.includes("智能剪辑")));
result = await task({
operation: "separate",
plan,
clipId: plan.audioClips[0].id,
track: "audio",
});
plan = result.plans[0];
assert.equal(plan.audioClips.length, 3, "independent audio separation");
for (const keep of ["vocals", "background"]) {
const processed = await task({
operation: "separate",
plan,
clipId: plan.audioClips[0].id,
track: "audio",
keep,
});
const next = processed.plans[0];
assert.equal(next.audioClips.length, plan.audioClips.length);
const added = next.audioClips.filter(
(c) => !plan.audioClips.some((old) => old.id === c.id),
);
assert.equal(added.length, 1);
assert(
next.assets
.find((a) => a.id === added[0].assetId)
.name.endsWith(keep === "vocals" ? "人声" : "背景声"),
);
}
plan.captions[0].style = { ...plan.captions[0].style, fontFamily: "catalog-0006-font" };
let render = await task(
{ operation: "render", plan, sourceNodeId: "qa-edit" },
{ ...node, id: "qa-render", x: 800, title: "本地验收成片", kind: "media", inputType: "video" },
);
let media = await fetch(render.url);
assert.equal(media.status, 200);
fs.writeFileSync(path.join(q, "main-result.mp4"), Buffer.from(await media.arrayBuffer()));
let srt = await fetch(render.subtitleUrl);
assert.equal(srt.status, 200);
assert((await srt.text()).includes("智能剪辑"));
fs.writeFileSync(
path.join(q, "main-result.json"),
JSON.stringify({ projectId: id, plan, render, report }, null, 2),
);
console.log("MAIN_CHAIN_OK", JSON.stringify(report));
})().catch((e) => {
console.error(e);
process.exitCode = 1;
});