mirror of
https://github.com/HiMeditator/auto-caption.git
synced 2026-02-10 08:59:48 +08:00
Compare commits
1 Commits
v0.7.0
...
dev-0.6.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36636d0caa |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,9 +6,7 @@ out
|
||||
*.log*
|
||||
__pycache__
|
||||
.venv
|
||||
test.py
|
||||
subenv
|
||||
engine/build
|
||||
engine/models
|
||||
engine/notebook
|
||||
.repomap
|
||||
.virtualme
|
||||
|
||||
44
README.md
44
README.md
@@ -3,7 +3,7 @@
|
||||
<h1 align="center">auto-caption</h1>
|
||||
<p>Auto Caption 是一个跨平台的实时字幕显示软件。</p>
|
||||
<p>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/releases"><img src="https://img.shields.io/badge/release-0.7.0-blue"></a>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/releases"><img src="https://img.shields.io/badge/release-0.6.0-blue"></a>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/issues"><img src="https://img.shields.io/github/issues/HiMeditator/auto-caption?color=orange"></a>
|
||||
<img src="https://img.shields.io/github/languages/top/HiMeditator/auto-caption?color=royalblue">
|
||||
<img src="https://img.shields.io/github/repo-size/HiMeditator/auto-caption?color=green">
|
||||
@@ -14,7 +14,7 @@
|
||||
| <a href="./README_en.md">English</a>
|
||||
| <a href="./README_ja.md">日本語</a> |
|
||||
</p>
|
||||
<p><i>v0.7.0 版本已经发布,优化了软件界面,添加了日志记录显示。本地的字幕引擎正在尝试开发中,预计以 Python 代码的形式进行发布...</i></p>
|
||||
<p><i>v0.6.0 版本已经发布,对字幕引擎代码进行了大重构,提升了代码的可扩展性。更多的字幕引擎正在尝试开发中...</i></p>
|
||||
</div>
|
||||
|
||||

