This commit is contained in:
fengchuanhn@gmail.com
2026-05-18 18:40:03 +08:00
parent 09f37e3206
commit 1325ff92e5
5 changed files with 865 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
"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) };
}
};