import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdir, rm, stat } from "node:fs/promises"; import { resolve } from "node:path"; import { dbGet, dbRun, withTransaction } from "../server/db.mjs"; import { registerJobArtifacts } from "../server/media-artifacts.mjs"; const projectRoot = resolve(import.meta.dirname, ".."); const orgId = "org-studio-lab"; const workspaceId = "ws-local-aidrama"; const projectId = "thunder-mouth"; const shotId = "shot-01"; const shot = dbGet("SELECT * FROM shots WHERE id = ?", [shotId]); assert.ok(shot, "smoke shot must exist"); const nextShot = dbGet("SELECT id, first_frame_path FROM shots WHERE episode_id = ? AND shot_number = ?", [shot.episode_id, Number(shot.shot_number) + 1]); const originalLastFrame = shot.last_frame_path; const originalNextFirstFrame = nextShot?.first_frame_path || ""; const jobId = `smoke-media-${Date.now()}`; const relative = `storage/jobs/${jobId}/output.mp4`; const absolute = resolve(projectRoot, relative); const context = { organization: { id: orgId }, workspace: { id: workspaceId }, project: { id: projectId }, user: { id: "u-owner" } }; try { await mkdir(resolve(projectRoot, `storage/jobs/${jobId}`), { recursive: true }); execFileSync("ffmpeg", ["-y", "-v", "error", "-f", "lavfi", "-i", "color=c=0x1f5f8b:s=320x568:d=1", "-c:v", "libx264", "-pix_fmt", "yuv420p", absolute], { stdio: "pipe" }); const timestamp = new Date().toISOString(); dbRun("INSERT INTO generation_jobs(id, organization_id, workspace_id, project_id, episode_id, shot_id, kind, adapter_id, status, output_path, qa_status, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, '视频片段', 'owned-i2v', 'completed', ?, 'wait', 'u-owner', ?, ?)", [jobId, orgId, workspaceId, projectId, shot.episode_id, shotId, relative, timestamp, timestamp]); const artifacts = await registerJobArtifacts(context, { id: jobId, episode_id: shot.episode_id, shot_id: shotId, kind: "视频片段", output_path: relative, created_by: "u-owner" }, { outputPath: relative }); const artifact = artifacts.find((item) => item.kind === "video"); assert.equal(artifact?.status, "inspected", "视频文件必须通过 FFprobe 检查"); assert.equal((artifact.sha256 || "").length, 64, "视频证据必须登记 SHA-256"); assert.ok(artifact.last_frame_path, "视频证据必须提取实际末帧路径"); await stat(resolve(projectRoot, artifact.last_frame_path)); assert.equal(dbGet("SELECT last_frame_path FROM shots WHERE id = ?", [shotId]).last_frame_path, artifact.last_frame_path, "镜头必须继承实际末帧"); if (nextShot) assert.equal(dbGet("SELECT first_frame_path FROM shots WHERE id = ?", [nextShot.id]).first_frame_path, artifact.last_frame_path, "下一镜头必须继承上一镜头实际末帧"); console.log(`media evidence smoke passed: ${artifact.path} -> ${artifact.last_frame_path}`); } finally { withTransaction(() => { dbRun("DELETE FROM media_artifacts WHERE job_id = ?", [jobId]); dbRun("DELETE FROM generation_jobs WHERE id = ?", [jobId]); dbRun("UPDATE shots SET last_frame_path = ?, updated_at = ? WHERE id = ?", [originalLastFrame, new Date().toISOString(), shotId]); if (nextShot) dbRun("UPDATE shots SET first_frame_path = ?, updated_at = ? WHERE id = ?", [originalNextFirstFrame, new Date().toISOString(), nextShot.id]); }); await rm(resolve(projectRoot, `storage/jobs/${jobId}`), { recursive: true, force: true }); }