// Offline integration: real PySceneDetect/librosa and FFmpeg, no provider generation requests. const fs = require("node:fs"), path = require("node:path"), assert = require("node:assert/strict"), { execFileSync } = require("node:child_process"); (async () => { const root = process.cwd(), dir = path.resolve(".qa/smart-edit"); fs.mkdirSync(dir, { recursive: true }); execFileSync("python3", ["scripts/qa/fixtures/smart-edit.py"]); execFileSync("ffmpeg", [ "-v", "error", "-y", ...["red", "blue", "green"].flatMap((c) => [ "-f", "lavfi", "-i", `color=c=${c}:s=320x180:r=30:d=2`, ]), "-filter_complex", "[0:v][1:v][2:v]concat=n=3:v=1:a=0[v]", "-map", "[v]", "-c:v", "libx264", "-threads", "2", path.join(dir, "colors.mp4"), ]); execFileSync("ffmpeg", [ "-v", "error", "-y", "-i", path.join(dir, "colors.mp4"), "-i", path.join(dir, "speech.wav"), "-c:v", "copy", "-c:a", "aac", path.join(dir, "with-audio.mp4"), ]); const esbuild = require( path.resolve( "node_modules/.pnpm", fs.readdirSync("node_modules/.pnpm").find((n) => n.startsWith("esbuild@0.25.")), "node_modules/esbuild", ), ); await esbuild.build({ entryPoints: ["lib/video-canvas/smart-edit/render.ts", "lib/video-canvas/smart-edit/local.ts"], outdir: ".qa/smart-edit/build", bundle: true, platform: "node", format: "cjs", packages: "external", alias: { "@": root }, }); const { renderEditPlan } = require(path.join(dir, "build/render.js")), { probeMedia, pythonAnalysis, prepareAnalysis } = require(path.join(dir, "build/local.js")); const a = { id: "a", name: "无音轨", url: "https://example.com/colors.mp4", hash: "a".repeat(64), ...(await probeMedia(path.join(dir, "colors.mp4"))), subtitles: [{ start: 0, end: 1000000, text: "红色测试" }], }, b = { id: "b", name: "有音轨", url: "https://example.com/with-audio.mp4", hash: "b".repeat(64), ...(await probeMedia(path.join(dir, "with-audio.mp4"))), subtitles: [{ start: 3000000, end: 4000000, text: "第二句" }], }; const plan = { version: 1, title: "QA", assets: [a, b], ratio: "16:9", resolution: "720p", transition: 0, subtitles: false, warnings: [], clips: [ { id: "1", assetId: "a", sourceIn: 0, sourceOut: 2000000, speed: 1, gain: 1, label: "red" }, { id: "2", assetId: "b", sourceIn: 2000000, sourceOut: 6000000, speed: 2, gain: 1, label: "blue-green", }, ], }; const files = { a: path.join(dir, "colors.mp4"), b: path.join(dir, "with-audio.mp4") }; for (const transition of [0, 0.5]) { const out = path.join(dir, "render-" + transition); fs.mkdirSync(out, { recursive: true }); const result = await renderEditPlan( { ...plan, transition, subtitles: true, music: { url: "https://example.com/beats.wav", gain: 0.2, start: 0 }, }, files, path.join(dir, "beats.wav"), out, ); const probe = await probeMedia(result.file); assert.equal(probe.width, 1280); assert.equal(probe.height, 720); assert.equal(probe.hasAudio, true); assert.ok(Math.abs(probe.duration / 1e6 - (4 - transition)) < 0.15, JSON.stringify(probe)); const srt = fs.readFileSync(result.srt, "utf8"); assert.match(srt, /红色测试/); assert.match(srt, /第二句/); console.log( "PASS: actual FFmpeg " + transition + "s transition, speed 2x, silent source, music, subtitles, 720p and duration " + probe.duration / 1e6, ); } if (process.env.SMART_EDIT_PYTHON) { const prepared = await prepareAnalysis(files.b, "fixture", dir, true); const analysis = await pythonAnalysis( { version: 1, video: prepared.proxy, audio: prepared.audio, duration: 6000000 }, dir, ); assert.ok(analysis.scenes.length >= 3, JSON.stringify(analysis)); assert.ok(analysis.active.length >= 2, JSON.stringify(analysis)); const beats = await pythonAnalysis( { version: 1, audio: path.join(dir, "beats.wav"), beats: true }, dir, ); assert.ok(beats.beats.length > 10, JSON.stringify(beats)); console.log( "PASS: actual PySceneDetect found " + analysis.scenes.length + " scenes; RMS gate " + analysis.active.length + " active intervals; librosa " + beats.beats.length + " beats", ); } fs.writeFileSync(path.join(dir, "fixture-plan.json"), JSON.stringify(plan, null, 2)); })().catch((e) => { console.error(e); process.exitCode = 1; });