214 lines
5.9 KiB
JavaScript
214 lines
5.9 KiB
JavaScript
"use strict";
|
||
|
||
/**
|
||
* 调用 bundled Python 封面脚本(对齐 Electron ipAgent:generateVideoCover)
|
||
*/
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const { spawnSync } = require("child_process");
|
||
|
||
const pipelineNative = require("./pipeline_native.js");
|
||
|
||
function locatePython() {
|
||
const env = process.env.AICLIENT_PYTHON_PATH;
|
||
if (env && fs.existsSync(env)) return env;
|
||
return null;
|
||
}
|
||
|
||
function locateCoverScriptsDir() {
|
||
const env = process.env.AICLIENT_COVER_SCRIPTS_DIR;
|
||
if (env && fs.existsSync(path.join(env, "advanced_cover_generator.py"))) {
|
||
return env;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function normalizePathForPython(filePath) {
|
||
if (process.platform === "win32") {
|
||
return String(filePath).replace(/\\/g, "/");
|
||
}
|
||
return String(filePath);
|
||
}
|
||
|
||
function buildPythonEnv() {
|
||
const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
|
||
const ffmpeg = pipelineNative.locateFfmpeg();
|
||
if (ffmpeg && fs.existsSync(ffmpeg)) {
|
||
const ffmpegDir = path.dirname(ffmpeg);
|
||
const sep = process.platform === "win32" ? ";" : ":";
|
||
env.PATH = `${ffmpegDir}${sep}${env.PATH || ""}`;
|
||
env.AICLIENT_FFMPEG_PATH = ffmpeg;
|
||
}
|
||
const scriptsDir = locateCoverScriptsDir();
|
||
console.log("scriptsDir", scriptsDir);
|
||
if (scriptsDir) {
|
||
env.AICLIENT_COVER_SCRIPTS_DIR = scriptsDir;
|
||
env.APP_ROOT = path.dirname(scriptsDir);
|
||
const fontsDir = path.join(scriptsDir, "fonts");
|
||
if (fs.existsSync(fontsDir)) {
|
||
env.AICLIENT_COVER_FONTS_DIR = fontsDir;
|
||
}
|
||
}
|
||
return env;
|
||
}
|
||
|
||
/** 与 Electron main 中 isNewTemplate 判定一致 */
|
||
function isAdvancedCoverConfig(config) {
|
||
if (!config || typeof config !== "object") return false;
|
||
return (
|
||
config.blurBackground !== undefined ||
|
||
config.extractPerson !== undefined ||
|
||
config.personOutlineColor !== undefined ||
|
||
config.personOutlineWidth !== undefined ||
|
||
config.maskImagePath !== undefined ||
|
||
Boolean(config.titleFontFamily) ||
|
||
config.titleBackgroundEnabled !== undefined ||
|
||
config.personSize !== undefined ||
|
||
config.backgroundBlurEnabled !== undefined
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param {{ videoPath: string, titleText: string, outputPath: string, config: object, onProgress?: (s: string) => void }} opts
|
||
*/
|
||
function runAdvancedCoverGenerator(opts) {
|
||
const python = locatePython();
|
||
const scriptsDir = locateCoverScriptsDir();
|
||
if (!python) {
|
||
return { success: false, error: "未找到 Python 运行时,请检查 resources-bundles/python-runtimebackup" };
|
||
}
|
||
if (!scriptsDir) {
|
||
return { success: false, error: "未找到封面脚本目录(cover-python)" };
|
||
}
|
||
|
||
const script = path.join(scriptsDir, "advanced_cover_generator.py");
|
||
if (!fs.existsSync(script)) {
|
||
return { success: false, error: `缺少脚本: ${script}` };
|
||
}
|
||
|
||
const configJson = JSON.stringify({
|
||
...opts.config,
|
||
output: opts.outputPath,
|
||
customVideoPath: opts.config.customVideoPath || undefined,
|
||
});
|
||
|
||
opts.onProgress?.("正在使用高级封面生成器…");
|
||
|
||
const args = [
|
||
script,
|
||
"--video",
|
||
normalizePathForPython(opts.videoPath),
|
||
"--title",
|
||
opts.titleText,
|
||
"--output",
|
||
normalizePathForPython(opts.outputPath),
|
||
"--config",
|
||
configJson,
|
||
];
|
||
|
||
const r = spawnSync(python, args, {
|
||
encoding: "utf8",
|
||
windowsHide: true,
|
||
cwd: scriptsDir,
|
||
env: buildPythonEnv(),
|
||
maxBuffer: 20 * 1024 * 1024,
|
||
});
|
||
|
||
if (r.error) {
|
||
return { success: false, error: r.error.message || String(r.error) };
|
||
}
|
||
|
||
let previewImages = [];
|
||
const stdout = (r.stdout || "").trim();
|
||
if (stdout) {
|
||
try {
|
||
const lastLine = stdout.split("\n").filter(Boolean).pop();
|
||
const parsed = JSON.parse(lastLine || stdout);
|
||
if (parsed.previewImages) previewImages = parsed.previewImages;
|
||
if (parsed.success === false) {
|
||
return {
|
||
success: false,
|
||
error: parsed.error || parsed.message || "高级封面生成失败",
|
||
stderr: r.stderr,
|
||
};
|
||
}
|
||
} catch {
|
||
/* 非 JSON 时仅检查文件 */
|
||
}
|
||
}
|
||
|
||
if (r.status !== 0 || !fs.existsSync(opts.outputPath)) {
|
||
const errMsg =
|
||
(r.stderr || "").trim() ||
|
||
stdout ||
|
||
`Python 退出码 ${r.status ?? "unknown"}`;
|
||
return { success: false, error: errMsg };
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
coverPath: opts.outputPath,
|
||
previewImages,
|
||
message: "高级封面生成完成",
|
||
};
|
||
}
|
||
|
||
/**
|
||
* PIL 中等复杂度封面(cover_generator.py)
|
||
* @param {{ videoPath: string, titleText: string, outputPath: string, config: object, timestamp?: number, onProgress?: (s: string) => void }} opts
|
||
*/
|
||
function runBasicCoverGenerator(opts) {
|
||
const python = locatePython();
|
||
const scriptsDir = locateCoverScriptsDir();
|
||
if (!python || !scriptsDir) {
|
||
return { success: false, error: "未找到 Python 或封面脚本" };
|
||
}
|
||
|
||
const script = path.join(scriptsDir, "cover_generator.py");
|
||
if (!fs.existsSync(script)) {
|
||
return { success: false, error: `缺少脚本: ${script}` };
|
||
}
|
||
|
||
opts.onProgress?.("正在使用 PIL 封面生成器…");
|
||
|
||
const configJson = JSON.stringify(opts.config || {});
|
||
const args = [
|
||
script,
|
||
"--video",
|
||
normalizePathForPython(opts.videoPath),
|
||
"--title",
|
||
opts.titleText,
|
||
"--output",
|
||
normalizePathForPython(opts.outputPath),
|
||
"--config",
|
||
configJson,
|
||
"--timestamp",
|
||
String(opts.timestamp ?? 3),
|
||
];
|
||
|
||
const r = spawnSync(python, args, {
|
||
encoding: "utf8",
|
||
windowsHide: true,
|
||
cwd: scriptsDir,
|
||
env: buildPythonEnv(),
|
||
maxBuffer: 10 * 1024 * 1024,
|
||
});
|
||
|
||
if (r.status === 0 && fs.existsSync(opts.outputPath)) {
|
||
return { success: true, coverPath: opts.outputPath, message: "封面生成完成" };
|
||
}
|
||
|
||
return {
|
||
success: false,
|
||
error: (r.stderr || r.stdout || "").trim() || `Python 退出码 ${r.status}`,
|
||
};
|
||
}
|
||
|
||
module.exports = {
|
||
isAdvancedCoverConfig,
|
||
runAdvancedCoverGenerator,
|
||
runBasicCoverGenerator,
|
||
locatePython,
|
||
locateCoverScriptsDir,
|
||
};
|