feat(engine): 优化字幕引擎、提升程序健壮性

- 优化服务器启动流程,增加异常处理
- 主程序和字幕引擎的 WebSocket 端口号改为随机生成
This commit is contained in:
himeditator mac
2025-07-29 19:37:03 +08:00
parent e4f937e6b6
commit d5d692188e
17 changed files with 182 additions and 856 deletions

View File

@@ -122,7 +122,7 @@ npm install
### 构建字幕引擎
首先进入 `engine` 文件夹,执行如下指令创建虚拟环境:
首先进入 `engine` 文件夹,执行如下指令创建虚拟环境(需要使用大于等于 Python 3.10 的 Python 运行环境)
```bash
# in ./engine folder
@@ -160,11 +160,10 @@ pip install samplerate --only-binary=:all:
然后使用 `pyinstaller` 构建项目:
```bash
pyinstaller ./main-gummy.spec
pyinstaller ./main-vosk.spec
pyinstaller ./main.spec
```
注意 `main-vosk.spec` 文件中 `vosk` 库的路径可能不正确,需要根据实际状况配置。
注意 `main.spec` 文件中 `vosk` 库的路径可能不正确,需要根据实际状况配置。
```
# Windows
@@ -197,13 +196,9 @@ npm run build:linux
```yml
extraResources:
# For Windows
- from: ./engine/dist/main-gummy.exe
to: ./engine/main-gummy.exe
- from: ./engine/dist/main-vosk.exe
to: ./engine/main-vosk.exe
- from: ./engine/dist/main.exe
to: ./engine/main.exe
# For macOS and Linux
# - from: ./engine/dist/main-gummy
# to: ./engine/main-gummy
# - from: ./engine/dist/main-vosk
# to: ./engine/main-vosk
# - from: ./engine/dist/main
# to: ./engine/main
```

View File

@@ -236,13 +236,13 @@
### `control.engine.started`
**介绍:** 引擎启动成功
**介绍:** 引擎启动成功,参数为引擎的进程 ID
**发起方:** 后端
**接收方:** 前端控制窗口
**数据类型:** 无数据
**数据类型:** `number`
### `control.engine.stopped`

View File

@@ -67,7 +67,7 @@ if __name__ == "__main__":
parser.add_argument('-e', '--caption_engine', default='gummy', help='Caption engine: gummy or vosk')
parser.add_argument('-a', '--audio_type', default=0, help='Audio stream source: 0 for output, 1 for input')
parser.add_argument('-c', '--chunk_rate', default=20, help='Number of audio stream chunks collected per second')
parser.add_argument('-p', '--port', default=7070, help='The port to run the server on, 0 for no server')
parser.add_argument('-p', '--port', default=8080, help='The port to run the server on, 0 for no server')
# gummy
parser.add_argument('-s', '--source_language', default='en', help='Source language code')
parser.add_argument('-t', '--target_language', default='zh', help='Target language code')

View File

@@ -26,8 +26,13 @@ def handle_client(client_socket):
def start_server(port: int):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost', port))
server.listen(1)
try:
server.bind(('localhost', port))
server.listen(1)
except Exception as e:
stderr(str(e))
stdout_cmd('kill')
return
stdout_cmd('connect')
client, addr = server.accept()

871
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "auto-caption",
"productName": "Auto Caption",
"version": "0.5.1",
"version": "0.6.0",
"description": "A cross-platform subtitle display software.",
"main": "./out/main/index.js",
"author": "himeditator",
@@ -13,7 +13,7 @@
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "chcp 65001 && electron-vite dev",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",

View File

