diff --git a/.npmrc b/.npmrc index 34862ff..dcfa498 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -electron_mirror=https://npmmirror.com/mirrors/electron/ -electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ +# electron_mirror=https://npmmirror.com/mirrors/electron/ +# electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c18b5..cbefb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,4 +19,9 @@ ### 新增文档 - 添加用户说明文档 -- 添加字幕引擎说明文档 \ No newline at end of file +- 添加字幕引擎说明文档 + +## v1.0.0 + +> 预计为稳定版,之后除非大改,否则版本号第一位不再改变。 + diff --git a/assets/engine-manual_zh.md b/assets/engine-manual_zh.md index c70edc1..b19aeb8 100644 --- a/assets/engine-manual_zh.md +++ b/assets/engine-manual_zh.md @@ -4,7 +4,7 @@ ## 字幕引擎介绍 -所谓的字幕引擎实际上是一个子程序,它会实时获取系统音频输入(录音)或输出(播放声音)的流式数据,并调用音频转文字的模型生成对应音频的字幕。生成的字幕通过 IPC 输出为转换为 JSON 格式的字符串数据,并返回给主程序。主程序读取字幕数据,处理后显示在窗口上。 +所谓的字幕引擎实际上是一个子程序,它会实时获取系统音频输入(录音)或输出(播放声音)的流式数据,并调用音频转文字的模型生成对应音频的字幕。生成的字幕转换为 JSON 格式的字符串数据,并通过 IPC 传递给主程序。主程序读取字幕数据,处理后显示在窗口上。 ## 字幕引擎需要实现的功能 diff --git a/package.json b/package.json index 94207ae..dda2ca1 100644 --- a/package.json +++ b/package.json @@ -25,15 +25,13 @@ "@electron-toolkit/utils": "^4.0.0", "ant-design-vue": "^4.2.6", "pinia": "^3.0.2", - "vue-router": "^4.5.1", - "ws": "^8.18.2" + "vue-router": "^4.5.1" }, "devDependencies": { "@electron-toolkit/eslint-config-prettier": "3.0.0", "@electron-toolkit/eslint-config-ts": "^3.0.0", "@electron-toolkit/tsconfig": "^1.0.1", "@types/node": "^22.14.1", - "@types/ws": "^8.18.1", "@vitejs/plugin-vue": "^5.2.3", "electron": "^35.1.5", "electron-builder": "^25.1.8", diff --git a/python-subprocess/requirements.txt b/python-subprocess/requirements.txt index 7276312..d888ae5 100644 Binary files a/python-subprocess/requirements.txt and b/python-subprocess/requirements.txt differ diff --git a/src/main/caption.ts b/src/main/CaptionWindow.ts similarity index 85% rename from src/main/caption.ts rename to src/main/CaptionWindow.ts index ea3010d..a28c13f 100644 --- a/src/main/caption.ts +++ b/src/main/CaptionWindow.ts @@ -2,13 +2,13 @@ import { shell, BrowserWindow, ipcMain } from 'electron' import path from 'path' import { is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' -import { controlWindow } from './control' -import { sendStyles, sendCaptionLog } from './utils/config' +import { controlWindow } from './ControlWindow' +import { allConfig } from './utils/AllConfig' -class CaptionWindow { +class CaptionWindow { window: BrowserWindow | undefined; - - public createWindow(): void { + + public createWindow(): void { this.window = new BrowserWindow({ icon: icon, width: 900, @@ -26,11 +26,11 @@ class CaptionWindow { sandbox: false } }) - + setTimeout(() => { if (this.window) { - sendStyles(this.window); - sendCaptionLog(this.window, 'set'); + allConfig.sendStyles(this.window); + allConfig.sendCaptionLog(this.window, 'set'); } }, 1000); @@ -46,7 +46,7 @@ class CaptionWindow { shell.openExternal(details.url) return { action: 'deny' } }) - + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { this.window.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/#/caption`) } else { @@ -57,7 +57,7 @@ class CaptionWindow { } public handleMessage() { - // 字幕窗口请求创建控制窗口 + // 激活控制窗口 ipcMain.on('caption.controlWindow.activate', () => { if(!controlWindow.window){ controlWindow.createWindow() @@ -66,18 +66,21 @@ class CaptionWindow { controlWindow.window.show() } }) + // 字幕窗口高度发生变化 ipcMain.on('caption.windowHeight.change', (_, height) => { if(this.window){ - this.window.setSize(this.window.getSize()[0], height) + this.window.setSize(this.window.getSize()[0], height) } }) + // 关闭字幕窗口 ipcMain.on('caption.window.close', () => { if(this.window){ this.window.close() } }) + // 是否固定在最前面 ipcMain.on('caption.pin.set', (_, pinned) => { if(this.window){ diff --git a/src/main/control.ts b/src/main/ControlWindow.ts similarity index 62% rename from src/main/control.ts rename to src/main/ControlWindow.ts index a3cf626..7a622f9 100644 --- a/src/main/control.ts +++ b/src/main/ControlWindow.ts @@ -2,20 +2,9 @@ import { shell, BrowserWindow, ipcMain } from 'electron' import path from 'path' import { is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' -import { captionWindow } from './caption' -import { - captionEngine, - captionLog, - controls, - setStyles, - resetStyles, - sendStyles, - sendCaptionLog, - setControls, - sendControls, - readConfig, - writeConfig -} from './utils/config' +import { captionWindow } from './CaptionWindow' +import { allConfig } from './utils/AllConfig' +import { captionEngine } from './utils/CaptionEngine' class ControlWindow { window: BrowserWindow | undefined; @@ -25,8 +14,8 @@ class ControlWindow { icon: icon, width: 1200, height: 800, - minWidth: 900, - minHeight: 600, + minWidth: 600, + minHeight: 400, show: false, center: true, autoHideMenuBar: true, @@ -37,30 +26,30 @@ class ControlWindow { } }) + allConfig.readConfig() + setTimeout(() => { if (this.window) { - readConfig() - sendStyles(this.window) // 配置初始样式 - sendCaptionLog(this.window, 'set') // 配置当前字幕记录 - sendControls(this.window) // 配置字幕引擎配置 + allConfig.sendStyles(this.window) + allConfig.sendControls(this.window) + allConfig.sendCaptionLog(this.window, 'set') } }, 1000); - this.window.on('ready-to-show', () => { this.window?.show() }) - + this.window.on('closed', () => { this.window = undefined - writeConfig() + allConfig.writeConfig() }) - + this.window.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) - + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { this.window.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { @@ -69,23 +58,26 @@ class ControlWindow { } public handleMessage() { - // 控制窗口样式更新 + // 样式变更 ipcMain.on('control.style.change', (_, args) => { - setStyles(args) + allConfig.setStyles(args) if(captionWindow.window){ - sendStyles(captionWindow.window) + allConfig.sendStyles(captionWindow.window) } }) + + // 样式重置 ipcMain.on('control.style.reset', () => { - resetStyles() - if(captionWindow.window){ - sendStyles(captionWindow.window) - } + allConfig.resetStyles() if(this.window){ - sendStyles(this.window) + allConfig.sendStyles(this.window) + } + if(captionWindow.window){ + allConfig.sendStyles(captionWindow.window) } }) - // 控制窗口请求创建字幕窗口 + + // 激活字幕窗口 ipcMain.on('control.captionWindow.activate', () => { if(!captionWindow.window){ captionWindow.createWindow() @@ -94,37 +86,31 @@ class ControlWindow { captionWindow.window.show() } }) - // 字幕引擎控制配置更新并启动引擎 + + // 字幕引擎配置更新 ipcMain.on('control.control.change', (_, args) => { - setControls(args) + allConfig.setControls(args) }) + // 启动字幕引擎 ipcMain.on('control.engine.start', () => { - if(controls.engineEnabled){ + if(allConfig.controls.engineEnabled){ this.window?.webContents.send('control.engine.already') } else { - if( - process.env.DASHSCOPE_API_KEY || - (controls.customized && controls.customizedApp) - ) { - if(this.window){ - captionEngine.start(this.window) - } - } - else { - this.sendErrorMessage('没有检测到 DASHSCOPE_API_KEY 环境变量,如果要使用 gummy 引擎,需要在阿里云百炼平台获取 API Key 并添加到本机环境变量') - } + captionEngine.start() } }) + // 停止字幕引擎 ipcMain.on('control.engine.stop', () => { captionEngine.stop() this.window?.webContents.send('control.engine.stopped') }) + // 清空字幕记录 ipcMain.on('control.caption.clear', () => { - captionLog.splice(0) + allConfig.captionLog.splice(0) }) } @@ -133,4 +119,4 @@ class ControlWindow { } } -export const controlWindow = new ControlWindow() \ No newline at end of file +export const controlWindow = new ControlWindow() diff --git a/src/main/index.ts b/src/main/index.ts index 513d773..fd27cac 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,8 +1,9 @@ import { app, BrowserWindow } from 'electron' import { electronApp, optimizer } from '@electron-toolkit/utils' -import { controlWindow } from './control' -import { captionWindow } from './caption' -import { captionEngine, writeConfig } from './utils/config' +import { controlWindow } from './ControlWindow' +import { captionWindow } from './CaptionWindow' +import { allConfig } from './utils/AllConfig' +import { captionEngine } from './utils/CaptionEngine' app.whenReady().then(() => { electronApp.setAppUserModelId('com.himeditator.autocaption') @@ -23,9 +24,9 @@ app.whenReady().then(() => { }) }) -app.on('will-quit', async () => { +app.on('will-quit', async () => { captionEngine.stop() - writeConfig() + allConfig.writeConfig() }); app.on('window-all-closed', () => { diff --git a/src/main/types/index.ts b/src/main/types/index.ts index 90e76e5..05f2b4f 100644 --- a/src/main/types/index.ts +++ b/src/main/types/index.ts @@ -1,3 +1,5 @@ +export type UILanguage = "zh" | "en" | "ja" + export interface Styles { fontFamily: string, fontSize: number, @@ -22,10 +24,10 @@ export interface Controls { engineEnabled: boolean, sourceLang: string, targetLang: string, - engine: string, + engine: 'gummy', audio: 0 | 1, translation: boolean, customized: boolean, customizedApp: string, customizedCommand: string -} \ No newline at end of file +} diff --git a/src/main/utils/AllConfig.ts b/src/main/utils/AllConfig.ts new file mode 100644 index 0000000..a67b74d --- /dev/null +++ b/src/main/utils/AllConfig.ts @@ -0,0 +1,106 @@ +import { Styles, CaptionItem, Controls } from '../types' +import { app, BrowserWindow } from 'electron' +import * as path from 'path' +import * as fs from 'fs' + +const defaultStyles: Styles = { + fontFamily: 'sans-serif', + fontSize: 24, + fontColor: '#000000', + background: '#dbe2ef', + opacity: 80, + transDisplay: true, + transFontFamily: 'sans-serif', + transFontSize: 24, + transFontColor: '#000000' +}; + +const defaultControls: Controls = { + sourceLang: 'en', + targetLang: 'zh', + engine: 'gummy', + audio: 0, + engineEnabled: false, + translation: true, + customized: false, + customizedApp: '', + customizedCommand: '' +}; + + +class AllConfig { + styles: Styles = {...defaultStyles}; + controls: Controls = {...defaultControls}; + captionLog: CaptionItem[] = []; + + constructor() {} + + public readConfig() { + const configPath = path.join(app.getPath('userData'), 'config.json') + if(fs.existsSync(configPath)){ + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + this.setStyles(config.styles) + this.setControls(config.controls) + console.log('[INFO] Read Config from:', configPath) + } + } + + public writeConfig() { + const config = { + controls: this.controls, + styles: this.styles + } + const configPath = path.join(app.getPath('userData'), 'config.json') + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)) + console.log('[INFO] Write Config to:', configPath) + } + + public setStyles(args: any) { + this.styles = {...args} + console.log('[INFO] Set Styles:', this.styles) + } + + public resetStyles() { + this.setStyles(defaultStyles) + } + + public sendStyles(window: BrowserWindow) { + window.webContents.send('caption.style.set', this.styles) + console.log(`[INFO] Send Styles to #${window.id}:`, this.styles) + } + + public setControls(args: any) { + const engineEnabled = args.engineEnabled + this.controls = {...args} + this.controls.engineEnabled = engineEnabled + console.log('[INFO] Set Controls:', this.controls) + } + + public sendControls(window: BrowserWindow) { + window.webContents.send('control.control.set', this.controls) + console.log(`[INFO] Send Controls to #${window.id}:`, this.controls) + } + + public updateCaptionLog(log: CaptionItem) { + if(this.captionLog.length && this.captionLog[this.captionLog.length - 1].index === log.index) { + this.captionLog.splice(this.captionLog.length - 1, 1, log) + } + else { + this.captionLog.push(log) + } + for(const window of BrowserWindow.getAllWindows()){ + this.sendCaptionLog(window, 'add') + } + } + + public sendCaptionLog(window: BrowserWindow, command: 'add' | 'set') { + if(command === 'add'){ + window.webContents.send(`both.log.add`, this.captionLog[this.captionLog.length - 1]) + } + else if(command === 'set'){ + window.webContents.send(`both.log.${command}`, this.captionLog) + } + } +} + +export const allConfig = new AllConfig() diff --git a/src/main/utils/engine.ts b/src/main/utils/CaptionEngine.ts similarity index 57% rename from src/main/utils/engine.ts rename to src/main/utils/CaptionEngine.ts index e12d23b..b3c9001 100644 --- a/src/main/utils/engine.ts +++ b/src/main/utils/CaptionEngine.ts @@ -1,22 +1,26 @@ import { spawn, exec } from 'child_process' -import { app, BrowserWindow } from 'electron' +import { app } from 'electron' import { is } from '@electron-toolkit/utils' import path from 'path' -import { addCaptionLog, controls, sendControls } from './config' -import { controlWindow } from '../control' +import { controlWindow } from '../ControlWindow' +import { allConfig } from './AllConfig' export class CaptionEngine { appPath: string = '' command: string[] = [] process: any | undefined - private getApp() { - if (controls.customized && controls.customizedApp) { - this.appPath = controls.customizedApp - this.command = [controls.customizedCommand] + private getApp(): boolean { + if (allConfig.controls.customized && allConfig.controls.customizedApp) { + this.appPath = allConfig.controls.customizedApp + this.command = [allConfig.controls.customizedCommand] } - else if (controls.engine === 'gummy') { - controls.customized = false + else if (allConfig.controls.engine === 'gummy') { + allConfig.controls.customized = false + if(!process.env.DASHSCOPE_API_KEY) { + controlWindow.sendErrorMessage('没有检测到 DASHSCOPE_API_KEY 环境变量,如果要使用 gummy 引擎,需要在阿里云百炼平台获取 API Key 并添加到本机环境变量') + return false + } let gummyName = '' if (process.platform === 'win32') { gummyName = 'main-gummy.exe' @@ -25,7 +29,7 @@ export class CaptionEngine { gummyName = 'main-gummy' } else { - controlWindow.sendErrorMessage('不支持的操作系统平台:' + process.platform) + controlWindow.sendErrorMessage('Unsupported platform: ' + process.platform) throw new Error('Unsupported platform') } if (is.dev) { @@ -41,20 +45,23 @@ export class CaptionEngine { ) } this.command = [] - this.command.push('-s', controls.sourceLang) - this.command.push('-t', controls.translation ? controls.targetLang : 'none') - this.command.push('-a', controls.audio ? '1' : '0') + 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') console.log('[INFO] Engine Path:', this.appPath) console.log('[INFO] Engine Command:', this.command) } + return true } - public start(window: BrowserWindow) { - if (this.process) { - this.stop(); - } - this.getApp() + public start() { + if (this.process) { this.stop() } + if(!this.getApp()){ return } + try { this.process = spawn(this.appPath, this.command) } @@ -64,14 +71,14 @@ export class CaptionEngine { return } - console.log('[INFO] Caption Engine Started: ', { - appPath: this.appPath, - command: this.command - }) + console.log('[INFO] Caption Engine Started') - controls.engineEnabled = true - sendControls(window) - window.webContents.send('control.engine.started') + allConfig.controls.engineEnabled = true + + if(controlWindow.window){ + allConfig.sendControls(controlWindow.window) + controlWindow.window.webContents.send('control.engine.started') + } this.process.stdout.on('data', (data) => { const lines = data.toString().split('\n'); @@ -79,7 +86,7 @@ export class CaptionEngine { if (line.trim()) { try { const caption = JSON.parse(line); - addCaptionLog(caption); + allConfig.updateCaptionLog(caption); } catch (e) { controlWindow.sendErrorMessage('字幕引擎输出内容无法解析为 JSON 对象:' + e) console.error('[ERROR] Error parsing JSON:', e); @@ -96,8 +103,10 @@ export class CaptionEngine { this.process.on('close', (code: any) => { console.log(`[INFO] Subprocess exited with code ${code}`); this.process = undefined; - controls.engineEnabled = false - sendControls(window) + allConfig.controls.engineEnabled = false + if(controlWindow.window){ + allConfig.sendControls(controlWindow.window) + } }); } @@ -107,7 +116,7 @@ export class CaptionEngine { exec(`taskkill /pid ${this.process.pid} /t /f`, (error) => { if (error) { controlWindow.sendErrorMessage('字幕引擎进程关闭失败:' + error) - console.error(`[ERROR] Failed to kill process: ${error}`); + console.error(`[ERROR] Failed to kill process: ${error}`) } }); } else { @@ -115,8 +124,12 @@ export class CaptionEngine { } } this.process = undefined; - controls.engineEnabled = false; - console.log('[INFO] Caption engine process stopped'); - if(controlWindow.window) sendControls(controlWindow.window); + allConfig.controls.engineEnabled = false; + console.log('[INFO] Caption engine process stopped') + if(controlWindow.window) { + allConfig.sendControls(controlWindow.window) + } } -} \ No newline at end of file +} + +export const captionEngine = new CaptionEngine() diff --git a/src/main/utils/config.ts b/src/main/utils/config.ts deleted file mode 100644 index b1dcef7..0000000 --- a/src/main/utils/config.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Styles, CaptionItem, Controls } from '../types' -import { app, BrowserWindow } from 'electron' -import { CaptionEngine } from './engine' -import * as path from 'path' -import * as fs from 'fs' - -export const captionEngine = new CaptionEngine() - -export const styles: Styles = { - fontFamily: 'sans-serif', - fontSize: 24, - fontColor: '#000000', - background: '#dbe2ef', - opacity: 80, - transDisplay: true, - transFontFamily: 'sans-serif', - transFontSize: 24, - transFontColor: '#000000' -} - -export const captionLog: CaptionItem[] = [] - -export const controls: Controls = { - sourceLang: 'en', - targetLang: 'zh', - engine: 'gummy', - audio: 0, - engineEnabled: false, - translation: true, - customized: false, - customizedApp: '', - customizedCommand: '' -} - -export function setStyles(args: any) { - styles.fontFamily = args.fontFamily - styles.fontSize = args.fontSize - styles.fontColor = args.fontColor - styles.background = args.background - styles.opacity = args.opacity - styles.transDisplay = args.transDisplay - styles.transFontFamily = args.transFontFamily - styles.transFontSize = args.transFontSize - styles.transFontColor = args.transFontColor - console.log('[INFO] Set Styles:', styles) -} - -export function resetStyles() { - setStyles({ - fontFamily: 'sans-serif', - fontSize: 24, - fontColor: '#000000', - background: '#dbe2ef', - opacity: 80, - transDisplay: true, - transFontFamily: 'sans-serif', - transFontSize: 24, - transFontColor: '#000000' - }) -} - -export function sendStyles(window: BrowserWindow) { - window.webContents.send('caption.style.set', styles) - console.log(`[INFO] Send Styles to #${window.id}:`, styles) -} - -export function sendCaptionLog(window: BrowserWindow, command: string) { - 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) { - if(captionLog.length && captionLog[captionLog.length - 1].index === log.index) { - captionLog.splice(captionLog.length - 1, 1, log) - } - else { - captionLog.push(log) - } - for(const window of BrowserWindow.getAllWindows()){ - sendCaptionLog(window, 'add') - } -} - -export function setControls(args: any) { - controls.sourceLang = args.sourceLang - controls.targetLang = args.targetLang - controls.engine = args.engine - controls.audio = args.audio - controls.translation = args.translation - controls.customized = args.customized - controls.customizedApp = args.customizedApp - controls.customizedCommand = args.customizedCommand - 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) -} - -export function readConfig() { - const configPath = path.join(app.getPath('userData'), 'config.json') - if(fs.existsSync(configPath)){ - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) - setStyles(config.styles) - setControls(config.controls) - console.log('[INFO] Read Config from:', configPath) - } -} - -export function writeConfig() { - const config = { - controls: controls, - styles: styles - } - const configPath = path.join(app.getPath('userData'), 'config.json') - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)) - console.log('[INFO] Write Config to:', configPath) -} \ No newline at end of file diff --git a/src/main/utils/configSave.ts b/src/main/utils/configSave.ts deleted file mode 100644 index 8eb78c9..0000000 --- a/src/main/utils/configSave.ts +++ /dev/null @@ -1,3 +0,0 @@ -class configSave { - -} \ No newline at end of file