feat(main): 实现字幕引擎控制功能

- 新增字幕引擎启动和停止功能
- 实现控制窗口的字幕引擎状态显示
- 优化字幕日志的发送逻辑
- 重构子进程相关代码
This commit is contained in:
himeditator
2025-06-19 22:22:17 +08:00
parent 54c618aa3f
commit c446f846bd
12 changed files with 218 additions and 71 deletions

View File

@@ -30,7 +30,7 @@ class CaptionWindow {
setTimeout(() => { setTimeout(() => {
if (this.window) { if (this.window) {
sendStyles(this.window); sendStyles(this.window);
sendCaptionLog(this.window); sendCaptionLog(this.window, 'set');
} }
}, 1000); }, 1000);

View File

@@ -4,15 +4,18 @@ import { is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import { captionWindow } from './caption' import { captionWindow } from './caption'
import { import {
captionEngine,
controls,
setStyles, setStyles,
sendStyles, sendStyles,
sendCaptionLog, sendCaptionLog,
setControls setControls,
sendControls
} from './utils/config' } from './utils/config'
class ControlWindow { class ControlWindow {
window: BrowserWindow | undefined; window: BrowserWindow | undefined;
public createWindow(): void { public createWindow(): void {
this.window = new BrowserWindow({ this.window = new BrowserWindow({
icon: icon, icon: icon,
@@ -32,8 +35,9 @@ class ControlWindow {
setTimeout(() => { setTimeout(() => {
if (this.window) { if (this.window) {
sendStyles(this.window); sendStyles(this.window) // 配置初始样式
sendCaptionLog(this.window); sendCaptionLog(this.window, 'set') // 配置当前字幕记录
sendControls(this.window) // 配置字幕引擎配置
} }
}, 1000); }, 1000);
@@ -75,10 +79,25 @@ class ControlWindow {
captionWindow.window.show() captionWindow.window.show()
} }
}) })
// 字幕引擎控制配置更新 // 字幕引擎控制配置更新并启动引擎
ipcMain.on('control.control.change', (_, args) => { ipcMain.on('control.control.change', (_, args) => {
setControls(args) setControls(args)
}) })
// 启动字幕引擎
ipcMain.on('control.engine.start', () => {
if(controls.engineEnabled){
this.window?.webContents.send('control.engine.already')
}
else {
captionEngine.start()
this.window?.webContents.send('control.engine.started')
}
})
// 停止字幕引擎
ipcMain.on('control.engine.stop', () => {
captionEngine.stop()
this.window?.webContents.send('control.engine.stopped')
})
} }
} }

View File

@@ -3,9 +3,6 @@ import { electronApp, optimizer } from '@electron-toolkit/utils'
import { controlWindow } from './control' import { controlWindow } from './control'
import { captionWindow } from './caption' import { captionWindow } from './caption'
import { PythonProcess } from './utils/pythonProcess'
const pySubProcess = new PythonProcess()
app.whenReady().then(() => { app.whenReady().then(() => {
electronApp.setAppUserModelId('com.himeditator.autocaption') electronApp.setAppUserModelId('com.himeditator.autocaption')
@@ -18,8 +15,6 @@ app.whenReady().then(() => {
controlWindow.createWindow() controlWindow.createWindow()
// pySubProcess.start()
app.on('activate', function () { app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0){ if (BrowserWindow.getAllWindows().length === 0){
controlWindow.createWindow() controlWindow.createWindow()

View File

@@ -19,6 +19,7 @@ export interface CaptionItem {
} }
export interface Controls { export interface Controls {
engineEnabled: boolean,
sourceLang: string, sourceLang: string,
targetLang: string, targetLang: string,
engine: string, engine: string,

View File

@@ -1,5 +1,8 @@
import { Styles, CaptionItem, Controls } from '../types' import { Styles, CaptionItem, Controls } from '../types'
import { BrowserWindow } from 'electron' import { BrowserWindow } from 'electron'
import { CaptionEngine } from './engine'
export const captionEngine = new CaptionEngine()
export const styles: Styles = { export const styles: Styles = {
fontFamily: 'sans-serif', fontFamily: 'sans-serif',
@@ -19,12 +22,15 @@ export const controls: Controls = {
sourceLang: 'en', sourceLang: 'en',
targetLang: 'zh', targetLang: 'zh',
engine: 'gummy', engine: 'gummy',
engineEnabled: false,
translation: true, translation: true,
customized: false, customized: false,
customizedApp: '', customizedApp: '',
customizedCommand: '' customizedCommand: ''
} }
export let engineRunning: boolean = false
export function setStyles(args: any) { export function setStyles(args: any) {
styles.fontFamily = args.fontFamily styles.fontFamily = args.fontFamily
styles.fontSize = args.fontSize styles.fontSize = args.fontSize
@@ -40,23 +46,27 @@ export function setStyles(args: any) {
export function sendStyles(window: BrowserWindow) { export function sendStyles(window: BrowserWindow) {
window.webContents.send('caption.style.set', styles) window.webContents.send('caption.style.set', styles)
console.log('[INFO] Send Styles:', styles) console.log(`[INFO] Send Styles to #${window.id}:`, styles)
} }
export function sendCaptionLog(window: BrowserWindow) { export function sendCaptionLog(window: BrowserWindow, command: string) {
window.webContents.send('both.log.set', captionLog) if(command === 'add'){
window.webContents.send(`both.log.add`, captionLog[captionLog.length - 1])
}
else if(command === 'set'){
window.webContents.send(`both.log.${command}`, captionLog)
}
} }
export function addCaptionLog(log: CaptionItem) { export function addCaptionLog(log: CaptionItem) {
if(captionLog.length && captionLog[captionLog.length - 1].index === log.index) { if(captionLog.length && captionLog[captionLog.length - 1].index === log.index) {
captionLog.splice(captionLog.length - 1, 1) captionLog.splice(captionLog.length - 1, 1, log)
captionLog.push(log)
} }
else { else {
captionLog.push(log) captionLog.push(log)
} }
for(const window of BrowserWindow.getAllWindows()){ for(const window of BrowserWindow.getAllWindows()){
sendCaptionLog(window) sendCaptionLog(window, 'add')
} }
} }
@@ -69,4 +79,9 @@ export function setControls(args: any) {
controls.customizedApp = args.customizedApp controls.customizedApp = args.customizedApp
controls.customizedCommand = args.customizedCommand controls.customizedCommand = args.customizedCommand
console.log('[INFO] Set Controls:', controls) console.log('[INFO] Set Controls:', controls)
}
export function sendControls(window: BrowserWindow) {
window.webContents.send('control.control.set', controls)
console.log(`[INFO] Send Controls to #${window.id}:`, controls)
} }

79
src/main/utils/engine.ts Normal file
View File

@@ -0,0 +1,79 @@
import { spawn } from 'child_process'
import { app } from 'electron'
import path from 'path'
import { addCaptionLog, controls } from './config'
export class CaptionEngine {
appPath: string = ''
command: string[] = []
process: any | undefined
private getApp() {
if(controls.customized && controls.customizedCommand && controls.customizedApp){
this.appPath = controls.customizedApp
this.command = [ controls.customizedCommand ]
}
else if(controls.engine === 'gummy'){
this.appPath = path.join(
app.getAppPath(),
'python-subprocess', 'subenv', 'Scripts', 'python.exe'
)
this.command = []
this.command.push(path.join(
app.getAppPath(),
'python-subprocess', 'main.py'
))
this.command.push('-s', controls.sourceLang)
this.command.push('-t', controls.targetLang)
console.log(this.appPath)
console.log(this.command)
}
}
public start() {
if (this.process) {
this.stop();
}
this.getApp()
this.process = spawn(this.appPath, this.command)
controls.engineEnabled = true
console.log('[INFO] Caption Engine Started: ', {
appPath: this.appPath,
command: this.command
})
this.process.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
lines.forEach( (line: string) => {
if (line.trim()) {
try {
const caption = JSON.parse(line);
addCaptionLog(caption);
} catch (e) {
console.error('Error parsing JSON:', e);
}
}
});
});
this.process.stderr.on('data', (data) => {
console.error(`Python Error: ${data}`);
});
this.process.on('close', (code: any) => {
console.log(`Python process exited with code ${code}`);
this.process = undefined;
});
}
public stop() {
if (this.process) {
this.process.kill();
this.process = undefined;
controls.engineEnabled = false;
console.log('[INFO] Caption engine process stopped');
}
}
}

View File

@@ -1,42 +0,0 @@
import { spawn } from 'child_process'
import { app } from 'electron'
import path from 'path'
import { addCaptionLog } from './config'
export class PythonProcess {
public start() {
const basePath = app.getAppPath()
const pythonPath = path.join(
basePath,
'python-subprocess', 'subenv', 'Scripts', 'python.exe'
)
const targetPath = path.join(basePath, 'python-subprocess', 'main.py')
console.log(pythonPath)
console.log(targetPath)
const pythonProcess = spawn(pythonPath, [targetPath])
pythonProcess.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
lines.forEach( (line: string) => {
if (line.trim()) {
try {
const caption = JSON.parse(line);
addCaptionLog(caption);
} catch (e) {
console.error('Error parsing JSON:', e);
}
}
});
});
pythonProcess.stderr.on('data', (data) => {
console.error(`Python Error: ${data}`);
});
pythonProcess.on('close', (code) => {
console.log(`Python process exited with code ${code}`);
});
}
}

View File

@@ -59,12 +59,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, watch } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useCaptionControlStore } from '@renderer/stores/captionControl' import { useCaptionControlStore } from '@renderer/stores/captionControl'
const captionControl = useCaptionControlStore() const captionControl = useCaptionControlStore()
const { captionEngine } = storeToRefs(captionControl) const { captionEngine, changeSignal } = storeToRefs(captionControl)
const currentSourceLang = ref('auto') const currentSourceLang = ref('auto')
const currentTargetLang = ref('zh') const currentTargetLang = ref('zh')
@@ -107,6 +107,13 @@ function cancelChange(){
currentCustomizedApp.value = captionControl.customizedApp currentCustomizedApp.value = captionControl.customizedApp
currentCustomizedCommand.value = captionControl.customizedCommand currentCustomizedCommand.value = captionControl.customizedCommand
} }
watch(changeSignal, (val) => {
if(val == true) {
cancelChange();
captionControl.changeSignal = false;
}
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,10 +2,10 @@
<div class="caption-stat"> <div class="caption-stat">
<a-row> <a-row>
<a-col :span="6"> <a-col :span="6">
<a-statistic title="字幕引擎" :value="'gummy'" /> <a-statistic title="字幕引擎" :value="engine" />
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-statistic title="字幕引擎状态" :value="'未连接'" /> <a-statistic title="字幕引擎状态" :value="engineEnabled?'已启动':'未启动'" />
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-statistic title="已记录字幕" :value="captionData.length" /> <a-statistic title="已记录字幕" :value="captionData.length" />
@@ -14,9 +14,19 @@
</div> </div>
<div class="caption-control"> <div class="caption-control">
<a-button type="primary" class="control-button" @click="openCaptionWindow">打开字幕窗口</a-button> <a-button
<a-button class="control-button">启动字幕引擎</a-button> type="primary"
<a-button danger class="control-button">关闭字幕引擎</a-button> class="control-button"
@click="openCaptionWindow"
>打开字幕窗口</a-button>
<a-button
class="control-button"
@click="captionControl.startEngine"
>启动字幕引擎</a-button>
<a-button
danger class="control-button"
@click="captionControl.stopEngine"
>关闭字幕引擎</a-button>
</div> </div>
<div class="caption-list"> <div class="caption-list">
@@ -51,9 +61,11 @@
import { ref } from 'vue' import { ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useCaptionLogStore } from '@renderer/stores/captionLog' import { useCaptionLogStore } from '@renderer/stores/captionLog'
import { useCaptionControlStore } from '@renderer/stores/captionControl'
const captionLog = useCaptionLogStore() const captionLog = useCaptionLogStore()
const { captionData } = storeToRefs(captionLog) const { captionData } = storeToRefs(captionLog)
const captionControl = useCaptionControlStore()
const { engineEnabled, engine } = storeToRefs(captionControl)
const pagination = ref({ const pagination = ref({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
@@ -93,7 +105,6 @@ const columns = [
function openCaptionWindow() { function openCaptionWindow() {
window.electron.ipcRenderer.send('control.captionWindow.activate') window.electron.ipcRenderer.send('control.captionWindow.activate')
} }
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,5 +1,6 @@
import { ref } from 'vue' import { ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { notification } from 'ant-design-vue'
export const useCaptionControlStore = defineStore('captionControl', () => { export const useCaptionControlStore = defineStore('captionControl', () => {
const captionEngine = ref([ const captionEngine = ref([
@@ -24,18 +25,21 @@ export const useCaptionControlStore = defineStore('captionControl', () => {
] ]
}, },
]) ])
const engineEnabled = ref(false)
const sourceLang = ref<string>('auto') const sourceLang = ref<string>('en')
const targetLang = ref<string>('zh') const targetLang = ref<string>('zh')
const engine = ref<string>('gummy') const engine = ref<string>('gummy')
const translation = ref<boolean>(false) const translation = ref<boolean>(true)
const customized = ref<boolean>(false) const customized = ref<boolean>(false)
const customizedApp = ref<string>('') const customizedApp = ref<string>('')
const customizedCommand = ref<string>('') const customizedCommand = ref<string>('')
const changeSignal = ref<boolean>(false)
function sendControlChange() { function sendControlChange() {
const controls = { const controls = {
engineEnabled: engineEnabled.value,
sourceLang: sourceLang.value, sourceLang: sourceLang.value,
targetLang: targetLang.value, targetLang: targetLang.value,
engine: engine.value, engine: engine.value,
@@ -47,8 +51,52 @@ export const useCaptionControlStore = defineStore('captionControl', () => {
window.electron.ipcRenderer.send('control.control.change', controls) window.electron.ipcRenderer.send('control.control.change', controls)
} }
function startEngine() {
window.electron.ipcRenderer.send('control.engine.start')
}
function stopEngine() {
window.electron.ipcRenderer.send('control.engine.stop')
}
window.electron.ipcRenderer.on('control.control.set', (_, controls) => {
sourceLang.value = controls.sourceLang
targetLang.value = controls.targetLang
engine.value = controls.engine
translation.value = controls.translation
customized.value = controls.customized
customizedApp.value = controls.customizedApp
customizedCommand.value = controls.customizedCommand
changeSignal.value = true
})
window.electron.ipcRenderer.on('control.engine.already', () => {
engineEnabled.value = true
notification.open({
message: '字幕引擎已经启动',
description: '字幕引擎已经启动,请勿重复启动'
});
})
window.electron.ipcRenderer.on('control.engine.started', () => {
engineEnabled.value = true
notification.open({
message: '字幕引擎启动',
description: `原语言:${sourceLang.value},是否翻译:${translation.value?'是':'否'},翻译语言:${targetLang.value}`
});
})
window.electron.ipcRenderer.on('control.engine.stopped', () => {
engineEnabled.value = false
notification.open({
message: '字幕引擎停止',
description: '可点击“启动字幕引擎”按钮重新启动'
});
})
return { return {
captionEngine, // 字幕引擎 captionEngine, // 字幕引擎
engineEnabled, // 字幕引擎是否启用
sourceLang, // 源语言 sourceLang, // 源语言
targetLang, // 目标语言 targetLang, // 目标语言
engine, // 字幕引擎 engine, // 字幕引擎
@@ -56,6 +104,9 @@ export const useCaptionControlStore = defineStore('captionControl', () => {
customized, // 是否使用自定义字幕引擎 customized, // 是否使用自定义字幕引擎
customizedApp, // 自定义字幕引擎的应用程序 customizedApp, // 自定义字幕引擎的应用程序
customizedCommand, // 自定义字幕引擎的命令 customizedCommand, // 自定义字幕引擎的命令
sendControlChange // 发送最新控制消息到后端 sendControlChange, // 发送最新控制消息到后端
startEngine, // 启动字幕引擎
stopEngine, // 停止字幕引擎
changeSignal, // 配置改变信号
} }
}) })

View File

@@ -12,6 +12,15 @@ interface CaptionItem {
export const useCaptionLogStore = defineStore('captionLog', () => { export const useCaptionLogStore = defineStore('captionLog', () => {
const captionData = ref<CaptionItem[]>([]) const captionData = ref<CaptionItem[]>([])
window.electron.ipcRenderer.on('both.log.add', (_, log) => {
if(captionData.value.length && log.index === captionData.value[captionData.value.length - 1].index) {
captionData.value.splice(captionData.value.length - 1, 1, log)
}
else {
captionData.value.push(log)
}
})
window.electron.ipcRenderer.on('both.log.set', (_, logs) => { window.electron.ipcRenderer.on('both.log.set', (_, logs) => {
captionData.value = logs captionData.value = logs
}) })

View File

@@ -7,10 +7,12 @@ export const useCaptionStyleStore = defineStore('captionStyle', () => {
const fontColor = ref<string>('#000000') const fontColor = ref<string>('#000000')
const background = ref<string>('#dbe2ef') const background = ref<string>('#dbe2ef')
const opacity = ref<number>(80) const opacity = ref<number>(80)
const transDisplay = ref<boolean>(true) const transDisplay = ref<boolean>(true)
const transFontFamily = ref<string>('sans-serif') const transFontFamily = ref<string>('sans-serif')
const transFontSize = ref<number>(24) const transFontSize = ref<number>(24)
const transFontColor = ref<string>('#000000') const transFontColor = ref<string>('#000000')
const changeSignal = ref<boolean>(false) const changeSignal = ref<boolean>(false)
function addOpicityToColor(color: string, opicity: number) { function addOpicityToColor(color: string, opicity: number) {