199 lines
7.0 KiB
TypeScript
199 lines
7.0 KiB
TypeScript
import { exec, spawn, type ChildProcessWithoutNullStreams } from "child_process";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
import { promisify } from "util";
|
|
import { VideoSubtitleCoverGeneratorModelConfig } from "../../src/types/VideoSubtitleCoverGenerator";
|
|
import { getPythonPath } from "../lib/python-util";
|
|
import { getPythonScriptPath, getFFmpegExecutablePath, getRuntimeBundleRoot, resourceExists } from "../lib/resource-path";
|
|
import { Log } from "../mapi/log/main";
|
|
import { buildRuntimeProcessEnv } from "../mapi/shell";
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
export class VideoSubtitleCoverGenerator {
|
|
type = "buildIn";
|
|
ServerApi: any;
|
|
ServerInfo: any;
|
|
private currentProcess: ChildProcessWithoutNullStreams | null = null;
|
|
|
|
private async stopCurrentProcess() {
|
|
if (!this.currentProcess?.pid) {
|
|
this.currentProcess = null;
|
|
return;
|
|
}
|
|
|
|
const currentPid = this.currentProcess.pid;
|
|
const processToStop = this.currentProcess;
|
|
this.currentProcess = null;
|
|
|
|
try {
|
|
if (process.platform === "win32") {
|
|
await execAsync(`taskkill /F /PID ${currentPid} /T`);
|
|
} else {
|
|
processToStop.kill("SIGKILL");
|
|
}
|
|
} catch (error: any) {
|
|
Log.warn("VideoSubtitleCoverGenerator", `Failed to stop python process ${currentPid}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async init() {
|
|
// 初始化
|
|
}
|
|
|
|
async start() {
|
|
// 启动服务
|
|
return { code: 0, msg: "VideoSubtitleCoverGenerator服务启动成功" };
|
|
}
|
|
|
|
async ping() {
|
|
// 检查服务状态
|
|
return { code: 0, msg: "pong" };
|
|
}
|
|
|
|
async stop() {
|
|
await this.stopCurrentProcess();
|
|
return { code: 0, msg: "服务已停止" };
|
|
}
|
|
|
|
async cancel() {
|
|
await this.stopCurrentProcess();
|
|
return { code: 0, msg: "任务已取消" };
|
|
}
|
|
|
|
async config() {
|
|
// 返回配置信息
|
|
return {
|
|
code: 0,
|
|
config: {
|
|
name: "VideoSubtitleCoverGenerator",
|
|
displayName: "字幕封面生成器",
|
|
description: "一键生成带字幕的视频和封面,支持多种样式模板",
|
|
version: "1.0.0",
|
|
functions: ["VideoSubtitleCoverGenerator"],
|
|
models: []
|
|
}
|
|
};
|
|
}
|
|
|
|
async VideoSubtitleCoverGenerator(data: VideoSubtitleCoverGeneratorModelConfig, option: any = {}) {
|
|
try {
|
|
// 创建临时配置文件
|
|
const configPath = path.join(process.cwd(), "temp_config.json");
|
|
const config = {
|
|
modelConfig: data,
|
|
...option
|
|
};
|
|
|
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
|
|
// 调用Python脚本
|
|
// ⚠️ 只使用程序自带的 Python,不依赖系统环境
|
|
const pythonScript = getPythonScriptPath('video_subtitle_cover_generator.py');
|
|
const pythonPath = getPythonPath();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
// 🔧 修复:传递APP_ROOT环境变量给Python进程,使其能正确识别打包环境
|
|
const env = buildRuntimeProcessEnv({
|
|
APP_ROOT: process.env.APP_ROOT || process.cwd(),
|
|
RESOURCE_BUNDLE_ROOT: getRuntimeBundleRoot()
|
|
});
|
|
|
|
// 🔧 修复:添加 FFmpeg 目录到 PATH,让 Python 脚本能找到 ffmpeg
|
|
const ffmpegPath = getFFmpegExecutablePath();
|
|
if (resourceExists(ffmpegPath)) {
|
|
const ffmpegDir = path.dirname(ffmpegPath);
|
|
const pathSep = process.platform === 'win32' ? ';' : ':';
|
|
env['PATH'] = ffmpegDir + pathSep + (env['PATH'] || process.env.PATH || '');
|
|
Log.info('VideoSubtitleCoverGenerator', `Added FFmpeg to PATH: ${ffmpegDir}`);
|
|
}
|
|
|
|
const pythonProcess = spawn(pythonPath, [pythonScript, configPath], {
|
|
cwd: process.env.APP_ROOT || process.cwd(),
|
|
env: env,
|
|
stdio: ["pipe", "pipe", "pipe"]
|
|
});
|
|
this.currentProcess = pythonProcess;
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
|
|
pythonProcess.stdout.on("data", (data) => {
|
|
stdout += data.toString();
|
|
console.log("Python stdout:", data.toString());
|
|
});
|
|
|
|
pythonProcess.stderr.on("data", (data) => {
|
|
stderr += data.toString();
|
|
console.error("Python stderr:", data.toString());
|
|
});
|
|
|
|
pythonProcess.on("close", (code) => {
|
|
if (this.currentProcess?.pid === pythonProcess.pid) {
|
|
this.currentProcess = null;
|
|
}
|
|
|
|
// 清理临时文件
|
|
try {
|
|
fs.unlinkSync(configPath);
|
|
} catch (e) {
|
|
console.warn("清理临时文件失败:", e);
|
|
}
|
|
|
|
if (code === 0) {
|
|
try {
|
|
const result = JSON.parse(stdout);
|
|
resolve(result);
|
|
} catch (e) {
|
|
resolve({
|
|
code: -1,
|
|
msg: `解析Python输出失败: ${e}`
|
|
});
|
|
}
|
|
} else {
|
|
resolve({
|
|
code: -1,
|
|
msg: `Python脚本执行失败 (退出码: ${code}): ${stderr}`
|
|
});
|
|
}
|
|
});
|
|
|
|
pythonProcess.on("error", (error) => {
|
|
if (this.currentProcess?.pid === pythonProcess.pid) {
|
|
this.currentProcess = null;
|
|
}
|
|
|
|
// 清理临时文件
|
|
try {
|
|
fs.unlinkSync(configPath);
|
|
} catch (e) {
|
|
console.warn("清理临时文件失败:", e);
|
|
}
|
|
|
|
resolve({
|
|
code: -1,
|
|
msg: `启动Python进程失败: ${error.message}`
|
|
});
|
|
});
|
|
|
|
// 设置超时
|
|
setTimeout(() => {
|
|
this.stopCurrentProcess().catch((error) => {
|
|
Log.warn("VideoSubtitleCoverGenerator", `Timeout stop failed: ${error}`);
|
|
});
|
|
resolve({
|
|
code: -1,
|
|
msg: "处理超时"
|
|
});
|
|
}, 300000); // 5分钟超时
|
|
});
|
|
|
|
} catch (error: any) {
|
|
return {
|
|
code: -1,
|
|
msg: `处理失败: ${error.message}`
|
|
};
|
|
}
|
|
}
|
|
}
|