|
||||
@@ -33,15 +33,6 @@
|
||||
|
||||
[更新日志](./docs/CHANGELOG.md)
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 生成音频输出或麦克风输入的字幕
|
||||
- 跨平台(Windows、macOS、Linux)、多界面语言(中文、英语、日语)支持
|
||||
- 丰富的字幕样式设置(字体、字体大小、字体粗细、字体颜色、背景颜色等)
|
||||
- 灵活的字幕引擎选择(阿里云 Gummy 云端模型、本地 Vosk 模型、自己开发的模型)
|
||||
- 多语言识别与翻译(见下文“⚙️ 自带字幕引擎说明”)
|
||||
- 字幕记录展示与导出(支持导出 `.srt` 和 `.json` 格式)
|
||||
|
||||
## 📖 基本使用
|
||||
|
||||
软件已经适配了 Windows、macOS 和 Linux 平台。测试过的平台信息如下:
|
||||
@@ -71,6 +62,15 @@ macOS 平台和 Linux 平台获取系统音频输出需要进行额外设置,
|
||||
|
||||
**如果你觉得上述字幕引擎不能满足你的需求,而且你会 Python,那么你可以考虑开发自己的字幕引擎。详细说明请参考[字幕引擎说明文档](./docs/engine-manual/zh.md)。**
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 跨平台、多界面语言支持
|
||||
- 丰富的字幕样式设置
|
||||
- 灵活的字幕引擎选择
|
||||
- 多语言识别与翻译
|
||||
- 字幕记录展示与导出
|
||||
- 生成音频输出或麦克风输入的字幕
|
||||
|
||||
## ⚙️ 自带字幕引擎说明
|
||||
|
||||
目前软件自带 2 个字幕引擎,正在规划新的引擎。它们的详细信息如下。
|
||||
@@ -113,6 +113,7 @@ $$
|
||||
- [SenseVoice](https://github.com/FunAudioLLM/SenseVoice)
|
||||
- [FunASR](https://github.com/modelscope/FunASR)
|
||||
|
||||
|
||||
## 🚀 项目运行
|
||||
|
||||

|
||||
@@ -129,24 +130,29 @@ npm install
|
||||
|
||||
```bash
|
||||
# in ./engine folder
|
||||
python -m venv .venv
|
||||
python -m venv subenv
|
||||
# or
|
||||
python3 -m venv .venv
|
||||
python3 -m venv subenv
|
||||
```
|
||||
|
||||
然后激活虚拟环境:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
.venv/Scripts/activate
|
||||
subenv/Scripts/activate
|
||||
# Linux or macOS
|
||||
source .venv/bin/activate
|
||||
source subenv/bin/activate
|
||||
```
|
||||
|
||||
然后安装依赖(这一步在 macOS 和 Linux 可能会报错,一般是因为构建失败,需要根据报错信息进行处理):
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
# Windows
|
||||
pip install -r requirements_win.txt
|
||||
# macOS
|
||||
pip install -r requirements_darwin.txt
|
||||
# Linux
|
||||
pip install -r requirements_linux.txt
|
||||
```
|
||||
|
||||
如果在 Linux 系统上安装 `samplerate` 模块报错,可以尝试使用以下命令单独安装:
|
||||
@@ -165,12 +171,12 @@ pyinstaller ./main.spec
|
||||
|
||||
```
|
||||
# Windows
|
||||
vosk_path = str(Path('./.venv/Lib/site-packages/vosk').resolve())
|
||||
vosk_path = str(Path('./subenv/Lib/site-packages/vosk').resolve())
|
||||
# Linux or macOS
|
||||
vosk_path = str(Path('./.venv/lib/python3.x/site-packages/vosk').resolve())
|
||||
vosk_path = str(Path('./subenv/lib/python3.x/site-packages/vosk').resolve())
|
||||
```
|
||||
|
||||
此时项目构建完成,进入 `engine/dist` 文件夹可见对应的可执行文件。即可进行后续操作。
|
||||
此时项目构建完成,在进入 `engine/dist` 文件夹可见对应的可执行文件。即可进行后续操作。
|
||||
|
||||
### 运行项目
|
||||
|
||||
|
||||
41
README_en.md
41
README_en.md
@@ -3,7 +3,7 @@
|
||||
<h1 align="center">auto-caption</h1>
|
||||
<p>Auto Caption is a cross-platform real-time caption display software.</p>
|
||||
<p>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/releases"><img src="https://img.shields.io/badge/release-0.7.0-blue"></a>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/releases"><img src="https://img.shields.io/badge/release-0.6.0-blue"></a>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/issues"><img src="https://img.shields.io/github/issues/HiMeditator/auto-caption?color=orange"></a>
|
||||
<img src="https://img.shields.io/github/languages/top/HiMeditator/auto-caption?color=royalblue">
|
||||
<img src="https://img.shields.io/github/repo-size/HiMeditator/auto-caption?color=green">
|
||||
@@ -14,7 +14,7 @@
|
||||
| <b>English</b>
|
||||
| <a href="./README_ja.md">日本語</a> |
|
||||
</p>
|
||||
<p><i>Version 0.7.0 has been released, imporving the software interface and adding software log display. The local caption engine is under development and is expected to be released in the form of Python code...</i></p>
|
||||
<p><i>Version 0.6.0 has been released, featuring a major refactor of the subtitle engine code to improve code extensibility. More subtitle engines are being developed...</i></p>
|
||||
</div>
|
||||
|
||||

|
||||
@@ -33,15 +33,6 @@
|
||||
|
||||
[Changelog](./docs/CHANGELOG.md)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Generate captions from audio output or microphone input
|
||||
- Cross-platform (Windows, macOS, Linux) and multi-language interface (Chinese, English, Japanese) support
|
||||
- Rich caption style settings (font, font size, font weight, font color, background color, etc.)
|
||||
- Flexible caption engine selection (Alibaba Cloud Gummy cloud model, local Vosk model, self-developed model)
|
||||
- Multi-language recognition and translation (see below "⚙️ Built-in Subtitle Engines")
|
||||
- Subtitle record display and export (supports exporting `.srt` and `.json` formats)
|
||||
|
||||
## 📖 Basic Usage
|
||||
|
||||
The software has been adapted for Windows, macOS, and Linux platforms. The tested platform information is as follows:
|
||||
@@ -71,6 +62,15 @@ To use the Vosk local caption engine, first download your required model from [V
|
||||
|
||||
**If you find the above caption engines don't meet your needs and you know Python, you may consider developing your own caption engine. For detailed instructions, please refer to the [Caption Engine Documentation](./docs/engine-manual/en.md).**
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Cross-platform, multi-language UI support
|
||||
- Rich caption style settings
|
||||
- Flexible caption engine selection
|
||||
- Multi-language recognition and translation
|
||||
- Caption recording display and export
|
||||
- Generate captions for audio output or microphone input
|
||||
|
||||
## ⚙️ Built-in Subtitle Engines
|
||||
|
||||
Currently, the software comes with 2 subtitle engines, with new engines under development. Their detailed information is as follows.
|
||||
@@ -129,24 +129,29 @@ First enter the `engine` folder and execute the following commands to create a v
|
||||
|
||||
```bash
|
||||
# in ./engine folder
|
||||
python -m venv .venv
|
||||
python -m venv subenv
|
||||
# or
|
||||
python3 -m venv .venv
|
||||
python3 -m venv subenv
|
||||
```
|
||||
|
||||
Then activate the virtual environment:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
.venv/Scripts/activate
|
||||
subenv/Scripts/activate
|
||||
# Linux or macOS
|
||||
source .venv/bin/activate
|
||||
source subenv/bin/activate
|
||||
```
|
||||
|
||||
Then install dependencies (this step might result in errors on macOS and Linux, usually due to build failures, and you need to handle them based on the error messages):
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
# Windows
|
||||
pip install -r requirements_win.txt
|
||||
# macOS
|
||||
pip install -r requirements_darwin.txt
|
||||
# Linux
|
||||
pip install -r requirements_linux.txt
|
||||
```
|
||||
|
||||
If you encounter errors when installing the `samplerate` module on Linux systems, you can try installing it separately with this command:
|
||||
@@ -165,9 +170,9 @@ Note that the path to the `vosk` library in `main-vosk.spec` might be incorrect
|
||||
|
||||
```
|
||||
# Windows
|
||||
vosk_path = str(Path('./.venv/Lib/site-packages/vosk').resolve())
|
||||
vosk_path = str(Path('./subenv/Lib/site-packages/vosk').resolve())
|
||||
# Linux or macOS
|
||||
vosk_path = str(Path('./.venv/lib/python3.x/site-packages/vosk').resolve())
|
||||
vosk_path = str(Path('./subenv/lib/python3.x/site-packages/vosk').resolve())
|
||||
```
|
||||
|
||||
After the build completes, you can find the executable file in the `engine/dist` folder. Then proceed with subsequent operations.
|
||||
|
||||
45
README_ja.md
45
README_ja.md
@@ -3,7 +3,7 @@
|
||||
<h1 align="center">auto-caption</h1>
|
||||
<p>Auto Caption はクロスプラットフォームのリアルタイム字幕表示ソフトウェアです。</p>
|
||||
<p>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/releases"><img src="https://img.shields.io/badge/release-0.7.0-blue"></a>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/releases"><img src="https://img.shields.io/badge/release-0.6.0-blue"></a>
|
||||
<a href="https://github.com/HiMeditator/auto-caption/issues"><img src="https://img.shields.io/github/issues/HiMeditator/auto-caption?color=orange"></a>
|
||||
<img src="https://img.shields.io/github/languages/top/HiMeditator/auto-caption?color=royalblue">
|
||||
<img src="https://img.shields.io/github/repo-size/HiMeditator/auto-caption?color=green">
|
||||
@@ -14,7 +14,7 @@
|
||||
| <a href="./README_en.md">English</a>
|
||||
| <b>日本語</b> |
|
||||
</p>
|
||||
<p><i>バージョン 0.7.0 がリリースされ、ソフトウェアインターフェースが最適化され、ログ記録表示機能が追加されました。ローカルの字幕エンジンは現在開発中であり、Pythonコードの形式でリリースされる予定です...</i></p>
|
||||
<p><i>v0.6.0 バージョンがリリースされ、字幕エンジンコードが大規模にリファクタリングされ、コードの拡張性が向上しました。より多くの字幕エンジンの開発が試みられています...</i></p>
|
||||
</div>
|
||||
|
||||

|
||||
@@ -33,18 +33,9 @@
|
||||
|
||||
[更新履歴](./docs/CHANGELOG.md)
|
||||
|
||||
## ✨ 特徴
|
||||
|
||||
- 音声出力またはマイク入力からの字幕生成
|
||||
- クロスプラットフォーム(Windows、macOS、Linux)、多言語インターフェース(中国語、英語、日本語)対応
|
||||
- 豊富な字幕スタイル設定(フォント、フォントサイズ、フォント太さ、フォント色、背景色など)
|
||||
- 柔軟な字幕エンジン選択(阿里雲 Gummy クラウドモデル、ローカル Vosk モデル、独自開発モデル)
|
||||
- 多言語認識と翻訳(下記「⚙️ 字幕エンジン説明」参照)
|
||||
- 字幕記録表示とエクスポート(`.srt` および `.json` 形式のエクスポートに対応)
|
||||
|
||||
## 📖 基本使い方
|
||||
|
||||
このソフトウェアは Windows、macOS、Linux プラットフォームに対応しています。テスト済みのプラットフォーム情報は以下の通りです:
|
||||
このソフトウェアはWindows、macOS、Linuxプラットフォームに対応しています。テスト済みのプラットフォーム情報は以下の通りです:
|
||||
|
||||
| OS バージョン | アーキテクチャ | システムオーディオ入力 | システムオーディオ出力 |
|
||||
| ------------------ | ------------ | ------------------ | ------------------- |
|
||||
@@ -54,7 +45,7 @@
|
||||
| Kali Linux 2022.3 | x64 | ✅ | ✅ |
|
||||
| Kylin Server V10 SP3 | x64 | ✅ | ✅ |
|
||||
|
||||
macOS および Linux プラットフォームでシステムオーディオ出力を取得するには追加設定が必要です。詳細は[Auto Captionユーザーマニュアル](./docs/user-manual/ja.md)をご覧ください。
|
||||
macOSおよびLinuxプラットフォームでシステムオーディオ出力を取得するには追加設定が必要です。詳細は[Auto Captionユーザーマニュアル](./docs/user-manual/ja.md)をご覧ください。
|
||||
|
||||
> 阿里雲の国際版サービスでは Gummy モデルを提供していないため、現在中国以外のユーザーは Gummy 字幕エンジンを使用できません。
|
||||
|
||||
@@ -71,6 +62,15 @@ Vosk ローカル字幕エンジンを使用するには、まず [Vosk Models](
|
||||
|
||||
**上記の字幕エンジンがご要望を満たさず、かつ Python の知識をお持ちの場合、独自の字幕エンジンを開発することも可能です。詳細な説明は[字幕エンジン説明書](./docs/engine-manual/ja.md)をご参照ください。**
|
||||
|
||||
## ✨ 特徴
|
||||
|
||||
- クロスプラットフォーム、多言語 UI サポート
|
||||
- 豊富な字幕スタイル設定
|
||||
- 柔軟な字幕エンジン選択
|
||||
- 多言語認識と翻訳
|
||||
- 字幕記録の表示とエクスポート
|
||||
- オーディオ出力またはマイク入力からの字幕生成
|
||||
|
||||
## ⚙️ 字幕エンジン説明
|
||||
|
||||
現在、ソフトウェアには2つの字幕エンジンが搭載されており、新しいエンジンが計画されています。それらの詳細情報は以下の通りです。
|
||||
@@ -129,24 +129,29 @@ npm install
|
||||
|
||||
```bash
|
||||
# ./engine フォルダ内
|
||||
python -m venv .venv
|
||||
python -m venv subenv
|
||||
# または
|
||||
python3 -m venv .venv
|
||||
python3 -m venv subenv
|
||||
```
|
||||
|
||||
次に仮想環境をアクティブにします:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
.venv/Scripts/activate
|
||||
subenv/Scripts/activate
|
||||
# Linux または macOS
|
||||
source .venv/bin/activate
|
||||
source subenv/bin/activate
|
||||
```
|
||||
|
||||
次に依存関係をインストールします(このステップでは macOS と Linux でエラーが発生する可能性があります。通常はビルド失敗によるもので、エラーメッセージに基づいて対処する必要があります):
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
# Windows
|
||||
pip install -r requirements_win.txt
|
||||
# macOS
|
||||
pip install -r requirements_darwin.txt
|
||||
# Linux
|
||||
pip install -r requirements_linux.txt
|
||||
```
|
||||
|
||||
Linux システムで `samplerate` モジュールのインストールに問題が発生した場合、以下のコマンドで個別にインストールを試すことができます:
|
||||
@@ -165,9 +170,9 @@ pyinstaller ./main.spec
|
||||
|
||||
```
|
||||
# Windows
|
||||
vosk_path = str(Path('./.venv/Lib/site-packages/vosk').resolve())
|
||||
vosk_path = str(Path('./subenv/Lib/site-packages/vosk').resolve())
|
||||
# Linux または macOS
|
||||
vosk_path = str(Path('./.venv/lib/python3.x/site-packages/vosk').resolve())
|
||||
vosk_path = str(Path('./subenv/lib/python3.x/site-packages/vosk').resolve())
|
||||
```
|
||||
|
||||
これでプロジェクトのビルドが完了し、`engine/dist` フォルダ内に対応する実行可能ファイルが確認できます。その後、次の操作に進むことができます。
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 370 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 413 KiB After Width: | Height: | Size: 387 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 396 KiB |
@@ -137,20 +137,11 @@
|
||||
- 合并 Gummy 和 Vosk 引擎为单个可执行文件
|
||||
- 字幕引擎和主程序添加 Socket 通信,完全避免字幕引擎成为孤儿进程
|
||||
|
||||
|
||||
## v0.7.0
|
||||
|
||||
2025-08-20
|
||||
2025-08-xx
|
||||
|
||||
### 新增功能
|
||||
|
||||
- 添加字幕窗口宽度记忆,重新打开时与上次字幕窗口宽度一致
|
||||
- 在尝试关闭字幕引擎 4s 后字幕引擎仍未关闭,则强制关闭字幕引擎
|
||||
- 添加复制最新字幕选项,用户可以选择只复制最近1~3条字幕 (#13)
|
||||
- 添加主题颜色设置,支持六种颜色:蓝色、绿色、橙色、紫色、粉色、暗色/明色
|
||||
- 添加日志记录显示:可以查看软件的字幕引擎输出的日志记录
|
||||
|
||||
### 优化体验
|
||||
|
||||
- 优化软件用户界面的部分组件
|
||||
- 更清晰的日志输出
|
||||
- 在尝试关闭字幕引擎 4s 后字幕引擎仍未关闭,则强制关闭字幕引擎
|
||||
@@ -18,13 +18,9 @@
|
||||
- [x] 添加字幕记录按时间降序排列选择 *2025/07/26*
|
||||
- [x] 重构字幕引擎 *2025/07/28*
|
||||
- [x] 优化前端界面提示消息 *2025/07/29*
|
||||
- [x] 复制字幕记录可选择只复制最近的字幕记录 *2025/08/18*
|
||||
- [x] 添加颜色主题设置 *2025/08/18*
|
||||
- [x] 前端页面添加日志内容展示 *2025/08/19*
|
||||
|
||||
## 待完成
|
||||
|
||||
- [ ] 调研更多的云端模型(火山、OpenAI、Google等)
|
||||
- [ ] 验证 / 添加基于 sherpa-onnx 的字幕引擎
|
||||
|
||||
## 后续计划
|
||||
|
||||
@@ -80,17 +80,6 @@ Python 端监听到的音频流转换为的字幕数据。
|
||||
|
||||
Python 端打印的提示信息,比起 `print`,该信息更希望 Electron 端的关注。
|
||||
|
||||
### `error`
|
||||
|
||||
```js
|
||||
{
|
||||
command: "error",
|
||||
content: string
|
||||
}
|
||||
```
|
||||
|
||||
Python 端打印的错误信息,该错误信息需要在前端弹窗显示。
|
||||
|
||||
### `usage`
|
||||
|
||||
```js
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
### `control.uiTheme.change`
|
||||
|
||||
**介绍:** 前端修改界面主题,将修改同步给后端
|
||||
**介绍:** 前端修改字界面主题,将修改同步给后端
|
||||
|
||||
**发起方:** 前端控制窗口
|
||||
|
||||
@@ -92,16 +92,6 @@
|
||||
|
||||
**数据类型:** `UITheme`
|
||||
|
||||
### `control.uiColor.change`
|
||||
|
||||
**介绍:** 前端修改界面主题颜色,将修改同步给后端
|
||||
|
||||
**发起方:** 前端控制窗口
|
||||
|
||||
**接收方:** 后端控制窗口实例
|
||||
|
||||
**数据类型:** `string`
|
||||
|
||||
### `control.leftBarWidth.change`
|
||||
|
||||
**介绍:** 前端修改边栏宽度,将修改同步给后端
|
||||
@@ -284,16 +274,6 @@
|
||||
|
||||
**数据类型:** `Controls`
|
||||
|
||||
### `control.softwareLog.add`
|
||||
|
||||
**介绍:** 添加一条新的日志数据
|
||||
|
||||
**发起方:** 后端
|
||||
|
||||
**接收方:** 前端控制窗口
|
||||
|
||||
**数据类型:** `SoftwareLog`
|
||||
|
||||
### `both.styles.set`
|
||||
|
||||
**介绍:** 后端将最新字幕样式发送给前端,前端进行设置
|
||||
|
||||
@@ -155,7 +155,7 @@ if __name__ == "__main__":
|
||||
# Common parameters
|
||||
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=10, help='Number of audio stream chunks collected per second')
|
||||
parser.add_argument('-c', '--chunk_rate', default=20, help='Number of audio stream chunks collected per second')
|
||||
parser.add_argument('-p', '--port', default=8080, help='The port to run the server on, 0 for no server')
|
||||
# Gummy-specific parameters
|
||||
parser.add_argument('-s', '--source_language', default='en', help='Source language code')
|
||||
|
||||
@@ -157,7 +157,7 @@ if __name__ == "__main__":
|
||||
# 共通
|
||||
parser.add_argument('-e', '--caption_engine', default='gummy', help='字幕エンジン: gummyまたはvosk')
|
||||
parser.add_argument('-a', '--audio_type', default=0, help='オーディオストリームソース: 0は出力、1は入力')
|
||||
parser.add_argument('-c', '--chunk_rate', default=10, help='1秒あたりに収集するオーディオストリームブロックの数')
|
||||
parser.add_argument('-c', '--chunk_rate', default=20, help='1秒あたりに収集するオーディオストリームブロックの数')
|
||||
parser.add_argument('-p', '--port', default=8080, help='サーバーを実行するポート、0はサーバーなし')
|
||||
# gummy専用
|
||||
parser.add_argument('-s', '--source_language', default='en', help='ソース言語コード')
|
||||
|
||||
@@ -156,7 +156,7 @@ if __name__ == "__main__":
|
||||
# both
|
||||
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=10, help='Number of audio stream chunks collected per second')
|
||||
parser.add_argument('-c', '--chunk_rate', default=20, help='Number of audio stream chunks collected per second')
|
||||
parser.add_argument('-p', '--port', default=8080, help='The port to run the server on, 0 for no server')
|
||||
# gummy only
|
||||
parser.add_argument('-s', '--source_language', default='en', help='Source language code')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import argparse
|
||||
from utils import stdout_cmd, stdout_err
|
||||
from utils import stdout_cmd, stderr
|
||||
from utils import thread_data, start_server
|
||||
from utils import merge_chunk_channels, resample_chunk_mono
|
||||
from audio2text import InvalidParameter, GummyRecognizer
|
||||
@@ -17,7 +17,6 @@ def main_gummy(s: str, t: str, a: int, c: int, k: str):
|
||||
|
||||
stream.open_stream()
|
||||
engine.start()
|
||||
chunk_mono = bytes()
|
||||
|
||||
restart_count = 0
|
||||
while thread_data.status == "running":
|
||||
@@ -29,17 +28,15 @@ def main_gummy(s: str, t: str, a: int, c: int, k: str):
|
||||
engine.send_audio_frame(chunk_mono)
|
||||
except InvalidParameter as e:
|
||||
restart_count += 1
|
||||
if restart_count > 5:
|
||||
stdout_err(str(e))
|
||||
if restart_count > 8:
|
||||
stderr(str(e))
|
||||
thread_data.status = "kill"
|
||||
stdout_cmd('kill')
|
||||
break
|
||||
else:
|
||||
stdout_cmd('info', f'Gummy engine stopped, restart attempt: {restart_count}...')
|
||||
stdout_cmd('info', f'Gummy engine stopped, trying to restart #{restart_count}')
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
engine.send_audio_frame(chunk_mono)
|
||||
stream.close_stream()
|
||||
engine.stop()
|
||||
|
||||
@@ -70,7 +67,7 @@ if __name__ == "__main__":
|
||||
# both
|
||||
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=10, help='Number of audio stream chunks collected per second')
|
||||
parser.add_argument('-c', '--chunk_rate', default=20, help='Number of audio stream chunks collected per second')
|
||||
parser.add_argument('-p', '--port', default=8080, help='The port to run the server on, 0 for no server')
|
||||
# gummy only
|
||||
parser.add_argument('-s', '--source_language', default='en', help='Source language code')
|
||||
@@ -79,7 +76,7 @@ if __name__ == "__main__":
|
||||
# vosk only
|
||||
parser.add_argument('-m', '--model_path', default='', help='The path to the vosk model.')
|
||||
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
if int(args.port) == 0:
|
||||
thread_data.status = "running"
|
||||
else:
|
||||
|
||||
@@ -4,9 +4,9 @@ from pathlib import Path
|
||||
import sys
|
||||
|
||||
if sys.platform == 'win32':
|
||||
vosk_path = str(Path('./.venv/Lib/site-packages/vosk').resolve())
|
||||
vosk_path = str(Path('./subenv/Lib/site-packages/vosk').resolve())
|
||||
else:
|
||||
vosk_path = str(Path('./.venv/lib/python3.12/site-packages/vosk').resolve())
|
||||
vosk_path = str(Path('./subenv/lib/python3.12/site-packages/vosk').resolve())
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
dashscope
|
||||
numpy
|
||||
samplerate
|
||||
vosk
|
||||
pyinstaller
|
||||
pyaudio; sys_platform == 'darwin'
|
||||
pyaudiowpatch; sys_platform == 'win32'
|
||||
6
engine/requirements_darwin.txt
Normal file
6
engine/requirements_darwin.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
dashscope
|
||||
numpy
|
||||
samplerate
|
||||
PyAudio
|
||||
vosk
|
||||
pyinstaller
|
||||
5
engine/requirements_linux.txt
Normal file
5
engine/requirements_linux.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
dashscope
|
||||
numpy
|
||||
vosk
|
||||
pyinstaller
|
||||
samplerate # pip install samplerate --only-binary=:all:
|
||||
6
engine/requirements_win.txt
Normal file
6
engine/requirements_win.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
dashscope
|
||||
numpy
|
||||
samplerate
|
||||
PyAudioWPatch
|
||||
vosk
|
||||
pyinstaller
|
||||
@@ -22,9 +22,9 @@ class AudioStream:
|
||||
|
||||
初始化参数:
|
||||
audio_type: 0-系统音频输出流(需配合 BlackHole),1-系统音频输入流
|
||||
chunk_rate: 每秒采集音频块的数量,默认为10
|
||||
chunk_rate: 每秒采集音频块的数量,默认为20
|
||||
"""
|
||||
def __init__(self, audio_type=0, chunk_rate=10):
|
||||
def __init__(self, audio_type=0, chunk_rate=20):
|
||||
self.audio_type = audio_type
|
||||
self.mic = pyaudio.PyAudio()
|
||||
if self.audio_type == 0:
|
||||
@@ -40,12 +40,6 @@ class AudioStream:
|
||||
self.RATE = int(self.device["defaultSampleRate"])
|
||||
self.CHUNK = self.RATE // chunk_rate
|
||||
|
||||
def reset_chunk_size(self, chunk_size: int):
|
||||
"""
|
||||
重新设置音频块大小
|
||||
"""
|
||||
self.CHUNK = chunk_size
|
||||
|
||||
def get_info(self):
|
||||
dev_info = f"""
|
||||
采样设备:
|
||||
|
||||
@@ -41,9 +41,9 @@ class AudioStream:
|
||||
|
||||
初始化参数:
|
||||
audio_type: 0-系统音频输出流(不支持,不会生效),1-系统音频输入流(默认)
|
||||
chunk_rate: 每秒采集音频块的数量,默认为10
|
||||
chunk_rate: 每秒采集音频块的数量,默认为20
|
||||
"""
|
||||
def __init__(self, audio_type=1, chunk_rate=10):
|
||||
def __init__(self, audio_type=1, chunk_rate=20):
|
||||
self.audio_type = audio_type
|
||||
|
||||
if self.audio_type == 0:
|
||||
@@ -58,12 +58,6 @@ class AudioStream:
|
||||
self.RATE = 48000
|
||||
self.CHUNK = self.RATE // chunk_rate
|
||||
|
||||
def reset_chunk_size(self, chunk_size: int):
|
||||
"""
|
||||
重新设置音频块大小
|
||||
"""
|
||||
self.CHUNK = chunk_size
|
||||
|
||||
def get_info(self):
|
||||
dev_info = f"""
|
||||
音频捕获进程:
|
||||
|
||||
@@ -46,9 +46,9 @@ class AudioStream:
|
||||
|
||||
初始化参数:
|
||||
audio_type: 0-系统音频输出流(默认),1-系统音频输入流
|
||||
chunk_rate: 每秒采集音频块的数量,默认为10
|
||||
chunk_rate: 每秒采集音频块的数量,默认为20
|
||||
"""
|
||||
def __init__(self, audio_type=0, chunk_rate=10, chunk_size=-1):
|
||||
def __init__(self, audio_type=0, chunk_rate=20):
|
||||
self.audio_type = audio_type
|
||||
self.mic = pyaudio.PyAudio()
|
||||
if self.audio_type == 0:
|
||||
@@ -64,12 +64,6 @@ class AudioStream:
|
||||
self.RATE = int(self.device["defaultSampleRate"])
|
||||
self.CHUNK = self.RATE // chunk_rate
|
||||
|
||||
def reset_chunk_size(self, chunk_size: int):
|
||||
"""
|
||||
重新设置音频块大小
|
||||
"""
|
||||
self.CHUNK = chunk_size
|
||||
|
||||
def get_info(self):
|
||||
dev_info = f"""
|
||||
采样设备:
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
from .audioprcs import (
|
||||
merge_chunk_channels,
|
||||
resample_chunk_mono,
|
||||
resample_chunk_mono_np,
|
||||
resample_mono_chunk
|
||||
)
|
||||
from .sysout import stdout, stdout_err, stdout_cmd, stdout_obj, stderr
|
||||
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
|
||||
@@ -55,38 +55,6 @@ def resample_chunk_mono(chunk: bytes, channels: int, orig_sr: int, target_sr: in
|
||||
return chunk_mono_r.tobytes()
|
||||
|
||||
|
||||
def resample_chunk_mono_np(chunk: bytes, channels: int, orig_sr: int, target_sr: int, mode="sinc_best", dtype=np.float32) -> np.ndarray:
|
||||
"""
|
||||
将当前多通道音频数据块转换成单通道音频数据块,然后进行重采样,返回 Numpy 数组
|
||||
|
||||
Args:
|
||||
chunk: 多通道音频数据块
|
||||
channels: 通道数
|
||||
orig_sr: 原始采样率
|
||||
target_sr: 目标采样率
|
||||
mode: 重采样模式,可选:'sinc_best' | 'sinc_medium' | 'sinc_fastest' | 'zero_order_hold' | 'linear'
|
||||
dtype: 返回 Numpy 数组的数据类型
|
||||
|
||||
Return:
|
||||
单通道音频数据块
|
||||
"""
|
||||
if channels == 1:
|
||||
chunk_mono = np.frombuffer(chunk, dtype=np.int16)
|
||||
chunk_mono = chunk_mono.astype(np.float32)
|
||||
else:
|
||||
# (length * channels,)
|
||||
chunk_np = np.frombuffer(chunk, dtype=np.int16)
|
||||
# (length, channels)
|
||||
chunk_np = chunk_np.reshape(-1, channels)
|
||||
# (length,)
|
||||
chunk_mono = np.mean(chunk_np.astype(np.float32), axis=1)
|
||||
|
||||
ratio = target_sr / orig_sr
|
||||
chunk_mono_r = samplerate.resample(chunk_mono, ratio, converter_type=mode)
|
||||
chunk_mono_r = chunk_mono_r.astype(dtype)
|
||||
return chunk_mono_r
|
||||
|
||||
|
||||
def resample_mono_chunk(chunk: bytes, orig_sr: int, target_sr: int, mode="sinc_best") -> bytes:
|
||||
"""
|
||||
将当前单通道音频块进行重采样
|
||||
|
||||
@@ -4,9 +4,6 @@ import json
|
||||
def stdout(text: str):
|
||||
stdout_cmd("print", text)
|
||||
|
||||
def stdout_err(text: str):
|
||||
stdout_cmd("error", text)
|
||||
|
||||
def stdout_cmd(command: str, content = ""):
|
||||
msg = { "command": command, "content": content }
|
||||
sys.stdout.write(json.dumps(msg) + "\n")
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "auto-caption",
|
||||
"version": "0.7.0",
|
||||
"version": "0.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "auto-caption",
|
||||
"version": "0.7.0",
|
||||
"version": "0.6.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "auto-caption",
|
||||
"productName": "Auto Caption",
|
||||
"version": "0.7.0",
|
||||
"version": "0.6.0",
|
||||
"description": "A cross-platform subtitle display software.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "himeditator",
|
||||
|
||||
@@ -7,10 +7,8 @@ import icon from '../../build/icon.png?asset'
|
||||
import { captionWindow } from './CaptionWindow'
|
||||
import { allConfig } from './utils/AllConfig'
|
||||
import { captionEngine } from './utils/CaptionEngine'
|
||||
import { Log } from './utils/Log'
|
||||
|
||||
class ControlWindow {
|
||||
mounted: boolean = false;
|
||||
window: BrowserWindow | undefined;
|
||||
|
||||
public createWindow(): void {
|
||||
@@ -36,7 +34,6 @@ class ControlWindow {
|
||||
})
|
||||
|
||||
this.window.on('closed', () => {
|
||||
this.mounted = false
|
||||
this.window = undefined
|
||||
allConfig.writeConfig()
|
||||
})
|
||||
@@ -66,8 +63,7 @@ class ControlWindow {
|
||||
})
|
||||
|
||||
ipcMain.handle('both.window.mounted', () => {
|
||||
this.mounted = true
|
||||
return allConfig.getFullConfig(Log.getAndClearLogQueue())
|
||||
return allConfig.getFullConfig()
|
||||
})
|
||||
|
||||
ipcMain.handle('control.nativeTheme.get', () => {
|
||||
@@ -113,10 +109,6 @@ class ControlWindow {
|
||||
allConfig.uiTheme = args
|
||||
})
|
||||
|
||||
ipcMain.on('control.uiColor.change', (_, args) => {
|
||||
allConfig.uiColor = args
|
||||
})
|
||||
|
||||
ipcMain.on('control.leftBarWidth.change', (_, args) => {
|
||||
allConfig.leftBarWidth = args
|
||||
})
|
||||
|
||||
@@ -45,23 +45,14 @@ export interface CaptionItem {
|
||||
translation: string
|
||||
}
|
||||
|
||||
export interface SoftwareLogItem {
|
||||
type: "INFO" | "WARN" | "ERROR",
|
||||
index: number,
|
||||
time: string,
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface FullConfig {
|
||||
platform: string,
|
||||
uiLanguage: UILanguage,
|
||||
uiTheme: UITheme,
|
||||
uiColor: string,
|
||||
leftBarWidth: number,
|
||||
styles: Styles,
|
||||
controls: Controls,
|
||||
captionLog: CaptionItem[],
|
||||
softwareLog: SoftwareLogItem[]
|
||||
captionLog: CaptionItem[]
|
||||
}
|
||||
|
||||
export interface EngineInfo {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
UILanguage, UITheme, Styles, Controls,
|
||||
CaptionItem, FullConfig, SoftwareLogItem
|
||||
CaptionItem, FullConfig
|
||||
} from '../types'
|
||||
import { Log } from './Log'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
@@ -49,7 +49,6 @@ class AllConfig {
|
||||
uiLanguage: UILanguage = 'zh';
|
||||
leftBarWidth: number = 8;
|
||||
uiTheme: UITheme = 'system';
|
||||
uiColor: string = '#1677ff';
|
||||
styles: Styles = {...defaultStyles};
|
||||
controls: Controls = {...defaultControls};
|
||||
|
||||
@@ -61,15 +60,14 @@ class AllConfig {
|
||||
public readConfig() {
|
||||
const configPath = path.join(app.getPath('userData'), 'config.json')
|
||||
if(fs.existsSync(configPath)){
|
||||
Log.info('Read Config from:', configPath)
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
||||
if(config.captionWindowWidth) this.captionWindowWidth = config.captionWindowWidth
|
||||
if(config.uiLanguage) this.uiLanguage = config.uiLanguage
|
||||
if(config.uiTheme) this.uiTheme = config.uiTheme
|
||||
if(config.uiColor) this.uiColor = config.uiColor
|
||||
if(config.leftBarWidth) this.leftBarWidth = config.leftBarWidth
|
||||
if(config.styles) this.setStyles(config.styles)
|
||||
if(config.controls) this.setControls(config.controls)
|
||||
Log.info('Read Config from:', configPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +76,6 @@ class AllConfig {
|
||||
captionWindowWidth: this.captionWindowWidth,
|
||||
uiLanguage: this.uiLanguage,
|
||||
uiTheme: this.uiTheme,
|
||||
uiColor: this.uiColor,
|
||||
leftBarWidth: this.leftBarWidth,
|
||||
controls: this.controls,
|
||||
styles: this.styles
|
||||
@@ -88,17 +85,15 @@ class AllConfig {
|
||||
Log.info('Write Config to:', configPath)
|
||||
}
|
||||
|
||||
public getFullConfig(softwareLog: SoftwareLogItem[]): FullConfig {
|
||||
public getFullConfig(): FullConfig {
|
||||
return {
|
||||
platform: process.platform,
|
||||
uiLanguage: this.uiLanguage,
|
||||
uiTheme: this.uiTheme,
|
||||
uiColor: this.uiColor,
|
||||
leftBarWidth: this.leftBarWidth,
|
||||
styles: this.styles,
|
||||
controls: this.controls,
|
||||
captionLog: this.captionLog,
|
||||
softwareLog: softwareLog
|
||||
captionLog: this.captionLog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,9 +123,7 @@ class AllConfig {
|
||||
}
|
||||
}
|
||||
this.controls.engineEnabled = engineEnabled
|
||||
let _controls = {...this.controls}
|
||||
_controls.API_KEY = _controls.API_KEY.replace(/./g, '*')
|
||||
Log.info('Set Controls:', _controls)
|
||||
Log.info('Set Controls:', this.controls)
|
||||
}
|
||||
|
||||
public sendControls(window: BrowserWindow, info = true) {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class CaptionEngine {
|
||||
if(process.platform === "win32") {
|
||||
this.appPath = path.join(
|
||||
app.getAppPath(), 'engine',
|
||||
'.venv', 'Scripts', 'python.exe'
|
||||
'subenv', 'Scripts', 'python.exe'
|
||||
)
|
||||
this.command.push(path.join(
|
||||
app.getAppPath(), 'engine', 'main.py'
|
||||
@@ -47,7 +47,7 @@ export class CaptionEngine {
|
||||
else {
|
||||
this.appPath = path.join(
|
||||
app.getAppPath(), 'engine',
|
||||
'.venv', 'bin', 'python3'
|
||||
'subenv', 'bin', 'python3'
|
||||
)
|
||||
this.command.push(path.join(
|
||||
app.getAppPath(), 'engine', 'main.py'
|
||||
@@ -85,16 +85,12 @@ export class CaptionEngine {
|
||||
}
|
||||
}
|
||||
Log.info('Engine Path:', this.appPath)
|
||||
if(this.command.length > 2 && this.command.at(-2) === '-k') {
|
||||
const _command = [...this.command]
|
||||
_command[_command.length -1] = _command[_command.length -1].replace(/./g, '*')
|
||||
Log.info('Engine Command:', _command)
|
||||
}
|
||||
else Log.info('Engine Command:', this.command)
|
||||
Log.info('Engine Command:', this.command)
|
||||
return true
|
||||
}
|
||||
|
||||
public connect() {
|
||||
Log.info('Connecting to caption engine server...')
|
||||
if(this.client) { Log.warn('Client already exists, ignoring...') }
|
||||
this.client = net.createConnection({ port: this.port }, () => {
|
||||
Log.info('Connected to caption engine server');
|
||||
@@ -150,7 +146,8 @@ export class CaptionEngine {
|
||||
const lines = data.toString().split('\n')
|
||||
lines.forEach((line: string) => {
|
||||
if(line.trim()){
|
||||
Log.error(line)
|
||||
controlWindow.sendErrorMessage(/*i18n('engine.error') +*/ line)
|
||||
console.error(line)
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -180,6 +177,7 @@ export class CaptionEngine {
|
||||
this.client = undefined
|
||||
}
|
||||
this.status = 'stopping'
|
||||
Log.info('Caption engine process stopping...')
|
||||
this.timerID = setTimeout(() => {
|
||||
if(this.status !== 'stopping') return
|
||||
Log.warn('Engine process still not stopped, trying to kill...')
|
||||
@@ -227,12 +225,8 @@ function handleEngineData(data: any) {
|
||||
else if(data.command === 'info') {
|
||||
Log.info('Engine Info:', data.content)
|
||||
}
|
||||
else if(data.command === 'error') {
|
||||
Log.error('Engine Error:', data.content)
|
||||
controlWindow.sendErrorMessage(/*i18n('engine.error') +*/ data.content)
|
||||
}
|
||||
else if(data.command === 'usage') {
|
||||
Log.info('Engine Token Usage: ', data.content)
|
||||
Log.info('Engine Usage: ', data.content)
|
||||
}
|
||||
else {
|
||||
Log.warn('Unknown command:', data)
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import { controlWindow } from "../ControlWindow"
|
||||
import { type SoftwareLogItem } from "../types"
|
||||
|
||||
let logIndex = 0
|
||||
const logQueue: SoftwareLogItem[] = []
|
||||
|
||||
function getTimeString() {
|
||||
const now = new Date()
|
||||
const HH = String(now.getHours()).padStart(2, '0')
|
||||
@@ -14,45 +8,15 @@ function getTimeString() {
|
||||
}
|
||||
|
||||
export class Log {
|
||||
static getAndClearLogQueue() {
|
||||
const copiedQueue = structuredClone(logQueue)
|
||||
logQueue.length = 0
|
||||
return copiedQueue
|
||||
}
|
||||
|
||||
static handleLog(logType: "INFO" | "WARN" | "ERROR", ...msg: any[]) {
|
||||
const timeStr = getTimeString()
|
||||
const logPre = `[${logType} ${timeStr}]`
|
||||
let logStr = ""
|
||||
for(let i = 0; i < msg.length; i++) {
|
||||
logStr += i ? " " : ""
|
||||
if(typeof msg[i] === "string") logStr += msg[i]
|
||||
else logStr += JSON.stringify(msg[i], undefined, 2)
|
||||
}
|
||||
console.log(logPre, logStr)
|
||||
const logItem: SoftwareLogItem = {
|
||||
type: logType,
|
||||
index: ++logIndex,
|
||||
time: timeStr,
|
||||
text: logStr
|
||||
}
|
||||
if(controlWindow.mounted && controlWindow.window) {
|
||||
controlWindow.window.webContents.send('control.softwareLog.add', logItem)
|
||||
}
|
||||
else {
|
||||
logQueue.push(logItem)
|
||||
}
|
||||
}
|
||||
|
||||
static info(...msg: any[]){
|
||||
this.handleLog("INFO", ...msg)
|
||||
console.log(`[INFO ${getTimeString()}]`, ...msg)
|
||||
}
|
||||
|
||||
static warn(...msg: any[]){
|
||||
this.handleLog("WARN", ...msg)
|
||||
console.warn(`[WARN ${getTimeString()}]`, ...msg)
|
||||
}
|
||||
|
||||
static error(...msg: any[]){
|
||||
this.handleLog("ERROR", ...msg)
|
||||
console.error(`[ERROR ${getTimeString()}]`, ...msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Auto Caption v0.7.0</title>
|
||||
<title>Auto Caption</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { onMounted } from 'vue'
|
||||
import { FullConfig } from './types'
|
||||
import { useCaptionLogStore } from './stores/captionLog'
|
||||
import { useSoftwareLogStore } from './stores/softwareLog'
|
||||
import { useCaptionStyleStore } from './stores/captionStyle'
|
||||
import { useEngineControlStore } from './stores/engineControl'
|
||||
import { useGeneralSettingStore } from './stores/generalSetting'
|
||||
@@ -15,13 +14,11 @@ onMounted(() => {
|
||||
window.electron.ipcRenderer.invoke('both.window.mounted').then((data: FullConfig) => {
|
||||
useGeneralSettingStore().uiLanguage = data.uiLanguage
|
||||
useGeneralSettingStore().uiTheme = data.uiTheme
|
||||
useGeneralSettingStore().uiColor = data.uiColor
|
||||
useGeneralSettingStore().leftBarWidth = data.leftBarWidth
|
||||
useCaptionStyleStore().setStyles(data.styles)
|
||||
useEngineControlStore().platform = data.platform
|
||||
useEngineControlStore().setControls(data.controls)
|
||||
useCaptionLogStore().captionData = data.captionLog
|
||||
useSoftwareLogStore().softwareLogs = data.softwareLog
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: inline-block;
|
||||
width: calc(100% - 100px);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
@@ -1,151 +1,138 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="caption-title">
|
||||
<span style="margin-right: 30px;">{{ $t('log.title') }}</span>
|
||||
<div class="caption-list">
|
||||
<div>
|
||||
<a-app class="caption-title">
|
||||
<span style="margin-right: 30px;">{{ $t('log.title') }}</span>
|
||||
</a-app>
|
||||
<a-popover :title="$t('log.baseTime')">
|
||||
<template #content>
|
||||
<div class="base-time">
|
||||
<div class="base-time-container">
|
||||
<a-input
|
||||
type="number" min="0"
|
||||
v-model:value="baseHH"
|
||||
></a-input>
|
||||
<span class="base-time-label">{{ $t('log.hour') }}</span>
|
||||
</div>
|
||||
</div><span style="margin: 0 4px;">:</span>
|
||||
<div class="base-time">
|
||||
<div class="base-time-container">
|
||||
<a-input
|
||||
type="number" min="0" max="59"
|
||||
v-model:value="baseMM"
|
||||
></a-input>
|
||||
<span class="base-time-label">{{ $t('log.min') }}</span>
|
||||
</div>
|
||||
</div><span style="margin: 0 4px;">:</span>
|
||||
<div class="base-time">
|
||||
<div class="base-time-container">
|
||||
<a-input
|
||||
type="number" min="0" max="59"
|
||||
v-model:value="baseSS"
|
||||
></a-input>
|
||||
<span class="base-time-label">{{ $t('log.sec') }}</span>
|
||||
</div>
|
||||
</div><span style="margin: 0 4px;">.</span>
|
||||
<div class="base-time">
|
||||
<div class="base-time-container">
|
||||
<a-input
|
||||
type="number" min="0" max="999"
|
||||
v-model:value="baseMS"
|
||||
></a-input>
|
||||
<span class="base-time-label">{{ $t('log.ms') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-button
|
||||
type="primary"
|
||||
style="margin-right: 20px;"
|
||||
@click="changeBaseTime"
|
||||
:disabled="captionData.length === 0"
|
||||
>{{ $t('log.changeTime') }}</a-button>
|
||||
</a-popover>
|
||||
<a-popover :title="$t('log.exportOptions')">
|
||||
<template #content>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.exportFormat') }}</span>
|
||||
<a-radio-group v-model:value="exportFormat">
|
||||
<a-radio-button value="srt">.srt</a-radio-button>
|
||||
<a-radio-button value="json">.json</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.exportContent') }}</span>
|
||||
<a-radio-group v-model:value="contentOption">
|
||||
<a-radio-button value="both">{{ $t('log.both') }}</a-radio-button>
|
||||
<a-radio-button value="source">{{ $t('log.source') }}</a-radio-button>
|
||||
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<a-button
|
||||
style="margin-right: 20px;"
|
||||
@click="exportCaptions"
|
||||
:disabled="captionData.length === 0"
|
||||
>{{ $t('log.export') }}</a-button>
|
||||
</a-popover>
|
||||
<a-popover :title="$t('log.copyOptions')">
|
||||
<template #content>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.addIndex') }}</span>
|
||||
<a-switch v-model:checked="showIndex" />
|
||||
<span class="input-label">{{ $t('log.copyTime') }}</span>
|
||||
<a-switch v-model:checked="copyTime" />
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.copyContent') }}</span>
|
||||
<a-radio-group v-model:value="contentOption">
|
||||
<a-radio-button value="both">{{ $t('log.both') }}</a-radio-button>
|
||||
<a-radio-button value="source">{{ $t('log.source') }}</a-radio-button>
|
||||
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<a-button
|
||||
style="margin-right: 20px;"
|
||||
@click="copyCaptions"
|
||||
>{{ $t('log.copy') }}</a-button>
|
||||
</a-popover>
|
||||
<a-button
|
||||
danger
|
||||
@click="clearCaptions"
|
||||
>{{ $t('log.clear') }}</a-button>
|
||||
</div>
|
||||
<a-popover :title="$t('log.baseTime')">
|
||||
<template #content>
|
||||
<div class="base-time">
|
||||
<div class="base-time-container">
|
||||
<a-input
|
||||
type="number" min="0"
|
||||
v-model:value="baseHH"
|
||||
></a-input>
|
||||
<span class="base-time-label">{{ $t('log.hour') }}</span>
|
||||
</div>
|
||||
</div><span style="margin: 0 4px;">:</span>
|
||||
<div class="base-time">
|
||||
<div class="base-time-container">
|
||||
<a-input
|
||||
type="number" min="0" max="59"
|
||||
v-model:value="baseMM"
|
||||
></a-input>
|
||||
<span class="base-time-label">{{ $t('log.min') }}</span>
|
||||
</div>
|
||||
</div><span style="margin: 0 4px;">:</span>
|
||||
<div class="base-time">
|
||||
<div class="base-time-container">
|
||||
<a-input
|
||||
type="number" min="0" max="59"
|
||||
v-model:value="baseSS"
|
||||
></a-input>
|
||||
<span class="base-time-label">{{ $t('log.sec') }}</span>
|
||||
</div>
|
||||
</div><span style="margin: 0 4px;">.</span>
|
||||
<div class="base-time">
|
||||
<div class="base-time-container">
|
||||
<a-input
|
||||
type="number" min="0" max="999"
|
||||
v-model:value="baseMS"
|
||||
></a-input>
|
||||
<span class="base-time-label">{{ $t('log.ms') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-button
|
||||
type="primary"
|
||||
style="margin-right: 20px;"
|
||||
@click="changeBaseTime"
|
||||
:disabled="captionData.length === 0"
|
||||
>{{ $t('log.changeTime') }}</a-button>
|
||||
</a-popover>
|
||||
<a-popover :title="$t('log.exportOptions')">
|
||||
<template #content>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.exportFormat') }}</span>
|
||||
<a-radio-group v-model:value="exportFormat">
|
||||
<a-radio-button value="srt"><code>.srt</code></a-radio-button>
|
||||
<a-radio-button value="json"><code>.json</code></a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.exportContent') }}</span>
|
||||
<a-radio-group v-model:value="contentOption">
|
||||
<a-radio-button value="both">{{ $t('log.both') }}</a-radio-button>
|
||||
<a-radio-button value="source">{{ $t('log.source') }}</a-radio-button>
|
||||
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<a-button
|
||||
style="margin-right: 20px;"
|
||||
@click="exportCaptions"
|
||||
:disabled="captionData.length === 0"
|
||||
>{{ $t('log.export') }}</a-button>
|
||||
</a-popover>
|
||||
<a-popover :title="$t('log.copyOptions')">
|
||||
<template #content>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.addIndex') }}</span>
|
||||
<a-switch v-model:checked="showIndex" />
|
||||
<span class="input-label">{{ $t('log.copyTime') }}</span>
|
||||
<a-switch v-model:checked="copyTime" />
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.copyContent') }}</span>
|
||||
<a-radio-group v-model:value="contentOption">
|
||||
<a-radio-button value="both">{{ $t('log.both') }}</a-radio-button>
|
||||
<a-radio-button value="source">{{ $t('log.source') }}</a-radio-button>
|
||||
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('log.copyNum') }}</span>
|
||||
<a-radio-group v-model:value="copyNum">
|
||||
<a-radio-button :value="0"><code>[:]</code></a-radio-button>
|
||||
<a-radio-button :value="1"><code>[-1:]</code></a-radio-button>
|
||||
<a-radio-button :value="2"><code>[-2:]</code></a-radio-button>
|
||||
<a-radio-button :value="3"><code>[-3:]</code></a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<a-button
|
||||
style="margin-right: 20px;"
|
||||
@click="copyCaptions"
|
||||
>{{ $t('log.copy') }}</a-button>
|
||||
</a-popover>
|
||||
<a-button
|
||||
danger
|
||||
@click="clearCaptions"
|
||||
>{{ $t('log.clear') }}</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="captionData"
|
||||
v-model:pagination="pagination"
|
||||
style="margin-top: 10px;"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ record.index }}
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="captionData"
|
||||
v-model:pagination="pagination"
|
||||
style="margin-top: 10px;"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ record.index }}
|
||||
</template>
|
||||
<template v-if="column.key === 'time'">
|
||||
<div class="time-cell">
|
||||
<div class="time-start">{{ record.time_s }}</div>
|
||||
<div class="time-end">{{ record.time_t }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'content'">
|
||||
<div class="caption-content">
|
||||
<div class="caption-text">{{ record.text }}</div>
|
||||
<div class="caption-translation">{{ record.translation }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'time'">
|
||||
<div class="time-cell">
|
||||
<code class="time-start"
|
||||
:style="`color: ${uiColor}`"
|
||||
>{{ record.time_s }}</code>
|
||||
<code class="time-end">{{ record.time_t }}</code>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'content'">
|
||||
<div class="caption-content">
|
||||
<div class="caption-text">{{ record.text }}</div>
|
||||
<div
|
||||
class="caption-translation"
|
||||
:style="`border-left: 3px solid ${uiColor};`"
|
||||
>{{ record.translation }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useCaptionLogStore } from '@renderer/stores/captionLog'
|
||||
import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import * as tc from '../utils/timeCalc'
|
||||
@@ -156,14 +143,10 @@ const { t } = useI18n()
|
||||
const captionLog = useCaptionLogStore()
|
||||
const { captionData } = storeToRefs(captionLog)
|
||||
|
||||
const generalSetting = useGeneralSettingStore()
|
||||
const { uiColor } = storeToRefs(generalSetting)
|
||||
|
||||
const exportFormat = ref('srt')
|
||||
const showIndex = ref(true)
|
||||
const copyTime = ref(true)
|
||||
const contentOption = ref('both')
|
||||
const copyNum = ref(0)
|
||||
|
||||
const baseHH = ref<number>(0)
|
||||
const baseMM = ref<number>(0)
|
||||
@@ -202,7 +185,7 @@ const columns = [
|
||||
title: 'time',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
width: 150,
|
||||
width: 160,
|
||||
sorter: (a: CaptionItem, b: CaptionItem) => {
|
||||
if(a.time_s <= b.time_s) return -1
|
||||
return 1
|
||||
@@ -272,12 +255,7 @@ function getExportData() {
|
||||
|
||||
function copyCaptions() {
|
||||
let content = ''
|
||||
let start = 0
|
||||
if(copyNum.value > 0) {
|
||||
start = captionData.value.length - copyNum.value
|
||||
if(start < 0) start = 0
|
||||
}
|
||||
for(let i = start; i < captionData.value.length; i++){
|
||||
for(let i = 0; i < captionData.value.length; i++){
|
||||
const item = captionData.value[i]
|
||||
if(showIndex.value) content += `${i+1}\n`
|
||||
if(copyTime.value) content += `${item.time_s} --> ${item.time_t}\n`.replace(/\./g, ',')
|
||||
@@ -307,7 +285,7 @@ function clearCaptions() {
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.base-time {
|
||||
@@ -335,13 +313,10 @@ function clearCaptions() {
|
||||
}
|
||||
|
||||
.time-start {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.time-end {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@@ -357,5 +332,6 @@ function clearCaptions() {
|
||||
.caption-translation {
|
||||
font-size: 14px;
|
||||
padding-left: 16px;
|
||||
border-left: 3px solid #1890ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,18 +34,20 @@
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('style.fontSize') }}</span>
|
||||
<a-slider
|
||||
<a-input
|
||||
class="input-area"
|
||||
:min="0" :max="72"
|
||||
type="range"
|
||||
min="0" max="72"
|
||||
v-model:value="currentFontSize"
|
||||
/>
|
||||
<div class="input-item-value">{{ currentFontSize }}px</div>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('style.fontWeight') }}</span>
|
||||
<a-slider
|
||||
<a-input
|
||||
class="input-area"
|
||||
:min="1" :max="9"
|
||||
type="range"
|
||||
min="1" max="9"
|
||||
v-model:value="currentFontWeight"
|
||||
/>
|
||||
<div class="input-item-value">{{ currentFontWeight*100 }}</div>
|
||||
@@ -61,10 +63,11 @@
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('style.opacity') }}</span>
|
||||
<a-slider
|
||||
<a-input
|
||||
class="input-area"
|
||||
:min="0"
|
||||
:max="100"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
v-model:value="currentOpacity"
|
||||
/>
|
||||
<div class="input-item-value">{{ currentOpacity }}%</div>
|
||||
@@ -108,18 +111,20 @@
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('style.fontSize') }}</span>
|
||||
<a-slider
|
||||
<a-input
|
||||
class="input-area"
|
||||
:min="0" :max="72"
|
||||
type="range"
|
||||
min="0" max="72"
|
||||
v-model:value="currentTransFontSize"
|
||||
/>
|
||||
<div class="input-item-value">{{ currentTransFontSize }}px</div>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('style.fontWeight') }}</span>
|
||||
<a-slider
|
||||
<a-input
|
||||
class="input-area"
|
||||
:min="1" :max="9"
|
||||
type="range"
|
||||
min="1" max="9"
|
||||
v-model:value="currentTransFontWeight"
|
||||
/>
|
||||
<div class="input-item-value">{{ currentTransFontWeight*100 }}</div>
|
||||
@@ -131,27 +136,30 @@
|
||||
<a-card size="small" :title="$t('style.shadow.title')">
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('style.shadow.offsetX') }}</span>
|
||||
<a-slider
|
||||
<a-input
|
||||
class="input-area"
|
||||
:min="-10" :max="10"
|
||||
type="range"
|
||||
min="-10" max="10"
|
||||
v-model:value="currentOffsetX"
|
||||
/>
|
||||
<div class="input-item-value">{{ currentOffsetX }}px</div>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('style.shadow.offsetY') }}</span>
|
||||
<a-slider
|
||||
<a-input
|
||||
class="input-area"
|
||||
:min="-10" :max="10"
|
||||
type="range"
|
||||
min="-10" max="10"
|
||||
v-model:value="currentOffsetY"
|
||||
/>
|
||||
<div class="input-item-value">{{ currentOffsetY }}px</div>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('style.shadow.blur') }}</span>
|
||||
<a-slider
|
||||
<a-input
|
||||
class="input-area"
|
||||
:min="0" :max="12"
|
||||
type="range"
|
||||
min="0" max="12"
|
||||
v-model:value="currentBlur"
|
||||
/>
|
||||
<div class="input-item-value">{{ currentBlur }}px</div>
|
||||
@@ -307,7 +315,7 @@ function resetStyle() {
|
||||
}
|
||||
|
||||
watch(changeSignal, (val) => {
|
||||
if(val === true) {
|
||||
if(val == true) {
|
||||
backStyle();
|
||||
captionStyle.changeSignal = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="caption-stat">
|
||||
<a-row>
|
||||
<a-col :span="5">
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
:title="$t('status.engine')"
|
||||
:value="customized?$t('status.customized'):engine"
|
||||
@@ -36,7 +36,7 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<a-col :span="5" @mouseenter="getEngineInfo" style="cursor: pointer;">
|
||||
<a-col :span="6" @mouseenter="getEngineInfo" style="cursor: pointer;">
|
||||
<a-statistic
|
||||
:title="$t('status.status')"
|
||||
:value="engineEnabled?$t('status.started'):$t('status.stopped')"
|
||||
@@ -47,13 +47,10 @@
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
</a-popover>
|
||||
<a-col :span="5">
|
||||
<a-col :span="6">
|
||||
<a-statistic :title="$t('status.logNumber')" :value="captionData.length" />
|
||||
</a-col>
|
||||
<a-col :span="5">
|
||||
<a-statistic :title="$t('status.logNumber2')" :value="softwareLogs.length" />
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-col :span="6">
|
||||
<div class="about-tag">{{ $t('status.aboutProj') }}</div>
|
||||
<GithubOutlined class="proj-info" @click="showAbout = true"/>
|
||||
</a-col>
|
||||
@@ -86,7 +83,7 @@
|
||||
<p class="about-desc">{{ $t('status.about.desc') }}</p>
|
||||
<a-divider />
|
||||
<div class="about-info">
|
||||
<p><b>{{ $t('status.about.version') }}</b><a-tag color="green">v0.7.0</a-tag></p>
|
||||
<p><b>{{ $t('status.about.version') }}</b><a-tag color="green">v0.6.0</a-tag></p>
|
||||
<p>
|
||||
<b>{{ $t('status.about.author') }}</b>
|
||||
<a
|
||||
@@ -131,17 +128,14 @@ import { EngineInfo } from '@renderer/types'
|
||||
import { ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useCaptionLogStore } from '@renderer/stores/captionLog'
|
||||
import { useSoftwareLogStore } from '@renderer/stores/softwareLog'
|
||||
import { useEngineControlStore } from '@renderer/stores/engineControl'
|
||||
import { GithubOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { GithubOutlined, InfoCircleOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const showAbout = ref(false)
|
||||
const pending = ref(false)
|
||||
|
||||
const captionLog = useCaptionLogStore()
|
||||
const { captionData } = storeToRefs(captionLog)
|
||||
const softwareLog = useSoftwareLogStore()
|
||||
const { softwareLogs } = storeToRefs(softwareLog)
|
||||
const engineControl = useEngineControlStore()
|
||||
const { engineEnabled, engine, customized, errorSignal } = storeToRefs(engineControl)
|
||||
|
||||
|
||||
@@ -28,24 +28,11 @@
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('general.color') }}</span>
|
||||
<a-radio-group v-model:value="uiColor">
|
||||
<template v-for="color in colorList" :key="color">
|
||||
<a-radio-button :value="color"
|
||||
:style="{backgroundColor: color}"
|
||||
>
|
||||
<CheckOutlined style="color: white;" v-if="color === uiColor" />
|
||||
<span v-else> </span>
|
||||
</a-radio-button>
|
||||
</template>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="input-item">
|
||||
<span class="input-label">{{ $t('general.barWidth') }}</span>
|
||||
<a-slider class="span-input"
|
||||
:min="6" :max="12" v-model:value="leftBarWidth"
|
||||
<a-input
|
||||
type="range" class="span-input"
|
||||
min="6" max="12" v-model:value="leftBarWidth"
|
||||
/>
|
||||
<div class="input-item-value">{{ (leftBarWidth * 100 / 24).toFixed(0) }}%</div>
|
||||
</div>
|
||||
@@ -54,55 +41,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
|
||||
import { InfoCircleOutlined, CheckOutlined } from '@ant-design/icons-vue'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const generalSettingStore = useGeneralSettingStore()
|
||||
const { uiLanguage, realTheme, uiTheme, uiColor, leftBarWidth } = storeToRefs(generalSettingStore)
|
||||
|
||||
const colorListLight = [
|
||||
'#1677ff',
|
||||
'#00b96b',
|
||||
'#fa8c16',
|
||||
'#9254de',
|
||||
'#eb2f96',
|
||||
'#000000'
|
||||
]
|
||||
|
||||
const colorListDark = [
|
||||
'#1677ff',
|
||||
'#00b96b',
|
||||
'#fa8c16',
|
||||
'#9254de',
|
||||
'#eb2f96',
|
||||
'#b9d7ea'
|
||||
]
|
||||
|
||||
const colorList = ref(colorListLight)
|
||||
|
||||
watch(realTheme, (val) => {
|
||||
if(val === 'dark') {
|
||||
colorList.value = colorListDark
|
||||
} else {
|
||||
colorList.value = colorListLight
|
||||
}
|
||||
console.log(val)
|
||||
})
|
||||
|
||||
watch(uiTheme, (val) => {
|
||||
console.log(val)
|
||||
})
|
||||
const { uiLanguage, uiTheme, leftBarWidth } = storeToRefs(generalSettingStore)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url(../assets/input.css);
|
||||
|
||||
.span-input {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.general-note {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="log-title">
|
||||
<span style="margin-right: 30px;">{{ $t('log.title2') }}</span>
|
||||
</div>
|
||||
<a-button
|
||||
danger
|
||||
@click="softwareLog.clear()"
|
||||
>{{ $t('log.clear') }}</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="softwareLogs"
|
||||
v-model:pagination="pagination"
|
||||
style="margin-top: 10px;"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ record.index }}
|
||||
</template>
|
||||
<template v-if="column.key === 'type'">
|
||||
<code :class="record.type">{{ record.type }}</code>
|
||||
</template>
|
||||
<template v-if="column.key === 'time'">
|
||||
<code>{{ record.time }}</code>
|
||||
</template>
|
||||
<template v-if="column.key === 'content'">
|
||||
<code>{{ record.text }}</code>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSoftwareLogStore } from '@renderer/stores/softwareLog'
|
||||
import { type SoftwareLogItem } from '../types'
|
||||
|
||||
const softwareLog = useSoftwareLogStore()
|
||||
const { softwareLogs } = storeToRefs(softwareLog)
|
||||
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
pagination.value.current = page
|
||||
pagination.value.pageSize = pageSize
|
||||
},
|
||||
onShowSizeChange: (current: number, size: number) => {
|
||||
pagination.value.current = current
|
||||
pagination.value.pageSize = size
|
||||
}
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'index',
|
||||
dataIndex: 'index',
|
||||
key: 'index',
|
||||
width: 80,
|
||||
sorter: (a: SoftwareLogItem, b: SoftwareLogItem) => {
|
||||
if(a.index <= b.index) return -1
|
||||
return 1
|
||||
},
|
||||
sortDirections: ['descend'],
|
||||
defaultSortOrder: 'descend',
|
||||
},
|
||||
{
|
||||
title: 'type',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 80,
|
||||
sorter: (a: SoftwareLogItem, b: SoftwareLogItem) => {
|
||||
if(a.type <= b.type) return -1
|
||||
return 1
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'time',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
width: 135,
|
||||
sortDirections: ['descend'],
|
||||
},
|
||||
{
|
||||
title: 'content',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
},
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-title {
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.WARN {
|
||||
color: #ff7c05;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ERROR {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,41 +0,0 @@
|
||||
import { h } from 'vue';
|
||||
import { OrderedListOutlined, FileTextOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
export const logMenu = {
|
||||
zh: [
|
||||
{
|
||||
key: 'captionLog',
|
||||
icon: () => h(OrderedListOutlined),
|
||||
label: '字幕记录',
|
||||
},
|
||||
{
|
||||
key: 'projLog',
|
||||
icon: () => h(FileTextOutlined),
|
||||
label: '日志记录',
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
key: 'captionLog',
|
||||
icon: () => h(OrderedListOutlined),
|
||||
label: 'Caption Log',
|
||||
},
|
||||
{
|
||||
key: 'projLog',
|
||||
icon: () => h(FileTextOutlined),
|
||||
label: 'Software Log',
|
||||
},
|
||||
],
|
||||
ja: [
|
||||
{
|
||||
key: 'captionLog',
|
||||
icon: () => h(OrderedListOutlined),
|
||||
label: '字幕記録',
|
||||
},
|
||||
{
|
||||
key: 'projLog',
|
||||
icon: () => h(FileTextOutlined),
|
||||
label: 'ログ記録',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,29 +1,10 @@
|
||||
import { theme } from 'ant-design-vue';
|
||||
|
||||
let isLight = true
|
||||
let themeColor = '#1677ff'
|
||||
|
||||
export function setThemeColor(color: string) {
|
||||
themeColor = color
|
||||
}
|
||||
|
||||
export function getTheme(curIsLight?: boolean) {
|
||||
const lightTheme = {
|
||||
token: {
|
||||
colorPrimary: themeColor,
|
||||
colorInfo: themeColor
|
||||
}
|
||||
}
|
||||
const darkTheme = {
|
||||
export const antDesignTheme = {
|
||||
light: {
|
||||
token: {}
|
||||
},
|
||||
dark: {
|
||||
algorithm: theme.darkAlgorithm,
|
||||
token: {
|
||||
colorPrimary: themeColor,
|
||||
colorInfo: themeColor
|
||||
}
|
||||
}
|
||||
|
||||
if(curIsLight !== undefined){
|
||||
isLight = curIsLight
|
||||
}
|
||||
return isLight ? lightTheme : darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,3 @@ export * from './config/engine'
|
||||
export * from './config/audio'
|
||||
export * from './config/theme'
|
||||
export * from './config/linebreak'
|
||||
export * from './config/logMenu'
|
||||
@@ -37,8 +37,7 @@ export default {
|
||||
"theme": "Theme",
|
||||
"light": "light",
|
||||
"dark": "dark",
|
||||
"system": "system",
|
||||
"color": "Color"
|
||||
"system": "system"
|
||||
},
|
||||
engine: {
|
||||
"title": "Caption Engine Settings",
|
||||
@@ -106,7 +105,6 @@ export default {
|
||||
"started": "Started",
|
||||
"stopped": "Not Started",
|
||||
"logNumber": "Caption Count",
|
||||
"logNumber2": "Log Count",
|
||||
"aboutProj": "About Project",
|
||||
"openCaption": "Open Caption Window",
|
||||
"startEngine": "Start Caption Engine",
|
||||
@@ -121,7 +119,7 @@ export default {
|
||||
"projLink": "Project Link",
|
||||
"manual": "User Manual",
|
||||
"engineDoc": "Caption Engine Manual",
|
||||
"date": "August 20, 2025"
|
||||
"date": "July 30, 2025"
|
||||
}
|
||||
},
|
||||
log: {
|
||||
@@ -144,10 +142,7 @@ export default {
|
||||
"both": "Both",
|
||||
"source": "Original",
|
||||
"translation": "Translation",
|
||||
"copyNum": "Copy Count",
|
||||
"all": "All",
|
||||
"copySuccess": "Subtitle copied to clipboard",
|
||||
"clear": "Clear Log",
|
||||
"title2": "Software Log"
|
||||
"clear": "Clear Log"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ export default {
|
||||
"theme": "テーマ",
|
||||
"light": "明るい",
|
||||
"dark": "暗い",
|
||||
"system": "システム",
|
||||
"color": "カラー"
|
||||
"system": "システム"
|
||||
},
|
||||
engine: {
|
||||
"title": "字幕エンジン設定",
|
||||
@@ -106,7 +105,6 @@ export default {
|
||||
"started": "開始済み",
|
||||
"stopped": "未開始",
|
||||
"logNumber": "字幕数",
|
||||
"logNumber2": "ログ数",
|
||||
"aboutProj": "プロジェクト情報",
|
||||
"openCaption": "字幕ウィンドウを開く",
|
||||
"startEngine": "字幕エンジンを開始",
|
||||
@@ -121,11 +119,11 @@ export default {
|
||||
"projLink": "プロジェクトリンク",
|
||||
"manual": "ユーザーマニュアル",
|
||||
"engineDoc": "字幕エンジンマニュアル",
|
||||
"date": "2025 年 8 月 20 日"
|
||||
"date": "2025 年 7 月 30 日"
|
||||
}
|
||||
},
|
||||
log: {
|
||||
"title": "字幕記録",
|
||||
"title": "字幕ログ",
|
||||
"changeTime": "時間を変更",
|
||||
"baseTime": "最初の字幕開始時間",
|
||||
"hour": "時",
|
||||
@@ -133,7 +131,7 @@ export default {
|
||||
"sec": "秒",
|
||||
"ms": "ミリ秒",
|
||||
"export": "エクスポート",
|
||||
"copy": "記録をコピー",
|
||||
"copy": "ログをコピー",
|
||||
"exportOptions": "エクスポートオプション",
|
||||
"exportFormat": "形式",
|
||||
"exportContent": "内容",
|
||||
@@ -144,10 +142,7 @@ export default {
|
||||
"both": "すべて",
|
||||
"source": "原文",
|
||||
"translation": "翻訳",
|
||||
"copyNum": "コピー数",
|
||||
"all": "すべて",
|
||||
"copySuccess": "字幕がクリップボードにコピーされました",
|
||||
"clear": "記録をクリア",
|
||||
"title2": "ログ記録"
|
||||
"clear": "ログをクリア"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ export default {
|
||||
"theme": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "系统",
|
||||
"color": "颜色"
|
||||
"system": "系统"
|
||||
},
|
||||
engine: {
|
||||
"title": "字幕引擎设置",
|
||||
@@ -106,7 +105,6 @@ export default {
|
||||
"started": "已启动",
|
||||
"stopped": "未启动",
|
||||
"logNumber": "字幕数量",
|
||||
"logNumber2": "日志数量",
|
||||
"aboutProj": "项目关于",
|
||||
"openCaption": "打开字幕窗口",
|
||||
"startEngine": "启动字幕引擎",
|
||||
@@ -121,7 +119,7 @@ export default {
|
||||
"projLink": "项目链接",
|
||||
"manual": "用户手册",
|
||||
"engineDoc": "字幕引擎手册",
|
||||
"date": "2025 年 8 月 20 日"
|
||||
"date": "2025 年 7 月 30 日"
|
||||
}
|
||||
},
|
||||
log: {
|
||||
@@ -144,10 +142,7 @@ export default {
|
||||
"both": "全部",
|
||||
"source": "原文",
|
||||
"translation": "翻译",
|
||||
"copyNum": "复制数量",
|
||||
"all": "全部",
|
||||
"copySuccess": "字幕已复制到剪贴板",
|
||||
"clear": "清空记录",
|
||||
"title2": "日志记录"
|
||||
"clear": "清空记录"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,20 @@ import { defineStore } from 'pinia'
|
||||
import { i18n } from '../i18n'
|
||||
import type { UILanguage, UITheme } from '../types'
|
||||
|
||||
import { engines, audioTypes, breakOptions, setThemeColor, getTheme } from '../i18n'
|
||||
import { engines, audioTypes, antDesignTheme, breakOptions } from '../i18n'
|
||||
import { useEngineControlStore } from './engineControl'
|
||||
import { useCaptionStyleStore } from './captionStyle'
|
||||
|
||||
type RealTheme = 'light' | 'dark'
|
||||
|
||||
export const useGeneralSettingStore = defineStore('generalSetting', () => {
|
||||
const uiLanguage = ref<UILanguage>('zh')
|
||||
const realTheme = ref<RealTheme>('light')
|
||||
const uiTheme = ref<UITheme>('system')
|
||||
const uiColor = ref<string>('#1677ff')
|
||||
const leftBarWidth = ref<number>(8)
|
||||
|
||||
const antdTheme = ref<Object>(getTheme())
|
||||
|
||||
function handleThemeChange(newTheme: RealTheme) {
|
||||
realTheme.value = newTheme
|
||||
if(newTheme === 'dark' && uiColor.value === '#000000') {
|
||||
uiColor.value = '#b9d7ea'
|
||||
}
|
||||
if(newTheme === 'light' && uiColor.value === '#b9d7ea') {
|
||||
uiColor.value = '#000000'
|
||||
}
|
||||
}
|
||||
const antdTheme = ref<Object>(antDesignTheme['light'])
|
||||
|
||||
window.electron.ipcRenderer.invoke('control.nativeTheme.get').then((theme) => {
|
||||
if(theme === 'light') setLightTheme()
|
||||
else if(theme === 'dark') setDarkTheme()
|
||||
handleThemeChange(theme)
|
||||
})
|
||||
|
||||
watch(uiLanguage, (newValue) => {
|
||||
@@ -45,48 +30,30 @@ export const useGeneralSettingStore = defineStore('generalSetting', () => {
|
||||
watch(uiTheme, (newValue) => {
|
||||
window.electron.ipcRenderer.send('control.uiTheme.change', newValue)
|
||||
if(newValue === 'system'){
|
||||
window.electron.ipcRenderer.invoke('control.nativeTheme.get').then((theme: RealTheme) => {
|
||||
window.electron.ipcRenderer.invoke('control.nativeTheme.get').then((theme) => {
|
||||
if(theme === 'light') setLightTheme()
|
||||
else if(theme === 'dark') setDarkTheme()
|
||||
handleThemeChange(theme)
|
||||
})
|
||||
}
|
||||
else if(newValue === 'light'){
|
||||
setLightTheme()
|
||||
handleThemeChange('light')
|
||||
}
|
||||
else if(newValue === 'dark') {
|
||||
setDarkTheme()
|
||||
handleThemeChange('dark')
|
||||
}
|
||||
})
|
||||
|
||||
watch(uiColor, (newValue) => {
|
||||
setThemeColor(newValue)
|
||||
antdTheme.value = getTheme()
|
||||
window.electron.ipcRenderer.send('control.uiColor.change', newValue)
|
||||
else if(newValue === 'light') setLightTheme()
|
||||
else if(newValue === 'dark') setDarkTheme()
|
||||
})
|
||||
|
||||
watch(leftBarWidth, (newValue) => {
|
||||
window.electron.ipcRenderer.send('control.leftBarWidth.change', newValue)
|
||||
})
|
||||
|
||||
watch(realTheme, (newValue) => {
|
||||
console.log('realTheme', newValue)
|
||||
})
|
||||
|
||||
window.electron.ipcRenderer.on('control.uiLanguage.set', (_, args: UILanguage) => {
|
||||
uiLanguage.value = args
|
||||
})
|
||||
|
||||
window.electron.ipcRenderer.on('control.nativeTheme.change', (_, args: RealTheme) => {
|
||||
window.electron.ipcRenderer.on('control.nativeTheme.change', (_, args) => {
|
||||
if(args === 'light') setLightTheme()
|
||||
else if(args === 'dark') setDarkTheme()
|
||||
handleThemeChange(args)
|
||||
})
|
||||
|
||||
function setLightTheme(){
|
||||
antdTheme.value = getTheme(true)
|
||||
antdTheme.value = antDesignTheme.light
|
||||
const root = document.documentElement
|
||||
root.style.setProperty('--control-background', '#fff')
|
||||
root.style.setProperty('--tag-color', 'rgba(0, 0, 0, 0.45)')
|
||||
@@ -94,7 +61,7 @@ export const useGeneralSettingStore = defineStore('generalSetting', () => {
|
||||
}
|
||||
|
||||
function setDarkTheme(){
|
||||
antdTheme.value = getTheme(false)
|
||||
antdTheme.value = antDesignTheme.dark
|
||||
const root = document.documentElement
|
||||
root.style.setProperty('--control-background', '#000')
|
||||
root.style.setProperty('--tag-color', 'rgba(255, 255, 255, 0.45)')
|
||||
@@ -103,9 +70,7 @@ export const useGeneralSettingStore = defineStore('generalSetting', () => {
|
||||
|
||||
return {
|
||||
uiLanguage,
|
||||
realTheme,
|
||||
uiTheme,
|
||||
uiColor,
|
||||
leftBarWidth,
|
||||
antdTheme
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { type SoftwareLogItem } from '../types'
|
||||
|
||||
export const useSoftwareLogStore = defineStore('softwareLog', () => {
|
||||
const softwareLogs = ref<SoftwareLogItem[]>([])
|
||||
|
||||
function clear() {
|
||||
softwareLogs.value = []
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on('control.softwareLog.add', (_, log) => {
|
||||
softwareLogs.value.push(log)
|
||||
console.log(log)
|
||||
})
|
||||
|
||||
return {
|
||||
softwareLogs,
|
||||
clear
|
||||
}
|
||||
})
|
||||
@@ -45,23 +45,14 @@ export interface CaptionItem {
|
||||
translation: string
|
||||
}
|
||||
|
||||
export interface SoftwareLogItem {
|
||||
type: "INFO" | "WARN" | "ERROR",
|
||||
index: number,
|
||||
time: string,
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface FullConfig {
|
||||
platform: string,
|
||||
uiLanguage: UILanguage,
|
||||
uiTheme: UITheme,
|
||||
uiColor: string,
|
||||
leftBarWidth: number,
|
||||
styles: Styles,
|
||||
controls: Controls,
|
||||
captionLog: CaptionItem[],
|
||||
softwareLog: SoftwareLogItem[]
|
||||
captionLog: CaptionItem[]
|
||||
}
|
||||
|
||||
export interface EngineInfo {
|
||||
|
||||
@@ -11,13 +11,7 @@
|
||||
<a-col :span="24 - leftBarWidth">
|
||||
<div class="caption-data">
|
||||
<EngineStatus />
|
||||
<div class="log-container">
|
||||
<a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" />
|
||||
<div style="padding: 16px;">
|
||||
<CaptionLog v-if="current[0] === 'captionLog'" />
|
||||
<SoftwareLog v-else />
|
||||
</div>
|
||||
</div>
|
||||
<CaptionLog />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -30,22 +24,11 @@ import CaptionStyle from '../components/CaptionStyle.vue'
|
||||
import EngineControl from '../components/EngineControl.vue'
|
||||
import EngineStatus from '@renderer/components/EngineStatus.vue'
|
||||
import CaptionLog from '../components/CaptionLog.vue'
|
||||
import SoftwareLog from '@renderer/components/SoftwareLog.vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
|
||||
import { ref, watch } from 'vue'
|
||||
import { MenuProps } from 'ant-design-vue'
|
||||
import { logMenu } from '@renderer/i18n'
|
||||
|
||||
const generalSettingStore = useGeneralSettingStore()
|
||||
const { leftBarWidth, antdTheme, uiLanguage } = storeToRefs(generalSettingStore)
|
||||
|
||||
const current = ref<string[]>(['captionLog'])
|
||||
const items = ref<MenuProps['items']>(logMenu[uiLanguage.value])
|
||||
|
||||
watch(uiLanguage, (val) => {
|
||||
items.value = logMenu[val]
|
||||
})
|
||||
const { leftBarWidth, antdTheme } = storeToRefs(generalSettingStore)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -70,10 +53,4 @@ watch(uiLanguage, (val) => {
|
||||
.caption-data::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
padding: 20px 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user