7 Commits

Author SHA1 Message Date
himeditator
e30124cb87 fix: 修复样式载入问题、微调文档 2025-06-26 23:04:39 +08:00
himeditator
301c691f04 feat(ControlPage): 添加项目信息展示 2025-06-26 21:53:42 +08:00
himeditator
4ff1346b6d docs: 更新用户手册和字幕引擎文档链接 2025-06-26 21:33:45 +08:00
himeditator
b28799b03f feat: 新增配置保存和读取、新增文档
- 添加配置数据文件保存和载入
- 添加字幕样式恢复默认的选项
- 添加用户说明文档
- 添加字幕引擎说明文档
2025-06-26 21:29:06 +08:00
himeditator
147e328d8c refactor(main): 重构字幕引擎启动和错误处理逻辑
- 修改了字幕引擎的启动条件,增加了对环境变量和自定义应用的检查
- 优化了错误处理机制,通过控制窗口发送错误消息
- 在前端增加了错误通知功能
2025-06-26 18:59:53 +08:00
himeditator
c086725d98 feat(renderer): 修改部分页面提示、增加英文版README 2025-06-23 20:23:03 +08:00
Chen Janai
fae8b32edf Update README.md 2025-06-22 16:21:38 +08:00
21 changed files with 576 additions and 118 deletions

22
CHANGELOG.md Normal file
View File

@@ -0,0 +1,22 @@
## v0.0.1
2025-06-22
发布第一版软件。
## v0.1.0
2025-06-26
### 新增功能
- 添加错误通知
- 添加默认引擎的环境变量检查
- 添加配置数据文件保存和载入
- 添加字幕样式恢复默认的选项
- 添加项目关于信息
### 新增文档
- 添加用户说明文档
- 添加字幕引擎说明文档

View File

