|
59 | 59 |
|
60 | 60 | 安装完成后,打开软件,下载模型一键启动包,即可使用。
|
61 | 61 |
|
| 62 | +## 模型自定义接入 |
| 63 | + |
| 64 | +如果有第三方一键启动的模型,可以按照以下方式接入。 |
| 65 | + |
| 66 | +模型文件夹格式,只需要编写 `config.json` 和 `server.js` 两个文件即可。 |
| 67 | + |
| 68 | +``` |
| 69 | +|- 模型文件夹/ |
| 70 | +|-|- config.json - 模型配置文件 |
| 71 | +|-|- server.js - 模型对接文件 |
| 72 | +|-|- xxx - 其他模型文件,推荐将模型文件放在 model 文件夹下 |
| 73 | +``` |
| 74 | + |
| 75 | +### config.json 文件示例 |
| 76 | + |
| 77 | +```json5 |
| 78 | +{ |
| 79 | + "name": "server-xxx", // 模型名称 |
| 80 | + "version": "0.1.0", // 模型版本 |
| 81 | + "title": "语音模型", // 模型标题 |
| 82 | + "description": "模型描述", // 模型描述 |
| 83 | + "platformName": "win", // 支持系统,win, osx, linux |
| 84 | + "platformArch": "x86", // 支持架构,x86, arm64 |
| 85 | + "entry": "server/main", // 入口文件,一键启动包文件 |
| 86 | + "functions": [ |
| 87 | + "videoGen", // 支持视频生成 |
| 88 | + "soundTTS", // 支持语音合成 |
| 89 | + "soundClone" // 支持语音克隆 |
| 90 | + ], |
| 91 | + "settings": [ // 模型配置项,可以显示在模型配置页面 |
| 92 | + { |
| 93 | + "name": "port", |
| 94 | + "type": "text", |
| 95 | + "title": "服务端口", |
| 96 | + "default": "", |
| 97 | + "placeholder": "留空会检测使用随机端口" |
| 98 | + } |
| 99 | + ] |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +### server.js 文件示例 |
| 104 | + |
| 105 | +> 以下以 MuseTalk 为例 |
| 106 | +
|
| 107 | +```js |
| 108 | +const serverRuntime = { |
| 109 | + port: 0, |
| 110 | +} |
| 111 | + |
| 112 | +let shellController = null |
| 113 | + |
| 114 | +module.exports = { |
| 115 | + ServerApi: null, |
| 116 | + _url() { |
| 117 | + return `http://localhost:${serverRuntime.port}/` |
| 118 | + }, |
| 119 | + async _client() { |
| 120 | + return await this.ServerApi.GradioClient.connect(this._url()); |
| 121 | + }, |
| 122 | + _send(serverInfo, type, data) { |
| 123 | + this.ServerApi.event.sendChannel(serverInfo.eventChannelName, {type, data}) |
| 124 | + }, |
| 125 | + |
| 126 | + // 模型初始化 |
| 127 | + async init(ServerApi) { |
| 128 | + this.ServerApi = ServerApi; |
| 129 | + }, |
| 130 | + // 模型启动 |
| 131 | + async start(serverInfo) { |
| 132 | + console.log('start', JSON.stringify(serverInfo)) |
| 133 | + this._send(serverInfo, 'starting', serverInfo) |
| 134 | + let command = [] |
| 135 | + if (serverInfo.setting?.['port']) { |
| 136 | + serverRuntime.port = serverInfo.setting.port |
| 137 | + } else if (!serverRuntime.port || !await this.ServerApi.app.isPortAvailable(serverRuntime.port)) { |
| 138 | + serverRuntime.port = await this.ServerApi.app.availablePort(50617) |
| 139 | + } |
| 140 | + if (serverInfo.setting?.['startCommand']) { |
| 141 | + command.push(serverInfo.setting.startCommand) |
| 142 | + } else { |
| 143 | + //command.push(`"${serverInfo.localPath}/server/main"`) |
| 144 | + command.push(`"${serverInfo.localPath}/server/.ai/python.exe"`) |
| 145 | + command.push('-u') |
| 146 | + command.push(`"${serverInfo.localPath}/server/run.py"`) |
| 147 | + if (serverInfo.setting?.['gpuMode'] === 'cpu') { |
| 148 | + command.push('--gpu_mode=cpu') |
| 149 | + } |
| 150 | + } |
| 151 | + shellController = await this.ServerApi.app.spawnShell(command, { |
| 152 | + cwd: `${serverInfo.localPath}/server`, |
| 153 | + env: { |
| 154 | + GRADIO_SERVER_PORT: serverRuntime.port, |
| 155 | + PATH: [ |
| 156 | + process.env.PATH, |
| 157 | + `${serverInfo.localPath}/server`, |
| 158 | + `${serverInfo.localPath}/server/.ai/ffmpeg/bin`, |
| 159 | + ].join(';') |
| 160 | + }, |
| 161 | + stdout: (data) => { |
| 162 | + this.ServerApi.file.appendText(serverInfo.logFile, data) |
| 163 | + }, |
| 164 | + stderr: (data) => { |
| 165 | + this.ServerApi.file.appendText(serverInfo.logFile, data) |
| 166 | + }, |
| 167 | + success: (data) => { |
| 168 | + this._send(serverInfo, 'success', serverInfo) |
| 169 | + }, |
| 170 | + error: (data, code) => { |
| 171 | + this.ServerApi.file.appendText(serverInfo.logFile, data) |
| 172 | + this._send(serverInfo, 'error', serverInfo) |
| 173 | + }, |
| 174 | + }) |
| 175 | + }, |
| 176 | + // 模型启动检测 |
| 177 | + async ping(serverInfo) { |
| 178 | + try { |
| 179 | + const res = await this.ServerApi.request(`${this._url()}info`) |
| 180 | + return true |
| 181 | + } catch (e) { |
| 182 | + } |
| 183 | + return false |
| 184 | + }, |
| 185 | + // 模型停止 |
| 186 | + async stop(serverInfo) { |
| 187 | + this._send(serverInfo, 'stopping', serverInfo) |
| 188 | + try { |
| 189 | + shellController.stop() |
| 190 | + shellController = null |
| 191 | + } catch (e) { |
| 192 | + console.log('stop error', e) |
| 193 | + } |
| 194 | + this._send(serverInfo, 'stopped', serverInfo) |
| 195 | + }, |
| 196 | + // 模型配置 |
| 197 | + async config() { |
| 198 | + return { |
| 199 | + "code": 0, |
| 200 | + "msg": "ok", |
| 201 | + "data": { |
| 202 | + "httpUrl": shellController ? this._url() : null, |
| 203 | + "functions": { |
| 204 | + "videoGen": { |
| 205 | + "param": [ |
| 206 | + { |
| 207 | + name: "box", |
| 208 | + type: "inputNumber", |
| 209 | + title: "嘴巴张开度", |
| 210 | + defaultValue: -7, |
| 211 | + placeholder: "", |
| 212 | + tips: '嘴巴张开度可以控制生成视频中嘴巴的张开程度', |
| 213 | + min: -9, |
| 214 | + max: 9, |
| 215 | + step: 1, |
| 216 | + } |
| 217 | + ] |
| 218 | + }, |
| 219 | + } |
| 220 | + } |
| 221 | + } |
| 222 | + }, |
| 223 | + // 视频生成 |
| 224 | + async videoGen(serverInfo, data) { |
| 225 | + console.log('videoGen', serverInfo, data) |
| 226 | + const client = await this._client() |
| 227 | + const resultData = { |
| 228 | + // success, querying, retry |
| 229 | + type: 'success', |
| 230 | + start: 0, |
| 231 | + end: 0, |
| 232 | + jobId: '', |
| 233 | + data: { |
| 234 | + filePath: null |
| 235 | + } |
| 236 | + } |
| 237 | + resultData.start = Date.now() |
| 238 | + const result = await client.predict("/predict", [ |
| 239 | + this.ServerApi.GradioHandleFile(data.videoFile), |
| 240 | + this.ServerApi.GradioHandleFile(data.soundFile), |
| 241 | + parseInt(data.param.box) |
| 242 | + ]); |
| 243 | + // console.log('videoGen.result', JSON.stringify(result)) |
| 244 | + resultData.end = Date.now() |
| 245 | + resultData.data.filePath = result.data[0].value.video.path |
| 246 | + return { |
| 247 | + code: 0, |
| 248 | + msg: 'ok', |
| 249 | + data: resultData |
| 250 | + } |
| 251 | + }, |
| 252 | +} |
| 253 | +``` |
| 254 | +
|
62 | 255 | ## 技术栈
|
63 | 256 |
|
64 | 257 | - `electron`
|
|
0 commit comments