@@ -85,12 +85,13 @@ class ControlWindow {
ipcMain.handle('control.engine.info', async () => {
const info: EngineInfo = {
pid: 0, ppid: 0, cpu: 0, mem: 0, elapsed: 0
pid: 0, ppid: 0, port: 0, cpu: 0, mem: 0, elapsed: 0
}
if(captionEngine.processStatus !== 'running') return info
if(captionEngine.status !== 'running') return info
const stats = await pidusage(captionEngine.process.pid)
info.pid = stats.pid
info.ppid = stats.ppid
info.port = captionEngine.port
info.cpu = stats.cpu
info.mem = stats.memory
info.elapsed = stats.elapsed

View File

@@ -58,6 +58,7 @@ export interface FullConfig {
export interface EngineInfo {
pid: number,
ppid: number,
port:number,
cpu: number,
mem: number,
elapsed: number

View File

@@ -121,9 +121,9 @@ class AllConfig {
Log.info('Set Controls:', this.controls)
}
public sendControls(window: BrowserWindow) {
public sendControls(window: BrowserWindow, info = true) {
window.webContents.send('control.controls.set', this.controls)
Log.info(`Send Controls to #${window.id}:`, this.controls)
if(info) Log.info(`Send Controls to #${window.id}:`, this.controls)
}
public updateCaptionLog(log: CaptionItem) {

View File

@@ -13,6 +13,7 @@ export class CaptionEngine {
command: string[] = []
process: any | undefined
client: net.Socket | undefined
port: number = 8080
status: 'running' | 'starting' | 'stopping' | 'stopped' = 'stopped'
private getApp(): boolean {
@@ -20,6 +21,8 @@ export class CaptionEngine {
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' &&
@@ -30,19 +33,34 @@ export class CaptionEngine {
}
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.py'
))
// this.appPath = path.join(app.getAppPath(), 'engine', 'dist', 'main.exe')
if(process.platform === "win32") {
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(
app.getAppPath(), 'engine',
'subenv', 'bin', 'python3'
)
this.command.push(path.join(
app.getAppPath(), 'engine', 'main.py'
))
}
}
else {
this.appPath = path.join(process.resourcesPath, 'engine', 'main.exe')
}
this.command.push('-a', allConfig.controls.audio ? '1' : '0')
this.port = Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024
this.command.push('-p', this.port.toString())
if(allConfig.controls.engine === 'gummy') {
this.command.push('-e', 'gummy')
this.command.push('-s', allConfig.controls.sourceLang)
@@ -50,14 +68,13 @@ export class CaptionEngine {
'-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}"`)
}
}
@@ -67,15 +84,15 @@ export class CaptionEngine {
}
public connect() {
Log.info('Connecting to caption engine server...')
if(this.client) { Log.warn('Client already exists, ignoring...') }
Log.info('Connecting to caption engine server...');
this.client = net.createConnection({ port: 7070 }, () => {
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)
allConfig.sendControls(controlWindow.window, false)
controlWindow.window.webContents.send(
'control.engine.started',
this.process.pid
@@ -95,7 +112,7 @@ export class CaptionEngine {
public start() {
if (this.status !== 'stopped') {
Log.warn('Casption engine is not stopped, current status:', this.status)
Log.warn('Caption engine is not stopped, current status:', this.status)
return
}
if(!this.getApp()){ return }
@@ -134,7 +151,7 @@ export class CaptionEngine {
this.client = undefined
allConfig.controls.engineEnabled = false
if(controlWindow.window){
allConfig.sendControls(controlWindow.window)
allConfig.sendControls(controlWindow.window, false)
controlWindow.window.webContents.send('control.engine.stopped')
}
this.status = 'stopped'
@@ -144,7 +161,7 @@ export class CaptionEngine {
public stop() {
if(this.status !== 'running'){
Log.warn('Engine is not running, current status:', this.status)
Log.warn('Trying to stop engine which is not running, current status:', this.status)
return
}
this.sendCommand('stop')
@@ -158,15 +175,14 @@ export class CaptionEngine {
public kill(){
if(this.status !== 'running'){
Log.warn('Engine is not running, current status:', this.status)
return
Log.warn('Trying to kill engine which is not running, current status:', this.status)
}
Log.warn('Trying to kill engine process, PID:', this.process.pid)
if(this.client){
this.client.destroy()
this.client = undefined
}
if (this.process.pid) {
Log.warn('Trying to kill engine process, PID:', this.process.pid)
if(this.client){
this.client.destroy()
this.client = undefined
}
let cmd = `kill ${this.process.pid}`;
if (process.platform === "win32") {
cmd = `taskkill /pid ${this.process.pid} /t /f`
@@ -183,7 +199,7 @@ function handleEngineData(data: any) {
}
else if(data.command === 'kill') {
if(captionEngine.status !== 'stopped') {
Log.warn('Error occurred, trying to kill Gummy engine...')
Log.warn('Error occurred, trying to kill caption engine...')
captionEngine.kill()
}
}
@@ -197,7 +213,7 @@ function handleEngineData(data: any) {
Log.info('Engine Info:', data.content)
}
else if(data.command === 'usage') {
Log.info('Gummy Engine Usage: ', data.content)
Log.info('Engine Usage: ', data.content)
}
else {
Log.warn('Unknown command:', data)

View File

@@ -18,6 +18,10 @@
<div class="engine-status-title">ppid</div>
<div>{{ ppid }}</div>
</a-col>
<a-col :flex="1" :title="$t('status.port')" style="cursor:pointer;">
<div class="engine-status-title">port</div>
<div>{{ port }}</div>
</a-col>
<a-col :flex="1" :title="$t('status.cpu')" style="cursor:pointer;">
<div class="engine-status-title">cpu</div>
<div>{{ cpu.toFixed(1) }}%</div>
@@ -79,7 +83,7 @@
<p class="about-desc">{{ $t('status.about.desc') }}</p>
<a-divider />
<div class="about-info">
<p><b>{{ $t('status.about.version') }}</b><a-tag color="green">v0.5.1</a-tag></p>
<p><b>{{ $t('status.about.version') }}</b><a-tag color="green">v0.6.0</a-tag></p>
<p>
<b>{{ $t('status.about.author') }}</b>
<a
@@ -133,10 +137,11 @@ const pending = ref(false)
const captionLog = useCaptionLogStore()
const { captionData } = storeToRefs(captionLog)
const engineControl = useEngineControlStore()
const { engineEnabled, engine, customized } = storeToRefs(engineControl)
const { engineEnabled, engine, customized, errorSignal } = storeToRefs(engineControl)
const pid = ref(0)
const ppid = ref(0)
const port = ref(0)
const cpu = ref(0)
const mem = ref(0)
const elapsed = ref(0)
@@ -163,6 +168,7 @@ function getEngineInfo() {
window.electron.ipcRenderer.invoke('control.engine.info').then((data: EngineInfo) => {
pid.value = data.pid
ppid.value = data.ppid
port.value = data.port
cpu.value = data.cpu
mem.value = data.mem
elapsed.value = data.elapsed
@@ -172,6 +178,11 @@ function getEngineInfo() {
watch(engineEnabled, () => {
pending.value = false
})
watch(errorSignal, () => {
pending.value = false
errorSignal.value = false
})
</script>
<style scoped>

View File

@@ -22,6 +22,8 @@ export default {
"stopped": "Caption Engine Stopped",
"stoppedInfo": "The caption engine has stopped. You can click the 'Start Caption Engine' button to restart it.",
"error": "An error occurred",
"engineError": "The subtitle engine encountered an error and requested a forced exit.",
"wsError": "The WebSocket connection between the main program and the subtitle engine was not successfully established.",
"engineChange": "Cpation Engine Configuration Changed",
"changeInfo": "If the caption engine is already running, you need to restart it for the changes to take effect.",
"styleChange": "Caption Style Changed",
@@ -95,6 +97,7 @@ export default {
"pid": "Process ID",
"ppid": "Parent Process ID",
"cpu": "CPU Usage",
"port": "WebSocket Port Number",
"mem": "Memory Usage",
"elapsed": "Running Time",
"customized": "Customized",
@@ -116,7 +119,7 @@ export default {
"projLink": "Project Link",
"manual": "User Manual",
"engineDoc": "Caption Engine Manual",
"date": "July 17, 2025"
"date": "July 29, 2025"
}
},
log: {

View File

@@ -22,6 +22,8 @@ export default {
"stopped": "字幕エンジンが停止しました",
"stoppedInfo": "字幕エンジンが停止しました。再起動するには「字幕エンジンを開始」ボタンをクリックしてください。",
"error": "エラーが発生しました",
"engineError": "字幕エンジンにエラーが発生し、強制終了が要求されました。",
"wsError": "メインプログラムと字幕エンジン間の WebSocket 接続が確立されませんでした。",
"engineChange": "字幕エンジンの設定が変更されました",
"changeInfo": "字幕エンジンがすでに起動している場合、変更を有効にするには再起動が必要です。",
"styleChange": "字幕のスタイルが変更されました",
@@ -94,6 +96,7 @@ export default {
"engineStatus": "字幕エンジンの状態",
"pid": "プロセス ID",
"ppid": "親プロセス ID",
"port": "WebSocket ポート番号",
"cpu": "CPU 使用率",
"mem": "メモリ使用量",
"elapsed": "稼働時間",
@@ -116,7 +119,7 @@ export default {
"projLink": "プロジェクトリンク",
"manual": "ユーザーマニュアル",
"engineDoc": "字幕エンジンマニュアル",
"date": "2025 年 7 月 17 日"
"date": "2025 年 7 月 29 日"
}
},
log: {

View File

@@ -22,6 +22,8 @@ export default {
"stopped": "字幕引擎停止",
"stoppedInfo": "字幕引擎已经停止,可点击“启动字幕引擎”按钮重新启动",
"error": "发生错误",
"engineError": "字幕引擎发生错误并请求强制退出",
"wsError": "主程序与字幕引擎的 WebSocket 未成功连接",
"engineChange": "字幕引擎配置已更改",
"changeInfo": "如果字幕引擎已经启动,需要重启字幕引擎修改才会生效",
"styleChange": "字幕样式已修改",
@@ -94,6 +96,7 @@ export default {
"engineStatus": "字幕引擎状态",
"pid": "进程ID",
"ppid": "父进程ID",
"port": "WebSocket 端口号",
"cpu": "CPU使用率",
"mem": "内存使用量",
"elapsed": "运行时间",
@@ -116,7 +119,7 @@ export default {
"projLink": "项目链接",
"manual": "用户手册",
"engineDoc": "字幕引擎手册",
"date": "2025 年 7 月 17 日"
"date": "2025 年 7 月 29 日"
}
},
log: {

View File

@@ -29,6 +29,7 @@ export const useEngineControlStore = defineStore('engineControl', () => {
const customizedCommand = ref<string>('')
const changeSignal = ref<boolean>(false)
const errorSignal = ref<boolean>(false)
function sendControlsChange() {
const controls: Controls = {
@@ -47,7 +48,22 @@ export const useEngineControlStore = defineStore('engineControl', () => {
window.electron.ipcRenderer.send('control.controls.change', controls)
}
function setControls(controls: Controls) {
function setControls(controls: Controls, set = false) {
if(set && !engineEnabled.value && !controls.engineEnabled) {
errorSignal.value = true
notification.open({
message: t('noti.error'),
description: t("noti.engineError"),
duration: null,
icon: () => h(ExclamationCircleOutlined, { style: 'color: #ff4d4f' })
});
notification.open({
message: t('noti.error'),
description: t("noti.wsError"),
duration: null,
icon: () => h(ExclamationCircleOutlined, { style: 'color: #ff4d4f' })
});
}
sourceLang.value = controls.sourceLang
targetLang.value = controls.targetLang
engine.value = controls.engine
@@ -71,7 +87,7 @@ export const useEngineControlStore = defineStore('engineControl', () => {
}
window.electron.ipcRenderer.on('control.controls.set', (_, controls: Controls) => {
setControls(controls)
setControls(controls, true)
})
window.electron.ipcRenderer.on('control.engine.started', (_, args) => {
@@ -125,5 +141,6 @@ export const useEngineControlStore = defineStore('engineControl', () => {
sendControlsChange, // 发送最新控制消息到后端
emptyModelPathErr, // 模型路径为空时显示警告
changeSignal, // 配置改变信号
errorSignal, // 错误信号
}
})

View File

@@ -58,6 +58,7 @@ export interface FullConfig {
export interface EngineInfo {
pid: number,
ppid: number,
port:number,
cpu: number,
mem: number,
elapsed: number

View File

@@ -36,7 +36,6 @@ const { leftBarWidth, antdTheme } = storeToRefs(generalSettingStore)
background-color: var(--control-background);
}
.caption-control {
height: 100vh;
border-right: 1px solid var(--tag-color);