├── .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 | # 漢字かんじChinese character 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)] --------------------------------------------------------------------------------