156 lines
4.7 KiB
JavaScript
156 lines
4.7 KiB
JavaScript
"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) };
|
||
}
|
||
};
|