feat(engine): 重构字幕引擎并实现 WebSocket 通信

- 重构了 Gummy 和 Vosk 字幕引擎的代码,提高了可扩展性和可读性
- 合并 Gummy 和 Vosk 引擎为单个可执行文件
- 实现了字幕引擎和主程序之间的 WebSocket 通信,避免了孤儿进程问题
This commit is contained in:
himeditator
2025-07-28 15:49:52 +08:00
parent b658ef5440
commit cd9f3a847d
19 changed files with 242 additions and 293 deletions

View File

@@ -1,7 +1,8 @@
import { spawn, exec } from 'child_process'
import { spawn } from 'child_process'
import { app } from 'electron'
import { is } from '@electron-toolkit/utils'
import path from 'path'
import net from 'net'
import { controlWindow } from '../ControlWindow'
import { allConfig } from './AllConfig'
import { i18n } from '../i18n'
@@ -11,91 +12,87 @@ export class CaptionEngine {
appPath: string = ''
command: string[] = []
process: any | undefined
processStatus: 'running' | 'stopping' | 'stopped' = 'stopped'
client: net.Socket | undefined
status: 'running' | 'stopping' | 'stopped' = 'stopped'
private getApp(): boolean {
if (allConfig.controls.customized && allConfig.controls.customizedApp) {
if (allConfig.controls.customized) {
Log.info('Using customized engine')
this.appPath = allConfig.controls.customizedApp
this.command = allConfig.controls.customizedCommand.split(' ')
}
else if (allConfig.controls.engine === 'gummy') {
allConfig.controls.customized = false
if(!allConfig.controls.API_KEY && !process.env.DASHSCOPE_API_KEY) {
else {
if(allConfig.controls.engine === 'gummy' &&
!allConfig.controls.API_KEY && !process.env.DASHSCOPE_API_KEY
) {
controlWindow.sendErrorMessage(i18n('gummy.key.missing'))
return false
}
let gummyName = 'main-gummy'
if (process.platform === 'win32') { gummyName += '.exe' }
this.command = []
if (is.dev) {
this.appPath = path.join(
app.getAppPath(), 'engine',
'subenv', 'Scripts', 'python.exe'
)
this.command.push(path.join(
app.getAppPath(), 'engine', 'main-gummy.py'
))
// this.appPath = path.join(
// app.getAppPath(), 'engine',
// 'subenv', 'Scripts', 'python.exe'
// )
// this.command.push(path.join(
// app.getAppPath(), 'engine', 'main.py'
// ))
this.appPath = path.join(app.getAppPath(), 'engine', 'dist', 'main.exe')
}
else {
this.appPath = path.join(
process.resourcesPath, 'engine', gummyName
this.appPath = path.join(process.resourcesPath, 'engine', 'main.exe')
}
if(allConfig.controls.engine === 'gummy') {
this.command.push('-e', 'gummy')
this.command.push('-s', allConfig.controls.sourceLang)
this.command.push(
'-t', allConfig.controls.translation ?
allConfig.controls.targetLang : 'none'
)
this.command.push('-a', allConfig.controls.audio ? '1' : '0')
if(allConfig.controls.API_KEY) {
this.command.push('-k', allConfig.controls.API_KEY)
}
}
this.command.push('-s', allConfig.controls.sourceLang)
this.command.push(
'-t', allConfig.controls.translation ?
allConfig.controls.targetLang : 'none'
)
this.command.push('-a', allConfig.controls.audio ? '1' : '0')
if(allConfig.controls.API_KEY) {
this.command.push('-k', allConfig.controls.API_KEY)
else if(allConfig.controls.engine === 'vosk'){
this.command.push('-e', 'vosk')
this.command.push('-a', allConfig.controls.audio ? '1' : '0')
this.command.push('-m', `"${allConfig.controls.modelPath}"`)
}
}
else if(allConfig.controls.engine === 'vosk'){
allConfig.controls.customized = false
let voskName = 'main-vosk'
if (process.platform === 'win32') { voskName += '.exe' }
this.command = []
if (is.dev) {
this.appPath = path.join(
app.getAppPath(), 'engine',
'subenv', 'Scripts', 'python.exe'
)
this.command.push(path.join(
app.getAppPath(), 'engine', 'main-vosk.py'
))
}
else {
this.appPath = path.join(
process.resourcesPath, 'engine', voskName
)
}
this.command.push('-a', allConfig.controls.audio ? '1' : '0')
this.command.push('-m', `"${allConfig.controls.modelPath}"`)
}
Log.info('Engine Path:', this.appPath)
Log.info('Engine Command:', this.command)
return true
}
public connect() {
if(this.client) { Log.warn('Client already exists, ignoring...') }
Log.info('Connecting to caption engine server...');
this.client = net.createConnection({ port: 7070 }, () => {
Log.info('Connected to caption engine server');
});
this.status = 'running'
}
public sendCommand(command: string, content: string = "") {
if(this.client === undefined) {
Log.error('Client not initialized yet')
return
}
const data = JSON.stringify({command, content})
this.client.write(data);
Log.info(`Send data to python server: ${data}`);
}
public start() {
if (this.processStatus !== 'stopped') {
Log.warn('Caption engine status is not stopped, cannot start')
if (this.status !== 'stopped') {
Log.warn('Casption engine is not stopped, current status:', this.status)
return
}
if(!this.getApp()){ return }
try {
this.process = spawn(this.appPath, this.command)
}
catch (e) {
controlWindow.sendErrorMessage(i18n('engine.start.error') + e)
Log.error('Error starting engine:', e)
return
}
this.processStatus = 'running'
this.process = spawn(this.appPath, this.command)
Log.info('Caption Engine Started, PID:', this.process.pid)
allConfig.controls.engineEnabled = true
@@ -123,7 +120,7 @@ export class CaptionEngine {
});
this.process.stderr.on('data', (data: any) => {
if(this.processStatus === 'stopping') return
if(this.status === 'stopping') return
controlWindow.sendErrorMessage(i18n('engine.error') + data)
Log.error(`Engine Error: ${data}`);
});
@@ -135,54 +132,43 @@ export class CaptionEngine {
allConfig.sendControls(controlWindow.window)
controlWindow.window.webContents.send('control.engine.stopped')
}
this.processStatus = 'stopped'
this.status = 'stopped'
Log.info(`Engine exited with code ${code}`)
});
}
public stop() {
if(this.processStatus !== 'running') return
if (this.process.pid) {
Log.info('Trying to stop process, PID:', this.process.pid)
let cmd = `kill ${this.process.pid}`;
if (process.platform === "win32") {
cmd = `taskkill /pid ${this.process.pid} /t /f`
}
exec(cmd, (error) => {
if (error) {
controlWindow.sendErrorMessage(i18n('engine.shutdown.error') + error)
Log.error(`Failed to kill process: ${error}`)
}
})
}
else {
this.process = undefined;
allConfig.controls.engineEnabled = false
if(controlWindow.window){
allConfig.sendControls(controlWindow.window)
controlWindow.window.webContents.send('control.engine.stopped')
}
this.processStatus = 'stopped'
Log.info('Process PID undefined, caption engine process stopped')
if(this.status !== 'running'){
Log.warn('Engine is not running, current status:', this.status)
return
}
this.processStatus = 'stopping'
Log.info('Caption engine process stopping')
this.sendCommand('stop')
if(this.client){
this.client.destroy()
this.client = undefined
}
this.status = 'stopping'
Log.info('Caption engine process stopping...')
}
}
function handleEngineData(data: any) {
if(data.command === 'caption') {
if(data.command === 'ready'){
captionEngine.connect()
}
else if(data.command === 'caption') {
allConfig.updateCaptionLog(data);
}
else if(data.command === 'print') {
Log.info('Engine print:', data.content)
console.log(data.content)
// Log.info('Engine Print:', data.content)
}
else if(data.command === 'info') {
Log.info('Engine info:', data.content)
Log.info('Engine Info:', data.content)
}
else if(data.command === 'usage') {
Log.info('Caption engine usage: ', data.content)
console.error(data.content)
// Log.info('Gummy Engine Usage: ', data.content)
}
}

View File

@@ -12,10 +12,10 @@ export class Log {
}
static warn(...msg: any[]){
console.log(`[WARN ${getTimeString()}]`, ...msg)
console.warn(`[WARN ${getTimeString()}]`, ...msg)
}
static error(...msg: any[]){
console.log(`[ERROR ${getTimeString()}]`, ...msg)
console.error(`[ERROR ${getTimeString()}]`, ...msg)
}
}