From cd9f3a847de3800bd8cafb2d6615f6f6301044ad Mon Sep 17 00:00:00 2001 From: himeditator Date: Mon, 28 Jul 2025 15:49:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(engine):=20=E9=87=8D=E6=9E=84=E5=AD=97?= =?UTF-8?q?=E5=B9=95=E5=BC=95=E6=93=8E=E5=B9=B6=E5=AE=9E=E7=8E=B0=20WebSoc?= =?UTF-8?q?ket=20=E9=80=9A=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构了 Gummy 和 Vosk 字幕引擎的代码,提高了可扩展性和可读性 - 合并 Gummy 和 Vosk 引擎为单个可执行文件 - 实现了字幕引擎和主程序之间的 WebSocket 通信,避免了孤儿进程问题 --- docs/CHANGELOG.md | 15 ++ docs/TODO.md | 3 +- electron-builder.yml | 13 +- engine/audio2text/__init__.py | 3 +- engine/audio2text/gummy.py | 3 +- engine/audio2text/vosk.py | 15 +- engine/main-gummy.py | 49 ------ engine/main-gummy.spec | 39 ----- engine/main-vosk.py | 77 --------- engine/main.py | 69 +++++++- engine/{main-vosk.spec => main.spec} | 4 +- engine/utils/__init__.py | 6 +- engine/utils/{process.py => audioprcs.py} | 21 ++- engine/utils/server.py | 37 +++++ engine/utils/thdata.py | 5 + src/main/utils/CaptionEngine.ts | 166 +++++++++---------- src/main/utils/Log.ts | 4 +- src/renderer/src/components/EngineStatus.vue | 4 +- src/renderer/src/stores/engineControl.ts | 2 +- 19 files changed, 242 insertions(+), 293 deletions(-) delete mode 100644 engine/main-gummy.py delete mode 100644 engine/main-gummy.spec delete mode 100644 engine/main-vosk.py rename engine/{main-vosk.spec => main.spec} (95%) rename engine/utils/{process.py => audioprcs.py} (82%) create mode 100644 engine/utils/server.py create mode 100644 engine/utils/thdata.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d6fcad9..a1afb67 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -114,3 +114,18 @@ - 修复无法调用自定义字幕引擎的 bug - 修复自定义字幕引擎的参数失效 bug + +## v0.6.0 + +2025-07-xx + +### 新增功能 + +- 新增字幕记录排序功能,可选择字幕记录正序或倒叙显示 + +### 项目优化 + +- 重构字幕引擎,提示字幕引擎代码的可扩展性和可读性 +- 合并 Gummy 和 Vosk 引擎为单个可执行文件 +- 字幕引擎和主程序添加 WebScoket 通信,完全避免字幕引擎成为孤儿进程 + diff --git a/docs/TODO.md b/docs/TODO.md index 71bad11..a725c93 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -16,10 +16,11 @@ - [x] 可以导出 srt 格式的字幕记录 *2025/07/14* - [x] 可以获取字幕引擎的系统资源消耗情况 *2025/07/15* - [x] 添加字幕记录按时间降序排列选择 *2025/07/26* +- [x] 重构字幕引擎 *2025/07/28* ## 待完成 -- [ ] 重构字幕引擎 +- [ ] 优化前端界面提示消息 - [ ] 验证 / 添加基于 sherpa-onnx 的字幕引擎 ## 后续计划 diff --git a/electron-builder.yml b/electron-builder.yml index 4dce5d8..4cfad9d 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -11,20 +11,15 @@ files: - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' - '!engine/*' - - '!engine-test/*' - '!docs/*' - '!assets/*' 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 win: executableName: auto-caption icon: build/icon.png diff --git a/engine/audio2text/__init__.py b/engine/audio2text/__init__.py index 6192084..988d76e 100644 --- a/engine/audio2text/__init__.py +++ b/engine/audio2text/__init__.py @@ -1,2 +1,3 @@ from dashscope.common.error import InvalidParameter -from .gummy import GummyTranslator +from .gummy import GummyRecognizer +from .vosk import VoskRecognizer \ No newline at end of file diff --git a/engine/audio2text/gummy.py b/engine/audio2text/gummy.py index e49f47b..1f503b8 100644 --- a/engine/audio2text/gummy.py +++ b/engine/audio2text/gummy.py @@ -62,7 +62,7 @@ class Callback(TranslationRecognizerCallback): stdout_obj(caption) -class GummyTranslator: +class GummyRecognizer: """ 使用 Gummy 引擎流式处理的音频数据,并在标准输出中输出与 Auto Caption 软件可读取的 JSON 字符串数据 @@ -70,6 +70,7 @@ class GummyTranslator: rate: 音频采样率 source: 源语言代码字符串(zh, en, ja 等) target: 目标语言代码字符串(zh, en, ja 等) + api_key: 阿里云百炼平台 API KEY """ def __init__(self, rate: int, source: str, target: str | None, api_key: str | None): if api_key: diff --git a/engine/audio2text/vosk.py b/engine/audio2text/vosk.py index 7c34459..402b7fd 100644 --- a/engine/audio2text/vosk.py +++ b/engine/audio2text/vosk.py @@ -2,7 +2,8 @@ import json from datetime import datetime from vosk import Model, KaldiRecognizer, SetLogLevel -from utils import stdout_obj +from utils import stdout_cmd, stdout_obj + class VoskRecognizer: """ @@ -11,7 +12,7 @@ class VoskRecognizer: 初始化参数: model_path: Vosk 识别模型路径 """ - def __int__(self, model_path: str): + def __init__(self, model_path: str): SetLogLevel(-1) if model_path.startswith('"'): model_path = model_path[1:] @@ -24,7 +25,11 @@ class VoskRecognizer: self.model = Model(self.model_path) self.recognizer = KaldiRecognizer(self.model, 16000) - + + def start(self): + """启动 Vosk 引擎""" + stdout_cmd('info', 'Vosk recognizer started.') + def send_audio_frame(self, data: bytes): """ 发送音频帧给 Vosk 引擎,引擎将自动识别并将识别结果输出到标准输出中 @@ -57,3 +62,7 @@ class VoskRecognizer: self.prev_content = content stdout_obj(caption) + + def stop(self): + """停止 Vosk 引擎""" + stdout_cmd('info', 'Vosk recognizer closed.') \ No newline at end of file diff --git a/engine/main-gummy.py b/engine/main-gummy.py deleted file mode 100644 index 690faae..0000000 --- a/engine/main-gummy.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -import argparse -from sysaudio import AudioStream -from utils import merge_chunk_channels -from audio2text import InvalidParameter, GummyTranslator - - -def convert_audio_to_text(s_lang, t_lang, audio_type, chunk_rate, api_key): - stream = AudioStream(audio_type, chunk_rate) - - if t_lang == 'none': - gummy = GummyTranslator(stream.RATE, s_lang, None, api_key) - else: - gummy = GummyTranslator(stream.RATE, s_lang, t_lang, api_key) - - stream.open_stream() - gummy.start() - - while True: - try: - chunk = stream.read_chunk() - if chunk is None: continue - chunk_mono = merge_chunk_channels(chunk, stream.CHANNELS) - try: - gummy.send_audio_frame(chunk_mono) - except InvalidParameter: - gummy.start() - gummy.send_audio_frame(chunk_mono) - except KeyboardInterrupt: - stream.close_stream() - gummy.stop() - break - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Convert system audio stream to text') - parser.add_argument('-s', '--source_language', default='en', help='Source language code') - parser.add_argument('-t', '--target_language', default='zh', help='Target language code') - parser.add_argument('-a', '--audio_type', default=0, help='Audio stream source: 0 for output audio stream, 1 for input audio stream') - parser.add_argument('-c', '--chunk_rate', default=20, help='The number of audio stream chunks collected per second.') - parser.add_argument('-k', '--api_key', default='', help='API KEY for Gummy model') - args = parser.parse_args() - convert_audio_to_text( - args.source_language, - args.target_language, - int(args.audio_type), - int(args.chunk_rate), - args.api_key - ) diff --git a/engine/main-gummy.spec b/engine/main-gummy.spec deleted file mode 100644 index cada0d1..0000000 --- a/engine/main-gummy.spec +++ /dev/null @@ -1,39 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -a = Analysis( - ['main-gummy.py'], - pathex=[], - binaries=[], - datas=[], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], - name='main-gummy', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, - onefile=True, -) diff --git a/engine/main-vosk.py b/engine/main-vosk.py deleted file mode 100644 index 5a3fb32..0000000 --- a/engine/main-vosk.py +++ /dev/null @@ -1,77 +0,0 @@ -import sys -import json -import argparse -from datetime import datetime -import numpy.core.multiarray - -from sysaudio import AudioStream -from vosk import Model, KaldiRecognizer, SetLogLevel -from utils import resample_chunk_mono - -SetLogLevel(-1) - -def convert_audio_to_text(audio_type, chunk_rate, model_path): - sys.stdout.reconfigure(line_buffering=True) # type: ignore - - if model_path.startswith('"'): - model_path = model_path[1:] - if model_path.endswith('"'): - model_path = model_path[:-1] - - model = Model(model_path) - recognizer = KaldiRecognizer(model, 16000) - - stream = AudioStream(audio_type, chunk_rate) - stream.open_stream() - - time_str = '' - cur_id = 0 - prev_content = '' - - while True: - chunk = stream.read_chunk() - if chunk is None: continue - chunk_mono = resample_chunk_mono(chunk, stream.CHANNELS, stream.RATE, 16000) - - caption = {} - if recognizer.AcceptWaveform(chunk_mono): - content = json.loads(recognizer.Result()).get('text', '') - caption['index'] = cur_id - caption['text'] = content - caption['time_s'] = time_str - caption['time_t'] = datetime.now().strftime('%H:%M:%S.%f')[:-3] - caption['translation'] = '' - prev_content = '' - cur_id += 1 - else: - content = json.loads(recognizer.PartialResult()).get('partial', '') - if content == '' or content == prev_content: - continue - if prev_content == '': - time_str = datetime.now().strftime('%H:%M:%S.%f')[:-3] - caption['command'] = 'caption' - caption['index'] = cur_id - caption['text'] = content - caption['time_s'] = time_str - caption['time_t'] = datetime.now().strftime('%H:%M:%S.%f')[:-3] - caption['translation'] = '' - prev_content = content - try: - json_str = json.dumps(caption) + '\n' - sys.stdout.write(json_str) - sys.stdout.flush() - except Exception as e: - print(e) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Convert system audio stream to text') - parser.add_argument('-a', '--audio_type', default=0, help='Audio stream source: 0 for output audio stream, 1 for input audio stream') - parser.add_argument('-c', '--chunk_rate', default=20, help='The number of audio stream chunks collected per second.') - parser.add_argument('-m', '--model_path', default='', help='The path to the vosk model.') - args = parser.parse_args() - convert_audio_to_text( - int(args.audio_type), - int(args.chunk_rate), - args.model_path - ) diff --git a/engine/main.py b/engine/main.py index 6ea3d53..09583e5 100644 --- a/engine/main.py +++ b/engine/main.py @@ -1,10 +1,59 @@ import argparse +from utils import stdout_cmd +from utils import thread_data, start_server +from utils import merge_chunk_channels, resample_chunk_mono +from audio2text import InvalidParameter, GummyRecognizer +from audio2text import VoskRecognizer +from sysaudio import AudioStream -def gummy_engine(s, t, a, c, k): - pass -def vosk_engine(a, c, m): - pass +def main_gummy(s: str, t: str, a: int, c: int, k: str): + stream = AudioStream(a, c) + if t == 'none': + engine = GummyRecognizer(stream.RATE, s, None, k) + else: + engine = GummyRecognizer(stream.RATE, s, t, k) + + stream.open_stream() + engine.start() + + while thread_data.status == "running": + try: + chunk = stream.read_chunk() + if chunk is None: continue + chunk_mono = merge_chunk_channels(chunk, stream.CHANNELS) + try: + engine.send_audio_frame(chunk_mono) + except InvalidParameter: + stdout_cmd('info', 'Gummy engine stopped, restart engine') + engine.start() + engine.send_audio_frame(chunk_mono) + except KeyboardInterrupt: + break + + stream.close_stream() + engine.stop() + + +def main_vosk(a: int, c: int, m: str): + stream = AudioStream(a, c) + engine = VoskRecognizer(m) + + stream.open_stream() + engine.start() + + while thread_data.status == "running": + try: + chunk = stream.read_chunk() + if chunk is None: continue + chunk_mono = resample_chunk_mono(chunk, stream.CHANNELS, stream.RATE, 16000) + engine.send_audio_frame(chunk_mono) + except KeyboardInterrupt: + break + + stream.close_stream() + engine.stop() + if __name__ == "__main__": parser = argparse.ArgumentParser(description='Convert system audio stream to text') @@ -12,15 +61,23 @@ 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') # 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') parser.add_argument('-k', '--api_key', default='', help='API KEY for Gummy model') # vosk parser.add_argument('-m', '--model_path', default='', help='The path to the vosk model.') + # for test args = parser.parse_args() + + if int(args.port) == 0: + thread_data.status = "running" + else: + start_server(int(args.port)) + if args.caption_engine == 'gummy': - gummy_engine( + main_gummy( args.source_language, args.target_language, int(args.audio_type), @@ -28,7 +85,7 @@ if __name__ == "__main__": args.api_key ) elif args.caption_engine == 'vosk': - vosk_engine( + main_vosk( int(args.audio_type), int(args.chunk_rate), args.model_path diff --git a/engine/main-vosk.spec b/engine/main.spec similarity index 95% rename from engine/main-vosk.spec rename to engine/main.spec index 0324833..81fdd2e 100644 --- a/engine/main-vosk.spec +++ b/engine/main.spec @@ -9,7 +9,7 @@ else: vosk_path = str(Path('./subenv/lib/python3.12/site-packages/vosk').resolve()) a = Analysis( - ['main-vosk.py'], + ['main.py'], pathex=[], binaries=[], datas=[(vosk_path, 'vosk')], @@ -30,7 +30,7 @@ exe = EXE( a.binaries, a.datas, [], - name='main-vosk', + name='main', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/engine/utils/__init__.py b/engine/utils/__init__.py index 2589863..5de3464 100644 --- a/engine/utils/__init__.py +++ b/engine/utils/__init__.py @@ -1,2 +1,4 @@ -from .process import merge_chunk_channels, resample_chunk_mono, resample_mono_chunk -from .sysout import stdout, stdout_cmd, stdout_obj, stderr \ No newline at end of file +from .audioprcs import merge_chunk_channels, resample_chunk_mono, resample_mono_chunk +from .sysout import stdout, stdout_cmd, stdout_obj, stderr +from .thdata import thread_data +from .server import start_server \ No newline at end of file diff --git a/engine/utils/process.py b/engine/utils/audioprcs.py similarity index 82% rename from engine/utils/process.py rename to engine/utils/audioprcs.py index 01c854d..7f24563 100644 --- a/engine/utils/process.py +++ b/engine/utils/audioprcs.py @@ -1,6 +1,6 @@ import samplerate import numpy as np - +import numpy.core.multiarray def merge_chunk_channels(chunk: bytes, channels: int) -> bytes: """ @@ -13,6 +13,7 @@ def merge_chunk_channels(chunk: bytes, channels: int) -> bytes: Returns: 单通道音频数据块 """ + if channels == 1: return chunk # (length * channels,) chunk_np = np.frombuffer(chunk, dtype=np.int16) # (length, channels) @@ -37,13 +38,17 @@ def resample_chunk_mono(chunk: bytes, channels: int, orig_sr: int, target_sr: in Return: 单通道音频数据块 """ - # (length * channels,) - chunk_np = np.frombuffer(chunk, dtype=np.int16) - # (length, channels) - chunk_np = chunk_np.reshape(-1, channels) - # (length,) - chunk_mono_f = np.mean(chunk_np.astype(np.float32), axis=1) - chunk_mono = chunk_mono_f.astype(np.int16) + if channels == 1: + chunk_mono = chunk + else: + # (length * channels,) + chunk_np = np.frombuffer(chunk, dtype=np.int16) + # (length, channels) + chunk_np = chunk_np.reshape(-1, channels) + # (length,) + chunk_mono_f = np.mean(chunk_np.astype(np.float32), axis=1) + chunk_mono = chunk_mono_f.astype(np.int16) + ratio = target_sr / orig_sr chunk_mono_r = samplerate.resample(chunk_mono, ratio, converter_type=mode) chunk_mono_r = np.round(chunk_mono_r).astype(np.int16) diff --git a/engine/utils/server.py b/engine/utils/server.py new file mode 100644 index 0000000..48fe3ce --- /dev/null +++ b/engine/utils/server.py @@ -0,0 +1,37 @@ +import socket +import threading +import json +from utils import thread_data, stdout_cmd, stderr + + +def handle_client(client_socket): + global thread_data + while True: + try: + data = client_socket.recv(4096).decode('utf-8') + if not data: + break + data = json.loads(data) + + if data['command'] == 'stop': + if thread_data.status == 'running': + thread_data.status = 'stop' + break + except Exception as e: + stderr(f'Communication error: {e}') + break + + thread_data.status = 'stop' + client_socket.close() + + +def start_server(port: int): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.bind(('localhost', port)) + server.listen(1) + stdout_cmd('ready') + + client, addr = server.accept() + client_handler = threading.Thread(target=handle_client, args=(client,)) + client_handler.daemon = True + client_handler.start() diff --git a/engine/utils/thdata.py b/engine/utils/thdata.py new file mode 100644 index 0000000..656880f --- /dev/null +++ b/engine/utils/thdata.py @@ -0,0 +1,5 @@ +class ThreadData: + def __init__(self): + self.status = "running" + +thread_data = ThreadData() \ No newline at end of file diff --git a/src/main/utils/CaptionEngine.ts b/src/main/utils/CaptionEngine.ts index 8f102b5..6bd3408 100644 --- a/src/main/utils/CaptionEngine.ts +++ b/src/main/utils/CaptionEngine.ts @@ -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) } } diff --git a/src/main/utils/Log.ts b/src/main/utils/Log.ts index f2568ce..93f1022 100644 --- a/src/main/utils/Log.ts +++ b/src/main/utils/Log.ts @@ -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) } } diff --git a/src/renderer/src/components/EngineStatus.vue b/src/renderer/src/components/EngineStatus.vue index 607433e..f838d72 100644 --- a/src/renderer/src/components/EngineStatus.vue +++ b/src/renderer/src/components/EngineStatus.vue @@ -4,7 +4,7 @@ @@ -130,7 +130,7 @@ const showAbout = ref(false) const captionLog = useCaptionLogStore() const { captionData } = storeToRefs(captionLog) const engineControl = useEngineControlStore() -const { engineEnabled, engine, customized, customizedApp } = storeToRefs(engineControl) +const { engineEnabled, engine, customized } = storeToRefs(engineControl) const pid = ref(0) const ppid = ref(0) diff --git a/src/renderer/src/stores/engineControl.ts b/src/renderer/src/stores/engineControl.ts index 5a6d13b..64f3fb9 100644 --- a/src/renderer/src/stores/engineControl.ts +++ b/src/renderer/src/stores/engineControl.ts @@ -82,7 +82,7 @@ export const useEngineControlStore = defineStore('engineControl', () => { notification.open({ message: t('noti.started'), description: - ((customized.value && customizedApp.value) ? str1 : str0) + + (customized.value ? str1 : str0) + `${t('noti.pidInfo')}${args}` }); })