import { spawn, execFile } from "node:child_process"; import { promisify } from "node:util"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { audioJobStatusSchema, type AudioJobInput, } from "../../lib/video-canvas/smart-edit/audio-contract"; const python = () => process.env.AUDIO_PYTHON || "python3"; export async function audioHealth() { const { stdout } = await promisify(execFile)( python(), [path.join(__dirname, "run.py"), "--check"], { timeout: 30000, maxBuffer: 65536 } ); const value = JSON.parse(stdout); if (value.asr !== true || value.separation !== true) throw Error("音频模型未准备就绪"); return { asr: true, separation: true }; } export async function audioJob( plan: AudioJobInput, _tenant: string, dir: string, progress: (stage: string) => Promise ) { await progress( plan.operation === "transcribe" ? "算力服务器正在自动转写" : "算力服务器正在分离人声与背景声" ); await new Promise((resolve, reject) => { const child = spawn( python(), [ path.join(__dirname, "run.py"), plan.operation, path.join(dir, "input.wav"), dir, ], { stdio: ["ignore", "ignore", "pipe"], detached: true, env: { ...process.env, CUDA_VISIBLE_DEVICES: "", OMP_NUM_THREADS: "2" }, } ); let tail = "", timedOut = false; const timer = setTimeout(() => { timedOut = true; try { process.kill(-child.pid!, "SIGKILL"); } catch {} }, 600000); child.stderr.on("data", (b) => { tail = (tail + b.toString()).slice(-2000); }); child.once("error", () => { clearTimeout(timer); reject(Error("音频处理器无法启动")); }); child.once("close", (code) => { clearTimeout(timer); if (code === 0) resolve(); else { console.error( JSON.stringify({ event: "audio-inference-failed", code, timedOut, detail: tail.replace(/https?:\S+/g, "[url]"), }) ); reject( Error( timedOut ? "音频处理超时,请缩短素材后重试" : "音频模型处理失败,请检查素材或服务日志" ) ); } }); }); const result = JSON.parse( await readFile(path.join(dir, "result.json"), "utf8") ); return audioJobStatusSchema.shape.result.parse(result)!; }