"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, };