// 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; });