ai-chatbot/scripts/qa/video-render-service.cjs

154 lines
5.3 KiB
JavaScript

// Isolated real HTTP + real FFmpeg + process restart. No paid model requests.
const fs = require("node:fs"),
path = require("node:path"),
crypto = require("node:crypto"),
assert = require("node:assert/strict");
const { spawn, execFileSync } = require("node:child_process");
const sha = (s) => crypto.createHash("sha256").update(s).digest("hex");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
(async () => {
const dir = path.resolve(".qa/render-service");
fs.mkdirSync(dir, { recursive: true });
const data = fs.mkdtempSync(path.join(dir, "run-")),
token = crypto.randomBytes(32).toString("hex"),
base = "http://127.0.0.1:19340";
const tenant = sha("qa-user"),
cache = path.join(data, "cache", sha(tenant).slice(0, 20));
fs.mkdirSync(cache, { recursive: true });
const plan = JSON.parse(fs.readFileSync(".qa/smart-edit/fixture-plan.json"));
for (const [i, a] of plan.assets.entries()) {
const file = path.resolve(".qa/smart-edit", i ? "with-audio.mp4" : "colors.mp4"),
bytes = fs.readFileSync(file);
a.hash = sha(bytes);
fs.writeFileSync(path.join(cache, a.hash), bytes);
fs.writeFileSync(
path.join(cache, sha(a.url) + ".json"),
JSON.stringify({ hash: a.hash, created: Date.now() }),
);
}
plan.transition = 0.5;
plan.subtitles = true;
plan.clips[1].gain = 2;
plan.captions = [
{
id: "font-http",
start: 0,
end: 2e6,
text: "API 字体验证",
style: {
fontFamily: "noto-serif-sc",
referenceHeight: 1080,
fontSize: 56,
color: "#ffffff",
outlineColor: "#ffffff",
outlineWidth: 0,
opacity: 1,
spacing: 0,
},
},
];
let child;
const start = async () => {
child = spawn(process.execPath, ["services/video-render/dist/server.cjs"], {
env: {
...process.env,
RENDER_PORT: "19340",
RENDER_DATA_DIR: data,
SMART_EDIT_CACHE_DIR: path.join(data, "cache"),
RENDER_API_TOKEN: token,
},
detached: true,
stdio: ["ignore", "pipe", "pipe"],
});
child.stdout.on("data", (b) => fs.appendFileSync(path.join(data, "log"), b));
child.stderr.on("data", (b) => fs.appendFileSync(path.join(data, "log"), b));
for (let i = 0; i < 60; i++) {
try {
if (
(await fetch(base + "/health", { headers: { authorization: "Bearer " + token } })).ok
)
return;
} catch {}
await sleep(100);
}
throw Error("service did not start");
};
const stop = async (signal = "SIGTERM") => {
if (child && child.exitCode === null) {
process.kill(-child.pid, signal);
await new Promise((r) => child.once("exit", r));
}
child = null;
};
const request = async (route, body) => {
const r = await fetch(base + route, {
method: body ? "POST" : "GET",
headers: { authorization: "Bearer " + token, "content-type": "application/json" },
body: body && JSON.stringify(body),
});
return { status: r.status, body: await r.json() };
};
try {
await start();
assert.equal((await fetch(base + "/health")).status, 401);
const id = sha("actual-render-restart"),
input = { id, tenant, plan };
assert.equal((await request("/v1/renders", input)).status, 202);
for (let i = 0; i < 100; i++) {
if (
(await request("/v1/renders/" + id)).body.status === "running" &&
JSON.parse(fs.readFileSync(path.join(data, id + ".json"))).status === "running"
)
break;
await sleep(50);
}
await stop("SIGKILL");
await start();
assert.equal((await request("/v1/renders", input)).status, 200);
assert.equal(
(await request("/v1/renders", { ...input, plan: { ...plan, title: "changed" } })).status,
409,
);
let state;
for (let i = 0; i < 300; i++) {
state = (await request("/v1/renders/" + id)).body;
if (["succeeded", "failed"].includes(state.status)) break;
await sleep(200);
}
assert.equal(state.status, "succeeded", JSON.stringify(state));
assert.equal(state.attempts, 2);
assert.equal((await fetch(base + "/v1/renders/" + id + "/video")).status, 401);
const result = await fetch(base + "/v1/renders/" + id + "/video", {
headers: { authorization: "Bearer " + token },
});
const out = path.join(data, "download.mp4");
fs.writeFileSync(out, Buffer.from(await result.arrayBuffer()));
const probe = JSON.parse(
execFileSync("ffprobe", [
"-v",
"error",
"-show_format",
"-show_streams",
"-of",
"json",
out,
]),
);
assert.ok(Math.abs(Number(probe.format.duration) - 3.5) < 0.15);
assert.equal(probe.streams[0].width, 1280);
assert.equal((await request("/v1/renders", input)).body.attempts, 2);
await stop();
await start();
assert.equal((await request("/v1/renders/" + id)).body.status, "succeeded");
console.log(
"PASS: authenticated API, real FFmpeg, persisted queue, kill/restart recovery, idempotency, conflicting request rejection, completed result survives restart",
);
fs.writeFileSync(path.join(dir, "last-run.json"), JSON.stringify({ data, result: state }));
} finally {
await stop();
}
})().catch((e) => {
console.error(e);
process.exitCode = 1;
});