215 lines
7.1 KiB
JavaScript
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;
|
|
});
|