mirror of
https://github.com/HiMeditator/auto-caption.git
synced 2026-02-10 17:44:48 +08:00
301 lines
10 KiB
TypeScript
301 lines
10 KiB
TypeScript
import { exec, spawn } from 'child_process'
|
|
import { app } from 'electron'
|
|
import { is } from '@electron-toolkit/utils'
|
|
import * as path from 'path'
|
|
import * as net from 'net'
|
|
import { controlWindow } from '../ControlWindow'
|
|
import { allConfig } from './AllConfig'
|
|
import { i18n } from '../i18n'
|
|
import { Log } from './Log'
|
|
import { passwordMaskingForList } from './UtilsFunc'
|
|
|
|
export class CaptionEngine {
|
|
appPath: string = ''
|
|
command: string[] = []
|
|
process: any | undefined
|
|
client: net.Socket | undefined
|
|
port: number = 8080
|
|
status: 'running' | 'starting' | 'stopping' | 'stopped' | 'starting-timeout' = 'stopped'
|
|
timerID: NodeJS.Timeout | undefined
|
|
startTimeoutID: NodeJS.Timeout | undefined
|
|
|
|
private getApp(): boolean {
|
|
if (allConfig.controls.customized) {
|
|
Log.info('Using customized caption engine')
|
|
this.appPath = allConfig.controls.customizedApp
|
|
this.command = allConfig.controls.customizedCommand.split(' ')
|
|
this.port = Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024
|
|
this.command.push('-p', this.port.toString())
|
|
}
|
|
else {
|
|
if(allConfig.controls.engine === 'gummy' &&
|
|
!allConfig.controls.API_KEY && !process.env.DASHSCOPE_API_KEY
|
|
) {
|
|
controlWindow.sendErrorMessage(i18n('gummy.key.missing'))
|
|
return false
|
|
}
|
|
this.command = []
|
|
if (is.dev) {
|
|
if(process.platform === "win32") {
|
|
this.appPath = path.join(
|
|
app.getAppPath(), 'engine',
|
|
'.venv', '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(
|
|
app.getAppPath(), 'engine',
|
|
'.venv', 'bin', 'python3'
|
|
)
|
|
this.command.push(path.join(
|
|
app.getAppPath(), 'engine', 'main.py'
|
|
))
|
|
}
|
|
}
|
|
else {
|
|
if(process.platform === 'win32') {
|
|
this.appPath = path.join(process.resourcesPath, 'engine', 'main.exe')
|
|
}
|
|
else {
|
|
this.appPath = path.join(process.resourcesPath, 'engine', 'main', 'main')
|
|
}
|
|
}
|
|
this.command.push('-a', allConfig.controls.audio ? '1' : '0')
|
|
if(allConfig.controls.recording) {
|
|
this.command.push('-r', '1')
|
|
this.command.push('-rp', `"${allConfig.controls.recordingPath}"`)
|
|
}
|
|
this.port = Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024
|
|
this.command.push('-p', this.port.toString())
|
|
this.command.push(
|
|
'-t', allConfig.controls.translation ?
|
|
allConfig.controls.targetLang : 'none'
|
|
)
|
|
|
|
if(allConfig.controls.engine === 'gummy') {
|
|
this.command.push('-e', 'gummy')
|
|
this.command.push('-s', allConfig.controls.sourceLang)
|
|
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('-vosk', `"${allConfig.controls.voskModelPath}"`)
|
|
this.command.push('-tm', allConfig.controls.transModel)
|
|
this.command.push('-omn', allConfig.controls.ollamaName)
|
|
if(allConfig.controls.ollamaUrl) this.command.push('-ourl', allConfig.controls.ollamaUrl)
|
|
if(allConfig.controls.ollamaApiKey) this.command.push('-okey', allConfig.controls.ollamaApiKey)
|
|
}
|
|
else if(allConfig.controls.engine === 'sosv'){
|
|
this.command.push('-e', 'sosv')
|
|
this.command.push('-s', allConfig.controls.sourceLang)
|
|
this.command.push('-sosv', `"${allConfig.controls.sosvModelPath}"`)
|
|
this.command.push('-tm', allConfig.controls.transModel)
|
|
this.command.push('-omn', allConfig.controls.ollamaName)
|
|
if(allConfig.controls.ollamaUrl) this.command.push('-ourl', allConfig.controls.ollamaUrl)
|
|
if(allConfig.controls.ollamaApiKey) this.command.push('-okey', allConfig.controls.ollamaApiKey)
|
|
}
|
|
else if(allConfig.controls.engine === 'glm'){
|
|
this.command.push('-e', 'glm')
|
|
this.command.push('-s', allConfig.controls.sourceLang)
|
|
this.command.push('-gurl', allConfig.controls.glmUrl)
|
|
this.command.push('-gmodel', allConfig.controls.glmModel)
|
|
if(allConfig.controls.glmApiKey) {
|
|
this.command.push('-gkey', allConfig.controls.glmApiKey)
|
|
}
|
|
this.command.push('-tm', allConfig.controls.transModel)
|
|
this.command.push('-omn', allConfig.controls.ollamaName)
|
|
if(allConfig.controls.ollamaUrl) this.command.push('-ourl', allConfig.controls.ollamaUrl)
|
|
if(allConfig.controls.ollamaApiKey) this.command.push('-okey', allConfig.controls.ollamaApiKey)
|
|
}
|
|
}
|
|
Log.info('Engine Path:', this.appPath)
|
|
Log.info('Engine Command:', passwordMaskingForList(this.command))
|
|
return true
|
|
}
|
|
|
|
public connect() {
|
|
if(this.client) { Log.warn('Client already exists, ignoring...') }
|
|
if (this.startTimeoutID) {
|
|
clearTimeout(this.startTimeoutID)
|
|
this.startTimeoutID = undefined
|
|
}
|
|
this.client = net.createConnection({ port: this.port }, () => {
|
|
Log.info('Connected to caption engine server');
|
|
});
|
|
this.status = 'running'
|
|
allConfig.controls.engineEnabled = true
|
|
if(controlWindow.window){
|
|
allConfig.sendControls(controlWindow.window, false)
|
|
controlWindow.window.webContents.send(
|
|
'control.engine.started',
|
|
this.process.pid
|
|
)
|
|
}
|
|
}
|
|
|
|
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.status !== 'stopped') {
|
|
Log.warn('Caption engine is not stopped, current status:', this.status)
|
|
return
|
|
}
|
|
if(!this.getApp()){ return }
|
|
|
|
this.process = spawn(this.appPath, this.command)
|
|
this.status = 'starting'
|
|
Log.info('Caption Engine Starting, PID:', this.process.pid)
|
|
|
|
const timeoutMs = allConfig.controls.startTimeoutSeconds * 1000
|
|
this.startTimeoutID = setTimeout(() => {
|
|
if (this.status === 'starting') {
|
|
Log.warn(`Engine start timeout after ${allConfig.controls.startTimeoutSeconds} seconds, forcing kill...`)
|
|
this.status = 'starting-timeout'
|
|
controlWindow.sendErrorMessage(i18n('engine.start.timeout'))
|
|
this.kill()
|
|
}
|
|
}, timeoutMs)
|
|
|
|
this.process.stdout.on('data', (data: any) => {
|
|
const lines = data.toString().split('\n')
|
|
lines.forEach((line: string) => {
|
|
if (line.trim()) {
|
|
try {
|
|
const data_obj = JSON.parse(line)
|
|
handleEngineData(data_obj)
|
|
} catch (e) {
|
|
// controlWindow.sendErrorMessage(i18n('engine.output.parse.error') + e)
|
|
Log.error('Error parsing JSON:', e)
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
this.process.stderr.on('data', (data: any) => {
|
|
const lines = data.toString().split('\n')
|
|
lines.forEach((line: string) => {
|
|
if(line.trim()){
|
|
Log.error(line)
|
|
}
|
|
})
|
|
});
|
|
|
|
this.process.on('close', (code: any) => {
|
|
this.process = undefined;
|
|
this.client = undefined
|
|
allConfig.controls.engineEnabled = false
|
|
if(controlWindow.window){
|
|
allConfig.sendControls(controlWindow.window, false)
|
|
controlWindow.window.webContents.send('control.engine.stopped')
|
|
}
|
|
this.status = 'stopped'
|
|
clearInterval(this.timerID)
|
|
if (this.startTimeoutID) {
|
|
clearTimeout(this.startTimeoutID)
|
|
this.startTimeoutID = undefined
|
|
}
|
|
Log.info(`Engine exited with code ${code}`)
|
|
});
|
|
}
|
|
|
|
public stop() {
|
|
if(this.status !== 'running'){
|
|
Log.warn('Trying to stop engine which is not running, current status:', this.status)
|
|
}
|
|
this.sendCommand('stop')
|
|
if(this.client){
|
|
this.client.destroy()
|
|
this.client = undefined
|
|
}
|
|
this.status = 'stopping'
|
|
this.timerID = setTimeout(() => {
|
|
if(this.status !== 'stopping') return
|
|
Log.warn('Engine process still not stopped, trying to kill...')
|
|
this.kill()
|
|
}, 4000);
|
|
}
|
|
|
|
public kill(){
|
|
if(!this.process || !this.process.pid) return
|
|
if(this.status !== 'running'){
|
|
Log.warn('Trying to kill engine which is not running, current status:', this.status)
|
|
}
|
|
Log.warn('Killing engine process, PID:', this.process.pid)
|
|
|
|
if (this.startTimeoutID) {
|
|
clearTimeout(this.startTimeoutID)
|
|
this.startTimeoutID = undefined
|
|
}
|
|
if(this.client){
|
|
this.client.destroy()
|
|
this.client = undefined
|
|
}
|
|
if (this.process.pid) {
|
|
let cmd = `kill -9 ${this.process.pid}`;
|
|
if (process.platform === "win32") {
|
|
cmd = `taskkill /pid ${this.process.pid} /t /f`
|
|
}
|
|
exec(cmd, (error) => {
|
|
if (error) {
|
|
Log.error('Failed to kill process:', error)
|
|
} else {
|
|
Log.info('Process killed successfully')
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleEngineData(data: any) {
|
|
if(data.command === 'connect'){
|
|
captionEngine.connect()
|
|
}
|
|
else if(data.command === 'kill') {
|
|
if(captionEngine.status !== 'stopped') {
|
|
Log.warn('Error occurred, trying to kill caption engine...')
|
|
captionEngine.kill()
|
|
}
|
|
}
|
|
else if(data.command === 'caption') {
|
|
allConfig.updateCaptionLog(data);
|
|
}
|
|
else if(data.command === 'translation') {
|
|
allConfig.updateCaptionTranslation(data);
|
|
}
|
|
else if(data.command === 'print') {
|
|
console.log(data.content)
|
|
}
|
|
else if(data.command === 'info') {
|
|
Log.info('Engine Info:', data.content)
|
|
}
|
|
else if(data.command === 'warn') {
|
|
Log.warn('Engine Warn:', data.content)
|
|
}
|
|
else if(data.command === 'error') {
|
|
Log.error('Engine Error:', data.content)
|
|
controlWindow.sendErrorMessage(/*i18n('engine.error') +*/ data.content)
|
|
}
|
|
else if(data.command === 'usage') {
|
|
Log.info('Engine Token Usage: ', data.content)
|
|
}
|
|
else {
|
|
Log.warn('Unknown command:', data)
|
|
}
|
|
}
|
|
|
|
export const captionEngine = new CaptionEngine()
|