Initial clean project import
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
import {Files} from "../mapi/file/main";
|
||||
import {Log} from "../mapi/log/main";
|
||||
import {SendType, ServerApiType, ServerFunctionDataType, ServerInfo} from "../mapi/server/type";
|
||||
import {AigcServerUtil} from "./util";
|
||||
import path from "path";
|
||||
import {getFFmpegExecutablePath, resourceExists} from "../lib/resource-path";
|
||||
|
||||
type LauncherResultType = {
|
||||
result: {
|
||||
[key: string]: any;
|
||||
};
|
||||
endTime: number | null;
|
||||
};
|
||||
|
||||
const normalizeEasyServerFunctions = (functions: Record<string, any> = {}) => {
|
||||
const normalized = {
|
||||
...functions,
|
||||
};
|
||||
if (normalized.speechRecognition && !normalized.asr) {
|
||||
normalized.asr = normalized.speechRecognition;
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export const EasyServer = function (config: any) {
|
||||
const me = this;
|
||||
let controller: any = null;
|
||||
let controllerWatching = {
|
||||
id: null as string | null,
|
||||
launcherResult: null as LauncherResultType | null,
|
||||
resolve: null as ((value: any) => void) | null,
|
||||
reject: null as ((reason?: any) => void) | null,
|
||||
promiseResolved: false,
|
||||
};
|
||||
const resetControllerWatching = () => {
|
||||
controllerWatching.id = null;
|
||||
controllerWatching.launcherResult = null;
|
||||
controllerWatching.resolve = null;
|
||||
controllerWatching.reject = null;
|
||||
controllerWatching.promiseResolved = false;
|
||||
};
|
||||
const settleControllerWatching = async (type: "resolve" | "reject", reason?: any, stopController = false) => {
|
||||
if (controllerWatching.promiseResolved) {
|
||||
return;
|
||||
}
|
||||
controllerWatching.promiseResolved = true;
|
||||
const resolve = controllerWatching.resolve;
|
||||
const reject = controllerWatching.reject;
|
||||
if (stopController && controller) {
|
||||
const runningController = controller;
|
||||
controller = null;
|
||||
try {
|
||||
await runningController.stop();
|
||||
} catch (e) {
|
||||
Log.warn("easyServer.settle.stop.error", e);
|
||||
}
|
||||
}
|
||||
if (type === "resolve") {
|
||||
resolve?.(undefined);
|
||||
} else {
|
||||
reject?.(reason);
|
||||
}
|
||||
};
|
||||
this.serverConfig = {
|
||||
...(config || {}),
|
||||
easyServer: {
|
||||
...(config?.easyServer || {}),
|
||||
functions: normalizeEasyServerFunctions(config?.easyServer?.functions || {}),
|
||||
},
|
||||
} as {
|
||||
easyServer: {
|
||||
entry: string;
|
||||
entryArgs: string[];
|
||||
envs: string[];
|
||||
content: string;
|
||||
functions: {
|
||||
[key: string]: {
|
||||
content?: string;
|
||||
param?: any[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
this.isRunning = false;
|
||||
this.ServerApi = null as ServerApiType | null;
|
||||
this.ServerInfo = null as ServerInfo | null;
|
||||
this.serverRuntime = {
|
||||
startTime: 0,
|
||||
};
|
||||
this.send = function (type: SendType, data: any) {
|
||||
this.ServerApi.event.sendChannel(this.ServerInfo.eventChannelName, {type, data});
|
||||
};
|
||||
this.init = async function () {
|
||||
};
|
||||
this.config = async function () {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "ok",
|
||||
data: {
|
||||
httpUrl: null,
|
||||
content: this.serverConfig.easyServer.content || "",
|
||||
functions: this.serverConfig.easyServer.functions || {},
|
||||
},
|
||||
};
|
||||
};
|
||||
this.start = async function () {
|
||||
// console.log('start', this.ServerInfo)
|
||||
this.serverRuntime.startTime = Date.now();
|
||||
this.send("starting", this.ServerInfo);
|
||||
};
|
||||
this.ping = async function (): Promise<boolean> {
|
||||
// console.log('ping', this.ServerInfo)
|
||||
return this.serverRuntime.startTime > 0;
|
||||
};
|
||||
this.stop = async function () {
|
||||
// console.log('stop', this.ServerInfo)
|
||||
this.send("stopping", this.ServerInfo);
|
||||
this.serverRuntime.startTime = 0;
|
||||
if (controller) {
|
||||
try {
|
||||
await controller.stop();
|
||||
} catch (e) {
|
||||
Log.warn("easyServer.stop.error", e);
|
||||
}
|
||||
controller = null;
|
||||
}
|
||||
this.send("stopped", this.ServerInfo);
|
||||
this.send("success", this.ServerInfo);
|
||||
};
|
||||
this.cancel = async function () {
|
||||
if (controller) {
|
||||
await controller.stop();
|
||||
controller = null;
|
||||
}
|
||||
};
|
||||
this._controllerRunIfNeeded = async function (
|
||||
configJsonPath: string | null,
|
||||
option: {
|
||||
timeout: number;
|
||||
}
|
||||
) {
|
||||
if (!controller) {
|
||||
const _localPath = this.ServerInfo?.localPath || '';
|
||||
Log.info("[EasyServer] ServerInfo.localPath", _localPath);
|
||||
let command = [];
|
||||
command.push(this.serverConfig.easyServer.entry);
|
||||
if (this.serverConfig.easyServer.entryArgs) {
|
||||
command = command.concat(this.serverConfig.easyServer.entryArgs);
|
||||
}
|
||||
for (let i = 0; i < command.length; i++) {
|
||||
command[i] = command[i].replace("${CONFIG}", `"${configJsonPath}"`);
|
||||
command[i] = command[i].replace("${ROOT}", _localPath);
|
||||
}
|
||||
Log.info("[EasyServer] command", JSON.stringify(command));
|
||||
Log.info("[EasyServer] localPath", this.ServerInfo.localPath);
|
||||
const envMap = {};
|
||||
Log.info('EasyServer.config', JSON.stringify(this.serverConfig.easyServer));
|
||||
if (this.serverConfig.easyServer.entry === "launcher") {
|
||||
const systemEnv = await this.ServerApi.env();
|
||||
// console.log('EasyServer.systemEnv', systemEnv)
|
||||
for (const k in systemEnv) {
|
||||
envMap[k] = systemEnv[k];
|
||||
}
|
||||
}
|
||||
// 🔧 修复:添加 FFmpeg 目录到 PATH,让 Python 脚本能找到 ffmpeg
|
||||
const pathDirs = [
|
||||
`${this.ServerInfo.localPath}`,
|
||||
`${this.ServerInfo.localPath}/binary`,
|
||||
];
|
||||
const ffmpegPath = getFFmpegExecutablePath();
|
||||
if (resourceExists(ffmpegPath)) {
|
||||
const ffmpegDir = path.dirname(ffmpegPath);
|
||||
pathDirs.push(ffmpegDir);
|
||||
Log.info(`[EasyServer] ✓ 已添加 FFmpeg 目录到 PATH: ${ffmpegDir}`);
|
||||
}
|
||||
envMap["PATH"] = this.ServerApi.getPathEnv(pathDirs);
|
||||
envMap["PYTHONIOENCODING"] = "utf-8";
|
||||
envMap["AIGCPANEL_SERVER_PLACEHOLDER_CONFIG"] = configJsonPath;
|
||||
envMap["AIGCPANEL_SERVER_PLACEHOLDER_ROOT"] = this.ServerInfo.localPath;
|
||||
if (this.serverConfig.easyServer.envs) {
|
||||
for (const e of this.serverConfig.easyServer.envs) {
|
||||
let pcs = e.split("=");
|
||||
const key = pcs.shift();
|
||||
envMap[key] = pcs.join("=");
|
||||
}
|
||||
}
|
||||
for (const k in envMap) {
|
||||
envMap[k] = envMap[k].replace("${CONFIG}", `"${configJsonPath}"`);
|
||||
envMap[k] = envMap[k].replace("${ROOT}", this.ServerInfo.localPath);
|
||||
}
|
||||
const hasMoreQueue = async () => {
|
||||
const queueRoot = this.ServerInfo.localPath + `/aigcpanel-queue/`;
|
||||
await Files.mkdir(queueRoot);
|
||||
const files = await Files.list(queueRoot);
|
||||
const validQueueFiles = files.filter(f => f.name.match(/\.queue\.json$/));
|
||||
if (validQueueFiles.length > 0) {
|
||||
const configJson = await Files.temp("json");
|
||||
await Files.copy(validQueueFiles[0].pathname, configJson);
|
||||
await Files.deletes(validQueueFiles[0].pathname);
|
||||
await this._controllerRunIfNeeded(configJson, option);
|
||||
}
|
||||
};
|
||||
let timer = null;
|
||||
if (option.timeout > 0) {
|
||||
timer = setTimeout(async () => {
|
||||
this.ServerApi.file.appendText(this.ServerInfo.logFile, "timeout", {isDataPath: true});
|
||||
await settleControllerWatching("reject", new Error("timeout"), true);
|
||||
}, option.timeout * 1000);
|
||||
}
|
||||
let buffer = "";
|
||||
controller = await this.ServerApi.app.spawnShell(command, {
|
||||
env: envMap,
|
||||
cwd: this.ServerInfo.localPath,
|
||||
stdout: _data => {
|
||||
// console.log('easyServer.stdout', _data)
|
||||
buffer += _data;
|
||||
// check if has \n and process the buffer
|
||||
let lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
this.ServerApi.file.appendText(this.ServerInfo.logFile, _data, {isDataPath: true});
|
||||
|
||||
// 鍙戦€佹棩蹇楀埌鍓嶇UI
|
||||
if (controllerWatching.id) {
|
||||
this.send("taskLog", {
|
||||
id: controllerWatching.id,
|
||||
type: "stdout",
|
||||
data: _data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
const result = this.ServerApi.extractResultFromLogs(controllerWatching.id, lines.join("\n") + "\n");
|
||||
if (result) {
|
||||
if (controllerWatching.launcherResult) {
|
||||
controllerWatching.launcherResult.result = Object.assign(controllerWatching.launcherResult.result, result);
|
||||
}
|
||||
if (controllerWatching.id) {
|
||||
this.send("taskResult", {id: controllerWatching.id, result});
|
||||
}
|
||||
}
|
||||
if (controllerWatching.launcherResult) {
|
||||
controllerWatching.launcherResult.result.error =
|
||||
AigcServerUtil.errorDetect(_data) || controllerWatching.launcherResult.result.error;
|
||||
// 馃敡 淇锛氬彧鏈夊湪End=true涓斿凡缁忔湁鏈夋晥缁撴灉(url/records/error)鏃舵墠resolve
|
||||
if (controllerWatching.launcherResult.result && controllerWatching.launcherResult.result['End']) {
|
||||
const hasValidResult =
|
||||
controllerWatching.launcherResult.result.url ||
|
||||
controllerWatching.launcherResult.result.records ||
|
||||
controllerWatching.launcherResult.result.error;
|
||||
if (hasValidResult) {
|
||||
settleControllerWatching("resolve", undefined, true).then();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
stderr: _data => {
|
||||
// console.log('easyServer.stderr', _data)
|
||||
this.ServerApi.file.appendText(this.ServerInfo.logFile, _data, {isDataPath: true});
|
||||
|
||||
// 鍙戦€佹棩蹇楀埌鍓嶇UI
|
||||
if (controllerWatching.id) {
|
||||
this.send("taskLog", {
|
||||
id: controllerWatching.id,
|
||||
type: "stderr",
|
||||
data: _data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
if (controllerWatching.launcherResult) {
|
||||
controllerWatching.launcherResult.result.error =
|
||||
AigcServerUtil.errorDetect(_data) || controllerWatching.launcherResult.result.error;
|
||||
}
|
||||
},
|
||||
success: _data => {
|
||||
// console.log('easyServer.success', _data)
|
||||
clearTimeout(timer);
|
||||
controller = null;
|
||||
hasMoreQueue().then()
|
||||
settleControllerWatching("resolve").then();
|
||||
},
|
||||
error: (_data, code) => {
|
||||
// console.log('easyServer.error', {_data, controllerWatching})
|
||||
this.ServerApi.file.appendText(this.ServerInfo.logFile, `exit code ${code}`, {isDataPath: true});
|
||||
clearTimeout(timer);
|
||||
controller = null;
|
||||
hasMoreQueue().then()
|
||||
settleControllerWatching("reject").then();
|
||||
},
|
||||
})
|
||||
} else if (configJsonPath) {
|
||||
const queueName = `${Date.now()}.queue.json`;
|
||||
const queuePath = this.ServerInfo.localPath + `/aigcpanel-queue/${queueName}`;
|
||||
await Files.copy(configJsonPath, queuePath);
|
||||
this.ServerApi.file.appendText(
|
||||
this.ServerInfo.logFile,
|
||||
`Another task is running, queued at ${queueName}`,
|
||||
{isDataPath: true}
|
||||
);
|
||||
}
|
||||
};
|
||||
this._callFunc = async function (
|
||||
data: ServerFunctionDataType,
|
||||
configCalculator: (data: ServerFunctionDataType) => Promise<any>,
|
||||
resultDataCalculator: (data: ServerFunctionDataType, launcherResult: LauncherResultType) => Promise<any>,
|
||||
option: {
|
||||
timeout: number;
|
||||
}
|
||||
) {
|
||||
option = Object.assign(
|
||||
{
|
||||
timeout: 24 * 3600,
|
||||
},
|
||||
option
|
||||
);
|
||||
const resultData = {
|
||||
// success, retry
|
||||
type: "success",
|
||||
start: 0,
|
||||
end: 0,
|
||||
data: {},
|
||||
};
|
||||
if (this.isRunning) {
|
||||
resultData.type = "retry";
|
||||
return {
|
||||
code: 0,
|
||||
msg: "ok",
|
||||
data: resultData,
|
||||
};
|
||||
}
|
||||
this.isRunning = true;
|
||||
resultData.start = Date.now();
|
||||
let configJsonPath = null;
|
||||
try {
|
||||
this.send("taskRunning", {id: data.id});
|
||||
const configData = await configCalculator(data);
|
||||
configData.setting = this.ServerInfo.setting;
|
||||
configJsonPath = await this.ServerApi.launcherPrepareConfigJson(configData);
|
||||
// console.log('EasyServer.envMap', envMap)
|
||||
const launcherResult: LauncherResultType = {
|
||||
result: {},
|
||||
endTime: null,
|
||||
};
|
||||
// console.log('easyServer.start', JSON.stringify({command, envMap, configData}))
|
||||
await (async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
controllerWatching.id = data.id;
|
||||
controllerWatching.launcherResult = launcherResult;
|
||||
controllerWatching.resolve = resolve;
|
||||
controllerWatching.reject = reject;
|
||||
controllerWatching.promiseResolved = false;
|
||||
me._controllerRunIfNeeded(configJsonPath, option);
|
||||
});
|
||||
})();
|
||||
resultData.end = Date.now();
|
||||
resultData.data = await resultDataCalculator(data, launcherResult);
|
||||
// console.log('easyServer.end', launcherResult)
|
||||
await Files.deletes(configJsonPath);
|
||||
return {
|
||||
code: 0,
|
||||
msg: "ok",
|
||||
data: resultData,
|
||||
};
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
resetControllerWatching();
|
||||
this.isRunning = false;
|
||||
}
|
||||
};
|
||||
this.soundTts = async function (data: ServerFunctionDataType) {
|
||||
// console.log('soundTts', {data, serverInfo: this.ServerInfo})
|
||||
return this._callFunc(
|
||||
data,
|
||||
async (data: ServerFunctionDataType) => {
|
||||
return {
|
||||
id: data.id,
|
||||
mode: "local",
|
||||
modelConfig: {
|
||||
type: "soundTts",
|
||||
param: data.param,
|
||||
text: data.text,
|
||||
},
|
||||
};
|
||||
},
|
||||
async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => {
|
||||
// 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒
|
||||
if (!("url" in launcherResult.result)) {
|
||||
// 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿?
|
||||
if (launcherResult.result.End === true) {
|
||||
return launcherResult.result;
|
||||
}
|
||||
if (launcherResult.result.error) {
|
||||
throw launcherResult.result.error;
|
||||
}
|
||||
throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織";
|
||||
}
|
||||
return {
|
||||
url: launcherResult.result.url,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
this.soundClone = async function (data: ServerFunctionDataType) {
|
||||
// console.log('soundClone', {data, serverInfo: this.ServerInfo})
|
||||
return this._callFunc(
|
||||
data,
|
||||
async (data: ServerFunctionDataType) => {
|
||||
return {
|
||||
id: data.id,
|
||||
mode: "local",
|
||||
modelConfig: {
|
||||
type: "soundClone",
|
||||
param: data.param,
|
||||
text: data.text,
|
||||
promptAudio: data.promptAudio,
|
||||
promptText: data.promptText,
|
||||
},
|
||||
};
|
||||
},
|
||||
async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => {
|
||||
// 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒
|
||||
if (!("url" in launcherResult.result)) {
|
||||
// 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿?
|
||||
if (launcherResult.result.End === true) {
|
||||
return launcherResult.result;
|
||||
}
|
||||
if (launcherResult.result.error) {
|
||||
throw launcherResult.result.error;
|
||||
}
|
||||
throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織";
|
||||
}
|
||||
return {
|
||||
url: launcherResult.result.url,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
this.videoGen = async function (data: ServerFunctionDataType) {
|
||||
// console.log('videoGen', JSON.stringify({data, serverInfo: this.ServerInfo}))
|
||||
return this._callFunc(
|
||||
data,
|
||||
async (data: ServerFunctionDataType) => {
|
||||
return {
|
||||
id: data.id,
|
||||
mode: "local",
|
||||
modelConfig: {
|
||||
type: "videoGen",
|
||||
param: data.param,
|
||||
video: data.video,
|
||||
audio: data.audio,
|
||||
},
|
||||
};
|
||||
},
|
||||
async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => {
|
||||
// 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒
|
||||
if (!("url" in launcherResult.result)) {
|
||||
// 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿?
|
||||
if (launcherResult.result.End === true) {
|
||||
return launcherResult.result;
|
||||
}
|
||||
if (launcherResult.result.error) {
|
||||
throw launcherResult.result.error;
|
||||
}
|
||||
throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織";
|
||||
}
|
||||
return {
|
||||
url: launcherResult.result.url,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
this.asr = async function (data: ServerFunctionDataType) {
|
||||
// console.log('videoGen', JSON.stringify({data, serverInfo: this.ServerInfo}))
|
||||
return this._callFunc(
|
||||
data,
|
||||
async (data: ServerFunctionDataType) => {
|
||||
return {
|
||||
id: data.id,
|
||||
mode: "local",
|
||||
modelConfig: {
|
||||
type: 'asr',
|
||||
audio: data.audio,
|
||||
param: data.param,
|
||||
},
|
||||
};
|
||||
},
|
||||
async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => {
|
||||
// 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒
|
||||
if (!("records" in launcherResult.result)) {
|
||||
// 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿?
|
||||
if (launcherResult.result.End === true) {
|
||||
return launcherResult.result;
|
||||
}
|
||||
if (launcherResult.result.error) {
|
||||
throw launcherResult.result.error;
|
||||
}
|
||||
throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織";
|
||||
}
|
||||
return {
|
||||
records: launcherResult.result.records,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
this.textToImage = async function (data: ServerFunctionDataType) {
|
||||
// console.log('textToImage', {data, serverInfo: this.ServerInfo})
|
||||
return this._callFunc(
|
||||
data,
|
||||
async (data: ServerFunctionDataType) => {
|
||||
return {
|
||||
id: data.id,
|
||||
mode: "local",
|
||||
modelConfig: {
|
||||
type: "textToImage",
|
||||
prompt: data.prompt,
|
||||
param: data.param,
|
||||
},
|
||||
};
|
||||
},
|
||||
async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => {
|
||||
// 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒
|
||||
if (!("url" in launcherResult.result)) {
|
||||
// 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿?
|
||||
if (launcherResult.result.End === true) {
|
||||
return launcherResult.result;
|
||||
}
|
||||
if (launcherResult.result.error) {
|
||||
throw launcherResult.result.error;
|
||||
}
|
||||
throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織";
|
||||
}
|
||||
return {
|
||||
url: launcherResult.result.url,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
this.imageToImage = async function (data: ServerFunctionDataType) {
|
||||
// console.log('imageToImage', {data, serverInfo: this.ServerInfo})
|
||||
return this._callFunc(
|
||||
data,
|
||||
async (data: ServerFunctionDataType) => {
|
||||
return {
|
||||
id: data.id,
|
||||
mode: "local",
|
||||
modelConfig: {
|
||||
type: "imageToImage",
|
||||
image: data.image,
|
||||
prompt: data.prompt,
|
||||
param: data.param,
|
||||
},
|
||||
};
|
||||
},
|
||||
async (data: ServerFunctionDataType, launcherResult: LauncherResultType) => {
|
||||
// 馃敡 淇锛氬拷鐣?{"End": true} 缁撴潫鏍囧織,鍙鏌ョ湡姝g殑閿欒
|
||||
if (!("url" in launcherResult.result)) {
|
||||
// 濡傛灉鏄疎nd鏍囧織,鐩存帴杩斿洖鐜版湁缁撴灉(涓嶆姤閿?
|
||||
if (launcherResult.result.End === true) {
|
||||
return launcherResult.result;
|
||||
}
|
||||
if (launcherResult.result.error) {
|
||||
throw launcherResult.result.error;
|
||||
}
|
||||
throw "鎵ц澶辫触锛岃鏌ョ湅妯″瀷鏃ュ織";
|
||||
}
|
||||
return {
|
||||
url: launcherResult.result.url,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
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}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {EasyServer} from "./EasyServer";
|
||||
import {ServerLive} from "./server-live";
|
||||
|
||||
export const AigcServer = {
|
||||
EasyServer: EasyServer,
|
||||
"server-live": ServerLive,
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import {SendType, ServerApiType, ServerContext, ServerInfo} from "../mapi/server/type";
|
||||
|
||||
const serverRuntime = {
|
||||
port: 0,
|
||||
};
|
||||
|
||||
let shellController = null;
|
||||
let isRunning = false;
|
||||
|
||||
export const ServerLive: ServerContext = {
|
||||
ServerApi: null as ServerApiType | null,
|
||||
ServerInfo: null as ServerInfo | null,
|
||||
url() {
|
||||
return `http://localhost:${serverRuntime.port}/`;
|
||||
},
|
||||
send(type: SendType, data: any) {
|
||||
this.ServerApi.event.sendChannel(this.ServerInfo.eventChannelName, {type, data});
|
||||
},
|
||||
|
||||
async _client() {
|
||||
return await this.ServerApi.GradioClient.connect(this.url());
|
||||
},
|
||||
async init() {},
|
||||
async start() {
|
||||
// console.log('this.ServerApi.app.availablePort(50617)', await this.ServerApi.app.availablePort(50617))
|
||||
this.send("starting", this.ServerInfo);
|
||||
let command = [];
|
||||
serverRuntime.port = await this.ServerApi.availablePort(serverRuntime.port, this.ServerInfo.setting);
|
||||
const env = await this.ServerApi.env();
|
||||
command.push(`"${this.ServerInfo.localPath}/launcher"`);
|
||||
command.push(`--env=DEBUG=true`);
|
||||
env["PATH"] = this.ServerApi.getPathEnv(`${this.ServerInfo.localPath}/binary`);
|
||||
env["AIGCPANEL_SERVER_PORT"] = `${serverRuntime.port}`;
|
||||
env["AIGCPANEL_SERVER_PLACEHOLDER_CONFIG"] = await this.ServerApi.launcherPrepareConfigJson({
|
||||
id: "live",
|
||||
modelConfig: {},
|
||||
setting: this.ServerInfo.setting,
|
||||
});
|
||||
// console.log('command', JSON.stringify(command))
|
||||
shellController = await this.ServerApi.app.spawnShell(command, {
|
||||
stdout: data => {
|
||||
this.ServerApi.file.appendText(this.ServerInfo.logFile, data, {isDataPath: true});
|
||||
const result = this.ServerApi.extractResultFromLogs("live", data);
|
||||
if (result) {
|
||||
if (result["Action"]) {
|
||||
const action = result["Action"].split(":");
|
||||
this.send("action", {
|
||||
type: action[0],
|
||||
msg: action.length > 1 ? action[1] : "",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
stderr: data => {
|
||||
this.ServerApi.file.appendText(this.ServerInfo.logFile, data, {isDataPath: true});
|
||||
},
|
||||
success: data => {
|
||||
// console.log('serverLive.success', {data})
|
||||
this.send("success", this.ServerInfo);
|
||||
},
|
||||
error: (data, code) => {
|
||||
// console.log('serverLive.error', {code, data})
|
||||
this.ServerApi.file.appendText(this.ServerInfo.logFile, data, {isDataPath: true});
|
||||
this.send("error", this.ServerInfo);
|
||||
},
|
||||
env,
|
||||
cwd: this.ServerInfo.localPath,
|
||||
});
|
||||
},
|
||||
async ping() {
|
||||
try {
|
||||
const res = await this.ServerApi.request(`${this.url()}ping`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("ping error", e);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async stop() {
|
||||
this.send("stopping", this.ServerInfo);
|
||||
try {
|
||||
if (shellController) {
|
||||
await shellController.stop();
|
||||
}
|
||||
shellController = null;
|
||||
} catch (e) {
|
||||
console.log("stop error", e);
|
||||
}
|
||||
this.send("stopped", this.ServerInfo);
|
||||
},
|
||||
async cancel() {
|
||||
await this.ServerApi.launcherCancel(this);
|
||||
},
|
||||
async config() {
|
||||
return {
|
||||
code: 0,
|
||||
msg: "ok",
|
||||
data: {
|
||||
httpUrl: shellController ? this.url() : null,
|
||||
content: ``,
|
||||
functions: {
|
||||
live: {
|
||||
content: ``,
|
||||
param: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
async apiRequest(
|
||||
data: {
|
||||
url: string;
|
||||
param: any;
|
||||
},
|
||||
option: any
|
||||
) {
|
||||
// serverRuntime.port = 60617
|
||||
// console.log('apiRequest', {url: this.url(), data, option})
|
||||
const {url, param} = data;
|
||||
return this.ServerApi.request(`${this.url()}${url}`, param, {
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import {t} from "../config/lang";
|
||||
|
||||
export const AigcServerUtil = {
|
||||
errorDetect: (data: string): string | null => {
|
||||
const errorMap = {
|
||||
"torch.cuda.OutOfMemoryError": t("CUDA内存不足"),
|
||||
};
|
||||
for (const [key, value] of Object.entries(errorMap)) {
|
||||
if (data.includes(key)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user