Files
yaoyaoai/scripts/nodejs/cover_python_runner.js
fengchuanhn@gmail.com 7f57dfa9ed 11
2026-05-21 00:19:11 +08:00

204 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
};