"use strict"; /** * 口播视频生成:调用 Gradio 数字人 API(对齐 Electron compshare:digitalHumanProcess) */ const fs = require("fs"); const path = require("path"); const os = require("os"); const { spawn } = require("child_process"); const desktopConfig = require("./desktop_config.js"); const TEMP_DIR = path.join(os.tmpdir(), "aiclient-digital-human"); function downloadFile(url, destPath) { return new Promise((resolve, reject) => { const client = url.startsWith("https") ? require("https") : require("http"); const file = fs.createWriteStream(destPath); client .get(url, (res) => { if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { file.close(); return downloadFile(res.headers.location, destPath).then(resolve).catch(reject); } if (res.statusCode !== 200) { reject(new Error(`下载失败 HTTP ${res.statusCode}`)); return; } res.pipe(file); file.on("finish", () => file.close(() => resolve(destPath))); }) .on("error", reject); }); } async function ensureLocalVideo(videoPath) { const raw = String(videoPath || "").trim(); if (!raw) throw new Error("视频路径为空"); if (/^https?:\/\//i.test(raw)) { fs.mkdirSync(TEMP_DIR, { recursive: true }); const ext = raw.includes(".mp4") ? ".mp4" : ".mp4"; const dest = path.join(TEMP_DIR, `remote_${Date.now()}${ext}`); await downloadFile(raw, dest); return dest; } if (raw.startsWith("file://")) { return raw.replace(/^file:\/\//, ""); } return raw; } function runPythonDigitalHuman(apiUrl, audioFile, videoFile) { const scriptPath = path.join(__dirname, "digital_human_process.py"); const py = process.env.AICLIENT_PYTHON || (process.platform === "win32" ? "python" : "python3"); return new Promise((resolve) => { const child = spawn(py, [scriptPath, apiUrl, audioFile, videoFile], { cwd: __dirname, env: process.env, windowsHide: true, }); let stdout = ""; let stderr = ""; child.stdout.on("data", (d) => { stdout += d.toString(); }); child.stderr.on("data", (d) => { stderr += d.toString(); }); child.on("error", (err) => { resolve({ success: false, error: err.message }); }); const timer = setTimeout(() => { child.kill(); resolve({ success: false, error: "数字人处理超时(10 分钟)" }); }, 600000); child.on("close", (code) => { clearTimeout(timer); if (code !== 0 || !stdout.trim()) { resolve({ success: false, error: stderr.trim() || `数字人脚本退出码 ${code}`, }); return; } try { const lines = stdout .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); const jsonLine = [...lines] .reverse() .find((l) => l.startsWith("{") && l.endsWith("}")); if (!jsonLine) { resolve({ success: false, error: `解析结果失败: ${stdout.slice(0, 200)}`, }); return; } resolve(JSON.parse(jsonLine)); } catch (e) { resolve({ success: false, error: `解析 JSON 失败: ${e.message}`, }); } }); }); } globalThis.__nodejsMain = async function (params) { const p = params || {}; const apiUrl = desktopConfig.get("DIGITAL_HUMAN_API_URL") || desktopConfig.get("VIDEO_GEN_API_URL") || desktopConfig.get("DIGITAL_HUMAN_GRADIO_URL"); if (!apiUrl) { return { success: false, error: "缺少 DIGITAL_HUMAN_API_URL(或 VIDEO_GEN_API_URL),请在配置管理中设置数字人 Gradio 服务地址", }; } const audioFile = String(p.audioPath || "").trim(); const videoFile = String(p.avatarVideoPath || "").trim(); if (!audioFile) { return { success: false, error: "缺少音频文件,请先生成语音" }; } if (!videoFile) { return { success: false, error: "缺少形象视频,请先选择形象" }; } if (!fs.existsSync(audioFile)) { return { success: false, error: `音频文件不存在: ${audioFile}` }; } if (!fs.existsSync(videoFile)) { return { success: false, error: `形象视频不存在: ${videoFile}` }; } const result = await runPythonDigitalHuman(apiUrl, audioFile, videoFile); if (!result?.success || !result?.videoPath) { return { success: false, error: String(result?.error || "口播视频生成失败"), }; } try { const localPath = await ensureLocalVideo(result.videoPath); return { success: true, videoPath: localPath }; } catch (e) { return { success: false, error: e.message || String(e) }; } };