This commit is contained in:
fengchuanhn@gmail.com
2026-05-21 00:19:11 +08:00
parent 1325ff92e5
commit 7f57dfa9ed
31 changed files with 10437 additions and 12 deletions

View File

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