@@ -1,11 +1,14 @@
<div align="center" >
<img src="./resources/icon.png" width="100px" height="100px"/>
<h1 align="center">auto-caption</h1>
<p>Auto Caption 是一个跨平台的视频播放和字幕显示软件。</p>
<b>项目初版已经开发完毕。</b>
<p>Auto Caption 是一个跨平台的实时字幕显示软件。</p>
<p>
| <b>简体中文</b>
| <a href="https://github.com/HiMeditator/auto-caption/blob/main/README_en.md">English</a> |
</p>
</div>
![](./assets/01.png)
![](./assets/media/main.png)
## 📥 下载
@@ -13,13 +16,15 @@
## 📚 用户手册
暂无
[Auto Caption 用户手册](./assets/user-manual_zh.md)
[字幕引擎说明文档](./assets/engine-manual_zh.md)
### 基本使用
目前仅提供 Windows 平台的可安装版本。如果使用默认的 Gummy 字幕引擎,需要获取阿里云百炼平台的 API KEY 并配置到环境变量中才能正常使用该模型。相关教程:[获取API KEY](https://help.aliyun.com/zh/model-studio/get-api-key)、[将API Key配置到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。
目前仅提供 Windows 平台的可安装版本。如果使用默认的 Gummy 字幕引擎,需要获取阿里云百炼平台的 API KEY 并配置到环境变量中才能正常使用该模型。相关教程:[获取 API KEY](https://help.aliyun.com/zh/model-studio/get-api-key)、[ API Key 配置到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。
对于开发者,可以自己创建新的字幕引擎。具体通信规范请参考源代码
对于开发者,可以自己开发新的字幕引擎,自定义字幕引擎的开发请参考[字幕引擎说明文档](./assets/engine-manual_zh.md)
## ✨ 特性
- 丰富的字幕样式设置
@@ -32,6 +37,8 @@
## 🚀 项目运行
![](./assets/media/structure.png)
### 安装依赖
```bash
@@ -42,9 +49,11 @@ npm install
> #### 背景介绍
>
> 如果你是开发者,想开发自定义字幕引擎,请查看[字幕引擎说明文档](./assets/engine-manual_zh.md)。
>
> 所谓的字幕引擎实际上是一个子程序,它会实时获取系统音频输入(录音)或输出(播放声音)的流式数据,并调用音频转文字的模型生成对应音频的字幕。生成的字幕通过 IPC 输出为转换为字符串的 JSON 数据,并返回给主程序。主程序读取字幕数据,处理后显示在窗口上。
>
>目前项目默认使用[阿里云 Gummy 模型](https://help.aliyun.com/zh/model-studio/gummy-speech-recognition-translation/),需要获取阿里云百炼平台的 API KEY 并配置到环境变量中才能正常使用该模型,相关教程:[获取API KEY](https://help.aliyun.com/zh/model-studio/get-api-key)、[将API Key配置到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)
>目前项目默认使用[阿里云 Gummy 模型](https://help.aliyun.com/zh/model-studio/gummy-speech-recognition-translation/),需要获取阿里云百炼平台的 API KEY 并配置到环境变量中才能正常使用该模型。
>
> 本项目的 gummy 字幕引擎是一个 python 子程序,通过 pyinstaller 打包为可执行文件。 运行字幕引擎子程序的代码在 `src\main\utils\engine.ts` 文件中。
@@ -84,7 +93,7 @@ npm run dev
```
### 构建项目
注意目前软件没有适配 macOS 平台,请使用 Windows 或 Linux 系统进行构建。
注意目前软件没有适配 macOS 平台,请使用 Windows 或 Linux 系统进行构建,更建议使用实现了完整功能的 Windows 平台
```bash
# For windows
@@ -93,4 +102,4 @@ npm run build:win
npm run build:mac
# For Linux
npm run build:linux
```
```

113
README_en.md Normal file
View File

@@ -0,0 +1,113 @@
<div align="center" >
<img src="./resources/icon.png" width="100px" height="100px"/>
<h1 align="center">auto-caption</h1>
<p>Auto Caption is a cross-platform real-time subtitle display software.</p>
<p>
| <a href="https://github.com/HiMeditator/auto-caption/blob/main/README.md">简体中文</a>
| <b>English</b> |
</p>
</div>
![](./assets/media/main.png)
## ⚠️ Attention
**The current software interface language is Chinese. English adaptation has not been done yet.**
## 📥 Download
[GitHub Releases](https://github.com/HiMeditator/auto-caption/releases)
## 📚 User Manual
[Auto Caption User Manual (Chinese)](./assets/user-manual_en.md)
[Caption Engine Documentation (Chinese)](./assets/engine-manual_en.md)
### Basic Usage
Currently, only an installable version for the Windows platform is provided. If using the default Gummy subtitle engine, you need to obtain an API KEY from Alibaba Cloud's Bailian platform and configure it in the environment variables to use the model properly. Related tutorials: [Get API KEY](https://help.aliyun.com/zh/model-studio/get-api-key), [Configure API Key through Environment Variables](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables).
For developers, you can create a new subtitle engine. For instructions on customizing the subtitle engine, please refer to the [Caption Engine Documentation (Chinese)](./assets/engine-manual_zh.md).
## ✨ Features
- Rich subtitle style settings
- Flexible subtitle engine selection
- Multi-language recognition and translation
- Subtitle record display and export
- Generate subtitles for audio output and microphone input
Note: The Windows platform supports generating subtitles for both audio output and microphone input, while the Linux platform only supports generating subtitles for microphone input.
## 🚀 Project Execution
![](./assets/media/structure.png)
### Install Dependencies
```bash
npm install
```
### Build Subtitle Engine
> #### Background
>
> If you are a developer and want to develop a custom subtitle engine, please refer to the [Caption Engine Documentation (Chinese)](./assets/engine-manual_zh.md).
>
> The so-called subtitle engine is actually a subprocess that will real-time acquire streaming data from system audio input (recording) or output (playing sound) and call an audio-to-text model to generate corresponding subtitles for the audio. The generated subtitles are output as JSON data converted to strings via IPC and returned to the main program. The main program reads the subtitle data, processes it, and displays it on the window.
>
> Currently, the project uses the [Alibaba Cloud Gummy Model](https://help.aliyun.com/zh/model-studio/gummy-speech-recognition-translation/) by default, which requires obtaining an API KEY from Alibaba Cloud's Bailian platform and configuring it in the environment variables to function properly. Related tutorials: [Get API KEY](https://help.aliyun.com/zh/model-studio/get-api-key), [Configure API Key through Environment Variables](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables).
>
> The gummy subtitle engine in this project is a Python subprocess, packaged into an executable file using pyinstaller. The code for running the subtitle engine subprocess is in the `src\main\utils\engine.ts` file.
First, enter the `python-subprocess` folder and execute the following command to create a virtual environment:
```bash
python -m venv subenv
```
Then activate the virtual environment:
```bash
# Windows
subenv/Scripts/activate
# Linux
source subenv/bin/activate
```
Then install the dependencies (note that if you are in a Linux environment, you need to comment out `PyAudioWPatch` in `requirements.txt`, as this module is only applicable to the Windows environment):
```bash
pip install -r requirements.txt
```
Then build the project using `pyinstaller`:
```bash
pyinstaller --onefile main-gummy.py
```
At this point, the project is built. You can find the corresponding executable file in the `python-subprocess/dist` folder. You can proceed with further operations.
### Run the Project
```bash
npm run dev
```
### Build the Project
Please note that the software is currently not compatible with the macOS platform. Use Windows or Linux systems for building, with Windows being more recommended as it implements the full set of features.
```bash
# For Windows
npm run build:win
# For macOS
npm run build:mac
# For Linux
npm run build:linux
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

View File

@@ -0,0 +1,87 @@
# 字幕引擎说明文档
![](./media/structure.png)
## 字幕引擎介绍
所谓的字幕引擎实际上是一个子程序,它会实时获取系统音频输入(录音)或输出(播放声音)的流式数据,并调用音频转文字的模型生成对应音频的字幕。生成的字幕通过 IPC 输出为转换为 JSON 格式的字符串数据,并返回给主程序。主程序读取字幕数据,处理后显示在窗口上。
## 字幕引擎需要实现的功能
### 音频获取
首先,你的字幕引擎需要获取系统音频输入(录音)或输出(播放声音)的流式数据。如果使用 Python 开发,可以使用 PyAudio 库获取麦克风音频输入数据(全平台通用)。使用 PyAudioWPatch 库获取系统音频输出(仅适用于 Windows 平台)。
一般获取的音频流数据实际上是一个一个的时间比较短的音频块,需要根据模型调整音频块的大小。比如阿里云的 Gummy 模型使用 0.05 秒大小的音频块识别效果优于使用 0.2 秒大小的音频块。
### 音频处理
获取到的音频流在转文字之前可能需要进行预处理。比如阿里云的 Gummy 模型只能识别单通道的音频流,而收集的音频流一般是双通道的,因此要将双通道音频流转换为单通道。通道数的转换可以使用 NumPy 库中的方法实现。
### 音频转文字
在得到了合适的音频流后,就可以将音频流转换为文字了。一般使用各种模型来实现音频流转文字。可根据需求自行选择模型。
### 数据传递
在获取到当前音频流的文字后,需要将文字传递给主程序。使用进程间通信(IPC)的方式,比如通过标准输入输出流或者命名管道来实现。传递的内容必须是 JSON 字符串,其中 JSON 对象需要包含的参数如下:
```typescript
export interface CaptionItem {
index: number, // 字幕序号
time_s: string, // 当前字幕开始时间
time_t: string, // 当前字幕结束时间
text: string, // 字幕内容
translation: string // 字幕翻译
}
```
如果使用 python 语言,可以参考以下方式将数据传递给主程序:
```python
# python-subprocess\audio2text\gummy.py
...
def send_to_node(self, data):
"""
将数据发送到 Node.js 进程
"""
try:
json_data = json.dumps(data) + '\n'
sys.stdout.write(json_data)
sys.stdout.flush()
except Exception as e:
print(f"Error sending data to Node.js: {e}", file=sys.stderr)
...
```
数据接收端代码如下:
```typescript
// src\main\utils\engine.ts
...
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) {
controlWindow.sendErrorMessage('字幕引擎输出内容无法解析为 JSON 对象:' + e)
console.error('[ERROR] Error parsing JSON:', e);
}
}
});
});
this.process.stderr.on('data', (data) => {
controlWindow.sendErrorMessage('字幕引擎错误:' + data)
console.error(`[ERROR] Subprocess Error: ${data}`);
});
...
```
## 参考代码
本项目 `python-subprocess` 文件夹下的 `main-gummy.py` 文件为默认字幕引擎的入口代码。`src\main\utils\engine.ts` 为服务端获取字幕引擎数据和进行处理的代码。可以根据需要阅读了解字幕引擎的实现细节和完整运行过程。

BIN
assets/img/01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/img/02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/media/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

BIN
assets/media/structure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

59
assets/user-manual_zh.md Normal file
View File

@@ -0,0 +1,59 @@
# Auto Caption 用户手册
对应版本v0.1.0
## 软件简介
Auto Caption 是一个跨平台的字幕显示软件,能够实时获取系统音频输入(录音)或输出(播放声音)的流式数据,并调用音频转文字的模型生成对应音频的字幕。软件提供的默认字幕引擎(使用阿里云 Gummy 模型)支持九种语言(中英日韩德法俄西意)的识别与翻译。
目前软件默认字幕引擎只有在 Windows 平台下才拥有完整功能。在 Linux 平台下只能生成音频输入(麦克风)的字幕,暂不支持音频输出(播放声音)的字幕生成。
![](./media/main.png)
### 软件缺点
要使用默认字幕服务需要获取阿里云的 API KEY。
软件使用 Electron 构建,因此软件体积不可避免的较大。
## 软件使用
### 准备阿里云百炼平台 API KEY
要使用软件提供的默认字幕引擎(阿里云 Gummy需要从阿里云百炼平台获取 API KEY 并在本机环境变量中配置。
这部分阿里云提供了详细的教程,可参考:
- [获取 API KEY](https://help.aliyun.com/zh/model-studio/get-api-key)
- [将 API Key 配置到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。
### 修改字幕设置
字幕设置可以分为两类:修改字幕引擎配置、修改字幕样式设置。需要注意的是,在调整的设置的参数后,需要点击每个设置模块右上角的“更改设置”(字幕引擎设置)或“应用样式”(字幕样式设置),更改才会真正生效。如果点击“取消更改”那么当前设置将不会被保存,而是回到上次修改的状态。
### 启动和关闭字幕
在修改完全部配置后,点击界面的“启动字幕引擎”按钮,即可启动字幕。如果需要独立的字幕展示窗口,单击界面的“打开字幕窗口”按钮即可激活独立的字幕展示窗口。如果需要暂停字幕识别,单击界面的“关闭字幕引擎”按钮即可。
### 调整字幕展示窗口
如下图为字幕展示窗口,该窗口实时展示当前最新字幕。窗口右上角三个按钮的功能分别是:将窗口固定在最前面、打开字幕控制窗口、关闭字幕展示窗口。该窗口宽度可以调整,将鼠标移动至窗口的左右边缘,拖动鼠标即可调整宽度。
![](./img/01.png)
### 字幕记录的导出
在字幕控制窗口中可以看到当前收集的所有字幕的记录,点击“导出字幕记录”按钮,即可将字幕记录导出为 JSON 文件。
## 字幕引擎
所谓的字幕引擎实际上是一个子程序,它会实时获取系统音频输入(录音)或输出(播放声音)的流式数据,并调用音频转文字的模型生成对应音频的字幕。生成的字幕通过 IPC 输出为转换为字符串的 JSON 数据,并返回给主程序。主程序读取字幕数据,处理后显示在窗口上。
软件提供了一个默认的字幕引擎,如果你需要其他的字幕引擎,可以通过打开自定义引擎选项来调用其他字幕引擎(其他引擎需要针对进行开发)。其中引擎路径是自定义字幕引擎在你的电脑上的路径,引擎指令是自定义字幕引擎的运行参数,这部分需要按该字幕引擎的规则进行填写。
![](./img/02.png)
注意使用自定义字幕引擎时,前面的字幕引擎的设置将全部不起作用,自定义字幕引擎的配置完全通过引擎指令进行配置。
如果你是开发者,想开发自定义字幕引擎,请查看[字幕引擎说明文档](./engine-manual_zh.md)。

View File

@@ -1,6 +1,6 @@
{
"name": "auto-caption",
"version": "0.0.1",
"version": "0.1.0",
"description": "A cross-platform subtitle display software.",
"main": "./out/main/index.js",
"author": "himeditator",

View File

@@ -8,10 +8,13 @@ import {
captionLog,
controls,
setStyles,
resetStyles,
sendStyles,
sendCaptionLog,
setControls,
sendControls
sendControls,
readConfig,
writeConfig
} from './utils/config'
class ControlWindow {
@@ -36,6 +39,7 @@ class ControlWindow {
setTimeout(() => {
if (this.window) {
readConfig()
sendStyles(this.window) // 配置初始样式
sendCaptionLog(this.window, 'set') // 配置当前字幕记录
sendControls(this.window) // 配置字幕引擎配置
@@ -49,6 +53,7 @@ class ControlWindow {
this.window.on('closed', () => {
this.window = undefined
writeConfig()
})
this.window.webContents.setWindowOpenHandler((details) => {
@@ -71,6 +76,15 @@ class ControlWindow {
sendStyles(captionWindow.window)
}
})
ipcMain.on('control.style.reset', () => {
resetStyles()
if(captionWindow.window){
sendStyles(captionWindow.window)
}
if(this.window){
sendStyles(this.window)
}
})
// 控制窗口请求创建字幕窗口
ipcMain.on('control.captionWindow.activate', () => {
if(!captionWindow.window){
@@ -90,8 +104,17 @@ class ControlWindow {
this.window?.webContents.send('control.engine.already')
}
else {
captionEngine.start()
this.window?.webContents.send('control.engine.started')
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 并添加到本机环境变量')
}
}
})
// 停止字幕引擎
@@ -104,6 +127,10 @@ class ControlWindow {
captionLog.splice(0)
})
}
public sendErrorMessage(message: string) {
this.window?.webContents.send('control.error.send', message)
}
}
export const controlWindow = new ControlWindow()

View File

@@ -2,7 +2,7 @@ import { app, BrowserWindow } from 'electron'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { controlWindow } from './control'
import { captionWindow } from './caption'
import { captionEngine } from './utils/config'
import { captionEngine, writeConfig } from './utils/config'
app.whenReady().then(() => {
electronApp.setAppUserModelId('com.himeditator.autocaption')
@@ -25,6 +25,7 @@ app.whenReady().then(() => {
app.on('will-quit', async () => {
captionEngine.stop()
writeConfig()
});
app.on('window-all-closed', () => {

View File

@@ -1,6 +1,8 @@
import { Styles, CaptionItem, Controls } from '../types'
import { BrowserWindow } from 'electron'
import { app, BrowserWindow } from 'electron'
import { CaptionEngine } from './engine'
import * as path from 'path'
import * as fs from 'fs'
export const captionEngine = new CaptionEngine()
@@ -30,8 +32,6 @@ export const controls: Controls = {
customizedCommand: ''
}
export let engineRunning: boolean = false
export function setStyles(args: any) {
styles.fontFamily = args.fontFamily
styles.fontSize = args.fontSize
@@ -45,6 +45,20 @@ export function setStyles(args: any) {
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)
@@ -86,4 +100,24 @@ export function setControls(args: any) {
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)
}

View File

@@ -0,0 +1,3 @@
class configSave {
}

View File

@@ -1,103 +1,122 @@
import { spawn, exec } from 'child_process'
import { app } from 'electron'
import { app, BrowserWindow } from 'electron'
import { is } from '@electron-toolkit/utils'
import path from 'path'
import { addCaptionLog, controls } from './config'
import { addCaptionLog, controls, sendControls } from './config'
import { controlWindow } from '../control'
export class CaptionEngine {
appPath: string = ''
command: string[] = []
process: any | undefined
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 ]
}
else if(controls.engine === 'gummy'){
let gummyName = ''
if(process.platform === 'win32'){
gummyName = 'main-gummy.exe'
}
else if(process.platform === 'linux'){
gummyName = 'main-gummy'
}
else{
throw new Error('Unsupported platform')
}
if(is.dev){
this.appPath = path.join(
app.getAppPath(),
'python-subprocess', 'dist', gummyName
)
}
else{
this.appPath = path.join(
process.resourcesPath,
'python-subprocess', 'dist', gummyName
)
}
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')
private getApp() {
if (controls.customized && controls.customizedApp) {
this.appPath = controls.customizedApp
this.command = [controls.customizedCommand]
}
else if (controls.engine === 'gummy') {
controls.customized = false
let gummyName = ''
if (process.platform === 'win32') {
gummyName = 'main-gummy.exe'
}
else if (process.platform === 'linux') {
gummyName = 'main-gummy'
}
else {
controlWindow.sendErrorMessage('不支持的操作系统平台:' + process.platform)
throw new Error('Unsupported platform')
}
if (is.dev) {
this.appPath = path.join(
app.getAppPath(),
'python-subprocess', 'dist', gummyName
)
}
else {
this.appPath = path.join(
process.resourcesPath,
'python-subprocess', 'dist', gummyName
)
}
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')
console.log('[INFO] engine', this.appPath)
console.log('[INFO] engine command',this.command)
}
console.log('[INFO] Engine Path:', this.appPath)
console.log('[INFO] Engine Command:', this.command)
}
}
public start(window: BrowserWindow) {
if (this.process) {
this.stop();
}
this.getApp()
try {
this.process = spawn(this.appPath, this.command)
}
catch (e) {
controlWindow.sendErrorMessage('字幕引擎启动失败:' + e)
console.error('[ERROR] Error starting subprocess:', e)
return
}
public start() {
if (this.process) {
this.stop();
console.log('[INFO] Caption Engine Started: ', {
appPath: this.appPath,
command: this.command
})
controls.engineEnabled = true
sendControls(window)
window.webContents.send('control.engine.started')
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) {
controlWindow.sendErrorMessage('字幕引擎输出内容无法解析为 JSON 对象:' + e)
console.error('[ERROR] Error parsing JSON:', e);
}
}
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.stderr.on('data', (data) => {
controlWindow.sendErrorMessage('字幕引擎错误:' + data)
console.error(`[ERROR] Subprocess Error: ${data}`);
});
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;
this.process.on('close', (code: any) => {
console.log(`[INFO] Subprocess exited with code ${code}`);
this.process = undefined;
controls.engineEnabled = false
sendControls(window)
});
}
public stop() {
if (this.process) {
if (process.platform === "win32" && this.process.pid) {
exec(`taskkill /pid ${this.process.pid} /t /f`, (error) => {
if (error) {
controlWindow.sendErrorMessage('字幕引擎进程关闭失败:' + error)
console.error(`[ERROR] Failed to kill process: ${error}`);
}
});
} else {
this.process.kill('SIGKILL');
}
}
public stop() {
if (this.process) {
if (process.platform === "win32" && this.process.pid) {
exec(`taskkill /pid ${this.process.pid} /t /f`, (error) => {
if (error) {
console.error(`Failed to kill process: ${error}`);
}
});
} else {
this.process.kill('SIGKILL');
}
this.process = undefined;
controls.engineEnabled = false;
console.log('[INFO] Caption engine process stopped');
}
}
this.process = undefined;
controls.engineEnabled = false;
console.log('[INFO] Caption engine process stopped');
if(controlWindow.window) sendControls(controlWindow.window);
}
}

View File

@@ -2,7 +2,7 @@
<div class="caption-stat">
<a-row>
<a-col :span="6">
<a-statistic title="字幕引擎" :value="engine" />
<a-statistic title="字幕引擎" :value="(customized && customizedApp)?'自定义':engine" />
</a-col>
<a-col :span="6">
<a-statistic title="字幕引擎状态" :value="engineEnabled?'已启动':'未启动'" />
@@ -81,7 +81,7 @@ import { useCaptionControlStore } from '@renderer/stores/captionControl'
const captionLog = useCaptionLogStore()
const { captionData } = storeToRefs(captionLog)
const captionControl = useCaptionControlStore()
const { engineEnabled, engine } = storeToRefs(captionControl)
const { engineEnabled, engine, customized, customizedApp } = storeToRefs(captionControl)
const pagination = ref({
current: 1,
pageSize: 10,

View File

@@ -2,7 +2,8 @@
<a-card size="small" title="字幕样式设置">
<template #extra>
<a @click="applyStyle">应用样式</a> |
<a @click="resetStyle">取消更改</a>
<a @click="backStyle">取消更改</a> |
<a @click="resetStyle">恢复默认</a>
</template>
<div class="style-item">
<span class="style-label">字体族</span>
@@ -168,7 +169,7 @@ function applyStyle(){
captionStyle.sendStyleChange();
}
function resetStyle(){
function backStyle(){
currentFontFamily.value = captionStyle.fontFamily;
currentFontSize.value = captionStyle.fontSize;
currentFontColor.value = captionStyle.fontColor;
@@ -181,9 +182,13 @@ function resetStyle(){
currentTransFontColor.value = captionStyle.transFontColor;
}
function resetStyle() {
captionStyle.sendStyleReset();
}
watch(changeSignal, (val) => {
if(val == true) {
resetStyle();
backStyle();
captionStyle.changeSignal = false;
}
})

View File

@@ -1,6 +1,9 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { notification } from 'ant-design-vue'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
import { h } from 'vue'
export const useCaptionControlStore = defineStore('captionControl', () => {
const captionEngine = ref([
@@ -73,6 +76,7 @@ export const useCaptionControlStore = defineStore('captionControl', () => {
targetLang.value = controls.targetLang
engine.value = controls.engine
audio.value = controls.audio
engineEnabled.value = controls.engineEnabled
translation.value = controls.translation
customized.value = controls.customized
customizedApp.value = controls.customizedApp
@@ -81,7 +85,6 @@ export const useCaptionControlStore = defineStore('captionControl', () => {
})
window.electron.ipcRenderer.on('control.engine.already', () => {
engineEnabled.value = true
notification.open({
message: '字幕引擎已经启动',
description: '字幕引擎已经启动,请勿重复启动'
@@ -89,23 +92,34 @@ export const useCaptionControlStore = defineStore('captionControl', () => {
})
window.electron.ipcRenderer.on('control.engine.started', () => {
engineEnabled.value = true
const str0 =
`原语言:${sourceLang.value},是否翻译:${translation.value?'是':'否'}` +
`字幕引擎:${engine.value},音频类型:${audio.value ? '输入音频' : '输出音频'}` +
(translation.value ? `,翻译语言:${targetLang.value}` : '');
const str1 = `类型:自定义引擎,引擎路径:${customizedApp.value},命令参数:${customizedCommand.value}`;
notification.open({
message: '字幕引擎启动',
description: `原语言:${sourceLang.value},是否翻译:${translation.value?'是':'否'}` +
`字幕引擎:${engine.value},音频类型:${audio.value ? '输入音频' : '输出音频'}` +
(translation.value ? `,翻译语言:${targetLang.value}` : '')
description: (customized.value && customizedApp.value) ? str1 : str0
});
})
window.electron.ipcRenderer.on('control.engine.stopped', () => {
engineEnabled.value = false
notification.open({
message: '字幕引擎停止',
description: '可点击“启动字幕引擎”按钮重新启动'
});
})
window.electron.ipcRenderer.on('control.error.send', (_, message) => {
notification.open({
message: '发生错误',
description: message,
duration: null,
placement: 'topLeft',
icon: () => h(ExclamationCircleOutlined, { style: 'color: #ff4d4f' })
});
})
return {
captionEngine, // 字幕引擎
audioType, // 音频类型

View File

@@ -40,6 +40,10 @@ export const useCaptionStyleStore = defineStore('captionStyle', () => {
window.electron.ipcRenderer.send('control.style.change', styles)
}
function sendStyleReset() {
window.electron.ipcRenderer.send('control.style.reset')
}
window.electron.ipcRenderer.on('caption.style.set', (_, args) => {
fontFamily.value = args.fontFamily
fontSize.value = args.fontSize
@@ -65,6 +69,7 @@ export const useCaptionStyleStore = defineStore('captionStyle', () => {
transFontColor, // 翻译字体颜色
backgroundRGBA, // 带透明度的背景颜色
sendStyleChange, // 发送样式改变
sendStyleReset, // 恢复默认样式
changeSignal // 样式改变信号
}
})

View File

@@ -4,6 +4,9 @@
<a-col :span="controlSpan">
<div class="caption-control">
<a-card size="small" title="页面宽度">
<template #extra>
<a-button type="link" @click="showAbout = true">关于本项目</a-button>
</template>
<div>
<a-input type="range" class="span-input" min="6" max="18" v-model:value="controlSpan" />
</div>
@@ -18,8 +21,34 @@
</div>
</a-col>
</a-row>
<a-modal v-model:open="showAbout" title="关于本项目" :footer="null">
<div class="about-modal-content">
<h2 class="about-title">Auto Caption 项目</h2>
<p class="about-desc">一个跨平台的实时字幕显示软件</p>
<a-divider />
<div class="about-info">
<p><b>作者</b>HiMeditator</p>
<p><b>版本</b>v0.1.0</p>
<p>
<b>项目地址</b>
<a href="https://github.com/HiMeditator/auto-caption" target="_blank">
GitHub | auto-caption
</a>
</p>
<p>
<b>用户手册</b>
<a
href="https://github.com/HiMeditator/auto-caption/blob/main/assets/user-manual_zh.md"
target="_blank"
>
GitHub | user-manual_zh.md
</a>
</p>
</div>
<div class="about-date">2026 6 26 </div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
@@ -28,6 +57,7 @@ import CaptionControl from '../components/CaptionControl.vue';
import CaptionData from '../components/CaptionData.vue'
import { ref } from 'vue'
const controlSpan = ref(8)
const showAbout = ref(false)
</script>
<style scoped>
@@ -49,4 +79,34 @@ const controlSpan = ref(8)
.span-input {
width: 100px;
}
.about-modal-content {
text-align: center;
padding: 8px 0 0 0;
}
.about-title {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 0.2em;
}
.about-desc {
color: #666;
margin-bottom: 0.5em;
}
.about-info {
text-align: left;
display: inline-block;
margin: 0 auto;
font-size: 1em;
}
.about-date {
margin-top: 1.5em;
color: #aaa;
font-size: 0.95em;
text-align: right;
}
</style>