├── .github
└── workflows
│ └── manual.yml
├── .gitignore
├── 00_开发.bat
├── README.md
├── README_EN.md
├── README_JA.md
├── app.py
├── base
├── Base.py
├── BaseLanguage.py
├── CLIManager.py
├── EventManager.py
├── LogManager.py
└── VersionManager.py
├── frontend
├── AppFluentWindow.py
├── AppSettingsPage.py
├── EmptyPage.py
├── Extra
│ ├── BatchCorrectionPage.py
│ ├── LaboratoryPage.py
│ ├── NameFieldExtractionPage.py
│ ├── ReTranslationPage.py
│ └── ToolBoxPage.py
├── Project
│ ├── ArgsEditPage.py
│ ├── ModelListPage.py
│ ├── PlatformEditPage.py
│ ├── PlatformPage.py
│ └── ProjectPage.py
├── Quality
│ ├── CustomPromptPage.py
│ ├── GlossaryPage.py
│ ├── TextPreservePage.py
│ └── TextReplacementPage.py
├── Setting
│ ├── BasicSettingsPage.py
│ └── ExpertSettingsPage.py
└── TranslationPage.py
├── module
├── Cache
│ ├── CacheItem.py
│ ├── CacheManager.py
│ └── CacheProject.py
├── Config.py
├── Engine
│ ├── API
│ │ └── APITester.py
│ ├── Engine.py
│ ├── TaskLimiter.py
│ ├── TaskRequester.py
│ └── Translator
│ │ ├── Translator.py
│ │ └── TranslatorTask.py
├── File
│ ├── ASS.py
│ ├── EPUB.py
│ ├── FileManager.py
│ ├── KVJSON.py
│ ├── MD.py
│ ├── MESSAGEJSON.py
│ ├── RENPY.py
│ ├── SRT.py
│ ├── TRANS
│ │ ├── KAG.py
│ │ ├── NONE.py
│ │ ├── RENPY.py
│ │ ├── RPGMAKER.py
│ │ ├── TRANS.py
│ │ └── WOLF.py
│ ├── TXT.py
│ ├── WOLFXLSX.py
│ └── XLSX.py
├── Filter
│ ├── LanguageFilter.py
│ └── RuleFilter.py
├── Fixer
│ ├── CodeFixer.py
│ ├── EscapeFixer.py
│ ├── HangeulFixer.py
│ ├── KanaFixer.py
│ ├── NumberFixer.py
│ └── PunctuationFixer.py
├── Localizer
│ ├── Localizer.py
│ ├── LocalizerEN.py
│ └── LocalizerZH.py
├── Normalizer.py
├── ProgressBar.py
├── PromptBuilder.py
├── Response
│ ├── ResponseChecker.py
│ └── ResponseDecoder.py
├── ResultChecker.py
├── RubyCleaner.py
├── TableManager.py
├── Text
│ ├── TextBase.py
│ └── TextHelper.py
└── TextProcessor.py
├── requirements.txt
├── resource
├── 7za.exe
├── custom_prompt
│ └── zh
│ │ ├── 01_文言文风格.txt
│ │ └── 02_文学措辞风格.txt
├── glossary_preset
│ ├── en
│ │ ├── 01_example.json
│ │ └── 02_common_menu_text.json
│ └── zh
│ │ ├── 01_示例.json
│ │ └── 02_常用菜单文本.json
├── icon.ico
├── icon_full.png
├── icon_no_bg.png
├── platforms
│ ├── en
│ │ ├── 0_sakura.json
│ │ ├── 10_volcengine.json
│ │ ├── 11_custom_google.json
│ │ ├── 12_custom_openai.json
│ │ ├── 13_custom_anthropic.json
│ │ ├── 1_google.json
│ │ ├── 2_openai.json
│ │ ├── 3_deepseek.json
│ │ ├── 4_anthropic.json
│ │ ├── 5_aliyun.json
│ │ ├── 6_zhipu.json
│ │ ├── 7_yi.json
│ │ ├── 8_moonshot.json
│ │ └── 9_siliconflow.json
│ └── zh
│ │ ├── 0_sakura.json
│ │ ├── 10_volcengine.json
│ │ ├── 11_custom_google.json
│ │ ├── 12_custom_openai.json
│ │ ├── 13_custom_anthropic.json
│ │ ├── 1_google.json
│ │ ├── 2_openai.json
│ │ ├── 3_deepseek.json
│ │ ├── 4_anthropic.json
│ │ ├── 5_aliyun.json
│ │ ├── 6_zhipu.json
│ │ ├── 7_yi.json
│ │ ├── 8_moonshot.json
│ │ └── 9_siliconflow.json
├── post_translation_replacement_preset
│ ├── en
│ │ ├── 01_common.json
│ │ └── 02_leading_trailing_quotes_half_to_full.json
│ └── zh
│ │ ├── 01_常用.json
│ │ └── 02_首尾半角引号转直角引号.json
├── pre_translation_replacement_preset
│ ├── en
│ │ └── 01_example.json
│ └── zh
│ │ └── 01_示例.json
├── prompt
│ ├── en
│ │ ├── base.txt
│ │ ├── prefix.txt
│ │ ├── suffix.txt
│ │ └── suffix_glossary.txt
│ └── zh
│ │ ├── base.txt
│ │ ├── prefix.txt
│ │ ├── suffix.txt
│ │ └── suffix_glossary.txt
├── pyinstaller.py
└── text_preserve_preset
│ ├── en
│ ├── kag.json
│ ├── none.json
│ ├── renpy.json
│ ├── rpgmaker.json
│ └── wolf.json
│ └── zh
│ ├── kag.json
│ ├── none.json
│ ├── renpy.json
│ ├── rpgmaker.json
│ └── wolf.json
├── version.txt
└── widget
├── ComboBoxCard.py
├── CommandBarCard.py
├── EmptyCard.py
├── FlowCard.py
├── GroupCard.py
├── LineEditCard.py
├── LineEditMessageBox.py
├── PushButtonCard.py
├── SearchCard.py
├── Separator.py
├── SliderCard.py
├── SpinCard.py
├── SwitchButtonCard.py
└── WaveformWidget.py
/.github/workflows/manual.yml:
--------------------------------------------------------------------------------
1 | name: Manual Build
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | build:
8 | runs-on: windows-latest
9 | permissions:
10 | actions: write
11 | checks: write
12 | contents: write
13 | deployments: write
14 | issues: write
15 | packages: write
16 | pages: write
17 | pull-requests: write
18 | repository-projects: write
19 | security-events: write
20 | statuses: write
21 |
22 | steps:
23 | - name: Setup Python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: 3.13
27 |
28 | - name: Checkout Repository
29 | uses: actions/checkout@v4
30 |
31 | - name: Check Version
32 | id: check_version
33 | shell: pwsh
34 | run: |
35 | $version = Get-Content -Path version.txt
36 | echo "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
37 |
38 | - name: Install Requirements
39 | shell: cmd
40 | run: |
41 | python -m pip install --upgrade pip
42 | python -m pip install --upgrade setuptools
43 | python -m pip install pyinstaller
44 | python -m pip install -U -r requirements.txt
45 | python -m pip cache purge
46 |
47 | - name: Build EXE
48 | shell: cmd
49 | run: |
50 | python .\resource\pyinstaller.py
51 |
52 | - name: Copy Files
53 | shell: cmd
54 | run: |
55 | xcopy ".\version.txt" ".\dist\LinguaGacha\" /Q /Y
56 | xcopy ".\resource\" ".\dist\LinguaGacha\resource\" /E /I /Q /H /Y
57 | xcopy ".\resource\" ".\dist\LinguaGacha\resource\" /E /I /Q /H /Y
58 |
59 | del /Q /F ".\dist\LinguaGacha\resource\7za.exe"
60 | del /Q /F ".\dist\LinguaGacha\resource\pyinstaller.py"
61 |
62 | - name: Compress Archive
63 | shell: cmd
64 | run: |
65 | .\resource\7za.exe a -y -bt -mx5 LinguaGacha_${{ steps.check_version.outputs.version }}.zip .\dist\*
66 |
67 | - name: Create Release
68 | id: create_release
69 | uses: actions/create-release@v1
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72 | with:
73 | tag_name: MANUAL_BUILD_${{ steps.check_version.outputs.version }}
74 | release_name: LinguaGacha_${{ steps.check_version.outputs.version }}
75 | draft: true
76 | prerelease: false
77 |
78 | - name: Upload Release
79 | uses: actions/upload-release-asset@v1
80 | env:
81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82 | with:
83 | upload_url: ${{ steps.create_release.outputs.upload_url }}
84 | asset_path: ./LinguaGacha_${{ steps.check_version.outputs.version }}.zip
85 | asset_name: LinguaGacha_${{ steps.check_version.outputs.version }}.zip
86 | asset_content_type: application/zip
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *__pycache__
2 |
3 | /*log*
4 | /*debug*
5 | /*input*
6 | /*output*
7 | /*.json
8 | /*.xlsx
9 | /resource/config.json
10 | /resource/expert_config.json
--------------------------------------------------------------------------------
/00_开发.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | @chcp 65001 > nul
3 |
4 | @REM 设置工作目录
5 | cd /d %~dp0
6 |
7 | @REM 设置临时 PATH
8 | @REM set PATH=%~dp0\resource;%PATH%
9 |
10 | @REM 启动应用
11 | call python app.py
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | 使用 AI 能力一键翻译 小说、游戏、字幕 等文本内容的次世代文本翻译器
4 |
5 | ## README 🌍
6 | - [ [中文](./README.md) ] | [ [English](./README_EN.md) ] | [ [日本語](./README_JA.md) ]
7 |
8 | ## 概述 📢
9 | - [LinguaGacha](https://github.com/neavo/LinguaGacha) (/ˈlɪŋɡwə ˈɡɑːtʃə/),使用 AI 技术次世代文本翻译器
10 | - 开箱即用,(几乎)无需设置,功能的强大,不需要通过繁琐的设置来体现
11 | - 支持 `中` `英` `日` `韩` `俄` `德` `法` `意` 等 16 种语言的一键互译
12 | - 支持 `字幕`、`电子书`、`游戏文本` 等多种文本类型与文本格式
13 | - 支持 `Claude`、`ChatGPT`、`DeepSeek`、`SakuraLLM` 等各种本地或在线接口
14 |
15 | >
16 |
17 | >
18 |
19 | ## 特别说明 ⚠️
20 | - 如您在翻译过程中使用了 [LinguaGacha](https://github.com/neavo/LinguaGacha) ,请在作品信息或发布页面的显要位置进行说明!
21 | - 如您的项目涉及任何商业行为或者商业收益,在使用 [LinguaGacha](https://github.com/neavo/LinguaGacha) 前,请先与作者联系以获得授权!
22 |
23 | ## 功能优势 📌
24 | - 极快的翻译速度,十秒钟一份字幕,一分钟一本小说,五分钟一部游戏
25 | - 自动生成术语表,保证角色姓名等专有名词在整部作品中的译名统一 `👈👈 独家绝技`
26 | - 最优的翻译质量,无论是 旗舰模型 `诸如 DeepSeek-R1` 还是 本地小模型 `诸如 Qwen2.5-7B`
27 | - 同类应用中最强的样式与代码保留能力,显著减少后期工作量,是制作内嵌汉化的最佳选择
28 | - `.md` `.ass` `.epub` 格式几乎可以保留所有原有样式
29 | - 大部分的 `WOLF`、`RenPy`、`RPGMaker`、`Kirikiri` 引擎游戏无需人工处理,即翻即玩 `👈👈 独家绝技`
30 |
31 | ## 配置要求 🖥️
32 | - 兼容 `OpenAI` `Google` `Anthropic` `SakuraLLM` 标准的 AI 大模型接口
33 | - 兼容 [KeywordGacha](https://github.com/neavo/KeywordGacha) `👈👈 使用 AI 能力一键生成术语表的次世代工具`
34 |
35 | ## 基本流程 🛸
36 | - 从 [发布页](https://github.com/neavo/LinguaGacha/releases) 下载应用
37 | - 获取一个可靠的 AI 大模型接口,建议选择其一:
38 | - [ [本地接口](https://github.com/neavo/OneClickLLAMA) ],免费,需至少 8G 显存的独立显卡,Nvidia 显卡为佳
39 | - [ [火山引擎](https://github.com/neavo/LinguaGacha/wiki/VolcEngine) ],需付费但便宜,速度快,质量高,无显卡要求 `👈👈 推荐`
40 | - [ [DeepSeek](https://github.com/neavo/LinguaGacha/wiki/DeepSeek) ],需付费但便宜,速度快,质量高,无显卡要求 `👈👈 白天不稳定,备选`
41 | - 准备要翻译的文本
42 | - `字幕`、`电子书` 等一般不需要预处理
43 | - `游戏文本` 需要根据游戏引擎选择合适的工具进行提取
44 | - 双击 `app.exe` 启动应用
45 | - 在 `项目设置` 中设置原文语言、译文语言等必要信息
46 | - 将要翻译的文本文件复制到输入文件夹(默认为 `input` 文件夹),在 `开始翻译` 中点击开始翻译
47 |
48 | ## 使用教程 📝
49 | - 综合
50 | - [基础教程](https://github.com/neavo/LinguaGacha/wiki/BasicTutorial) `👈👈 手把手教学,有手就行,新手必看`
51 | - [Google Gemini 免费接口](https://github.com/neavo/LinguaGacha/wiki/GoogleGeminiFree)
52 | - [高质量翻译 WOLF 引擎游戏的最佳实践](https://github.com/neavo/LinguaGacha/wiki/BestPracticeForWOLF)
53 | - [高质量翻译 RenPy 引擎游戏的最佳实践](https://github.com/neavo/LinguaGacha/wiki/BestPracticeForRenPy)
54 | - [高质量翻译 RPGMaker 系列引擎游戏的最佳实践](https://github.com/neavo/LinguaGacha/wiki/BestPracticeForRPGMaker)
55 | - 视频教程
56 | - [How to Translate RPGMV with LinguaGacha and Translator++ (English)](https://www.youtube.com/watch?v=wtV_IODzi8I)
57 | - 功能说明
58 | - [命令行模式](https://github.com/neavo/LinguaGacha/wiki/CLIMode)
59 | - [术语表](https://github.com/neavo/LinguaGacha/wiki/Glossary) [文本保护](https://github.com/neavo/LinguaGacha/wiki/TextPreserve) [文本替换](https://github.com/neavo/LinguaGacha/wiki/Replacement)
60 | - [补充翻译](https://github.com/neavo/LinguaGacha/wiki/IncrementalTranslation) [MTool 优化器](https://github.com/neavo/LinguaGacha/wiki/MToolOptimizer)
61 | - [百宝箱 - 批量修正](https://github.com/neavo/LinguaGacha/wiki/BatchCorrection) [百宝箱 - 部分重翻](https://github.com/neavo/LinguaGacha/wiki/ReTranslation) [百宝箱 - 姓名字段提取](https://github.com/neavo/LinguaGacha/wiki/NameFieldExtraction)
62 | - 你可以在 [Wiki](https://github.com/neavo/LinguaGacha/wiki) 找到各项功能的更详细介绍,也欢迎在 [讨论区](https://github.com/neavo/LinguaGacha/discussions) 投稿你的使用心得
63 |
64 | ## 文本格式 🏷️
65 | - 在任务开始时,应用将读取输入文件夹(及其子目录)内所有支持的文件,包括但是不限于:
66 | - 字幕(.srt .ass)
67 | - 电子书(.txt .epub)
68 | - Markdown(.md)
69 | - [RenPy](https://www.renpy.org) 导出游戏文本(.rpy)
70 | - [MTool](https://mtool.app) 导出游戏文本(.json)
71 | - [SExtractor](https://github.com/satan53x/SExtractor) 导出游戏文本(.txt .json .xlsx)
72 | - [VNTextPatch](https://github.com/arcusmaximus/VNTranslationTools) 导出游戏文本(.json)
73 | - [Translator++](https://dreamsavior.net/translator-plusplus) 项目文件(.trans)
74 | - [Translator++](https://dreamsavior.net/translator-plusplus) 导出游戏文本(.xlsx)
75 | - [WOLF 官方翻译工具](https://silversecond.booth.pm/items/5151747) 导出游戏文本(.xlsx)
76 | - 具体示例可见 [Wiki - 支持的文件格式](https://github.com/neavo/LinguaGacha/wiki/%E6%94%AF%E6%8C%81%E7%9A%84%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F),更多格式将持续添加,你也可以在 [ISSUES](https://github.com/neavo/LinguaGacha/issues) 中提出你的需求
77 |
78 | ## 近期更新 📅
79 | - 20250603 v0.29.1
80 | - 修正 - 继续任务功能
81 |
82 | - 20250602 v0.29.0
83 | - 新增 - [命令行模式](https://github.com/neavo/LinguaGacha/wiki/CLIMode)
84 | - 调整 - 更准确的请求超时时间控制
85 | - 调整 - 接口测试时打印失败的密钥
86 |
87 | ## 常见问题 📥
88 | - [LinguaGacha](https://github.com/neavo/LinguaGacha) 与 [AiNiee](https://github.com/NEKOparapa/AiNiee) 的关系
89 | - `LinguaGacha` 的作者是 `AiNiee v5` 的主要开发与维护者之一
90 | - `AiNiee v5` 及延用至 `AiNiee v6` 的 UI 框架也是由作者主要负责设计和开发的
91 | - 这也是两者 UI 相似的原因,因为作者已经没有灵感再重新设计一套了,求放过 🤣
92 | - 不过 `LinguaGacha` 并不是 `AiNiee` 的分支版本,而是在其经验上开发的全新翻译器应用
93 | - 相对作者主力开发的 `AiNiee v5`,`LinguaGacha` 有一些独有的优势,包括但是不限于:
94 | - 零设置,全默认设置下即可实现最佳的翻译质量与翻译速度
95 | - 更好的性能优化,即使 512+ 并发任务时电脑也不会卡顿,实际翻译速度也更快
96 | - 原生支持 `.rpy` `.trans`,大部分 `WOLF`、`RenPy`、`RPGMaker`、`Kirikiri` 游戏即翻即玩
97 | - 对文件格式的支持更好,例如 `.md` `.ass` `.epub` 格式几乎可以保留所有原有样式
98 | - 更完善的预处理、后处理和结果检查功能,让制作高品质翻译的校对工作量显著减少
99 |
100 | ## 问题反馈 😥
101 | - 运行时的日志保存在应用根目录下的 `log` 等文件夹
102 | - 反馈问题的时候请附上这些日志文件
103 | - 你也可以来群组讨论与反馈
104 | - QQ - 41763231⑥
105 | - Discord - https://discord.gg/pyMRBGse75
106 |
--------------------------------------------------------------------------------
/README_JA.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | AIの能力を活用して小説、ゲーム、字幕などのテキストをワンクリックで翻訳する次世代のテキスト翻訳ツール
4 |
5 |
6 |
7 |
8 | ## README 🌍
9 | - [ [中文](./README.md) ] | [ [English](/README_EN.md) ] | [ [日本語](/README_JA.md) ]
10 |
11 | ## 概要 📢
12 | - [LinguaGacha](https://github.com/neavo/LinguaGacha) (/ˈlɪŋɡwə ˈɡɑːtʃə/)、AIを活用した次世代のテキスト翻訳ツールです
13 | - 箱から出してすぐに使え、(ほぼ)設定不要。機能の強力さは、煩雑な設定を必要としません。
14 | - `中国語`、`英語`、`日本語`、`韓国語`、`ロシア語`、`ドイツ語`、`フランス語`、`イタリア語`など 16 言語にワンタッチ双方向翻訳対応。
15 | - `字幕`、`電子書籍`、`ゲームテキストなど`、色々なテキストタイプと形式に対応。
16 | - `Claude`、`ChatGPT`、`DeepSeek`、`SakuraLLM` などのローカルおよびオンラインインターフェースをサポート
17 |
18 | >
19 |
20 | >
21 |
22 | ## 特別なお知らせ ⚠️
23 | - 翻訳中に [LinguaGacha](https://github.com/neavo/LinguaGacha) を使用する場合は、作品の情報やリリースページの目立つ場所に明確な帰属を含めてください!
24 | - 商業活動や利益を伴うプロジェクトの場合は、[LinguaGacha](https://github.com/neavo/LinguaGacha) を使用する前に、著者に連絡して許可を得てください!
25 |
26 | ## 機能の利点 📌
27 | - 圧倒的な翻訳速度、10秒で字幕1本、1分で小説1冊、5分でゲーム1本
28 | - 用語集の自動生成、キャラクター名などの専門用語の訳語を作品全体で統一 `👈👈 独自の強み`
29 | - 最高の翻訳品質、フラッグシップモデル `DeepSeek-R1など` でも、ローカル小規模モデル `Qwen2.5-7Bなど` でも
30 | - 同種のアプリケーションの中で最強のスタイルとコード保持能力、後工程の作業量を大幅に削減、字幕埋め込み(内嵌字幕)作成に最適
31 | - `.md` `.ass` `.epub` 形式はほぼすべての元のスタイルを保持可能
32 | - 大部分の `WOLF`、`RenPy`、`RPGMaker`、`Kirikiri` エンジンゲームは手作業なしで、即翻訳即プレイ可能 `👈👈 独自の強み`
33 |
34 | ## システム要件 🖥️
35 | - `OpenAI`、`Google`、`Anthropic`、`SakuraLLM` 標準に準拠したAIモデルインターフェースに対応
36 | - [KeywordGacha](https://github.com/neavo/KeywordGacha) と互換性あり `👈👈 AIを活用して用語集をワンクリックで生成する次世代ツール`
37 |
38 | ## ワークフロー 🛸
39 | - [リリースページ](https://github.com/neavo/LinguaGacha/releases) からアプリケーションをダウンロード
40 | - 信頼できるAIモデルインターフェースを取得(以下のいずれかを選択):
41 | - [ [Local API](https://github.com/neavo/OneClickLLAMA) ] (無料、8GB以上のVRAM GPUが必要、Nvidia推奨)
42 | - [ [Gemini API](https://aistudio.google.com/) ] (有料、費用対効果が高い、高速、比較的高品質、GPU不要) `👈👈 推奨`
43 | - [ [DeepSeek API](https://github.com/neavo/LinguaGacha/wiki/DeepSeek) ] (有料、費用対効果が高い、高速、高品質、GPU不要) `👈👈 日中不安定、代替案`
44 | - ソーステキストを準備:
45 | - `字幕`/`電子書籍`は通常、前処理が不要
46 | - `ゲームテキスト`は特定のゲームエンジンに適したツールを使用して抽出が必要
47 | - `app.exe` を実行してアプリケーションを起動:
48 | - `プロジェクト設定` で必要な設定(ソース/ターゲット言語)を行う
49 | - 入力フォルダ(デフォルト:`input`)にファイルをコピーし、`翻訳開始` で翻訳を開始
50 |
51 | ## 使い方チュートリアル - English 📝
52 | - Overall
53 | - [Basic Tutorial](https://github.com/neavo/LinguaGacha/wiki/BasicTutorial) `👈👈 Step-by-step tutorial, easy to follow, a must-read for beginners`
54 | - [Google Gemini Free API](https://github.com/neavo/LinguaGacha/wiki/GoogleGeminiFreeEN)
55 | - [Best Practices for High-Quality Translation of WOLF Engine Games](https://github.com/neavo/LinguaGacha/wiki/BestPracticeForWOLFEN)
56 | - [Best Practices for High-Quality Translation of RPGMaker Series Engine Games](https://github.com/neavo/LinguaGacha/wiki/BestPracticeForRPGMakerEN)
57 | - Video Tutorial
58 | - [How to Translate RPGMV with LinguaGacha and Translator++ (English)](https://www.youtube.com/watch?v=NbpyL2fMgDc)
59 | - Feature Description
60 | - [CLI Mode](https://github.com/neavo/LinguaGacha/wiki/CLIModeEN)
61 | - [Glossary](https://github.com/neavo/LinguaGacha/wiki/GlossaryEN) [Text Preserve](https://github.com/neavo/LinguaGacha/wiki/TextPreserveEN) [Text Replacement](https://github.com/neavo/LinguaGacha/wiki/ReplacementEN)
62 | - [Incremental Translation](https://github.com/neavo/LinguaGacha/wiki/IncrementalTranslationEN) [MTool Optimizer](https://github.com/neavo/LinguaGacha/wiki/MToolOptimizerEN)
63 | - [Treasure Chest - Batch Correction](https://github.com/neavo/LinguaGacha/wiki/BatchCorrectionEN) [Treasure Chest - Partial ReTranslatio](https://github.com/neavo/LinguaGacha/wiki/ReTranslationEN) [Treasure Chest - Name-Field Extraction](https://github.com/neavo/LinguaGacha/wiki/NameFieldExtractionEN)
64 | - You can find more details on each feature in the [Wiki](https://github.com/neavo/LinguaGacha/wiki), and you are welcome to share your experience in the [Discussions](https://github.com/neavo/LinguaGacha/discussions)
65 |
66 | ## 対応フォーマット 🏷️
67 | - 入力フォルダ内のすべての対応ファイル(サブディレクトリを含む)を処理:
68 | - 字幕 (.srt .ass)
69 | - 電子書籍 (.txt .epub)
70 | - Markdown(.md)
71 | - [RenPy](https://www.renpy.org) エクスポート (.rpy)
72 | - [MTool](https://mtool.app) エクスポート (.json)
73 | - [SExtractor](https://github.com/satan53x/SExtractor) エクスポート (.txt .json .xlsx)
74 | - [VNTextPatch](https://github.com/arcusmaximus/VNTranslationTools) exports (.json)
75 | - [Translator++](https://dreamsavior.net/translator-plusplus) プロジェクト (.trans)
76 | - [Translator++](https://dreamsavior.net/translator-plusplus) エクスポート (.xlsx)
77 | - [WOLF 公式翻訳ツール](https://silversecond.booth.pm/items/5151747) エクスポート(.xlsx)
78 | - 例については [Wiki - 対応フォーマット](https://github.com/neavo/LinguaGacha/wiki/%E6%94%AF%E6%8C%81%E7%9A%84%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F) を参照。フォーマットのリクエストは [ISSUES](https://github.com/neavo/LinguaGacha/issues) で提出
79 |
80 | ## FAQ 📥
81 | - [LinguaGacha](https://github.com/neavo/LinguaGacha) と [AiNiee](https://github.com/NEKOparapa/AiNiee) の関係
82 | - `LinguaGacha` の作者は `AiNiee v5` の主要な開発およびメンテナンス担当者の一人です
83 | - `LinguaGacha` は `AiNiee` の派生ではなく、`AiNiee` の経験を基に開発された、よりシンプルで強力な、全く新しい翻訳アプリです
84 | - よりシンプルに、よりパワフルに。機能の強力さは、煩雑な設定を必要としません。
85 |
86 | ## サポート 😥
87 | - 実行時のログは `log` フォルダに保存されます
88 | - 問題を報告する際は、関連するログを添付してください
89 | - グループに参加して、ディスカッションやフィードバックもできます。
90 | - Discord - https://discord.gg/pyMRBGse75
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import ctypes
3 | import os
4 | import signal
5 | import sys
6 | import time
7 | from types import TracebackType
8 |
9 | from PyQt5.QtCore import Qt
10 | from PyQt5.QtGui import QFont
11 | from PyQt5.QtGui import QIcon
12 | from PyQt5.QtWidgets import QApplication
13 | from qfluentwidgets import Theme
14 | from qfluentwidgets import setTheme
15 | from rich.console import Console
16 |
17 | from base.Base import Base
18 | from base.CLIManager import CLIManager
19 | from base.LogManager import LogManager
20 | from base.VersionManager import VersionManager
21 | from frontend.AppFluentWindow import AppFluentWindow
22 | from module.Config import Config
23 | from module.Engine.Engine import Engine
24 | from module.Localizer.Localizer import Localizer
25 |
26 | def excepthook(exc_type: type[BaseException], exc_value: BaseException, exc_traceback: TracebackType) -> None:
27 | LogManager.get().error(Localizer.get().log_crash, exc_value)
28 |
29 | if not isinstance(exc_value, KeyboardInterrupt):
30 | print("")
31 | for i in range(3):
32 | print(f"退出中 … Exiting … {3 - i} …")
33 | time.sleep(1)
34 |
35 | os.kill(os.getpid(), signal.SIGTERM)
36 |
37 | if __name__ == "__main__":
38 | # 捕获全局异常
39 | sys.excepthook = lambda exc_type, exc_value, exc_traceback: excepthook(exc_type, exc_value, exc_traceback)
40 |
41 | # 当运行在 Windows 系统且没有运行在新终端时,禁用快速编辑模式
42 | if os.name == "nt" and Console().color_system != "truecolor":
43 | kernel32 = ctypes.windll.kernel32
44 |
45 | # 获取控制台句柄
46 | hStdin = kernel32.GetStdHandle(-10)
47 | mode = ctypes.c_ulong()
48 |
49 | # 获取当前控制台模式
50 | if kernel32.GetConsoleMode(hStdin, ctypes.byref(mode)):
51 | # 清除启用快速编辑模式的标志 (0x0040)
52 | mode.value &= ~0x0040
53 | # 设置新的控制台模式
54 | kernel32.SetConsoleMode(hStdin, mode)
55 |
56 | # 1. 全局缩放使能 (Enable High DPI Scaling)
57 | QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
58 | # 2. 适配非整数倍缩放 (Adapt non-integer scaling)
59 | QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
60 |
61 | # 设置工作目录
62 | sys.path.append(os.path.dirname(os.path.abspath(sys.argv[0])))
63 |
64 | # 创建文件夹
65 | os.makedirs("./input", exist_ok = True)
66 | os.makedirs("./output", exist_ok = True)
67 |
68 | # 载入并保存默认配置
69 | config = Config().load()
70 |
71 | # 加载版本号
72 | with open("version.txt", "r", encoding = "utf-8-sig") as reader:
73 | version = reader.read().strip()
74 |
75 | # 设置主题
76 | setTheme(Theme.DARK if config.theme == Config.Theme.DARK else Theme.LIGHT)
77 |
78 | # 设置应用语言
79 | Localizer.set_app_language(config.app_language)
80 |
81 | # 打印日志
82 | LogManager.get().info(f"LinguaGacha {version}")
83 | LogManager.get().info(Localizer.get().log_expert_mode) if LogManager.get().is_expert_mode() else None
84 |
85 | # 网络代理
86 | if config.proxy_enable == False or config.proxy_url == "":
87 | os.environ.pop("http_proxy", None)
88 | os.environ.pop("https_proxy", None)
89 | else:
90 | LogManager.get().info(Localizer.get().log_proxy)
91 | os.environ["http_proxy"] = config.proxy_url
92 | os.environ["https_proxy"] = config.proxy_url
93 |
94 | # 设置全局缩放比例
95 | if config.scale_factor == "50%":
96 | os.environ["QT_SCALE_FACTOR"] = "0.50"
97 | elif config.scale_factor == "75%":
98 | os.environ["QT_SCALE_FACTOR"] = "0.75"
99 | elif config.scale_factor == "150%":
100 | os.environ["QT_SCALE_FACTOR"] = "1.50"
101 | elif config.scale_factor == "200%":
102 | os.environ["QT_SCALE_FACTOR"] = "2.00"
103 | else:
104 | os.environ.pop("QT_SCALE_FACTOR", None)
105 |
106 | # 创建全局应用对象
107 | app = QApplication(sys.argv)
108 |
109 | # 设置应用图标
110 | app.setWindowIcon(QIcon("resource/icon_no_bg.png"))
111 |
112 | # 设置全局字体属性,解决狗牙问题
113 | font = QFont()
114 | if config.font_hinting == True:
115 | font.setHintingPreference(QFont.HintingPreference.PreferFullHinting)
116 | else:
117 | font.setHintingPreference(QFont.HintingPreference.PreferNoHinting)
118 | app.setFont(font)
119 |
120 | # 启动任务引擎
121 | Engine.get().run()
122 |
123 | # 创建版本管理器
124 | VersionManager.get().set_version(version)
125 |
126 | # 处理启动参数
127 | if CLIManager.get().run() == False:
128 | app_fluent_window = AppFluentWindow()
129 | app_fluent_window.show()
130 |
131 | # 进入事件循环,等待用户操作
132 | sys.exit(app.exec())
--------------------------------------------------------------------------------
/base/Base.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 | from typing import Callable
3 |
4 | from base.EventManager import EventManager
5 | from base.LogManager import LogManager
6 |
7 | class Base():
8 |
9 | # 事件
10 | class Event(StrEnum):
11 |
12 | PLATFORM_TEST_DONE = "PLATFORM_TEST_DONE" # API 测试完成
13 | PLATFORM_TEST_START = "PLATFORM_TEST_START" # API 测试开始
14 | TRANSLATION_START = "TRANSLATION_START" # 翻译开始
15 | TRANSLATION_STOP = "TRANSLATION_STOP" # 翻译停止
16 | TRANSLATION_DONE = "TRANSLATION_DONE" # 翻译完成
17 | TRANSLATION_UPDATE = "TRANSLATION_UPDATE" # 翻译状态更新
18 | TRANSLATION_MANUAL_EXPORT = "TRANSLATION_MANUAL_EXPORT" # 翻译结果手动导出
19 | CACHE_FILE_AUTO_SAVE = "CACHE_FILE_AUTO_SAVE" # 缓存文件自动保存
20 | PROJECT_STATUS = "PROJECT_STATUS" # 项目状态检查
21 | PROJECT_STATUS_CHECK_DONE = "PROJECT_STATUS_CHECK_DONE" # 项目状态检查完成
22 | APP_UPDATE_CHECK_START = "APP_UPDATE_CHECK_START" # 更新 - 检查开始
23 | APP_UPDATE_CHECK_DONE = "APP_UPDATE_CHECK_DONE" # 更新 - 检查完成
24 | APP_UPDATE_DOWNLOAD_START = "APP_UPDATE_DOWNLOAD_START" # 更新 - 下载开始
25 | APP_UPDATE_DOWNLOAD_DONE = "APP_UPDATE_DOWNLOAD_DONE" # 更新 - 下载完成
26 | APP_UPDATE_DOWNLOAD_ERROR = "APP_UPDATE_DOWNLOAD_ERROR" # 更新 - 下载报错
27 | APP_UPDATE_DOWNLOAD_UPDATE = "APP_UPDATE_DOWNLOAD_UPDATE" # 更新 - 下载更新
28 | APP_UPDATE_EXTRACT = "APP_UPDATE_EXTRACT" # 更新 - 解压
29 | APP_TOAST_SHOW = "APP_TOAST_SHOW" # 显示 Toast
30 | GLOSSARY_REFRESH = "GLOSSARY_REFRESH" # 术语表刷新
31 |
32 | # 接口格式
33 | class APIFormat(StrEnum):
34 |
35 | OPENAI = "OpenAI"
36 | GOOGLE = "Google"
37 | ANTHROPIC = "Anthropic"
38 | SAKURALLM = "SakuraLLM"
39 |
40 | # 接口格式
41 | class ToastType(StrEnum):
42 |
43 | INFO = "INFO"
44 | ERROR = "ERROR"
45 | SUCCESS = "SUCCESS"
46 | WARNING = "WARNING"
47 |
48 | # 翻译状态
49 | class TranslationStatus(StrEnum):
50 |
51 | UNTRANSLATED = "UNTRANSLATED" # 待翻译
52 | TRANSLATING = "TRANSLATING" # 翻译中
53 | TRANSLATED = "TRANSLATED" # 已翻译
54 | TRANSLATED_IN_PAST = "TRANSLATED_IN_PAST" # 过去已翻译
55 | EXCLUDED = "EXCLUDED" # 已排除
56 | DUPLICATED = "DUPLICATED" # 重复条目
57 |
58 | # 构造函数
59 | def __init__(self) -> None:
60 | pass
61 |
62 | # PRINT
63 | def print(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
64 | LogManager.get().print(msg, e, file, console)
65 |
66 | # DEBUG
67 | def debug(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
68 | LogManager.get().debug(msg, e, file, console)
69 |
70 | # INFO
71 | def info(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
72 | LogManager.get().info(msg, e, file, console)
73 |
74 | # ERROR
75 | def error(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
76 | LogManager.get().error(msg, e, file, console)
77 |
78 | # WARNING
79 | def warning(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
80 | LogManager.get().warning(msg, e, file, console)
81 |
82 | # 触发事件
83 | def emit(self, event: Event, data: dict) -> None:
84 | EventManager.get().emit(event, data)
85 |
86 | # 订阅事件
87 | def subscribe(self, event: Event, hanlder: Callable) -> None:
88 | EventManager.get().subscribe(event, hanlder)
89 |
90 | # 取消订阅事件
91 | def unsubscribe(self, event: Event, hanlder: Callable) -> None:
92 | EventManager.get().unsubscribe(event, hanlder)
--------------------------------------------------------------------------------
/base/BaseLanguage.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 |
3 | class BaseLanguage():
4 |
5 | class Enum(StrEnum):
6 | ZH = "ZH" # 中文 (Chinese)
7 | EN = "EN" # 英文 (English)
8 | JA = "JA" # 日文 (Japanese)
9 | KO = "KO" # 韩文 (Korean)
10 | RU = "RU" # 阿拉伯文 (Russian)
11 | AR = "AR" # 俄文 (Arabic)
12 | DE = "DE" # 德文 (German)
13 | FR = "FR" # 法文 (French)
14 | PL = "PL" # 波兰文 (Polish)
15 | ES = "ES" # 西班牙文 (Spanish)
16 | IT = "IT" # 意大利文 (Italian)
17 | PT = "PT" # 葡萄牙文 (Portuguese)
18 | HU = "HU" # 匈牙利文 (Hungrarian)
19 | TR = "TR" # 土耳其文 (Turkish)
20 | TH = "TH" # 泰文 (Thai)
21 | ID = "ID" # 印尼文 (Indonesian)
22 | VI = "VI" # 越南文 (Vietnamese)
23 |
24 | LANGUAGE_NAMES: dict[Enum, dict[str, str]] = {
25 | Enum.ZH: {"zh": "中文", "en": "Chinese"},
26 | Enum.EN: {"zh": "英文", "en": "English"},
27 | Enum.JA: {"zh": "日文", "en": "Japanese"},
28 | Enum.KO: {"zh": "韩文", "en": "Korean"},
29 | Enum.RU: {"zh": "俄文", "en": "Russian"},
30 | Enum.AR: {"zh": "阿拉伯文", "en": "Arabic"},
31 | Enum.DE: {"zh": "德文", "en": "German"},
32 | Enum.FR: {"zh": "法文", "en": "French"},
33 | Enum.PL: {"zh": "波兰文", "en": "Polish"},
34 | Enum.ES: {"zh": "西班牙", "en": "Spanish"},
35 | Enum.IT: {"zh": "意大利文", "en": "Italian"},
36 | Enum.PT: {"zh": "葡萄牙文", "en": "Portuguese"},
37 | Enum.HU: {"zh": "匈牙利文", "en": "Hungrarian"},
38 | Enum.TR: {"zh": "土耳其文", "en": "Turkish"},
39 | Enum.TH: {"zh": "泰文", "en": "Thai"},
40 | Enum.ID: {"zh": "印尼文", "en": "Indonesian"},
41 | Enum.VI: {"zh": "越南文", "en": "Vietnamese"},
42 | }
43 |
44 | @classmethod
45 | def is_cjk(cls, language: Enum) -> bool:
46 | return language in (cls.Enum.ZH, cls.Enum.JA, cls.Enum.KO)
47 |
48 | @classmethod
49 | def get_name_zh(cls, language: Enum) -> str:
50 | return cls.LANGUAGE_NAMES.get(language, {}).get("zh" "")
51 |
52 | @classmethod
53 | def get_name_en(cls, language: Enum) -> str:
54 | return cls.LANGUAGE_NAMES.get(language, {}).get("en", "")
55 |
56 | @classmethod
57 | def get_languages(cls) -> list[str]:
58 | return list(cls.LANGUAGE_NAMES.keys())
--------------------------------------------------------------------------------
/base/CLIManager.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | import signal
4 | import time
5 | from typing import Self
6 |
7 | from base.Base import Base
8 | from base.BaseLanguage import BaseLanguage
9 | from module.Config import Config
10 | from module.Localizer.Localizer import Localizer
11 |
12 | class CLIManager(Base):
13 |
14 | def __init__(self) -> None:
15 | super().__init__()
16 |
17 | @classmethod
18 | def get(cls) -> Self:
19 | if getattr(cls, "__instance__", None) is None:
20 | cls.__instance__ = cls()
21 |
22 | return cls.__instance__
23 |
24 | def translation_stop_done(self, event: str, data: dict) -> None:
25 | self.exit()
26 |
27 | def exit(self) -> None:
28 | print("")
29 | for i in range(3):
30 | print(f"退出中 … Exiting … {3 - i} …")
31 | time.sleep(1)
32 |
33 | os.kill(os.getpid(), signal.SIGTERM)
34 |
35 | def verify_file(self, path: str) -> bool:
36 | return os.path.isfile(path)
37 |
38 | def verify_folder(self, path: str) -> bool:
39 | return os.path.isdir(path)
40 |
41 | def verify_language(self, language: str) -> bool:
42 | return language in BaseLanguage.Enum
43 |
44 | def run(self) -> bool:
45 | parser = argparse.ArgumentParser()
46 | parser.add_argument("--cli", action = "store_true")
47 | parser.add_argument("--config", type = str)
48 | parser.add_argument("--input_folder", type = str)
49 | parser.add_argument("--output_folder", type = str)
50 | parser.add_argument("--source_language", type = str)
51 | parser.add_argument("--target_language", type = str)
52 | args = parser.parse_args()
53 |
54 | if args.cli == False:
55 | return False
56 |
57 | config: Config = None
58 | if isinstance(args.config, str) and self.verify_file(args.config):
59 | config = Config().load(args.config)
60 | else:
61 | config = Config().load()
62 |
63 | if isinstance(args.input_folder, str) == False:
64 | pass
65 | elif self.verify_folder(args.input_folder):
66 | config.input_folder = args.input_folder
67 | else:
68 | self.error(f"--input_folder {Localizer.get().cli_verify_folder}")
69 | self.exit()
70 |
71 | if isinstance(args.output_folder, str) == False:
72 | pass
73 | elif self.verify_folder(args.output_folder):
74 | config.output_folder = args.output_folder
75 | else:
76 | self.error(f"--output_folder {Localizer.get().cli_verify_folder}")
77 | self.exit()
78 |
79 | if isinstance(args.source_language, str) == False:
80 | pass
81 | elif self.verify_language(args.source_language):
82 | config.source_language = args.source_language
83 | else:
84 | self.error(f"--source_language {Localizer.get().cli_verify_language}")
85 | self.exit()
86 |
87 | if isinstance(args.target_language, str) == False:
88 | pass
89 | elif self.verify_language(args.target_language):
90 | config.target_language = args.target_language
91 | else:
92 | self.error(f"--target_language {Localizer.get().cli_verify_language}")
93 | self.exit()
94 |
95 | self.emit(Base.Event.TRANSLATION_START, {
96 | "config": config,
97 | "status": Base.TranslationStatus.UNTRANSLATED,
98 | })
99 | self.subscribe(Base.Event.TRANSLATION_DONE, self.translation_stop_done)
100 |
101 | return True
--------------------------------------------------------------------------------
/base/EventManager.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 | from typing import Self
3 | from typing import Callable
4 |
5 | from PyQt5.QtCore import Qt
6 | from PyQt5.QtCore import QObject
7 | from PyQt5.QtCore import pyqtSignal
8 |
9 | class EventManager(QObject):
10 |
11 | # 自定义信号
12 | # 字典类型或者其他复杂对象应该使用 object 作为信号参数类型,这样可以传递任意 Python 对象,包括 dict
13 | signal: pyqtSignal = pyqtSignal(StrEnum, object)
14 |
15 | # 事件列表
16 | event_callbacks: dict[StrEnum, list[Callable]] = {}
17 |
18 | def __init__(self) -> None:
19 | super().__init__()
20 |
21 | self.signal.connect(self.process_event, Qt.ConnectionType.QueuedConnection)
22 |
23 | @classmethod
24 | def get(cls) -> Self:
25 | if not hasattr(cls, "__instance__"):
26 | cls.__instance__ = cls()
27 |
28 | return cls.__instance__
29 |
30 | # 处理事件
31 | def process_event(self, event: StrEnum, data: dict) -> None:
32 | if event in self.event_callbacks:
33 | for hanlder in self.event_callbacks[event]:
34 | hanlder(event, data)
35 |
36 | # 触发事件
37 | def emit(self, event: StrEnum, data: dict) -> None:
38 | self.signal.emit(event, data)
39 |
40 | # 订阅事件
41 | def subscribe(self, event: StrEnum, hanlder: Callable) -> None:
42 | if callable(hanlder):
43 | self.event_callbacks.setdefault(event, []).append(hanlder)
44 |
45 | # 取消订阅事件
46 | def unsubscribe(self, event: StrEnum, hanlder: Callable) -> None:
47 | if event in self.event_callbacks:
48 | self.event_callbacks[event].remove(hanlder)
--------------------------------------------------------------------------------
/base/LogManager.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import traceback
4 | from logging.handlers import TimedRotatingFileHandler
5 | from typing import Self
6 |
7 | from rich.console import Console
8 | from rich.logging import RichHandler
9 |
10 | from module.ProgressBar import ProgressBar
11 |
12 | class LogManager():
13 |
14 | PATH: str = "./log"
15 |
16 | def __init__(self) -> None:
17 | super().__init__()
18 |
19 | # 控制台实例
20 | self.console = Console()
21 |
22 | # 文件日志实例
23 | os.makedirs(__class__.PATH, exist_ok = True)
24 | self.file_handler = TimedRotatingFileHandler(
25 | f"{__class__.PATH}/app.log",
26 | when = "midnight",
27 | interval = 1,
28 | encoding = "utf-8",
29 | backupCount = 3,
30 | )
31 | self.file_handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt = "%Y-%m-%d %H:%M:%S"))
32 | self.file_logger = logging.getLogger("linguagacha_file")
33 | self.file_logger.propagate = False
34 | self.file_logger.setLevel(logging.DEBUG)
35 | self.file_logger.addHandler(self.file_handler)
36 |
37 | # 控制台日志实例
38 | self.console_handler = RichHandler(
39 | markup = True,
40 | show_path = False,
41 | rich_tracebacks = False,
42 | tracebacks_extra_lines = 0,
43 | log_time_format = "[%X]",
44 | omit_repeated_times = False,
45 | )
46 | self.console_logger = logging.getLogger("linguagacha_console")
47 | self.console_logger.propagate = False
48 | self.console_logger.setLevel(logging.INFO)
49 | self.console_logger.addHandler(self.console_handler)
50 |
51 | @classmethod
52 | def get(cls) -> Self:
53 | if getattr(cls, "__instance__", None) is None:
54 | cls.__instance__ = cls()
55 |
56 | return cls.__instance__
57 |
58 | def is_expert_mode(self) -> bool:
59 | if getattr(self, "expert_mode", None) is None:
60 | from module.Config import Config
61 | self.expert_mode = Config().load().expert_mode
62 | self.console_logger.setLevel(logging.DEBUG if self.expert_mode == True else logging.INFO)
63 |
64 | return self.expert_mode
65 |
66 | def print(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
67 | msg_e: str = f"{msg} {e}" if msg != "" else f"{e}"
68 | if e == None:
69 | self.file_logger.info(f"{msg}") if file == True else None
70 | self.console.print(f"{msg}") if console == True else None
71 | elif self.is_expert_mode() == False:
72 | self.file_logger.info(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
73 | self.console.print(msg_e) if console == True else None
74 | else:
75 | self.file_logger.info(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
76 | self.console.print(f"{msg_e}\n{self.get_trackback(e)}\n") if console == True else None
77 |
78 | def debug(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
79 | msg_e: str = f"{msg} {e}" if msg != "" else f"{e}"
80 | if e == None:
81 | self.file_logger.debug(f"{msg}") if file == True else None
82 | self.console_logger.debug(f"{msg}") if console == True else None
83 | elif self.is_expert_mode() == False:
84 | self.file_logger.debug(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
85 | self.console_logger.debug(msg_e) if console == True else None
86 | else:
87 | self.file_logger.debug(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
88 | self.console_logger.debug(f"{msg_e}\n{self.get_trackback(e)}\n") if console == True else None
89 |
90 | def info(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
91 | msg_e: str = f"{msg} {e}" if msg != "" else f"{e}"
92 | if e == None:
93 | self.file_logger.info(f"{msg}") if file == True else None
94 | self.console_logger.info(f"{msg}") if console == True else None
95 | elif self.is_expert_mode() == False:
96 | self.file_logger.info(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
97 | self.console_logger.info(msg_e) if console == True else None
98 | else:
99 | self.file_logger.info(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
100 | self.console_logger.info(f"{msg_e}\n{self.get_trackback(e)}\n") if console == True else None
101 |
102 | def error(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
103 | msg_e: str = f"{msg} {e}" if msg != "" else f"{e}"
104 | if e == None:
105 | self.file_logger.error(f"{msg}") if file == True else None
106 | self.console_logger.error(f"{msg}") if console == True else None
107 | elif self.is_expert_mode() == False:
108 | self.file_logger.error(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
109 | self.console_logger.error(msg_e) if console == True else None
110 | else:
111 | self.file_logger.error(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
112 | self.console_logger.error(f"{msg_e}\n{self.get_trackback(e)}\n") if console == True else None
113 |
114 | def warning(self, msg: str, e: Exception = None, file: bool = True, console: bool = True) -> None:
115 | msg_e: str = f"{msg} {e}" if msg != "" else f"{e}"
116 | if e == None:
117 | self.file_logger.warning(f"{msg}") if file == True else None
118 | self.console_logger.warning(f"{msg}") if console == True else None
119 | elif self.is_expert_mode() == False:
120 | self.file_logger.warning(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
121 | self.console_logger.warning(msg_e) if console == True else None
122 | else:
123 | self.file_logger.warning(f"{msg_e}\n{self.get_trackback(e)}\n") if file == True else None
124 | self.console_logger.warning(f"{msg_e}\n{self.get_trackback(e)}\n") if console == True else None
125 |
126 | def get_trackback(self, e: Exception) -> str:
127 | return f"{("".join(traceback.format_exception(e))).strip()}"
--------------------------------------------------------------------------------
/frontend/EmptyPage.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QWidget
2 | from qfluentwidgets import FluentWindow
3 |
4 | class EmptyPage(QWidget):
5 |
6 | def __init__(self, text: str, window: FluentWindow) -> None:
7 | super().__init__(window)
8 | self.setObjectName(text.replace(" ", "-"))
--------------------------------------------------------------------------------
/frontend/Extra/LaboratoryPage.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QWidget
2 | from PyQt5.QtWidgets import QLayout
3 | from PyQt5.QtWidgets import QVBoxLayout
4 | from qfluentwidgets import FluentWindow
5 |
6 | from base.Base import Base
7 | from module.Config import Config
8 | from module.Localizer.Localizer import Localizer
9 | from widget.SwitchButtonCard import SwitchButtonCard
10 |
11 | class LaboratoryPage(QWidget, Base):
12 |
13 | def __init__(self, text: str, window: FluentWindow) -> None:
14 | super().__init__(window)
15 | self.setObjectName(text.replace(" ", "-"))
16 |
17 | # 载入并保存默认配置
18 | config = Config().load().save()
19 |
20 | # 设置主容器
21 | self.root = QVBoxLayout(self)
22 | self.root.setSpacing(8)
23 | self.root.setContentsMargins(24, 24, 24, 24) # 左、上、右、下
24 |
25 | # 添加控件
26 | self.add_widget_mtool(self.root, config, window)
27 | self.add_widget_auto_glossary(self.root, config, window)
28 |
29 | # 填充
30 | self.root.addStretch(1)
31 |
32 | # MTool 优化器
33 | def add_widget_mtool(self, parent: QLayout, config: Config, window: FluentWindow) -> None:
34 |
35 | def init(widget: SwitchButtonCard) -> None:
36 | widget.get_switch_button().setChecked(
37 | config.mtool_optimizer_enable
38 | )
39 |
40 | def checked_changed(widget: SwitchButtonCard) -> None:
41 | config = Config().load()
42 | config.mtool_optimizer_enable = widget.get_switch_button().isChecked()
43 | config.save()
44 |
45 | parent.addWidget(
46 | SwitchButtonCard(
47 | title = Localizer.get().laboratory_page_mtool_optimizer_enable,
48 | description = Localizer.get().laboratory_page_mtool_optimizer_enable_desc,
49 | init = init,
50 | checked_changed = checked_changed,
51 | )
52 | )
53 |
54 | # 自动补全术语表
55 | def add_widget_auto_glossary(self, parent: QLayout, config: Config, window: FluentWindow) -> None:
56 |
57 | def init(widget: SwitchButtonCard) -> None:
58 | widget.get_switch_button().setChecked(
59 | config.auto_glossary_enable
60 | )
61 |
62 | def checked_changed(widget: SwitchButtonCard) -> None:
63 | config = Config().load()
64 | config.auto_glossary_enable = widget.get_switch_button().isChecked()
65 | config.save()
66 |
67 | parent.addWidget(
68 | SwitchButtonCard(
69 | title = Localizer.get().laboratory_page_auto_glossary_enable,
70 | description = Localizer.get().laboratory_page_auto_glossary_enable_desc,
71 | init = init,
72 | checked_changed = checked_changed,
73 | )
74 | )
--------------------------------------------------------------------------------
/frontend/Extra/ToolBoxPage.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtCore import Qt
5 | from PyQt5.QtWidgets import QWidget
6 | from PyQt5.QtWidgets import QLayout
7 | from PyQt5.QtWidgets import QHBoxLayout
8 | from PyQt5.QtWidgets import QVBoxLayout
9 | from qfluentwidgets import FlowLayout
10 | from qfluentwidgets import CardWidget
11 | from qfluentwidgets import FluentIcon
12 | from qfluentwidgets import FluentWindow
13 | from qfluentwidgets import CaptionLabel
14 | from qfluentwidgets import SubtitleLabel
15 | from qfluentwidgets import TransparentToolButton
16 |
17 | from base.Base import Base
18 | from module.Config import Config
19 | from module.Localizer.Localizer import Localizer
20 | from widget.Separator import Separator
21 |
22 | class ItemCard(CardWidget):
23 |
24 | def __init__(self, parent: QWidget, title: str, description: str, init: Callable = None, clicked: Callable = None) -> None:
25 | super().__init__(parent)
26 |
27 | # 设置容器
28 | self.setFixedSize(300, 150)
29 | self.setBorderRadius(4)
30 | self.root = QVBoxLayout(self)
31 | self.root.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
32 |
33 | # 添加标题
34 | self.head_hbox_container = QWidget(self)
35 | self.head_hbox = QHBoxLayout(self.head_hbox_container)
36 | self.head_hbox.setSpacing(0)
37 | self.head_hbox.setContentsMargins(0, 0, 0, 0)
38 | self.root.addWidget(self.head_hbox_container)
39 |
40 | self.title_label = SubtitleLabel(title, self)
41 | self.title_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) # 在上层控件上禁用鼠标事件以将事件向下层传播
42 | self.head_hbox.addWidget(self.title_label)
43 | self.head_hbox.addStretch(1)
44 | self.title_button = TransparentToolButton(FluentIcon.PAGE_RIGHT)
45 | self.head_hbox.addWidget(self.title_button)
46 |
47 | # 添加分割线
48 | self.root.addWidget(Separator(self))
49 |
50 | # 添加描述
51 | self.description_label = CaptionLabel(description, self)
52 | self.description_label.setWordWrap(True)
53 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
54 | self.description_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) # 在上层控件上禁用鼠标事件以将事件向下层传播
55 | self.root.addWidget(self.description_label, 1)
56 |
57 | if callable(init):
58 | init(self)
59 |
60 | if callable(clicked):
61 | self.clicked.connect(lambda : clicked(self))
62 | self.title_button.clicked.connect(lambda : clicked(self))
63 |
64 |
65 | class ToolBoxPage(QWidget, Base):
66 |
67 | def __init__(self, text: str, window: FluentWindow) -> None:
68 | super().__init__(window)
69 | self.setObjectName(text.replace(" ", "-"))
70 |
71 | # 载入并保存默认配置
72 | config = Config().load().save()
73 |
74 | # 设置主容器
75 | self.vbox = QVBoxLayout(self)
76 | self.vbox.setSpacing(8)
77 | self.vbox.setContentsMargins(24, 24, 24, 24) # 左、上、右、下
78 |
79 | # 添加流式布局容器
80 | self.flow_container = QWidget(self)
81 | self.flow_layout = FlowLayout(self.flow_container, needAni = False)
82 | self.flow_layout.setSpacing(8)
83 | self.flow_layout.setContentsMargins(0, 0, 0, 0)
84 | self.vbox.addWidget(self.flow_container)
85 |
86 | # 添加控件
87 | self.add_batch_correction(self.flow_layout, config, window)
88 | self.add_re_translation(self.flow_layout, config, window)
89 | self.add_name_field_extraction(self.flow_layout, config, window)
90 |
91 | # 批量修正
92 | def add_batch_correction(self, parent: QLayout, config: Config, window: FluentWindow) -> None:
93 |
94 | def clicked(widget: ItemCard) -> None:
95 | window.switchTo(window.batch_correction_page)
96 |
97 | parent.addWidget(ItemCard(
98 | parent = self,
99 | title = Localizer.get().tool_box_page_batch_correction,
100 | description = Localizer.get().tool_box_page_batch_correction_desc,
101 | init = None,
102 | clicked = clicked,
103 | ))
104 |
105 | # 部分重翻
106 | def add_re_translation(self, parent: QLayout, config: Config, window: FluentWindow) -> None:
107 |
108 | def clicked(widget: ItemCard) -> None:
109 | window.switchTo(window.re_translation_page)
110 |
111 | parent.addWidget(ItemCard(
112 | parent = self,
113 | title = Localizer.get().tool_box_page_re_translation,
114 | description = Localizer.get().tool_box_page_re_translation_desc,
115 | init = None,
116 | clicked = clicked,
117 | ))
118 |
119 | # 姓名字段提取
120 | def add_name_field_extraction(self, parent: QLayout, config: Config, window: FluentWindow) -> None:
121 |
122 | def clicked(widget: ItemCard) -> None:
123 | window.switchTo(window.name_field_extraction_page)
124 |
125 | parent.addWidget(ItemCard(
126 | parent = self,
127 | title = Localizer.get().tool_box_page_name_field_extraction,
128 | description = Localizer.get().tool_box_page_name_field_extraction_desc,
129 | init = None,
130 | clicked = clicked,
131 | ))
--------------------------------------------------------------------------------
/frontend/Project/ModelListPage.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 |
3 | import openai
4 | import anthropic
5 | from google import genai
6 | from PyQt5.QtCore import Qt
7 | from PyQt5.QtWidgets import QWidget
8 | from PyQt5.QtWidgets import QLayout
9 | from PyQt5.QtWidgets import QVBoxLayout
10 | from qfluentwidgets import PushButton
11 | from qfluentwidgets import FluentIcon
12 | from qfluentwidgets import FluentWindow
13 | from qfluentwidgets import MessageBoxBase
14 | from qfluentwidgets import PillPushButton
15 | from qfluentwidgets import SingleDirectionScrollArea
16 |
17 | from base.Base import Base
18 | from module.Config import Config
19 | from module.Localizer.Localizer import Localizer
20 | from widget.FlowCard import FlowCard
21 | from widget.LineEditMessageBox import LineEditMessageBox
22 |
23 | class ModelListPage(MessageBoxBase, Base):
24 |
25 | def __init__(self, id: int, window: FluentWindow) -> None:
26 | super().__init__(window)
27 |
28 | # 初始化
29 | self.id: int = id
30 | self.filter: str = ""
31 | self.models: list[str] = None
32 |
33 | # 载入并保存默认配置
34 | config = Config().load().save()
35 |
36 | # 设置框体
37 | self.widget.setFixedSize(960, 720)
38 | self.yesButton.setText(Localizer.get().close)
39 | self.cancelButton.hide()
40 |
41 | # 设置主布局
42 | self.viewLayout.setContentsMargins(0, 0, 0, 0)
43 |
44 | # 设置滚动器
45 | self.scroller = SingleDirectionScrollArea(self, orient = Qt.Orientation.Vertical)
46 | self.scroller.setWidgetResizable(True)
47 | self.scroller.setStyleSheet("QScrollArea { border: none; background: transparent; }")
48 | self.viewLayout.addWidget(self.scroller)
49 |
50 | # 设置滚动控件
51 | self.vbox_parent = QWidget(self)
52 | self.vbox_parent.setStyleSheet("QWidget { background: transparent; }")
53 | self.vbox = QVBoxLayout(self.vbox_parent)
54 | self.vbox.setSpacing(8)
55 | self.vbox.setContentsMargins(24, 24, 24, 24) # 左、上、右、下
56 | self.scroller.setWidget(self.vbox_parent)
57 |
58 | # 添加控件
59 | self.add_widget(self.vbox, config, window)
60 |
61 | # 填充
62 | self.vbox.addStretch(1)
63 |
64 | # 点击事件
65 | def clicked(self, widget: PillPushButton) -> None:
66 | config = Config().load()
67 | platform = config.get_platform(self.id)
68 | platform["model"] = widget.text().strip()
69 | config.set_platform(platform)
70 | config.save()
71 |
72 | # 关闭窗口
73 | self.close()
74 |
75 | # 过滤按钮点击事件
76 | def filter_button_clicked(self, widget: PushButton, window: FluentWindow) -> None:
77 | if self.filter != "":
78 | self.filter = ""
79 | self.filter_button.setText(Localizer.get().filter)
80 |
81 | # 更新子控件
82 | self.update_sub_widgets(self.flow_card)
83 | else:
84 | message_box = LineEditMessageBox(
85 | window,
86 | Localizer.get().platform_edit_page_model,
87 | message_box_close = self.filter_message_box_close
88 | )
89 | message_box.get_line_edit().setText(self.filter)
90 | message_box.exec()
91 |
92 | # 过滤输入框关闭事件
93 | def filter_message_box_close(self, widget: LineEditMessageBox, text: str) -> None:
94 | self.filter = text.strip()
95 | self.filter_button.setText(f"{Localizer.get().filter} - {self.filter}")
96 |
97 | # 更新子控件
98 | self.update_sub_widgets(self.flow_card)
99 |
100 | # 获取模型
101 | def get_models(self, api_url: str, api_key: str, api_format: Base.APIFormat) -> list[str]:
102 | result = []
103 |
104 | try:
105 | if api_format == Base.APIFormat.GOOGLE:
106 | client = genai.Client(
107 | api_key = api_key,
108 | )
109 | return [model.name for model in client.models.list()]
110 | elif api_format == Base.APIFormat.ANTHROPIC:
111 | client = anthropic.Anthropic(
112 | api_key = api_key,
113 | base_url = api_url,
114 | )
115 | return [model.id for model in client.models.list()]
116 | else:
117 | client = openai.OpenAI(
118 | base_url = api_url,
119 | api_key = api_key,
120 | )
121 | return [model.id for model in client.models.list()]
122 | except Exception as e:
123 | self.debug(Localizer.get().model_list_page_fail, e)
124 | self.emit(Base.Event.APP_TOAST_SHOW, {
125 | "type": Base.ToastType.WARNING,
126 | "message": Localizer.get().model_list_page_fail,
127 | })
128 |
129 | return result
130 |
131 | # 更新子控件
132 | def update_sub_widgets(self, widget: FlowCard) -> None:
133 | if self.models is None:
134 | platform: dict = Config().load().get_platform(self.id)
135 | self.models = self.get_models(
136 | platform.get("api_url"),
137 | platform.get("api_key")[0],
138 | platform.get("api_format"),
139 | )
140 |
141 | widget.take_all_widgets()
142 | for model in [v for v in self.models if self.filter.lower() in v.lower()]:
143 | pilled_button = PillPushButton(model)
144 | pilled_button.setFixedWidth(432)
145 | pilled_button.clicked.connect(partial(self.clicked, pilled_button))
146 | widget.add_widget(pilled_button)
147 |
148 | # 模型名称
149 | def add_widget(self, parent: QLayout, config: Config, window: FluentWindow) -> None:
150 |
151 | self.filter_button: PushButton = None
152 |
153 | def init(widget: FlowCard) -> None:
154 | self.filter_button = PushButton(Localizer.get().filter)
155 | self.filter_button.setIcon(FluentIcon.FILTER)
156 | self.filter_button.setContentsMargins(4, 0, 4, 0)
157 | self.filter_button.clicked.connect(lambda _: self.filter_button_clicked(self.filter_button, window))
158 | widget.add_widget_to_head(self.filter_button)
159 |
160 | # 更新子控件
161 | self.update_sub_widgets(widget)
162 |
163 | self.flow_card = FlowCard(
164 | parent = self,
165 | title = Localizer.get().model_list_page_title,
166 | description = Localizer.get().model_list_page_content,
167 | init = init,
168 | )
169 | parent.addWidget(self.flow_card)
170 |
--------------------------------------------------------------------------------
/frontend/Setting/BasicSettingsPage.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import Qt
2 | from PyQt5.QtWidgets import QWidget
3 | from PyQt5.QtWidgets import QLayout
4 | from PyQt5.QtWidgets import QVBoxLayout
5 | from qfluentwidgets import FluentWindow
6 | from qfluentwidgets import SingleDirectionScrollArea
7 |
8 | from base.Base import Base
9 | from module.Config import Config
10 | from module.Localizer.Localizer import Localizer
11 | from widget.SpinCard import SpinCard
12 |
13 | class BasicSettingsPage(QWidget, Base):
14 |
15 | def __init__(self, text: str, window: FluentWindow) -> None:
16 | super().__init__(window)
17 | self.setObjectName(text.replace(" ", "-"))
18 |
19 | # 载入并保存默认配置
20 | config = Config().load().save()
21 |
22 | # 设置容器
23 | self.root = QVBoxLayout(self)
24 | self.root.setSpacing(8)
25 | self.root.setContentsMargins(24, 24, 24, 24) # 左、上、右、下
26 |
27 | # 创建滚动区域的内容容器
28 | scroll_area_vbox_widget = QWidget()
29 | scroll_area_vbox = QVBoxLayout(scroll_area_vbox_widget)
30 | scroll_area_vbox.setContentsMargins(0, 0, 0, 0)
31 |
32 | # 创建滚动区域
33 | scroll_area = SingleDirectionScrollArea(orient = Qt.Orientation.Vertical)
34 | scroll_area.setWidget(scroll_area_vbox_widget)
35 | scroll_area.setWidgetResizable(True)
36 | scroll_area.enableTransparentBackground()
37 |
38 | # 将滚动区域添加到父布局
39 | self.root.addWidget(scroll_area)
40 |
41 | # 添加控件
42 | self.add_widget_max_workers(scroll_area_vbox, config, window)
43 | self.add_widget_rpm_threshold(scroll_area_vbox, config, window)
44 | self.add_widget_token_threshold(scroll_area_vbox, config, window)
45 | self.add_widget_request_timeout(scroll_area_vbox, config, window)
46 | self.add_widget_max_round(scroll_area_vbox, config, window)
47 |
48 | # 填充
49 | scroll_area_vbox.addStretch(1)
50 |
51 | # 每秒任务数阈值
52 | def add_widget_max_workers(self, parent: QLayout, config: Config, window: FluentWindow) -> None:
53 |
54 | def init(widget: SpinCard) -> None:
55 | widget.get_spin_box().setRange(0, 9999999)
56 | widget.get_spin_box().setValue(config.max_workers)
57 |
58 | def value_changed(widget: SpinCard) -> None:
59 | config = Config().load()
60 | config.max_workers = widget.get_spin_box().value()
61 | config.save()
62 |
63 | parent.addWidget(
64 | SpinCard(
65 | title = Localizer.get().basic_settings_page_max_workers_title,
66 | description = Localizer.get().basic_settings_page_max_workers_content,
67 | init = init,
68 | value_changed = value_changed,
69 | )
70 | )
71 |
72 | # 每分钟任务数阈值
73 | def add_widget_rpm_threshold(self, parent: QLayout, config: Config, window: FluentWindow) -> None:
74 |
75 | def init(widget: SpinCard) -> None:
76 | widget.get_spin_box().setRange(0, 9999999)
77 | widget.get_spin_box().setValue(config.rpm_threshold)
78 |
79 | def value_changed(widget: SpinCard) -> None:
80 | config = Config().load()
81 | config.rpm_threshold = widget.get_spin_box().value()
82 | config.save()
83 |
84 | parent.addWidget(
85 | SpinCard(
86 | title = Localizer.get().basic_settings_page_rpm_threshold_title,
87 | description = Localizer.get().basic_settings_page_rpm_threshold_content,
88 | init = init,
89 | value_changed = value_changed,
90 | )
91 | )
92 |
93 | # 翻译任务长度阈值
94 | def add_widget_token_threshold(self, parent: QLayout, config: Config, window: FluentWindow)-> None:
95 |
96 | def init(widget: SpinCard) -> None:
97 | widget.get_spin_box().setRange(0, 9999999)
98 | widget.get_spin_box().setValue(config.token_threshold)
99 |
100 | def value_changed(widget: SpinCard) -> None:
101 | config = Config().load()
102 | config.token_threshold = widget.get_spin_box().value()
103 | config.save()
104 |
105 | parent.addWidget(
106 | SpinCard(
107 | title = Localizer.get().basic_settings_page_token_threshold_title,
108 | description = Localizer.get().basic_settings_page_token_threshold_content,
109 | init = init,
110 | value_changed = value_changed,
111 | )
112 | )
113 |
114 | # 请求超时时间
115 | def add_widget_request_timeout(self, parent: QLayout, config: Config, window: FluentWindow)-> None:
116 |
117 | def init(widget: SpinCard) -> None:
118 | widget.get_spin_box().setRange(0, 9999999)
119 | widget.get_spin_box().setValue(config.request_timeout)
120 |
121 | def value_changed(widget: SpinCard) -> None:
122 | config = Config().load()
123 | config.request_timeout = widget.get_spin_box().value()
124 | config.save()
125 |
126 | parent.addWidget(
127 | SpinCard(
128 | title = Localizer.get().basic_settings_page_request_timeout_title,
129 | description = Localizer.get().basic_settings_page_request_timeout_content,
130 | init = init,
131 | value_changed = value_changed,
132 | )
133 | )
134 |
135 | # 翻译流程最大轮次
136 | def add_widget_max_round(self, parent: QLayout, config: Config, window: FluentWindow)-> None:
137 |
138 | def init(widget: SpinCard) -> None:
139 | widget.get_spin_box().setRange(0, 9999999)
140 | widget.get_spin_box().setValue(config.max_round)
141 |
142 | def value_changed(widget: SpinCard) -> None:
143 | config = Config().load()
144 | config.max_round = widget.get_spin_box().value()
145 | config.save()
146 |
147 | parent.addWidget(
148 | SpinCard(
149 | title = Localizer.get().basic_settings_page_max_round_title,
150 | description = Localizer.get().basic_settings_page_max_round_content,
151 | init = init,
152 | value_changed = value_changed,
153 | )
154 | )
--------------------------------------------------------------------------------
/module/Cache/CacheProject.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | import threading
3 | from typing import Any
4 | from typing import Self
5 |
6 | from base.Base import Base
7 |
8 | @dataclasses.dataclass
9 | class CacheProject():
10 |
11 | id: str = "" # 项目 ID
12 | status: Base.TranslationStatus = Base.TranslationStatus.UNTRANSLATED # 翻译状态
13 | extras: dict = dataclasses.field(default_factory = dict) # 额外数据
14 |
15 | # 线程锁
16 | lock: threading.Lock = dataclasses.field(init = False, repr = False, compare = False, default_factory = threading.Lock)
17 |
18 | @classmethod
19 | def from_dict(cls, data: dict) -> Self:
20 | class_fields = {f.name for f in dataclasses.fields(cls)}
21 | filtered_data = {k: v for k, v in data.items() if k in class_fields}
22 | return cls(**filtered_data)
23 |
24 | # 获取项目 ID
25 | def get_id(self) -> str:
26 | with self.lock:
27 | return self.id
28 |
29 | # 设置项目 ID
30 | def set_id(self, id: str) -> None:
31 | with self.lock:
32 | self.id = id
33 |
34 | # 获取翻译状态
35 | def get_status(self) -> Base.TranslationStatus:
36 | with self.lock:
37 | return self.status
38 |
39 | # 设置翻译状态
40 | def set_status(self, status: Base.TranslationStatus) -> None:
41 | with self.lock:
42 | self.status = status
43 |
44 | # 获取额外数据
45 | def get_extras(self) -> dict:
46 | with self.lock:
47 | return self.extras
48 |
49 | # 设置额外数据
50 | def set_extras(self, extras: dict) -> None:
51 | with self.lock:
52 | self.extras = extras
53 |
54 | def asdict(self) -> dict[str, Any]:
55 | with self.lock:
56 | return {
57 | v.name: getattr(self, v.name)
58 | for v in dataclasses.fields(self)
59 | if v.init != False
60 | }
--------------------------------------------------------------------------------
/module/Config.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | import json
3 | import os
4 | import threading
5 | from enum import StrEnum
6 | from typing import Any
7 | from typing import ClassVar
8 | from typing import Self
9 |
10 | from base.BaseLanguage import BaseLanguage
11 | from base.LogManager import LogManager
12 | from module.Localizer.Localizer import Localizer
13 |
14 | @dataclasses.dataclass
15 | class Config():
16 |
17 | class Theme(StrEnum):
18 |
19 | DARK = "DARK"
20 | LIGHT = "LIGHT"
21 |
22 | # Application
23 | theme: str = Theme.LIGHT
24 | app_language: BaseLanguage.Enum = BaseLanguage.Enum.ZH
25 |
26 | # PlatformPage
27 | activate_platform: int = 0
28 | platforms: list[dict[str, Any]] = None
29 |
30 | # AppSettingsPage
31 | expert_mode: bool = False
32 | proxy_url: str = ""
33 | proxy_enable: bool = False
34 | font_hinting: bool = True
35 | scale_factor: str = ""
36 |
37 | # BasicSettingsPage
38 | token_threshold: int = 384
39 | max_workers: int = 0
40 | rpm_threshold: int = 0
41 | request_timeout: int = 120
42 | max_round: int = 16
43 |
44 | # ExpertSettingsPage
45 | preceding_lines_threshold: int = 0
46 | enable_preceding_on_local: bool = False
47 | clean_ruby: bool = True
48 | deduplication_in_trans: bool = True
49 | deduplication_in_bilingual: bool = True
50 | write_translated_name_fields_to_file: bool = True
51 | result_checker_retry_count_threshold: bool = False
52 |
53 | # ProjectPage
54 | source_language: BaseLanguage.Enum = BaseLanguage.Enum.JA
55 | target_language: BaseLanguage.Enum = BaseLanguage.Enum.ZH
56 | input_folder: str = "./input"
57 | output_folder: str = "./output"
58 | output_folder_open_on_finish: bool = False
59 | traditional_chinese_enable: bool = False
60 |
61 | # GlossaryPage
62 | glossary_enable: bool = True
63 | glossary_data: list[Any] = dataclasses.field(default_factory = list)
64 |
65 | # TextPreservePage
66 | text_preserve_enable: bool = False
67 | text_preserve_data: list[Any] = dataclasses.field(default_factory = list)
68 |
69 | # PreTranslationReplacementPage
70 | pre_translation_replacement_enable: bool = True
71 | pre_translation_replacement_data: list[Any] = dataclasses.field(default_factory = list)
72 |
73 | # PostTranslationReplacementPage
74 | post_translation_replacement_enable: bool = True
75 | post_translation_replacement_data: list[Any] = dataclasses.field(default_factory = list)
76 |
77 | # CustomPromptZHPage
78 | custom_prompt_zh_enable: bool = False
79 | custom_prompt_zh_data: str = None
80 |
81 | # CustomPromptENPage
82 | custom_prompt_en_enable: bool = False
83 | custom_prompt_en_data: str = None
84 |
85 | # LaboratoryPage
86 | auto_glossary_enable: bool = False
87 | mtool_optimizer_enable: bool = False
88 |
89 | # 类属性
90 | CONFIG_PATH: ClassVar[str] = "./resource/config.json"
91 | CONFIG_LOCK: ClassVar[threading.Lock] = threading.Lock()
92 |
93 | def load(self, path: str = None) -> Self:
94 | if path is None:
95 | path = __class__.CONFIG_PATH
96 |
97 | with __class__.CONFIG_LOCK:
98 | try:
99 | os.makedirs(os.path.dirname(path), exist_ok = True)
100 | if os.path.isfile(path):
101 | with open(path, "r", encoding = "utf-8-sig") as reader:
102 | config: dict = json.load(reader)
103 | for k, v in config.items():
104 | if hasattr(self, k):
105 | setattr(self, k, v)
106 | except Exception as e:
107 | LogManager.get().error(f"{Localizer.get().log_read_file_fail}", e)
108 |
109 | return self
110 |
111 | def save(self, path: str = None) -> Self:
112 | if path is None:
113 | path = __class__.CONFIG_PATH
114 |
115 | with __class__.CONFIG_LOCK:
116 | try:
117 | os.makedirs(os.path.dirname(path), exist_ok = True)
118 | with open(path, "w", encoding = "utf-8") as writer:
119 | json.dump(dataclasses.asdict(self), writer, indent = 4, ensure_ascii = False)
120 | except Exception as e:
121 | LogManager.get().error(f"{Localizer.get().log_write_file_fail}", e)
122 |
123 | return self
124 |
125 | # 重置专家模式
126 | def reset_expert_settings(self) -> None:
127 | # ExpertSettingsPage
128 | self.preceding_lines_threshold: int = 0
129 | self.enable_preceding_on_local: bool = False
130 | self.clean_ruby: bool = True
131 | self.deduplication_in_trans: bool = True
132 | self.deduplication_in_bilingual: bool = True
133 | self.write_translated_name_fields_to_file: bool = True
134 | self.result_checker_retry_count_threshold: bool = False
135 |
136 | # TextPreservePage
137 | self.text_preserve_enable: bool = False
138 | self.text_preserve_data: list[Any] = []
139 |
140 | # 获取平台配置
141 | def get_platform(self, id: int) -> dict[str, Any]:
142 | item: dict[str, str | bool | int | float | list[str]] = None
143 | for item in self.platforms:
144 | if item.get("id", 0) == id:
145 | return item
146 |
147 | # 更新平台配置
148 | def set_platform(self, platform: dict[str, Any]) -> None:
149 | for i, item in enumerate(self.platforms):
150 | if item.get("id", 0) == platform.get("id", 0):
151 | self.platforms[i] = platform
152 | break
--------------------------------------------------------------------------------
/module/Engine/API/APITester.py:
--------------------------------------------------------------------------------
1 | import threading
2 |
3 | from base.Base import Base
4 | from module.Config import Config
5 | from module.Engine.Engine import Engine
6 | from module.Localizer.Localizer import Localizer
7 | from module.Engine.TaskRequester import TaskRequester
8 |
9 | class APITester(Base):
10 |
11 | def __init__(self) -> None:
12 | super().__init__()
13 |
14 | # 注册事件
15 | self.subscribe(Base.Event.PLATFORM_TEST_START, self.platform_test_start)
16 |
17 | # 接口测试开始事件
18 | def platform_test_start(self, event: str, data: dict) -> None:
19 | if Engine.get().get_status() != Engine.Status.IDLE:
20 | self.emit(Base.Event.APP_TOAST_SHOW, {
21 | "type": Base.ToastType.WARNING,
22 | "message": Localizer.get().platofrm_tester_running,
23 | })
24 | else:
25 | threading.Thread(
26 | target = self.platform_test_start_target,
27 | args = (event, data),
28 | ).start()
29 |
30 | # 接口测试开始
31 | def platform_test_start_target(self, event: str, data: dict) -> None:
32 | # 更新运行状态
33 | Engine.get().set_status(Engine.Status.TESTING)
34 |
35 | # 加载配置
36 | config = Config().load()
37 | platform = config.get_platform(data.get("id"))
38 |
39 | # 测试结果
40 | failure = []
41 | success = []
42 |
43 | # 构造提示词
44 | if platform.get("api_format") == Base.APIFormat.SAKURALLM:
45 | messages = [
46 | {
47 | "role": "system",
48 | "content": "你是一个轻小说翻译模型,可以流畅通顺地以日本轻小说的风格将日文翻译成简体中文,并联系上下文正确使用人称代词,不擅自添加原文中没有的代词。",
49 | },
50 | {
51 | "role": "user",
52 | "content": "将下面的日文文本翻译成中文:魔導具師ダリヤはうつむかない",
53 | },
54 | ]
55 | else:
56 | messages = [
57 | {
58 | "role": "user",
59 | "content": "将下面的日文文本翻译成中文,按输入格式返回结果:{\"0\":\"魔導具師ダリヤはうつむかない\"}",
60 | },
61 | ]
62 |
63 | # 重置请求器
64 | TaskRequester.reset()
65 |
66 | # 开始测试
67 | requester = TaskRequester(config, platform, 0)
68 | for key in platform.get("api_key"):
69 | self.print("")
70 | self.info(f"{Localizer.get().platofrm_tester_key} - {key}")
71 | self.info(f"{Localizer.get().platofrm_tester_messages}\n{messages}")
72 | skip, response_think, response_result, _, _ = requester.request(messages)
73 |
74 | # 提取回复内容
75 | if skip == True:
76 | failure.append(key)
77 | self.warning(Localizer.get().log_api_test_fail)
78 | elif response_think == "":
79 | success.append(key)
80 | self.info(f"{Localizer.get().platofrm_tester_response_result}\n{response_result}")
81 | else:
82 | success.append(key)
83 | self.info(f"{Localizer.get().platofrm_tester_response_think}\n{response_think}")
84 | self.info(f"{Localizer.get().platofrm_tester_response_result}\n{response_result}")
85 |
86 | # 测试结果
87 | result_msg = (
88 | Localizer.get().platofrm_tester_result.replace("{COUNT}", f"{len(platform.get("api_key"))}")
89 | .replace("{SUCCESS}", f"{len(success)}")
90 | .replace("{FAILURE}", f"{len(failure)}")
91 | )
92 | self.print("")
93 | self.info(result_msg)
94 |
95 | # 失败密钥
96 | if len(failure) > 0:
97 | self.warning(Localizer.get().platofrm_tester_result_failure + "\n" + "\n".join(failure))
98 |
99 | # 发送完成事件
100 | self.emit(Base.Event.PLATFORM_TEST_DONE, {
101 | "result": len(failure) == 0,
102 | "result_msg": result_msg,
103 | })
104 |
105 | # 更新运行状态
106 | Engine.get().set_status(Engine.Status.IDLE)
--------------------------------------------------------------------------------
/module/Engine/Engine.py:
--------------------------------------------------------------------------------
1 | import threading
2 | from enum import StrEnum
3 | from typing import Self
4 |
5 | class Engine():
6 |
7 | class Status(StrEnum):
8 |
9 | IDLE = "IDLE" # 无任务
10 | TESTING = "TESTING" # 测试中
11 | TRANSLATING = "TRANSLATING" # 运行中
12 | STOPPING = "STOPPING" # 停止中
13 |
14 | TASK_PREFIX: str = "ENGINE_"
15 |
16 | def __init__(self) -> None:
17 | super().__init__()
18 |
19 | # 初始化
20 | self.status: __class__.Status = __class__.Status.IDLE
21 |
22 | # 线程锁
23 | self.lock = threading.Lock()
24 |
25 | @classmethod
26 | def get(cls) -> Self:
27 | if not hasattr(cls, "__instance__"):
28 | cls.__instance__ = cls()
29 |
30 | return cls.__instance__
31 |
32 | def run(self) -> None:
33 | from module.Engine.API.APITester import APITester
34 | self.api_test = APITester()
35 |
36 | from module.Engine.Translator.Translator import Translator
37 | self.translator = Translator()
38 |
39 | def get_status(self) -> Status:
40 | with self.lock:
41 | return self.status
42 |
43 | def set_status(self, status: Status) -> None:
44 | with self.lock:
45 | self.status = status
46 |
47 | def get_running_task_count(self) -> int:
48 | return sum(1 for t in threading.enumerate() if t.name.startswith(__class__.TASK_PREFIX))
--------------------------------------------------------------------------------
/module/Engine/TaskLimiter.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | class TaskLimiter:
4 |
5 | def __init__(self, rps: int, rpm: int) -> None:
6 | self.rps = rps
7 | self.rpm = rpm
8 | self.max_tokens = self._calculate_max_tokens()
9 | self.rate_per_second = self._calculate_stricter_rate()
10 | self.available_tokens = self.max_tokens
11 | self.last_request_time = time.time()
12 |
13 | # 计算最大令牌数
14 | def _calculate_max_tokens(self) -> float:
15 | return min(
16 | self.rps if self.rps > 0 else 0,
17 | self.rpm / 60 if self.rpm > 0 else 0,
18 | )
19 |
20 | # 计算每秒恢复的请求额度
21 | def _calculate_stricter_rate(self) -> float:
22 | return min(
23 | self.rps if self.rps > 0 else float("inf"),
24 | self.rpm / 60 if self.rpm > 0 else float("inf"),
25 | )
26 |
27 | # 等待直到有足够的请求额度
28 | def wait(self) -> None:
29 | current_time = time.time()
30 | elapsed_time = current_time - self.last_request_time
31 |
32 | # 恢复额度
33 | self.available_tokens = self.available_tokens + elapsed_time * self.rate_per_second
34 | self.available_tokens = min(self.available_tokens, self.max_tokens)
35 |
36 | # 如果额度不足,等待
37 | if self.available_tokens < 1:
38 | time.sleep((1 - self.available_tokens) / self.rate_per_second)
39 | self.available_tokens = 1
40 |
41 | # 扣减令牌
42 | self.available_tokens = self.available_tokens - 1
43 |
44 | # 更新最后请求时间
45 | self.last_request_time = time.time()
--------------------------------------------------------------------------------
/module/File/ASS.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from base.Base import Base
4 | from base.BaseLanguage import BaseLanguage
5 | from module.Text.TextHelper import TextHelper
6 | from module.Cache.CacheItem import CacheItem
7 | from module.Config import Config
8 | from module.Localizer.Localizer import Localizer
9 |
10 | class ASS(Base):
11 |
12 | # [Script Info]
13 | # ; This is an Advanced Sub Station Alpha v4+ script.
14 | # Title:
15 | # ScriptType: v4.00+
16 | # PlayDepth: 0
17 | # ScaledBorderAndShadow: Yes
18 |
19 | # [V4+ Styles]
20 | # Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
21 | # Style: Default,Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,1,2,10,10,10,1
22 |
23 | # [Events]
24 | # Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
25 | # Dialogue: 0,0:00:08.12,0:00:10.46,Default,,0,0,0,,にゃにゃにゃ
26 | # Dialogue: 0,0:00:14.00,0:00:15.88,Default,,0,0,0,,えーこの部屋一人で使\Nえるとか最高じゃん
27 | # Dialogue: 0,0:00:15.88,0:00:17.30,Default,,0,0,0,,えるとか最高じゃん
28 |
29 | def __init__(self, config: Config) -> None:
30 | super().__init__()
31 |
32 | # 初始化
33 | self.config = config
34 | self.input_path: str = config.input_folder
35 | self.output_path: str = config.output_folder
36 | self.source_language: BaseLanguage.Enum = config.source_language
37 | self.target_language: BaseLanguage.Enum = config.target_language
38 |
39 | # 在扩展名前插入文本
40 | def insert_target(self, path: str) -> str:
41 | root, ext = os.path.splitext(path)
42 | return f"{root}.{self.target_language.lower()}{ext}"
43 |
44 | # 在扩展名前插入文本
45 | def insert_source_target(self, path: str) -> str:
46 | root, ext = os.path.splitext(path)
47 | return f"{root}.{self.source_language.lower()}.{self.target_language.lower()}{ext}"
48 |
49 | # 读取
50 | def read_from_path(self, abs_paths: list[str]) -> list[CacheItem]:
51 | items:list[CacheItem] = []
52 | for abs_path in abs_paths:
53 | # 获取相对路径
54 | rel_path = os.path.relpath(abs_path, self.input_path)
55 |
56 | # 获取文件编码
57 | encoding = TextHelper.get_enconding(path = abs_path, add_sig_to_utf8 = True)
58 |
59 | # 数据处理
60 | with open(abs_path, "r", encoding = encoding) as reader:
61 | lines = [line.strip() for line in reader.readlines()]
62 |
63 | # 格式字段的数量
64 | in_event = False
65 | format_field_num = -1
66 | for line in lines:
67 | # 判断是否进入事件块
68 | if line == "[Events]":
69 | in_event = True
70 | # 在事件块中寻找格式字段
71 | if in_event == True and line.startswith("Format:"):
72 | format_field_num = len(line.split(",")) - 1
73 | break
74 |
75 | for line in lines:
76 | content = ",".join(line.split(",")[format_field_num:]) if line.startswith("Dialogue:") else ""
77 | extra_field = line.replace(f"{content}", "{{CONTENT}}") if content != "" else line
78 |
79 | # 添加数据
80 | items.append(
81 | CacheItem.from_dict({
82 | "src": content.replace("\\N", "\n"),
83 | "dst": content.replace("\\N", "\n"),
84 | "extra_field": extra_field,
85 | "row": len(items),
86 | "file_type": CacheItem.FileType.ASS,
87 | "file_path": rel_path,
88 | })
89 | )
90 |
91 | return items
92 |
93 | # 写入
94 | def write_to_path(self, items: list[CacheItem]) -> None:
95 | # 筛选
96 | target = [
97 | item for item in items
98 | if item.get_file_type() == CacheItem.FileType.ASS
99 | ]
100 |
101 | # 按文件路径分组
102 | group: dict[str, list[str]] = {}
103 | for item in target:
104 | group.setdefault(item.get_file_path(), []).append(item)
105 |
106 | # 分别处理每个文件
107 | for rel_path, items in group.items():
108 | abs_path = os.path.join(self.output_path, rel_path)
109 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
110 |
111 | result: list[str] = []
112 | for item in items:
113 | result.append(item.get_extra_field().replace("{{CONTENT}}", item.get_dst().replace("\n", "\\N")))
114 |
115 | with open(self.insert_target(abs_path), "w", encoding = "utf-8") as writer:
116 | writer.write("\n".join(result))
117 |
118 | # 分别处理每个文件(双语)
119 | for rel_path, items in group.items():
120 | result: list[str] = []
121 | for item in items:
122 | if self.config.deduplication_in_bilingual == True and item.get_src() == item.get_dst():
123 | line = item.get_extra_field().replace("{{CONTENT}}", "{{CONTENT}}\\N{{CONTENT}}")
124 | line = line.replace("{{CONTENT}}", item.get_dst().replace("\n", "\\N"), 1)
125 | result.append(line)
126 | else:
127 | line = item.get_extra_field().replace("{{CONTENT}}", "{{CONTENT}}\\N{{CONTENT}}")
128 | line = line.replace("{{CONTENT}}", item.get_src().replace("\n", "\\N"), 1)
129 | line = line.replace("{{CONTENT}}", item.get_dst().replace("\n", "\\N"), 1)
130 | result.append(line)
131 |
132 | abs_path = f"{self.output_path}/{Localizer.get().path_bilingual}/{rel_path}"
133 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
134 | with open(self.insert_source_target(abs_path), "w", encoding = "utf-8") as writer:
135 | writer.write("\n".join(result))
--------------------------------------------------------------------------------
/module/File/FileManager.py:
--------------------------------------------------------------------------------
1 | import os
2 | import random
3 | from datetime import datetime
4 |
5 | from base.Base import Base
6 | from module.Cache.CacheItem import CacheItem
7 | from module.Cache.CacheProject import CacheProject
8 | from module.Config import Config
9 | from module.File.ASS import ASS
10 | from module.File.EPUB import EPUB
11 | from module.File.KVJSON import KVJSON
12 | from module.File.MD import MD
13 | from module.File.MESSAGEJSON import MESSAGEJSON
14 | from module.File.RENPY import RENPY
15 | from module.File.SRT import SRT
16 | from module.File.TRANS.TRANS import TRANS
17 | from module.File.TXT import TXT
18 | from module.File.WOLFXLSX import WOLFXLSX
19 | from module.File.XLSX import XLSX
20 | from module.Localizer.Localizer import Localizer
21 |
22 | class FileManager(Base):
23 |
24 | def __init__(self, config: Config) -> None:
25 | super().__init__()
26 |
27 | # 初始化
28 | self.config = config
29 |
30 | # 读
31 | def read_from_path(self) -> tuple[CacheProject, list[CacheItem]]:
32 | project: CacheProject = CacheProject.from_dict({
33 | "id": f"{datetime.now().strftime("%Y%m%d_%H%M%S")}_{random.randint(100000, 999999)}",
34 | })
35 |
36 | items: list[CacheItem] = []
37 | try:
38 | paths: list[str] = []
39 | input_folder: str = self.config.input_folder
40 | if os.path.isfile(input_folder):
41 | paths = [input_folder]
42 | elif os.path.isdir(input_folder):
43 | for root, _, files in os.walk(input_folder):
44 | paths.extend([f"{root}/{file}".replace("\\", "/") for file in files])
45 |
46 | items.extend(MD(self.config).read_from_path([path for path in paths if path.lower().endswith(".md")]))
47 | items.extend(TXT(self.config).read_from_path([path for path in paths if path.lower().endswith(".txt")]))
48 | items.extend(ASS(self.config).read_from_path([path for path in paths if path.lower().endswith(".ass")]))
49 | items.extend(SRT(self.config).read_from_path([path for path in paths if path.lower().endswith(".srt")]))
50 | items.extend(EPUB(self.config).read_from_path([path for path in paths if path.lower().endswith(".epub")]))
51 | items.extend(XLSX(self.config).read_from_path([path for path in paths if path.lower().endswith(".xlsx")]))
52 | items.extend(WOLFXLSX(self.config).read_from_path([path for path in paths if path.lower().endswith(".xlsx")]))
53 | items.extend(RENPY(self.config).read_from_path([path for path in paths if path.lower().endswith(".rpy")]))
54 | items.extend(TRANS(self.config).read_from_path([path for path in paths if path.lower().endswith(".trans")]))
55 | items.extend(KVJSON(self.config).read_from_path([path for path in paths if path.lower().endswith(".json")]))
56 | items.extend(MESSAGEJSON(self.config).read_from_path([path for path in paths if path.lower().endswith(".json")]))
57 | except Exception as e:
58 | self.error(f"{Localizer.get().log_read_file_fail}", e)
59 |
60 | return project, items
61 |
62 | # 写
63 | def write_to_path(self, items: list[CacheItem]) -> None:
64 | try:
65 | MD(self.config).write_to_path(items)
66 | TXT(self.config).write_to_path(items)
67 | ASS(self.config).write_to_path(items)
68 | SRT(self.config).write_to_path(items)
69 | EPUB(self.config).write_to_path(items)
70 | XLSX(self.config).write_to_path(items)
71 | WOLFXLSX(self.config).write_to_path(items)
72 | RENPY(self.config).write_to_path(items)
73 | TRANS(self.config).write_to_path(items)
74 | KVJSON(self.config).write_to_path(items)
75 | MESSAGEJSON(self.config).write_to_path(items)
76 | except Exception as e:
77 | self.error(f"{Localizer.get().log_write_file_fail}", e)
--------------------------------------------------------------------------------
/module/File/KVJSON.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 |
4 | from base.Base import Base
5 | from base.BaseLanguage import BaseLanguage
6 | from module.Text.TextHelper import TextHelper
7 | from module.Cache.CacheItem import CacheItem
8 | from module.Config import Config
9 |
10 | class KVJSON(Base):
11 |
12 | # {
13 | # "「あ・・」": "「あ・・」",
14 | # "「ごめん、ここ使う?」": "「ごめん、ここ使う?」",
15 | # "「じゃあ・・私は帰るね」": "「じゃあ・・私は帰るね」",
16 | # }
17 |
18 | def __init__(self, config: Config) -> None:
19 | super().__init__()
20 |
21 | # 初始化
22 | self.config = config
23 | self.input_path: str = config.input_folder
24 | self.output_path: str = config.output_folder
25 | self.source_language: BaseLanguage.Enum = config.source_language
26 | self.target_language: BaseLanguage.Enum = config.target_language
27 |
28 | # 读取
29 | def read_from_path(self, abs_paths: list[str]) -> list[CacheItem]:
30 | items:list[CacheItem] = []
31 | for abs_path in abs_paths:
32 | # 获取相对路径
33 | rel_path = os.path.relpath(abs_path, self.input_path)
34 |
35 | # 获取文件编码
36 | encoding = TextHelper.get_enconding(path = abs_path, add_sig_to_utf8 = True)
37 |
38 | # 数据处理
39 | with open(abs_path, "r", encoding = encoding) as reader:
40 | json_data: dict[str, str] = json.load(reader)
41 |
42 | # 格式校验
43 | if not isinstance(json_data, dict):
44 | continue
45 |
46 | # 读取数据
47 | for k, v in json_data.items():
48 | if isinstance(k, str) and isinstance(v, str):
49 | src = k
50 | dst = v
51 | if src == "":
52 | items.append(
53 | CacheItem.from_dict({
54 | "src": src,
55 | "dst": dst,
56 | "row": len(items),
57 | "file_type": CacheItem.FileType.KVJSON,
58 | "file_path": rel_path,
59 | "status": Base.TranslationStatus.EXCLUDED,
60 | })
61 | )
62 | elif dst != "" and src != dst:
63 | items.append(
64 | CacheItem.from_dict({
65 | "src": src,
66 | "dst": dst,
67 | "row": len(items),
68 | "file_type": CacheItem.FileType.KVJSON,
69 | "file_path": rel_path,
70 | "status": Base.TranslationStatus.TRANSLATED_IN_PAST,
71 | })
72 | )
73 | else:
74 | items.append(
75 | CacheItem.from_dict({
76 | "src": src,
77 | "dst": dst,
78 | "row": len(items),
79 | "file_type": CacheItem.FileType.KVJSON,
80 | "file_path": rel_path,
81 | "status": Base.TranslationStatus.UNTRANSLATED,
82 | })
83 | )
84 |
85 | return items
86 |
87 | # 写入
88 | def write_to_path(self, items: list[CacheItem]) -> None:
89 | target = [
90 | item for item in items
91 | if item.get_file_type() == CacheItem.FileType.KVJSON
92 | ]
93 |
94 | group: dict[str, list[str]] = {}
95 | for item in target:
96 | group.setdefault(item.get_file_path(), []).append(item)
97 |
98 | for rel_path, items in group.items():
99 | abs_path = os.path.join(self.output_path, rel_path)
100 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
101 | with open(abs_path, "w", encoding = "utf-8") as writer:
102 | writer.write(
103 | json.dumps(
104 | {
105 | item.get_src(): item.get_dst() for item in items
106 | },
107 | indent = 4,
108 | ensure_ascii = False,
109 | )
110 | )
--------------------------------------------------------------------------------
/module/File/MD.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 |
4 | from base.Base import Base
5 | from base.BaseLanguage import BaseLanguage
6 | from module.Text.TextHelper import TextHelper
7 | from module.Cache.CacheItem import CacheItem
8 | from module.Config import Config
9 |
10 | class MD(Base):
11 |
12 | # 添加图片匹配的正则表达式
13 | IMAGE_PATTERN = re.compile(r'!\[.*?\]\(.*?\)')
14 |
15 | def __init__(self, config: Config) -> None:
16 | super().__init__()
17 |
18 | # 初始化
19 | self.config = config
20 | self.input_path: str = config.input_folder
21 | self.output_path: str = config.output_folder
22 | self.source_language: BaseLanguage.Enum = config.source_language
23 | self.target_language: BaseLanguage.Enum = config.target_language
24 |
25 | # 在扩展名前插入文本
26 | def insert_target(self, path: str) -> str:
27 | root, ext = os.path.splitext(path)
28 | return f"{root}.{self.target_language.lower()}{ext}"
29 |
30 | # 在扩展名前插入文本
31 | def insert_source_target(self, path: str) -> str:
32 | root, ext = os.path.splitext(path)
33 | return f"{root}.{self.source_language.lower()}.{self.target_language.lower()}{ext}"
34 |
35 | # 读取
36 | def read_from_path(self, abs_paths: list[str]) -> list[CacheItem]:
37 | items:list[CacheItem] = []
38 |
39 | for abs_path in abs_paths:
40 | # 获取相对路径
41 | rel_path = os.path.relpath(abs_path, self.input_path)
42 |
43 | # 获取文件编码
44 | encoding = TextHelper.get_enconding(path = abs_path, add_sig_to_utf8 = True)
45 |
46 | # 数据处理
47 | with open(abs_path, "r", encoding = encoding) as reader:
48 | lines = [line.removesuffix("\n") for line in reader.readlines()]
49 | in_code_block = False # 跟踪是否在代码块内
50 |
51 | for line in lines:
52 | # 检查是否进入或退出代码块
53 | if line.strip().startswith("```"):
54 | in_code_block = not in_code_block
55 |
56 | # 如果是图片行或在代码块内,设置状态为 EXCLUDED
57 | if (MD.IMAGE_PATTERN.search(line) or in_code_block):
58 | items.append(
59 | CacheItem.from_dict({
60 | "src": line,
61 | "dst": line,
62 | "row": len(items),
63 | "file_type": CacheItem.FileType.MD,
64 | "file_path": rel_path,
65 | "text_type": CacheItem.TextType.MD,
66 | "status": Base.TranslationStatus.EXCLUDED,
67 | })
68 | )
69 | else:
70 | items.append(
71 | CacheItem.from_dict({
72 | "src": line,
73 | "dst": line,
74 | "row": len(items),
75 | "file_type": CacheItem.FileType.MD,
76 | "file_path": rel_path,
77 | "text_type": CacheItem.TextType.MD,
78 | })
79 | )
80 |
81 | return items
82 |
83 | # 写入
84 | def write_to_path(self, items: list[CacheItem]) -> None:
85 | # 筛选
86 | target = [
87 | item for item in items
88 | if item.get_file_type() == CacheItem.FileType.MD
89 | ]
90 |
91 | # 按文件路径分组
92 | group: dict[str, list[str]] = {}
93 | for item in target:
94 | group.setdefault(item.get_file_path(), []).append(item)
95 |
96 | # 分别处理每个文件
97 | for rel_path, items in group.items():
98 | abs_path = os.path.join(self.output_path, rel_path)
99 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
100 | with open(self.insert_target(abs_path), "w", encoding = "utf-8") as writer:
101 | writer.write("\n".join([item.get_dst() for item in items]))
102 |
--------------------------------------------------------------------------------
/module/File/SRT.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 |
4 | from base.Base import Base
5 | from base.BaseLanguage import BaseLanguage
6 | from module.Text.TextHelper import TextHelper
7 | from module.Cache.CacheItem import CacheItem
8 | from module.Config import Config
9 | from module.Localizer.Localizer import Localizer
10 |
11 | class SRT(Base):
12 |
13 | # 1
14 | # 00:00:08,120 --> 00:00:10,460
15 | # にゃにゃにゃ
16 |
17 | # 2
18 | # 00:00:14,000 --> 00:00:15,880
19 | # えーこの部屋一人で使
20 |
21 | # 3
22 | # 00:00:15,880 --> 00:00:17,300
23 | # えるとか最高じゃん
24 |
25 | def __init__(self, config: Config) -> None:
26 | super().__init__()
27 |
28 | # 初始化
29 | self.config = config
30 | self.input_path: str = config.input_folder
31 | self.output_path: str = config.output_folder
32 | self.source_language: BaseLanguage.Enum = config.source_language
33 | self.target_language: BaseLanguage.Enum = config.target_language
34 |
35 | # 在扩展名前插入文本
36 | def insert_target(self, path: str) -> str:
37 | root, ext = os.path.splitext(path)
38 | return f"{root}.{self.target_language.lower()}{ext}"
39 |
40 | # 在扩展名前插入文本
41 | def insert_source_target(self, path: str) -> str:
42 | root, ext = os.path.splitext(path)
43 | return f"{root}.{self.source_language.lower()}.{self.target_language.lower()}{ext}"
44 |
45 | # 读取
46 | def read_from_path(self, abs_paths: list[str]) -> list[CacheItem]:
47 | items:list[CacheItem] = []
48 | for abs_path in abs_paths:
49 | # 获取相对路径
50 | rel_path = os.path.relpath(abs_path, self.input_path)
51 |
52 | # 获取文件编码
53 | encoding = TextHelper.get_enconding(path = abs_path, add_sig_to_utf8 = True)
54 |
55 | # 数据处理
56 | with open(abs_path, "r", encoding = encoding) as reader:
57 | chunks = re.split(r"\n{2,}", reader.read().strip())
58 | for chunk in chunks:
59 | lines = [line.strip() for line in chunk.splitlines()]
60 |
61 | # isdecimal
62 | # 字符串中的字符是否全是十进制数字。也就是说,只有那些在数字系统中被认为是“基本”的数字字符(0-9)才会返回 True。
63 | # isdigit
64 | # 字符串中的字符是否都是数字字符。它不仅检查十进制数字,还包括其他可以表示数字的字符,如数字上标、罗马数字、圆圈数字等。
65 | # isnumeric
66 | # 字符串中的字符是否表示任何类型的数字,包括整数、分数、数字字符的变种(比如上标、下标)以及其他可以被认为是数字的字符(如中文数字)。
67 |
68 | # 格式校验
69 | if len(lines) < 3 or not lines[0].isdecimal():
70 | continue
71 |
72 | # 添加数据
73 | if lines[-1] != "":
74 | items.append(
75 | CacheItem.from_dict({
76 | "src": "\n".join(lines[2:]), # 如有多行文本则用换行符拼接
77 | "dst": "\n".join(lines[2:]), # 如有多行文本则用换行符拼接
78 | "extra_field": lines[1],
79 | "row": str(lines[0]),
80 | "file_type": CacheItem.FileType.SRT,
81 | "file_path": rel_path,
82 | })
83 | )
84 |
85 | return items
86 |
87 | # 写入
88 | def write_to_path(self, items: list[CacheItem]) -> None:
89 | # 筛选
90 | target = [
91 | item for item in items
92 | if item.get_file_type() == CacheItem.FileType.SRT
93 | ]
94 |
95 | # 按文件路径分组
96 | group: dict[str, list[str]] = {}
97 | for item in target:
98 | group.setdefault(item.get_file_path(), []).append(item)
99 |
100 | # 分别处理每个文件
101 | for rel_path, items in group.items():
102 | abs_path = os.path.join(self.output_path, rel_path)
103 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
104 |
105 | result: list[str] = []
106 | for item in items:
107 | result.append([
108 | item.get_row(),
109 | item.get_extra_field(),
110 | item.get_dst(),
111 | ])
112 |
113 | with open(self.insert_target(abs_path), "w", encoding = "utf-8") as writer:
114 | for item in result:
115 | writer.write("\n".join(item))
116 | writer.write("\n\n")
117 |
118 | # 分别处理每个文件(双语)
119 | for rel_path, items in group.items():
120 | result: list[str] = []
121 | for item in items:
122 | if self.config.deduplication_in_bilingual == True and item.get_src() == item.get_dst():
123 | result.append([
124 | item.get_row(),
125 | item.get_extra_field(),
126 | item.get_dst(),
127 | ])
128 | else:
129 | result.append([
130 | item.get_row(),
131 | item.get_extra_field(),
132 | f"{item.get_src()}\n{item.get_dst()}",
133 | ])
134 |
135 | abs_path = f"{self.output_path}/{Localizer.get().path_bilingual}/{rel_path}"
136 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
137 | with open(self.insert_source_target(abs_path), "w", encoding = "utf-8") as writer:
138 | for item in result:
139 | writer.write("\n".join(item))
140 | writer.write("\n\n")
--------------------------------------------------------------------------------
/module/File/TRANS/KAG.py:
--------------------------------------------------------------------------------
1 | from module.File.TRANS.NONE import NONE
2 | from module.Cache.CacheItem import CacheItem
3 |
4 | class KAG(NONE):
5 |
6 | TEXT_TYPE: str = CacheItem.TextType.KAG
--------------------------------------------------------------------------------
/module/File/TRANS/NONE.py:
--------------------------------------------------------------------------------
1 | from base.Base import Base
2 | from module.Cache.CacheItem import CacheItem
3 |
4 | class NONE():
5 |
6 | TEXT_TYPE: str = CacheItem.TextType.NONE
7 |
8 | BLACKLIST_EXT: tuple[str] = (
9 | ".mp3", ".wav", ".ogg", "mid",
10 | ".png", ".jpg", ".jpeg", ".gif", ".psd", ".webp", ".heif", ".heic",
11 | ".avi", ".mp4", ".webm",
12 | ".txt", ".7z", ".gz", ".rar", ".zip", ".json",
13 | ".sav", ".mps", ".ttf", ".otf", ".woff",
14 | )
15 |
16 | def __init__(self, project: dict) -> None:
17 | super().__init__()
18 |
19 | # 初始化
20 | self.project: dict = project
21 |
22 | # 预处理
23 | def pre_process(self) -> None:
24 | pass
25 |
26 | # 后处理
27 | def post_process(self) -> None:
28 | pass
29 |
30 | # 检查
31 | def check(self, path: str, data: list[str], tag: list[str], context: list[str]) -> tuple[str, str, list[str], str, bool]:
32 | src: str = data[0] if len(data) > 0 and isinstance(data[0], str) else ""
33 | dst: str = data[1] if len(data) > 1 and isinstance(data[1], str) else src
34 |
35 | # 如果数据为空,则跳过
36 | if src == "":
37 | status: str = Base.TranslationStatus.EXCLUDED
38 | skip_internal_filter: bool = False
39 | # 如果包含 水蓝色 标签,则翻译
40 | elif any(v == "aqua" for v in tag):
41 | status: str = Base.TranslationStatus.UNTRANSLATED
42 | skip_internal_filter: bool = True
43 | # 如果 第一列、第二列 都有文本,则跳过
44 | elif dst != "" and src != dst:
45 | status: str = Base.TranslationStatus.TRANSLATED_IN_PAST
46 | skip_internal_filter: bool = False
47 | else:
48 | block = self.filter(src, path, tag, context)
49 | skip_internal_filter: bool = False
50 |
51 | # 如果全部数据都不需要过滤,则移除 red blue gold 标签
52 | if all(v == False for v in block):
53 | tag: list[str] = [v for v in tag if v not in ("red", "blue", "gold")]
54 | # 如果任意数据需要过滤,且不包含 red blue gold 标签,则添加 gold 标签
55 | elif any(v == True for v in block) and not any(v in ("red", "blue", "gold") for v in tag):
56 | tag: list[str] = tag + ["gold"]
57 |
58 | # 如果不需要过滤的数据,则翻译,否则排除
59 | if any(v == False for v in block):
60 | status: str = Base.TranslationStatus.UNTRANSLATED
61 | else:
62 | status: str = Base.TranslationStatus.EXCLUDED
63 |
64 | return src, dst, tag, status, skip_internal_filter
65 |
66 | # 过滤
67 | def filter(self, src: str, path: str, tag: list[str], context: list[str]) -> bool:
68 | if any(v in src for v in NONE.BLACKLIST_EXT):
69 | return [True] * len(context)
70 |
71 | block: list[bool] = []
72 | for _ in range(len(context) if len(context) > 0 else 1):
73 | # 包含 red blue 标签,则过滤
74 | if any(v in ("red", "blue") for v in tag):
75 | block.append(True)
76 | # 默认,无需过滤
77 | else:
78 | block.append(False)
79 |
80 | return block
81 |
82 | # 生成参数
83 | def generate_parameter(self, src:str, context: list[str], parameter: list[dict[str, str]], block: list[bool]) -> list[dict[str, str]]:
84 | # 如果全部需要排除或者全部需要保留,则不需要启用分区翻译功能
85 | if all(v is True for v in block) or all(v is False for v in block):
86 | pass
87 | else:
88 | if parameter is None:
89 | parameter = []
90 | for i, v in enumerate(block):
91 | # 索引检查
92 | if i >= len(parameter):
93 | parameter.append({})
94 |
95 | # 有效性检查
96 | if not isinstance(parameter[i], dict):
97 | parameter[i] = {}
98 |
99 | # 填充数据
100 | parameter[i]["contextStr"] = context[i]
101 | parameter[i]["translation"] = src if v == True else ""
102 |
103 | return parameter
--------------------------------------------------------------------------------
/module/File/TRANS/RENPY.py:
--------------------------------------------------------------------------------
1 | from module.File.TRANS.NONE import NONE
2 | from module.Cache.CacheItem import CacheItem
3 |
4 | class RENPY(NONE):
5 |
6 | TEXT_TYPE: str = CacheItem.TextType.RENPY
--------------------------------------------------------------------------------
/module/File/TRANS/RPGMAKER.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from module.File.TRANS.NONE import NONE
4 | from module.Cache.CacheItem import CacheItem
5 |
6 | class RPGMAKER(NONE):
7 |
8 | TEXT_TYPE: str = CacheItem.TextType.RPGMAKER
9 |
10 | BLACKLIST_PATH: tuple[re.Pattern] = (
11 | re.compile(r"\.js$", flags = re.IGNORECASE),
12 | )
13 |
14 | BLACKLIST_ADDRESS: tuple[re.Pattern] = (
15 | re.compile(r"^(?=.*MZ Plugin Command)(?!.*text).*", flags = re.IGNORECASE),
16 | re.compile(r"filename", flags = re.IGNORECASE),
17 | re.compile(r"/events/\d+/name", flags = re.IGNORECASE),
18 | re.compile(r"Tilesets/\d+/name", flags = re.IGNORECASE),
19 | re.compile(r"MapInfos/\d+/name", flags = re.IGNORECASE),
20 | re.compile(r"Animations/\d+/name", flags = re.IGNORECASE),
21 | re.compile(r"CommonEvents/\d+/name", flags = re.IGNORECASE),
22 | )
23 |
24 | # 过滤
25 | def filter(self, src: str, path: str, tag: list[str], context: list[str]) -> bool:
26 | if any(v in src for v in RPGMAKER.BLACKLIST_EXT):
27 | return [True] * len(context)
28 |
29 | if any(v.search(path) is not None for v in RPGMAKER.BLACKLIST_PATH):
30 | return [True] * len(context)
31 |
32 | block: list[bool] = []
33 | for address in context:
34 | # 如果在标签黑名单,则需要过滤
35 | if any(v in ("red", "blue") for v in tag):
36 | block.append(True)
37 | # 如果在地址黑名单,则需要过滤
38 | elif any(rule.search(address) is not None for rule in RPGMAKER.BLACKLIST_ADDRESS):
39 | block.append(True)
40 | # 默认,无需过滤
41 | else:
42 | block.append(False)
43 |
44 | return block
--------------------------------------------------------------------------------
/module/File/TRANS/WOLF.py:
--------------------------------------------------------------------------------
1 | import re
2 | import itertools
3 |
4 | from module.File.TRANS.NONE import NONE
5 | from module.Cache.CacheItem import CacheItem
6 | from base.LogManager import LogManager
7 |
8 | class WOLF(NONE):
9 |
10 | TEXT_TYPE: str = CacheItem.TextType.WOLF
11 |
12 | WHITELIST_ADDRESS: tuple[re.Pattern] = (
13 | re.compile(r"/Database/stringArgs/0$", flags = re.IGNORECASE),
14 | re.compile(r"/CommonEvent/stringArgs/\d*[1-9]\d*$", flags = re.IGNORECASE),
15 | re.compile(r"/CommonEventByName/stringArgs/\d*[1-9]\d*$", flags = re.IGNORECASE),
16 | re.compile(r"/Message/stringArgs/\d+$", flags = re.IGNORECASE),
17 | re.compile(r"/Picture/stringArgs/\d+$", flags = re.IGNORECASE),
18 | re.compile(r"/Choices/stringArgs/\d+$", flags = re.IGNORECASE),
19 | re.compile(r"/SetString/stringArgs/\d+$", flags = re.IGNORECASE),
20 | re.compile(r"/StringCondition/stringArgs/\d+$", flags = re.IGNORECASE),
21 | )
22 |
23 | BLACKLIST_ADDRESS: tuple[re.Pattern] = (
24 | re.compile(r"/Database/stringArgs/\d*[1-9]\d*$", flags = re.IGNORECASE),
25 | re.compile(r"/CommonEvent/stringArgs/0$", flags = re.IGNORECASE),
26 | re.compile(r"/CommonEventByName/stringArgs/0$", flags = re.IGNORECASE),
27 | re.compile(r"/name$", flags = re.IGNORECASE),
28 | re.compile(r"/description$", flags = re.IGNORECASE),
29 | re.compile(r"/Comment/stringArgs/", flags = re.IGNORECASE),
30 | re.compile(r"/DebugMessage/stringArgs/", flags = re.IGNORECASE),
31 | )
32 |
33 | # 预处理
34 | def pre_process(self) -> None:
35 | self.block_text: set[str] = self.generate_block_text(self.project)
36 |
37 | # 后处理
38 | def post_process(self) -> None:
39 | self.block_text: set[str] = self.generate_block_text(self.project)
40 |
41 | # 过滤
42 | def filter(self, src: str, path: str, tag: list[str], context: list[str]) -> bool:
43 | if any(v in src for v in WOLF.BLACKLIST_EXT):
44 | return [True] * len(context)
45 |
46 | block: list[bool] = []
47 | for address in context:
48 | # 如果在地址白名单,则无需过滤
49 | if any(rule.search(address) is not None for rule in WOLF.WHITELIST_ADDRESS):
50 | block.append(False)
51 | # 如果在地址黑名单,则需要过滤
52 | elif any(rule.search(address) is not None for rule in WOLF.BLACKLIST_ADDRESS):
53 | block.append(True)
54 | # 如果在标签黑名单,则需要过滤
55 | elif any(v in ("red", "blue") for v in tag):
56 | block.append(True)
57 | # 如果符合指定地址规则,并且没有命中以上规则,则需要过滤
58 | elif re.search(r"^common/", address, flags = re.IGNORECASE) is not None:
59 | block.append(True)
60 | # 如果符合指定地址规则,并且文本在屏蔽数据中,则需要过滤
61 | elif (
62 | re.search(r"DataBase.json/types/\d+/data/\d+/data/\d+/value", address, flags = re.IGNORECASE) is not None
63 | and src in self.block_text
64 | ):
65 | block.append(True)
66 | # 默认,无需过滤
67 | else:
68 | block.append(False)
69 |
70 | return block
71 |
72 | # 生成屏蔽文本集合
73 | def generate_block_text(self, project: dict) -> set[str]:
74 | result: set[str] = set()
75 |
76 | # 处理数据
77 | entry: dict = {}
78 | files: dict = project.get("files", {})
79 | for _, entry in files.items():
80 | for data, context in itertools.zip_longest(
81 | entry.get("data", []),
82 | entry.get("context", []),
83 | fillvalue = None
84 | ):
85 | # 处理可能为 None 的情况
86 | data: list[str] = data if data is not None else []
87 | context: list[str] = context if context is not None else []
88 |
89 | # 如果数据为空,则跳过
90 | if len(data) == 0 or not isinstance(data[0], str):
91 | continue
92 |
93 | # 判断是否需要屏蔽
94 | # 不需要屏蔽 - common/110.json/commands/29/Database/stringArgs/0
95 | # 需要屏蔽 - common/110.json/commands/29/Database/stringArgs/1
96 | context: str = "\n".join(context)
97 | if re.search(r"/Database/stringArgs/\d*[1-9]\d*$", context, flags = re.IGNORECASE) is not None:
98 | result.add(data[0])
99 |
100 | return result
--------------------------------------------------------------------------------
/module/File/TXT.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from base.Base import Base
4 | from base.BaseLanguage import BaseLanguage
5 | from module.Text.TextHelper import TextHelper
6 | from module.Cache.CacheItem import CacheItem
7 | from module.Config import Config
8 | from module.Localizer.Localizer import Localizer
9 |
10 | class TXT(Base):
11 |
12 | def __init__(self, config: Config) -> None:
13 | super().__init__()
14 |
15 | # 初始化
16 | self.config = config
17 | self.input_path: str = config.input_folder
18 | self.output_path: str = config.output_folder
19 | self.source_language: BaseLanguage.Enum = config.source_language
20 | self.target_language: BaseLanguage.Enum = config.target_language
21 |
22 | # 在扩展名前插入文本
23 | def insert_target(self, path: str) -> str:
24 | root, ext = os.path.splitext(path)
25 | return f"{root}.{self.target_language.lower()}{ext}"
26 |
27 | # 在扩展名前插入文本
28 | def insert_source_target(self, path: str) -> str:
29 | root, ext = os.path.splitext(path)
30 | return f"{root}.{self.source_language.lower()}.{self.target_language.lower()}{ext}"
31 |
32 | # 读取
33 | def read_from_path(self, abs_paths: list[str]) -> list[CacheItem]:
34 | items:list[CacheItem] = []
35 | for abs_path in abs_paths:
36 | # 获取相对路径
37 | rel_path = os.path.relpath(abs_path, self.input_path)
38 |
39 | # 获取文件编码
40 | encoding = TextHelper.get_enconding(path = abs_path, add_sig_to_utf8 = True)
41 |
42 | # 数据处理
43 | with open(abs_path, "r", encoding = encoding) as reader:
44 | for line in [line.removesuffix("\n") for line in reader.readlines()]:
45 | items.append(
46 | CacheItem.from_dict({
47 | "src": line,
48 | "dst": line,
49 | "row": len(items),
50 | "file_type": CacheItem.FileType.TXT,
51 | "file_path": rel_path,
52 | })
53 | )
54 |
55 | return items
56 |
57 | # 写入
58 | def write_to_path(self, items: list[CacheItem]) -> None:
59 | # 筛选
60 | target = [
61 | item for item in items
62 | if item.get_file_type() == CacheItem.FileType.TXT
63 | ]
64 |
65 | # 按文件路径分组
66 | group: dict[str, list[str]] = {}
67 | for item in target:
68 | group.setdefault(item.get_file_path(), []).append(item)
69 |
70 | # 分别处理每个文件
71 | for rel_path, items in group.items():
72 | abs_path = os.path.join(self.output_path, rel_path)
73 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
74 | with open(self.insert_target(abs_path), "w", encoding = "utf-8") as writer:
75 | writer.write("\n".join([item.get_dst() for item in items]))
76 |
77 | # 分别处理每个文件(双语)
78 | for rel_path, items in group.items():
79 | abs_path = f"{self.output_path}/{Localizer.get().path_bilingual}/{rel_path}"
80 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
81 | with open(self.insert_source_target(abs_path), "w", encoding = "utf-8") as writer:
82 | result: list[str] = []
83 | for item in items:
84 | if self.config.deduplication_in_bilingual == True and item.get_src() == item.get_dst():
85 | result.append(item.get_dst())
86 | else:
87 | result.append(f"{item.get_src()}\n{item.get_dst()}")
88 | writer.write("\n".join(result))
89 |
--------------------------------------------------------------------------------
/module/File/XLSX.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import openpyxl
4 | import openpyxl.worksheet.worksheet
5 |
6 | from base.Base import Base
7 | from base.BaseLanguage import BaseLanguage
8 | from module.Cache.CacheItem import CacheItem
9 | from module.Config import Config
10 | from module.TableManager import TableManager
11 |
12 | class XLSX(Base):
13 |
14 | def __init__(self, config: Config) -> None:
15 | super().__init__()
16 |
17 | # 初始化
18 | self.config = config
19 | self.input_path: str = config.input_folder
20 | self.output_path: str = config.output_folder
21 | self.source_language: BaseLanguage.Enum = config.source_language
22 | self.target_language: BaseLanguage.Enum = config.target_language
23 |
24 | # 读取
25 | def read_from_path(self, abs_paths: list[str]) -> list[CacheItem]:
26 | items:list[CacheItem] = []
27 | for abs_path in abs_paths:
28 | # 获取相对路径
29 | rel_path = os.path.relpath(abs_path, self.input_path)
30 |
31 | # 数据处理
32 | book: openpyxl.Workbook = openpyxl.load_workbook(abs_path)
33 | sheet: openpyxl.worksheet.worksheet.Worksheet = book.active
34 |
35 | # 跳过空表格
36 | if sheet.max_row == 0 or sheet.max_column == 0:
37 | continue
38 |
39 | # 判断是否为 WOLF 翻译表格文件
40 | if self.is_wold_xlsx(sheet):
41 | continue
42 |
43 | for row in range(1, sheet.max_row + 1):
44 | src = sheet.cell(row = row, column = 1).value
45 | dst = sheet.cell(row = row, column = 2).value
46 |
47 | # 跳过读取失败的行
48 | # 数据不存在时为 None,存在时可能是 str int float 等多种类型
49 | if src is None:
50 | continue
51 |
52 | src = str(src)
53 | dst = str(dst) if dst is not None else ""
54 |
55 | if src == "":
56 | items.append(
57 | CacheItem.from_dict({
58 | "src": src,
59 | "dst": dst,
60 | "row": row,
61 | "file_type": CacheItem.FileType.XLSX,
62 | "file_path": rel_path,
63 | "status": Base.TranslationStatus.EXCLUDED,
64 | })
65 | )
66 | elif dst != "" and src != dst:
67 | items.append(
68 | CacheItem.from_dict({
69 | "src": src,
70 | "dst": dst,
71 | "row": row,
72 | "file_type": CacheItem.FileType.XLSX,
73 | "file_path": rel_path,
74 | "status": Base.TranslationStatus.TRANSLATED_IN_PAST,
75 | })
76 | )
77 | else:
78 | items.append(
79 | CacheItem.from_dict({
80 | "src": src,
81 | "dst": dst,
82 | "row": row,
83 | "file_type": CacheItem.FileType.XLSX,
84 | "file_path": rel_path,
85 | "status": Base.TranslationStatus.UNTRANSLATED,
86 | })
87 | )
88 |
89 | return items
90 |
91 | # 写入
92 | def write_to_path(self, items: list[CacheItem]) -> None:
93 | target = [
94 | item for item in items
95 | if item.get_file_type() == CacheItem.FileType.XLSX
96 | ]
97 |
98 | group: dict[str, list[str]] = {}
99 | for item in target:
100 | group.setdefault(item.get_file_path(), []).append(item)
101 |
102 | # 分别处理每个文件
103 | for rel_path, items in group.items():
104 | # 按行号排序
105 | items = sorted(items, key = lambda x: x.get_row())
106 |
107 | # 新建工作表
108 | book: openpyxl.Workbook = openpyxl.Workbook()
109 | sheet: openpyxl.worksheet.worksheet.Worksheet = book.active
110 |
111 | # 设置表头
112 | sheet.column_dimensions["A"].width = 64
113 | sheet.column_dimensions["B"].width = 64
114 |
115 | # 将数据写入工作表
116 | for item in items:
117 | row: int = item.get_row()
118 | TableManager.set_cell_value(sheet, row, column = 1, value = item.get_src())
119 | TableManager.set_cell_value(sheet, row, column = 2, value = item.get_dst())
120 |
121 | # 保存工作簿
122 | abs_path = f"{self.output_path}/{rel_path}"
123 | os.makedirs(os.path.dirname(abs_path), exist_ok = True)
124 | book.save(abs_path)
125 |
126 | # 是否为 WOLF 翻译表格文件
127 | def is_wold_xlsx(self, sheet: openpyxl.worksheet.worksheet.Worksheet) -> bool:
128 | value: str = sheet.cell(row = 1, column = 1).value
129 | if not isinstance(value, str) or "code" not in value.lower():
130 | return False
131 |
132 | value: str = sheet.cell(row = 1, column = 2).value
133 | if not isinstance(value, str) or "flag" not in value.lower():
134 | return False
135 |
136 | value: str = sheet.cell(row = 1, column = 3).value
137 | if not isinstance(value, str) or "type" not in value.lower():
138 | return False
139 |
140 | value: str = sheet.cell(row = 1, column = 4).value
141 | if not isinstance(value, str) or "info" not in value.lower():
142 | return False
143 |
144 | return True
--------------------------------------------------------------------------------
/module/Filter/LanguageFilter.py:
--------------------------------------------------------------------------------
1 | from base.BaseLanguage import BaseLanguage
2 | from module.Text.TextHelper import TextHelper
3 |
4 | class LanguageFilter():
5 |
6 | def filter(src: str, source_language: BaseLanguage.Enum) -> bool:
7 | # 获取语言判断函数
8 | if source_language == BaseLanguage.Enum.ZH:
9 | func = TextHelper.CJK.any
10 | elif source_language == BaseLanguage.Enum.EN:
11 | func = TextHelper.Latin.any
12 | else:
13 | func = getattr(TextHelper, source_language).any
14 |
15 | # 返回值 True 表示需要过滤(即需要排除)
16 | if callable(func) != True:
17 | return False
18 | else:
19 | return not func(src)
--------------------------------------------------------------------------------
/module/Filter/RuleFilter.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from module.Text.TextHelper import TextHelper
4 |
5 | class RuleFilter():
6 |
7 | PREFIX: tuple[str] = (
8 | "MapData/".lower(),
9 | "SE/".lower(),
10 | "BGS".lower(),
11 | "0=".lower(),
12 | "BGM/".lower(),
13 | "FIcon/".lower(),
14 | )
15 |
16 | SUFFIX: tuple[str] = (
17 | ".mp3", ".wav", ".ogg", "mid",
18 | ".png", ".jpg", ".jpeg", ".gif", ".psd", ".webp", ".heif", ".heic",
19 | ".avi", ".mp4", ".webm",
20 | ".txt", ".7z", ".gz", ".rar", ".zip", ".json",
21 | ".sav", ".mps", ".ttf", ".otf", ".woff",
22 | )
23 |
24 | RE_ALL: tuple[re.Pattern] = (
25 | re.compile(r"^EV\d+$", flags = re.IGNORECASE),
26 | re.compile(r"^DejaVu Sans$", flags = re.IGNORECASE), # RenPy 默认字体名称
27 | re.compile(r"^Opendyslexic$", flags = re.IGNORECASE), # RenPy 默认字体名称
28 | re.compile(r"^\{#file_time\}", flags = re.IGNORECASE), # RenPy 存档时间
29 | )
30 |
31 | def filter(src: str) -> bool:
32 | flags = []
33 | for line in src.splitlines():
34 | line = line.strip().lower()
35 |
36 | # 空字符串
37 | if line.strip() == "":
38 | flags.append(True)
39 | continue
40 |
41 | # 格式校验
42 | # isdecimal
43 | # 字符串中的字符是否全是十进制数字。也就是说,只有那些在数字系统中被认为是“基本”的数字字符(0-9)才会返回 True。
44 | # isdigit
45 | # 字符串中的字符是否都是数字字符。它不仅检查十进制数字,还包括其他可以表示数字的字符,如数字上标、罗马数字、圆圈数字等。
46 | # isnumeric
47 | # 字符串中的字符是否表示任何类型的数字,包括整数、分数、数字字符的变种(比如上标、下标)以及其他可以被认为是数字的字符(如中文数字)。
48 | # 仅包含空白符、数字字符、标点符号
49 | if all(c.isspace() or c.isnumeric() or TextHelper.is_punctuation(c) for c in line):
50 | flags.append(True)
51 | continue
52 |
53 | # 以目标前缀开头
54 | if any(line.startswith(v) for v in RuleFilter.PREFIX):
55 | flags.append(True)
56 | continue
57 |
58 | # 以目标后缀结尾
59 | if any(line.endswith(v) for v in RuleFilter.SUFFIX):
60 | flags.append(True)
61 | continue
62 |
63 | # 符合目标规则
64 | if any(v.search(line) is not None for v in RuleFilter.RE_ALL):
65 | flags.append(True)
66 | continue
67 |
68 | # 都不匹配
69 | flags.append(False)
70 |
71 | # 返回值 True 表示需要过滤(即需要排除)
72 | if flags == []:
73 | return False
74 | else:
75 | return all(v == True for v in flags)
--------------------------------------------------------------------------------
/module/Fixer/CodeFixer.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from rich import print
4 |
5 | from module.Cache.CacheItem import CacheItem
6 | from module.Config import Config
7 |
8 | class CodeFixer():
9 |
10 | def __init__(self) -> None:
11 | super().__init__()
12 |
13 | # 检查并替换
14 | @classmethod
15 | def fix(cls, src: str, dst: str, text_type: str, config: Config) -> str:
16 | from module.TextProcessor import TextProcessor
17 |
18 | src_codes: list[str] = []
19 | dst_codes: list[str] = []
20 | rule: re.Pattern = TextProcessor(config, None).get_re_sample(
21 | custom = config.text_preserve_enable,
22 | text_type = text_type,
23 | )
24 | if rule is not None:
25 | src_codes = [v.group(0) for v in rule.finditer(src) if v.group(0).strip() != ""]
26 | dst_codes = [v.group(0) for v in rule.finditer(dst) if v.group(0).strip() != ""]
27 |
28 | if src_codes == dst_codes:
29 | return dst
30 |
31 | if len(src_codes) >= len(dst_codes):
32 | return dst
33 |
34 | # 判断是否是有序子集
35 | flag, mismatchs = cls.is_ordered_subset(src_codes, dst_codes)
36 | if flag == True:
37 | i: list[int] = [0]
38 | dst = rule.sub(lambda m: cls.repl(m, i, mismatchs), dst)
39 |
40 | return dst
41 |
42 | @classmethod
43 | def repl(cls, m: re.Match, i: list[int], mismatchs: list[int]) -> str:
44 | text: str = m.group(0)
45 | if text.strip() == "":
46 | return text
47 | elif i[0] in mismatchs:
48 | i[0] = i[0] + 1
49 | return ""
50 | else:
51 | i[0] = i[0] + 1
52 | return text
53 |
54 | # 判断是否是有序子集,并输出 y 中多余元素的索引
55 | @classmethod
56 | def is_ordered_subset(cls, x: list[str], y: list[str]) -> tuple[bool, list[int]]:
57 | y: list[str] = y.copy()
58 | mismatchs: list[int] = []
59 |
60 | y_index: int = -1
61 | for x_item in x:
62 | match_flag: bool = False
63 | break_flag: bool = False
64 |
65 | while break_flag == False and len(y) > 0:
66 | y_item = y.pop(0)
67 | y_index = y_index + 1
68 | if x_item == y_item:
69 | match_flag = True
70 | break_flag = True
71 | break
72 | else:
73 | mismatchs.append(y_index)
74 |
75 | if match_flag == False:
76 | return False, []
77 |
78 | # 如果还有剩余未匹配项,则将其索引全部添加
79 | for i in range(len(y)):
80 | mismatchs.append(y_index + i + 1)
81 |
82 | # 如果所有 x 元素都匹配成功,返回 True
83 | return True, mismatchs
84 |
85 | @classmethod
86 | def test(cls, config: Config) -> None:
87 | x = "合計 \\V[62]! やったやった♪ 私の勝ちね!\n\\c[17]――レナリスの勝ち! 【3000 G】手に入れた!\\c[0]\n\\$"
88 | y = "总计 \\V[62]! 哈哈! 我赢了!\n\\c[17]――雷纳里斯赢了! 获得了\\c[2]【3000 G】\\c[0]!\\c[0]\n\\$"
89 | z = cls().fix(x, y, CacheItem.TextType.RPGMAKER, config)
90 | print(f"{repr(x)}\n{repr(y)}\n{repr(z)}")
--------------------------------------------------------------------------------
/module/Fixer/EscapeFixer.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from base.LogManager import LogManager
4 |
5 | class EscapeFixer():
6 |
7 | # \f[21]\c[4]\E仲良くなるためのヒント
8 | # \f[21]\c[4]\\E增进亲密度的小提示
9 |
10 | # 「\\n[1]様、おはようございます」
11 | # 「\n[1] 殿下,早安」
12 |
13 | RE_ESCAPE_PATTERN: re.Pattern = re.compile(r"\\+", flags = re.IGNORECASE)
14 |
15 | def __init__(self) -> None:
16 | super().__init__()
17 |
18 | # 检查并替换
19 | @classmethod
20 | def fix(cls, src: str, dst: str) -> str:
21 | # 理论上文本中不会包含换行符,如果出现换行符,将其还原为 \\n
22 | dst = dst.replace("\n", "\\n")
23 | src_results: list[str] = cls.RE_ESCAPE_PATTERN.findall(src)
24 | dst_results: list[str] = cls.RE_ESCAPE_PATTERN.findall(dst)
25 |
26 | # 检查匹配项是否完全相同
27 | if src_results == dst_results:
28 | return dst
29 |
30 | # 检查匹配项数量是否一致
31 | if len(src_results) != len(dst_results):
32 | return dst
33 |
34 | # 逐一替换
35 | i: list[int] = [0]
36 | dst = cls.RE_ESCAPE_PATTERN.sub(
37 | lambda m: cls.repl(m, i, src_results),
38 | dst,
39 | )
40 |
41 | return dst
42 |
43 | @classmethod
44 | def repl(cls, m: re.Match, i: list[int], src_results: list[str]) -> str:
45 | i[0] = i[0] + 1
46 | return src_results[i[0] - 1]
--------------------------------------------------------------------------------
/module/Fixer/HangeulFixer.py:
--------------------------------------------------------------------------------
1 | from module.Text.TextHelper import TextHelper
2 |
3 | class HangeulFixer():
4 |
5 | # 拟声词
6 | RULE_ONOMATOPOEIA = (
7 | "뿅",
8 | "슝",
9 | "쩝",
10 | "콕",
11 | "끙",
12 | "힝",
13 | )
14 |
15 | def __init__(self) -> None:
16 | super().__init__()
17 |
18 | # 检查并替换
19 | @classmethod
20 | def fix(cls, dst: str) -> str:
21 | # 将字符串转换为列表,方便逐个处理字符
22 | result = []
23 | length = len(dst)
24 |
25 | for i, char in enumerate(dst):
26 | if char in HangeulFixer.RULE_ONOMATOPOEIA:
27 | # 检查前后字符是否为假名
28 | prev_char = dst[i - 1] if i > 0 else None
29 | next_char = dst[i + 1] if i < length - 1 else None
30 |
31 | is_prev_hangeul = prev_char is not None and TextHelper.KO.hangeul(prev_char)
32 | is_next_hangeul = next_char is not None and TextHelper.KO.hangeul(next_char)
33 |
34 | # 如果前后字符中有谚文,则保留当前字符
35 | if is_prev_hangeul == True or is_next_hangeul == True :
36 | result.append(char)
37 | else:
38 | # 非拟声词字符直接保留
39 | result.append(char)
40 |
41 | # 将列表转换回字符串
42 | return "".join(result)
43 |
--------------------------------------------------------------------------------
/module/Fixer/KanaFixer.py:
--------------------------------------------------------------------------------
1 | from module.Text.TextHelper import TextHelper
2 |
3 | class KanaFixer():
4 |
5 | # 拟声词
6 | RULE_ONOMATOPOEIA = (
7 | "ッ",
8 | "っ",
9 | "ぁ",
10 | "ぃ",
11 | "ぅ",
12 | "ぇ",
13 | "ぉ",
14 | "ゃ",
15 | "ゅ",
16 | "ょ",
17 | "ゎ",
18 | )
19 |
20 | def __init__(self) -> None:
21 | super().__init__()
22 |
23 | # 检查并替换
24 | @classmethod
25 | def fix(cls, dst: str) -> str:
26 | # 将字符串转换为列表,方便逐个处理字符
27 | result = []
28 | length = len(dst)
29 |
30 | for i, char in enumerate(dst):
31 | if char in KanaFixer.RULE_ONOMATOPOEIA:
32 | # 检查前后字符是否为假名
33 | prev_char = dst[i - 1] if i > 0 else None
34 | next_char = dst[i + 1] if i < length - 1 else None
35 |
36 | is_prev_kana = prev_char is not None and (TextHelper.JA.hiragana(prev_char) or TextHelper.JA.katakana(prev_char))
37 | is_next_kana = next_char is not None and (TextHelper.JA.hiragana(next_char) or TextHelper.JA.katakana(next_char))
38 |
39 | # 如果前后字符中有假名,则保留当前字符
40 | if is_prev_kana == True or is_next_kana == True :
41 | result.append(char)
42 | else:
43 | # 非拟声词字符直接保留
44 | result.append(char)
45 |
46 | # 将列表转换回字符串
47 | return "".join(result)
48 |
--------------------------------------------------------------------------------
/module/Fixer/NumberFixer.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | class NumberFixer():
4 |
5 | # 圆圈数字列表
6 | CIRCLED_NUMBERS = tuple(chr(i) for i in range(0x2460, 0x2474)) # ①-⑳
7 | CIRCLED_NUMBERS_CJK_01 = tuple(chr(i) for i in range(0x3251, 0x3260)) # ㉑-㉟
8 | CIRCLED_NUMBERS_CJK_02 = tuple(chr(i) for i in range(0x32B1, 0x32C0)) # ㊱-㊿
9 | CIRCLED_NUMBERS_ALL = ("",) + CIRCLED_NUMBERS + CIRCLED_NUMBERS_CJK_01 + CIRCLED_NUMBERS_CJK_02 # 开头加个空字符来对齐索引和数值
10 |
11 | # 预设编译正则
12 | PATTERN_ALL_NUM = re.compile(r"\d+|[①-⑳㉑-㉟㊱-㊿]", re.IGNORECASE)
13 | PATTERN_CIRCLED_NUM = re.compile(r"[①-⑳㉑-㉟㊱-㊿]", re.IGNORECASE)
14 |
15 | def __init__(self) -> None:
16 | super().__init__()
17 |
18 | # 检查并替换
19 | @classmethod
20 | def fix(cls, src: str, dst: str) -> str:
21 | # 找出 src 与 dst 中的圆圈数字
22 | src_nums = cls.PATTERN_ALL_NUM.findall(src)
23 | dst_nums = cls.PATTERN_ALL_NUM.findall(dst)
24 | src_circled_nums = cls.PATTERN_CIRCLED_NUM.findall(src)
25 | dst_circled_nums = cls.PATTERN_CIRCLED_NUM.findall(dst)
26 |
27 | # 如果原文中没有圆圈数字,则跳过
28 | if len(src_circled_nums) == 0:
29 | return dst
30 |
31 | # 如果原文和译文中数字(含圆圈数字)的数量不一致,则跳过
32 | if len(src_nums) != len(dst_nums):
33 | return dst
34 |
35 | # 如果原文中的圆圈数字数量少于译文中的圆圈数字数量,则跳过
36 | if len(src_circled_nums) < len(dst_circled_nums):
37 | return dst
38 |
39 | # 遍历原文与译文中的数字(含圆圈数字),尝试恢复
40 | for i in range(len(src_nums)):
41 | src_num_srt = src_nums[i]
42 | dst_num_srt = dst_nums[i]
43 | dst_num_int = cls.safe_int(dst_num_srt)
44 |
45 | # 如果原文中该位置不是圆圈数字,则跳过
46 | if src_num_srt not in cls.CIRCLED_NUMBERS_ALL:
47 | continue
48 |
49 | # 如果译文中该位置数值不在有效范围,则跳过
50 | if dst_num_int < 0 or dst_num_int >= len(cls.CIRCLED_NUMBERS_ALL):
51 | continue
52 |
53 | # 如果原文、译文中该位置的圆圈数字不一致,则跳过
54 | if src_num_srt != cls.CIRCLED_NUMBERS_ALL[dst_num_int]:
55 | continue
56 |
57 | # 尝试恢复
58 | dst = cls.fix_circled_numbers_by_index(dst, i, src_num_srt)
59 |
60 | return dst
61 |
62 | # 安全转换字符串为整数
63 | @classmethod
64 | def safe_int(cls, s: str) -> int:
65 | result = -1
66 |
67 | try:
68 | result = int(s)
69 | except Exception:
70 | pass
71 |
72 | return result
73 |
74 | # 通过索引修复圆圈数字
75 | @classmethod
76 | def fix_circled_numbers_by_index(cls, dst: str, target_i: int, target_str: str) -> str:
77 | # 用于标识目标位置
78 | i = [0]
79 |
80 | def repl(m: re.Match) -> str:
81 | if i[0] == target_i:
82 | i[0] = i[0] + 1
83 | return target_str
84 | else:
85 | i[0] = i[0] + 1
86 | return m.group(0)
87 |
88 | return cls.PATTERN_ALL_NUM.sub(repl, dst)
--------------------------------------------------------------------------------
/module/Fixer/PunctuationFixer.py:
--------------------------------------------------------------------------------
1 | from base.BaseLanguage import BaseLanguage
2 |
3 | class PunctuationFixer():
4 |
5 | # 数量匹配规则
6 | RULE_SAME_COUNT_A: dict[str, tuple[str]] = {
7 | " ": (" ", ), # 全角空格和半角空格之间的转换
8 | ":": (":", ),
9 | "・": ("·", ),
10 | "?": ("?", ),
11 | "!": ("!", ),
12 | "\u2014": ("\u002d", "\u2015"), # 破折号之间的转换,\u002d = - ,\u2014 = ― ,\u2015 = —
13 | "\u2015": ("\u002d", "\u2014"), # 破折号之间的转换,\u002d = - ,\u2014 = ― ,\u2015 = —
14 | "<": ("<", "《"),
15 | ">": (">", "》"),
16 | "<": ("<", "《"),
17 | ">": (">", "》"),
18 | "[": ("【", ),
19 | "]": ("】", ),
20 | "【": ("[", ),
21 | "】": ("]", ),
22 | "(": ("(", ),
23 | ")": (")", ),
24 | "(": ("(", ),
25 | ")": (")", ),
26 | "「": ("‘", "“", "『"),
27 | "」": ("’", "”", "』"),
28 | "『": ("‘", "“", "「"),
29 | "』": ("’", "”", "」"),
30 | "‘": ("“", "「", "『"),
31 | "’": ("”", "」", "』"),
32 | "“": ("‘", "「", "『"),
33 | "”": ("’", "」", "』"),
34 | }
35 |
36 | # 数量匹配规则
37 | RULE_SAME_COUNT_B: dict[str, tuple[str]] = {
38 | " ": (" ", ), # 全角空格和半角空格之间的转换
39 | ":": (":", ),
40 | "·": ("・", ),
41 | "?": ("?", ),
42 | "!": ("!", ),
43 | "\u002d": ("\u2014", "\u2015"), # 破折号之间的转换,\u002d = - ,\u2014 = ― ,\u2015 = —
44 | }
45 |
46 | # 强制替换规则
47 | # 译文语言为 CJK 语言时,执行此规则
48 | RULE_FORCE_CJK: dict[str, tuple[str]] = {
49 | "「": ("“"),
50 | "」": ("”"),
51 | }
52 |
53 | def __init__(self) -> None:
54 | super().__init__()
55 |
56 | # 检查并替换
57 | @classmethod
58 | def fix(cls, src: str, dst: str, source_language: BaseLanguage.Enum, target_language: BaseLanguage.Enum) -> str:
59 | # 首尾标点修正
60 | dst = cls.fix_start_end(src, dst, target_language)
61 |
62 | # CJK To CJK = A + B
63 | # CJK To 非CJK = A + B
64 | # 非CJK To CJK = A
65 | # 非CJK To 非CJK = A + B
66 | if BaseLanguage.is_cjk(source_language) and BaseLanguage.is_cjk(target_language):
67 | dst = cls.apply_fix_rules(src, dst, cls.RULE_SAME_COUNT_A)
68 | dst = cls.apply_fix_rules(src, dst, cls.RULE_SAME_COUNT_B)
69 | elif BaseLanguage.is_cjk(source_language) and not BaseLanguage.is_cjk(target_language):
70 | dst = cls.apply_fix_rules(src, dst, cls.RULE_SAME_COUNT_A)
71 | dst = cls.apply_fix_rules(src, dst, cls.RULE_SAME_COUNT_B)
72 | elif not BaseLanguage.is_cjk(source_language) and BaseLanguage.is_cjk(target_language):
73 | dst = cls.apply_fix_rules(src, dst, cls.RULE_SAME_COUNT_A)
74 | else:
75 | dst = cls.apply_fix_rules(src, dst, cls.RULE_SAME_COUNT_A)
76 | dst = cls.apply_fix_rules(src, dst, cls.RULE_SAME_COUNT_B)
77 |
78 | # 译文语言为 CJK 语言时,执行强制规则
79 | if BaseLanguage.is_cjk(target_language):
80 | for key, value in cls.RULE_FORCE_CJK.items():
81 | dst = cls.apply_replace_rules(dst, key, value)
82 |
83 | return dst
84 |
85 | # 检查
86 | @classmethod
87 | def check(cls, src: str, dst: str, key: str, value: tuple) -> tuple[str, bool]:
88 | num_s_x = src.count(key)
89 | num_s_y = sum(src.count(t) for t in value)
90 | num_t_x = dst.count(key)
91 | num_t_y = sum(dst.count(t) for t in value)
92 |
93 | # 首先,原文中的目标符号的数量应大于零,否则表示没有需要修复的标点
94 | # 然后,原文中目标符号和错误符号的数量不应相等,否则无法确定哪个符号是正确的
95 | # 然后,原文中的目标符号的数量应大于译文中的目标符号的数量,否则表示没有需要修复的标点
96 | # 最后,如果原文中目标符号的数量等于译文中目标符号与错误符号的数量之和,则判断为需要修复
97 | return num_s_x > 0 and num_s_x != num_s_y and num_s_x > num_t_x and num_s_x == num_t_x + num_t_y
98 |
99 | # 应用修复规则
100 | @classmethod
101 | def apply_fix_rules(cls, src: str, dst: str, rules: dict) -> str:
102 | for key, value in rules.items():
103 | if cls.check(src, dst, key, value) == True:
104 | dst = cls.apply_replace_rules(dst, key, value)
105 |
106 | return dst
107 |
108 | # 应用替换规则
109 | @classmethod
110 | def apply_replace_rules(cls, dst: str, key: str, value: tuple) -> str:
111 | for t in value:
112 | dst = dst.replace(t, key)
113 |
114 | return dst
115 |
116 | # 首尾标点修正
117 | @classmethod
118 | def fix_start_end(self, src: str, dst: str, target_language: BaseLanguage.Enum) -> str:
119 | # 纠正首尾错误的引号
120 | if dst.startswith(("'", "\"", "‘", "“", "「", "『")):
121 | if src.startswith(("「", "『")):
122 | dst = f"{src[0]}{dst[1:]}"
123 | elif BaseLanguage.is_cjk(target_language) and src.startswith(("‘", "“")):
124 | dst = f"{src[0]}{dst[1:]}"
125 | if dst.endswith(("'", "\"", "’", "”", "」", "』")):
126 | if src.endswith(("」", "』")):
127 | dst = f"{dst[:-1]}{src[-1]}"
128 | elif BaseLanguage.is_cjk(target_language) and src.endswith(("’", "”")):
129 | dst = f"{dst[:-1]}{src[-1]}"
130 |
131 | # 移除首尾多余的引号
132 | # for v in ("‘", "“", "「", "『"):
133 | # if dst.startswith(v) and not src.startswith(v) and dst.count(v) > src.count(v):
134 | # dst = dst[1:]
135 | # break
136 | # for v in ("’", "”", "」", "』"):
137 | # if dst.endswith(v) and not src.endswith(v) and dst.count(v) > src.count(v):
138 | # dst = dst[:-1]
139 | # break
140 |
141 | return dst
--------------------------------------------------------------------------------
/module/Localizer/Localizer.py:
--------------------------------------------------------------------------------
1 | from base.BaseLanguage import BaseLanguage
2 | from module.Localizer.LocalizerZH import LocalizerZH
3 | from module.Localizer.LocalizerEN import LocalizerEN
4 |
5 | class Localizer():
6 |
7 | APP_LANGUAGE: BaseLanguage.Enum = BaseLanguage.Enum.ZH
8 |
9 | @classmethod
10 | def get(cls) -> LocalizerZH | LocalizerEN:
11 | if cls.APP_LANGUAGE == BaseLanguage.Enum.EN:
12 | return LocalizerEN
13 | else:
14 | return LocalizerZH
15 |
16 | @classmethod
17 | def get_app_language(cls) -> BaseLanguage.Enum:
18 | return cls.APP_LANGUAGE
19 |
20 | @classmethod
21 | def set_app_language(cls, app_language: BaseLanguage.Enum) -> None:
22 | cls.APP_LANGUAGE = app_language
--------------------------------------------------------------------------------
/module/Normalizer.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import unicodedata
3 |
4 | class Normalizer():
5 |
6 | # 自定义规则
7 | CUSTOM_RULE = {}
8 |
9 | # 全角转半角
10 | CUSTOM_RULE.update({chr(i): chr(i - 0xFEE0) for i in itertools.chain(
11 | range(0xFF21, 0xFF3A + 1), # 全角 A-Z 转换为 半角 A-Z
12 | range(0xFF41, 0xFF5A + 1), # 全角 a-z 转换为 半角 a-z
13 | range(0xFF10, 0xFF19 + 1), # 全角 0-9 转换为 半角 0-9
14 | )})
15 |
16 | # 全角转半角 - 片假名
17 | CUSTOM_RULE.update({
18 | "ア": "ア",
19 | "イ": "イ",
20 | "ウ": "ウ",
21 | "エ": "エ",
22 | "オ": "オ",
23 | "カ": "カ",
24 | "キ": "キ",
25 | "ク": "ク",
26 | "ケ": "ケ",
27 | "コ": "コ",
28 | "サ": "サ",
29 | "シ": "シ",
30 | "ス": "ス",
31 | "セ": "セ",
32 | "ソ": "ソ",
33 | "タ": "タ",
34 | "チ": "チ",
35 | "ツ": "ツ",
36 | "テ": "テ",
37 | "ト": "ト",
38 | "ナ": "ナ",
39 | "ニ": "ニ",
40 | "ヌ": "ヌ",
41 | "ネ": "ネ",
42 | "ノ": "ノ",
43 | "ハ": "ハ",
44 | "ヒ": "ヒ",
45 | "フ": "フ",
46 | "ヘ": "ヘ",
47 | "ホ": "ホ",
48 | "マ": "マ",
49 | "ミ": "ミ",
50 | "ム": "ム",
51 | "メ": "メ",
52 | "モ": "モ",
53 | "ヤ": "ヤ",
54 | "ユ": "ユ",
55 | "ヨ": "ヨ",
56 | "ラ": "ラ",
57 | "リ": "リ",
58 | "ル": "ル",
59 | "レ": "レ",
60 | "ロ": "ロ",
61 | "ワ": "ワ",
62 | "ヲ": "ヲ",
63 | "ン": "ン",
64 | "ァ": "ァ",
65 | "ィ": "ィ",
66 | "ゥ": "ゥ",
67 | "ェ": "ェ",
68 | "ォ": "ォ",
69 | "ャ": "ャ",
70 | "ュ": "ュ",
71 | "ョ": "ョ",
72 | "ッ": "ッ",
73 | "ー": "ー",
74 | "゙": "゛", # 浊音符号
75 | "゚": "゜", # 半浊音符号
76 | })
77 |
78 | # 规范化
79 | @classmethod
80 | def normalize(CLS, text: str) -> str:
81 | # NFC(Normalization Form C):将字符分解后再合并成最小数量的单一字符(合成字符)。
82 | # NFD(Normalization Form D):将字符分解成组合字符(即一个字母和附加的重音符号等)。
83 | # NFKC(Normalization Form KC):除了合成与分解外,还会进行兼容性转换,例如将全角字符转换为半角字符。
84 | # NFKD(Normalization Form KD):除了分解外,还会进行兼容性转换。
85 | text = unicodedata.normalize("NFC", text)
86 |
87 | # 应用自定义的规则
88 | text = "".join([CLS.CUSTOM_RULE.get(char, char) for char in text])
89 |
90 | # 返回结果
91 | return text
--------------------------------------------------------------------------------
/module/ProgressBar.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from types import TracebackType
3 | from typing import Any
4 | from typing import Self
5 |
6 | from rich.progress import BarColumn
7 | from rich.progress import Progress
8 | from rich.progress import TaskID
9 | from rich.progress import TextColumn
10 | from rich.progress import TimeElapsedColumn
11 | from rich.progress import TimeRemainingColumn
12 |
13 | class ProgressBar():
14 |
15 | # 类变量
16 | progress: Progress | None = None
17 |
18 | def __init__(self, transient: bool) -> None:
19 | super().__init__()
20 |
21 | # 初始化
22 | self.tasks: dict[TaskID, dict[str, Any]] = {}
23 | self.transient: bool = transient
24 |
25 | def __enter__(self) -> Self:
26 | if not isinstance(__class__.progress, Progress):
27 | __class__.progress = Progress(
28 | TextColumn(datetime.now().strftime("[%H:%M:%S]"), style = "log.time"),
29 | TextColumn("INFO ", style = "logging.level.info"),
30 | BarColumn(bar_width = None),
31 | "•",
32 | TextColumn("{task.completed}/{task.total}", justify = "right"),
33 | "•",
34 | TimeElapsedColumn(),
35 | "/",
36 | TimeRemainingColumn(),
37 | transient = self.transient,
38 | )
39 | __class__.progress.start()
40 |
41 | return self
42 |
43 | def __exit__(self, exc_type: BaseException, exc_val: BaseException, exc_tb: TracebackType) -> None:
44 | for id, attr in self.tasks.items():
45 | attr["running"] = False
46 | __class__.progress.stop_task(id)
47 | __class__.progress.remove_task(id) if self.transient == True else None
48 |
49 | task_ids: set[TaskID] = {k for k, v in self.tasks.items() if v.get("running") == False}
50 | if all(v in task_ids for v in __class__.progress.task_ids):
51 | __class__.progress.stop()
52 | __class__.progress = None
53 |
54 | def new(self) -> TaskID:
55 | if __class__.progress is None:
56 | return None
57 | else:
58 | id = __class__.progress.add_task("", total = None)
59 | self.tasks[id] = {
60 | "running": True,
61 | }
62 | return id
63 |
64 | def update(self, id: TaskID, *, total: int = None, advance: int = None, completed: int = None) -> None:
65 | if __class__.progress is None:
66 | pass
67 | else:
68 | __class__.progress.update(id, total = total, advance = advance, completed = completed)
--------------------------------------------------------------------------------
/module/Response/ResponseChecker.py:
--------------------------------------------------------------------------------
1 | import re
2 | from enum import StrEnum
3 |
4 | from base.Base import Base
5 | from base.BaseLanguage import BaseLanguage
6 | from module.Text.TextHelper import TextHelper
7 | from module.Cache.CacheItem import CacheItem
8 | from module.Config import Config
9 | from module.Filter.RuleFilter import RuleFilter
10 | from module.Filter.LanguageFilter import LanguageFilter
11 | from module.TextProcessor import TextProcessor
12 |
13 | class ResponseChecker(Base):
14 |
15 | class Error(StrEnum):
16 |
17 | NONE = "NONE"
18 | UNKNOWN = "UNKNOWN"
19 | FAIL_DATA = "FAIL_DATA"
20 | FAIL_LINE_COUNT = "FAIL_LINE_COUNT"
21 | LINE_ERROR_KANA = "LINE_ERROR_KANA"
22 | LINE_ERROR_HANGEUL = "LINE_ERROR_HANGEUL"
23 | LINE_ERROR_FAKE_REPLY = "LINE_ERROR_FAKE_REPLY"
24 | LINE_ERROR_EMPTY_LINE = "LINE_ERROR_EMPTY_LINE"
25 | LINE_ERROR_SIMILARITY = "LINE_ERROR_SIMILARITY"
26 | LINE_ERROR_DEGRADATION = "LINE_ERROR_DEGRADATION"
27 |
28 | LINE_ERROR: tuple[StrEnum] = (
29 | Error.LINE_ERROR_KANA,
30 | Error.LINE_ERROR_HANGEUL,
31 | Error.LINE_ERROR_FAKE_REPLY,
32 | Error.LINE_ERROR_EMPTY_LINE,
33 | Error.LINE_ERROR_SIMILARITY,
34 | Error.LINE_ERROR_DEGRADATION,
35 | )
36 |
37 | # 重试次数阈值
38 | RETRY_COUNT_THRESHOLD: int = 2
39 |
40 | # 退化检测规则
41 | RE_DEGRADATION = re.compile(r"(.{1,3})\1{16,}", flags = re.IGNORECASE)
42 |
43 | def __init__(self, config: Config, items: list[CacheItem]) -> None:
44 | super().__init__()
45 |
46 | # 初始化
47 | self.items = items
48 | self.config = config
49 |
50 | # 检查
51 | def check(self, srcs: list[str], dsts: list[str], text_type: CacheItem.TextType) -> list[str]:
52 | # 数据解析失败
53 | if len(dsts) == 0 or all(v == "" or v == None for v in dsts):
54 | return [__class__.Error.FAIL_DATA] * len(srcs)
55 |
56 | # 当翻译任务为单条目任务,且此条目已经是第二次单独重试时,直接返回,不进行后续判断
57 | if len(self.items) == 1 and self.items[0].get_retry_count() >= __class__.RETRY_COUNT_THRESHOLD:
58 | return [__class__.Error.NONE] * len(srcs)
59 |
60 | # 行数检查
61 | if len(srcs) != len(dsts):
62 | return [__class__.Error.FAIL_LINE_COUNT] * len(srcs)
63 |
64 | # 逐行检查
65 | checks = self.check_lines(srcs, dsts, text_type)
66 | if any(v != __class__.Error.NONE for v in checks):
67 | return checks
68 |
69 | # 默认无错误
70 | return [__class__.Error.NONE] * len(srcs)
71 |
72 | # 逐行检查错误
73 | def check_lines(self, srcs: list[str], dsts: list[str], text_type: CacheItem.TextType) -> list[Error]:
74 | checks: list[__class__.Error] = []
75 | for src, dst in zip(srcs, dsts):
76 | src = src.strip()
77 | dst = dst.strip()
78 |
79 | # 原文不为空而译文为空时,判断为错误翻译
80 | if src != "" and dst == "":
81 | checks.append(__class__.Error.LINE_ERROR_EMPTY_LINE)
82 | continue
83 |
84 | # 原文内容符合规则过滤条件时,判断为正确翻译
85 | if RuleFilter.filter(src) == True:
86 | checks.append(__class__.Error.NONE)
87 | continue
88 |
89 | # 原文内容符合语言过滤条件时,判断为正确翻译
90 | if LanguageFilter.filter(src, self.config.source_language) == True:
91 | checks.append(__class__.Error.NONE)
92 | continue
93 |
94 | # 当原文中不包含重复文本但是译文中包含重复文本时,判断为 退化
95 | if __class__.RE_DEGRADATION.search(src) == None and __class__.RE_DEGRADATION.search(dst) != None:
96 | checks.append(__class__.Error.LINE_ERROR_DEGRADATION)
97 | continue
98 |
99 | # 排除代码保护规则覆盖的文本以后再继续进行检查
100 | rule: re.Pattern = TextProcessor(self.config, None).get_re_sample(
101 | custom = self.config.text_preserve_enable,
102 | text_type = text_type,
103 | )
104 | if rule is not None:
105 | src = rule.sub("", src)
106 | dst = rule.sub("", dst)
107 |
108 | # 当原文语言为日语,且译文中包含平假名或片假名字符时,判断为 假名残留
109 | if self.config.source_language == BaseLanguage.Enum.JA and (TextHelper.JA.any_hiragana(dst) or TextHelper.JA.any_katakana(dst)):
110 | checks.append(__class__.Error.LINE_ERROR_KANA)
111 | continue
112 |
113 | # 当原文语言为韩语,且译文中包含谚文字符时,判断为 谚文残留
114 | if self.config.source_language == BaseLanguage.Enum.KO and TextHelper.KO.any_hangeul(dst):
115 | checks.append(__class__.Error.LINE_ERROR_HANGEUL)
116 | continue
117 |
118 | # 判断是否包含或相似
119 | if src in dst or dst in src or TextHelper.check_similarity_by_jaccard(src, dst) > 0.80 == True:
120 | # 日翻中时,只有译文至少包含一个平假名或片假名字符时,才判断为 相似
121 | if self.config.source_language == BaseLanguage.Enum.JA and self.config.target_language == BaseLanguage.Enum.ZH:
122 | if TextHelper.JA.any_hiragana(dst) or TextHelper.JA.any_katakana(dst):
123 | checks.append(__class__.Error.LINE_ERROR_SIMILARITY)
124 | continue
125 | # 韩翻中时,只有译文至少包含一个谚文字符时,才判断为 相似
126 | elif self.config.source_language == BaseLanguage.Enum.KO and self.config.target_language == BaseLanguage.Enum.ZH:
127 | if TextHelper.KO.any_hangeul(dst):
128 | checks.append(__class__.Error.LINE_ERROR_SIMILARITY)
129 | continue
130 | # 其他情况,只要原文译文相同或相似就可以判断为 相似
131 | else:
132 | checks.append(__class__.Error.LINE_ERROR_SIMILARITY)
133 | continue
134 |
135 | # 默认为无错误
136 | checks.append(__class__.Error.NONE)
137 |
138 | # 返回结果
139 | return checks
--------------------------------------------------------------------------------
/module/Response/ResponseDecoder.py:
--------------------------------------------------------------------------------
1 | import json_repair as repair
2 |
3 | from base.Base import Base
4 |
5 | class ResponseDecoder(Base):
6 |
7 | def __init__(self) -> None:
8 | super().__init__()
9 |
10 | # 解析文本
11 | def decode(self, response: str) -> tuple[list[str], list[dict[str, str]]]:
12 | dsts: list[str] = []
13 | glossarys: list[dict[str, str]] = []
14 |
15 | # 按行解析失败时,尝试按照普通 JSON 字典进行解析
16 | for line in response.splitlines():
17 | json_data = repair.loads(line)
18 | if isinstance(json_data, dict):
19 | # 翻译结果
20 | if len(json_data) == 1:
21 | _, v = list(json_data.items())[0]
22 | if isinstance(v, str):
23 | dsts.append(v if isinstance(v, str) else "")
24 |
25 | # 术语表条目
26 | if len(json_data) == 3:
27 | if any(v in json_data for v in ("src", "dst", "gender")):
28 | src: str = json_data.get("src")
29 | dst: str = json_data.get("dst")
30 | gender: str = json_data.get("gender")
31 | glossarys.append(
32 | {
33 | "src": src if isinstance(src, str) else "",
34 | "dst": dst if isinstance(dst, str) else "",
35 | "info": gender if isinstance(gender, str) else "",
36 | }
37 | )
38 |
39 | # 按行解析失败时,尝试按照普通 JSON 字典进行解析
40 | if len(dsts) == 0:
41 | json_data = repair.loads(response)
42 | if isinstance(json_data, dict):
43 | for k, v in json_data.items():
44 | if isinstance(v, str):
45 | dsts.append(v if isinstance(v, str) else "")
46 |
47 | # 返回默认值
48 | return dsts, glossarys
--------------------------------------------------------------------------------
/module/RubyCleaner.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | class RubyCleaner():
4 |
5 | RULE: tuple[re.Pattern] = (
6 | # (漢字/かんじ)
7 | (re.compile(r'\((.+)/.+\)', flags = re.IGNORECASE), r"\1"),
8 | # [漢字/かんじ]
9 | (re.compile(r'\[(.+)/.+\]', flags = re.IGNORECASE), r"\1"),
10 | # |漢字[かんじ]
11 | (re.compile(r'\|(.+?)\[.+?\]', flags = re.IGNORECASE), r"\1"),
12 | # WOLF - \r[漢字,かんじ]
13 | (re.compile(r'\\r\[(.+?),.+?\]', flags = re.IGNORECASE), r"\1"),
14 | # [r_かんじ][ch_漢字]
15 | (re.compile(r'\[r_.+?\]\[ch_(.+?)\]', flags = re.IGNORECASE), r"\1"),
16 | # [ch_漢字]
17 | (re.compile(r'\[ch_(.+?)\]', flags = re.IGNORECASE), r"\1"),
18 | # 漢字
19 | (re.compile(r'(.*?)', flags = re.IGNORECASE), r"\1"),
20 | # 漢字
21 | (re.compile(r'.*?(.*?).*?', flags = re.IGNORECASE), r"\1"),
22 | # [ruby text=かんじ] [ruby text = かんじ] [ruby text="かんじ"] [ruby text = "かんじ"]
23 | (re.compile(rf'\[ruby text\s*=\s*.*?\]', flags = re.IGNORECASE), ""),
24 | )
25 |
26 | @classmethod
27 | def clean(cls, text: str) -> str:
28 | for pattern, replacement in cls.RULE:
29 | text = re.sub(pattern, replacement, text)
30 |
31 | return text
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # System
2 | PyQt-Fluent-Widgets[full]
3 | openai
4 | anthropic
5 | google-genai
6 | tiktoken
7 | opencc
8 | openpyxl
9 | lxml
10 | beautifulsoup4
11 |
12 | # Tools
13 | rich
14 | httpx
15 | json-repair
16 | charset-normalizer
--------------------------------------------------------------------------------
/resource/7za.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neavo/LinguaGacha/1724fba0bb5442cf9109d81940e1977a4726bd07/resource/7za.exe
--------------------------------------------------------------------------------
/resource/custom_prompt/zh/01_文言文风格.txt:
--------------------------------------------------------------------------------
1 | 1、严格按照输入行数翻译,不得拆分或合并行。
2 | 2、原文中的空白符、转义符、英文代码等控制字符必须在译文中原样保留。
3 | 3、除控制字符外,完整翻译所有文本,包括拟声词、语气词和专有名词(如角色姓名)等。
4 | 4、使用文言文风格,力求言简意赅,古雅精炼,即使原文包含露骨描写,也须忠实再现,不回避不淡化。
--------------------------------------------------------------------------------
/resource/custom_prompt/zh/02_文学措辞风格.txt:
--------------------------------------------------------------------------------
1 | 1、严格按照输入行数翻译,不得拆分或合并行。
2 | 2、原文中的空白符、转义符、英文代码等控制字符必须在译文中原样保留。
3 | 3、除控制字符外,完整翻译所有文本,包括拟声词、语气词和专有名词(如角色姓名)等。
4 | 4、遵循信达雅的原则,使用典雅、精炼、富有文学色彩的措辞,即使原文包含露骨描写,也须忠实再现,不回避不淡化。
--------------------------------------------------------------------------------
/resource/glossary_preset/en/01_example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "ダリヤ",
4 | "dst": "Daria",
5 | "info": "female"
6 | },
7 | {
8 | "src": "コリウス",
9 | "dst": "Kelvis",
10 | "info": "name"
11 | },
12 | {
13 | "src": "エルギア",
14 | "dst": "Eerga",
15 | "info": "location"
16 | },
17 | {
18 | "src": "",
19 | "dst": "",
20 | "info": ""
21 | }
22 | ]
--------------------------------------------------------------------------------
/resource/glossary_preset/en/02_common_menu_text.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "スキル",
4 | "dst": "Skill",
5 | "info": ""
6 | },
7 | {
8 | "src": "アイテム",
9 | "dst": "Item",
10 | "info": ""
11 | },
12 | {
13 | "src": "ステータス",
14 | "dst": "Status",
15 | "info": ""
16 | },
17 | {
18 | "src": "大事なもの",
19 | "dst": "Key Item",
20 | "info": ""
21 | },
22 | {
23 | "src": "セーブ",
24 | "dst": "Save",
25 | "info": ""
26 | },
27 | {
28 | "src": "ロード",
29 | "dst": "Load",
30 | "info": ""
31 | },
32 | {
33 | "src": "オプション",
34 | "dst": "Option",
35 | "info": ""
36 | },
37 | {
38 | "src": "コンティニュー",
39 | "dst": "Continue",
40 | "info": ""
41 | },
42 | {
43 | "src": "ニューゲーム",
44 | "dst": "New Game",
45 | "info": ""
46 | },
47 | {
48 | "src": "タイトルへ",
49 | "dst": "Back to Title",
50 | "info": ""
51 | },
52 | {
53 | "src": "",
54 | "dst": ""
55 | }
56 | ]
--------------------------------------------------------------------------------
/resource/glossary_preset/zh/01_示例.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "ダリヤ",
4 | "dst": "达莉雅",
5 | "info": "女性"
6 | },
7 | {
8 | "src": "コリウス",
9 | "dst": "科尔维斯",
10 | "info": "名字"
11 | },
12 | {
13 | "src": "エルギア",
14 | "dst": "埃尔迦",
15 | "info": "地点"
16 | },
17 | {
18 | "src": "",
19 | "dst": ""
20 | }
21 | ]
--------------------------------------------------------------------------------
/resource/glossary_preset/zh/02_常用菜单文本.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "スキル",
4 | "dst": "技能",
5 | "info": ""
6 | },
7 | {
8 | "src": "アイテム",
9 | "dst": "道具",
10 | "info": ""
11 | },
12 | {
13 | "src": "ステータス",
14 | "dst": "状态",
15 | "info": ""
16 | },
17 | {
18 | "src": "大事なもの",
19 | "dst": "重要物品",
20 | "info": ""
21 | },
22 | {
23 | "src": "セーブ",
24 | "dst": "保存",
25 | "info": ""
26 | },
27 | {
28 | "src": "ロード",
29 | "dst": "读取",
30 | "info": ""
31 | },
32 | {
33 | "src": "オプション",
34 | "dst": "选项",
35 | "info": ""
36 | },
37 | {
38 | "src": "コンティニュー",
39 | "dst": "继续",
40 | "info": ""
41 | },
42 | {
43 | "src": "ニューゲーム",
44 | "dst": "重新开始",
45 | "info": ""
46 | },
47 | {
48 | "src": "タイトルへ",
49 | "dst": "返回标题",
50 | "info": ""
51 | },
52 | {
53 | "src": "",
54 | "dst": ""
55 | }
56 | ]
--------------------------------------------------------------------------------
/resource/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neavo/LinguaGacha/1724fba0bb5442cf9109d81940e1977a4726bd07/resource/icon.ico
--------------------------------------------------------------------------------
/resource/icon_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neavo/LinguaGacha/1724fba0bb5442cf9109d81940e1977a4726bd07/resource/icon_full.png
--------------------------------------------------------------------------------
/resource/icon_no_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neavo/LinguaGacha/1724fba0bb5442cf9109d81940e1977a4726bd07/resource/icon_no_bg.png
--------------------------------------------------------------------------------
/resource/platforms/en/0_sakura.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 0,
3 | "name": "SakuraLLM",
4 | "api_url": "http://127.0.0.1:8080",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "SakuraLLM",
9 | "model": "no_model_required",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/10_volcengine.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 10,
3 | "name": "Volcengine",
4 | "api_url": "https://ark.cn-beijing.volces.com/api/v3",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "deepseek-v3-250324",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/11_custom_google.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 11,
3 | "name": "Custom - Google",
4 | "api_url": "https://generativelanguage.googleapis.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "Google",
9 | "model": "gemini-2.0-flash",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/12_custom_openai.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 12,
3 | "name": "Custom - OpenAI",
4 | "api_url": "http://127.0.0.1:8080",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "no_model_required",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/13_custom_anthropic.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 13,
3 | "name": "Custom - Anthropic",
4 | "api_url": "https://api.anthropic.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "Anthropic",
9 | "model": "no_model_required",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/1_google.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 1,
3 | "name": "Google",
4 | "api_url": "https://generativelanguage.googleapis.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "Google",
9 | "model": "gemini-2.0-flash",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/2_openai.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 2,
3 | "name": "OpenAI",
4 | "api_url": "https://api.openai.com/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "gpt-4.1-mini",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/3_deepseek.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 3,
3 | "name": "DeepSeek",
4 | "api_url": "https://api.deepseek.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "deepseek-chat",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/4_anthropic.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 4,
3 | "name": "Anthropic",
4 | "api_url": "https://api.anthropic.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "Anthropic",
9 | "model": "claude-3-5-haiku",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/5_aliyun.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 5,
3 | "name": "Aliyun",
4 | "api_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "qwen-turbo",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/6_zhipu.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 6,
3 | "name": "ZhiPu",
4 | "api_url": "https://open.bigmodel.cn/api/paas/v4",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "glm-4-flash",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/7_yi.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 7,
3 | "name": "Yi",
4 | "api_url": "https://api.lingyiwanwu.com/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "yi-lightning",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/8_moonshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 8,
3 | "name": "MoonShot",
4 | "api_url": "https://api.moonshot.cn/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "moonshot-v1-8k",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/en/9_siliconflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 9,
3 | "name": "SiliconFlow",
4 | "api_url": "https://api.siliconflow.cn/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "deepseek-ai/DeepSeek-V3",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/0_sakura.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 0,
3 | "name": "SakuraLLM",
4 | "api_url": "http://127.0.0.1:8080",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "SakuraLLM",
9 | "model": "no_model_required",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/10_volcengine.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 10,
3 | "name": "火山引擎",
4 | "api_url": "https://ark.cn-beijing.volces.com/api/v3",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "deepseek-v3-250324",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/11_custom_google.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 11,
3 | "name": "自定义 Google 接口",
4 | "api_url": "https://generativelanguage.googleapis.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "Google",
9 | "model": "gemini-2.0-flash",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/12_custom_openai.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 12,
3 | "name": "自定义 OpenAI 接口",
4 | "api_url": "http://127.0.0.1:8080",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "no_model_required",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/13_custom_anthropic.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 13,
3 | "name": "自定义 Anthropic 接口",
4 | "api_url": "https://api.anthropic.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "Anthropic",
9 | "model": "no_model_required",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/1_google.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 1,
3 | "name": "Google",
4 | "api_url": "https://generativelanguage.googleapis.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "Google",
9 | "model": "gemini-2.0-flash",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/2_openai.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 2,
3 | "name": "OpenAI",
4 | "api_url": "https://api.openai.com/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "gpt-4.1-mini",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/3_deepseek.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 3,
3 | "name": "DeepSeek",
4 | "api_url": "https://api.deepseek.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "deepseek-chat",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/4_anthropic.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 4,
3 | "name": "Anthropic",
4 | "api_url": "https://api.anthropic.com",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "Anthropic",
9 | "model": "claude-3-5-haiku",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/5_aliyun.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 5,
3 | "name": "阿里云",
4 | "api_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "qwen-turbo",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/6_zhipu.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 6,
3 | "name": "智谱清言",
4 | "api_url": "https://open.bigmodel.cn/api/paas/v4",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "glm-4-flash",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/7_yi.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 7,
3 | "name": "零一万物",
4 | "api_url": "https://api.lingyiwanwu.com/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "yi-lightning",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/8_moonshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 8,
3 | "name": "月之暗面",
4 | "api_url": "https://api.moonshot.cn/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "moonshot-v1-8k",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/platforms/zh/9_siliconflow.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 9,
3 | "name": "硅基流动",
4 | "api_url": "https://api.siliconflow.cn/v1",
5 | "api_key": [
6 | "no_key_required"
7 | ],
8 | "api_format": "OpenAI",
9 | "model": "deepseek-ai/DeepSeek-V3",
10 | "thinking": false,
11 | "top_p": 0.95,
12 | "temperature": 0.95,
13 | "presence_penalty": 0.0,
14 | "frequency_penalty": 0.0
15 | }
--------------------------------------------------------------------------------
/resource/post_translation_replacement_preset/en/01_common.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "…。",
4 | "dst": "…"
5 | }
6 | ]
--------------------------------------------------------------------------------
/resource/post_translation_replacement_preset/en/02_leading_trailing_quotes_half_to_full.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "^\"",
4 | "dst": "「",
5 | "info": "",
6 | "regex": true
7 | },
8 | {
9 | "src": "\"$",
10 | "dst": "」",
11 | "info": "",
12 | "regex": true
13 | }
14 | ]
--------------------------------------------------------------------------------
/resource/post_translation_replacement_preset/zh/01_常用.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "…。",
4 | "dst": "…"
5 | },
6 | {
7 | "src": "学长",
8 | "dst": "前辈"
9 | },
10 | {
11 | "src": "学姐",
12 | "dst": "前辈"
13 | },
14 | {
15 | "src": "学弟",
16 | "dst": "后辈"
17 | },
18 | {
19 | "src": "学妹",
20 | "dst": "后辈"
21 | }
22 | ]
--------------------------------------------------------------------------------
/resource/post_translation_replacement_preset/zh/02_首尾半角引号转直角引号.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "^\"",
4 | "dst": "「",
5 | "info": "",
6 | "regex": true
7 | },
8 | {
9 | "src": "\"$",
10 | "dst": "」",
11 | "info": "",
12 | "regex": true
13 | }
14 | ]
--------------------------------------------------------------------------------
/resource/pre_translation_replacement_preset/en/01_example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "\\n[1]",
4 | "dst": "Daria",
5 | "regex": false
6 | },
7 | {
8 | "src": "\\[(.+?)\\]\\(.+\\)",
9 | "dst": "\\1",
10 | "regex": true
11 | }
12 | ]
--------------------------------------------------------------------------------
/resource/pre_translation_replacement_preset/zh/01_示例.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "\\n[1]",
4 | "dst": "达莉娅",
5 | "regex": false
6 | },
7 | {
8 | "src": "\\[(.+?)\\]\\(.+\\)",
9 | "dst": "\\1",
10 | "regex": true
11 | }
12 | ]
--------------------------------------------------------------------------------
/resource/prompt/en/base.txt:
--------------------------------------------------------------------------------
1 | 1.Strictly translate line by line according to the input. Do not split or merge lines.
2 | 2.Control characters in the original text such as whitespace, escape characters, English code must be preserved in the translation as they are.
3 | 3.Translate all text completely, except for control characters, including onomatopoeia, interjections, and proper nouns (such as character names), etc.
4 | 4.Maintain colloquial wording and avoid formal/written language unless necessary. Even if the original text includes explicit descriptions, they must be faithfully reproduced without avoidance or toning down.
--------------------------------------------------------------------------------
/resource/prompt/en/prefix.txt:
--------------------------------------------------------------------------------
1 | Task goal is to translate {source_language} into {target_language}:
--------------------------------------------------------------------------------
/resource/prompt/en/suffix.txt:
--------------------------------------------------------------------------------
1 | Output the translation result in the code block using JSONLINE, without extra explanation or clarification:
2 | ```jsonline
3 | {"":""}
4 | ```
--------------------------------------------------------------------------------
/resource/prompt/en/suffix_glossary.txt:
--------------------------------------------------------------------------------
1 | Use JSONLINE in the code block to output translation results and character name terms not present in the input glossary, without extra explanation or clarification:
2 | ```jsonline
3 | {"":""}
4 | {"src":"","dst":"","gender":""}
5 | ```
--------------------------------------------------------------------------------
/resource/prompt/zh/base.txt:
--------------------------------------------------------------------------------
1 | 1、严格按照输入行数翻译,不得拆分或合并行。
2 | 2、原文中的空白符、转义符、英文代码等控制字符必须在译文中原样保留。
3 | 3、除控制字符外,完整翻译所有文本,包括拟声词、语气词和专有名词(如角色姓名)等。
4 | 4、保持口语化的措辞,如非必要避免使用书面语,即使原文包含露骨描写,也须忠实再现,不回避不淡化。
--------------------------------------------------------------------------------
/resource/prompt/zh/prefix.txt:
--------------------------------------------------------------------------------
1 | 任务目标是将{source_language}翻译成{target_language}:
--------------------------------------------------------------------------------
/resource/prompt/zh/suffix.txt:
--------------------------------------------------------------------------------
1 | 使用JSONLINE在代码块中输出翻译结果,无需额外说明或解释:
2 | ```jsonline
3 | {"<序号>":"<译文文本>"}
4 | ```
--------------------------------------------------------------------------------
/resource/prompt/zh/suffix_glossary.txt:
--------------------------------------------------------------------------------
1 | 使用JSONLINE在代码块中输出翻译结果和输入术语表中没有的角色姓名术语,无需额外说明或解释:
2 | ```jsonline
3 | {"<序号>":"<译文文本>"}
4 | {"src":"<术语原文>","dst":"<术语译文>","gender":"<结合上下文判断性别:男性/女性>"}
5 | ```
--------------------------------------------------------------------------------
/resource/pyinstaller.py:
--------------------------------------------------------------------------------
1 | import os
2 | import PyInstaller.__main__
3 |
4 | cmd = [
5 | "./app.py",
6 | "--icon=./resource/icon.ico",
7 | "--clean", # Clean PyInstaller cache and remove temporary files before building
8 | # "--onedir", # Create a one-folder bundle containing an executable (default)
9 | "--onefile", # Create a one-file bundled executable
10 | "--noconfirm", # Replace output directory (default: SPECPATH/dist/SPECNAME) without asking for confirmation
11 | "--distpath=./dist/LinguaGacha", # Where to put the bundled app (default: ./dist)
12 | ]
13 |
14 | if os.path.exists("./requirements.txt"):
15 | with open("./requirements.txt", "r", encoding = "utf-8") as reader:
16 | for line in reader:
17 | if "#" not in line:
18 | cmd.append("--hidden-import=" + line.strip())
19 |
20 | PyInstaller.__main__.run(cmd)
--------------------------------------------------------------------------------
/resource/text_preserve_preset/en/kag.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "\\{[^\\{\\u4E00-\\u9FFF\\u3400-\\u4DBF\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B73F\\U0002B740-\\U0002B81F\\U0002B820-\\U0002CEAF\\u1100-\\u11FF\\uA960-\\uA97F\\uD7B0-\\uD7FF\\uAC00-\\uD7AF\\u3130-\\u318F\\u3040-\\u309F\\u30A0-\\u30FF\\uFF65-\\uFF9F\\u31F0-\\u31FF]*?\\}",
4 | "dst": "",
5 | "info": "Example - {=2.3}",
6 | "regex": false
7 | },
8 | {
9 | "src": "\\[[^\\[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B73F\\U0002B740-\\U0002B81F\\U0002B820-\\U0002CEAF\\u1100-\\u11FF\\uA960-\\uA97F\\uD7B0-\\uD7FF\\uAC00-\\uD7AF\\u3130-\\u318F\\u3040-\\u309F\\u30A0-\\u30FF\\uFF65-\\uFF9F\\u31F0-\\u31FF]*?\\]",
10 | "dst": "",
11 | "info": "Example - [renpy.version_only]",
12 | "regex": false
13 | },
14 | {
15 | "src": "
",
16 | "dst": "",
17 | "info": "Line break -
",
18 | "regex": false
19 | },
20 | {
21 | "src": "\\s",
22 | "dst": "",
23 | "info": "Whitespace - \\t \\n \\r \\f \\v",
24 | "regex": false
25 | }
26 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/en/none.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "
",
4 | "dst": "",
5 | "info": "Line break -
",
6 | "regex": false
7 | },
8 | {
9 | "src": "\\s",
10 | "dst": "",
11 | "info": "Whitespace - \\t \\n \\r \\f \\v",
12 | "regex": false
13 | }
14 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/en/renpy.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "\\{[^\\{\\u4E00-\\u9FFF\\u3400-\\u4DBF\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B73F\\U0002B740-\\U0002B81F\\U0002B820-\\U0002CEAF\\u1100-\\u11FF\\uA960-\\uA97F\\uD7B0-\\uD7FF\\uAC00-\\uD7AF\\u3130-\\u318F\\u3040-\\u309F\\u30A0-\\u30FF\\uFF65-\\uFF9F\\u31F0-\\u31FF]*?\\}",
4 | "dst": "",
5 | "info": "Example - {=2.3}",
6 | "regex": false
7 | },
8 | {
9 | "src": "\\[[^\\[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B73F\\U0002B740-\\U0002B81F\\U0002B820-\\U0002CEAF\\u1100-\\u11FF\\uA960-\\uA97F\\uD7B0-\\uD7FF\\uAC00-\\uD7AF\\u3130-\\u318F\\u3040-\\u309F\\u30A0-\\u30FF\\uFF65-\\uFF9F\\u31F0-\\u31FF]*?\\]",
10 | "dst": "",
11 | "info": "Example - [renpy.version_only]",
12 | "regex": false
13 | },
14 | {
15 | "src": "
",
16 | "dst": "",
17 | "info": "Line break -
",
18 | "regex": false
19 | },
20 | {
21 | "src": "\\s",
22 | "dst": "",
23 | "info": "Whitespace - \\t \\n \\r \\f \\v",
24 | "regex": false
25 | }
26 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/en/rpgmaker.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "<.+?:.+?>",
4 | "info": "Example - "
5 | },
6 | {
7 | "src": "en\\(.{0,8}[vs]\\[\\d+\\].{0,16}\\)",
8 | "info": "Example - en(!s[123]) en(v[123] >= 1)"
9 | },
10 | {
11 | "src": "if\\(.{0,8}[vs]\\[\\d+\\].{0,16}\\)",
12 | "info": "Example - if(!s[123]) if(v[123] >= 1)"
13 | },
14 | {
15 | "src": "[<【]{0,1}[/\\\\][a-z]{1,8}[<\\[][a-z\\d]{0,16}[>\\]][>】]{0,1}",
16 | "info": "Example - /c[xy123] \\bc[xy123] <\\bc[xy123]>【/c[xy123]】"
17 | },
18 | {
19 | "src": "%\\d+",
20 | "info": "Example - %1 %2"
21 | },
22 | {
23 | "src": "@\\d+",
24 | "info": "WOLF Character ID"
25 | },
26 | {
27 | "src": "\\\\[cus]db\\[.+?:.+?:.+?\\]",
28 | "info": "WOLF Database Variable"
29 | },
30 | {
31 | "src": "\\\\f[rbi]",
32 | "info": "Text - Reset, Bold, Italicize "
33 | },
34 | {
35 | "src": "\\\\[\\{\\}]",
36 | "info": "Font - Enlarge, Shrink"
37 | },
38 | {
39 | "src": "\\\\\\$",
40 | "info": "Open gold window"
41 | },
42 | {
43 | "src": "\\\\\\.",
44 | "info": "Wait 0.25 seconds"
45 | },
46 | {
47 | "src": "\\\\\\|",
48 | "info": "Wait 1 second"
49 | },
50 | {
51 | "src": "\\\\!",
52 | "info": "Wait for button press"
53 | },
54 | {
55 | "src": "\\\\>",
56 | "info": "Display text on the same line"
57 | },
58 | {
59 | "src": "\\\\<",
60 | "info": "Cancel display of all text"
61 | },
62 | {
63 | "src": "\\\\\\^",
64 | "info": "No wait after displaying text"
65 | },
66 | {
67 | "src": "[/\\\\][a-z]{1,8}(?=<.{0,16}>|\\[.{0,16}\\])",
68 | "info": "Part before <> [] in /C<> \\FS<> /C[] \\FS[]"
69 | },
70 | {
71 | "src": "\\\\[a-z](?=[^a-z<>\\[\\]])",
72 | "info": "Single letter escape character - \\n \\e \\I"
73 | },
74 | {
75 | "src": "
",
76 | "info": "Line break"
77 | },
78 | {
79 | "src": "\\s",
80 | "info": "Whitespace character"
81 | }
82 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/en/wolf.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "<.+?:.+?>",
4 | "info": "Example - "
5 | },
6 | {
7 | "src": "en\\(.{0,8}[vs]\\[\\d+\\].{0,16}\\)",
8 | "info": "Example - en(!s[123]) en(v[123] >= 1)"
9 | },
10 | {
11 | "src": "if\\(.{0,8}[vs]\\[\\d+\\].{0,16}\\)",
12 | "info": "Example - if(!s[123]) if(v[123] >= 1)"
13 | },
14 | {
15 | "src": "[<【]{0,1}[/\\\\][a-z]{1,8}[<\\[][a-z\\d]{0,16}[>\\]][>】]{0,1}",
16 | "info": "Example - /c[xy123] \\bc[xy123] <\\bc[xy123]>【/c[xy123]】"
17 | },
18 | {
19 | "src": "%\\d+",
20 | "info": "Example - %1 %2"
21 | },
22 | {
23 | "src": "@\\d+",
24 | "info": "WOLF Character ID"
25 | },
26 | {
27 | "src": "\\\\[cus]db\\[.+?:.+?:.+?\\]",
28 | "info": "WOLF Database Variable"
29 | },
30 | {
31 | "src": "\\\\f[rbi]",
32 | "info": "Text - Reset, Bold, Italicize "
33 | },
34 | {
35 | "src": "\\\\[\\{\\}]",
36 | "info": "Font - Enlarge, Shrink"
37 | },
38 | {
39 | "src": "\\\\\\$",
40 | "info": "Open gold window"
41 | },
42 | {
43 | "src": "\\\\\\.",
44 | "info": "Wait 0.25 seconds"
45 | },
46 | {
47 | "src": "\\\\\\|",
48 | "info": "Wait 1 second"
49 | },
50 | {
51 | "src": "\\\\!",
52 | "info": "Wait for button press"
53 | },
54 | {
55 | "src": "\\\\>",
56 | "info": "Display text on the same line"
57 | },
58 | {
59 | "src": "\\\\<",
60 | "info": "Cancel display of all text"
61 | },
62 | {
63 | "src": "\\\\\\^",
64 | "info": "No wait after displaying text"
65 | },
66 | {
67 | "src": "[/\\\\][a-z]{1,8}(?=<.{0,16}>|\\[.{0,16}\\])",
68 | "info": "Part before <> [] in /C<> \\FS<> /C[] \\FS[]"
69 | },
70 | {
71 | "src": "\\\\[a-z](?=[^a-z<>\\[\\]])",
72 | "info": "Single letter escape character - \\n \\e \\I"
73 | },
74 | {
75 | "src": "
",
76 | "info": "Line break"
77 | },
78 | {
79 | "src": "\\s",
80 | "info": "Whitespace character"
81 | }
82 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/zh/kag.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "\\{[^\\{\\u4E00-\\u9FFF\\u3400-\\u4DBF\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B73F\\U0002B740-\\U0002B81F\\U0002B820-\\U0002CEAF\\u1100-\\u11FF\\uA960-\\uA97F\\uD7B0-\\uD7FF\\uAC00-\\uD7AF\\u3130-\\u318F\\u3040-\\u309F\\u30A0-\\u30FF\\uFF65-\\uFF9F\\u31F0-\\u31FF]*?\\}",
4 | "dst": "",
5 | "info": "示例 - {=2.3}",
6 | "regex": false
7 | },
8 | {
9 | "src": "\\[[^\\[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B73F\\U0002B740-\\U0002B81F\\U0002B820-\\U0002CEAF\\u1100-\\u11FF\\uA960-\\uA97F\\uD7B0-\\uD7FF\\uAC00-\\uD7AF\\u3130-\\u318F\\u3040-\\u309F\\u30A0-\\u30FF\\uFF65-\\uFF9F\\u31F0-\\u31FF]*?\\]",
10 | "dst": "",
11 | "info": "示例 - [renpy.version_only]",
12 | "regex": false
13 | },
14 | {
15 | "src": "
",
16 | "dst": "",
17 | "info": "换行符 -
",
18 | "regex": false
19 | },
20 | {
21 | "src": "\\s",
22 | "dst": "",
23 | "info": "空白符 - \\t \\n \\r \\f \\v",
24 | "regex": false
25 | }
26 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/zh/none.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "
",
4 | "dst": "",
5 | "info": "换行符
",
6 | "regex": false
7 | },
8 | {
9 | "src": "\\s",
10 | "dst": "",
11 | "info": "空白字符",
12 | "regex": false
13 | }
14 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/zh/renpy.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "\\{[^\\{\\u4E00-\\u9FFF\\u3400-\\u4DBF\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B73F\\U0002B740-\\U0002B81F\\U0002B820-\\U0002CEAF\\u1100-\\u11FF\\uA960-\\uA97F\\uD7B0-\\uD7FF\\uAC00-\\uD7AF\\u3130-\\u318F\\u3040-\\u309F\\u30A0-\\u30FF\\uFF65-\\uFF9F\\u31F0-\\u31FF]*?\\}",
4 | "dst": "",
5 | "info": "示例 - {=2.3}",
6 | "regex": false
7 | },
8 | {
9 | "src": "\\[[^\\[\\u4E00-\\u9FFF\\u3400-\\u4DBF\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B73F\\U0002B740-\\U0002B81F\\U0002B820-\\U0002CEAF\\u1100-\\u11FF\\uA960-\\uA97F\\uD7B0-\\uD7FF\\uAC00-\\uD7AF\\u3130-\\u318F\\u3040-\\u309F\\u30A0-\\u30FF\\uFF65-\\uFF9F\\u31F0-\\u31FF]*?\\]",
10 | "dst": "",
11 | "info": "示例 - [renpy.version_only]",
12 | "regex": false
13 | },
14 | {
15 | "src": "
",
16 | "dst": "",
17 | "info": "换行符 -
",
18 | "regex": false
19 | },
20 | {
21 | "src": "\\s",
22 | "dst": "",
23 | "info": "空白符 - \\t \\n \\r \\f \\v",
24 | "regex": false
25 | }
26 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/zh/rpgmaker.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "<.+?:.+?>",
4 | "info": "示例 - "
5 | },
6 | {
7 | "src": "en\\(.{0,8}[vs]\\[\\d+\\].{0,16}\\)",
8 | "info": "示例 - en(!s[123]) en(v[123] >= 1)"
9 | },
10 | {
11 | "src": "if\\(.{0,8}[vs]\\[\\d+\\].{0,16}\\)",
12 | "info": "示例 - if(!s[123]) if(v[123] >= 1)"
13 | },
14 | {
15 | "src": "[<【]{0,1}[/\\\\][a-z]{1,8}[<\\[][a-z\\d]{0,16}[>\\]][>】]{0,1}",
16 | "info": "示例 - /c[xy123] \\bc[xy123] <\\bc[xy123]>【/c[xy123]】"
17 | },
18 | {
19 | "src": "%\\d+",
20 | "info": "示例 - %1 %2"
21 | },
22 | {
23 | "src": "@\\d+",
24 | "info": "WOLF 角色 ID"
25 | },
26 | {
27 | "src": "\\\\[cus]db\\[.+?:.+?:.+?\\]",
28 | "info": "WOLF 数据库变量"
29 | },
30 | {
31 | "src": "\\\\f[rbi]",
32 | "info": "文本 - 重置、加粗、倾斜"
33 | },
34 | {
35 | "src": "\\\\[\\{\\}]",
36 | "info": "字体 - 放大、缩小"
37 | },
38 | {
39 | "src": "\\\\\\$",
40 | "info": "打开金币框"
41 | },
42 | {
43 | "src": "\\\\\\.",
44 | "info": "等待 0.25 秒"
45 | },
46 | {
47 | "src": "\\\\\\|",
48 | "info": "等待 1.00 秒"
49 | },
50 | {
51 | "src": "\\\\!",
52 | "info": "等待按钮按下"
53 | },
54 | {
55 | "src": "\\\\>",
56 | "info": "在同一行显示文字"
57 | },
58 | {
59 | "src": "\\\\<",
60 | "info": "取消显示所有文字"
61 | },
62 | {
63 | "src": "\\\\\\^",
64 | "info": "显示文本后不需要等待"
65 | },
66 | {
67 | "src": "[/\\\\][a-z]{1,8}(?=<.{0,16}>|\\[.{0,16}\\])",
68 | "info": "/C<> \\FS<> /C[] \\FS[] 中 <> [] 前的部分"
69 | },
70 | {
71 | "src": "\\\\[a-z](?=[^a-z<>\\[\\]])",
72 | "info": "单字母转义符 - \\n \\e \\I"
73 | },
74 | {
75 | "src": "
",
76 | "info": "换行符"
77 | },
78 | {
79 | "src": "\\s",
80 | "info": "空白字符"
81 | }
82 | ]
--------------------------------------------------------------------------------
/resource/text_preserve_preset/zh/wolf.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "src": "<.+?:.+?>",
4 | "info": "示例 - "
5 | },
6 | {
7 | "src": "en\\(.{0,8}[vs]\\[\\d+\\].{0,16}\\)",
8 | "info": "示例 - en(!s[123]) en(v[123] >= 1)"
9 | },
10 | {
11 | "src": "if\\(.{0,8}[vs]\\[\\d+\\].{0,16}\\)",
12 | "info": "示例 - if(!s[123]) if(v[123] >= 1)"
13 | },
14 | {
15 | "src": "[<【]{0,1}[/\\\\][a-z]{1,8}[<\\[][a-z\\d]{0,16}[>\\]][>】]{0,1}",
16 | "info": "示例 - /c[xy123] \\bc[xy123] <\\bc[xy123]>【/c[xy123]】"
17 | },
18 | {
19 | "src": "%\\d+",
20 | "info": "示例 - %1 %2"
21 | },
22 | {
23 | "src": "@\\d+",
24 | "info": "WOLF 角色 ID"
25 | },
26 | {
27 | "src": "\\\\[cus]db\\[.+?:.+?:.+?\\]",
28 | "info": "WOLF 数据库变量"
29 | },
30 | {
31 | "src": "\\\\f[rbi]",
32 | "info": "文本 - 重置、加粗、倾斜"
33 | },
34 | {
35 | "src": "\\\\[\\{\\}]",
36 | "info": "字体 - 放大、缩小"
37 | },
38 | {
39 | "src": "\\\\\\$",
40 | "info": "打开金币框"
41 | },
42 | {
43 | "src": "\\\\\\.",
44 | "info": "等待 0.25 秒"
45 | },
46 | {
47 | "src": "\\\\\\|",
48 | "info": "等待 1.00 秒"
49 | },
50 | {
51 | "src": "\\\\!",
52 | "info": "等待按钮按下"
53 | },
54 | {
55 | "src": "\\\\>",
56 | "info": "在同一行显示文字"
57 | },
58 | {
59 | "src": "\\\\<",
60 | "info": "取消显示所有文字"
61 | },
62 | {
63 | "src": "\\\\\\^",
64 | "info": "显示文本后不需要等待"
65 | },
66 | {
67 | "src": "[/\\\\][a-z]{1,8}(?=<.{0,16}>|\\[.{0,16}\\])",
68 | "info": "/C<> \\FS<> /C[] \\FS[] 中 <> [] 前的部分"
69 | },
70 | {
71 | "src": "\\\\[a-z](?=[^a-z<>\\[\\]])",
72 | "info": "单字母转义符 - \\n \\e \\I"
73 | },
74 | {
75 | "src": "
",
76 | "info": "换行符"
77 | },
78 | {
79 | "src": "\\s",
80 | "info": "空白字符"
81 | }
82 | ]
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | v0.29.1
--------------------------------------------------------------------------------
/widget/ComboBoxCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtWidgets import QHBoxLayout
5 | from PyQt5.QtWidgets import QVBoxLayout
6 | from qfluentwidgets import CardWidget
7 | from qfluentwidgets import ComboBox
8 | from qfluentwidgets import CaptionLabel
9 | from qfluentwidgets import StrongBodyLabel
10 |
11 | class ComboBoxCard(CardWidget):
12 |
13 | def __init__(self, title: str, description: str, items: list, init: Callable = None, current_changed: Callable = None) -> None:
14 | super().__init__(None)
15 |
16 | # 设置容器
17 | self.setBorderRadius(4)
18 | self.hbox = QHBoxLayout(self)
19 | self.hbox.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
20 |
21 | # 文本控件
22 | self.vbox = QVBoxLayout()
23 |
24 | self.title_label = StrongBodyLabel(title, self)
25 | self.description_label = CaptionLabel(description, self)
26 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
27 |
28 | self.vbox.addWidget(self.title_label)
29 | self.vbox.addWidget(self.description_label)
30 | self.hbox.addLayout(self.vbox)
31 |
32 | # 填充
33 | self.hbox.addStretch(1)
34 |
35 | # 下拉框控件
36 | self.combo_box = ComboBox(self)
37 | self.combo_box.addItems(items)
38 | self.hbox.addWidget(self.combo_box)
39 |
40 | if callable(init):
41 | init(self)
42 |
43 | if callable(current_changed):
44 | self.combo_box.currentIndexChanged.connect(lambda _: current_changed(self))
45 |
46 | def get_combo_box(self) -> ComboBox:
47 | return self.combo_box
--------------------------------------------------------------------------------
/widget/CommandBarCard.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import Qt
2 | from PyQt5.QtWidgets import QHBoxLayout
3 |
4 | from qfluentwidgets import CardWidget
5 | from qfluentwidgets import Action
6 | from qfluentwidgets import CommandBar
7 | from qfluentwidgets.components.widgets.command_bar import CommandButton
8 |
9 | class CommandBarCard(CardWidget):
10 |
11 | def __init__(self) -> None:
12 | super().__init__(None)
13 |
14 | # 设置容器
15 | self.setBorderRadius(4)
16 | self.hbox = QHBoxLayout(self)
17 | self.hbox.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
18 |
19 | # 文本控件
20 | self.command_bar = CommandBar()
21 | self.command_bar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
22 | self.hbox.addWidget(self.command_bar)
23 |
24 | def add_widget(self, widget) -> None:
25 | return self.hbox.addWidget(widget)
26 |
27 | def add_stretch(self, stretch: int) -> None:
28 | self.hbox.addStretch(stretch)
29 |
30 | def add_spacing(self, spacing: int) -> None:
31 | self.hbox.addSpacing(spacing)
32 |
33 | def add_action(self, action: Action) -> CommandButton:
34 | return self.command_bar.addAction(action)
35 |
36 | def add_separator(self) -> None:
37 | self.command_bar.addSeparator()
38 |
39 | def set_minimum_width(self, min_width: int) -> None:
40 | self.command_bar.setMinimumWidth(min_width)
--------------------------------------------------------------------------------
/widget/EmptyCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtWidgets import QHBoxLayout
5 | from PyQt5.QtWidgets import QVBoxLayout
6 | from qfluentwidgets import CardWidget
7 | from qfluentwidgets import CaptionLabel
8 | from qfluentwidgets import StrongBodyLabel
9 |
10 | class EmptyCard(CardWidget):
11 |
12 | def __init__(self, title: str, description: str, init: Callable = None) -> None:
13 | super().__init__(None)
14 |
15 | # 设置容器
16 | self.setBorderRadius(4)
17 | self.root = QHBoxLayout(self)
18 | self.root.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
19 |
20 | # 文本控件
21 | self.vbox = QVBoxLayout()
22 | self.root.addLayout(self.vbox)
23 |
24 | self.title_label = StrongBodyLabel(title, self)
25 | self.vbox.addWidget(self.title_label)
26 |
27 | self.description_label = CaptionLabel(description, self)
28 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
29 | self.vbox.addWidget(self.description_label)
30 |
31 | # 填充
32 | self.root.addStretch(1)
33 |
34 | if callable(init):
35 | init(self)
36 |
37 | def get_title_label(self) -> StrongBodyLabel:
38 | return self.title_label
39 |
40 | def get_description_label(self) -> CaptionLabel:
41 | return self.description_label
42 |
43 | def add_widget(self, widget) -> None:
44 | self.root.addWidget(widget)
45 |
46 | def add_spacing(self, space: int) -> None:
47 | self.root.addSpacing(space)
48 |
49 | def remove_title(self) -> None:
50 | self.vbox.removeWidget(self.title_label)
--------------------------------------------------------------------------------
/widget/FlowCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtWidgets import QWidget
5 | from PyQt5.QtWidgets import QHBoxLayout
6 | from PyQt5.QtWidgets import QVBoxLayout
7 | from qfluentwidgets import CardWidget
8 | from qfluentwidgets import FlowLayout
9 | from qfluentwidgets import CaptionLabel
10 | from qfluentwidgets import StrongBodyLabel
11 |
12 | from widget.Separator import Separator
13 |
14 | class FlowCard(CardWidget):
15 |
16 | def __init__(self, parent: QWidget, title: str, description: str, init: Callable = None, clicked: Callable = None) -> None:
17 | super().__init__(parent)
18 |
19 | # 设置容器
20 | self.setBorderRadius(4)
21 | self.root = QVBoxLayout(self)
22 | self.root.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
23 |
24 | # 添加头部容器
25 | self.head_container = QWidget(self)
26 | self.head_hbox = QHBoxLayout(self.head_container)
27 | self.head_hbox.setSpacing(8)
28 | self.head_hbox.setContentsMargins(0, 0, 0, 0)
29 | self.root.addWidget(self.head_container)
30 |
31 | # 添加文本容器
32 | self.text_container = QWidget(self)
33 | self.text_vbox = QVBoxLayout(self.text_container)
34 | self.text_vbox.setSpacing(8)
35 | self.text_vbox.setContentsMargins(0, 0, 0, 0)
36 | self.head_hbox.addWidget(self.text_container)
37 |
38 | self.title_label = StrongBodyLabel(title, self)
39 | self.text_vbox.addWidget(self.title_label)
40 |
41 | self.description_label = CaptionLabel(description, self)
42 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
43 | self.text_vbox.addWidget(self.description_label)
44 |
45 | # 填充
46 | self.head_hbox.addStretch(1)
47 |
48 | # 添加分割线
49 | self.root.addWidget(Separator(self))
50 |
51 | # 添加流式布局容器
52 | self.flow_container = QWidget(self)
53 | self.flow_layout = FlowLayout(self.flow_container, needAni = False)
54 | self.flow_layout.setSpacing(8)
55 | self.flow_layout.setContentsMargins(0, 0, 0, 0)
56 | self.root.addWidget(self.flow_container)
57 |
58 | if callable(init):
59 | init(self)
60 |
61 | if callable(clicked):
62 | self.clicked.connect(lambda : clicked(self))
63 |
64 | def get_title_label(self) -> StrongBodyLabel:
65 | return self.title_label
66 |
67 | def get_description_label(self) -> CaptionLabel:
68 | return self.description_label
69 |
70 | # 添加控件
71 | def add_widget(self, widget: QWidget) -> None:
72 | self.flow_layout.addWidget(widget)
73 |
74 | # 添加控件到头部
75 | def add_widget_to_head(self, widget: QWidget) -> None:
76 | self.head_hbox.addWidget(widget)
77 |
78 | # 移除所有控件并且删除他们
79 | def take_all_widgets(self) -> None:
80 | self.flow_layout.takeAllWidgets()
--------------------------------------------------------------------------------
/widget/GroupCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtWidgets import QWidget
5 | from PyQt5.QtWidgets import QVBoxLayout
6 | from qfluentwidgets import CardWidget
7 | from qfluentwidgets import CaptionLabel
8 | from qfluentwidgets import StrongBodyLabel
9 |
10 | from widget.Separator import Separator
11 |
12 | class GroupCard(CardWidget):
13 |
14 | def __init__(self, parent: QWidget, title: str, description: str, init: Callable = None, clicked: Callable = None) -> None:
15 | super().__init__(parent)
16 |
17 | # 设置容器
18 | self.setBorderRadius(4)
19 | self.root = QVBoxLayout(self)
20 | self.root.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
21 |
22 | self.title_label = StrongBodyLabel(title, self)
23 | self.root.addWidget(self.title_label)
24 |
25 | self.description_label = CaptionLabel(description, self)
26 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
27 | self.root.addWidget(self.description_label)
28 |
29 | # 添加分割线
30 | self.root.addWidget(Separator(self))
31 |
32 | # 添加流式布局容器
33 | self.vbox_container = QWidget(self)
34 | self.vbox = QVBoxLayout(self.vbox_container)
35 | self.vbox.setSpacing(0)
36 | self.vbox.setContentsMargins(0, 0, 0, 0)
37 | self.root.addWidget(self.vbox_container)
38 |
39 | if callable(init):
40 | init(self)
41 |
42 | if callable(clicked):
43 | self.clicked.connect(lambda : clicked(self))
44 |
45 | # 添加控件
46 | def add_widget(self, widget) -> None:
47 | self.vbox.addWidget(widget)
--------------------------------------------------------------------------------
/widget/LineEditCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtWidgets import QWidget
5 | from PyQt5.QtWidgets import QHBoxLayout
6 | from PyQt5.QtWidgets import QVBoxLayout
7 | from qfluentwidgets import CardWidget
8 | from qfluentwidgets import LineEdit
9 | from qfluentwidgets import CaptionLabel
10 | from qfluentwidgets import StrongBodyLabel
11 |
12 | class LineEditCard(CardWidget):
13 |
14 | def __init__(self, title: str, description: str, init: Callable = None, text_changed: Callable = None) -> None:
15 | super().__init__(None)
16 |
17 | # 设置容器
18 | self.setBorderRadius(4)
19 | self.root = QHBoxLayout(self)
20 | self.root.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
21 |
22 | # 文本控件
23 | self.vbox_container = QWidget(self)
24 | self.vbox = QVBoxLayout(self.vbox_container)
25 | self.vbox.setSpacing(0)
26 | self.vbox.setContentsMargins(0, 0, 0, 0)
27 | self.root.addWidget(self.vbox_container)
28 |
29 | self.title_label = StrongBodyLabel(title, self)
30 | self.vbox.addWidget(self.title_label)
31 |
32 | self.description_label = CaptionLabel(description, self)
33 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
34 | self.vbox.addWidget(self.description_label)
35 |
36 | # 填充
37 | self.root.addStretch(1)
38 |
39 | # 添加控件
40 | self.line_edit = LineEdit()
41 | self.line_edit.setFixedWidth(192)
42 | self.line_edit.setClearButtonEnabled(True)
43 | self.root.addWidget(self.line_edit)
44 |
45 | if callable(init):
46 | init(self)
47 |
48 | if callable(text_changed):
49 | self.line_edit.textChanged.connect(lambda text: text_changed(self, text))
50 |
51 | def get_line_edit(self) -> LineEdit:
52 | return self.line_edit
53 |
54 | # 添加控件
55 | def add_widget(self, widget) -> None:
56 | self.root.addWidget(widget)
57 |
58 | # 添加间隔
59 | def add_spacing(self, spacing: int) -> None:
60 | self.root.addSpacing(spacing)
--------------------------------------------------------------------------------
/widget/LineEditMessageBox.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtWidgets import QWidget
4 | from qfluentwidgets import LineEdit
5 | from qfluentwidgets import MessageBoxBase
6 | from qfluentwidgets import StrongBodyLabel
7 |
8 | from module.Localizer.Localizer import Localizer
9 |
10 | class LineEditMessageBox(MessageBoxBase):
11 |
12 | def __init__(self, parent: QWidget, title: str, message_box_close: Callable = None) -> None:
13 | super().__init__(parent = parent)
14 |
15 | # 初始化
16 | self.message_box_close = message_box_close
17 |
18 | # 设置框体
19 | self.yesButton.setText(Localizer.get().confirm)
20 | self.cancelButton.setText(Localizer.get().cancel)
21 |
22 | # 设置主布局
23 | self.viewLayout.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
24 |
25 | # 标题
26 | self.title_label = StrongBodyLabel(title, self)
27 | self.viewLayout.addWidget(self.title_label)
28 |
29 | # 输入框
30 | self.line_edit = LineEdit(self)
31 | self.line_edit.setMinimumWidth(384)
32 | self.line_edit.setClearButtonEnabled(True)
33 | self.viewLayout.addWidget(self.line_edit)
34 |
35 | # 重写验证方法
36 | def validate(self) -> bool:
37 | if self.line_edit.text().strip() != "":
38 | if callable(self.message_box_close):
39 | self.message_box_close(self, self.line_edit.text())
40 |
41 | return True
42 |
43 | def get_line_edit(self) -> LineEdit:
44 | return self.line_edit
--------------------------------------------------------------------------------
/widget/PushButtonCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtWidgets import QHBoxLayout
5 | from PyQt5.QtWidgets import QVBoxLayout
6 | from qfluentwidgets import CardWidget
7 | from qfluentwidgets import PushButton
8 | from qfluentwidgets import CaptionLabel
9 | from qfluentwidgets import StrongBodyLabel
10 |
11 | class PushButtonCard(CardWidget):
12 |
13 | def __init__(self, title: str, description: str, init: Callable = None, clicked: Callable = None) -> None:
14 | super().__init__(None)
15 |
16 | # 设置容器
17 | self.setBorderRadius(4)
18 | self.root = QHBoxLayout(self)
19 | self.root.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
20 |
21 | # 文本控件
22 | self.vbox = QVBoxLayout()
23 |
24 | self.title_label = StrongBodyLabel(title, self)
25 | self.description_label = CaptionLabel(description, self)
26 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
27 |
28 | self.vbox.addWidget(self.title_label)
29 | self.vbox.addWidget(self.description_label)
30 | self.root.addLayout(self.vbox)
31 |
32 | # 填充
33 | self.root.addStretch(1)
34 |
35 | # 添加控件
36 | self.push_button = PushButton("", self)
37 | self.root.addWidget(self.push_button)
38 |
39 | if callable(init):
40 | init(self)
41 |
42 | if callable(clicked):
43 | self.push_button.clicked.connect(lambda _: clicked(self))
44 |
45 | def add_widget(self, widget) -> None:
46 | return self.root.addWidget(widget)
47 |
48 | def add_spacing(self, spacing: int) -> None:
49 | self.root.addSpacing(spacing)
50 |
51 | def add_stretch(self, stretch: int) -> None:
52 | self.root.addStretch(stretch)
53 |
54 | def get_title_label(self) -> StrongBodyLabel:
55 | return self.title_label
56 |
57 | def get_description_label(self) -> CaptionLabel:
58 | return self.description_label
59 |
60 | def get_push_button(self) -> PushButton:
61 | return self.push_button
--------------------------------------------------------------------------------
/widget/SearchCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtWidgets import QHBoxLayout
4 | from PyQt5.QtWidgets import QWidget
5 | from qfluentwidgets import CardWidget
6 | from qfluentwidgets import FluentIcon
7 | from qfluentwidgets import LineEdit
8 | from qfluentwidgets import TransparentPushButton
9 |
10 | from module.Localizer.Localizer import Localizer
11 |
12 | class SearchCard(CardWidget):
13 |
14 | def __init__(self, parent: QWidget) -> None:
15 | super().__init__(parent)
16 |
17 | # 设置容器
18 | self.setBorderRadius(4)
19 | self.root = QHBoxLayout(self)
20 | self.root.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
21 |
22 | # 添加控件
23 | self.line_edit = LineEdit()
24 | self.line_edit.setFixedWidth(256)
25 | self.line_edit.setPlaceholderText(Localizer.get().placeholder)
26 | self.line_edit.setClearButtonEnabled(True)
27 | self.root.addWidget(self.line_edit)
28 |
29 | self.next = TransparentPushButton(self)
30 | self.next.setIcon(FluentIcon.SCROLL)
31 | self.next.setText(Localizer.get().next)
32 | self.root.addWidget(self.next)
33 |
34 | # 填充
35 | self.root.addStretch(1)
36 |
37 | # 返回
38 | self.back = TransparentPushButton(self)
39 | self.back.setIcon(FluentIcon.EMBED)
40 | self.back.setText(Localizer.get().back)
41 | self.root.addWidget(self.back)
42 |
43 | def on_next_clicked(self, clicked: Callable) -> None:
44 | self.next.clicked.connect(lambda: clicked(self))
45 |
46 | def on_back_clicked(self, clicked: Callable) -> None:
47 | self.back.clicked.connect(lambda: clicked(self))
48 |
49 | def get_line_edit(self) -> LineEdit:
50 | return self.line_edit
--------------------------------------------------------------------------------
/widget/Separator.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QWidget
2 | from PyQt5.QtWidgets import QVBoxLayout
3 |
4 | class Separator(QWidget):
5 |
6 | def __init__(self, parent: QWidget = None, horizontal: bool = False) -> None:
7 | super().__init__(parent)
8 |
9 | if horizontal == True:
10 | # 设置容器
11 | self.root = QVBoxLayout(self)
12 | self.root.setContentsMargins(4, 0, 4, 0) # 左、上、右、下
13 |
14 | # 添加分割线
15 | line = QWidget(self)
16 | line.setFixedWidth(1)
17 | line.setStyleSheet("QWidget { background-color: #C0C0C0; }")
18 | self.root.addWidget(line)
19 | else:
20 | # 设置容器
21 | self.root = QVBoxLayout(self)
22 | self.root.setContentsMargins(0, 4, 0, 4) # 左、上、右、下
23 |
24 | # 添加分割线
25 | line = QWidget(self)
26 | line.setFixedHeight(1)
27 | line.setStyleSheet("QWidget { background-color: #C0C0C0; }")
28 | self.root.addWidget(line)
--------------------------------------------------------------------------------
/widget/SliderCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtCore import Qt
4 | from PyQt5.QtGui import QColor
5 | from PyQt5.QtWidgets import QWidget
6 | from PyQt5.QtWidgets import QHBoxLayout
7 | from PyQt5.QtWidgets import QVBoxLayout
8 | from qfluentwidgets import Slider
9 | from qfluentwidgets import CardWidget
10 | from qfluentwidgets import CaptionLabel
11 | from qfluentwidgets import StrongBodyLabel
12 |
13 | class SliderCard(CardWidget):
14 |
15 | def __init__(self, title: str, description: str, init: Callable = None, slider_released: Callable = None) -> None:
16 | super().__init__(None)
17 |
18 | # 设置容器
19 | self.setBorderRadius(4)
20 | self.root = QHBoxLayout(self)
21 | self.root.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
22 |
23 | # 文本控件
24 | self.vbox = QVBoxLayout()
25 |
26 | self.title_label = StrongBodyLabel(title, self)
27 | self.description_label = CaptionLabel(description, self)
28 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
29 |
30 | self.vbox.addWidget(self.title_label)
31 | self.vbox.addWidget(self.description_label)
32 | self.root.addLayout(self.vbox)
33 |
34 | # 填充
35 | self.root.addStretch(1)
36 |
37 | # 添加控件
38 | self.slider = Slider(Qt.Orientation.Horizontal)
39 | self.slider.setFixedWidth(256)
40 | self.value_label = StrongBodyLabel(title, self)
41 | self.value_label.setFixedWidth(48)
42 | self.value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
43 | self.root.addWidget(self.slider)
44 | self.root.addWidget(self.value_label)
45 |
46 | if callable(init):
47 | init(self)
48 |
49 | if callable(slider_released):
50 | self.slider.sliderReleased.connect(lambda: slider_released(self))
51 |
52 | def get_slider(self) -> Slider:
53 | return self.slider
54 |
55 | def get_value_label(self) -> CaptionLabel:
56 | return self.value_label
57 |
58 | def add_widget(self, widget: QWidget) -> None:
59 | self.root.addWidget(widget)
60 |
61 | def set_slider_visible(self, enabled: bool) -> None:
62 | self.slider.setVisible(enabled)
63 | self.value_label.setVisible(enabled)
--------------------------------------------------------------------------------
/widget/SpinCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtWidgets import QHBoxLayout
5 | from PyQt5.QtWidgets import QVBoxLayout
6 | from qfluentwidgets import CardWidget
7 | from qfluentwidgets import SpinBox
8 | from qfluentwidgets import CaptionLabel
9 | from qfluentwidgets import StrongBodyLabel
10 |
11 | class SpinCard(CardWidget):
12 |
13 | def __init__(self, title: str, description: str, init: Callable = None, value_changed: Callable = None) -> None:
14 | super().__init__(None)
15 |
16 | # 设置容器
17 | self.setBorderRadius(4)
18 | self.container = QHBoxLayout(self)
19 | self.container.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
20 |
21 | # 文本控件
22 | self.vbox = QVBoxLayout()
23 |
24 | self.title_label = StrongBodyLabel(title, self)
25 | self.description_label = CaptionLabel(description, self)
26 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
27 |
28 | self.vbox.addWidget(self.title_label)
29 | self.vbox.addWidget(self.description_label)
30 | self.container.addLayout(self.vbox)
31 |
32 | # 填充
33 | self.container.addStretch(1)
34 |
35 | # 微调框控件
36 | self.spin_box = SpinBox()
37 | self.container.addWidget(self.spin_box)
38 |
39 | if callable(init):
40 | init(self)
41 |
42 | if callable(value_changed):
43 | self.spin_box.valueChanged.connect(lambda _: value_changed(self))
44 |
45 | def get_spin_box(self) -> SpinBox:
46 | return self.spin_box
--------------------------------------------------------------------------------
/widget/SwitchButtonCard.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from PyQt5.QtGui import QColor
4 | from PyQt5.QtWidgets import QHBoxLayout
5 | from PyQt5.QtWidgets import QVBoxLayout
6 | from qfluentwidgets import CardWidget
7 | from qfluentwidgets import SwitchButton
8 | from qfluentwidgets import CaptionLabel
9 | from qfluentwidgets import StrongBodyLabel
10 |
11 | class SwitchButtonCard(CardWidget):
12 |
13 | def __init__(self, title: str, description: str, init: Callable = None, checked_changed: Callable = None) -> None:
14 | super().__init__(None)
15 |
16 | # 设置容器
17 | self.setBorderRadius(4)
18 | self.hbox = QHBoxLayout(self)
19 | self.hbox.setContentsMargins(16, 16, 16, 16) # 左、上、右、下
20 |
21 | # 文本控件
22 | self.vbox = QVBoxLayout()
23 |
24 | self.title_label = StrongBodyLabel(title, self)
25 | self.description_label = CaptionLabel(description, self)
26 | self.description_label.setTextColor(QColor(96, 96, 96), QColor(160, 160, 160))
27 |
28 | self.vbox.addWidget(self.title_label)
29 | self.vbox.addWidget(self.description_label)
30 | self.hbox.addLayout(self.vbox)
31 |
32 | # 填充
33 | self.hbox.addStretch(1)
34 |
35 | # 添加控件
36 | self.switch_button = SwitchButton()
37 | self.switch_button.setOnText("")
38 | self.switch_button.setOffText("")
39 | self.hbox.addWidget(self.switch_button)
40 |
41 | if callable(init):
42 | init(self)
43 |
44 | if callable(checked_changed):
45 | self.switch_button.checkedChanged.connect(lambda _: checked_changed(self))
46 |
47 | def get_switch_button(self) -> SwitchButton:
48 | return self.switch_button
--------------------------------------------------------------------------------
/widget/WaveformWidget.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from PyQt5.QtCore import Qt
4 | from PyQt5.QtCore import QTimer
5 | from PyQt5.QtGui import QFont
6 | from PyQt5.QtGui import QPainter
7 | from PyQt5.QtWidgets import QLabel
8 |
9 | from qfluentwidgets import isDarkTheme
10 |
11 | class WaveformWidget(QLabel):
12 |
13 | def __init__(self, *args, **kwargs):
14 | super().__init__(*args, **kwargs)
15 |
16 | # 自动填充背景
17 | # self.setAutoFillBackground(True)
18 |
19 | # 设置字体
20 | self.font = QFont("Consolas", 8)
21 |
22 | # 每个字符所占用的空间
23 | self.point_size = self.font.pointSize()
24 |
25 | # 历史数据
26 | self.history = [0]
27 |
28 | # 设置矩阵大小
29 | self.set_matrix_size(50, 20)
30 |
31 | # 刷新率
32 | self.refresh_rate = 2
33 |
34 | # 最近一次添加数据的时间
35 | self.last_add_value_time = 0
36 |
37 | # 开始刷新
38 | self.timer = QTimer(self)
39 | self.timer.timeout.connect(self.tick)
40 | self.timer.start(int(1000 / self.refresh_rate))
41 |
42 | # 刷新
43 | def tick(self):
44 | if time.time() - self.last_add_value_time >= (1 / self.refresh_rate):
45 | # 如果周期内数据没有更新,则重复最后一个数据
46 | self.repeat()
47 |
48 | # 刷新界面
49 | self.update()
50 |
51 | def paintEvent(self, event):
52 | # 初始化画笔
53 | painter = QPainter(self)
54 | painter.setFont(self.font)
55 | painter.setPen(Qt.GlobalColor.white if isDarkTheme() else Qt.GlobalColor.black)
56 |
57 | # 归一化以增大波形起伏
58 | min_val = min(self.history)
59 | max_val = max(self.history)
60 | if max_val - min_val == 0 and self.history[0] == 0:
61 | values = [0 for i in self.history]
62 | elif max_val - min_val == 0 and self.history[0] != 0:
63 | values = [1 for i in self.history]
64 | else:
65 | values = [(v - min_val) / (max_val - min_val) for v in self.history]
66 |
67 | # 生成文本
68 | lines = []
69 | for value in reversed(values):
70 | lines.append("▨" * int(value * (self.matrix_height - 1) + 1))
71 |
72 | # 绘制文本
73 | x = self.max_width - self.point_size
74 | for line in lines:
75 | y = self.max_height
76 |
77 | for point in line:
78 | painter.drawText(x, y, point)
79 | y = y - self.point_size
80 |
81 | x = x - self.point_size
82 |
83 | # 重复最后的数据
84 | def repeat(self):
85 | self.add_value(self.history[-1] if len(self.history) > 0 else 0)
86 |
87 | # 添加数据
88 | def add_value(self, value: int):
89 | if len(self.history) >= self.matrix_width:
90 | self.history.pop(0)
91 |
92 | self.history.append(value)
93 |
94 | # 记录下最后添加数据的时间
95 | self.last_add_value_time = time.time()
96 |
97 | # 设置矩阵大小
98 | def set_matrix_size(self, width: int, height: int):
99 | self.matrix_width = width
100 | self.matrix_height = height
101 | self.max_width = self.matrix_width * self.point_size
102 | self.max_height = self.matrix_height * self.point_size
103 | self.setFixedSize(self.max_width, self.max_height)
104 | self.history = [0 for i in range(self.matrix_width)]
--------------------------------------------------------------------------------