24 Commits

Author SHA1 Message Date
pluv 0e42d0410c fix: 改进与日志简化 2026-05-22 22:33:57 +08:00
pluv 31996f2532 feat: 网页界面与 API 总体布局 2026-05-20 23:58:33 +08:00
pluv 2415c1afdb chore: 修改目录结构 2026-05-20 23:57:03 +08:00
pluv f88475499f docs: 修改文档 2026-05-20 23:49:30 +08:00
pluv b31c045aa5 docs: 修改文档 2026-05-19 00:15:08 +08:00
pluv 3d113f2eaa feat: 改进命令行入口 2026-05-17 00:22:01 +08:00
pluv b0625ef636 fix: 修复 playsound3 依赖问题 2026-05-17 00:07:12 +08:00
pluv dc8fa36a28 fix: 修复 zmq 依赖问题 2026-05-17 00:05:04 +08:00
pluv 2918662222 docs: 修改文档 2026-05-17 00:01:09 +08:00
pluv 60caee0f67 docs: 修改文档 2026-05-16 23:59:25 +08:00
pluv 4ba164e2ab fix: 修改一处显示重复 2026-05-16 23:49:38 +08:00
pluv 2735465629 feat: 简化并统一命令行入口
删除了原有 __main__.py 中冗长且烦人的说教, heurams 现作为所有功能的
统一入口, 并移除了单独的 heurams-tui 入口(现为 heurams tui)
2026-05-16 23:46:39 +08:00
pluv be9e79b576 docs: 更换包下载方式 2026-05-16 23:30:54 +08:00
pluv 470a7383bf docs: 修改文档 2026-05-16 23:24:33 +08:00
pluv ceaebf2c54 docs: 修改文档 2026-05-16 23:21:07 +08:00
pluv 54daa4128d build: 将构建系统换为 hatchling
由于 uv_build 在 android termux 端需编译大量 rust 代码, 极大减慢安装速度, 故使用 hatchling 作为纯 python 构建系统, 这并不影响使用 uv 管理项目
2026-05-16 23:13:14 +08:00
pluv d22966b34d fix: 修复 Termux 声音功能 2026-05-16 22:56:57 +08:00
pluv 0889bfa1c3 fix: 几处修复 2026-05-15 13:53:48 +08:00
pluv 92796451d1 docs: 改动文档 2026-05-10 15:59:09 +08:00
pluv 66870c4987 docs: 修复一处换行 2026-05-10 00:24:08 +08:00
pluv 477fa972eb docs: 改变仓库说明 2026-05-10 00:22:04 +08:00
pluv 5b52e4b3ee chore: 改变仓库说明 2026-05-08 19:20:45 +08:00
pluv 5058fb010f chore: 改变仓库分支配置和说明 2026-05-08 19:20:45 +08:00
pluv f1e87c6ff2 docs: 改进文档 2026-05-08 19:20:45 +08:00
93 changed files with 6598 additions and 1058 deletions
+33 -33
View File
@@ -1,50 +1,50 @@
# AI 编程工具说明
# AI Coding Assistant Guide
本文档为 AI 工具以及在使用 AI 辅助向 HeurAMS 项目贡献代码的开发者提供指导, 一般而言此文件会被自动读入多种 AI 工具的上下文.
This document provides guidance for AI tools and developers using AI assistance to contribute code to the HeurAMS project. Generally, this file will be automatically loaded into the context of various AI tools.
AI 工具应当完整阅读此 `/AGENTS.md` 文件.
AI tools should read this `/AGENTS.md` file in its entirety.
## 查阅开发文档
## Reference Documents
在帮助进行 HeurAMS 开发时,AI 工具应遵循标准的开发规范与流程, 应当自动查看或是在用户发出"初始化/init"指令后查看:
When assisting with HeurAMS development, AI tools should follow standard development practices and procedures. They should automatically check, or check upon user issuing an "init/初始化" command:
- [贡献指南](/CONTRIBUTING.md)
- [自述文件](/README.md)
- [项目架构](/ARCHITECTURE.md)
- [Contributing Guide](/CONTRIBUTING.md)
- [README](/README.md)
- [Architecture Overview](/docs/ARCHITECTURE.md)
## 明确禁止行为
## Prohibited Actions
1. 禁止 AI 自动生成 PR patch 文件
2. 禁止 AI 在未经人工确认的情况下修改现有代码
3. 禁止 AI 不使用格式化工具而生成格式化文件的行为
4. 禁止 AI 修复任何"bug", 而不经人工确认
5. 禁止一切不遵循项目设计原则, 另造独立库的 "糊屎" 行为
6. 禁止 AI 直接操作 pip, uv, apt 等工具修改外部依赖或工具, 而应让人类开发者自己操作依赖
7. 禁止使用不同于任何现有文件的现有注释语言的其他语言写新注释
8. 禁止不读文件就直接覆写
9. 绝对禁止修改此 `/AGENTS.md` 文件
1. AI must not automatically generate PR or patch files
2. AI must not modify existing code without human confirmation
3. AI must not generate formatted files without using formatting tools
4. AI must not fix any "bugs" without human confirmation
5. AI must not engage in any "cowboy coding" that bypasses project design principles or creates separate libraries
6. AI must not directly operate pip, uv, apt, or other tools to modify external dependencies or tools — human developers should handle dependencies themselves
7. AI must not write new comments in a language different from the existing comment language of any existing file
8. AI must not overwrite files without reading them first
9. **Modification of this `/AGENTS.md` file is absolutely forbidden**
## 许可证与法律要求
## License & Legal Requirements
所有贡献必须符合许可要求, 所有代码必须与 AGPL-3.0-or-later 许可以及项目附加豁免条款(位于 LICENSE 文件尾部 237245 行)兼容.
All contributions must comply with licensing requirements. All code must be compatible with the AGPL-3.0-or-later license and the project's additional exemption clause (located at lines 237-245 at the end of the LICENSE file).
## Signed-off-by DCO
## Signed-off-by & DCO
AI 代理**严禁添加** Signed-off-by 标签.
AI agents are **strictly forbidden** from adding Signed-off-by tags.
只有人类能够合法地认证 DCO.
Only humans can legally certify the DCO.
人类提交者负责:
Human committers are responsible for:
- 审阅所有 AI 生成的代码
- 确保符合许可要求
- 添加自己的 Signed-off-by 标签以认证 DCO
- 对贡献负责任
- Reviewing all AI-generated code
- Ensuring compliance with licensing requirements
- Adding their own Signed-off-by tag to certify the DCO
- Taking responsibility for contributions
AI 助手负责:
AI assistants are responsible for:
- 了解运行环境, 例如操作系统或具体发行版
- 遵循此文档所述规则
- 主动提醒使用 AI 工具的开发者
- Understanding the runtime environment, such as the operating system or specific distribution
- Following the rules outlined in this document
- Proactively reminding developers using AI tools
本文档参考自 <a href="https://docs.kernel.org/process/coding-assistants.html" target="_blank" rel="noopener noreferrer">AI Coding Assistants — The Linux Kernel documentation</a>
This document references <a href="https://docs.kernel.org/process/coding-assistants.html" target="_blank" rel="noopener noreferrer">AI Coding Assistants — The Linux Kernel documentation</a>
+63 -61
View File
@@ -1,117 +1,119 @@
# 贡献指南与二次开发
# Contributing Guide & Development
欢迎支持此项目!
Welcome and thank you for supporting this project!
目前, 项目仓库主服务器为<a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">作者的 Gitea 实例</a>, 以保证可用性并同时接受来自多个社区的协作, 并在 <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a> <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a> 设置了镜像同步.
Currently, the primary project repository server is <a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">git.pluv27.top</a>, which manages synchronization, ensures availability, and accepts collaboration from multiple communities. Mirror syncs are set up on <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a>, and <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a>.
这丝毫不影响项目接受来自 <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a> <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a> 的 PR, 在 GitHub KDE Invent 所接受的 PR 会保留贡献者标识并按原样同步回所有平台, 欢迎在任意平台为项目做出贡献.
This does not affect the project's acceptance of PRs from <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a>, and <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a>. PRs accepted on GitHub, KDE Invent, and Gitee will retain contributor attribution and be synced back to all platforms as-is. Contributions on any platform are welcome.
> [!NOTE]
> 我们已经开始着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
> 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS Plasma Mobile 提供现代用户界面
> 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发
> We have begun development of a modern cross-platform frontend based on the KDE UI framework `Kirigami`, called "KiriMemo" (package name "org.kde.kirimemo"). Note that it is not a KDE project.\
> It directly reuses the Python kernel via `PyOtherSide`, providing a modern UI for Windows, Linux, macOS, Android, iOS, and Plasma Mobile.\
> If you are skilled in C++, QML, Qt, and KDE frameworks, you are welcome to join the KiriMemo project development.
## 开发规范
## Development Conventions
分支划分:
Branch structure:
- `dev` 分支(也是默认分支): 主线开发分支, 自身仅用于非重构的问题修复和整合功能分支, 拉取请求在该分支合并
- `master` 分支: 稳定版本, 仅当稳定版本释出或修补版本时将 `dev` 合并到 `master`
- 功能与重构分支: 从 `dev` 分支创建, 命名格式为 `feature/描述` `fix/描述` `refactor/v版本号`
- 功能与重构分支应先合并至 `dev`, 再合并至 `master`
- `dev` branch (default branch): Mainline development branch. Used only for non-refactoring bug fixes and integration of feature branches. Pull requests are merged into this branch.
- `master` branch: Mainline stable version. `dev` is merged into `master` only when a stable version is released or a patch version is prepared.
- Feature and refactoring branches: Created from `dev`, named as `feature/description` or `fix/description` or `refactor/description` or `next/version`.
- Feature and refactoring branches should first be merged into `dev`, then into `master`.
代码格式化:
Code formatting:
- 安装工具:
- Install tools:
```bash
python -m pip install black autoflake mdformat
```
- 对于 Python, 使用 `black` `autoflake` 格式化\
命令:
- For Python, use `black` and `autoflake`.\
Commands:
```bash
# black 的多线程在某些环境下有兼容性问题
# black's multi-threading may have compatibility issues in some environments
black . --workers=1
```
```bash
# autoflake 注意排除 __init__.py
# autoflake: note to exclude __init__.py
autoflake --in-place --remove-all-unused-imports --recursive ./src/ --exclude __init__.py
```
- 对于 Markdown, 使用 `mdformat` 格式化
命令:
- For Markdown, use `mdformat`.\
Command:
```bash
mdformat --number .
```
- 对于 Textual CSS, 可以使用 `prettier` 格式化
- 格式化不是必需的, 可以整合入一次 `style` 提交, 但 `master` `dev` 分支上的代码应尽量整洁, 以便合并时审查
- For Textual CSS, use `prettier`.
- Formatting is not mandatory and can be consolidated into a single `style` commit, but code on `master` and `dev` branches should be kept clean for review.
提交消息:
Commit messages:
- 使用简体中文或英文撰写清晰的提交消息
- 提交消息格式: 遵循 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 规范, 建议使用 `koji` 工具
- Write clear commit messages in Simplified Chinese or English.
- Commit message format: Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. The `koji` tool is recommended.
合并方式:
Merge method:
- 为了一致性和可追溯性, 项目自 v0.4.0 重构后重新初始化仓库起就禁止使用 Fast-forward 合并
- 可以设置 `git config merge.ff false`
- For consistency and traceability, Fast-forward merges have been prohibited since the repository was re-initialized after the v0.4.0 refactoring.
- You can set `git config merge.ff false`.
## 设置开发环境
Commit author format:\
Due to the quirky git hook on KDE Invent infrastructure, the commit Author field must appear to be a real name (e.g., "Wang Zhiyu" instead of "wangzhiyu"; otherwise, KDE Invent's hook will reject the push).\
Therefore, please ensure your git configuration uses a proper name-like format (e.g., `git config user.name "Li Hua"`, i.e., must include a space). A real name is not required; the email field has no restrictions. You can also repeat a single name twice (e.g., "Thura Thura") to pass the check.
## Setting Up Development Environment
```bash
git clone https://git.pluv27.top/pluv/HeurAMS # 默认分支为 dev, 所以不必切换分支
git clone https://git.pluv27.top/pluv/HeurAMS # Default branch is dev, no need to switch
cd HeurAMS
# 如果决定使用 uv (推荐)
# If using uv (recommended)
python3 -m pip install uv
uv sync --all-extras # 同步开发运行环境
uv sync --all-extras # Sync development environment
uv run heurams # 验证包安装
uv run heurams
uv run heurams-tui # 启动 TUI
# If using native Python environment (not recommended, but retained for environments where uv and hardlinks are not supported, e.g., termux)
# 如果决定使用原生 python 环境 (不推荐, 但我们保留了这种方式以便在不便支持 uv 与硬链接的环境和文件系统(例如 termux)上运行)
python3 -m pip install -e .[all] # Install dependencies and HeurAMS as a local package
python3 -m pip install -e .[all] # 安装依赖并将 HeurAMS 安装为本地包
python3 -m heurams # Verify installation
python3 -m heurams # 验证安装
python3 -m heurams.interface # 启动 TUI
python3 -m heurams.interface # Launch TUI
```
## 许可证与外部引用
## License & External References
贡献者拥有其贡献部分的版权同意其贡献将在 AGPL-3.0 许可证(包括附加的本机 API 调用豁免条款)下发布.
Contributors retain copyright of their contributions and agree that their contributions will be released under the AGPL-3.0 license (including the additional local API call exemption clause).
如有以下情况, 请在 PR 描述中注明:
Please note in your PR description if:
- 如果需要引入其他开源 vendor
- 如果需要引入其他专有的网络服务(例如当前项目中的 edgetts)
- 如果需要升级某个依赖或运行环境的版本
- You need to introduce other open-source vendor code
- You need to introduce other proprietary online services (e.g., edgetts in the current project)
- You need to upgrade a dependency or runtime environment version
## 新的用户界面前端
## New User Interface Frontends
HeurAMS 被设计为一个可独立于前端的程序库, 这意味着:
HeurAMS is designed as a frontend-independent library, meaning:
- 我们的内置 Textual TUI 前端不是唯一可用的前端
- Our built-in Textual TUI frontend is not the only available frontend.
- 如果您有一个自己开发的且可用的 HeurAMS 前端 (例如未实现的 Flutter 前端), 并且以 AGPL-3.0/GPL-3.0 开放源代码, 可以联系我们将它转移到 HeurAMS 的官方仓库中以便共同维护, 您将保留您的版权并可主导该仓库下的开发工作 :)
- If you have developed a usable HeurAMS frontend (e.g., a not-yet-implemented Flutter frontend) and open-sourced it under AGPL-3.0/GPL-3.0, you can contact us to transfer it to the HeurAMS official repository for joint maintenance. You will retain your copyright and can lead development under that repository. :)
- 您还可以在自己的项目中以独立进程/服务调用 HeurAMS, 根据 AGPL-3.0 及本项目的附加许可条款, 如果调用发生在同一主机上且不涉及外部网络转发, 则可豁免许可证规定的特定义务而免于受 AGPL-3.0 "污染". 为了这点, 我们正在完善可选择启用的跨进程 RPC 模块, 这将成为潜进内核的跨平台标准件.
- You can also call HeurAMS as an independent process/service in your own projects. Per AGPL-3.0 and this project's additional license terms, if the call occurs on the same host and does not involve external network forwarding, it is exempt from specific obligations of the license and will not be "contaminated" by AGPL-3.0. To this end, we are improving the optional cross-process RPC module, which will become a cross-platform standard component of the HeurAMS kernel.
- 如果您通过独立进程/服务调用方式开发了另外的软件, 开源但不愿使用 AGPL-3.0/GPL-3.0 许可证, 也可以联系我们, 我们乐于将您的项目链接添加到友链中
- If you develop another piece of software through independent process/service invocation and open-source it but prefer not to use the AGPL-3.0/GPL-3.0 license, you can also contact us. We would be happy to add your project link to our friendly links.
## 软件开发之外的贡献
## Non-Software Contributions
即使您不是软件开发人员, 我们也欢迎您加入贡献!
Even if you are not a software developer, you are welcome to contribute!
您可以:
You can:
- 协助创建或核对各种语言的翻译来翻译软件的界面和文档
- 制作开放的记忆单元集(包括但不限于文字、图像、音效)给其他用户使用
- 改进软件配套的文档
- 给其他用户答疑解惑或分享自己的经验
- 在讨论区提出新想法或反馈问题
- Help create or proofread translations for the software interface and documentation in various languages
- Create open memory unit sets (including but not limited to text, images, and audio) for other users
- Improve the software documentation
- Answer questions from other users or share your experience
- Propose new ideas or report issues in the discussion area
您的角色您来定!
You decide your role!
+119
View File
@@ -0,0 +1,119 @@
# 贡献指南与二次开发
欢迎支持此项目!
目前, 项目仓库主服务器为<a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">作者的 Gitea 实例</a>, 它负责管理同步, 保证可用性并同时接受来自多个社区的协作, 并在 <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a> 和 <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a> 设置了镜像同步.
这丝毫不影响项目接受来自 <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a> 和 <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a> 的 PR, 在 GitHub, KDE Invent 和 Gitee 所接受的 PR 会保留贡献者标识并按原样同步回所有平台, 欢迎在任意平台为项目做出贡献.
> [!NOTE]
> 我们已经开始着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
> 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS 和 Plasma Mobile 提供现代用户界面
> 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发
## 开发规范
分支划分:
- `dev` 分支(仓库默认分支): 主线开发分支, 自身仅用于非重构的问题修复和整合功能分支, 拉取请求在该分支合并
- `master` 分支: 主线稳定版本, 仅当稳定版本释出或修补版本时将 `dev` 合并到 `master`
- 功能与重构分支: 从 `dev` 分支创建, 命名格式为 `feature/描述``fix/描述``refactor/描述``next/版本号`
- 功能与重构分支应先合并至 `dev`, 再合并至 `master`
代码格式化:
- 安装工具:
```bash
python -m pip install black autoflake mdformat
```
- 对于 Python, 使用 `black` 与 `autoflake` 格式化\
命令:
```bash
# black 的多线程在某些环境下有兼容性问题
black . --workers=1
```
```bash
# autoflake 注意排除 __init__.py
autoflake --in-place --remove-all-unused-imports --recursive ./src/ --exclude __init__.py
```
- 对于 Markdown, 使用 `mdformat` 格式化
命令:
```bash
mdformat --number .
```
- 对于 Textual CSS, 可以使用 `prettier` 格式化
- 格式化不是必需的, 可以整合入一次 `style` 提交, 但 `master` 和 `dev` 分支上的代码应尽量整洁, 以便合并时审查
提交消息:
- 使用简体中文或英文撰写清晰的提交消息
- 提交消息格式: 遵循 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 规范, 建议使用 `koji` 工具
合并方式:
- 为了一致性和可追溯性, 项目自 v0.4.0 重构后重新初始化仓库起就禁止使用 Fast-forward 合并
- 可以设置 `git config merge.ff false`
提交署名方式:
由于 KDE Invent 设施的奇怪 git hook, commit 的 Author 字段需要看起来像个真名(例如 Wang Zhiyu 不能写为 wangzhiyu, 否则 KDE Invent 的 hook 会拒绝 push),
所以请确保您的 git 配置使用了类似正式姓名的格式 (例如 git config user.name "Li Hua", 也即中间需要空格), 不一定要真实姓名, 邮箱无要求, 也可以将单名重复使用两次 (例如 Thura Thura) 以通过检测.
## 设置开发环境
```bash
git clone https://git.pluv27.top/pluv/HeurAMS # 默认分支为 dev, 所以不必切换分支
cd HeurAMS
# 如果决定使用 uv (推荐)
python3 -m pip install uv
uv sync --all-extras # 同步开发运行环境
uv run heurams
# 如果决定使用原生 python 环境 (不推荐, 但我们保留了这种方式以便在不便支持 uv 与硬链接的环境和文件系统(例如 termux)上运行)
python3 -m pip install -e .[all] # 安装依赖并将 HeurAMS 安装为本地包
python3 -m heurams # 验证安装
python3 -m heurams.interface # 启动 TUI
```
## 许可证与外部引用
贡献者拥有其贡献部分的版权同意其贡献将在 AGPL-3.0 许可证(包括附加的本机 API 调用豁免条款)下发布.
如有以下情况, 请在 PR 描述中注明:
- 如果需要引入其他开源 vendor
- 如果需要引入其他专有的网络服务(例如当前项目中的 edgetts)
- 如果需要升级某个依赖或运行环境的版本
## 新的用户界面前端
HeurAMS 被设计为一个可独立于前端的程序库, 这意味着:
- 我们的内置 Textual TUI 前端不是唯一可用的前端
- 如果您有一个自己开发的且可用的 HeurAMS 前端 (例如未实现的 Flutter 前端), 并且以 AGPL-3.0/GPL-3.0 开放源代码, 可以联系我们将它转移到 HeurAMS 的官方仓库中以便共同维护, 您将保留您的版权并可主导该仓库下的开发工作 :)
- 您还可以在自己的项目中以独立进程/服务调用 HeurAMS, 根据 AGPL-3.0 及本项目的附加许可条款, 如果调用发生在同一主机上且不涉及外部网络转发, 则可豁免许可证规定的特定义务而免于受 AGPL-3.0 "污染". 为了这点, 我们正在完善可选择启用的跨进程 RPC 模块, 这将成为潜进内核的跨平台标准件.
- 如果您通过独立进程/服务调用方式开发了另外的软件, 开源但不愿使用 AGPL-3.0/GPL-3.0 许可证, 也可以联系我们, 我们乐于将您的项目链接添加到友链中
## 软件开发之外的贡献
即使您不是软件开发人员, 我们也欢迎您加入贡献!
您可以:
- 协助创建或核对各种语言的翻译来翻译软件的界面和文档
- 制作开放的记忆单元集(包括但不限于文字、图像、音效)给其他用户使用
- 改进软件配套的文档
- 给其他用户答疑解惑或分享自己的经验
- 在讨论区提出新想法或反馈问题
您的角色您来定!
+96 -125
View File
@@ -1,172 +1,143 @@
# 潜进 (HeurAMS) - 启发式辅助记忆调度器
# HeurAMS - Heuristic Auxiliary Memorizing Scheduler
## 概述
[中文](README_zh.md) | English
"潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是一种基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划,\
也是一种开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
## Overview
## 关于此仓库
HeurAMS "潜进" (Heuristic Auxiliary Memorizing Scheduler) is an auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently.
It is also an open, elegant, and extensible spaced repetition scheduler experiment platform, intended to help researchers conduct investigations, experiments, and research on cutting-edge memory algorithms more efficiently.
此仓库为 "潜进" 的核心程序库在 python 语言下的实现\
包含数据模型与框架, 并内置了基于 textual 框架的前端实现 (interface 子模块)\
除了通过内置前端进行学习外, 开发者也能在 python 环境中导入 `heurams` 库或使用 `RPC``heurams` 程序库实例通讯, 使用框架构建其他辅助记忆功能前端或其他应用程序
[Detailed Introduction](docs/INTRODUCTION.md) | [Screenshots](docs/SCREENSHOTS.md)
> [!NOTE]
> 我们已经着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
> 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS 和 Plasma Mobile 提供现代用户界面\
> 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发
<p align="left">
<a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/GitHub-fafafa?style=for-the-badge&logo=github&logoColor=181717" alt="GitHub" /></a><a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/KDE_Invent-1D99F3?style=for-the-badge&logo=kde&logoColor=white" alt="KDE Invent" /></a><a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Gitee-C71D23?style=for-the-badge&logo=gitee&logoColor=white" alt="Gitee" /></a><a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/git.pluv27.top-609926?style=for-the-badge&logo=gitea&logoColor=white" alt="git.pluv27.top" /></a>
</p>
## 特性
## Quick Start
### 间隔重复调度器
### Installation
> 许多出版物都广泛讨论了不同重复间隔对学习效果的影响. 特别是, 间隔效应被认为是一种普遍现象. 间隔效应是指, 如果重复的间隔是分散/稀疏的, 而不是集中重复, 那么学习任务的表现会更好. 因此, 有观点提出, 学习中使用的最佳重复间隔是**最长的, 但不会导致遗忘的间隔**.
#### Install from Package Manager
- 软件开箱即用, 无需多加配置即可使用默认的 `SM-2` 算法进行学习
- 此外, 算法模块是 "潜进" 内核 (heurams.kernel) 中的一等公民, 内核天然支持插拔各型算法
- 无需安装繁杂的插件即可分单元集完成算法快速切换与调优, 研究者可以方便地修改算法模块以便捷地进行研究与测试
- 默认使用 `SM-2` 简单间隔重复算法, 此算法亦用作 `Anki` 闪卡记忆软件的默认闪卡调度器
- 内置 `NSP-0` 筛选用非间隔重复算法以便快速筛选记忆内容, `FSRS` 先进间隔重复算法作为效率更高的调度器, 与 `SM-15M (移植自 sm.js 项目)` 复杂间隔重复算法(逆向工程)
- 算法模块可以标记记忆项目, 也可以动态规划每个记忆单元的记忆间隔时间表, 动态跟踪记忆反馈数据, 以优化长期记忆保留率与稳定性
- 得益于项目的模块化架构与单元集结构设计, 一个项目甚至可以与任意种算法共存并互通, 这对研究者及想探索/实验高效率方法的用户极其友好
HeurAMS (package name `heurams`) is in early development and not yet available on PyPI.
However, you can install stable and development versions from the repository using pip, which requires a Python environment (Python 3.12.13 or later recommended).
### 多模态学习进程
与 Anki 的 SQLite `.apkg` 包不同, 我们坚持使用人类可读的文件夹组织单元集, 这带来了若干好处, 包括:
- 人类可读: 您可以用任意工具, 乃至一个记事本自由修改记忆载荷数据而无需打开软件
- 元数据配置: 配置自由度极高, 可以任意组合, 重造, 乃至创造新内容
- 测验, 算法与知识互相隔离: 一条知识不再是单一的闪卡, 不仅可以用若干不同的算法规划, 还可以用多种并行的谜题类型测验, 极大地提升学习效果和丰富度. 作为学习者, 您无需担忧概念复杂--仅需从云端下载单元集即可开箱即用上述特性!
- 多模态学习
- 软件自身集成了文本转语音 (TTS) , 音频与语言模型 (LLM) 模块, 这些功能乃至功能本身都是可插拔, 可扩展, 可切换驱动的, 这为内容创建了极大的丰富度
- 软件内置多种谜题类型, 包括选择题 (MCQ), 填空题 (Cloze) 与识别题 (Recognition), 您可在同一单元应用多种, 或是选择性启用
- 软件天然支持动态内容生成, 支持宏驱动的模板系统, 根据上下文乃至语言模型动态生成知识点的解析
- 在间隔重复研究尚被 SuperMemo 系列独占的时代, Wozniak 就早已表示 "如果不能理解知识, 就无需记忆它". 今天, 我们依然相信理解是记忆的基石
- 云同步与分享优化:
- 由于记忆数据和单元集文件都是文本文件, 故可进行快速的增量同步而无需完整地上传所有文件, 并且设计天然支持版本控制
- 如果您想分享单文件, 软件也支持导出为压缩包或合并成单文本文件以通过纯文本文件形式在 pastebin 等平台分享
- 性能提升: 得益于现代且支持分块的文件组织结构, 潜进能在保持高自由度的同时仅使用 python 就能达到敏捷且低占用的用户体验
- AI 辅助友好: 想象您有一些 .apkg 牌组或一大段教材内容, 您可以方便且高效率地使用 AI 工具创建可在 HeurAMS 使用的单元集
### 内置实用用户界面
尽管不是唯一前端, 但响应式 Textual 框架构建的内置终端用户界面在多种场景下仍具有独特优势:
- 跨平台, 并支持触屏/鼠标/键盘多操作模式
- 与几乎所有现代终端模拟器相容
- 对于<a href="https://www.arewesixelyet.com/" target="_blank" rel="noopener noreferrer">支持 sixel 协议的终端模拟器</a>, 可高清显示图像内容
- 对于不支持 sixel 协议的终端模拟器, 也支持图片低清的兼容显示模式
- 可通过 textual-web 作为服务部署, 并在任意浏览器使用
- 简洁直观, 键盘友好, 全功能且高效率的用户界面设计
- 易于嵌入: 可在 getty/kmscon 中运行而无需任何桌面图形服务
- 资源占用小, 运行流畅, 不拖泥带水
- 便于测试与调试程序库
查看[屏幕截图](SCREENSHOTS.md).
## 快速开始
### 从包管理器安装
潜进 (包名是 `heurams`) 处于早期开发考虑, 尚未上架 PyPI, 但您可以用我们的基础设施安装稳定版和开发版本.
#### 稳定版本
安装适用于用户体验的可选依赖(推荐):
Install from the stable `master` branch with optional dependencies for user experience (recommended):
```
python -m pip install heurams[basic] -i https://pypi.pluv27.top/root/stable/+simple/
pip install --upgrade 'heurams[basic] @ https://git.pluv27.top/pluv/HeurAMS/archive/master.zip'
```
安装适用于一般计算机的通用音频模块(基于 playsound3):\
(此项不适用于 termux 环境, termux 的音频支持是内建的)
Install from the more recent, roughly stable `dev` branch with optional dependencies (if you want cutting-edge improvements):
```
python -m pip install heurams[audio-playsound] -i https://pypi.pluv27.top/root/stable/+simple/
pip install --force-reinstall --no-deps 'heurams[basic] @ https://git.pluv27.top/pluv/HeurAMS/archive/dev.zip'
```
#### 开发版本
> [!CAUTION]
> 对于部分 Linux 发行版和 Android Termux 用户:\
> 您需要先行安装 `cmake` 和 `libzmq` 才能正确安装项目的 `zmq` 依赖.\
> 例如在 termux 上先运行 `pkg install cmake clang libzmq`.\
> 项目功能本身不依赖它, 但需要该依赖用于启动可选的调试服务器.
安装全部可选依赖(推荐):
Install the general audio module for desktop computers (based on playsound3):\
(Not applicable for termux environments; termux has built-in audio support)
```
python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/
pip install --upgrade 'heurams[audio-playsound] @ https://git.pluv27.top/pluv/HeurAMS/archive/master.zip'
```
#### 依赖组说明
> You can also install from specific branches like `refactor/...` to test particular changes.
由于部分依赖只被少数功能需要, 所以我们把可选依赖分得比较细, 前面提供的命令会安装部分可选依赖, 以下是依赖组列表:
[Dependency Group Reference](docs/INTRODUCTION.md#package-dependency-groups)
| 依赖组 | 包含模块 | 说明 |
|--------|----------|------|
| 最小化安装 | tabulate, toml, transitions | 核心驱动程序库, 始终必需 |
| interface | textual, psutil | 基本用户界面依赖 |
| algo-fsrs | fsrs | FSRS 算法模块 |
| tts-edgetts | edge-tts | 微软文本转语音 |
| llm | llms-py | API 调用 |
| audio-playsound | playsound3 | 通用音频模块 |
| dev | zmq, pytest, pytest-cov | 开发调试与测试工具 |
| basic | [tts-edgetts], [llm], [algo-fsrs] | 适用于用户体验的较轻依赖组(推荐) |
| all | 以上所有依赖 | 完整安装组 |
#### Install from Source
### 从源码安装
We provide two source installation methods: native Python and uv.
See the [Contributing Guide - Setting up Development Environment](CONTRIBUTING.md#setting-up-development-environment) for details.
我们提供原生 python 和 uv 两种安装方式.\
详见[贡献指南](CONTRIBUTING.md).
### Usage
## 常见问题 (FAQ)
Run `heurams` in your terminal, and you will see help information:
详见[常见问题](FAQ.md).
```plain
~ $ heurams
Usage: heurams [OPTIONS] COMMAND [ARGS]...
## 项目架构
HeurAMS 0.5.1 - Heuristic Auxiliary Memorizing Scheduler
详见[架构说明](ARCHITECTURE.md).
Options:
-v, --version Show the version and exit.
-h, --help Show this message and exit.
## 参与项目
Commands:
help Show this help message
tui Launch the built-in basic user interface (TUI)
version Print version information
```
欢迎参与到项目协作中!\
详见[贡献指南](CONTRIBUTING.md).\
关于 AI 辅助开发的说明, 请参阅 [AGENTS.md](AGENTS.md).
Start the basic user interface by typing `heurams tui`:
## 项目标识
```plain
~ $ heurams tui
Welcome to the basic user interface!
Loading config and context... Done! (2ms)
Loading UI framework... Done! (89ms)
Loading UI layout... Done! (56ms)
Component directory: <package directory>
Working directory: <working directory, ./data folder will be created here>
Pre-work total: 147ms
HeurAMS 项目标识如下, 文件(位图和矢量图)位于 `./src/heurams/assets/art/` 目录.
(Your terminal will now display the TUI)
```
<img src="src/heurams/assets/art/banner128-light.png" height="96px" title="位图横幅(不透明)">
Check the version with `heurams -v`:
```
~ $ heurams -v
HeurAMS 0.5.1 stable (fulcrum/支点), Linux
```
## Frequently Asked Questions (FAQ)
See [FAQ](docs/FAQ.md).
## Project Architecture
See [Architecture Overview](docs/ARCHITECTURE.md).
## Contributing
Contributions are welcome!
See the [Contributing Guide](CONTRIBUTING.md).
For AI-assisted development guidelines, see [AGENTS.md](AGENTS.md).
## Project Identity
HeurAMS project identity assets are located in `./src/heurams/assets/art/` directory:
<img src="src/heurams/assets/art/banner128-light.png" height="96px" title="Bitmap Banner (Opaque)">
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
<img src="src/heurams/assets/art/logo.svg" height="96px" title="矢量图标">
<img src="src/heurams/assets/art/logo-mono-light.svg" height="96px" title="单色明亮矢量图标">
<img src="src/heurams/assets/art/logo-mono-dark.svg" height="96px" title="单色暗色矢量图标">
<img src="src/heurams/assets/art/logo.svg" height="96px" title="Vector Icon">
<img src="src/heurams/assets/art/logo-mono-light.svg" height="96px" title="Monochrome Light Vector Icon">
<img src="src/heurams/assets/art/logo-mono-dark.svg" height="96px" title="Monochrome Dark Vector Icon">
</div>
颜色分别是: `#1660A5 (海蓝色)` `#545F70 (蓝灰色)` `#FFFFFF (单色明亮图标白色)` `#1A1A1A (单色暗色图标深黑色)` `#2f2f35 (文字颜色)`.
Colors: `#1660A5 (Ocean Blue)` `#545F70 (Blue Gray)` `#FFFFFF (Monochrome Light Icon White)` `#1A1A1A (Monochrome Dark Icon Deep Black)` `#2f2f35 (Text Color)`.
## License
## 许可证
### Project
### 项目本身
This project is open source under the AGPL-3.0 license, with an additional exemption clause for local API calls, making it more permissive than the standard AGPL-3.0.
本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款, 较标准 AGPL-3.0 更松.
See the [LICENSE](LICENSE) file in the root directory.
详见根目录下 [LICENSE](LICENSE) 文件.
### Third-Party Code
### 第三方代码
The project embeds or directly uses the following third-party code or its derivatives (possibly with modifications) in `src/heurams/vendor/` or other locations:
项目在 `src/heurams/vendor/` 目录下嵌入或在其他位置间接使用了以下第三方代码(可能有修改):
#### SM.js
#### SM.js (slaypni)
- Upstream version: commit `6e3bb4a` (upstream discontinued as of Feb 4, 2015)
- Usage: Rewritten from CoffeeScript to Python with indirect reference, maintaining the same mathematical principles; improved logic, performance, and standardized API
- Location: `src/heurams/kernel/algorithms/sm15m.py`
- Original project: [SM.js](https://github.com/slaypni/SM-15)
- Original copyright: Copyright (c) 2014 Kazuaki Tanida
- Original license: MIT License
- 上游版本: commit `6e3bb4afaf484426deb4a9fa3bcffe42ac066b45` (2015年2月4日上游已停止维护)
- 引用方式: 将 coffeescript 重写为 python 并间接引用, 数学原理一致; 并对重写后代码进行逻辑, 性能与标准化 API 改进
- 位置: `src/heurams/kernel/algorithms/sm15m*.py`
- 原项目: [SM.js](https://github.com/slaypni/SM-15)
- 原版权: Copyright (c) 2014 Kazuaki Tanida
- 原许可证: MIT License
本项目受益于他们无私且优秀的工作.
This project benefits from their selfless and excellent work.
+144
View File
@@ -0,0 +1,144 @@
# HeurAMS "潜进" - 启发式辅助记忆调度器
中文 | [English](README.md)
## 概述
HeurAMS "潜进" (Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是一种基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划,
也是一种开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的调查实验与研究.
[详细介绍](docs/INTRODUCTION_zh.md) [屏幕截图](docs/SCREENSHOTS_zh.md)
<p align="left">
<a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/GitHub-fafafa?style=for-the-badge&logo=github&logoColor=181717" alt="GitHub" /></a><a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/KDE_Invent-1D99F3?style=for-the-badge&logo=kde&logoColor=white" alt="KDE Invent" /></a><a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Gitee-C71D23?style=for-the-badge&logo=gitee&logoColor=white" alt="Gitee" /></a><a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/git.pluv27.top-609926?style=for-the-badge&logo=gitea&logoColor=white" alt="git.pluv27.top" /></a>
</p>
## 快速开始
### 安装软件
#### 从包管理器安装
潜进 (包名是 `heurams`) 处于早期开发考虑, 尚未上架 PyPI.
但可以用 pip 从仓库安装稳定版和开发版本, 这要求设备上安装了 python 环境 (建议 3.12.13 及之后版本).
从稳定的 `master` 分支安装, 并安装适用于用户体验的可选依赖(推荐):
```
pip install --upgrade 'heurams[basic] @ https://git.pluv27.top/pluv/HeurAMS/archive/master.zip'
```
从较前沿, 大致稳定的 `dev` 分支安装, 并安装适用于用户体验的可选依赖(如果您追求较前沿的改进):
```
pip install --force-reinstall --no-deps 'heurams[basic] @ https://git.pluv27.top/pluv/HeurAMS/archive/dev.zip'
```
安装适用于一般计算机的通用音频模块 (基于 playsound3):\
(此项不适用于 termux 环境, termux 的音频支持是内建的)
```
pip install --upgrade 'heurams[audio-playsound] @ https://git.pluv27.top/pluv/HeurAMS/archive/master.zip'
```
> 您也可以从 `refactor/...` 等特定分支安装以测试某项更改
[依赖分组说明](docs/INTRODUCTION_zh.md#包依赖组说明)
#### 从源码安装
我们提供原生 python 和 uv 两种源码安装方式.\
详见[贡献指南 - 设置开发环境](CONTRIBUTING_zh.md#设置开发环境).
### 使用软件
在终端中运行 `heurams`, 您会看到一系列帮助信息, 例如:
```plain
~ $ heurams
Usage: heurams [OPTIONS] COMMAND [ARGS]...
HeurAMS 0.5.1 - 启发式辅助记忆调度器
Options:
-v, --version Show the version and exit.
-h, --help Show this message and exit.
Commands:
help 显示此帮助信息
tui 启动内置基本用户界面 (TUI)
version 输出版本信息
```
可以通过键入 `heurams tui` 启动基本用户界面, 例如:
```plain
~ $ heurams tui
欢迎使用基本用户界面!
加载配置与上下文... 已完成! (耗时: 2ms)
加载用户界面框架... 已完成! (耗时: 89ms)
加载用户界面布局... 已完成! (耗时: 56ms)
组件目录: <软件包所在目录>
工作目录: <运行目录, 将在此目录下建立 ./data 文件夹>
前置工作共计耗时: 147ms
(此时您的终端将转为呈现美观的 TUI 基本用户界面)
```
通过键入 `heurams -v` 查看版本:
```
~ $ heurams -v
HeurAMS 0.5.1 stable (fulcrum/支点), Linux
```
## 常见问题 (FAQ)
详见[常见问题](docs/FAQ_zh.md).
## 项目架构
详见[架构说明](docs/ARCHITECTURE_zh.md).
## 参与项目
欢迎参与到项目协作中!\
详见[贡献指南](CONTRIBUTING_zh.md).\
关于 AI 辅助开发的说明, 请参阅 [AGENTS.md](AGENTS.md).
## 项目标识
HeurAMS 项目标识如下, 文件(位图和矢量图)位于 `./src/heurams/assets/art/` 目录.
<img src="src/heurams/assets/art/banner128-light.png" height="96px" title="位图横幅(不透明)">
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
<img src="src/heurams/assets/art/logo.svg" height="96px" title="矢量图标">
<img src="src/heurams/assets/art/logo-mono-light.svg" height="96px" title="单色明亮矢量图标">
<img src="src/heurams/assets/art/logo-mono-dark.svg" height="96px" title="单色暗色矢量图标">
</div>
颜色分别是: `#1660A5 (海蓝色)` `#545F70 (蓝灰色)` `#FFFFFF (单色明亮图标白色)` `#1A1A1A (单色暗色图标深黑色)` `#2f2f35 (文字颜色)`.
## 许可证
### 项目本身
本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款, 较标准 AGPL-3.0 更宽松.
详见根目录下 [LICENSE](LICENSE) 文件.
### 第三方代码
项目在 `src/heurams/vendor/` 目录下嵌入或在其他位置直接使用了以下第三方代码或其衍生作品 (可能有修改):
#### SM.js
- 上游版本: commit `6e3bb4a` (2015年2月4日上游已停止维护)
- 引用方式: 将 coffeescript 重写为 python 并间接引用, 数学原理一致; 并对重写后代码进行逻辑, 性能与标准化 API 改进
- 位置: `src/heurams/kernel/algorithms/sm15m.py`
- 原项目: [SM.js](https://github.com/slaypni/SM-15)
- 原版权: Copyright (c) 2014 Kazuaki Tanida
- 原许可证: MIT License
本项目受益于他们无私且优秀的工作.
+438
View File
@@ -0,0 +1,438 @@
## Overall Architecture Overview
```mermaid
graph TB
subgraph "User Interface Layer (TUI)"
TUI[Textual App]
Screens[Application Screens]
Widgets[Puzzle Widgets]
end
subgraph "Kernel Layer"
Reactor[Scheduling Reactor]
Algorithms[Algorithm Modules]
Particles[Data Models]
Puzzles[Puzzle Engine]
RepoLib[Repository System]
Auxiliary[Auxiliary Tools]
end
subgraph "Service Layer"
Config[Config Management ConfigDict]
Logger[Logging System]
Timer[Time Service]
Audio[Audio Service]
TTS[TTS Service]
Favorites[Favorites Management]
Attic[Persistence]
Hasher[Hash Service]
end
subgraph "Provider Layer"
AudioProv[Audio Provider]
TTSProv[TTS Provider]
LLMProv[LLM Provider]
end
subgraph "Data Layer"
RepoDir[TOML/JSON Repository Directory]
ConfigDir[TOML Config Directory]
Logs[Log Files]
end
TUI --> Screens
Screens --> Reactor
Screens --> RepoLib
Screens --> Widgets
Widgets --> Puzzles
Widgets --> Reactor
Reactor --> Algorithms
Reactor --> Particles
Reactor --> Puzzles
Particles --> RepoLib
RepoLib --> Config
RepoLib --> Auxiliary
Auxiliary --> Lict
Auxiliary --> Evalizer
TUI --> Config
TUI --> Logger
TUI --> Audio
TUI --> TTS
Config --> ConfigDir
Audio --> AudioProv
TTS --> TTSProv
Attic --> RepoDir
```
## Data Model
The project uses the physical particle metaphor as its core, decomposing memory units into three models:
### Nucleon - Content Layer
```
Nucleon(ident, payload, common)
```
- **Read-only** content container. Compiles and expands `payload` and `common` via `Evalizer` (an `eval()`-based template system).
- Contains `puzzles` field, defining which puzzle types this memory unit supports.
- Created by pairing `repo.payload` and `repo.typedef["common"]`.
- Once created, content cannot be modified (`__setitem__` raises `AttributeError`).
### Electron - State Layer
```
Electron(ident, algodata, algo_name)
```
- Wrapper for algorithm state data. Each Electron is bound to one algorithm (`algorithms[algo_name]`).
- `algodata` is a **reference** to the corresponding dictionary in the repository's `algodata.lict` — modifications are persisted immediately.
- Core methods: `activate()` (mark as activated), `revisor()` (rating iteration), `is_due()` (due check).
### Orbital - Strategy Layer
```
orbital = {
"schedule": ["quick_review", "recognition"],
"routes": {
"quick_review": [["MCQ", "1.0"], ["Cloze", "0.5"]],
"recognition": [["Recognition", "1.0"]],
}
}
```
- A plain dictionary defining the review phase flow and puzzle selection strategy within each phase.
- Each phase corresponds to a list of `(puzzle_type, probability_coefficient)` tuples; coefficients >1 indicate forced repetition count.
### Atom - Runtime Assembly
```
Atom(nucleon, electron, orbital)
```
- Runtime composition of the three, with `runtime` flags (`locked`, `min_rate`, `new_activation`).
- The basic unit operated on by the UI and scheduling layers.
- `revise()` calls `electron.revisor(min_rate)` when `locked` is true, performing the final rating iteration.
**Relationship Diagram**:
```mermaid
graph LR
subgraph "Persistent Storage"
Payload[(payload.toml)]
Common[(typedef.toml)]
Algodata[(algodata.json)]
Schedule[(schedule.toml)]
end
subgraph "Runtime Assembly"
Nucleon -->|Content| Atom
Electron -->|State| Atom
Orbital -->|Strategy| Atom
end
Payload -->|Repo| Nucleon
Common -->|Repo| Nucleon
Algodata -->|Repo| Electron
Schedule -->|Repo| Orbital
```
## Scheduling Reactor
The Scheduling Reactor is the core business process engine, designed as a three-layer nested finite state machine (based on the `transitions` library).
### State Enumeration
| State Machine | State | Description |
|---------------|-------|-------------|
| **RouterState** | `unsure` | Initial state, auto-advances |
| | `quick_review` | Quick review phase |
| | `recognition` | New memory recognition phase |
| | `final_review` | Final comprehensive review phase |
| | `finished` | Complete, execute rating |
| **ProcessionState** | `active` | In progress |
| | `finished` | Completed |
| **ExpanderState** | `exammode` | Exam mode (frontal answering) |
| | `retronly` | Retrospective mode (recognition only) |
### State Machine Nesting Structure
```mermaid
graph TB
subgraph "Router (Global Router)"
R[Router<br/>State: unsure→quick_review<br/>→recognition→final_review<br/>→finished]
P1[Procession Queue 1: Quick Review]
P2[Procession Queue 2: New Memories]
P3[Procession Queue 3: Final Review]
R --> P1
R --> P2
R --> P3
end
subgraph "Procession (Single-Phase Queue)"
P1 --> E1[Expander Atom A]
P1 --> E2[Expander Atom B]
P1 --> E3[Expander Atom C]
M{forward} --> |Done| Finish((FINISHED))
end
subgraph "Expander (Single Atom Expander)"
E1 --> S[(Orbital Strategy)]
S -->|Probability Expansion| PZ1[Puzzle 1: MCQ]
S -->|Probability Expansion| PZ2[Puzzle 2: Cloze]
PZ1 -->|Rating| RPT[report]
PZ2 -->|Rating| RPT
RPT -->|finish| RETRO[retronly mode]
end
```
### Data Flow Detail
```
Router.__init__(atoms)
├─ Split atoms into new/old
│ ├─ old_atoms → Procession(quick_review) "Initial review"
│ └─ new_atoms → Procession(recognition) "New memories"
└─ all atoms → Procession(final_review) "Final review"
└─ Procession.forward()
├─ cursor >= len(atoms) → finish()
└─ cursor < len(atoms) → next_atom
└─ Procession.get_expander()
└─ Expander(atom, route)
├─ Read orbital.routes[route_value]
├─ Probability expansion → self.puzzles_inf
├─ exammode → display puzzles sequentially
├─ report(rating) → record minimum rating
├─ forward() → next puzzle or finish → retronly
└─ retronly → display Recognition
└─ Atom.revise()
└─ Electron.revisor(min_rate)
└─ Algorithm.revisor(algodata, feedback)
```
Rating accumulation: The final rating for an atom across multiple puzzles takes the minimum rating (`min_rate`) across all puzzles, ensuring strict evaluation.
## Algorithm System
All algorithms inherit from `BaseAlgorithm`, implemented in a class-method style, registered via the `algorithms` dictionary.
| Algorithm | File | Status | Description |
|-----------|------|--------|-------------|
| **SM-2** | `sm2.py` | ✅ Complete | Classic SuperMemo 1987 algorithm |
| **NSP-0** | `nsp0.py` | ✅ Complete | Non-spaced filtering scheduler |
| **SM-15M** | `sm15m.py` | ✅ Complete | SM-15 ported from CoffeeScript |
| **FSRS** | `fsrs.py` | ✅ Partial | Optimizer not available |
| **Base** | `base.py` | ✅ Base class | Defines `AlgodataDict` structure and defaults |
Each algorithm provides the following class methods:
| Method | Function |
|--------|----------|
| `revisor(algodata, feedback, is_new_activation)` | Iterate memory data based on rating |
| `is_due(algodata)` | Check if due for review |
| `get_rating(algodata)` | Get rating information |
| `nextdate(algodata)` | Get next review timestamp |
| `check_integrity(algodata)` | Validate algodata data structure integrity |
### Algorithm Data Structure (AlgodataDict)
```python
{
"real_rept": int, # Actual review count
"rept": int, # Current repetition count
"interval": int, # Interval in days
"last_date": int, # Last review date
"next_date": int, # Next due date
"is_activated": int, # Whether activated (0/1)
"last_modify": float, # Last modification timestamp
}
```
## Repository System (Repo)
The repository is a directory of TOML/JSON files with no database dependency.
### Directory Structure
```
data/repo/<package_name>/
├── manifest.toml # Meta info: title, author, package, desc
├── typedef.toml # Common metadata, puzzle definitions, annotations
├── payload.toml # Memory items (key=ident)
├── algodata.json # Algorithm state (key=ident)
└── schedule.toml # Orbital/review strategy
```
### Repo Class Design
```mermaid
classDiagram
class Repo {
+dict schedule
+Lict payload
+dict manifest
+dict typedef
+Lict algodata
+Path source
+Lict nucleonic_data_lict
+dict orbitic_data
+Lict electronic_data_lict
+from_repodir(source) ~Repo
+from_dict(dictdata) ~Repo
+create_new_repo() ~Repo
+persist_to_repodir(save_list, source)
+export_to_dict() dict
}
```
- `payload` and `algodata` use `Lict` (list+dict hybrid container), supporting dual-mode access.
- `_generate_particles_data()` automatically converts payload data to `Nucleon`-required format during initialization.
- Default save list: `default_save_list = ["algodata"]`, only persists algorithm state.
## Lict Collection
`Lict` extends `MutableSequence`, maintaining both list and dictionary access:
```python
lict = Lict()
lict.append(("key1", value1)) # List append
lict["key1"] # Dict access
lict[0] # Index access
lict.keys() # All keys
lict.dicted_data # Plain dict export
```
Dirty sync mechanism: modifying the list automatically syncs to the dictionary; modifying the dictionary automatically syncs to the list. Used for dual-mode access to `payload` and `algodata`.
## Configuration System (ConfigDict)
`ConfigDict` extends `UserDict`, a **singleton** TOML lazy-loading configuration manager.
### Configuration Directory Convention
```
data/config/
├── _.toml # Top-level defaults (recursive merge)
├── interface/
│ ├── _.toml # Interface layer defaults
│ ├── global.toml
│ └── puzzles.toml
├── services/
│ ├── _.toml # Services layer defaults
│ ├── audio.toml
│ └── tts.toml
└── repo/
└── _.toml
```
- `_.toml` files = defaults for that directory level, merged into parent
- Suffixed files = lazy-loaded on demand
- Subdirectories = recursive sub-configuration
### Context Management
```python
from heurams.context import config_var, ConfigContext
# Global access
config = config_var.get()
algo = config["interface"]["global"]["algorithm"]
# Scope override
with ConfigContext(test_config):
... # Temporarily use test configuration
```
## Provider System (Providers)
Pluggable backend implementations, registered via dictionaries in `providers/__init__.py`.
| Category | Provider | Description |
|----------|----------|-------------|
| **TTS** | `edge_tts` | Microsoft Edge TTS (online) |
| | `basetts` | Stub base class (not implemented) |
| **Audio** | `playsound` | Cross-platform audio playback |
| | `termux` | Android Termux environment |
| **LLM** | `openai` | OpenAI compatible API (not fully implemented) |
Selection method: `provider` field in `services/*.toml`.
## Puzzle System (Puzzles)
The puzzle engine generates evaluation views during review phases:
| Puzzle | File | Description |
|--------|------|-------------|
| **MCQ** | `mcq.py` | Multiple Choice Questions |
| **Cloze** | `cloze.py` | Cloze Deletion |
| **Recognition** | `recognition.py` | Recognition identification |
| **Guess** | `guess.py` | Word meaning guessing |
| **Base** | `base.py` | Abstract base class |
Puzzles are expanded probabilistically by the Orbital strategy in the `Expander`. Each atom can generate multiple puzzles, and each puzzle is rated independently.
## Service Layer
| Service | File | Description |
|---------|------|-------------|
| **Config** | `config.py` | `ConfigDict(UserDict)` TOML lazy-loading singleton |
| **Logger** | `logger.py` | `get_logger(name)` → hierarchical logger (`heurams.*`) |
| **Timer** | `timer.py` | `get_daystamp()` / `get_timestamp()`, supports configurable override |
| **Audio** | `audio_service.py` | Audio playback, routes to configured audio provider |
| **TTS** | `tts_service.py` | Text-to-speech, routes to configured TTS provider |
| **Favorites** | `favorite_service.py` | JSON5-persisted favorites manager (singleton) |
| **Attic** | `attic.py` | Structured pickle persistence, supports `<DAYSTAMP>`/`<TIMESTAMP>` placeholders |
| **Hasher** | `hasher.py` | MD5 hashing |
| **Epath** | `epath.py` | Dot-notation nested dict access (`epath(dct, "a.b.c")`) |
| **TextProc** | `textproc.py` | `truncate()`, `domize()`, `undomize()` |
Logging system: Each module creates its own logger via `get_logger(__name__)`. Log files rotate at 10MB, max 5 backups, appended to `heurams.log`.
## Complete Review Flow
```mermaid
sequenceDiagram
participant User as User
participant UI as TUI
participant Router as Router
participant Procession as Procession
participant Expander as Expander
participant Atom as Atom
participant Electron as Electron
participant Algo as Algorithm
User->>UI: Start Review
UI->>Router: Router(atoms)
Router->>Procession: Create review queue
Router->>Procession: Create new memory queue
Router->>Procession: Create final review queue
Procession->>Expander: Expand current atom
Expander->>Expander: Parse orbital strategy
Expander-->>UI: Display puzzle
User->>UI: Rate (1-5)
UI->>Expander: report(rating)
Expander->>Expander: forward() next puzzle
Expander-->>UI: Next puzzle or retrospective
Expander->>Expander: finish() → retronly
Expander-->>UI: Recognition retrospective
User->>UI: Final rating
UI->>Atom: revise()
Atom->>Electron: revisor(min_rate)
Electron->>Algo: revisor(algodata, feedback)
Algo-->>Electron: Update algodata
Algo-->>Atom: Update interval, next_date
Procession->>Procession: forward() next atom
Procession-->>Router: Queue complete
Router->>Router: Switch phase
Router-->>UI: Complete (finished)
UI->>User: Show summary
```
+416
View File
@@ -0,0 +1,416 @@
# Frequently Asked Questions
## What is a terminal emulator?
A terminal emulator is an application that simulates and uses a terminal within a graphical desktop environment, e.g., KDE Konsole, GNOME Terminal, Windows Terminal, iTerm2, etc.
The old, modest little black window on older Windows (conhost.exe) is also a terminal emulator, but it has poor support for this software's basic user interface (and all modern terminal applications). We recommend using WezTerm (supports sixel) or Windows Terminal (does not support sixel) on Windows.
## Does the software support mobile devices?
The basic user interface (Textual TUI) works well in Android Termux.
Additionally, the KiriMemo frontend under development is based on the KDE Kirigami framework and will natively support Android and iOS.
## What is the difference between HeurAMS and Anki?
At a high level:
| Aspect | HeurAMS | Anki |
|--------|---------|------|
| Data format | Text files (TOML/JSON), human-readable | Proprietary compressed format with SQLite and resources (.apkg) |
| Review modes | Multi-phase flow + multiple puzzle types | Single/double-sided flashcards |
| Algorithm system | Modular, pluggable, multiple algorithm options | Built-in SM-2 / FSRS |
| Plugin ecosystem | Smaller, manifests as "capability extensions" like new algorithms or services | Larger, but uses unrestricted "monkey patching" |
| User base | Small | Large |
| Existing resource richness | Low | High |
| AI-assisted unit set/deck creation | Natively supported | Difficult |
| License | AGPL-3.0 with additional exemption clause | AGPL-3.0 |
## Is the software free?
Yes, completely free and open source. You can use all features without paying anything.
## How do I use this dark-looking interface?
First, if you just want a light color scheme, press the `d` key or click the "d theme" button to switch to a light interface. :)
Thanks to Microsoft's decades of "the command line is outdated" education, and the poor experience of `conhost.exe` and `cmd.exe`, it's completely normal to feel uncomfortable with a terminal user interface.
But in reality, even though it looks like an old computer screen, Textual and terminal standards are more modern than you might think.
### You can use the mouse
Modern terminal emulators (such as Windows Terminal, Konsole, iTerm2, WezTerm, etc.) support a feature called "Mouse Tracking". When Textual starts, it sends special instructions to the terminal to report mouse events.
So contrary to what you might expect, you can actually click buttons with your mouse, just like in regular software.
### You can also use the keyboard
- `Tab` key switches focus between different areas
- `Arrow keys` move up and down in lists
- `Enter` confirms a selection
- `q` goes back
- Key hints are displayed on screen, e.g., `[n] Navigator` means pressing `n` opens the navigator
### Touchscreen also works
On tablets or phones in Termux, you can touch or swipe to operate.
## How do I start the software?
First, ensure Python (3.12.13 recommended) is installed on your system along with the required HeurAMS components.
### Windows
Open "Command Prompt" or "PowerShell", enter the following command and press Enter, or save it as a shortcut:
```
python -m heurams.interface
```
### macOS
Open the "Terminal" application and enter the above command.
### Linux
Open your terminal emulator (usually Ctrl + Alt + T) and enter the above command.
If you find typing the command every time too cumbersome, you can create a desktop shortcut or script file. See online tutorials for details.
## How do I exit the software?
Press the `q` key to return to the main screen, then exit.
Your learning progress is saved automatically and will not be lost.
## Images are very pixelated and blurry, what should I do?
This means the image is being displayed in Halfcell compatibility mode.
The terminal emulator needs to support the sixel image protocol for high-quality image display. For terminals that don't support it, the software can only display images in low-quality compatibility mode.
- WezTerm (all major OS): Supported
- KDE Konsole: Supported
- GNOME Terminal: Not supported
- iTerm2 (macOS): Supported
- Windows Terminal: Not supported
- mintty (Windows): Supported
If your terminal doesn't support images, other software features are unaffected — only images in memorization content won't be displayed.
## Chinese characters display as garbled text or boxes?
This means your terminal's Chinese font is not set correctly. Please check:
1. You are not using terminals like getty or xterm that explicitly do not support non-ASCII fonts
2. In the terminal settings, choose a font that supports Chinese, such as "Noto Sans SC", "Microsoft YaHei", "Source Han Sans"
3. Ensure the terminal's character encoding is set to UTF-8 (usually the default)
## Where is my data stored? Will it be lost?
Data is stored in the `data/` folder under the software installation directory:
- You can open and modify files directly with a text editor
- Copy the `data/` folder to a USB drive or cloud storage for backup (regular backups recommended)
- Even if the software is uninstalled, as long as you keep the `data/` folder, all learning records will be preserved when you reinstall and copy it back
## How do I share my unit sets with friends?
Find the corresponding folder under `data/repo/`, copy the entire folder, and send it to your friend. They can place it in their own `data/repo/` directory to use it.
You can also export as a single text file or compressed archive and share via messaging apps, email, etc.
## I can't copy/paste content?
Generally, in a terminal:
- Copy: Ctrl+Shift+C
- Paste: Ctrl+Shift+V
This differs from regular software habits (because Ctrl+C means "interrupt process" in terminal semantics), but you'll get used to it quickly.
## The font is too small/large?
Adjust the "font size" option in your terminal emulator settings.
The software follows the terminal's font settings.
## Why does my interface look different from the screenshots?
The screenshots were taken using Konsole on KDE Plasma desktop, 80x25 character size, with Cascadia Code and Noto Sans SC fonts.
If your terminal size is larger, the interface will have more room. Using different fonts or operating systems may result in slight visual differences.
Functionality is identical.
## What do the ratings (1-5) mean? How should I rate?
We should note that we strongly discourage the Anki-like approach of having users directly self-rate their performance (implemented as `basic_puzzle` in our program, used primarily for algorithm testing).
We believe this approach is highly subjective and requires you to think about "What score am I?", "Am I being too optimistic?", "Am I scoring too low?", "What if I rate incorrectly?" — a series of questions that interrupt the memorization process and cause anxiety. This essentially shifts responsibility to the user and contradicts cognitive science principles.
Moreover, this approach is detrimental to academic research and experimentation, as user self-rated data is unreliable.
Therefore, HeurAMS frontends have built-in automatic rating systems based on user behavior analysis, i.e., "puzzles".
It automatically rates you based on the difficulty of the question and your answering behavior (including but not limited to correctness, number of operations undone, effective answering time).
However, if you or a unit set chooses to use `basic_puzzle`, or if you plan to implement your own automatic rating system, the meaning of scores is as follows:
| Score | Meaning | Description |
|-------|---------|-------------|
| 1 | Completely forgotten | Couldn't recall at all, as if never learned |
| 2 | Vague | Seems familiar, but couldn't answer |
| 3 | Some impression | Recalled after thinking for a while, not very certain |
| 4 | Relatively smooth | Could answer, but hesitated slightly |
| 5 | Very easy | Immediately recalled, no effort |
- Scoring **1-2**: The software considers you haven't mastered it and will schedule another review soon
- Scoring **3**: Normal mastery, review at the planned interval
- Scoring **4-5**: Well mastered, the next review interval will be extended
We suggest you don't overthink it, just score based on your first instinct — the software will automatically adjust the review pace based on your ratings.
Of course, we still recommend avoiding this approach and using other puzzle types for evaluation when possible.
You might think this method allows users to directly "intervene" with the algorithm, similar to Baicizhan's "斩" (chop) feature for skipping familiar content. In reality, you don't need to do that: we have built-in "quick pass/correct response" functionality, equivalent to directly selecting "5".
## Do I need to open the software every day? What if I don't study?
In theory, you don't need to open it every day. The software automatically records when each knowledge point is due for review next.
However, it is recommended to open the software daily to check your status.
Even if you skip studying for days or weeks:
- Knowledge points you've already learned won't disappear — they'll just need a few more reviews next time
- Learning records are safely stored in the `data/` folder and won't be lost
For best results, try to follow the software's review reminders; but it's okay to skip a few days when you're busy.
## Can I study multiple subjects simultaneously?
Yes.
Each subject or course can be made into an independent "unit set".
## I changed computers. How do I migrate data?
1. On the old computer, copy the entire `data/` folder to a USB drive or cloud storage
2. Install HeurAMS on the new computer
3. Overwrite the new computer's `data/` folder with the one from the USB drive
All learning records, configurations, and unit sets are fully migrated.
## I don't understand some terminology
| Term | Meaning |
|------|---------|
| **Unit Set** | Like a "book" or "course", containing a series of knowledge points |
| **Puzzle** | A question type to test you (MCQ, Cloze, etc.) |
| **Algorithm** | The "intelligent scheduler" that decides when to review which knowledge point |
| **Review Queue** | The list of knowledge points due for review today |
| **Activation** | Starting to learn a knowledge point for the first time |
| **Due** | A knowledge point is due for review |
## The software is stuck/unresponsive?
1. First, wait a moment
2. If that doesn't work, close the terminal window directly
3. Reopen the software
4. If you have time, please report the issue. We apologize for the inconvenience.
## Will using both Anki and HeurAMS cause conflicts?
No.
They are independent software programs with no data interference. You can gradually migrate content to HeurAMS, or use both together.
## Do I need to install Python?
Yes. HeurAMS is a Python-based software.
- Windows/macOS: Download and install from python.org
- Linux: Python is usually pre-installed
- Android: Install the Termux app, then install Python within Termux (run `pkg in python`)
If you see an error like "python is not recognized as an internal or external command", Python is not properly installed or added to your system PATH. Search for "Python installation tutorial" and follow the steps.
The recommended Python version for HeurAMS is 3.12.13.
## Is the software safe? Could it contain viruses?
HeurAMS is open source software. All code is publicly available for inspection and contains no viruses or backdoors.
It only reads and writes to its own `data/` folder and will not touch other files on your computer.
## The software shows an error with a lot of text I don't understand?
1. Copy the error message
2. First check if this page covers your issue
3. If not, upload it along with the software logs to issues, and we will handle it as soon as possible
## Can I pause mid-review? Will it restart from the beginning?
There is currently no save-state functionality, but we will add it soon.
## How do I see how much I've learned? Are there statistics?
The dashboard interface displays statistical information.
You can return to the dashboard anytime via the navigator.
## I feel the review is too fast/slow. Can I adjust it?
Yes. You can change the review pace by switching algorithms, adjusting algorithm parameters, or changing the number of memory units.
Relevant settings can be found in the settings interface.
## Can I change the interface color/theme?
Yes. The Textual framework provides various themes.
## I accidentally deleted something. Can I recover it?
This depends on whether your system has a recycle bin enabled. The software itself does not have an auto-backup function.
## Can I carry the software on a USB drive?
Yes. Copy the entire HeurAMS folder to a USB drive, install Python on any computer, and run it. Learning data in the `data/` directory will also be carried along.
## How do I turn off voice reading?
Find the TTS-related option in the settings interface and disable it.
## Where can I download unit sets made by others?
Currently, there is no official unit set marketplace.
However, as the community grows, users may share unit sets in the future. You can also share unit sets with friends.
## Can I export learning content for printing?
Yes.
The software supports exporting unit sets as a single text file, which you can open and print with any text editor. You can also copy content directly into software like Word.
## I want to start over from scratch. How do I reset?
Delete the `algodata.json` file in the corresponding unit set folder under `data/repo/` to reset all learning progress.
## How do I create my own unit set?
Create a new folder under `data/repo/` with the following files:
```
data/repo/my_pack/
├── manifest.toml # Meta info: title, author, etc.
├── typedef.toml # Common field definitions and puzzle configuration
├── payload.toml # Memory item content
├── algodata.json # Algorithm state data (can be empty)
└── schedule.toml # Review strategy configuration
```
You can also use the tools we provide to convert from CSV format, or use AI tools to generate unit sets.
## How do I switch algorithms?
Detailed instructions are available in the settings interface.
## How do I import from Anki?
There is currently no migration tool, as the design philosophies of the two software programs differ.
But keep an eye on the HeurStudio project — it will fundamentally solve content creation and migration issues. :)
## Why not Flutter?
Flutter is an excellent framework for building cross-platform graphical interfaces. One of HeurAMS's design goals is to keep the core library independent of any specific frontend.
However, Flutter is not as good as PyOtherSide when it comes to "integrating Python" — it can only communicate with the library through RPC standard components. Additionally, Flutter's desktop multi-window support has not been officially and stably supported, so we temporarily abandoned Flutter in favor of Kirigami.
Currently, we have prioritized the development of a Textual-based TUI frontend and a Kirigami-based native frontend. However, this does not preclude the possibility of a Flutter or other framework frontend in the future.
If you are interested in developing a Flutter frontend, please refer to the [Contributing Guide](../CONTRIBUTING.md#new-user-interface-frontends).
## Does the software need internet access?
Core review functionality works completely offline. The following features require internet:
- Edge TTS provider for Text-to-Speech
- LLM providers (e.g., OpenAI compatible API)
- Downloading unit sets from remote repositories
## What is the "local API call exemption" in the license?
In short, if you use HeurAMS in your own program through local inter-process API communication (such as RPC calls on the same host) without network forwarding, your program is not bound by the AGPL-3.0 license.
This additional clause is designed to encourage the development of third-party frontends and tools.
Therefore, HeurAMS's license is effectively more permissive than the original AGPL-3.0.
## What is the difference between HeurAMS and Baicizhan?
At a high level:
| Aspect | HeurAMS | Baicizhan |
|--------|---------|-----------|
| Usage scenario | Computer/Terminal | Mobile App |
| Learning content | Any knowledge, unlimited languages and subjects | English vocabulary |
| Memory strategy | Multiple algorithms, customizable review flow | Fixed algorithm, not adjustable |
| Quiz methods | Multiple puzzle types: MCQ, Cloze, Recognition | Picture-word matching, audio-meaning selection, etc. |
| Content creation | Self-created or imported, completely free | Official word books only |
| Cost | Completely free, no in-app purchases | Free memorization + paid courses |
| Data ownership | Data in your hands, text files | Data cannot be extracted |
| Offline use | Core functionality fully offline | Some features require internet |
| Learning statistics | Basic statistics | Check-in/Leaderboard/Social |
## Baicizhan has picture association memory. Does HeurAMS have it too?
Yes.
If your terminal supports image display (such as Konsole or WezTerm), unit sets can include images that will be displayed during review.
However, you need to add images to the unit set yourself.
## Baicizhan has check-ins and leaderboards. Does HeurAMS have them?
Currently no.
HeurAMS does not have check-ins, leaderboards, or social features, and does not collect your learning data.
## Baicizhan has ready-made word books. Where can I find content for HeurAMS?
Baicizhan's courses are officially made. HeurAMS content needs to be created by yourself or obtained from the community.
See "How do I create my own unit set?" for details.
## Baicizhan is convenient to use on mobile. Can HeurAMS be used on mobile?
Yes, but at this stage it requires some setup.
Android users can install Termux to run HeurAMS's basic user interface.
Additionally, the KiriMemo frontend under development will natively support Android and iOS, requiring no setup from users.
## Baicizhan helps with vocabulary. What else can HeurAMS be used for?
Any knowledge that requires memorization: foreign language vocabulary, medical terminology, legal texts, historical dates, chemical equations, programming syntax, musical notation...
Unit set content is entirely defined by you.
## Baicizhan is effective for learning English. Will HeurAMS be less effective?
Considering that Baicizhan's algorithms and vocabulary database are effectively closed-source, we have no way of knowing the algorithm source.
However, HeurAMS's architecture design ensures that once a unit set is created, it can be at least as effective as Baicizhan — or even better.
HeurAMS's spaced repetition algorithms are based on the same cognitive science principles, and the algorithms are transparent and adjustable, allowing you to freely choose the scheduling strategy that best suits you.
## How do I participate in the project?
See the [Contributing Guide](../CONTRIBUTING.md).
Even if you are not a developer, you can participate by writing documentation, creating memory unit sets, translating the interface, answering questions, and more.
+4 -2
View File
@@ -33,6 +33,8 @@
## 黑乎乎的这个界面我怎么用?
首先, 如果您只是想要一个亮色模式, 可以直接按下 `d` 键或点击 "d 主题" 按钮, 这会让您的界面变得白乎乎的(
得益于微软几十年对用户进行的"命令行即落后"教育, 以及 `conhost.exe``cmd.exe` 的糟糕体验, 您对终端用户界面感到不适应是完全正常的.
但实际上, 虽然看起来像老式电脑屏幕, Textual 和终端标准其实比您想象得要现代一些.
@@ -331,7 +333,7 @@ Flutter 是构建跨平台图形界面的优秀框架, HeurAMS 的设计目标
当前我们优先开发了基于 Textual 的 TUI 前端和基于 Kirigami 的原生前端, 但这不排除未来出现 Flutter 或其他框架前端的可能性.
如果您有兴趣开发 Flutter 前端, 欢迎参考[贡献指南](CONTRIBUTING.md#%E6%96%B0%E7%9A%84%E7%94%A8%E6%88%B7%E7%95%8C%E9%9D%A2%E5%89%8D%E7%AB%AF).
如果您有兴趣开发 Flutter 前端, 欢迎参考[贡献指南](../CONTRIBUTING_zh.md#%E6%96%B0%E7%9A%84%E7%94%A8%E6%88%B7%E7%95%8C%E9%9D%A2%E5%89%8D%E7%AB%AF).
## 软件需要联网吗?
@@ -409,6 +411,6 @@ HeurAMS 的间隔重复算法基于相同的认知科学原理, 且算法透明
## 如何参与项目?
详见[贡献指南](CONTRIBUTING.md).
详见[贡献指南](../CONTRIBUTING_zh.md).
即使不是开发者, 您也可以通过编写文档、制作记忆单元集、翻译界面、答疑等方式参与.
+83
View File
@@ -0,0 +1,83 @@
## Features
### Spaced Repetition Scheduler
> Numerous publications have extensively discussed the effects of different repetition intervals on learning outcomes. In particular, the spacing effect is considered a universal phenomenon. The spacing effect refers to the fact that learning performance improves when repetitions are distributed/spaced rather than massed. Therefore, it has been proposed that the optimal repetition interval for learning is the **longest interval that does not lead to forgetting**.
- The software works out of the box, using the default `SM-2` algorithm for learning without additional configuration
- The algorithm module is a first-class citizen in the kernel (`heurams.kernel`), which natively supports pluggable algorithms of various types
- No complicated plugins required — algorithms can be quickly switched and tuned per unit set, enabling researchers to easily modify algorithm modules for convenient research and testing
- Defaults to the `SM-2` simple spaced repetition algorithm, also used as the default flashcard scheduler for Anki
- Also includes `NSP-0` non-spaced filtering algorithm for rapid content screening, `FSRS` advanced spaced repetition algorithm as a more efficient scheduler, and `SM-15M` (ported from sm.js project) complex spaced repetition algorithm (reverse-engineered)
- Algorithm modules can tag memorization items, dynamically plan memory interval schedules for each unit, and track memory feedback data to optimize long-term retention and stability
- Thanks to the modular architecture and unit set structure design, the same unit set can coexist and interoperate with any algorithm — extremely friendly for researchers and users exploring/experimenting with efficient methods
### Multimodal Learning Process
Unlike Anki's SQLite `.apkg` packages, we insist on using human-readable folder-organized unit sets, which brings several benefits:
- **Human-readable**: You can freely modify memorization payload data with any tool, even a simple text editor, without opening the software
- **Metadata configuration**: Extremely flexible configuration — you can freely combine, remix, or even create new content
- **Quiz, algorithm, and knowledge isolation**: A piece of knowledge is no longer a single flashcard; it can not only be scheduled with different algorithms but also tested with multiple parallel puzzle types, greatly enhancing learning effectiveness and richness. As a learner, you don't need to worry about complexity — just download a unit set from the cloud and use these features out of the box!
- **Multimodal learning**
- The software integrates Text-to-Speech (TTS), audio, and LLM modules — all pluggable, extensible, and driver-switchable, creating great content richness
- Built-in puzzle types include Multiple Choice Questions (MCQ), Cloze Deletion, and Recognition, all applicable to the same unit or selectively enabled
- Natively supports dynamic content generation with macro-driven template systems that generate knowledge point explanations based on context or even LLMs
- In the era when SuperMemo series dominated spaced repetition research, Wozniak already stated, "If you cannot understand knowledge, you don't need to memorize it." Today, we still believe understanding is the foundation of memorization
- **Cloud sync and sharing optimized**:
- Since memory data and unit set files are text files, fast incremental sync is possible without uploading all files, and the design natively supports version control
- If you want to share a single file, the software supports exporting as a compressed package or merging into a single text file for sharing on platforms like pastebin
- **Performance**: Thanks to the modern, chunked file organization structure, HeurAMS achieves agile and low-footprint user experience using only Python while maintaining high flexibility
- **AI-assisted friendly**: Imagine you have some `.apkg` decks or a large amount of textbook content — you can conveniently and efficiently use AI tools to create HeurAMS-compatible unit sets
### Built-in Practical User Interface
Although not the only frontend, the responsive Textual framework-based built-in terminal user interface has unique advantages in many scenarios:
- Cross-platform, with touch/mouse/keyboard multi-operation modes
- Compatible with almost all modern terminal emulators
- High-quality image display on terminal emulators that [support the sixel protocol](https://www.arewesixelyet.com/)
- Low-resolution compatibility display mode for terminals that don't support sixel
- Deployable as a service via textual-web, usable from any browser
- Clean, intuitive, keyboard-friendly, full-featured, and efficient UI design
- Easy to embed: can run in getty/kmscon without any desktop graphics service
- Low resource footprint, smooth operation
- Convenient for testing and debugging the library
View [Screenshots](SCREENSHOTS.md).
## Package Dependency Groups
Since some dependencies are only needed by a few features, we split optional dependencies into fine-grained groups. Here are the dependency groups:
| Dependency Group | Included Modules | Description |
|------------------|------------------|-------------|
| Build System | hatchling | Build-time installation |
| Minimal | tabulate, toml, transitions, click | Core driver libraries, always required |
| interface | textual | Basic user interface dependencies |
| algo-fsrs | fsrs | FSRS algorithm module |
| tts-edgetts | edge-tts | Microsoft Text-to-Speech |
| llm | llms-py | API calls |
| audio-playsound | playsound3 | General audio module |
| dev | zmq, pytest, pytest-cov | Development, debugging, and testing tools |
| basic | [tts-edgetts], [llm], [algo-fsrs | Lighter dependency group for user experience (recommended) |
| all | All of the above | Complete installation group |
## About This Repository
This repository is the Python implementation of the HeurAMS core library.
It contains data models and framework, and includes a built-in frontend based on the Textual framework (interface submodule).
Besides learning through the built-in frontend, developers can import the `heurams` library in a Python environment or communicate with `heurams` library instances via `RPC`, using the framework to build other auxiliary memory frontends or applications.
All repositories of the project group:
| Project Name | Status | Description | Package Name | Tech Stack | Target Platform |
| :--- | :--- | :--- | :--- | :--- | :--- |
| HeurAMS | In Development<br/>Prototype Usable | General core library with basic UI | `heurams` | Python | Standard Python Environment |
| KiriMemo | In Development<br/>Prototype Usable | Modern cross-platform frontend based on KDE technology | `org.kde.kirimemo` | C++, Qt6, Kirigami, PyOtherSide | Desktop & Mobile |
| ArkMemo | In Development | Modern cross-platform frontend based on ArkUI | `top.pluv27.arkmemo` | ArkTS, ArkUI | Mobile Devices |
| HeurStudio | Planned | AI-assisted single-unit set advanced creation and editing tool | `top.pluv27.heurstudio` | C++, Qt6, Kirigami, PyOtherSide | Desktop |
| HeurSync | In Development | User data sync server<br/>with Web frontend and leaderboard | `heursync` | Go, SQL | Web & Server |
| HeurRepo | In Development | Unit set document source server<br/>and sharing platform | `heurrepo` | Go, SQL | Web & Server |
Although the last three might sound a bit ambitious, our roadmap is clear.
+84
View File
@@ -0,0 +1,84 @@
## 特性
### 间隔重复调度器
> 许多出版物都广泛讨论了不同重复间隔对学习效果的影响. 特别是, 间隔效应被认为是一种普遍现象. 间隔效应是指, 如果重复的间隔是分散/稀疏的, 而不是集中重复, 那么学习任务的表现会更好. 因此, 有观点提出, 学习中使用的最佳重复间隔是**最长的, 但不会导致遗忘的间隔**.
- 软件开箱即用, 无需多加配置即可使用默认的 `SM-2` 算法进行学习
- 算法模块是 "潜进" 内核 (heurams.kernel) 中的一等公民, 内核天然支持插拔各型算法
- 无需安装繁杂的插件即可分单元集完成算法快速切换与调优, 研究者可以方便地修改算法模块以便捷地进行研究与测试
- 默认使用 `SM-2` 简单间隔重复算法, 此算法亦用作 `Anki` 闪卡记忆软件的默认闪卡调度器
- 还内置了 `NSP-0` 筛选用非间隔重复算法以便快速筛选记忆内容, `FSRS` 先进间隔重复算法作为效率更高的调度器, 与 `SM-15M (移植自 sm.js 项目)` 复杂间隔重复算法(逆向工程)
- 算法模块可以标记记忆项目, 也可以动态规划每个记忆单元的记忆间隔时间表, 动态跟踪记忆反馈数据, 以优化长期记忆保留率与稳定性
- 得益于项目的模块化架构与单元集结构设计, 同一个单元集可以与任意种算法共存并互通, 这对研究者及想探索/实验高效率方法的用户极其友好
### 多模态学习进程
与 Anki 的 SQLite `.apkg` 包不同, 我们坚持使用人类可读的文件夹组织单元集, 这带来了若干好处, 包括:
- 人类可读: 您可以用任意工具, 乃至一个记事本自由修改记忆载荷数据而无需打开软件
- 元数据配置: 配置自由度极高, 可以任意组合, 重造, 乃至创造新内容
- 测验, 算法与知识互相隔离: 一条知识不再是单一的闪卡, 不仅可以用若干不同的算法规划, 还可以用多种并行的谜题类型测验, 极大地提升学习效果和丰富度. 作为学习者, 您无需担忧概念复杂--仅需从云端下载单元集即可开箱即用上述特性!
- 多模态学习
- 软件自身集成了文本转语音 (TTS) , 音频与语言模型 (LLM) 模块, 这些功能乃至功能本身都是可插拔, 可扩展, 可切换驱动的, 这为内容创建了极大的丰富度
- 软件内置多种谜题类型, 包括选择题 (MCQ), 填空题 (Cloze) 与识别题 (Recognition), 您可在同一单元应用多种, 或是选择性启用
- 软件天然支持动态内容生成, 支持宏驱动的模板系统, 根据上下文乃至语言模型动态生成知识点的解析
- 在间隔重复研究尚被 SuperMemo 系列独占的时代, Wozniak 就早已表示 "如果不能理解知识, 就无需记忆它". 今天, 我们依然相信理解是记忆的基石
- 云同步与分享优化:
- 由于记忆数据和单元集文件都是文本文件, 故可进行快速的增量同步而无需完整地上传所有文件, 并且设计天然支持版本控制
- 如果您想分享单文件, 软件也支持导出为压缩包或合并成单文本文件以通过纯文本文件形式在 pastebin 等平台分享
- 性能提升: 得益于现代且支持分块的文件组织结构, 潜进能在保持高自由度的同时仅使用 python 就能达到敏捷且低占用的用户体验
- AI 辅助友好: 想象您有一些 `.apkg` 牌组或一大段教材内容, 您可以方便且高效率地使用 AI 工具以创建可在 HeurAMS 使用的单元集
### 内置实用用户界面
尽管不是唯一前端, 但响应式 Textual 框架构建的内置终端用户界面在多种场景下仍具有独特优势:
- 跨平台, 并支持触屏/鼠标/键盘多操作模式
- 与几乎所有现代终端模拟器相容
- 对于<a href="https://www.arewesixelyet.com/" target="_blank" rel="noopener noreferrer">支持 sixel 协议的终端模拟器</a>, 可高清显示图像内容
- 对于不支持 sixel 协议的终端模拟器, 也支持图片低清的兼容显示模式
- 可通过 textual-web 作为服务部署, 并在任意浏览器使用
- 简洁直观, 键盘友好, 全功能且高效率的用户界面设计
- 易于嵌入: 可在 getty/kmscon 中运行而无需任何桌面图形服务
- 资源占用小, 运行流畅, 不拖泥带水
- 便于测试与调试程序库
查看[屏幕截图](SCREENSHOTS_zh.md).
## 包依赖组说明
由于部分依赖只被少数功能需要, 所以我们把可选依赖分得比较细, 前面提供的命令会安装部分可选依赖, 以下是依赖组列表:
| 依赖组 | 包含模块 | 说明 |
|--------|----------|------|
| 构建系统 | hatchling | 构建时安装 |
| 最小化安装 | tabulate, toml, transitions, click | 核心驱动程序库, 始终必需 |
| interface | textual | 基本用户界面依赖 |
| algo-fsrs | fsrs | FSRS 算法模块 |
| tts-edgetts | edge-tts | 微软文本转语音 |
| llm | llms-py | API 调用 |
| audio-playsound | playsound3 | 通用音频模块 |
| dev | zmq, pytest, pytest-cov | 开发调试与测试工具 |
| basic | [tts-edgetts], [llm], [algo-fsrs] | 适用于用户体验的较轻依赖组(推荐) |
| all | 以上所有依赖 | 完整安装组 |
## 关于此仓库
此仓库为 HeurAMS "潜进" 的核心程序库在 python 语言下的实现\
包含数据模型与框架, 并内置了基于 textual 框架的前端实现 (interface 子模块)\
除了通过内置前端进行学习外, 开发者也能在 python 环境中导入 `heurams` 库或使用 `RPC``heurams` 程序库实例通讯, 使用框架构建其他辅助记忆功能前端或其他应用程序
项目组的所有仓库如下:
| 项目名称 | 状态 | 说明 | 包名 | 技术栈 | 目标平台 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| HeurAMS | 开发中<br/>原型可用 | 提供通用核心程序库与基本用户界面 | `heurams` | Python | 标准 Python 环境 |
| KiriMemo | 开发中<br/>原型可用 | 基于 KDE 技术的现代跨平台前端 | `org.kde.kirimemo` | C++, Qt6, Kirigami, PyOtherSide | 桌面与移动设备 |
| ArkMemo | 开发中 | 基于 ArkUI 的现代跨平台前端 | `top.pluv27.arkmemo` | ArkTS, ArkUI | 移动设备 |
| HeurStudio | 计划中 | AI 辅助的单体单元集高级创建与编辑工具 | `top.pluv27.heurstudio` | C++, Qt6, Kirigami, PyOtherSide | 桌面 |
| HeurSync | 开发中 | 用户数据同步服务器<br/>集成 Web 前端与排行榜 | `heursync` | Go, SQL | 网页与服务器 |
| HeurRepo | 开发中 | 单元集文档源服务器<br/>与单元集分享平台 | `heurrepo` | Go, SQL | 网页与服务器 |
尽管现在后三样有点画大饼的意思, 但是我们的路线是明了的
+73
View File
@@ -0,0 +1,73 @@
# User Interface Screenshots
The HeurAMS project currently has two frontend implementations. This document presents their screenshots (kept as up-to-date as possible):
- **Textual Basic User Interface** (`heurams.interface`): A built-in cross-platform TUI built with the Python Textual framework, supporting touch, mouse, and keyboard operation modes. This is the current out-of-the-box default frontend.
- **KiriMemo** (`org.kde.kirimemo`): A modern cross-platform frontend based on the KDE Kirigami framework, built with C++ and QML, directly reusing the Python kernel via `PyOtherSide`, providing native experiences across multiple platforms (not yet stable).
<!--- ArkMemo (top.pluv27.arkmemo): A modern mobile device frontend based on ArkUI, built with ArkTS, calling the Python kernel via API, providing native experiences for Android, HarmonyOS, and iOS platforms (not yet stable)-->
Feel free to contribute code to existing frontends or develop your own.\
See the [Contributing Guide](../CONTRIBUTING.md#new-user-interface-frontends).
## Screenshots of the Basic User Interface
> The terminal emulator used for screenshots is KDE Konsole.\
> Fonts used: Cascadia Code and Noto Sans SC.\
> Terminal size set to 80x25 (the software also supports larger terminal sizes).
### Dashboard & Navigator
The dashboard provides an overview of the learning panel, including entry points for different functional areas, statistics, and a unit set overview.\
The navigator is a practical modal window that lets you quickly switch between various features. Press the `n` key or click the button below to open/close the navigator from any screen.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="../screenshots/dashboard_1.png" width="48%">
<img src="../screenshots/dashboard_2.png" width="48%">
<img src="../screenshots/navigator_1.png" width="48%">
</div>
### Preparation Screen & Pre-cache Tool
The preparation screen shows unit set basic information and the learning status of each unit, providing entry points for learning and pre-caching.\
The pre-cache tool allows you to pre-cache TTS resources to ensure a smooth review experience and offline review capability. Even without pre-caching, resources are loaded automatically during review playback.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="../screenshots/preparation.png" width="48%">
<img src="../screenshots/precache_1.png" width="48%">
</div>
### Review Queue Screen
The main screen for queue-based learning and memorization.\
The same knowledge point can generate multiple puzzle types for evaluation. The software includes built-in Cloze, Recognition, and other test types, allowing you to complete different tests sequentially during the review flow.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="../screenshots/memoqueue_cloze_1.png" width="48%">
<img src="../screenshots/memoqueue_recognition_1.png" width="48%">
<img src="../screenshots/memoqueue_recognition_2.png" width="48%">
</div>
### Settings Screen
The configuration screen includes algorithm selection, provider switching for audio and various services, as well as interface and algorithm settings.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="../screenshots/setting_1.png" width="48%">
<img src="../screenshots/setting_2.png" width="48%">
</div>
### Other Screens
The favorites manager lets you manage your manually marked personal collections.\
The about page provides program version number, license information, and more.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="../screenshots/about_1.png" width="48%">
<img src="../screenshots/favmanager_1.png" width="48%">
</div>
## Screenshots of the KiriMemo Frontend
Screenshots will be added when the KiriMemo frontend development stabilizes.
<!-- TODO: Add screenshots -->
+14 -13
View File
@@ -4,9 +4,10 @@
- Textual 基本用户界面 (heurams.interface): 基于 Python Textual 框架构建的程序库内置跨平台 TUI 界面, 支持触屏、鼠标、键盘多操作模式, 是当前开箱即用的默认前端.
- KiriMemo (org.kde.kirimemo): 基于 KDE Kirigami 框架的现代跨平台前端, 使用 C++ 和 QML 构建, 通过 `PyOtherSide` 直接复用 Python 内核, 为多种平台提供原生体验 (尚未稳定).
<!--- ArkMemo (top.pluv27.arkmemo): 基于 ArkUI 的现代移动设备前端, 使用 ArkTS 构建, 通过 API 调用 Python 内核, 为 Android, HarmonyOS, iOS 平台提供原生体验 (尚未稳定)-->
欢迎为现有前端贡献代码, 或开发您自己的前端.\
详见[贡献指南](CONTRIBUTING.md#%E6%96%B0%E7%9A%84%E7%94%A8%E6%88%B7%E7%95%8C%E9%9D%A2%E5%89%8D%E7%AB%AF).
详见[贡献指南](../CONTRIBUTING_zh.md#%E6%96%B0%E7%9A%84%E7%94%A8%E6%88%B7%E7%95%8C%E9%9D%A2%E5%89%8D%E7%AB%AF).
## 基本用户界面前端的截图
@@ -20,9 +21,9 @@
导航器是一个实用的模态窗口, 能带您在多种功能间自如切换, 按 `n` 键或单击下方按钮可在任意界面迅速打开/关闭导航器.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/dashboard_1.png" width="48%">
<img src="screenshots/dashboard_2.png" width="48%">
<img src="screenshots/navigator_1.png" width="48%">
<img src="../screenshots/dashboard_1.png" width="48%">
<img src="../screenshots/dashboard_2.png" width="48%">
<img src="../screenshots/navigator_1.png" width="48%">
</div>
### 准备界面与预缓存工具
@@ -31,8 +32,8 @@
预缓存工具使您能提前预缓存文本转语音资源以确保复习流程的顺畅体验和离线复习能力, 但即使您不预先缓存, 资源也会在复习播放时被自动加载.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/preparation.png" width="48%">
<img src="screenshots/precache_1.png" width="48%">
<img src="../screenshots/preparation.png" width="48%">
<img src="../screenshots/precache_1.png" width="48%">
</div>
### 记忆队列界面
@@ -41,9 +42,9 @@
同一知识点可产生多种谜题类型的评估方式, 软件内置完形填空与识别题等多种测试类型, 您可在复习流程中按顺序完成不同测试.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/memoqueue_cloze_1.png" width="48%">
<img src="screenshots/memoqueue_recognition_1.png" width="48%">
<img src="screenshots/memoqueue_recognition_2.png" width="48%">
<img src="../screenshots/memoqueue_cloze_1.png" width="48%">
<img src="../screenshots/memoqueue_recognition_1.png" width="48%">
<img src="../screenshots/memoqueue_recognition_2.png" width="48%">
</div>
### 设置界面
@@ -51,8 +52,8 @@
配置界面包含算法选择、音频与多种服务的提供者切换、以及界面与算法设置等选项.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/setting_1.png" width="48%">
<img src="screenshots/setting_2.png" width="48%">
<img src="../screenshots/setting_1.png" width="48%">
<img src="../screenshots/setting_2.png" width="48%">
</div>
### 其他界面
@@ -61,8 +62,8 @@
关于页面提供了程序版本号、许可协议等信息.
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/about_1.png" width="48%">
<img src="screenshots/favmanager_1.png" width="48%">
<img src="../screenshots/about_1.png" width="48%">
<img src="../screenshots/favmanager_1.png" width="48%">
</div>
## KiriMemo 前端的截图
+12 -5
View File
@@ -1,7 +1,7 @@
[project]
name = "heurams"
version = "0.5.0"
authors = [{ name = "pluvium27", email = "pluvium27@outlook.com" }]
version = "0.5.1"
authors = [{ name = "Wang Zhiyu", email = "pluvium27@outlook.com" }]
description = "Heuristic Auxiliary Memory Scheduler"
readme = "README.md"
requires-python = ">=3.12"
@@ -13,9 +13,12 @@ license = "AGPL-3.0-or-later"
license-files = ["LICENSE"]
dependencies = [ # 这些依赖只能驱动 kernel 程序库
"click>=8.3.3",
"fastapi>=0.136.1",
"tabulate>=0.10.0",
"toml>=0.10.2",
"transitions>=0.9.3",
"uvicorn[standard]>=0.47.0",
]
[project.optional-dependencies]
@@ -60,7 +63,6 @@ default = true
[project.scripts]
heurams = "heurams.__main__:main"
heurams-tui = "heurams.interface.__main__:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
@@ -72,5 +74,10 @@ markers = [
]
[build-system]
requires = ["uv_build>=0.7.19"]
build-backend = "uv_build"
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"httpx>=0.28.1",
]
+1 -4
View File
@@ -1,7 +1,4 @@
print("欢迎使用 HeurAMS 及其组件!")
# 补充日志记录
from heurams.services.logger import get_logger
logger = get_logger(__name__)
logger.info("欢迎使用 HeurAMS 及其组件!")
logger.info("HeurAMS is imported")
+113 -18
View File
@@ -1,23 +1,118 @@
import heurams.services.version as ver
"""命令行入口
"""
import platform
import click
from heurams.i18n import _, setup_locale
from heurams.services.version import ver, stage, codename, codename_cn
from heurams.services.logger import get_logger
logger = get_logger(__name__)
def _apply_locale(ctx, param, value):
"""立即显式应用 locale"""
if value:
setup_locale(value)
return value
class _I18nGroup(click.Group):
"""在运行时完成翻译工作的命令组"""
_option_help_map: dict[str, str] = {
"version": "Show the version and exit.",
"locale": "Explicitly specify locale (defaults to LANG env).",
"host": "Listening address",
"port": "Listening port",
"reload": "Development mode hot reload",
}
def format_help(self, ctx, formatter):
# 重新翻译每个子命令的帮助信息
for cmd in self.commands.values():
raw = getattr(cmd, "_raw_help", None)
if raw is not None:
cmd.help = _(raw)
# Re-translate every option's help text by name lookup.
for param in self.params:
if isinstance(param, click.Option) and param.name in self._option_help_map:
param.help = _(self._option_help_map[param.name])
self.help = _("HeurAMS {ver} - Heuristic Auxiliary Memorizing Scheduler").format(
ver=ver
)
return super().format_help(ctx, formatter)
class _I18nCommand(click.Command):
"""在运行时完成翻译工作的命令"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._raw_help = self.help or (self.short_help or "")
@click.group(
cls=_I18nGroup,
invoke_without_command=True,
context_settings={"help_option_names": ["-h", "--help"]},
)
@click.version_option(
ver, "-v", "--version",
prog_name="HeurAMS",
message=f"%(prog)s %(version)s {stage} ({codename}/{codename_cn}), {platform.system()}",
help=_I18nGroup._option_help_map["version"],
)
@click.option(
"--locale", "-l", default=None,
callback=_apply_locale,
is_eager=True,
expose_value=True,
help=_I18nGroup._option_help_map["locale"],
)
@click.pass_context
def cli(ctx, locale):
if ctx.invoked_subcommand is None:
click.echo(cli.get_help(ctx))
ctx.exit(0)
@cli.command(cls=_I18nCommand)
def tui():
"""Launch the built-in user interface (TUI)"""
import heurams.interface.__main__ as tui_module
tui_module.main()
@cli.command(cls=_I18nCommand)
@click.option("--host", default="127.0.0.1", help=_I18nGroup._option_help_map["host"])
@click.option("--port", default=8821, help=_I18nGroup._option_help_map["port"], type=int)
@click.option("--reload", is_flag=True, help=_I18nGroup._option_help_map["reload"])
def serve(host, port, reload):
"""Launch the API service (unifront)"""
from heurams.unifront.server import create_app
app = create_app()
click.echo(
_("unifront API service started: http://{host}:{port}").format(
host=host, port=port
)
)
import uvicorn
uvicorn.run(app, host=host, port=port, reload=reload, log_level="info")
@cli.command(cls=_I18nCommand, name="help")
@click.pass_context
def help_cmd(ctx):
"""Show this help message"""
click.echo(cli.get_help(ctx.parent))
# __main__.py
def main():
prompt = f"""HeurAMS {ver.ver} 已经被成功地安装在系统中.
HeurAMS 被设计为一个带有辅助记忆调度器功能的软件包, 无法直接被执行, 但可被其他 Python 程序调用.
若您想启动内置的基本用户界面:
请运行 python -m heurams.interface,
或者 python -m heurams.interface.__main__
python 代指您使用的解释器, 在某些发行版中可能是 python3, 而 python 命令被指向了 python2.
尽管项目保留了 requirements.txt, 我们仍不推荐使用系统 python 和原始 venv 进行开发.
项目的推荐开发环境工具是 uv.
如果你的环境已经安装了 uv:
先运行 uv sync --all-extras 同步环境, 此命令只需要执行一遍, uv 会自动处理依赖.
然后通过运行 uv run heurams-tui 启动内置基本用户界面.
此时您的解释器在项目目录里的 .venv/bin 中, 使用 IDE 开发前, 务必切换解释器!
注意: 一个常见的误区是, 执行 interface 下的 __main__.py 运行基本用户界面, 这会导致 Python 上下文环境异常, 请不要这样做."""
print(prompt)
cli()
if __name__ == "__main__":
main()
logger.info("HeurAMS cmdline entrypoint invoked")
main()
+7 -6
View File
@@ -1,6 +1,6 @@
"""
全局上下文管理模块
以及基准路径
"""全局上下文模块
初始化并管理基准路径, 程序配置对象, 并提供调试所需上下文管理器
"""
import pathlib
@@ -19,13 +19,14 @@ workdir = pathlib.Path.cwd()
"""工作目录路径."""
logger = get_logger(__name__)
logger.debug(f"包目录: {rootdir}")
logger.debug(f"工作目录: {workdir}")
logger.info(f"rootdir: {rootdir}")
logger.info(f"workdir: {workdir}")
default_data = rootdir / "assets" / "data"
user_data = workdir / "data"
if not user_data.exists():
logger.info("初始化数据目录: %s", user_data)
logger.info("Create a new data directory: %s", user_data)
import shutil
shutil.copytree(default_data, user_data)
+38
View File
@@ -0,0 +1,38 @@
"""国际化支持模块
基于 GNU gettext, 并使用英文 (en_US) 作为 msgid 的基础语言.
"""
import gettext
import os
from pathlib import Path
import locale
from heurams.context import rootdir
_LOCALE_DIR = rootdir / "locale"
_DEFAULT_LOCALE = "en_US"
_translation = gettext.NullTranslations()
def setup_locale(_locale: str | None = None) -> None:
"""获得翻译项.
如果没有找到对应的 locale, 就回退到原始 msgid.
"""
global _translation
if _locale is None:
_locale = locale.getlocale()[0]
try:
_translation = gettext.translation(
"heurams", localedir=str(_LOCALE_DIR), languages=[_locale], fallback=True
)
except FileNotFoundError:
_translation = gettext.NullTranslations()
def _(message: str) -> str:
"""执行翻译的函数"""
return _translation.gettext(message)
# 导入时初始化
setup_locale()
+23 -22
View File
@@ -2,52 +2,53 @@ from time import sleep, perf_counter
# import gc
# gc.set_threshold(100, 1, 1)
print("欢迎使用基本用户界面!")
print("加载配置与上下文... ", end="", flush=True)
from heurams.i18n import _
print(_("Welcome to the basic user interface!"))
print(_("Loading config and context... "), end="", flush=True)
_start_all = perf_counter()
_start = _start_all
from heurams.context import rootdir, workdir, config_var
_end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
print("加载用户界面框架... ", end="", flush=True)
print(_("Loading UI framework... "), end="", flush=True)
_start = perf_counter()
from textual.app import App
from textual.widgets import Button
_end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
print("加载用户界面布局... ", end="", flush=True)
print(_("Loading UI layout... "), end="", flush=True)
_start = perf_counter()
from .screens.about import AboutScreen
from .screens.dashboard import DashboardScreen
from .screens.navigator import NavigatorScreen
from .screens.precache import PrecachingScreen
from .screens.setting import SettingScreen
from .screens.synctool import SyncScreen
from .screens.about import AboutScreen
from . import shim
_end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
print(f"组件目录: {rootdir}")
print(f"工作目录: {workdir}")
print(_("Component directory: {path}").format(path=rootdir))
print(_("Working directory: {path}").format(path=workdir))
_end_all = perf_counter()
print(f"前置工作共计耗时: {round(1000 * (_end_all - _start_all))}ms")
print(_("Pre-work total: {time}ms").format(time=round(1000 * (_end_all - _start_all))))
class HeurAMSApp(App):
TITLE = "潜进"
TITLE = "HeurAMS"
CSS_PATH = rootdir / "interface" / "css" / "main.tcss"
SUB_TITLE = "启发式辅助记忆调度器"
SUB_TITLE = _("Heuristic Auxiliary Memorizing Scheduler")
BINDINGS = [
("q", "go_back", "退出"),
("d", "toggle_dark", "主题"),
("n", "app.push_screen('navigator')", "导航"),
("s", "app.push_screen('setting')", "设置"),
("z", "app.push_screen('about')", "关于"),
("q", "go_back", _("Quit")),
("d", "toggle_dark", _("Theme")),
("n", "app.push_screen('navigator')", _("Navigate")),
("s", "app.push_screen('setting')", _("Settings")),
("z", "app.push_screen('about')", _("About")),
]
SCREENS = {
"dashboard": DashboardScreen,
@@ -72,9 +73,8 @@ class HeurAMSApp(App):
) -> None: # 用来给没使用/禁用的快捷键占位, 因为 Binding 删除不了
pass
# 移除烦人的 "rich traceback"
# Textual 官方不会管这破事, 写 Rich 写入脑了导致的
# 不知道哪来的自信改标准库的 traceback
'''
# 移除烦人的 "rich traceback", 但可能导致未定义行为出现, 所以注释掉
# https://github.com/Textualize/textual/discussions/6255
# NOTE: 进行 textual 版本升级时, 确保查看过上游代码, 尤其是 App 的 _exception
# 如果行为变了就把下面的删了 (虽然有 fallback)
@@ -89,3 +89,4 @@ class HeurAMSApp(App):
self._close_messages_no_wait()
raise self._exception
super().panic(*args) # ditto
'''
+3 -2
View File
@@ -1,14 +1,15 @@
from heurams.interface import *
from heurams.context import config_var
from heurams.i18n import _
from heurams.services.logger import get_logger
import threading
import zmq
import pickle
logger = get_logger(__name__)
def start_debug_server(app):
import zmq
logger = get_logger("zmq_debug")
context = zmq.Context()
socket = context.socket(zmq.REP)
@@ -21,7 +22,7 @@ def start_debug_server(app):
code = pickle.loads(msg)
namespace = {"app": app, "logger": logger, "config_var": config_var}
if first:
app.title += " [调试已连接]"
app.title += _(" [Debug Connected]")
first = 0
try:
# 先尝试 eval
+47 -39
View File
@@ -1,4 +1,4 @@
"""关于界面"""
"""About screen"""
from textual.app import ComposeResult
from textual.containers import ScrollableContainer
@@ -8,6 +8,7 @@ from textual import events, on
import heurams.services.version as version
from heurams.context import *
from heurams.i18n import _
import platform
import shutil
import os
@@ -16,10 +17,10 @@ import sys
class AboutScreen(Screen):
BINDINGS = [
("q", "go_back", "返回"),
("z", "go_back", "关于"),
("q", "go_back", _("Back")),
("z", "go_back", _("About")),
]
SUB_TITLE = "关于"
SUB_TITLE = _("About")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -37,55 +38,64 @@ class AboutScreen(Screen):
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer(id="about_container"):
yield Label("[b]关于与版本信息[/b]")
# 获取系统信息
yield Label(_("[b]About & Version Info[/b]"))
# Get system info
textual_version = self._get_textual_version()
terminal_info = self._get_terminal_info()
python_version = self._get_python_version()
os_version = self._get_os_version()
disk_usage = self._get_disk_usage()
about_text = f"""
# 关于 HeurAMS "潜进"
about_text = _(
"""# About HeurAMS
主程序库版本: `{version.ver}-python`
用户界面分支: `Textual TUI (基本用户界面)`
用户界面版本: `{version.ver}`
API 版本代号: `{version.codename.capitalize()}`
Main library version: `{ver}-python`
UI frontend: `Textual TUI (Basic UI)`
UI version: `{ver}`
API codename: `{codename}`
> 一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.
> 一个开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
> A heuristic auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently.
> An open, elegant, and extensible spaced repetition scheduler experimental platform, designed to help researchers conduct investigations, experiments, and research on cutting-edge memory algorithms more efficiently.
您可在项目主页 https://ams.pluv27.top 获取用户指南, 开发文档与软件更新, 并参与到软件的开发与改进工作.
You can visit the project homepage at https://ams.pluv27.top for user guides, development documentation and software updates, and participate in software development and improvement.
以 GNU Affero 通用公共许可证 (第3版) 开放源代码, 并有一条豁免本机 API 调用的附加条款, 用于其他前端到程序库的接口调用.
Open source under the GNU Affero General Public License (version 3), with an additional exemption clause for local API calls, used for other frontend to library interface calls.
您正使用程序库内置的终端用户界面, 它是第一个全功能前端实现与程序库测试套件, 位于程序库的 interface 子目录.
You are using the built-in terminal user interface, which is the first full-featured frontend implementation and library test suite, located in the interface subdirectory of the library.
开发人员列表:
- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): 项目发起与主要开发者
Developers:
- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer
感谢以下人士与团体, 他们的算法与理论构成了此软件现有算法的基石:
Special thanks to the following individuals and groups; their algorithms and theories form the cornerstone of the current software algorithms:
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 算法与 SM-15 算法理论
- [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 逆向实现
- [Thoughts Memo](https://www.zhihu.com/people/L.M.Sherlock): 间隔重复文献参考
- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 算法底层实现
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 algorithm and SM-15 algorithm theory
- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references
- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm
- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation
# 运行环境信息
# Runtime Environment
Python 解释器版本: {python_version}
Python 解释器路径: {sys.executable}
Textual 框架版本: {textual_version}
终端模拟器: {terminal_info}
操作系统版本: {os_version}
存储余量: {disk_usage}
Python interpreter version: {python_version}
Python interpreter path: {executable}
Textual framework version: {textual_version}
Terminal emulator: {terminal_info}
Operating system version: {os_version}
Disk free space: {disk_usage}
报告问题时, 请复制这些信息到问题描述, 并上传软件日志 `heurams.log` 作为附件, 以协助开发者定位错误
"""
When reporting issues, please copy this information into the issue description and attach `heurams.log` as an attachment to help developers locate the error."""
).format(
ver=version.ver,
codename=version.codename.capitalize(),
python_version=python_version,
executable=sys.executable,
textual_version=textual_version,
terminal_info=terminal_info,
os_version=os_version,
disk_usage=disk_usage,
)
yield Markdown(about_text, classes="about-markdown")
yield Button(
"返回主界面",
_("Back to Main"),
id="back_button",
variant="primary",
flat=True,
@@ -102,22 +112,20 @@ Textual 框架版本: {textual_version}
self.action_go_back()
def _get_textual_version(self) -> str:
"""获取 Textual 框架版本"""
try:
import textual
return textual.__version__
except (ImportError, AttributeError):
return "未知"
return _("Unknown")
def _get_terminal_info(self) -> str:
"""获取终端模拟器信息"""
terminal = shutil.which("terminal")
if terminal:
return terminal
# 尝试从环境变量获取
# Try from environment variables
terminal_env = os.environ.get("TERM_PROGRAM") or os.environ.get("TERM")
return terminal_env or "未知"
return terminal_env or _("Unknown")
def _get_python_version(self) -> str:
"""获取 Python 解释器版本"""
+46 -23
View File
@@ -1,4 +1,4 @@
"""仪表盘界面"""
"""Dashboard screen"""
from functools import reduce
from pathlib import Path
@@ -14,6 +14,7 @@ import heurams.kernel.particles as pt
import heurams.services.timer as timer
import heurams.services.version as version
from heurams.context import *
from heurams.i18n import _
from heurams.kernel.particles import *
from heurams.kernel.repolib import *
from heurams.services.logger import get_logger
@@ -25,11 +26,11 @@ logger = get_logger(__name__)
class DashboardScreen(Screen):
"""主仪表盘屏幕"""
"""Main dashboard screen"""
SUB_TITLE = "仪表盘"
SUB_TITLE = _("Dashboard")
BINDINGS = [
("q", "go_back", "返回"),
("q", "go_back", _("Back")),
]
CSS_PATH = rootdir / "interface" / "css" / "screens" / "dashboard.tcss"
@@ -46,45 +47,50 @@ class DashboardScreen(Screen):
self._load_data()
def compose(self) -> ComposeResult:
"""组合界面组件"""
"""Compose UI components"""
if config_var.get()["interface"]["global"]["show_header"]:
yield Header(
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer():
yield Horizontal( # 顶部的状态
yield Horizontal(
Vertical(
Label(f"当前日时间戳: {timer.get_daystamp()}"),
Label(_("Current daystamp: {ds}").format(ds=timer.get_daystamp())),
Label(
f"应用时区修正: UTC+{str(config_var.get()['services']['timer']['timezone_offset'] / 3600).removesuffix('.0')}"
_("Timezone offset: UTC+{offset}").format(offset=str(config_var.get()['services']['timer']['timezone_offset'] / 3600).removesuffix('.0'))
),
Label(
f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}",
_("Default algorithm: {algo}").format(algo=config_var.get()['interface']['global']['algorithm']),
),
classes="left",
),
Vertical(
Label(f"已加载 {len(self.repos)} 个单元集"),
Label(_("Loaded {n} repo(s)").format(n=len(self.repos))),
Label(
f"共计 {reduce(lambda x, y: x + y, map(lambda x: x.progress['total'], self.repos)) if self.repos else 0} 个单元"
_("Total {n} unit(s)").format(n=reduce(lambda x, y: x + y, map(lambda x: x.progress['total'], self.repos)) if self.repos else 0)
),
Label(
f"已激活 {reduce(lambda x, y: x + y, map(lambda x: x.progress['touched'], self.repos)) if self.repos else 0} 个单元"
_("Activated {n} unit(s)").format(n=reduce(lambda x, y: x + y, map(lambda x: x.progress['touched'], self.repos)) if self.repos else 0)
),
Label(f""),
classes="right",
),
id="header",
)
yield ListView(id="repo_list", classes="repo-list") # 单元集选择
yield ListView(id="repo_list", classes="repo-list")
from heurams.services.attic import Attic
a = Attic("ana", {"totaltime": 0, "openpuzzles": 0, "puzzles_err": 0})
yield Label(f"版本 {version.ver}-{version.stage}") # 版本信息
yield Label(_("Version {ver}-{stage}").format(ver=version.ver, stage=version.stage))
yield Label(
f"{round(a.data['totaltime'], 2)} 秒内处理了 {a.data['openpuzzles']} 个谜题, 正确率{'无法求解' if not a.data['openpuzzles'] else ' ' + str(round(100 * (1 - a.data['puzzles_err']/a.data['openpuzzles']), 2)) + '%'}, 平均速度{'无法求解' if not a.data['totaltime'] else ' ' + str(round(a.data['openpuzzles']/a.data['totaltime'], 2)) + ' 个每秒'}",
_("Processed {puzzles} puzzles in {time}s, accuracy {accuracy}, speed {speed} puzzle(s)/s").format(
puzzles=a.data['openpuzzles'],
time=round(a.data['totaltime'], 2),
accuracy=_("N/A") if not a.data['openpuzzles'] else str(round(100 * (1 - a.data['puzzles_err']/a.data['openpuzzles']), 2)) + '%',
speed=_("N/A") if not a.data['totaltime'] else str(round(a.data['openpuzzles']/a.data['totaltime'], 2)),
),
id="analysis",
) # 版本信息
) # Version info
yield Footer()
@on(events.ScreenResume)
@@ -140,9 +146,28 @@ class DashboardScreen(Screen):
repo.preview["review"] += 1
# initial_time = min(initial_time, e.)
repo.need_review = timer.get_daystamp() >= repo.nearest_review_time
repo.prompt = f"""{repo.manifest['title']} \\[{repo.config['algorithm']}]
[d]进度: {repo.progress['touched']}/{repo.progress['total']} ({round(repo.progress['touched']/repo.progress['total']*100, 1)}%)[/d]
[d]{f'需要学习: {repo.preview['review']}R + {repo.preview['new']}U' if repo.need_review else (f"暂未开始: 0R + {repo.preview['new']}U" if not repo.progress['have_activated_ever'] else '无需操作')}[/d]"""
repo.prompt = _(
"""{title} [{algo}]
[d]Progress: {touched}/{total} ({pct}%)[/d]
[d]{status}[/d]"""
).format(
title=repo.manifest["title"],
algo=repo.config["algorithm"],
touched=repo.progress["touched"],
total=repo.progress["total"],
pct=round(repo.progress["touched"] / repo.progress["total"] * 100, 1),
status=(
_("Due: {review}R + {new}U").format(
review=repo.preview["review"], new=repo.preview["new"]
)
if repo.need_review
else (
_("Not started: 0R + {new}U").format(new=repo.preview["new"])
if not repo.progress["have_activated_ever"]
else _("Up to date")
)
),
)
def on_mount(self) -> None:
"""挂载组件时初始化"""
@@ -160,8 +185,7 @@ class DashboardScreen(Screen):
repo_list_widget.append(
ListItem(
Static(
f"{config_var.get()['global']['paths']['repo']} 中未找到任何单元集仓库目录.\n"
"请导入单元集后重启应用, 或者新建单元集."
_("No repo directories found in {path}.\nPlease import a repo and restart, or create a new one.").format(path=config_var.get()['global']['paths']['repo'])
),
id="not-found",
)
@@ -175,7 +199,7 @@ class DashboardScreen(Screen):
list_item = ListItem(
*[Label(line) for line in r.prompt.splitlines()],
Button(
f"开始学习",
_("Start Learning"),
flat=True,
variant="primary",
id=f"slaunch_repo_{r.manifest['package']}",
@@ -210,7 +234,6 @@ class DashboardScreen(Screen):
def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件"""
logger.debug(f"event.button.id: {event.button.id}")
if event.button.id.startswith("slaunch_repo_"): # type: ignore
from .preparation import launch
+46 -45
View File
@@ -1,4 +1,4 @@
"""收藏夹管理器界面"""
"""Favorites manager screen"""
import base64
from pathlib import Path
@@ -21,6 +21,7 @@ from textual.widgets import (
from textual import events, on
from heurams.context import config_var
from heurams.i18n import _
from heurams.kernel.repolib import Repo
from heurams.services.favorite_service import FavoriteItem, favorite_manager
from heurams.services.logger import get_logger
@@ -29,12 +30,12 @@ logger = get_logger(__name__)
class FavoriteManagerScreen(Screen):
"""收藏夹管理器屏幕"""
"""Favorites manager screen"""
SUB_TITLE = "收藏夹"
SUB_TITLE = _("Favorites")
BINDINGS = [
("q", "go_back", "返回"),
("q", "go_back", _("Back")),
("d", "toggle_dark", ""),
]
@@ -49,12 +50,12 @@ class FavoriteManagerScreen(Screen):
self._load_favorites()
def _load_favorites(self) -> None:
"""加载收藏列表"""
"""Load favorites list"""
self.favorites = favorite_manager.get_all()
logger.debug("加载 %d 个收藏项", len(self.favorites))
logger.info("Loaded %d favorites", len(self.favorites))
def compose(self) -> ComposeResult:
"""组合界面组件"""
"""Compose UI components"""
if config_var.get()["interface"]["global"]["show_header"]:
yield Header(
@@ -62,10 +63,10 @@ class FavoriteManagerScreen(Screen):
)
with ScrollableContainer(id="favorites-container"):
if not self.favorites:
yield Label("暂无收藏", classes="empty-label")
yield Static("使用 * 键在记忆界面中添加收藏.")
yield Label(_("No favorites"), classes="empty-label")
yield Static(_("Press * in the memorization screen to add favorites."))
else:
yield Label(f"{len(self.favorites)} 个收藏项", classes="count-label")
yield Label(_("Total {n} favorite(s)").format(n=len(self.favorites)), classes="count-label")
yield ListView(id="favorites-list")
yield Footer()
@@ -83,41 +84,41 @@ class FavoriteManagerScreen(Screen):
list_view.append(self._create_favorite_item(fav)) # type: ignore
def _encode_favorite_key(self, repo_path: str, ident: str) -> str:
"""编码仓库路径和标识符为安全的按钮 ID 部分"""
# 使用 \x00 分隔两部分, 然后进行 base64 编码
"""Encode repo path and identifier into a safe button ID part"""
# Use \x00 as separator between the two parts, then base64 encode
combined = f"{repo_path}\x00{ident}"
encoded = base64.urlsafe_b64encode(combined.encode()).decode()
# 去掉填充的等号
# Strip padding
return encoded.rstrip("=")
def _decode_favorite_key(self, key: str) -> tuple[str, str]:
"""解码按钮 ID 部分为仓库路径和标识符"""
# 补全等号以使长度是4的倍数
"""Decode button ID part back to repo path and identifier"""
# Pad to make length multiple of 4
padded = key + "=" * ((4 - len(key) % 4) % 4)
decoded = base64.urlsafe_b64decode(padded.encode()).decode()
repo_path, ident = decoded.split("\x00", 1)
return repo_path, ident
def _create_favorite_item(self, fav: FavoriteItem) -> ListItem:
"""创建收藏项列表项"""
# 尝试获取仓库信息
"""Create a favorite list item"""
# Try to get repo info
repo_info = self._get_repo_info(fav.repo_path, fav)
title = repo_info.get("title", fav.repo_path) if repo_info else fav.repo_path
added_time = self._format_time(fav.added)
# 构建显示文本
# Build display text
display_text = f"{fav.ident}\n"
display_text += f" [d]添加于: {added_time}\n 来自 {title}[/d]"
display_text += _(" [d]Added: {time}\n From {title}[/d]").format(time=added_time, title=title)
if fav.tags:
display_text += f"{', '.join(fav.tags)}"
# 创建安全的按钮 ID
# Create safe button ID
button_key = self._encode_favorite_key(fav.repo_path, fav.ident)
# 创建列表项, 包含移除按钮
# Create list item with remove button
container = Horizontal(
Label(display_text, classes="favorite-content"),
Button(
"移除",
_("Remove"),
id=f"remove-{button_key}",
variant="error",
flat=True,
@@ -128,21 +129,21 @@ class FavoriteManagerScreen(Screen):
return ListItem(container)
def _get_repo_info(self, repo_path: str, fav: FavoriteItem) -> Optional[dict]:
"""获取仓库信息(标题、原子内容预览)"""
"""Get repo info (title, atom content preview)"""
try:
data_repo = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
repo_dir = data_repo / repo_path
if not repo_dir.exists():
logger.warning("仓库目录不存在: %s", repo_dir)
logger.warning("Repo directory does not exist: %s", repo_dir)
return None
repo = Repo.from_repodir(repo_dir)
# 获取原子内容预览
# Get atom content preview
content_preview = ""
payload = repo.payload
# 查找对应 ident 的 payload 条目
# Find the payload entry matching ident
for ident_key, content in payload:
if ident_key == fav.ident:
# 截断过长的内容
# Truncate long content
if isinstance(content, dict) and "content" in content:
text = content["content"]
else:
@@ -157,53 +158,53 @@ class FavoriteManagerScreen(Screen):
"content_preview": content_preview,
}
except Exception as e:
logger.error("获取仓库信息失败: %s", e)
logger.error("Failed to get repo info: %s", e)
return None
def _format_time(self, timestamp: int) -> str:
"""格式化时间戳"""
"""Format timestamp as datetime string"""
from datetime import datetime
dt = datetime.fromtimestamp(timestamp)
return dt.strftime("%Y-%m-%d %H:%M")
def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件"""
"""Handle button press event"""
button_id = event.button.id
if button_id and button_id.startswith("remove-"):
# 提取编码后的键
key = button_id[7:] # 去掉 "remove-" 前缀
# Extract encoded key
key = button_id[7:] # Remove "remove-" prefix
try:
repo_path, ident = self._decode_favorite_key(key)
self._remove_favorite(repo_path, ident)
except Exception as e:
logger.error("解析按钮 ID 失败: %s", e)
self.app.notify("操作失败: 无效的按钮标识", severity="error")
logger.error("Failed to parse button ID: %s", e)
self.app.notify(_("Operation failed: invalid button identifier"), severity="error")
def _remove_favorite(self, repo_path: str, ident: str) -> None:
"""移除收藏项"""
"""Remove a favorite item"""
if favorite_manager.remove(repo_path, ident):
self.app.notify(f"已移除收藏: {ident}", severity="information")
# 重新加载列表
self.app.notify(_("Removed favorite: {ident}").format(ident=ident), severity="information")
# Reload list
self._load_favorites()
# 刷新界面
# Refresh UI
self._refresh_list()
else:
self.app.notify(f"移除失败: {ident}", severity="error")
self.app.notify(_("Failed to remove: {ident}").format(ident=ident), severity="error")
def _refresh_list(self) -> None:
"""刷新列表显示"""
"""Refresh the list display"""
container = self.query_one("#favorites-container")
# 清空容器
# Clear container
for child in container.children:
child.remove()
# 重新组合
# Re-compose
if not self.favorites:
container.mount(Label("暂无收藏", classes="empty-label"))
container.mount(Static("使用 * 键在记忆界面中添加收藏。"))
container.mount(Label(_("No favorites"), classes="empty-label"))
container.mount(Static(_("Press * in the memorization screen to add favorites.")))
else:
container.mount(
Label(f"{len(self.favorites)} 个收藏项", classes="count-label")
Label(_("Total {n} favorite(s)").format(n=len(self.favorites)), classes="count-label")
)
list_view = ListView(id="favorites-list")
container.mount(list_view)
+31 -31
View File
@@ -1,4 +1,4 @@
"""队列式记忆工作界面"""
"""Queue-based memorization screen"""
from pathlib import Path
@@ -12,6 +12,7 @@ from textual import events, on
import heurams.kernel.particles as pt
from heurams.context import config_var, rootdir
from heurams.i18n import _
from heurams.kernel.reactor import *
from heurams.services.favorite_service import favorite_manager
from heurams.services.logger import get_logger
@@ -24,11 +25,11 @@ logger = get_logger(__name__)
class MemScreen(Screen):
BINDINGS = [
("q", "go_back_notif", "返回"),
("p", "prev", "查看上一个"),
("q", "go_back_notif", _("Back")),
("p", "prev", _("Previous")),
("d", "toggle_dark", ""),
("v", "play_voice", "朗读"),
("*", "toggle_favorite", "收藏"),
("v", "play_voice", _("Read Aloud")),
("*", "toggle_favorite", _("Favorite")),
("r", "resume_mark"),
("Q", "go_back"),
("n", "block_prompt"),
@@ -36,12 +37,12 @@ class MemScreen(Screen):
("z", "block_prompt"),
]
SUB_TITLE = "学习中"
SUB_TITLE = _("Learning")
CSS_PATH = rootdir / "interface" / "css" / "screens" / "memoqueue.tcss"
if config_var.get()["interface"]["global"]["quick_pass"]:
BINDINGS.append(("k", "quick_pass", "正确应答"))
BINDINGS.append(("f", "quick_fail", "错误应答"))
BINDINGS.append(("k", "quick_pass", _("Correct")))
BINDINGS.append(("f", "quick_fail", _("Incorrect")))
rating = reactive(-1)
@@ -80,7 +81,7 @@ class MemScreen(Screen):
yield Footer()
def update_state(self):
"""更新状态机"""
"""Update state machine"""
self.procession: Procession = self.router.current_procession() # type: ignore
self.atom: pt.Atom = self.procession.current_atom # type: ignore
@@ -101,17 +102,17 @@ class MemScreen(Screen):
atom=self.atom, alia=puzzle["alia"] # type: ignore
)
except Exception as e:
logger.debug(f"调度展开出错: {e}")
return Static(f"无法生成谜题 {e}")
logger.error(f"Failed to expand puzzle: {e}")
return Static(_("Failed to generate puzzle: {e}").format(e=e))
def _get_progress_text(self):
s = ""
if self.repo is not None:
fav_status = "已收藏" if self._is_current_atom_favorited() else "未收藏"
fav_status = _("Favorited") if self._is_current_atom_favorited() else _("Not favorited")
s += f"[{fav_status}] "
s += f"[{self.procession.process() + 1}/{self.procession.total_length()}] \[{self.procession.route.name}]\n"
s += f"[{self.procession.process() + 1}/{self.procession.total_length()}] \\[{self.procession.route.name}]\n"
if self.procession.cursor - 1 >= 0:
s += f"上一个: [d]{self.procession.atoms[self.procession.cursor - 1]['ident']}[/d]"
s += _("Previous: {ident}").format(ident=f"[d]{self.procession.atoms[self.procession.cursor - 1]['ident']}[/d]")
return s
def update_display(self):
@@ -120,7 +121,7 @@ class MemScreen(Screen):
progress_widget.update(self._get_progress_text()) # type: ignore
def mount_puzzle(self):
"""挂载当前谜题组件"""
"""Mount current puzzle widget"""
if self.procession.route == RouterState.FINISHED:
self.mount_finished_widget()
return
@@ -130,7 +131,7 @@ class MemScreen(Screen):
container.mount(self.puzzle_widget())
def mount_finished_widget(self):
"""挂载已完成组件"""
"""Mount finished widget"""
a = Attic("ana", {"finished": 0})
a.data["finished"] += 1
container = self.query_one("#puzzle_container")
@@ -153,7 +154,7 @@ class MemScreen(Screen):
self.run_worker(self.play_voice, exclusive=True, thread=True)
def play_voice(self):
"""朗读当前内容"""
"""Read current content aloud"""
from pathlib import Path
from heurams.services.audio_service import play_by_path
@@ -161,7 +162,6 @@ class MemScreen(Screen):
path = Path(config_var.get()["global"]["paths"]["data"]) / "cache" / "voice"
path = path / f"{get_md5(self.atom.registry['nucleon']["tts_text"])}.wav"
logger.debug(str(path))
if path.exists():
play_by_path(path)
else:
@@ -205,7 +205,7 @@ class MemScreen(Screen):
if not self.atom.registry["runtime"]["locked"]:
if not self.atom.registry["electron"].is_activated():
self.atom.registry["electron"].activate()
logger.debug(f"激活原子 {self.atom}")
logger.debug(f"Activated atom: {self.atom}")
self.atom.lock(1)
self.atom.minimize(5)
else:
@@ -228,7 +228,7 @@ class MemScreen(Screen):
self.expander = self.procession.get_expander()
def action_go_back_notif(self):
self.notify("确定吗? 按下大写 Q 以返回")
self.notify(_("Are you sure? Press uppercase Q to go back."))
def action_go_back(self):
self.app.pop_screen()
@@ -240,44 +240,44 @@ class MemScreen(Screen):
self.rating = 3
def _get_repo_rel_path(self) -> str:
"""获取仓库相对路径(相对于 data/repo"""
"""Get repo relative path (relative to data/repo)"""
if self.repo is None:
return ""
# self.repo.source 是 Path 对象, 指向仓库目录
# self.repo.source is the Path object pointing to the repo directory
repo_full_path = self.repo.source
data_repo_path = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
try:
rel_path = repo_full_path.relative_to(data_repo_path)
return str(rel_path)
except ValueError:
# 如果不在 data/repo 下, 则返回完整路径(字符串形式)
# If not under data/repo, return the full path as string
return str(repo_full_path)
def _is_current_atom_favorited(self) -> bool:
"""检查当前原子是否已收藏"""
"""Check if current atom is favorited"""
if self.repo is None:
return False
repo_path = self._get_repo_rel_path()
return favorite_manager.has(repo_path, self.atom.ident)
def action_toggle_favorite(self):
"""切换收藏状态"""
"""Toggle favorite status"""
if self.repo is None:
self.app.notify("无法收藏:未关联仓库", severity="error")
self.app.notify(_("Cannot favorite: no repo associated"), severity="error")
return
repo_path = self._get_repo_rel_path()
ident = self.atom.ident
if favorite_manager.has(repo_path, ident):
favorite_manager.remove(repo_path, ident)
self.app.notify(f"已取消收藏:{ident}", severity="information")
self.app.notify(_("Unfavorited: {ident}").format(ident=ident), severity="information")
else:
favorite_manager.add(repo_path, ident)
self.app.notify(f"已收藏:{ident}", severity="information")
# 更新显示(如果需要)
self.app.notify(_("Favorited: {ident}").format(ident=ident), severity="information")
# Update display if needed
self.update_display()
def action_block_prompt(self):
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", severity="error")
self.app.notify(_("This function is not available during memorization. Please finish or go back first."), severity="error")
def action_resume_mark(self):
from heurams.services.attic import Attic
@@ -286,4 +286,4 @@ class MemScreen(Screen):
a = Attic("ana")
l = a.data["last"]
a.data["last"] = time.time()
self.app.notify(f"时间恢复已修正: {l} -> {a.data['last']}")
self.app.notify(_("Time resume corrected: {old} -> {new}").format(old=l, new=a.data['last']))
+17 -19
View File
@@ -4,6 +4,7 @@ from textual.screen import ModalScreen
from textual.widgets import Button, Label, ListItem, ListView, Static
from heurams.i18n import _
from heurams.services.logger import get_logger
from .favmgr import FavoriteManagerScreen
@@ -12,36 +13,33 @@ logger = get_logger(__name__)
class NavigatorScreen(ModalScreen):
"""导航器模态窗口"""
"""Navigator modal screen"""
BINDINGS = [
("q", "go_back", "返回"),
("escape", "go_back", "返回"),
("n", "go_back", "切换"),
("q", "go_back", _("Back")),
("escape", "go_back", _("Back")),
("n", "go_back", _("Switch")),
]
SCREENS = [
("仪表盘", "dashboard"),
# ("创建仓库", "repo_creator"),
("缓存管理器", "precache_all"),
("收藏夹", FavoriteManagerScreen),
("设置页面", "setting"),
# ("调试日志", "logviewer"),
("同步工具", "synctool"),
("关于此软件", "about"),
# ("仓库编辑器", "repo_editor"),
(_("Dashboard"), "dashboard"),
(_("Cache Manager"), "precache_all"),
(_("Favorites"), FavoriteManagerScreen),
(_("Settings Page"), "setting"),
(_("Sync Tool"), "synctool"),
(_("About"), "about"),
]
OTHERS = [
("退出程序", "self.app.exit()"),
("项目主页", "webbrowser.open('https://ams.pluv27.top')"),
(_("Exit"), "self.app.exit()"),
(_("Project Homepage"), "webbrowser.open('https://ams.pluv27.top')"),
]
def compose(self) -> ComposeResult:
"""组合界面组件"""
"""Compose UI components"""
with Grid(id="dialog"):
yield Label(
"[b]请选择要跳转的功能\n或记忆会话实例[/b]\n\n将在此处显示提示",
_("[b]Select a function to navigate to\nor a memorization session instance[/b]\n\nTips will be displayed here"),
classes="title-label",
)
yield ListView(
@@ -49,9 +47,9 @@ class NavigatorScreen(ModalScreen):
id="nav-list",
classes="nav-list-view",
)
yield Static("按下回车以完成切换\n所有会话将被保存")
yield Static(_("Press Enter to switch\nAll sessions will be saved"))
yield Button(
"关闭 (n)",
_("Close (n)"),
id="close_button",
variant="primary",
classes="close-button",
+65 -57
View File
@@ -1,4 +1,4 @@
"""缓存工具界面"""
"""Cache tool screen"""
import pathlib
@@ -13,14 +13,15 @@ from textual import events, on
import heurams.kernel.particles as pt
import heurams.services.hasher as hasher
from heurams.context import *
from heurams.i18n import _
# 兼容性缓存路径:优先使用 paths.cache, 否则使用 data/cache
# Compatibility cache path: prefer paths.cache, otherwise data/cache
paths = config_var.get()["global"]["paths"]
cache_dir = pathlib.Path(paths.get("cache", paths["data"] + "/cache")) / "voice"
def human_size(bytes_num: int) -> str:
"""将字节数格式化为人类可读的字符串"""
"""Format byte count as human-readable string"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if bytes_num < 1024.0:
return f"{bytes_num:.2f} {unit}"
@@ -29,18 +30,18 @@ def human_size(bytes_num: int) -> str:
class PrecachingScreen(Screen):
"""预缓存音频文件屏幕
"""Audio file pre-caching screen
缓存记忆单元音频文件, 全部(默认) 或部分记忆单元(可选参数传入)
Cache memory unit audio files, all (default) or some memory units (optional params)
Args:
nucleons (list): 可选列表, 仅包含 Nucleon 对象
desc (list): 可选字符串, 包含对此次调用的文字描述
nucleons (list): Optional list containing Nucleon objects only
desc (list): Optional string containing description of this call
"""
SUB_TITLE = "缓存管理器"
SUB_TITLE = _("Cache Manager")
BINDINGS = [
("q", "go_back", "返回"),
("q", "go_back", _("Back")),
]
def __init__(self, nucleons: list = [], desc: str = ""):
@@ -67,7 +68,7 @@ class PrecachingScreen(Screen):
self._update_cache_stats()
def _get_total_units(self) -> int:
"""获取所有仓库的总单元数"""
"""Get total units across all repos"""
from heurams.context import config_var
from heurams.kernel.repolib import Repo
@@ -89,7 +90,7 @@ class PrecachingScreen(Screen):
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def _update_cache_stats(self) -> None:
"""更新缓存统计信息"""
"""Update cache statistics"""
total_size = 0
file_count = 0
cached_units = 0
@@ -117,21 +118,25 @@ class PrecachingScreen(Screen):
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer(id="precache_container"):
yield Label("[b]音频预缓存[/b]", classes="title-label")
yield Label(_("[b]Audio Pre-cache[/b]"), classes="title-label")
with Container():
yield Static(
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% (已缓存 {self.cache_stats.get('cached_units', 0)} / {self.cache_stats.get('total_units', 0)} 个单元)",
_("Cache rate: {rate:.1f}% ({cached} / {total} units)").format(
rate=self.cache_stats.get('cache_rate', 0),
cached=self.cache_stats.get('cached_units', 0),
total=self.cache_stats.get('total_units', 0),
),
classes="cache-usage-text",
)
if self.nucleons:
yield Static(
f"目标单元归属: [b]{self.desc}[/b]", classes="target-info"
_("Target units from: [b]{desc}[/b]").format(desc=self.desc), classes="target-info"
)
yield Static(
f"单元数量: {len(self.nucleons)}", classes="target-info"
_("Unit count: {n}").format(n=len(self.nucleons)), classes="target-info"
)
else:
yield Static("目标: 所有单元", classes="target-info")
yield Static(_("Target: all units"), classes="target-info")
yield Static(id="status", classes="status-info")
yield Static(id="current_item", classes="current-item")
@@ -139,69 +144,72 @@ class PrecachingScreen(Screen):
with Horizontal(classes="button-group"):
if not self.is_precaching:
yield Button(
"开始预缓存", id="start_precache", variant="primary"
_("Start Pre-cache"), id="start_precache", variant="primary"
)
else:
yield Button(
"取消预缓存", id="cancel_precache", variant="error"
_("Cancel Pre-cache"), id="cancel_precache", variant="error"
)
yield Button("清空缓存", id="clear_cache", variant="warning")
yield Button("返回", id="go_back", variant="default")
yield Button(_("Clear Cache"), id="clear_cache", variant="warning")
yield Button(_("Back"), id="go_back", variant="default")
with Container(classes="cache-info"):
yield Static(f"缓存路径: {cache_dir}", classes="cache-path")
yield Static(_("Cache path: {path}").format(path=cache_dir), classes="cache-path")
yield Static(
f"文件数: {self.cache_stats['file_count']}", classes="cache-count"
_("Files: {n}").format(n=self.cache_stats['file_count']), classes="cache-count"
)
yield Static(
f"总大小: {self.cache_stats['human_size']}", classes="cache-size"
_("Total size: {size}").format(size=self.cache_stats['human_size']), classes="cache-size"
)
yield Button(
"刷新", id="refresh_cache_stats", variant="default", flat=True
_("Refresh"), id="refresh_cache_stats", variant="default", flat=True
)
yield Static("若您离开此界面, 未完成的缓存进程会自动停止.")
yield Static('缓存程序支持 "断点续传".')
yield Static(_("If you leave this screen, ongoing cache processes will stop automatically."))
yield Static(_('Cache supports "resume from break".'))
yield Footer()
def on_mount(self):
"""挂载时初始化状态"""
self.update_status("就绪", "等待开始...")
"""Initialise state on mount"""
self.update_status(_("Ready"), _("Waiting to start..."))
self._update_cache_display()
def update_status(self, status, current_item="", progress=None):
"""更新状态显示"""
"""Update status display"""
status_widget = self.query_one("#status", Static)
item_widget = self.query_one("#current_item", Static)
progress_bar = self.query_one("#progress_bar", ProgressBar)
status_widget.update(f"状态: {status}")
item_widget.update(f"当前项目: {current_item}" if current_item else "")
status_widget.update(_("Status: {s}").format(s=status))
item_widget.update(_("Current item: {item}").format(item=current_item) if current_item else "")
if progress is not None:
progress_bar.progress = progress
progress_bar.advance(0) # 刷新显示
progress_bar.advance(0) # Refresh display
def _update_cache_display(self) -> None:
"""更新缓存信息显示"""
# 更新统计信息
"""Update cache info display"""
# Update stats
self._update_cache_stats()
# 更新缓存率进度条
# 更新缓存大小和文件数显示
# Update cache rate progress bar
# Update cache size and file count display
cache_count_widget = self.query_one(".cache-count", Static)
cache_size_widget = self.query_one(".cache-size", Static)
cache_usage_text = self.query_one(".cache-usage-text", Static)
if cache_count_widget:
cache_count_widget.update(f"文件数: {self.cache_stats['file_count']}")
cache_count_widget.update(_("Files: {n}").format(n=self.cache_stats['file_count']))
if cache_size_widget:
cache_size_widget.update(f"总大小: {self.cache_stats['human_size']}")
cache_size_widget.update(_("Total size: {size}").format(size=self.cache_stats['human_size']))
if cache_usage_text:
cache_usage_text.update(
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% "
f"(已缓存 {self.cache_stats.get('cached_units', 0)} / {self.cache_stats.get('total_units', 0)} 个单元)"
_("Cache rate: {rate:.1f}% ({cached} / {total} units)").format(
rate=self.cache_stats.get('cache_rate', 0),
cached=self.cache_stats.get('cached_units', 0),
total=self.cache_stats.get('total_units', 0),
)
)
def precache_by_text(self, text: str):
"""预缓存单段文本的音频"""
"""Pre-cache audio for a single text string"""
cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / f"{hasher.get_md5(text)}.wav"
@@ -212,21 +220,21 @@ class PrecachingScreen(Screen):
convertor(text, cache_file)
return 1
except Exception as e:
print(f"预缓存失败 '{text}': {e}")
print(f"Pre-cache failed '{text}': {e}")
return 0
return 1
def precache_by_nucleon(self, nucleon: pt.Nucleon):
"""依据 Nucleon 缓存"""
"""Cache based on Nucleon"""
ret = self.precache_by_text(nucleon["tts_text"])
return ret
def precache_by_list(self, nucleons: list):
"""依据 Nucleons 列表缓存"""
"""Cache based on Nucleons list"""
for idx, nucleon in enumerate(nucleons):
# print(f"PROC: {nucleon}")
worker = get_current_worker()
if worker and worker.is_cancelled: # 函数在worker中执行且已被取消
if worker and worker.is_cancelled: # Function running in worker and has been cancelled
return False
text = nucleon["tts_text"]
# self.current_item = text[:30] + "..." if len(text) > 50 else text
@@ -236,12 +244,12 @@ class PrecachingScreen(Screen):
# print(self.total)
progress = int((self.processed / self.total) * 100) if self.total > 0 else 0
# print(progress)
self.update_status(f"正处理 ({idx + 1}/{len(nucleons)})", text, progress)
self.update_status(_("Processing ({i}/{total})").format(i=idx + 1, total=len(nucleons)), text, progress)
ret = self.precache_by_nucleon(nucleon)
if not ret:
self.update_status(
"出错",
f"处理失败, 跳过: {self.current_item}",
_("Error"),
_("Failed, skipping: {item}").format(item=self.current_item),
)
import time
@@ -286,7 +294,7 @@ class PrecachingScreen(Screen):
def on_button_pressed(self, event: Button.Pressed) -> None:
event.stop()
if event.button.id == "start_precache" and not self.is_precaching:
# 开始预缓存
# Start pre-cache
if self.nucleons:
self.precache_worker = self.run_worker(
self.precache_by_nucleons,
@@ -303,32 +311,32 @@ class PrecachingScreen(Screen):
)
elif event.button.id == "cancel_precache" and self.is_precaching:
# 取消预缓存
# Cancel pre-cache
if self.precache_worker:
self.precache_worker.cancel()
self.is_precaching = False
self.processed = 0
self.progress = 0
self.update_status("已取消", "预缓存操作被用户取消", 0)
self.update_status(_("Cancelled"), _("Pre-cache cancelled by user"), 0)
elif event.button.id == "clear_cache":
# 清空缓存
# Clear cache
try:
import shutil
shutil.rmtree(cache_dir, ignore_errors=True)
self.update_status("已清空", "音频缓存已清空", 0)
self._update_cache_display() # 更新缓存统计显示
self.update_status(_("Cleared"), _("Audio cache cleared"), 0)
self._update_cache_display() # Update cache stats display
except Exception as e:
self.update_status("错误", f"清空缓存失败: {e}")
self.update_status(_("Error"), _("Failed to clear cache: {error}").format(error=e))
self.cancel_flag = 1
self.processed = 0
self.progress = 0
elif event.button.id == "refresh_cache_stats":
# 刷新缓存统计信息
# Refresh cache stats
self._update_cache_display()
self.app.notify("缓存信息已刷新", severity="information")
self.app.notify(_("Cache info refreshed"), severity="information")
elif event.button.id == "go_back":
self.action_go_back()
+23 -20
View File
@@ -1,4 +1,4 @@
"""记忆准备界面"""
"""Memorization preparation screen"""
from textual.app import ComposeResult
from textual.containers import ScrollableContainer, Horizontal
@@ -19,6 +19,7 @@ from textual import events, on
import heurams.kernel.particles as pt
from heurams.context import *
from heurams.context import config_var
from heurams.i18n import _
from heurams.kernel.repolib import *
from heurams.kernel.algorithms import algorithms
from heurams.services.logger import get_logger
@@ -28,11 +29,11 @@ logger = get_logger(__name__)
class PreparationScreen(Screen):
SUB_TITLE = "准备记忆集"
SUB_TITLE = _("Prepare Repository")
BINDINGS = [
("q", "go_back", "返回"),
("p", "precache", "缓存"),
("q", "go_back", _("Back")),
("p", "precache", _("Cache")),
("d", "toggle_dark", ""),
("0,1,2,3", "app.push_screen('about')", ""),
]
@@ -61,29 +62,40 @@ class PreparationScreen(Screen):
)
with ScrollableContainer(id="main_container"):
yield Markdown(
f"**准备就绪**: `{self.repo.manifest['title']}`\n", id="title"
_("**Ready**: `{title}`\n").format(title=self.repo.manifest['title']), id="title"
)
yield Label(f"单元集路径: {self.repo.source}")
yield Label(_("Repo path: {path}").format(path=self.repo.source))
yield Label(
f"学习完成度: {self.repo.progress['touched']}/{len(self.repo)} [d]\\[{round(self.repo.progress['touched']/self.repo.progress['total']*100, 1)}%][/d]"
_("Progress: {touched}/{total} [{pct}%]").format(
touched=self.repo.progress['touched'],
total=len(self.repo),
pct=round(self.repo.progress['touched'] / self.repo.progress['total'] * 100, 1)
)
)
yield Label(
f"调度算法: {self.repo.config["algorithm"]} {algorithms[self.repo.config["algorithm"]].desc}"
_("Scheduling algorithm: {algo} {desc}").format(
algo=self.repo.config["algorithm"],
desc=algorithms[self.repo.config["algorithm"]].desc
)
)
yield Label(
f"学习数量: {self.repo.preview['review'] + self.scheduled_num} = {self.repo.preview['review']} [d][复习][/d] + {self.scheduled_num} [d][新识记][/d]\n",
_("Study count: {total} = {review} [d][Review][/d] + {new} [d][New][/d]\n").format(
total=self.repo.preview['review'] + self.scheduled_num,
review=self.repo.preview['review'],
new=self.scheduled_num,
),
id="schnum_label",
)
yield Horizontal(
Button(
"开始记忆",
_("Start Memorizing"),
id="start_memorizing_button",
variant="primary",
classes="btn",
),
Button(
"管理缓存",
_("Manage Cache"),
id="precache_button",
variant="success",
classes="btn",
@@ -99,14 +111,6 @@ class PreparationScreen(Screen):
yield Static(i, classes="unit-statline")
yield Footer()
# def watch_scheduled_num(self, old_scheduled_num, new_scheduled_num):
# logger.debug("响应", old_scheduled_num, "->", new_scheduled_num)
# try:
# one = self.query_one("#schnum_label")
# one.update(f"单次记忆数量: {new_scheduled_num}") # type: ignore
# except:
# pass
def load_data(self):
self.scheduled_num = self.repo.config["scheduled_num"]
content = ""
@@ -154,7 +158,6 @@ class PreparationScreen(Screen):
def on_button_pressed(self, event: Button.Pressed) -> None:
event.stop()
logger.debug("按下按钮")
if event.button.id == "start_memorizing_button":
launch(repo=self.repo, app=self.app, scheduled_num=self.scheduled_num)
+14 -13
View File
@@ -1,4 +1,4 @@
"""设置页面"""
"""Settings screen"""
from textual.app import ComposeResult
from textual.containers import ScrollableContainer, Horizontal
@@ -16,6 +16,7 @@ from textual.widgets import (
from textual import events, on
from heurams.context import *
from heurams.i18n import _
from heurams.kernel.particles import *
from heurams.kernel.repolib import *
from heurams.services.logger import get_logger
@@ -26,12 +27,12 @@ logger = get_logger(__name__)
class SettingScreen(Screen):
"""设置页面屏幕"""
"""Settings screen"""
SUB_TITLE = "设置"
SUB_TITLE = _("Settings")
BINDINGS = [
("q", "go_back", "返回"),
("s", "go_back", "设置"),
("q", "go_back", _("Back")),
("s", "go_back", _("Settings")),
]
CSS_PATH = rootdir / "interface" / "css" / "screens" / "setting.tcss"
@@ -50,13 +51,13 @@ class SettingScreen(Screen):
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult:
"""组合界面组件"""
"""Compose UI components"""
if config_var.get()["interface"]["global"]["show_header"]:
yield Header(
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer():
yield Label("[b]设置页面[/b]")
yield Label("[b]" + _("Settings") + "[/b]")
for i in config_var.get():
if i.startswith("_"):
continue
@@ -67,7 +68,7 @@ class SettingScreen(Screen):
title=i + f'\n[d]{config_var.get().get(f"_{i}_desc", "")}[/d]',
)
yield Label(
"退出页面时, 所作的更改会立即保存, 但仍建议重启软件以确保新的配置得到应用",
_("Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."),
classes="foot",
)
yield Footer()
@@ -138,7 +139,7 @@ class SettingScreen(Screen):
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=str(parent[i]),
placeholder="要求一个浮点数",
placeholder=_("Requires a float"),
type="number",
id=domize(f"{parent_epath}.{i}"),
),
@@ -151,7 +152,7 @@ class SettingScreen(Screen):
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=parent[i],
placeholder="要求一个字符串",
placeholder=_("Requires a string"),
type="text",
id=domize(f"{parent_epath}.{i}"),
),
@@ -176,7 +177,7 @@ class SettingScreen(Screen):
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input(
value=str(parent[i]),
placeholder="要求一个整数",
placeholder=_("Requires an integer"),
type="integer",
id=domize(f"{parent_epath}.{i}"),
),
@@ -186,9 +187,9 @@ class SettingScreen(Screen):
elif isinstance(parent[i], list):
pass
else:
lst.append(Label("未知类型"))
lst.append(Label(_("Unknown type")))
return lst
return [Label("无子项")]
return [Label(_("No sub-items"))]
def on_mount(self) -> None:
"""挂载组件时初始化"""
+93 -83
View File
@@ -1,4 +1,4 @@
"""同步工具界面"""
"""Sync tool screen"""
import pathlib
import time
@@ -12,11 +12,12 @@ from textual.worker import get_current_worker
from textual import events, on
from heurams.context import *
from heurams.i18n import _
class SyncScreen(Screen):
BINDINGS = [("q", "go_back", "返回")]
BINDINGS = [("q", "go_back", _("Back"))]
def __init__(self, nucleons: list = [], desc: str = ""):
super().__init__(name=None, id=None, classes=None)
@@ -39,67 +40,67 @@ class SyncScreen(Screen):
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
)
with ScrollableContainer(id="sync_container"):
# 标题和连接状态
yield Static("同步工具", classes="title")
# Title and connection status
yield Static(_("Sync Tool"), classes="title")
yield Static("", id="status_label", classes="status")
# 配置信息
yield Static(f"同步协议: {config_var.get()['services']['sync']}")
yield Static("服务器配置:", classes="section_title")
# Config info
yield Static(_("Sync protocol: {proto}").format(proto=config_var.get()['services']['sync']))
yield Static(_("Server Configuration:"), classes="section_title")
with Horizontal(classes="config_info"):
yield Static("远程服务器:", classes="config_label")
yield Static(_("Remote server:"), classes="config_label")
yield Static("", id="server_url", classes="config_value")
with Horizontal(classes="config_info"):
yield Static("远程路径:", classes="config_label")
yield Static(_("Remote path:"), classes="config_label")
yield Static("", id="remote_path", classes="config_value")
with Horizontal(classes="control_buttons"):
yield Button("测试连接", id="test_connection", variant="primary")
yield Button("开始同步", id="start_sync", variant="success")
yield Button("暂停", id="pause_sync", variant="warning", disabled=True)
yield Button("取消", id="cancel_sync", variant="error", disabled=True)
yield Button(_("Test Connection"), id="test_connection", variant="primary")
yield Button(_("Start Sync"), id="start_sync", variant="success")
yield Button(_("Pause"), id="pause_sync", variant="warning", disabled=True)
yield Button(_("Cancel"), id="cancel_sync", variant="error", disabled=True)
yield Static("同步进度", classes="section_title")
yield Static(_("Sync Progress"), classes="section_title")
yield ProgressBar(id="progress_bar", show_percentage=True, total=100)
yield Static("", id="progress_label", classes="progress_text")
yield Static("同步日志", classes="section_title")
yield Static(_("Sync Log"), classes="section_title")
yield Static("", id="log_output", classes="log_output")
yield Footer()
def on_mount(self):
"""挂载时初始化状态"""
"""Initialise state on mount"""
self.update_ui_from_config()
self.log_message("同步工具已启动")
self.log_message(_("Sync tool started"))
def update_ui_from_config(self):
"""更新 UI 显示配置信息"""
"""Update UI with config info"""
try:
sync_cfg: dict = config_var.get()["providers"]["sync"]["webdav"]
# 更新服务器 URL
url = sync_cfg.get("url", "未配置")
# Update server URL
url = sync_cfg.get("url", _("Not configured"))
url_widget = self.query_one("#server_url")
url_widget.update(url) # type: ignore
# 更新远程路径
# Update remote path
remote_path = sync_cfg.get("remote_path", "/")
path_widget = self.query_one("#remote_path")
path_widget.update(remote_path) # type: ignore
# 更新状态标签
# Update status label
status_widget = self.query_one("#status_label")
if self.sync_service and self.sync_service.client:
status_widget.update("同步服务已就绪") # type: ignore
status_widget.update(_("Sync service ready")) # type: ignore
status_widget.add_class("ready")
else:
status_widget.update("同步服务未配置或未启用") # type: ignore
status_widget.update(_("Sync service not configured or not enabled")) # type: ignore
status_widget.add_class("error")
except Exception as e:
self.log_message(f"更新 UI 失败: {e}", is_error=True)
self.log_message(_("Failed to update UI: {error}").format(error=e), is_error=True)
def update_status(self, status, current_item="", progress=None):
"""更新状态显示"""
"""Update status display"""
try:
status_widget = self.query_one("#status_label")
status_widget.update(status) # type: ignore
@@ -112,28 +113,28 @@ class SyncScreen(Screen):
progress_label.update(f"{progress}% - {current_item}" if current_item else f"{progress}%") # type: ignore
except Exception as e:
self.log_message(f"更新状态失败: {e}", is_error=True)
self.log_message(_("Failed to update status: {error}").format(error=e), is_error=True)
def log_message(self, message: str, is_error: bool = False):
"""添加日志消息并更新显示"""
"""Add log message and update display"""
timestamp = time.strftime("%H:%M:%S")
prefix = "[ERROR]" if is_error else "[INFO]"
log_line = f"{timestamp} {prefix} {message}"
self.log_messages.append(log_line)
# 保持日志行数不超过最大值
# Keep log lines under max
if len(self.log_messages) > self.max_log_lines:
self.log_messages = self.log_messages[-self.max_log_lines :]
# 更新日志显示
# Update log display
try:
log_widget = self.query_one("#log_output")
log_widget.update("\n".join(self.log_messages)) # type: ignore
except Exception:
pass # 如果组件未就绪, 忽略错误
pass # Ignore if widget not ready
def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件"""
"""Handle button press events"""
button_id = event.button.id
if button_id == "test_connection":
@@ -148,124 +149,133 @@ class SyncScreen(Screen):
event.stop()
def test_connection(self):
"""测试 WebDAV 服务器连接"""
"""Test WebDAV server connection"""
if not self.sync_service:
self.log_message("同步服务未初始化, 请检查配置", is_error=True)
self.update_status("同步服务未初始化")
self.log_message(_("Sync service not initialised, please check configuration"), is_error=True)
self.update_status(_("Sync service not initialised"))
return
self.log_message("正在测试 WebDAV 连接...")
self.update_status("正在测试连接...")
self.log_message(_("Testing WebDAV connection..."))
self.update_status(_("Testing connection..."))
try:
success = self.sync_service.test_connection()
if success:
self.log_message("连接测试成功")
self.update_status("连接正常")
self.log_message(_("Connection test successful"))
self.update_status(_("Connection OK"))
else:
self.log_message("连接测试失败", is_error=True)
self.update_status("连接失败")
self.log_message(_("Connection test failed"), is_error=True)
self.update_status(_("Connection failed"))
except Exception as e:
self.log_message(f"连接测试异常: {e}", is_error=True)
self.update_status("连接异常")
self.log_message(_("Connection test error: {error}").format(error=e), is_error=True)
self.update_status(_("Connection error"))
def start_sync(self):
"""开始同步"""
"""Start syncing"""
if not self.sync_service:
self.log_message("同步服务未初始化, 无法开始同步", is_error=True)
self.log_message(_("Sync service not initialised, cannot start sync"), is_error=True)
return
if self.is_syncing:
self.log_message("同步已在进行中", is_error=True)
self.log_message(_("Sync already in progress"), is_error=True)
return
self.is_syncing = True
self.is_paused = False
self.update_button_states()
self.log_message("开始同步数据...")
self.update_status("正在同步...", progress=0)
self.log_message(_("Starting data sync..."))
self.update_status(_("Syncing..."), progress=0)
# 启动后台同步任务
# Start background sync task
self.run_worker(self.perform_sync, thread=True)
def perform_sync(self):
"""执行同步任务(在后台线程中运行)"""
"""Execute sync task (runs in background thread)"""
worker = get_current_worker()
try:
# 获取需要同步的本地目录
# Get local directories to sync
from heurams.context import config_var
config = config_var.get()
paths = config.get("paths", {})
# 同步 nucleon 目录
# Sync nucleon directory
nucleon_dir = pathlib.Path(paths.get("nucleon_dir", "./data/nucleon"))
if nucleon_dir.exists():
self.log_message(f"同步 nucleon 目录: {nucleon_dir}")
self.update_status(f"同步 nucleon 目录...", progress=10)
self.log_message(_("Syncing nucleon directory: {dir}").format(dir=nucleon_dir))
self.update_status(_("Syncing nucleon directory..."), progress=10)
result = self.sync_service.sync_directory(nucleon_dir) # type: ignore
if result.get("success"):
self.log_message(
f"nucleon 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}"
_("nucleon sync complete: uploaded {up}, downloaded {down}").format(
up=result.get('uploaded', 0),
down=result.get('downloaded', 0),
)
)
else:
self.log_message(
f"nucleon 同步失败: {result.get('error', '未知错误')}",
_("nucleon sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
is_error=True,
)
# 同步 electron 目录
# Sync electron directory
electron_dir = pathlib.Path(paths.get("electron_dir", "./data/electron"))
if electron_dir.exists():
self.log_message(f"同步 electron 目录: {electron_dir}")
self.update_status(f"同步 electron 目录...", progress=60)
self.log_message(_("Syncing electron directory: {dir}").format(dir=electron_dir))
self.update_status(_("Syncing electron directory..."), progress=60)
result = self.sync_service.sync_directory(electron_dir) # type: ignore
if result.get("success"):
self.log_message(
f"electron 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}"
_("electron sync complete: uploaded {up}, downloaded {down}").format(
up=result.get('uploaded', 0),
down=result.get('downloaded', 0),
)
)
else:
self.log_message(
f"electron 同步失败: {result.get('error', '未知错误')}",
_("electron sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
is_error=True,
)
# 同步 orbital 目录(如果存在)
# Sync orbital directory (if exists)
orbital_dir = pathlib.Path(paths.get("orbital_dir", "./data/orbital"))
if orbital_dir.exists():
self.log_message(f"同步 orbital 目录: {orbital_dir}")
self.update_status(f"同步 orbital 目录...", progress=80)
self.log_message(_("Syncing orbital directory: {dir}").format(dir=orbital_dir))
self.update_status(_("Syncing orbital directory..."), progress=80)
result = self.sync_service.sync_directory(orbital_dir) # type: ignore
if result.get("success"):
self.log_message(
f"orbital 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}"
_("orbital sync complete: uploaded {up}, downloaded {down}").format(
up=result.get('uploaded', 0),
down=result.get('downloaded', 0),
)
)
else:
self.log_message(
f"orbital 同步失败: {result.get('error', '未知错误')}",
_("orbital sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
is_error=True,
)
# 同步完成
self.update_status("同步完成", progress=100)
self.log_message("所有目录同步完成")
# Sync complete
self.update_status(_("Sync complete"), progress=100)
self.log_message(_("All directories synced"))
except Exception as e:
self.log_message(f"同步过程中发生错误: {e}", is_error=True)
self.update_status("同步失败")
self.log_message(_("Error during sync: {error}").format(error=e), is_error=True)
self.update_status(_("Sync failed"))
finally:
# 重置同步状态
# Reset sync state
self.is_syncing = False
self.is_paused = False
self.update_button_states() # type: ignore
def pause_sync(self):
"""暂停同步"""
"""Pause sync"""
if not self.is_syncing:
return
@@ -273,14 +283,14 @@ class SyncScreen(Screen):
self.update_button_states()
if self.is_paused:
self.log_message("同步已暂停")
self.update_status("同步已暂停")
self.log_message(_("Sync paused"))
self.update_status(_("Sync paused"))
else:
self.log_message("同步已恢复")
self.update_status("正在同步...")
self.log_message(_("Sync resumed"))
self.update_status(_("Syncing..."))
def cancel_sync(self):
"""取消同步"""
"""Cancel sync"""
if not self.is_syncing:
return
@@ -288,11 +298,11 @@ class SyncScreen(Screen):
self.is_paused = False
self.update_button_states()
self.log_message("同步已取消")
self.update_status("同步已取消")
self.log_message(_("Sync cancelled"))
self.update_status(_("Sync cancelled"))
def update_button_states(self):
"""更新按钮状态"""
"""Update button states"""
try:
start_button = self.query_one("#start_sync")
pause_button = self.query_one("#pause_sync")
@@ -302,14 +312,14 @@ class SyncScreen(Screen):
start_button.disabled = True
pause_button.disabled = False
cancel_button.disabled = False
pause_button.label = "继续" if self.is_paused else "暂停" # type: ignore
pause_button.label = _("Resume") if self.is_paused else _("Pause") # type: ignore
else:
start_button.disabled = False
pause_button.disabled = True
cancel_button.disabled = True
except Exception as e:
self.log_message(f"更新按钮状态失败: {e}", is_error=True)
self.log_message(_("Failed to update button state: {error}").format(error=e), is_error=True)
def action_go_back(self):
self.app.pop_screen()
+20 -19
View File
@@ -5,6 +5,7 @@ from textual.widgets import Button, Label, Static
import heurams.kernel.particles as pt
from heurams.i18n import _
from .base_puzzle_widget import BasePuzzleWidget
@@ -32,49 +33,49 @@ class BasicEvaluation(BasePuzzleWidget):
class RatingChanged(Message):
def __init__(self, rating: int) -> None:
self.rating = rating # 评分值 (0-5)
self.rating = rating # Rating value (0-5)
super().__init__()
# 反馈映射表
# Feedback mapping
feedback_mapping = {
"feedback_5": {"rating": 5, "text": "完美回想"},
"feedback_4": {"rating": 4, "text": "犹豫后正确"},
"feedback_3": {"rating": 3, "text": "困难地正确"},
"feedback_2": {"rating": 2, "text": "错误但熟悉"},
"feedback_1": {"rating": 1, "text": "错误且不熟"},
"feedback_0": {"rating": 0, "text": "完全空白"},
"feedback_5": {"rating": 5, "text": _("Perfect recall")},
"feedback_4": {"rating": 4, "text": _("Correct after hesitation")},
"feedback_3": {"rating": 3, "text": _("Correct with difficulty")},
"feedback_2": {"rating": 2, "text": _("Wrong but familiar")},
"feedback_1": {"rating": 1, "text": _("Wrong and unfamiliar")},
"feedback_0": {"rating": 0, "text": _("Complete blank")},
}
def compose(self):
# 显示主要内容
# Show main content
yield Label(self.atom.registry["nucleon"]["content"], id="main")
# 显示评估说明(可选)
yield Static("请评估你对这个内容的记忆程度: ", classes="instruction")
# Show instruction (optional)
yield Static(_("Evaluate how well you remember this content: "), classes="instruction")
# 按钮容器
# Button container
with ScrollableContainer(id="button_container"):
btn = {}
btn["5"] = Button(
"完美回想", variant="success", id="feedback_5", classes="choice"
_("Perfect recall"), variant="success", id="feedback_5", classes="choice"
)
btn["4"] = Button(
"犹豫后正确", variant="success", id="feedback_4", classes="choice"
_("Correct after hesitation"), variant="success", id="feedback_4", classes="choice"
)
btn["3"] = Button(
"困难地正确", variant="warning", id="feedback_3", classes="choice"
_("Correct with difficulty"), variant="warning", id="feedback_3", classes="choice"
)
btn["2"] = Button(
"错误但熟悉", variant="warning", id="feedback_2", classes="choice"
_("Wrong but familiar"), variant="warning", id="feedback_2", classes="choice"
)
btn["1"] = Button(
"错误且不熟", variant="error", id="feedback_1", classes="choice"
_("Wrong and unfamiliar"), variant="error", id="feedback_1", classes="choice"
)
btn["0"] = Button(
"完全空白", variant="error", id="feedback_0", classes="choice"
_("Complete blank"), variant="error", id="feedback_0", classes="choice"
)
# 布局按钮
# Layout buttons
yield Horizontal(btn["5"], btn["4"])
yield Horizontal(btn["3"], btn["2"])
yield Horizontal(btn["1"], btn["0"])
@@ -10,6 +10,7 @@ from textual.events import Key
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.services.hasher import hash
from heurams.i18n import _
from heurams.services.logger import get_logger
from .base_puzzle_widget import BasePuzzleWidget
@@ -79,7 +80,6 @@ class ClozePuzzle(BasePuzzleWidget):
c += 1
self.hashmap[h] = i
btnid = f"sel000-{h}"
logger.debug(f"建立按钮 {btnid}")
self.btn_shortcuts[f"{c}"] = btnid
btns.append(Button(f"{i}", id=f"{btnid}", classes="cloze-option-btn"))
for i in range((len(btns) + 1) // 2):
@@ -89,7 +89,7 @@ class ClozePuzzle(BasePuzzleWidget):
yield btns[i]
s.focus()
yield Button("退格", id="delete")
yield Button(_("Backspace"), id="delete")
self.btn_shortcuts[f"0"] = "delete"
self.btn_shortcuts[f"backspace"] = "delete"
self.btn_shortcuts[f"delete"] = "delete"
+5 -3
View File
@@ -1,6 +1,8 @@
from textual.widget import Widget
from textual.widgets import Button, Label
from heurams.i18n import _
class Finished(Widget):
def __init__(
@@ -26,9 +28,9 @@ class Finished(Widget):
)
def compose(self):
yield Label("本次记忆进程结束", id="finished_msg")
yield Label(f"算法数据{'已保存' if self.is_saved else "未能保存"}")
yield Button("返回上一级", flat=True, id="back-to-menu")
yield Label(_("This memorization session is finished"), id="finished_msg")
yield Label(_("Algorithm data {}").format(_("saved") if self.is_saved else _("not saved")))
yield Button(_("Back to Menu"), flat=True, id="back-to-menu")
def on_button_pressed(self, event):
button_id = event.button.id
+10 -18
View File
@@ -1,4 +1,4 @@
# 单项选择题
# Multiple-choice puzzle
from typing import TypedDict
from textual.containers import ScrollableContainer
@@ -8,6 +8,7 @@ from textual.widgets import Button, Label
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.services.hasher import hash
from heurams.i18n import _
from heurams.services.logger import get_logger
from textual.events import Key
from .base_puzzle_widget import BasePuzzleWidget
@@ -65,15 +66,10 @@ class MCQPuzzle(BasePuzzleWidget):
def compose(self):
setting: Setting = self.atom.registry["nucleon"]["puzzles"][self.alia]
if len(self.inputlist) > len(self.puzzle.options):
logger.debug("ERR IDX")
logger.debug(self.inputlist)
logger.debug(self.puzzle.options)
else:
current_options = self.puzzle.options[len(self.inputlist)]
yield Label(setting["primary"], id="sentence")
yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle")
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
current_options = self.puzzle.options[len(self.inputlist)]
yield Label(setting["primary"], id="sentence")
yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle")
yield Label(_("Current input: {input}").format(input=self.inputlist), id="inputpreview")
# 渲染当前问题的选项
c = 0
@@ -85,21 +81,19 @@ class MCQPuzzle(BasePuzzleWidget):
h = str(hash(i))
self.hashmap[h] = i
btnid = f"sel{str(self.cursor).zfill(3)}-{h}"
logger.debug(f"建立按钮 {btnid}")
self.btn_shortcuts[f"{c}"] = f"{btnid}"
yield Button(f"[{c}] " + i, id=f"{btnid}")
s.focus()
yield Button("退格", id="delete")
yield Button(_("Backspace"), id="delete")
self.btn_shortcuts["0"] = f"delete"
self.btn_shortcuts["delete"] = f"delete"
self.btn_shortcuts["backspace"] = f"delete"
def update_display(self, error=0):
# 更新预览标签
# Update preview label
preview = self.query_one("#inputpreview")
preview.update(f"当前输入: {self.inputlist}") # type: ignore
logger.debug("已经更新预览标签")
preview.update(_("Current input: {input}").format(input=self.inputlist)) # type: ignore
# 更新问题标签
puzzle_label = self.query_one("#puzzle")
current_question_index = len(self.inputlist)
@@ -122,7 +116,7 @@ class MCQPuzzle(BasePuzzleWidget):
# 选项选择处理
answer_text = self.hashmap[button_id[7:]] # type: ignore
self.inputlist.append(answer_text)
logger.debug(f"{self.inputlist}")
logger.debug(f"Input list: {self.inputlist}")
# 检查是否完成所有题目
if len(self.inputlist) >= len(self.puzzle.answer):
is_correct = self.inputlist == self.puzzle.answer
@@ -143,7 +137,6 @@ class MCQPuzzle(BasePuzzleWidget):
def refresh_buttons(self):
"""刷新按钮显示(用于题目切换)"""
# 移除所有选项按钮
logger.debug("刷新按钮")
self.cursor += 1
container = self.query_one("#btn-container")
buttons_to_remove = [
@@ -153,7 +146,6 @@ class MCQPuzzle(BasePuzzleWidget):
]
container.focus()
for button in buttons_to_remove:
logger.info(button)
container.remove_children("#" + button.id) # type: ignore
# 添加当前题目的选项按钮
+4 -2
View File
@@ -1,6 +1,8 @@
from textual.widget import Widget
from textual.widgets import Button, Label
from heurams.i18n import _
class Placeholder(Widget):
def __init__(
@@ -23,8 +25,8 @@ class Placeholder(Widget):
)
def compose(self):
yield Label("示例标签", id="testlabel")
yield Button("示例按钮", id="testbtn", classes="choice")
yield Label(_("Sample Label"), id="testlabel")
yield Button(_("Sample Button"), id="testbtn", classes="choice")
def on_button_pressed(self, event):
pass
+3 -2
View File
@@ -6,6 +6,7 @@ from textual.widget import Widget
from textual.widgets import Button, Label, Markdown, Static
import heurams.kernel.particles as pt
from heurams.i18n import _
from heurams.services.logger import get_logger
from .base_puzzle_widget import BasePuzzleWidget
@@ -86,7 +87,7 @@ class Recognition(BasePuzzleWidget):
for item in cfg["secondary"]:
if isinstance(item, list):
for j in item:
yield Markdown(f"### 笔记: {j}") # TODO ANNOTATION
yield Markdown(_("### Note: {note}").format(note=j)) # TODO ANNOTATION
continue
if isinstance(item, Dict):
total = ""
@@ -97,7 +98,7 @@ class Recognition(BasePuzzleWidget):
yield Markdown(item)
with Center() as c:
with Button("我已知晓", id="ok") as b:
with Button(_("I know this"), id="ok") as b:
b.focus()
def on_button_pressed(self, event: Button.Pressed) -> None:
+1 -18
View File
@@ -44,38 +44,21 @@ class BaseAlgorithm:
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
) -> None:
"""迭代记忆数据"""
logger.debug(
"BaseAlgorithm.revisor 被调用, algodata keys: %s, feedback: %d, is_new_activation: %s",
list(algodata.keys()) if algodata else [],
feedback,
is_new_activation,
)
@classmethod
def is_due(cls, algodata) -> int:
"""是否应该复习"""
logger.debug(
"BaseAlgorithm.is_due 被调用, algodata keys: %s",
list(algodata.keys()) if algodata else [],
)
return 1
@classmethod
def get_rating(cls, algodata) -> str:
"""获取评分信息"""
logger.debug(
"BaseAlgorithm.rate 被调用, algodata keys: %s",
list(algodata.keys()) if algodata else [],
)
return ""
@classmethod
def nextdate(cls, algodata) -> int:
"""获取下一次记忆时间戳"""
logger.debug(
"BaseAlgorithm.nextdate 被调用, algodata keys: %s",
list(algodata.keys()) if algodata else [],
)
return -1
@classmethod
+5 -10
View File
@@ -33,7 +33,7 @@ def _get_global_scheduler():
with open(_SCHEDULER_STATE_FILE, "r", encoding="utf-8") as f:
return Scheduler.from_json(f.read())
except Exception:
logger.warning("FSRS Scheduler 状态文件加载失败, 创建新实例")
logger.warning("No former FSRS Scheduler file founded, creating new instance")
return Scheduler()
@@ -45,7 +45,7 @@ def _save_global_scheduler(scheduler):
with open(_SCHEDULER_STATE_FILE, "w", encoding="utf-8") as f:
f.write(data)
except Exception:
logger.exception("FSRS Scheduler 状态保存失败")
logger.error("Failed to persist FSRS Scheduler state")
def _feedback_to_rating(feedback: int) -> Rating:
@@ -176,14 +176,9 @@ class FSRSAlgorithm(BaseAlgorithm):
feedback (int): 0-5 的记忆保留率量化参数
is_new_activation: 是否为全新激活(重置为初始状态)
"""
logger.debug(
"FSRS.revisor 开始, feedback: %d, is_new_activation: %s",
feedback,
is_new_activation,
)
if feedback == -1:
logger.debug("feedback -1, 跳过更新")
logger.debug("feedback = -1, update skipped")
return
scheduler = _get_global_scheduler()
@@ -191,7 +186,7 @@ class FSRSAlgorithm(BaseAlgorithm):
if is_new_activation:
card = Card()
logger.debug("新激活, 创建新 Card")
logger.debug("New activation, create new Card")
else:
card = cls._algodata_to_card(algodata)
@@ -206,7 +201,7 @@ class FSRSAlgorithm(BaseAlgorithm):
algodata[cls.algo_name]["rept"] += 1
logger.debug(
"FSRS.revisor 完成: stability=%s, difficulty=%s, state=%s, " "next_date=%d",
"FSRS.revisor finished: stability=%s, difficulty=%s, state=%s, " "next_date=%d",
card.stability,
card.difficulty,
card.state,
+2 -2
View File
@@ -51,7 +51,7 @@ class NSP0Algorithm(BaseAlgorithm):
)
if feedback == -1:
logger.debug("feedback -1, 跳过更新")
logger.debug("feedback = -1, update skipped")
return
algodata[cls.algo_name]["interval"] = 1 if feedback <= 3 else float("inf")
if not algodata[cls.algo_name]["important"]:
@@ -65,7 +65,7 @@ class NSP0Algorithm(BaseAlgorithm):
algodata[cls.algo_name]["last_modify"] = timer.get_timestamp()
logger.debug(
"更新日期: last_date=%d, next_date=%d, last_modify=%f",
"Update date: last_date=%d, next_date=%d, last_modify=%f",
algodata[cls.algo_name]["last_date"],
algodata[cls.algo_name]["next_date"],
algodata[cls.algo_name]["last_modify"],
+4 -4
View File
@@ -614,7 +614,7 @@ def _get_global_sm():
with open(_GLOBAL_STATE_FILE, "r", encoding="utf-8") as f:
return SM.load(json.load(f))
except Exception:
logger.warning("SM-15M 全局状态文件加载失败, 创建新实例")
logger.warning("Failed to load SM-15M global state file, creating new instance")
sm = SM()
_save_global_sm(sm)
return sm
@@ -626,7 +626,7 @@ def _save_global_sm(sm):
with open(_GLOBAL_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(sm.data(), f, indent=2)
except Exception:
logger.exception("SM-15M 全局状态保存失败")
logger.error("Failed to save SM-15M global state")
# ============================================================================
@@ -758,7 +758,7 @@ class SM15MAlgorithm(BaseAlgorithm):
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
):
logger.debug(
"SM-15M.revisor 开始, feedback=%d, is_new_activation=%s",
"SM-15M.revisor, feedback=%d, is_new_activation=%s",
feedback,
is_new_activation,
)
@@ -788,7 +788,7 @@ class SM15MAlgorithm(BaseAlgorithm):
algodata[cls.algo_name]["rept"] += 1
logger.debug(
"SM-15M.revisor 完成: repetition=%d, of=%.4f, next_date=%d",
"SM-15M.revisor: repetition=%d, of=%.4f, next_date=%d",
item.repetition,
item.of,
algodata[cls.algo_name]["next_date"],
+10 -10
View File
@@ -45,13 +45,13 @@ class SM2Algorithm(BaseAlgorithm):
quality (int): 记忆保留率量化参数
"""
logger.debug(
"SM2.revisor 开始, feedback: %d, is_new_activation: %s",
"SM2.revisor, feedback: %d, is_new_activation: %s",
feedback,
is_new_activation,
)
if feedback == -1:
logger.debug("feedback -1, 跳过更新")
logger.debug("feedback = -1, update skipped")
return
algodata[cls.algo_name]["efactor"] = algodata[cls.algo_name]["efactor"] + (
@@ -60,7 +60,7 @@ class SM2Algorithm(BaseAlgorithm):
algodata[cls.algo_name]["efactor"] = max(
1.3, algodata[cls.algo_name]["efactor"]
)
logger.debug("更新 efactor: %f", algodata[cls.algo_name]["efactor"])
logger.debug("Update efactor: %f", algodata[cls.algo_name]["efactor"])
if feedback < 3:
algodata[cls.algo_name]["rept"] = 0
@@ -68,28 +68,28 @@ class SM2Algorithm(BaseAlgorithm):
logger.debug("feedback < 3, 重置 rept 和 interval")
else:
algodata[cls.algo_name]["rept"] += 1
logger.debug("递增 rept: %d", algodata[cls.algo_name]["rept"])
logger.debug("Increase rept: %d", algodata[cls.algo_name]["rept"])
algodata[cls.algo_name]["real_rept"] += 1
logger.debug("递增 real_rept: %d", algodata[cls.algo_name]["real_rept"])
logger.debug("Increase real_rept: %d", algodata[cls.algo_name]["real_rept"])
if is_new_activation:
algodata[cls.algo_name]["rept"] = 0
algodata[cls.algo_name]["efactor"] = 2.5
logger.debug("新激活, 重置 rept efactor")
logger.debug("New activation, reset rept and efactor")
if algodata[cls.algo_name]["rept"] == 0:
algodata[cls.algo_name]["interval"] = 1
logger.debug("rept=0, 设置 interval=1")
logger.debug("rept=0, set interval=1")
elif algodata[cls.algo_name]["rept"] == 1:
algodata[cls.algo_name]["interval"] = 6
logger.debug("rept=1, 设置 interval=6")
logger.debug("rept=1, set interval=6")
else:
algodata[cls.algo_name]["interval"] = round(
algodata[cls.algo_name]["interval"] * algodata[cls.algo_name]["efactor"]
)
logger.debug(
"rept>1, 计算 interval: %d", algodata[cls.algo_name]["interval"]
"rept>1, providing interval: %d", algodata[cls.algo_name]["interval"]
)
algodata[cls.algo_name]["last_date"] = timer.get_daystamp()
@@ -99,7 +99,7 @@ class SM2Algorithm(BaseAlgorithm):
algodata[cls.algo_name]["last_modify"] = timer.get_timestamp()
logger.debug(
"更新日期: last_date=%d, next_date=%d, last_modify=%f",
"Update date: last_date=%d, next_date=%d, last_modify=%f",
algodata[cls.algo_name]["last_date"],
algodata[cls.algo_name]["next_date"],
algodata[cls.algo_name]["last_modify"],
+3 -3
View File
@@ -63,7 +63,7 @@ class Atom:
)
def lock(self, locked=-1):
logger.debug(f"锁定参数 {locked}")
logger.debug(f"Lock atom: {locked}")
"""锁定, 效果等同于 self.registry['runtime']['locked'] = locked 或者返回是否锁定"""
if locked == 1:
self.registry["runtime"]["locked"] = True
@@ -80,13 +80,13 @@ class Atom:
PuzzleWidget 的 handler 除了测试, 严禁直接执行 Electron 的 revisor 函数, 否则造成逻辑混乱
"""
if self.registry["runtime"]["locked"]:
logger.debug(f"允许总评分: {self.registry['runtime']['min_rate']}")
logger.debug(f"Rating allowed: {self.registry['runtime']['min_rate']}")
self.registry["electron"].revisor(
self.registry["runtime"]["min_rate"],
is_new_activation=self.registry["runtime"]["new_activation"],
)
else:
logger.debug("禁止总评分")
logger.debug("Rating disallowed")
def __getitem__(self, key):
return self.registry[key]
+2 -4
View File
@@ -8,9 +8,7 @@ class BasePuzzle:
"""谜题基类"""
def refresh(self):
logger.debug("BasePuzzle.refresh 被调用(未实现)")
raise NotImplementedError("谜题对象未实现 refresh 方法")
raise NotImplementedError("Method refresh not implemented")
def __str__(self):
logger.debug("BasePuzzle.__str__ 被调用")
return f"谜题: {type(self).__name__}"
return f"Puzzle: {type(self).__name__}"
+6 -7
View File
@@ -27,20 +27,20 @@ class ClozePuzzle(BasePuzzle):
self.wording = "填空题 - 尚未刷新谜题"
self.answer = ["填空题 - 尚未刷新谜题"]
self.delimiter = delimiter
logger.debug("ClozePuzzle 初始化完成")
logger.debug("ClozePuzzle inited")
def refresh(self): # 刷新谜题
logger.debug("ClozePuzzle.refresh 开始")
logger.debug("ClozePuzzle.refresh")
placeholder = "___SLASH___"
tmp_text = self.text.replace(self.delimiter, placeholder)
words = tmp_text.split(placeholder)
if not words:
logger.warning("ClozePuzzle.refresh: 无单词可处理")
logger.warning("ClozePuzzle.refresh: nothing to proceed")
return
words = [word for word in words if word]
logger.debug("ClozePuzzle.refresh: 分割出 %d 个单词", len(words))
logger.debug("ClozePuzzle.refresh: %d words splited", len(words))
num_blanks = min(max(1, len(words) // self.min_denominator), len(words))
logger.debug("ClozePuzzle.refresh: 需要生成 %d 个填空", num_blanks)
logger.debug("ClozePuzzle.refresh: %d blank required", num_blanks)
indices_to_blank = random.sample(range(len(words)), num_blanks)
indices_to_blank.sort()
blanked_words = list(words)
@@ -50,8 +50,7 @@ class ClozePuzzle(BasePuzzle):
answer.append(words[index])
self.answer = answer
self.wording = "".join(blanked_words)
logger.debug("ClozePuzzle.refresh 完成, 生成 %d 个填空", len(answer))
logger.debug("ClozePuzzle.refresh, %d blanks generated", len(answer))
def __str__(self):
logger.debug("ClozePuzzle.__str__ 被调用")
return f"{self.wording}\n{str(self.answer)}"
+3 -3
View File
@@ -65,8 +65,8 @@ class MCQPuzzle(BasePuzzle):
jammer: 传入的干扰项列表
"""
# 合并正确答案和传入的干扰项, 并去重
logger.debug(f"答案映射: {self.mapping}, {type(self.mapping)}")
logger.debug(f"干扰项: {jammer}, {type(jammer)}")
logger.debug(f"Answer table: {self.mapping}, {type(self.mapping)}")
logger.debug(f"Jammers: {jammer}, {type(jammer)}")
unique_jammers = set(jammer + list(self.mapping.values()))
self.jammer = list(unique_jammers)
@@ -94,7 +94,7 @@ class MCQPuzzle(BasePuzzle):
Raises:
ValueError: 当mapping为空时不会抛出异常, 但会设置空谜题状态
"""
logger.debug("MCQPuzzle.refresh 开始, mapping size=%d", len(self.mapping))
logger.debug("MCQPuzzle.refresh, mapping size=%d", len(self.mapping))
if not self.mapping:
self._set_empty_puzzle()
return
+1 -2
View File
@@ -11,8 +11,7 @@ class RecognitionPuzzle(BasePuzzle):
"""识别占位符"""
def __init__(self) -> None:
logger.debug("RecognitionPuzzle.__init__")
super().__init__()
def refresh(self):
logger.debug("RecognitionPuzzle.refresh(空实现)")
pass
+1 -1
View File
@@ -48,7 +48,7 @@ class Expander(Machine):
self.puzzles_inf = list()
self.min_ratings = []
for item, possibility in orbital_schedule: # type: ignore
logger.debug(f"开始处理: {item}")
logger.debug(f"Process: {item}")
puzzle = puz.puzzles[orbital_puzzles[item]["__origin__"]]
+2 -25
View File
@@ -14,12 +14,6 @@ class Procession(Machine):
"""队列: 标识单次记忆流程"""
def __init__(self, atoms: list, route_state: RouterState, name_: str = ""):
logger.debug(
"Procession.__init__: 原子数量=%d, route=%s, name='%s'",
len(atoms),
route_state.value,
name_,
)
self.current_atom: pt.Atom | None
self.atoms = atoms
self.current_atom = atoms[0] if atoms else None
@@ -52,65 +46,48 @@ class Procession(Machine):
initial=ProcessionState.ACTIVE.value,
)
logger.debug("Procession 初始化完成, 队列长度=%d", len(self.atoms))
def on_active(self):
"""进入active状态时的回调"""
logger.debug("Procession 进入 active 状态")
pass
def on_finished(self):
"""进入FINISHED状态时的回调"""
logger.debug("Procession 进入 FINISHED 状态")
pass
def forward(self, step=1):
"""将记忆原子指针向前移动并依情况更新原子(返回 1)或完成队列(返回 0)"""
logger.debug("Procession.forward: step=%d, 当前 cursor=%d", step, self.cursor)
self.cursor += step
if self.cursor >= len(self.atoms):
if self.state != ProcessionState.FINISHED.value:
self.finish() # 触发状态转换
logger.debug("Procession 已完成")
else:
if self.state != ProcessionState.ACTIVE.value:
self.restart() # 确保在active状态
self.current_atom = self.atoms[self.cursor]
logger.debug("cursor 更新为: %d", self.cursor)
logger.debug(
"当前原子更新为: %s",
self.current_atom.ident if self.current_atom else "None",
)
def append(self, atom=None):
"""追加(回忆失败的)原子(默认为当前原子)到队列末端"""
if atom is None:
atom = self.current_atom
logger.debug("Procession.append: atom=%s", atom.ident if atom else "None")
if not self.atoms or self.atoms[-1] != atom or len(self) <= 1:
self.atoms.append(atom)
logger.debug("原子已追加到队列, 新队列长度=%d", len(self.atoms))
else:
logger.debug("原子未追加(重复或队列长度<=1)")
def __len__(self):
if not self.atoms:
return 0
length = len(self.atoms) - self.cursor
logger.debug("Procession.__len__: 剩余长度=%d", length)
return length
def process(self):
logger.debug("Procession.process: cursor=%d", self.cursor)
return self.cursor
def total_length(self):
total = len(self.atoms)
logger.debug("Procession.total_length: %d", total)
return total
def is_empty(self):
empty = len(self.atoms) == 0
logger.debug("Procession.is_empty: %s", empty)
return empty
def get_expander(self):
+7 -14
View File
@@ -14,7 +14,7 @@ class Router(Machine):
"""全局调度阶段路由器"""
def __init__(self, atoms: list[pt.Atom]) -> None:
logger.debug(f"Router.__init__: 原子数量={len(atoms)}")
logger.debug(f"number of atoms={len(atoms)}")
self.atoms = atoms
new_atoms = list()
@@ -26,7 +26,7 @@ class Router(Machine):
else:
old_atoms.append(i)
logger.debug(f"新原子数量={len(new_atoms)}, 旧原子数量={len(old_atoms)}")
logger.debug(f"number of new atoms={len(new_atoms)}, number of old atoms={len(old_atoms)}")
self.processions = list()
"""路由中的所有队列"""
@@ -35,17 +35,14 @@ class Router(Machine):
self.processions.append(
Procession(old_atoms, RouterState.QUICK_REVIEW, "初始复习")
)
logger.debug("创建初始复习 Procession")
if len(new_atoms):
self.processions.append(
Procession(new_atoms, RouterState.RECOGNITION, "新记忆")
)
logger.debug("创建新记忆 Procession")
self.processions.append(Procession(atoms, RouterState.FINAL_REVIEW, "总体复习"))
logger.debug("创建总体复习 Procession")
logger.debug("Router 初始化完成, processions 数量=%d", len(self.processions))
logger.debug("Router inited, number of processions =%d", len(self.processions))
# 设置transitions状态机
states = [
@@ -91,29 +88,27 @@ class Router(Machine):
def on_unsure(self):
"""进入UNSURE状态时的回调"""
logger.debug("Router 进入 UNSURE 状态")
pass
def on_quick_review(self):
"""进入QUICK_REVIEW状态时的回调"""
logger.debug("Router 进入 QUICK_REVIEW 状态")
pass
def on_recognition(self):
"""进入RECOGNITION状态时的回调"""
logger.debug("Router 进入 RECOGNITION 状态")
pass
def on_final_review(self):
"""进入FINAL_REVIEW状态时的回调"""
logger.debug("Router 进入 FINAL_REVIEW 状态")
pass
def on_finished(self):
"""进入FINISHED状态时的回调"""
for i in self.atoms:
i.lock(1)
i.revise()
logger.debug("Router 进入 FINISHED 状态")
def current_procession(self):
logger.debug("Router.current_procession 被调用")
for i in self.processions:
i: Procession
if i.state != ProcessionState.FINISHED.value:
@@ -125,12 +120,10 @@ class Router(Machine):
elif i.route == RouterState.FINAL_REVIEW:
self.to_final_review()
logger.debug("找到未完成的 Procession: route=%s", i.route)
return i
# 所有Procession都已完成
self.to_finished()
logger.debug("所有 Procession 已完成, 状态设置为 FINISHED")
return Procession([AtomPlaceholder()], RouterState.FINISHED)
def __repr__(self, style="pipe", ends="\n"):
+1 -4
View File
@@ -20,7 +20,4 @@ class ProcessionState(Enum):
class ExpanderState(Enum):
EXAMMODE = "exammode"
RETRONLY = "retronly"
logger.debug("状态枚举定义已加载")
RETRONLY = "retronly"
+3
View File
@@ -0,0 +1,3 @@
[General]
LangCode=zh_CN
TargetLangCode=zh_CN
Binary file not shown.
@@ -0,0 +1,670 @@
# Japanese translations for HeurAMS.
# Copyright (C) 2026 Wang Zhiyu
#
msgid ""
msgstr ""
"Project-Id-Version: heurams 0.5.1\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "Welcome to the basic user interface!"
msgstr "基本ユーザーインターフェースへようこそ!"
msgid "Loading config and context... "
msgstr "設定とコンテキストを読み込み中… "
msgid "Done! ({time}ms)"
msgstr "完了!({time}ms"
msgid "Loading UI framework... "
msgstr "UI フレームワークを読み込み中… "
msgid "Loading UI layout... "
msgstr "UI レイアウトを読み込み中… "
msgid "Component directory: {path}"
msgstr "コンポーネントディレクトリ: {path}"
msgid "Working directory: {path}"
msgstr "作業ディレクトリ: {path}"
msgid "Pre-work total: {time}ms"
msgstr "前処理合計: {time}ms"
msgid "Heuristic Auxiliary Memorizing Scheduler"
msgstr "ヒューリスティック補助記憶スケジューラ"
msgid "Quit"
msgstr "終了"
msgid "Theme"
msgstr "テーマ"
msgid "Navigate"
msgstr "ナビゲート"
msgid "Settings"
msgstr "設定"
msgid "About"
msgstr "このソフトについて"
msgid "HeurAMS {ver} - Heuristic Auxiliary Memorizing Scheduler"
msgstr "HeurAMS {ver} - ヒューリスティック補助記憶スケジューラ"
msgid "Listening address"
msgstr "リッスンアドレス"
msgid "Listening port"
msgstr "リッスンポート"
msgid "Development mode hot reload"
msgstr "開発モードのホットリロード"
msgid "Show this help message"
msgstr "このヘルプを表示"
msgid "unifront API service started: http://{host}:{port}"
msgstr "unifront API サービス開始: http://{host}:{port}"
msgid "Show the version and exit."
msgstr "バージョンを表示して終了。"
msgid "Explicitly specify locale (defaults to LANG env)."
msgstr "UI ロケール(例: en_US, ja)。デフォルトは LANG 環境変数。"
msgid "Launch the built-in user interface (TUI)"
msgstr "内蔵 UITUI)を起動"
msgid "Launch the API service (unifront)"
msgstr "API サービス(unifront)を起動"
msgid "Dashboard"
msgstr "ダッシュボード"
msgid "Back"
msgstr "戻る"
msgid "Current daystamp: {ds}"
msgstr "現在の日付スタンプ: {ds}"
msgid "Timezone offset: UTC+{offset}"
msgstr "タイムゾーンオフセット: UTC+{offset}"
msgid "Default algorithm: {algo}"
msgstr "デフォルトアルゴリズム: {algo}"
msgid "Loaded {n} repo(s)"
msgstr "リポジトリ {n} 個を読み込み"
msgid "Total {n} unit(s)"
msgstr "ユニット合計 {n}"
msgid "Activated {n} unit(s)"
msgstr "アクティブ {n} ユニット"
msgid "Version {ver}-{stage}"
msgstr "バージョン {ver}-{stage}"
msgid "Processed {puzzles} puzzles in {time}s, accuracy {accuracy}, speed {speed} puzzle(s)/s"
msgstr "{time} 秒で {puzzles} 問処理, 正解率 {accuracy}, 速度 {speed} 問/秒"
msgid "N/A"
msgstr "N/A"
msgid ""
"No repo directories found in {path}.\n"
"Please import a repo and restart, or create a new one."
msgstr ""
"{path} にリポジトリが見つかりません。\n"
"リポジトリをインポートして再起動するか、新規作成してください。"
msgid "Start Learning"
msgstr "学習開始"
msgid ""
"{title} [{algo}]\n"
" [d]Progress: {touched}/{total} ({pct}%)[/d]\n"
" [d]{status}[/d]"
msgstr ""
"{title} [{algo}]\n"
" [d]進捗: {touched}/{total} ({pct}%)[/d]\n"
" [d]{status}[/d]"
msgid "Due: {review}R + {new}U"
msgstr "期限: {review}R + {new}U"
msgid "Not started: 0R + {new}U"
msgstr "未開始: 0R + {new}U"
msgid "Up to date"
msgstr "最新"
msgid "[b]About & Version Info[/b]"
msgstr "[b]バージョン情報[/b]"
msgid ""
"# About HeurAMS\n"
"\n"
"Main library version: `{ver}-python` \n"
"UI frontend: `Textual TUI (Basic UI)` \n"
"UI version: `{ver}` \n"
"API codename: `{codename}` \n"
"\n"
"> A heuristic auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently. \n"
"> An open, elegant, and extensible spaced repetition scheduler experimental platform, designed to help researchers conduct investigations, experiments, and research on cutting-edge memory algorithms more efficiently. \n"
"\n"
"You can visit the project homepage at https://ams.pluv27.top for user guides, development documentation and software updates, and participate in software development and improvement. \n"
"\n"
"Open source under the GNU Affero General Public License (version 3), with an additional exemption clause for local API calls, used for other frontend to library interface calls. \n"
"\n"
"You are using the built-in terminal user interface, which is the first full-featured frontend implementation and library test suite, located in the interface subdirectory of the library. \n"
"\n"
"Developers: \n"
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer \n"
"\n"
"Special thanks to the following individuals and groups; their algorithms and theories form the cornerstone of the current software algorithms: \n"
"\n"
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 algorithm and SM-15 algorithm theory \n"
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references \n"
"- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm \n"
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation \n"
"\n"
"# Runtime Environment\n"
"\n"
"Python interpreter version: {python_version} \n"
"Python interpreter path: {executable} \n"
"Textual framework version: {textual_version} \n"
"Terminal emulator: {terminal_info} \n"
"Operating system version: {os_version} \n"
"Disk free space: {disk_usage} \n"
"\n"
"When reporting issues, please copy this information into the issue description and attach `heurams.log` as an attachment to help developers locate the error."
msgstr ""
"# HeurAMS について\n"
"\n"
"メインライブラリバージョン: `{ver}-python` \n"
"UI フロントエンド: `Textual TUI(基本 UI` \n"
"UI バージョン: `{ver}` \n"
"API コードネーム: `{codename}` \n"
"\n"
"> ヒューリスティックアルゴリズムと認知科学理論に基づく補助記憶スケジューラ。 \n"
"> オープンで拡張可能な間隔反復実験プラットフォーム。 \n"
"\n"
"プロジェクトHP: https://ams.pluv27.top \n"
"\n"
"GNU AGPL-3.0 で公開。ローカル API 呼び出しの例外条項付き。 \n"
"\n"
"開発者: \n"
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)) \n"
"\n"
"謝辞: \n"
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2, SM-15 \n"
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS \n"
"- [Kazuaki Tanida](https://github.com/slaypni): SM-15 CoffeeScript 実装 \n"
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 実装 \n"
"\n"
"# 実行環境\n"
"\n"
"Python: {python_version} \n"
"Python パス: {executable} \n"
"Textual: {textual_version} \n"
"端末: {terminal_info} \n"
"OS: {os_version} \n"
"空き容量: {disk_usage} \n"
"\n"
"問題報告時はこの情報を添付してください。"
msgid "Back to Main"
msgstr "メインに戻る"
msgid "Unknown"
msgstr "不明"
msgid "Requires a float"
msgstr "浮動小数点数が必要"
msgid "Requires a string"
msgstr "文字列が必要"
msgid "Requires an integer"
msgstr "整数が必要"
msgid "Unknown type"
msgstr "不明な型"
msgid "No sub-items"
msgstr "サブ項目なし"
msgid "Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."
msgstr "このページを離れると変更は即保存されますが、確実に適用するには再起動を推奨します。"
msgid "Previous"
msgstr "前へ"
msgid "Read Aloud"
msgstr "読み上げ"
msgid "Favorite"
msgstr "お気に入り"
msgid "Learning"
msgstr "学習中"
msgid "Correct"
msgstr "正解"
msgid "Incorrect"
msgstr "不正解"
msgid "Failed to generate puzzle: {e}"
msgstr "パズル生成失敗: {e}"
msgid "Favorited"
msgstr "お気に入り済み"
msgid "Not favorited"
msgstr "未お気に入り"
msgid "Previous: {ident}"
msgstr "前: {ident}"
msgid "Are you sure? Press uppercase Q to go back."
msgstr "よろしいですか? 大文字 Q で戻ります。"
msgid "Cannot favorite: no repo associated"
msgstr "お気に入り不可: リポジトリが未関連"
msgid "Unfavorited: {ident}"
msgstr "お気に入り解除: {ident}"
msgid "Favorited: {ident}"
msgstr "お気に入り登録: {ident}"
msgid "This function is not available during memorization. Please finish or go back first."
msgstr "この機能は記憶中は使用できません。終了または戻ってからお試しください。"
msgid "Time resume corrected: {old} -> {new}"
msgstr "時間再開補正: {old} -> {new}"
msgid " [Debug Connected]"
msgstr " [デバッグ接続済]"
msgid "Favorites"
msgstr "お気に入り"
msgid "No favorites"
msgstr "お気に入りなし"
msgid "Press * in the memorization screen to add favorites."
msgstr "記憶画面で * キーを押すとお気に入りに追加。"
msgid "Total {n} favorite(s)"
msgstr "お気に入り合計 {n}"
msgid ""
" [d]Added: {time}\n"
" From {title}[/d]"
msgstr ""
" [d]追加: {time}\n"
" 出典: {title}[/d]"
msgid "Remove"
msgstr "削除"
msgid "Operation failed: invalid button identifier"
msgstr "操作失敗: 無効なボタンID"
msgid "Removed favorite: {ident}"
msgstr "お気に入り削除: {ident}"
msgid "Failed to remove: {ident}"
msgstr "削除失敗: {ident}"
msgid "Switch"
msgstr "切替"
msgid "Cache Manager"
msgstr "キャッシュ管理"
msgid "Settings Page"
msgstr "設定ページ"
msgid "Sync Tool"
msgstr "同期ツール"
msgid "Exit"
msgstr "終了"
msgid "Project Homepage"
msgstr "プロジェクトHP"
msgid ""
"[b]Select a function to navigate to\n"
"or a memorization session instance[/b]\n"
"\n"
"Tips will be displayed here"
msgstr ""
"[b]移動先の機能または\n"
"記憶セッションを選択[/b]\n"
"\n"
"ヒントがここに表示されます"
msgid ""
"Press Enter to switch\n"
"All sessions will be saved"
msgstr ""
"Enter で切替\n"
"全セッションは保存されます"
msgid "Close (n)"
msgstr "閉じる (n)"
msgid "Prepare Repository"
msgstr "リポジトリ準備"
msgid "Cache"
msgstr "キャッシュ"
msgid ""
"**Ready**: `{title}`\n"
""
msgstr ""
"**準備完了**: `{title}`\n"
""
msgid "Repo path: {path}"
msgstr "リポジトリパス: {path}"
msgid "Progress: {touched}/{total} [{pct}%]"
msgstr "進捗: {touched}/{total} [{pct}%]"
msgid "Scheduling algorithm: {algo} {desc}"
msgstr "スケジュールアルゴリズム: {algo} {desc}"
msgid ""
"Study count: {total} = {review} [d][Review][/d] + {new} [d][New][/d]\n"
""
msgstr ""
"学習数: {total} = {review} [d][復習][/d] + {new} [d][新規][/d]\n"
""
msgid "Start Memorizing"
msgstr "記憶開始"
msgid "Manage Cache"
msgstr "キャッシュ管理"
msgid "Sync protocol: {proto}"
msgstr "同期プロトコル: {proto}"
msgid "Server Configuration:"
msgstr "サーバー設定:"
msgid "Remote server:"
msgstr "リモートサーバー:"
msgid "Remote path:"
msgstr "リモートパス:"
msgid "Test Connection"
msgstr "接続テスト"
msgid "Start Sync"
msgstr "同期開始"
msgid "Pause"
msgstr "一時停止"
msgid "Cancel"
msgstr "キャンセル"
msgid "Sync Progress"
msgstr "同期進捗"
msgid "Sync Log"
msgstr "同期ログ"
msgid "Not configured"
msgstr "未設定"
msgid "Sync service ready"
msgstr "同期サービス準備完了"
msgid "Sync service not configured or not enabled"
msgstr "同期サービスが未設定または無効"
msgid "Failed to update UI: {error}"
msgstr "UI 更新失敗: {error}"
msgid "Failed to update status: {error}"
msgstr "ステータス更新失敗: {error}"
msgid "Sync service not initialised, please check configuration"
msgstr "同期サービスが未初期化。設定を確認してください。"
msgid "Sync service not initialised"
msgstr "同期サービス未初期化"
msgid "Testing WebDAV connection..."
msgstr "WebDAV 接続テスト中…"
msgid "Testing connection..."
msgstr "接続テスト中…"
msgid "Connection test successful"
msgstr "接続テスト成功"
msgid "Connection OK"
msgstr "接続正常"
msgid "Connection test failed"
msgstr "接続テスト失敗"
msgid "Connection failed"
msgstr "接続失敗"
msgid "Connection test error: {error}"
msgstr "接続テストエラー: {error}"
msgid "Connection error"
msgstr "接続エラー"
msgid "Sync service not initialised, cannot start sync"
msgstr "同期サービス未初期化のため開始不可"
msgid "Sync already in progress"
msgstr "同期は既に実行中"
msgid "Starting data sync..."
msgstr "データ同期開始…"
msgid "Syncing..."
msgstr "同期中…"
msgid "Syncing nucleon directory: {dir}"
msgstr "nucleon ディレクトリ同期中: {dir}"
msgid "Syncing electron directory: {dir}"
msgstr "electron ディレクトリ同期中: {dir}"
msgid "Syncing orbital directory: {dir}"
msgstr "orbital ディレクトリ同期中: {dir}"
msgid "nucleon sync complete: uploaded {up}, downloaded {down}"
msgstr "nucleon 同期完了: アップロード {up}, ダウンロード {down}"
msgid "electron sync complete: uploaded {up}, downloaded {down}"
msgstr "electron 同期完了: アップロード {up}, ダウンロード {down}"
msgid "orbital sync complete: uploaded {up}, downloaded {down}"
msgstr "orbital 同期完了: アップロード {up}, ダウンロード {down}"
msgid "Sync complete"
msgstr "同期完了"
msgid "All directories synced"
msgstr "全ディレクトリ同期完了"
msgid "Error during sync: {error}"
msgstr "同期エラー: {error}"
msgid "Sync failed"
msgstr "同期失敗"
msgid "Sync paused"
msgstr "同期一時停止"
msgid "Sync resumed"
msgstr "同期再開"
msgid "Sync cancelled"
msgstr "同期キャンセル"
msgid "Resume"
msgstr "再開"
msgid "Failed to update button state: {error}"
msgstr "ボタン状態更新失敗: {error}"
msgid "Unknown error"
msgstr "不明なエラー"
msgid "[b]Audio Pre-cache[/b]"
msgstr "[b]音声プリキャッシュ[/b]"
msgid "Cache rate: {rate:.1f}% ({cached} / {total} units)"
msgstr "キャッシュ率: {rate:.1f}% ({cached} / {total} ユニット)"
msgid "Target units from: [b]{desc}[/b]"
msgstr "対象ユニット: [b]{desc}[/b]"
msgid "Unit count: {n}"
msgstr "ユニット数: {n}"
msgid "Target: all units"
msgstr "対象: 全ユニット"
msgid "Start Pre-cache"
msgstr "プリキャッシュ開始"
msgid "Cancel Pre-cache"
msgstr "プリキャッシュ中止"
msgid "Clear Cache"
msgstr "キャッシュ消去"
msgid "Cache path: {path}"
msgstr "キャッシュパス: {path}"
msgid "Files: {n}"
msgstr "ファイル数: {n}"
msgid "Total size: {size}"
msgstr "合計サイズ: {size}"
msgid "Refresh"
msgstr "更新"
msgid "If you leave this screen, ongoing cache processes will stop automatically."
msgstr "この画面を離れるとキャッシュ処理は自動停止します。"
msgid "Cache supports \"resume from break\"."
msgstr "中断再開に対応しています。"
msgid "Ready"
msgstr "準備完了"
msgid "Waiting to start..."
msgstr "開始待機中…"
msgid "Status: {s}"
msgstr "ステータス: {s}"
msgid "Current item: {item}"
msgstr "現在の項目: {item}"
msgid "Processing ({i}/{total})"
msgstr "処理中 ({i}/{total})"
msgid "Error"
msgstr "エラー"
msgid "Failed, skipping: {item}"
msgstr "失敗、スキップ: {item}"
msgid "Cancelled"
msgstr "キャンセル済"
msgid "Pre-cache cancelled by user"
msgstr "ユーザーがプリキャッシュをキャンセル"
msgid "Cleared"
msgstr "消去済"
msgid "Audio cache cleared"
msgstr "音声キャッシュ消去済"
msgid "Failed to clear cache: {error}"
msgstr "キャッシュ消去失敗: {error}"
msgid "Cache info refreshed"
msgstr "キャッシュ情報更新完了"
msgid "This memorization session is finished"
msgstr "記憶セッション終了"
msgid "Algorithm data {}"
msgstr "アルゴリズムデータ{}"
msgid "saved"
msgstr "保存済"
msgid "not saved"
msgstr "未保存"
msgid "Back to Menu"
msgstr "メニューに戻る"
msgid "Perfect recall"
msgstr "完全に想起"
msgid "Correct after hesitation"
msgstr "ためらい後正解"
msgid "Correct with difficulty"
msgstr "困難だが正解"
msgid "Wrong but familiar"
msgstr "間違えたが見覚えあり"
msgid "Wrong and unfamiliar"
msgstr "間違えて見覚えなし"
msgid "Complete blank"
msgstr "まったく想起不可"
msgid "Evaluate how well you remember this content: "
msgstr "この内容の記憶程度を評価: "
msgid "### Note: {note}"
msgstr "### メモ: {note}"
msgid "I know this"
msgstr "分かっています"
msgid "Current input: {input}"
msgstr "現在の入力: {input}"
msgid "Backspace"
msgstr "BS"
msgid "Sample Label"
msgstr "サンプルラベル"
msgid "Sample Button"
msgstr "サンプルボタン"
Binary file not shown.
@@ -0,0 +1,669 @@
# Russian translations for HeurAMS.
# Copyright (C) 2026 Wang Zhiyu
#
msgid ""
msgstr ""
"Project-Id-Version: heurams 0.5.1\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "Welcome to the basic user interface!"
msgstr "Добро пожаловать в интерфейс!"
msgid "Loading config and context... "
msgstr "Загрузка конфигурации… "
msgid "Done! ({time}ms)"
msgstr "Готово! ({time}ms)"
msgid "Loading UI framework... "
msgstr "Загрузка UI… "
msgid "Loading UI layout... "
msgstr "Загрузка разметки… "
msgid "Component directory: {path}"
msgstr "Директория: {path}"
msgid "Working directory: {path}"
msgstr "Рабочая директория: {path}"
msgid "Pre-work total: {time}ms"
msgstr "Всего: {time}ms"
msgid "Heuristic Auxiliary Memorizing Scheduler"
msgstr "Эвристический планировщик запоминания"
msgid "Quit"
msgstr "Выход"
msgid "Theme"
msgstr "Тема"
msgid "Navigate"
msgstr "Навигация"
msgid "Settings"
msgstr "Настройки"
msgid "About"
msgstr "О программе"
msgid "HeurAMS {ver} - Heuristic Auxiliary Memorizing Scheduler"
msgstr "HeurAMS {ver} - Эвристический планировщик"
msgid "Listening address"
msgstr "Адрес"
msgid "Listening port"
msgstr "Порт"
msgid "Development mode hot reload"
msgstr "Горячая перезагрузка (dev)"
msgid "Show this help message"
msgstr "Показать справку"
msgid "unifront API service started: http://{host}:{port}"
msgstr "API запущен: http://{host}:{port}"
msgid "Show the version and exit."
msgstr "Показать версию и выйти."
msgid "Explicitly specify locale (defaults to LANG env)."
msgstr "Локаль (напр. en_US, ru). По умолчанию из LANG."
msgid "Launch the built-in user interface (TUI)"
msgstr "Запустить TUI"
msgid "Launch the API service (unifront)"
msgstr "Запустить API"
msgid "Dashboard"
msgstr "Панель"
msgid "Back"
msgstr "Назад"
msgid "Current daystamp: {ds}"
msgstr "Дата: {ds}"
msgid "Timezone offset: UTC+{offset}"
msgstr "Смещение: UTC+{offset}"
msgid "Default algorithm: {algo}"
msgstr "Алгоритм: {algo}"
msgid "Loaded {n} repo(s)"
msgstr "Загружено: {n}"
msgid "Total {n} unit(s)"
msgstr "Всего单元: {n}"
msgid "Activated {n} unit(s)"
msgstr "Активно: {n}"
msgid "Version {ver}-{stage}"
msgstr "Версия {ver}-{stage}"
msgid "Processed {puzzles} puzzles in {time}s, accuracy {accuracy}, speed {speed} puzzle(s)/s"
msgstr "Обработано {puzzles} задач за {time}с, точность {accuracy}, скорость {speed} задач/с"
msgid "N/A"
msgstr "Н/Д"
msgid ""
"No repo directories found in {path}.\n"
"Please import a repo and restart, or create a new one."
msgstr ""
"Репозитории не найдены в {path}.\n"
"Импортируйте или создайте новый."
msgid "Start Learning"
msgstr "Начать"
msgid ""
"{title} [{algo}]\n"
" [d]Progress: {touched}/{total} ({pct}%)[/d]\n"
" [d]{status}[/d]"
msgstr ""
"{title} [{algo}]\n"
" [d]Прогресс: {touched}/{total} ({pct}%)[/d]\n"
" [d]{status}[/d]"
msgid "Due: {review}R + {new}U"
msgstr "К сроку: {review}R + {new}U"
msgid "Not started: 0R + {new}U"
msgstr "Не начато: 0R + {new}U"
msgid "Up to date"
msgstr "Актуально"
msgid "[b]About & Version Info[/b]"
msgstr "[b]О программе[/b]"
msgid ""
"# About HeurAMS\n"
"\n"
"Main library version: `{ver}-python` \n"
"UI frontend: `Textual TUI (Basic UI)` \n"
"UI version: `{ver}` \n"
"API codename: `{codename}` \n"
"\n"
"> A heuristic auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently. \n"
"> An open, elegant, and extensible spaced repetition scheduler experimental platform, designed to help researchers conduct investigations, experiments, and research on cutting-edge memory algorithms more efficiently. \n"
"\n"
"You can visit the project homepage at https://ams.pluv27.top for user guides, development documentation and software updates, and participate in software development and improvement. \n"
"\n"
"Open source under the GNU Affero General Public License (version 3), with an additional exemption clause for local API calls, used for other frontend to library interface calls. \n"
"\n"
"You are using the built-in terminal user interface, which is the first full-featured frontend implementation and library test suite, located in the interface subdirectory of the library. \n"
"\n"
"Developers: \n"
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer \n"
"\n"
"Special thanks to the following individuals and groups; their algorithms and theories form the cornerstone of the current software algorithms: \n"
"\n"
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 algorithm and SM-15 algorithm theory \n"
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references \n"
"- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm \n"
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation \n"
"\n"
"# Runtime Environment\n"
"\n"
"Python interpreter version: {python_version} \n"
"Python interpreter path: {executable} \n"
"Textual framework version: {textual_version} \n"
"Terminal emulator: {terminal_info} \n"
"Operating system version: {os_version} \n"
"Disk free space: {disk_usage} \n"
"\n"
"When reporting issues, please copy this information into the issue description and attach `heurams.log` as an attachment to help developers locate the error."
msgstr ""
"# О HeurAMS\n"
"\n"
"Версия: `{ver}-python` \n"
"UI: `Textual TUI` \n"
"Версия UI: `{ver}` \n"
"Кодовое имя: `{codename}` \n"
"\n"
"> Эвристический планировщик запоминания на основе когнитивных наук. \n"
"\n"
"Сайт: https://ams.pluv27.top \n"
"\n"
"AGPL-3.0 с исключением для локальных API. \n"
"\n"
"Разработчик: \n"
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)) \n"
"\n"
"Благодарности: \n"
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2, SM-15 \n"
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS \n"
"- [Kazuaki Tanida](https://github.com/slaypni): SM-15 CoffeeScript \n"
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS \n"
"\n"
"# Окружение\n"
"\n"
"Python: {python_version} \n"
"Путь: {executable} \n"
"Textual: {textual_version} \n"
"Терминал: {terminal_info} \n"
"ОС: {os_version} \n"
"Диск: {disk_usage} \n"
"\n"
"Прикрепите эту информацию к баг-репорту."
msgid "Back to Main"
msgstr "На главную"
msgid "Unknown"
msgstr "Неизв."
msgid "Requires a float"
msgstr "Нужно число с точкой"
msgid "Requires a string"
msgstr "Нужна строка"
msgid "Requires an integer"
msgstr "Нужно целое"
msgid "Unknown type"
msgstr "Неизв. тип"
msgid "No sub-items"
msgstr "Нет подпунктов"
msgid "Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."
msgstr "Изменения сохраняются сразу. Рекомендуется перезапуск."
msgid "Previous"
msgstr "Назад"
msgid "Read Aloud"
msgstr "Озвучить"
msgid "Favorite"
msgstr "Избранное"
msgid "Learning"
msgstr "Обучение"
msgid "Correct"
msgstr "Верно"
msgid "Incorrect"
msgstr "Неверно"
msgid "Failed to generate puzzle: {e}"
msgstr "Ошибка генерации: {e}"
msgid "Favorited"
msgstr "В избранном"
msgid "Not favorited"
msgstr "Не в избранном"
msgid "Previous: {ident}"
msgstr "Пред.: {ident}"
msgid "Are you sure? Press uppercase Q to go back."
msgstr "Уверены? Нажмите Q для возврата."
msgid "Cannot favorite: no repo associated"
msgstr "Ошибка: репозиторий не связан"
msgid "Unfavorited: {ident}"
msgstr "Удалено: {ident}"
msgid "Favorited: {ident}"
msgstr "Добавлено: {ident}"
msgid "This function is not available during memorization. Please finish or go back first."
msgstr "Функция недоступна во время запоминания."
msgid "Time resume corrected: {old} -> {new}"
msgstr "Время скорректировано: {old} -> {new}"
msgid " [Debug Connected]"
msgstr " [Отладка]"
msgid "Favorites"
msgstr "Избранное"
msgid "No favorites"
msgstr "Нет избранного"
msgid "Press * in the memorization screen to add favorites."
msgstr "Нажмите * для добавления в избранное."
msgid "Total {n} favorite(s)"
msgstr "Избранного: {n}"
msgid ""
" [d]Added: {time}\n"
" From {title}[/d]"
msgstr ""
" [d]Добавлено: {time}\n"
" Из: {title}[/d]"
msgid "Remove"
msgstr "Удалить"
msgid "Operation failed: invalid button identifier"
msgstr "Ошибка: неверный ID кнопки"
msgid "Removed favorite: {ident}"
msgstr "Удалено: {ident}"
msgid "Failed to remove: {ident}"
msgstr "Ошибка удаления: {ident}"
msgid "Switch"
msgstr "Переключить"
msgid "Cache Manager"
msgstr "Кеш"
msgid "Settings Page"
msgstr "Настройки"
msgid "Sync Tool"
msgstr "Синхр."
msgid "Exit"
msgstr "Выход"
msgid "Project Homepage"
msgstr "Сайт проекта"
msgid ""
"[b]Select a function to navigate to\n"
"or a memorization session instance[/b]\n"
"\n"
"Tips will be displayed here"
msgstr ""
"[b]Выберите функцию\n"
"или сеанс[/b]\n"
"\n"
"Подсказки здесь"
msgid ""
"Press Enter to switch\n"
"All sessions will be saved"
msgstr ""
"Enter для переключения\n"
"Сеансы будут сохранены"
msgid "Close (n)"
msgstr "Закрыть (n)"
msgid "Prepare Repository"
msgstr "Подготовка"
msgid "Cache"
msgstr "Кеш"
msgid ""
"**Ready**: `{title}`\n"
""
msgstr ""
"**Готово**: `{title}`\n"
""
msgid "Repo path: {path}"
msgstr "Путь: {path}"
msgid "Progress: {touched}/{total} [{pct}%]"
msgstr "Прогресс: {touched}/{total} [{pct}%]"
msgid "Scheduling algorithm: {algo} {desc}"
msgstr "Алгоритм: {algo} {desc}"
msgid ""
"Study count: {total} = {review} [d][Review][/d] + {new} [d][New][/d]\n"
""
msgstr ""
"Всего: {total} = {review} [d][Повтор][/d] + {new} [d][Новое][/d]\n"
""
msgid "Start Memorizing"
msgstr "Начать"
msgid "Manage Cache"
msgstr "Кеш"
msgid "Sync protocol: {proto}"
msgstr "Протокол: {proto}"
msgid "Server Configuration:"
msgstr "Сервер:"
msgid "Remote server:"
msgstr "Сервер:"
msgid "Remote path:"
msgstr "Путь:"
msgid "Test Connection"
msgstr "Тест"
msgid "Start Sync"
msgstr "Синхр."
msgid "Pause"
msgstr "Пауза"
msgid "Cancel"
msgstr "Отмена"
msgid "Sync Progress"
msgstr "Прогресс"
msgid "Sync Log"
msgstr "Лог"
msgid "Not configured"
msgstr "Не настроено"
msgid "Sync service ready"
msgstr "Синхр. готова"
msgid "Sync service not configured or not enabled"
msgstr "Синхр. не настроена"
msgid "Failed to update UI: {error}"
msgstr "Ошибка UI: {error}"
msgid "Failed to update status: {error}"
msgstr "Ошибка статуса: {error}"
msgid "Sync service not initialised, please check configuration"
msgstr "Синхр. не инициализирована"
msgid "Sync service not initialised"
msgstr "Не инициализирована"
msgid "Testing WebDAV connection..."
msgstr "Тест WebDAV…"
msgid "Testing connection..."
msgstr "Тест соединения…"
msgid "Connection test successful"
msgstr "Тест OK"
msgid "Connection OK"
msgstr "Соединение OK"
msgid "Connection test failed"
msgstr "Тест не удался"
msgid "Connection failed"
msgstr "Ошибка соединения"
msgid "Connection test error: {error}"
msgstr "Ошибка теста: {error}"
msgid "Connection error"
msgstr "Ошибка соединения"
msgid "Sync service not initialised, cannot start sync"
msgstr "Синхр. не иниц., запуск невозможен"
msgid "Sync already in progress"
msgstr "Синхр. уже выполняется"
msgid "Starting data sync..."
msgstr "Запуск синхр…"
msgid "Syncing..."
msgstr "Синхр…"
msgid "Syncing nucleon directory: {dir}"
msgstr "Синхр. nucleon: {dir}"
msgid "Syncing electron directory: {dir}"
msgstr "Синхр. electron: {dir}"
msgid "Syncing orbital directory: {dir}"
msgstr "Синхр. orbital: {dir}"
msgid "nucleon sync complete: uploaded {up}, downloaded {down}"
msgstr "nucleon: загружено {up}, скачано {down}"
msgid "electron sync complete: uploaded {up}, downloaded {down}"
msgstr "electron: загружено {up}, скачано {down}"
msgid "orbital sync complete: uploaded {up}, downloaded {down}"
msgstr "orbital: загружено {up}, скачано {down}"
msgid "Sync complete"
msgstr "Синхр. завершена"
msgid "All directories synced"
msgstr "Всё синхронизировано"
msgid "Error during sync: {error}"
msgstr "Ошибка синхр.: {error}"
msgid "Sync failed"
msgstr "Синхр. не удалась"
msgid "Sync paused"
msgstr "Пауза"
msgid "Sync resumed"
msgstr "Продолжено"
msgid "Sync cancelled"
msgstr "Отменено"
msgid "Resume"
msgstr "Продолжить"
msgid "Failed to update button state: {error}"
msgstr "Ошибка кнопки: {error}"
msgid "Unknown error"
msgstr "Неизв. ошибка"
msgid "[b]Audio Pre-cache[/b]"
msgstr "[b]Аудиокеш[/b]"
msgid "Cache rate: {rate:.1f}% ({cached} / {total} units)"
msgstr "Кеш: {rate:.1f}% ({cached} / {total})"
msgid "Target units from: [b]{desc}[/b]"
msgstr "Цель: [b]{desc}[/b]"
msgid "Unit count: {n}"
msgstr "Единиц: {n}"
msgid "Target: all units"
msgstr "Цель: всё"
msgid "Start Pre-cache"
msgstr "Начать"
msgid "Cancel Pre-cache"
msgstr "Отмена"
msgid "Clear Cache"
msgstr "Очистить"
msgid "Cache path: {path}"
msgstr "Путь кеша: {path}"
msgid "Files: {n}"
msgstr "Файлов: {n}"
msgid "Total size: {size}"
msgstr "Размер: {size}"
msgid "Refresh"
msgstr "Обновить"
msgid "If you leave this screen, ongoing cache processes will stop automatically."
msgstr "При выходе кеширование остановится."
msgid "Cache supports \"resume from break\"."
msgstr "Поддерживается возобновление."
msgid "Ready"
msgstr "Готово"
msgid "Waiting to start..."
msgstr "Ожидание…"
msgid "Status: {s}"
msgstr "Статус: {s}"
msgid "Current item: {item}"
msgstr "Элемент: {item}"
msgid "Processing ({i}/{total})"
msgstr "Обработка ({i}/{total})"
msgid "Error"
msgstr "Ошибка"
msgid "Failed, skipping: {item}"
msgstr "Ошибка, пропуск: {item}"
msgid "Cancelled"
msgstr "Отменено"
msgid "Pre-cache cancelled by user"
msgstr "Отменено пользователем"
msgid "Cleared"
msgstr "Очищено"
msgid "Audio cache cleared"
msgstr "Кеш очищен"
msgid "Failed to clear cache: {error}"
msgstr "Ошибка очистки: {error}"
msgid "Cache info refreshed"
msgstr "Инфо обновлено"
msgid "This memorization session is finished"
msgstr "Сеанс завершен"
msgid "Algorithm data {}"
msgstr "Данные алгоритма{}"
msgid "saved"
msgstr "сохранено"
msgid "not saved"
msgstr "не сохранено"
msgid "Back to Menu"
msgstr "В меню"
msgid "Perfect recall"
msgstr "Идеально"
msgid "Correct after hesitation"
msgstr "Верно после паузы"
msgid "Correct with difficulty"
msgstr "Верно с трудом"
msgid "Wrong but familiar"
msgstr "Неверно, но знакомо"
msgid "Wrong and unfamiliar"
msgstr "Неверно и незнакомо"
msgid "Complete blank"
msgstr "Полный провал"
msgid "Evaluate how well you remember this content: "
msgstr "Оцените запоминание: "
msgid "### Note: {note}"
msgstr "### Заметка: {note}"
msgid "I know this"
msgstr "Знаю"
msgid "Current input: {input}"
msgstr "Ввод: {input}"
msgid "Backspace"
msgstr "Забой"
msgid "Sample Label"
msgstr "Метка"
msgid "Sample Button"
msgstr "Кнопка"
+7
View File
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE martif PUBLIC 'ISO 12200:1999A//DTD MARTIF core (DXFcdV04)//EN' 'TBXcdv04.dtd'>
<martif type="TBX" xml:lang="en_US">
<text>
<body/>
</text>
</martif>
Binary file not shown.
@@ -0,0 +1,679 @@
# Chinese (Simplified) Language Pack for HeurAMS.
# Copyright (C) 2026 Wang Zhiyu
# This file is distributed under the same license as the HeurAMS project.
# Wang Zhiyu <pluvium27@outlook.com>, 2026.
#
msgid ""
msgstr ""
"Project-Id-Version: heurams 0.5.1\n"
"Report-Msgid-Bugs-To: https://github.com/pluvium27/HeurAMS/issues\n"
"POT-Creation-Date: 2026-05-21 00:00+0000\n"
"PO-Revision-Date: 2026-05-21 00:00+0000\n"
"Last-Translator: Wang Zhiyu <pluvium27@outlook.com>\n"
"Language-Team: Chinese (Simplified) <pluvium27@outlook.com>\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "Welcome to the basic user interface!"
msgstr "欢迎使用基本用户界面!"
msgid "Loading config and context... "
msgstr "加载配置与上下文... "
msgid "Done! ({time}ms)"
msgstr "已完成! (耗时: {time}ms)"
msgid "Loading UI framework... "
msgstr "加载用户界面框架... "
msgid "Loading UI layout... "
msgstr "加载用户界面布局... "
msgid "Component directory: {path}"
msgstr "组件目录: {path}"
msgid "Working directory: {path}"
msgstr "工作目录: {path}"
msgid "Pre-work total: {time}ms"
msgstr "前置工作共计耗时: {time}ms"
msgid "Heuristic Auxiliary Memorizing Scheduler"
msgstr "启发式辅助记忆调度器"
msgid "Quit"
msgstr "退出"
msgid "Theme"
msgstr "主题"
msgid "Navigate"
msgstr "导航"
msgid "Settings"
msgstr "设置"
msgid "About"
msgstr "关于"
msgid "HeurAMS {ver} - Heuristic Auxiliary Memorizing Scheduler"
msgstr "HeurAMS {ver} - 启发式辅助记忆调度器"
msgid "Listening address"
msgstr "监听地址"
msgid "Listening port"
msgstr "监听端口"
msgid "Development mode hot reload"
msgstr "开发模式热重载"
msgid "Show this help message"
msgstr "显示此帮助信息"
msgid "unifront API service started: http://{host}:{port}"
msgstr "unifront API 服务启动: http://{host}:{port}"
msgid "Show the version and exit."
msgstr "显示版本并退出."
msgid "Explicitly specify locale (defaults to LANG env)."
msgstr "显式指定语言 (默认使用 LANG 环境变量)."
msgid "Launch the built-in user interface (TUI)"
msgstr "启动内置基本用户界面 (TUI)"
msgid "Launch the API service (unifront)"
msgstr "启动 API 服务 (unifront)"
msgid "Dashboard"
msgstr "仪表盘"
msgid "Back"
msgstr "返回"
msgid "Current daystamp: {ds}"
msgstr "当前日时间戳: {ds}"
msgid "Timezone offset: UTC+{offset}"
msgstr "应用时区修正: UTC+{offset}"
msgid "Default algorithm: {algo}"
msgstr "默认算法设置: {algo}"
msgid "Loaded {n} repo(s)"
msgstr "已加载 {n} 个单元集"
msgid "Total {n} unit(s)"
msgstr "共计 {n} 个单元"
msgid "Activated {n} unit(s)"
msgstr "已激活 {n} 个单元"
msgid "Version {ver}-{stage}"
msgstr "版本 {ver}-{stage}"
msgid "Processed {puzzles} puzzles in {time}s, accuracy {accuracy}, speed {speed} puzzle(s)/s"
msgstr "在 {time} 秒内处理了 {puzzles} 个谜题, 正确率 {accuracy}, 平均速度 {speed} 个/秒"
msgid "N/A"
msgstr "无法求解"
msgid ""
"No repo directories found in {path}.\n"
"Please import a repo and restart, or create a new one."
msgstr ""
"在 {path} 中未找到任何单元集仓库目录.\n"
"请导入单元集后重启应用, 或者新建单元集."
msgid "Start Learning"
msgstr "开始学习"
msgid ""
"{title} [{algo}]\n"
" [d]Progress: {touched}/{total} ({pct}%)[/d]\n"
" [d]{status}[/d]"
msgstr ""
"{title} [{algo}]\n"
" [d]进度: {touched}/{total} ({pct}%)[/d]\n"
" [d]{status}[/d]"
msgid "Due: {review}R + {new}U"
msgstr "需要学习: {review}R + {new}U"
msgid "Not started: 0R + {new}U"
msgstr "暂未开始: 0R + {new}U"
msgid "Up to date"
msgstr "无需操作"
msgid "[b]About & Version Info[/b]"
msgstr "[b]关于与版本信息[/b]"
msgid ""
"# About HeurAMS\n"
"\n"
"Main library version: `{ver}-python` \n"
"UI frontend: `Textual TUI (Basic UI)` \n"
"UI version: `{ver}` \n"
"API codename: `{codename}` \n"
"\n"
"> A heuristic auxiliary memorizing scheduler based on heuristic algorithms and cognitive science theories, designed to help users memorize and plan learning more efficiently. \n"
"> An open, elegant, and extensible spaced repetition scheduler experimental platform, designed to help researchers conduct investigations, experiments, and research on cutting-edge memory algorithms more efficiently. \n"
"\n"
"You can visit the project homepage at https://ams.pluv27.top for user guides, development documentation and software updates, and participate in software development and improvement. \n"
"\n"
"Open source under the GNU Affero General Public License (version 3), with an additional exemption clause for local API calls, used for other frontend to library interface calls. \n"
"\n"
"You are using the built-in terminal user interface, which is the first full-featured frontend implementation and library test suite, located in the interface subdirectory of the library. \n"
"\n"
"Developers: \n"
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer \n"
"\n"
"Special thanks to the following individuals and groups; their algorithms and theories form the cornerstone of the current software algorithms: \n"
"\n"
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 algorithm and SM-15 algorithm theory \n"
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS algorithm and spaced repetition theory references \n"
"- [Kazuaki Tanida](https://github.com/slaypni): CoffeeScript reverse implementation of SM-15 algorithm \n"
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS algorithm underlying implementation \n"
"\n"
"# Runtime Environment\n"
"\n"
"Python interpreter version: {python_version} \n"
"Python interpreter path: {executable} \n"
"Textual framework version: {textual_version} \n"
"Terminal emulator: {terminal_info} \n"
"Operating system version: {os_version} \n"
"Disk free space: {disk_usage} \n"
"\n"
"When reporting issues, please copy this information into the issue description and attach `heurams.log` as an attachment to help developers locate the error."
msgstr ""
"# 关于 HeurAMS\n"
"\n"
"主程序库版本: `{ver}-python` \n"
"用户界面分支: `Textual TUI (基本用户界面)` \n"
"用户界面版本: `{ver}` \n"
"API 版本代号: `{codename}` \n"
"\n"
"> 一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划. \n"
"> 一个开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究. \n"
"\n"
"您可在项目主页 https://ams.pluv27.top 获取用户指南, 开发文档与软件更新, 并参与到软件的开发与改进工作. \n"
"\n"
"以 GNU Affero 通用公共许可证 (第3版) 开放源代码, 并有一条豁免本机 API 调用的附加条款, 用于其他前端到程序库的接口调用. \n"
"\n"
"您正使用程序库内置的终端用户界面, 它是第一个全功能前端实现与程序库测试套件, 位于程序库的 interface 子目录. \n"
"\n"
"开发人员列表: \n"
"- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): 项目发起与主要开发者 \n"
"\n"
"感谢以下人士与团体, 他们的算法与理论构成了此软件现有算法的基石: \n"
"\n"
"- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 算法与 SM-15 算法理论 \n"
"- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS 算法与间隔重复理论文献参考 \n"
"- [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 逆向实现 \n"
"- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 算法底层实现 \n"
"\n"
"# 运行环境信息\n"
"\n"
"Python 解释器版本: {python_version} \n"
"Python 解释器路径: {executable} \n"
"Textual 框架版本: {textual_version} \n"
"终端模拟器: {terminal_info} \n"
"操作系统版本: {os_version} \n"
"存储余量: {disk_usage} \n"
"\n"
"报告问题时, 请复制这些信息到问题描述, 并上传软件日志 `heurams.log` 作为附件, 以协助开发者定位错误."
msgid "Back to Main"
msgstr "返回主界面"
msgid "Unknown"
msgstr "未知"
msgid "Requires a float"
msgstr "要求一个浮点数"
msgid "Requires a string"
msgstr "要求一个字符串"
msgid "Requires an integer"
msgstr "要求一个整数"
msgid "Unknown type"
msgstr "未知类型"
msgid "No sub-items"
msgstr "无子项"
msgid "Changes are saved immediately when you leave this page, but restart is recommended to ensure the new configuration is applied."
msgstr "退出页面时, 所作的更改会立即保存, 但仍建议重启软件以确保新的配置得到应用."
msgid "Previous"
msgstr "查看上一个"
msgid "Read Aloud"
msgstr "朗读"
msgid "Favorite"
msgstr "收藏"
msgid "Learning"
msgstr "学习中"
msgid "Correct"
msgstr "正确应答"
msgid "Incorrect"
msgstr "错误应答"
msgid "Failed to generate puzzle: {e}"
msgstr "无法生成谜题: {e}"
msgid "Favorited"
msgstr "已收藏"
msgid "Not favorited"
msgstr "未收藏"
msgid "Previous: {ident}"
msgstr "上一个: {ident}"
msgid "Are you sure? Press uppercase Q to go back."
msgstr "确定吗? 按下大写 Q 以返回."
msgid "Cannot favorite: no repo associated"
msgstr "无法收藏: 未关联仓库"
msgid "Unfavorited: {ident}"
msgstr "已取消收藏: {ident}"
msgid "Favorited: {ident}"
msgstr "已收藏: {ident}"
msgid "This function is not available during memorization. Please finish or go back first."
msgstr "功能在记忆界面中不可用, 完成或返回后再试."
msgid "Time resume corrected: {old} -> {new}"
msgstr "时间恢复已修正: {old} -> {new}"
msgid " [Debug Connected]"
msgstr " [调试已连接]"
msgid "Favorites"
msgstr "收藏夹"
msgid "No favorites"
msgstr "暂无收藏"
msgid "Press * in the memorization screen to add favorites."
msgstr "使用 * 键在记忆界面中添加收藏."
msgid "Total {n} favorite(s)"
msgstr "共 {n} 个收藏项"
msgid ""
" [d]Added: {time}\n"
" From {title}[/d]"
msgstr ""
" [d]添加于: {time}\n"
" 来自 {title}[/d]"
msgid "Remove"
msgstr "移除"
msgid "Operation failed: invalid button identifier"
msgstr "操作失败: 无效的按钮标识"
msgid "Removed favorite: {ident}"
msgstr "已移除收藏: {ident}"
msgid "Failed to remove: {ident}"
msgstr "移除失败: {ident}"
msgid "Switch"
msgstr "切换"
msgid "Cache Manager"
msgstr "缓存管理器"
msgid "Settings Page"
msgstr "设置页面"
msgid "Sync Tool"
msgstr "同步工具"
msgid "Exit"
msgstr "退出程序"
msgid "Project Homepage"
msgstr "项目主页"
msgid ""
"[b]Select a function to navigate to\n"
"or a memorization session instance[/b]\n"
"\n"
"Tips will be displayed here"
msgstr ""
"[b]请选择要跳转的功能\n"
"或记忆会话实例[/b]\n"
"\n"
"将在此处显示提示"
msgid ""
"Press Enter to switch\n"
"All sessions will be saved"
msgstr ""
"按下回车以完成切换\n"
"所有会话将被保存"
msgid "Close (n)"
msgstr "关闭 (n)"
msgid "Prepare Repository"
msgstr "准备记忆集"
msgid "Cache"
msgstr "缓存"
msgid ""
"**Ready**: `{title}`\n"
""
msgstr ""
"**准备就绪**: `{title}`\n"
""
msgid "Repo path: {path}"
msgstr "单元集路径: {path}"
msgid "Progress: {touched}/{total} [{pct}%]"
msgstr "学习完成度: {touched}/{total} [{pct}%]"
msgid "Scheduling algorithm: {algo} {desc}"
msgstr "调度算法: {algo} {desc}"
msgid ""
"Study count: {total} = {review} [d][Review][/d] + {new} [d][New][/d]\n"
""
msgstr ""
"学习数量: {total} = {review} [d][复习][/d] + {new} [d][新识记][/d]\n"
""
msgid "Start Memorizing"
msgstr "开始记忆"
msgid "Manage Cache"
msgstr "管理缓存"
msgid "Sync protocol: {proto}"
msgstr "同步协议: {proto}"
msgid "Server Configuration:"
msgstr "服务器配置:"
msgid "Remote server:"
msgstr "远程服务器:"
msgid "Remote path:"
msgstr "远程路径:"
msgid "Test Connection"
msgstr "测试连接"
msgid "Start Sync"
msgstr "开始同步"
msgid "Pause"
msgstr "暂停"
msgid "Cancel"
msgstr "取消"
msgid "Sync Progress"
msgstr "同步进度"
msgid "Sync Log"
msgstr "同步日志"
msgid "Not configured"
msgstr "未配置"
msgid "Sync service ready"
msgstr "同步服务已就绪"
msgid "Sync service not configured or not enabled"
msgstr "同步服务未配置或未启用"
msgid "Failed to update UI: {error}"
msgstr "更新 UI 失败: {error}"
msgid "Failed to update status: {error}"
msgstr "更新状态失败: {error}"
msgid "Sync service not initialised, please check configuration"
msgstr "同步服务未初始化, 请检查配置"
msgid "Sync service not initialised"
msgstr "同步服务未初始化"
msgid "Testing WebDAV connection..."
msgstr "正在测试 WebDAV 连接..."
msgid "Testing connection..."
msgstr "正在测试连接..."
msgid "Connection test successful"
msgstr "连接测试成功"
msgid "Connection OK"
msgstr "连接正常"
msgid "Connection test failed"
msgstr "连接测试失败"
msgid "Connection failed"
msgstr "连接失败"
msgid "Connection test error: {error}"
msgstr "连接测试异常: {error}"
msgid "Connection error"
msgstr "连接异常"
msgid "Sync service not initialised, cannot start sync"
msgstr "同步服务未初始化, 无法开始同步"
msgid "Sync already in progress"
msgstr "同步已在进行中"
msgid "Starting data sync..."
msgstr "开始同步数据..."
msgid "Syncing..."
msgstr "正在同步..."
msgid "Syncing nucleon directory: {dir}"
msgstr "同步 nucleon 目录: {dir}"
msgid "Syncing electron directory: {dir}"
msgstr "同步 electron 目录: {dir}"
msgid "Syncing orbital directory: {dir}"
msgstr "同步 orbital 目录: {dir}"
msgid "nucleon sync complete: uploaded {up}, downloaded {down}"
msgstr "nucleon 同步完成: 上传 {up} 个, 下载 {down} 个"
msgid "electron sync complete: uploaded {up}, downloaded {down}"
msgstr "electron 同步完成: 上传 {up} 个, 下载 {down} 个"
msgid "orbital sync complete: uploaded {up}, downloaded {down}"
msgstr "orbital 同步完成: 上传 {up} 个, 下载 {down} 个"
msgid "Sync complete"
msgstr "同步完成"
msgid "All directories synced"
msgstr "所有目录同步完成"
msgid "Error during sync: {error}"
msgstr "同步过程中发生错误: {error}"
msgid "Sync failed"
msgstr "同步失败"
msgid "Sync paused"
msgstr "同步已暂停"
msgid "Sync resumed"
msgstr "同步已恢复"
msgid "Sync cancelled"
msgstr "同步已取消"
msgid "Resume"
msgstr "继续"
msgid "Failed to update button state: {error}"
msgstr "更新按钮状态失败: {error}"
msgid "Unknown error"
msgstr "未知错误"
msgid "[b]Audio Pre-cache[/b]"
msgstr "[b]音频预缓存[/b]"
msgid "Cache rate: {rate:.1f}% ({cached} / {total} units)"
msgstr "缓存率: {rate:.1f}% (已缓存 {cached} / {total} 个单元)"
msgid "Target units from: [b]{desc}[/b]"
msgstr "目标单元归属: [b]{desc}[/b]"
msgid "Unit count: {n}"
msgstr "单元数量: {n}"
msgid "Target: all units"
msgstr "目标: 所有单元"
msgid "Start Pre-cache"
msgstr "开始预缓存"
msgid "Cancel Pre-cache"
msgstr "取消预缓存"
msgid "Clear Cache"
msgstr "清空缓存"
msgid "Cache path: {path}"
msgstr "缓存路径: {path}"
msgid "Files: {n}"
msgstr "文件数: {n}"
msgid "Total size: {size}"
msgstr "总大小: {size}"
msgid "Refresh"
msgstr "刷新"
msgid "If you leave this screen, ongoing cache processes will stop automatically."
msgstr "若您离开此界面, 未完成的缓存进程会自动停止."
msgid "Cache supports \"resume from break\"."
msgstr "缓存程序支持 [断点续传]."
msgid "Ready"
msgstr "就绪"
msgid "Waiting to start..."
msgstr "等待开始..."
msgid "Status: {s}"
msgstr "状态: {s}"
msgid "Current item: {item}"
msgstr "当前项目: {item}"
msgid "Processing ({i}/{total})"
msgstr "正处理 ({i}/{total})"
msgid "Error"
msgstr "出错"
msgid "Failed, skipping: {item}"
msgstr "处理失败, 已跳过: {item}"
msgid "Cancelled"
msgstr "已取消"
msgid "Pre-cache cancelled by user"
msgstr "预缓存操作被用户取消"
msgid "Cleared"
msgstr "已清空"
msgid "Audio cache cleared"
msgstr "音频缓存已清空"
msgid "Failed to clear cache: {error}"
msgstr "清空缓存失败: {error}"
msgid "Cache info refreshed"
msgstr "缓存信息已刷新"
msgid "This memorization session is finished"
msgstr "本次记忆进程结束"
msgid "Algorithm data {}"
msgstr "算法数据{}"
msgid "saved"
msgstr "已保存"
msgid "not saved"
msgstr "未能保存"
msgid "Back to Menu"
msgstr "返回上一级"
msgid "Perfect recall"
msgstr "完美回想"
msgid "Correct after hesitation"
msgstr "犹豫后正确"
msgid "Correct with difficulty"
msgstr "困难地正确"
msgid "Wrong but familiar"
msgstr "错误但熟悉"
msgid "Wrong and unfamiliar"
msgstr "错误且不熟"
msgid "Complete blank"
msgstr "完全空白"
msgid "Evaluate how well you remember this content: "
msgstr "请评估你对这个内容的记忆程度: "
msgid "### Note: {note}"
msgstr "### 笔记: {note}"
msgid "I know this"
msgstr "我已知晓"
msgid "Current input: {input}"
msgstr "当前输入: {input}"
msgid "Backspace"
msgstr "退格"
msgid "Sample Label"
msgstr "示例标签"
msgid "Sample Button"
msgstr "示例按钮"
+1 -1
View File
@@ -11,4 +11,4 @@ __all__ = [
]
providers = {"termux": termux_audio, "playsound": playsound_audio}
logger.debug("音频 providers 已注册: %s", list(providers.keys()))
logger.debug("Audio providers registered: %s", list(providers.keys()))
@@ -5,17 +5,16 @@
import pathlib
import playsound3
from heurams.services.logger import get_logger
logger = get_logger(__name__)
def play_by_path(path: pathlib.Path):
logger.debug("playsound_audio.play_by_path: 开始播放 %s", path)
logger.debug("playsound_audio.play_by_path: playing %s", path)
try:
import playsound3
playsound3.playsound(str(path))
logger.debug("播放完成: %s", path)
logger.debug("Audio playing finished: %s", path)
except Exception as e:
logger.error("播放失败: %s, 错误: %s", path, e)
logger.error("Failed to play: %s, error: %s", path, e)
+4 -6
View File
@@ -12,12 +12,10 @@ logger = get_logger(__name__)
# from .protocol import PlayFunctionProtocol
def play_by_path(path: pathlib.Path):
logger.debug("termux_audio.play_by_path: 开始播放 %s", path)
logger.debug("termux_audio.play_by_path: playing %s", path)
try:
os.system(f"play-audio {path}")
logger.debug("播放命令已执行: %s", path)
os.system(f"play-audio {path.resolve()}")
logger.debug("Play audio: %s", path)
except Exception as e:
logger.error("播放失败: %s, 错误: %s", path, e)
raise
logger.error("Failed to play audio: %s, error: %s", path, e)
+1 -1
View File
@@ -16,4 +16,4 @@ providers = {
"openai": OpenAILLM,
}
logger.debug("LLM providers 已注册: %s", list(providers.keys()))
logger.debug("LLM providers registered: %s", list(providers.keys()))
-55
View File
@@ -1,55 +0,0 @@
"""LLM 提供者基类"""
import asyncio
from typing import Any, Dict, List
from heurams.services.logger import get_logger
logger = get_logger(__name__)
class BaseLLM:
"""LLM 提供者基类"""
name = "BaseLLM"
def __init__(self, config: Dict[str, Any]):
"""初始化 LLM 提供者
Args:
config: 提供者配置字典
"""
self.config = config
logger.debug("BaseLLM 初始化完成")
async def chat(self, messages: List[Dict[str, str]], **kwargs) -> str:
"""发送聊天消息并获取响应
Args:
messages: 消息列表, 每个消息为 {"role": "user"|"assistant"|"system", "content": "消息内容"}
**kwargs: 其他参数, temperature, max_tokens
Returns:
模型返回的文本响应
"""
logger.debug("BaseLLM.chat: messages=%d, kwargs=%s", len(messages), kwargs)
logger.warning("BaseLLM.chat 是基类方法, 未实现具体功能")
await asyncio.sleep(0) # 避免未使用异步的警告
return "BaseLLM 未实现具体功能"
async def chat_stream(self, messages: List[Dict[str, str]], **kwargs):
"""流式聊天(可选实现)
Args:
messages: 消息列表
**kwargs: 其他参数
Yields:
流式响应的文本块
"""
logger.debug(
"BaseLLM.chat_stream: messages=%d, kwargs=%s", len(messages), kwargs
)
logger.warning("BaseLLM.chat_stream 是基类方法, 未实现具体功能")
await asyncio.sleep(0)
yield "BaseLLM 未实现流式功能"
-95
View File
@@ -1,95 +0,0 @@
"""OpenAI 兼容 LLM 提供者"""
from typing import Any, AsyncGenerator, Dict, List
from heurams.services.logger import get_logger
from .base import BaseLLM
logger = get_logger(__name__)
class OpenAILLM(BaseLLM):
"""OpenAI 兼容 LLM 提供者"""
name = "OpenAI"
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
self.api_key = config.get("key", "")
self.base_url = config.get("url", "https://api.openai.com/v1")
self._client = None
logger.debug("OpenAILLM 初始化完成: base_url=%s", self.base_url)
def _get_client(self):
"""获取 OpenAI 客户端(延迟导入)"""
if self._client is None:
try:
from openai import AsyncOpenAI
except ImportError:
logger.error("未安装 openai 库, 请运行: pip install openai")
raise ImportError("未安装 openai 库, 请运行: pip install openai")
self._client = AsyncOpenAI(
api_key=self.api_key if self.api_key else None,
base_url=self.base_url if self.base_url else None,
)
return self._client
async def chat(self, messages: List[Dict[str, str]], **kwargs) -> str:
"""发送聊天消息并获取响应"""
logger.debug("OpenAILLM.chat: messages=%d", len(messages))
client = self._get_client()
# 默认参数
default_kwargs = {
"model": kwargs.get("model", "gpt-3.5-turbo"),
"temperature": kwargs.get("temperature", 0.7),
"max_tokens": kwargs.get("max_tokens", 1000),
}
# 合并参数, 优先使用传入的 kwargs
request_kwargs = {**default_kwargs, **kwargs}
request_kwargs["messages"] = messages
try:
response = await client.chat.completions.create(**request_kwargs)
content = response.choices[0].message.content
logger.debug(
"OpenAILLM.chat 成功: response length=%d",
len(content) if content else 0,
)
return content or ""
except Exception as e:
logger.error("OpenAILLM.chat 失败: %s", e)
raise
async def chat_stream(
self, messages: List[Dict[str, str]], **kwargs
) -> AsyncGenerator[str, None]:
"""流式聊天"""
logger.debug("OpenAILLM.chat_stream: messages=%d", len(messages))
client = self._get_client()
# 默认参数
default_kwargs = {
"model": kwargs.get("model", "gpt-3.5-turbo"),
"temperature": kwargs.get("temperature", 0.7),
"max_tokens": kwargs.get("max_tokens", 1000),
"stream": True,
}
# 合并参数
request_kwargs = {**default_kwargs, **kwargs}
request_kwargs["messages"] = messages
try:
stream = await client.chat.completions.create(**request_kwargs)
async for chunk in stream:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
except Exception as e:
logger.error("OpenAILLM.chat_stream 失败: %s", e)
raise
+1 -1
View File
@@ -15,4 +15,4 @@ providers = {
"edgetts": EdgeTTS,
}
logger.debug("TTS providers 已注册: %s", list(providers.keys()))
logger.debug("TTS providers registered: %s", list(providers.keys()))
+1 -1
View File
@@ -12,5 +12,5 @@ class BaseTTS:
def convert(cls, text: str, path: pathlib.Path | str = "") -> pathlib.Path:
"""path 是可选参数, 不填则自动返回生成文件路径"""
logger.debug("BaseTTS.convert: text length=%d, path=%s", len(text), path)
logger.warning("BaseTTS.convert 是基类方法, 未实现具体功能")
logger.warning("BaseTTS.convert is not a functional implementation")
return path # type: ignore
+3 -3
View File
@@ -21,10 +21,10 @@ class EdgeTTS(BaseTTS):
text,
config_var.get()["providers"]["tts"]["edgetts"]["voice"],
)
logger.debug("EdgeTTS 通信对象创建成功, 正在保存音频")
logger.debug("EdgeTTS object created, saving audio")
communicate.save_sync(str(path))
logger.debug("EdgeTTS 音频已保存到: %s", path)
logger.debug("EdgeTTS audio saved as %s", path)
return path # type: ignore
except Exception as e:
logger.error("EdgeTTS.convert 失败: %s", e)
logger.error("EdgeTTS.convert failed: %s", e)
raise
+2 -2
View File
@@ -11,7 +11,7 @@ logger = get_logger(__name__)
play_by_path: Callable = prov[
config_var.get()["services"]["audio"]["provider"]
].play_by_path
logger.debug(
"音频服务初始化完成, 使用 Provider: %s",
logger.info(
"TTS Service inited, using provider %s",
config_var.get()["services"]["audio"]["provider"],
)
+2 -2
View File
@@ -84,7 +84,7 @@ class ConfigDict(UserDict):
if i.suffix == ".toml":
self.data[i.stem] = i
else:
logger.debug(f"配置目录中有无效的文件 {i.stem}") # what's up bro
logger.error(f"Illegal file detected in config: {i.stem}") # what's up bro
def persist(self):
if self.is_dir:
@@ -92,7 +92,7 @@ class ConfigDict(UserDict):
j = self[i]
if isinstance(j, ConfigDict):
j.persist()
logger.debug("完成配置持久化")
logger.info("Data persisted")
return
with open(self.path, "w+") as f:
+2 -4
View File
@@ -19,14 +19,12 @@ def epath(
path = path.lstrip(".")
target = dct
keys = path.split(".")
logger.debug(f"处理 EPATH {path}, {new_value}")
logger.debug(f"Proceeding EPATH {path}, {new_value}")
for idx, i in enumerate(keys):
is_last = idx == len(keys) - 1
# 处理字典键
logger.debug(
f"处理 {i}, {(isinstance(target, dict) or isinstance(target, ConfigDict))} {i in target}"
)
logger.debug(f"Proceeding in detail: {i}, {(isinstance(target, dict) or isinstance(target, ConfigDict))} {i in target}")
if is_last and enable_modify:
# 最后一次循环执行修改
+9 -9
View File
@@ -73,9 +73,9 @@ class FavoriteManager:
with open(self._file_path, "r", encoding="utf-8") as f:
data = json.load(f)
self._favorites = [FavoriteItem.from_dict(item) for item in data]
logger.debug("收藏列表加载成功, 共 %d", len(self._favorites))
logger.info("Finished loading favlist, %d items in total", len(self._favorites))
except Exception as e:
logger.error("加载收藏列表失败: %s", e)
logger.error("Filed to load favlist: %s", e)
self._favorites = []
else:
self._favorites = []
@@ -86,9 +86,9 @@ class FavoriteManager:
data = [item.to_dict() for item in self._favorites]
with open(self._file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.debug("收藏列表保存成功, 共 %d", len(self._favorites))
logger.info("Finished saving favlist, %d items in total", len(self._favorites))
except Exception as e:
logger.error("保存收藏列表失败: %s", e)
logger.error("Failed to save favlist: %s", e)
def add(self, repo_path: str, ident: str, tags: List[str] | None = None) -> bool:
"""添加收藏
@@ -103,7 +103,7 @@ class FavoriteManager:
# 检查是否已存在
for item in self._favorites:
if item.repo_path == repo_path and item.ident == ident:
logger.debug("收藏已存在: %s/%s", repo_path, ident)
logger.info("Favitem already exists: %s/%s", repo_path, ident)
return False
item = FavoriteItem(
repo_path=repo_path,
@@ -113,7 +113,7 @@ class FavoriteManager:
)
self._favorites.append(item)
self.save()
logger.info("添加收藏: %s/%s", repo_path, ident)
logger.info("Add favitem: %s/%s", repo_path, ident)
return True
def remove(self, repo_path: str, ident: str) -> bool:
@@ -126,9 +126,9 @@ class FavoriteManager:
if item.repo_path == repo_path and item.ident == ident:
del self._favorites[idx]
self.save()
logger.info("移除收藏: %s/%s", repo_path, ident)
logger.info("Remove favitem: %s/%s", repo_path, ident)
return True
logger.debug("收藏不存在: %s/%s", repo_path, ident)
logger.error("Non-existed favitem: %s/%s", repo_path, ident)
return False
def has(self, repo_path: str, ident: str) -> bool:
@@ -150,7 +150,7 @@ class FavoriteManager:
"""清空收藏列表"""
self._favorites = []
self.save()
logger.info("清空收藏列表")
logger.info("Clear favlist")
def count(self) -> int:
"""收藏总数"""
+2 -6
View File
@@ -6,15 +6,11 @@ logger = get_logger(__name__)
def get_md5(text):
logger.debug(f"计算MD5哈希, 输入`{text}`")
logger.debug(f"MD5 hash input`{text}`")
result = hashlib.md5(text.encode("utf-8")).hexdigest()
logger.debug("哈希结果: %s...", result[:8])
logger.debug("Providing MD5 hash: %s...", result[:8])
return result
def hash(text):
# logger.debug(f"计算MD5-时间复合哈希, 输入`{text}`")
# result = hashlib.md5(f"{text}{random.randint(0,1000)}".encode("utf-8")).hexdigest()
# logger.debug("哈希结果: %s...", result[:8])
# return result
return get_md5(text)
+16 -68
View File
@@ -1,6 +1,6 @@
"""
HeurAMS 日志服务模块
基于Python标准logging, 提供统一日志记录功能
"""日志服务模块
基于 logging , 提供统一日志记录功能
"""
import logging
@@ -8,26 +8,24 @@ import logging.handlers
import pathlib
from typing import Optional, Union
# 默认配置
DEFAULT_LOG_LEVEL = logging.DEBUG
DEFAULT_LOG_FILE = pathlib.Path("heurams.log")
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d:%(funcName)s] - %(message)s"
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
# 全局logger缓存
_loggers = {}
def setup_logging(
log_file: Union[str, pathlib.Path] = DEFAULT_LOG_FILE,
log_level: int = DEFAULT_LOG_LEVEL,
log_format: str = DEFAULT_LOG_FORMAT,
date_format: str = DEFAULT_DATE_FORMAT,
max_bytes: int = 10 * 1024 * 1024, # 10MB
max_bytes: int = 16 * 1024 * 1024, # 16MB
backup_count: int = 5,
) -> None:
"""
置全局日志系统
置全局日志服务
Args:
log_file: 日志文件路径
@@ -44,7 +42,7 @@ def setup_logging(
# 创建formatter
formatter = logging.Formatter(log_format, date_format)
# 创建文件handler(使用RotatingFileHandler防止日志过大)
# 创建文件 handler (RotatingFileHandler)
file_handler = logging.handlers.RotatingFileHandler(
filename=log_path,
maxBytes=max_bytes,
@@ -54,7 +52,6 @@ def setup_logging(
file_handler.setFormatter(formatter)
file_handler.setLevel(log_level)
# 配置root logger - 设置为 WARNING 级别(只记录重要信息)
root_logger = logging.getLogger()
root_logger.setLevel(logging.WARNING) # 这里改为 WARNING
@@ -62,40 +59,28 @@ def setup_logging(
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 创建自己的应用logger(单独设置DEBUG级别)
# 创建 heurams logger单独设置 DEBUG 级别
app_logger = logging.getLogger("heurams")
app_logger.setLevel(log_level) # 保持DEBUG级别
app_logger.addHandler(file_handler)
# 禁止传播到root logger, 避免双重记录
# 禁止传播到 root logger, 避免双重记录
app_logger.propagate = False
# 设置第三方库的日志级别为WARNING, 避免调试信息干扰
third_party_loggers = [
"markdown_it",
"markdown_it.rules_block",
"markdown_it.rules_core",
"markdown_it.rules_inline",
"asyncio",
]
for logger_name in third_party_loggers:
logging.getLogger(logger_name).setLevel(logging.WARNING)
# 记录日志系统初始化
app_logger.info("日志系统已初始化, 日志文件: %s", log_path)
app_logger.debug("HeurAMS logger inited, path: %s", log_path.resolve())
def get_logger(name: Optional[str] = None) -> logging.Logger:
"""
获取指定名称的logger
获取指定名称的 logger
Args:
name: logger名称, 通常使用模块名(__name__)
如果为None, 返回root logger
如果为None, 返回 root logger
Returns:
logging.Logger实例
logging.Logger 实例
"""
if name is None:
return logging.getLogger()
@@ -106,49 +91,12 @@ def get_logger(name: Optional[str] = None) -> logging.Logger:
else:
logger_name = name
# 缓存logger以提高性能
# 缓存 logger 以提高性能, 以模块为单位的单例
if logger_name not in _loggers:
logger = logging.getLogger(logger_name)
_loggers[logger_name] = logger
return _loggers[logger_name]
# 便捷函数
def debug(msg: str, *args, **kwargs) -> None:
"""DEBUG级别日志"""
get_logger().debug(msg, *args, **kwargs)
def info(msg: str, *args, **kwargs) -> None:
"""INFO级别日志"""
get_logger().info(msg, *args, **kwargs)
def warning(msg: str, *args, **kwargs) -> None:
"""WARNING级别日志"""
get_logger().warning(msg, *args, **kwargs)
def error(msg: str, *args, **kwargs) -> None:
"""ERROR级别日志"""
get_logger().error(msg, *args, **kwargs)
def critical(msg: str, *args, **kwargs) -> None:
"""CRITICAL级别日志"""
get_logger().critical(msg, *args, **kwargs)
def exception(msg: str, *args, **kwargs) -> None:
"""记录异常信息 (ERROR级别)"""
get_logger().exception(msg, *args, **kwargs)
# 初始化日志系统(硬编码配置)
setup_logging()
# 模块级别的logger实例
logger = get_logger(__name__)
logger.info("HeurAMS日志服务模块已加载")
# 初始化日志系统
setup_logging()
+23 -14
View File
@@ -1,4 +1,5 @@
# 时间服务
"""时间服务
"""
import datetime
import time
@@ -7,32 +8,40 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__)
daystamp_override = config_var.get()["services"]["timer"]["daystamp_override"]
timestamp_override = config_var.get()["services"]["timer"]["timestamp_override"]
last_daystamp = 0
last_timestamp = 0
def get_daystamp() -> int:
"""获取当前日戳(以天为单位的整数时间戳)"""
time_override = config_var.get()["services"]["timer"]["daystamp_override"]
if time_override != -1:
logger.debug("使用覆盖的日戳: %d", time_override)
return int(time_override)
if daystamp_override != -1:
logger.debug("Daystamp overrode: %d", daystamp_override)
return int(daystamp_override)
result = int(
(time.time() + config_var.get()["services"]["timer"]["timezone_offset"])
// (24 * 3600)
)
logger.debug("计算日戳: %d", result)
global last_daystamp
if last_daystamp != result: # 用于避免日志泛洪
logger.debug("Providing new daystamp: %d", result)
last_daystamp = result
return result
def get_timestamp() -> float:
"""获取 UNIX 时间戳"""
# 搞这个函数的原因是要支持可复现操作
time_override = config_var.get()["services"]["timer"]["timestamp_override"]
if time_override != -1:
logger.debug("使用覆盖的时间戳: %f", time_override)
return float(time_override)
if timestamp_override != -1:
logger.debug("Timestamp overrode: %f", timestamp_override)
return float(timestamp_override)
result = time.time()
logger.debug("获取当前时间戳: %f", result)
global last_timestamp
if last_timestamp != result:
logger.debug("Providing new timestamp: %d", result)
last_timestamp = result
return result
@@ -49,9 +58,9 @@ def daystamp_to_datetime(daystamp: int) -> datetime.datetime:
def datetime_to_daystamp(dt: datetime.datetime) -> int:
"""将 datetime 转换为日戳从 1970-01-01 起的天数
"""将 datetime 转换为日戳 (从 1970-01-01 起的天数)
接受带时区或 naive datetimenaive 视为 UTC
接受带时区或 naive datetime (naive 视为 UTC)
"""
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
if dt.tzinfo is None:
@@ -61,5 +70,5 @@ def datetime_to_daystamp(dt: datetime.datetime) -> int:
def get_now_datetime() -> datetime.datetime:
"""获取当前时间的 UTC datetime遵守时间覆盖"""
"""获取当前时间的 UTC datetime (遵守时间覆盖)"""
return datetime.datetime.fromtimestamp(get_timestamp(), tz=datetime.timezone.utc)
+4 -3
View File
@@ -1,4 +1,5 @@
# 文本转语音服务
"""文本转语音服务
"""
from typing import Callable
from heurams.context import config_var
@@ -8,7 +9,7 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__)
convertor: Callable = prov[config_var.get()["services"]["tts"]["provider"]].convert
logger.debug(
"TTS 服务初始化完成, 使用 provider: %s",
logger.info(
"TTS Service inited, using provider: %s",
config_var.get()["services"]["tts"]["provider"],
)
+4 -5
View File
@@ -1,11 +1,10 @@
# 版本控制集成服务
"""版本服务
"""
from heurams.services.logger import get_logger
logger = get_logger(__name__)
ver = "0.5.0"
ver = "0.5.1"
stage = "stable"
codename = "fulcrum"
codename_cn = "支点"
logger.info("HeurAMS 版本: %s (%s), 阶段: %s", ver, codename, stage)
codename_cn = "支点"
+6
View File
@@ -0,0 +1,6 @@
"""unifront - HeurAMS API 前端模块"""
from .server import create_app
from .session import Session
__all__ = ["create_app", "Session"]
+92
View File
@@ -0,0 +1,92 @@
"""unifront 依赖注入"""
from __future__ import annotations
from pathlib import Path
import heurams.kernel.particles as pt
from heurams.context import config_var, workdir
from heurams.kernel.repolib.repo import Repo
from heurams.services.logger import get_logger
from .schemas import RepoInfo
logger = get_logger(__name__)
_repo_cache: dict[str, Repo] = {}
def get_data_repo_dir() -> Path:
"""获取仓库目录"""
return workdir / "data" / "repo"
def compute_repo_progress(repo: Repo) -> dict:
"""计算 repo 的学习进度"""
progress = {
"total": repo.data_length,
"touched": 0,
}
for i in range(repo.data_length):
try:
e = pt.Electron.from_data(
electronic_data=repo.electronic_data_lict[i],
algo_name=repo.config["algorithm"],
)
if e.is_activated():
progress["touched"] += 1
except Exception:
pass
return progress
def get_repo(package: str) -> Repo | None:
"""按包名获取 Repo"""
if package in _repo_cache:
return _repo_cache[package]
repo_dir = get_data_repo_dir() / package
if not repo_dir.exists():
return None
try:
repo = Repo.from_repodir(repo_dir)
_repo_cache[package] = repo
return repo
except Exception as e:
logger.error("加载仓库 %s 失败: %s", package, e)
return None
def get_config():
"""获取配置对象"""
return config_var.get()
def list_repos() -> list[RepoInfo]:
"""列举可用仓库"""
from .schemas import RepoInfo
repo_dir = get_data_repo_dir()
valid_repos = Repo.probe_valid_repos_in_dir(repo_dir)
result = []
for rp in valid_repos:
if rp.name in _repo_cache:
repo = _repo_cache[rp.name]
else:
repo = Repo.from_repodir(rp)
_repo_cache[rp.name] = repo
progress = compute_repo_progress(repo)
result.append(
RepoInfo(
package=repo.manifest.get("package", rp.name),
title=repo.manifest.get("title", rp.name),
author=repo.manifest.get("author", ""),
desc=repo.manifest.get("desc", ""),
source=str(rp),
total=progress["total"],
touched=progress["touched"],
)
)
return result
+9
View File
@@ -0,0 +1,9 @@
"""unifront 路由"""
from .repos import router as repos_router
from .config import router as config_router
from .atoms import router as atoms_router
from .review import router as review_router
from .misc import router as misc_router
__all__ = ["repos_router", "config_router", "atoms_router", "review_router", "misc_router"]
+192
View File
@@ -0,0 +1,192 @@
"""原子信息 REST 路由"""
from fastapi import APIRouter, HTTPException
import heurams.kernel.particles as pt
import heurams.services.timer as timer
from heurams.context import config_var
from heurams.services.favorite_service import favorite_manager
from heurams.services.logger import get_logger
from ..dependencies import get_repo, compute_repo_progress
logger = get_logger(__name__)
router = APIRouter(prefix="/api/repos/{package}", tags=["atoms"])
def _safe_lastdate(e) -> int:
try:
return e.lastdate()
except (KeyError, TypeError):
return 0
@router.get("/atoms")
def list_atoms(package: str, page: int = 1, page_size: int = 50) -> dict:
"""获取仓库的原子列表"""
repo = get_repo(package)
if repo is None:
raise HTTPException(status_code=404, detail=f"仓库 '{package}' 未找到")
idents = list(repo.ident_index)
total = len(idents)
start = (page - 1) * page_size
end = start + page_size
page_idents = idents[start:end]
items = []
for ident in page_idents:
e = pt.Electron.from_data(
electronic_data=repo.electronic_data_lict.get_itemic_unit(ident),
algo_name=repo.config["algorithm"],
)
items.append({
"ident": ident,
"activated": bool(e.is_activated()),
"due": e.is_due(),
"rept": e.rept(real_rept=True),
"interval": e["interval"],
"next_date": e.nextdate(),
"last_date": _safe_lastdate(e),
})
return {"total": total, "page": page, "page_size": page_size, "items": items}
@router.get("/atoms/{ident}")
def get_atom(package: str, ident: str) -> dict:
"""获取单个原子的完整信息"""
repo = get_repo(package)
if repo is None:
raise HTTPException(status_code=404, detail=f"仓库 '{package}' 未找到")
if ident not in repo.ident_index:
raise HTTPException(status_code=404, detail=f"原子 '{ident}' 未找到")
n = pt.Nucleon.from_data(
nucleonic_data=repo.nucleonic_data_lict.get_itemic_unit(ident)
)
e = pt.Electron.from_data(
electronic_data=repo.electronic_data_lict.get_itemic_unit(ident),
algo_name=repo.config["algorithm"],
)
nucleon_data = {}
for key in n:
if key in ("puzzles",):
continue
try:
nucleon_data[key] = n[key]
except Exception:
pass
return {
"ident": ident,
"electron": {
"activated": bool(e.is_activated()),
"due": e.is_due(),
"rept": e.rept(real_rept=True),
"interval": e["interval"],
"next_date": e.nextdate(),
"last_date": _safe_lastdate(e),
"last_modify": e.last_modify(),
"algodata": e.algodata.get(e.algoname, {}),
},
"nucleon": nucleon_data,
}
@router.get("/prepare")
def prepare_repo(package: str) -> dict:
"""获取仓库复习准备数据(repo 信息 + 所有原子状态预览)"""
repo = get_repo(package)
if repo is None:
raise HTTPException(status_code=404, detail=f"仓库 '{package}' 未找到")
progress = compute_repo_progress(repo)
today = timer.get_daystamp()
atoms = []
review_count = 0
new_count = 0
for i in repo.ident_index:
e = pt.Electron.from_data(
electronic_data=repo.electronic_data_lict.get_itemic_unit(i),
algo_name=repo.config["algorithm"],
)
activated = bool(e.is_activated())
due = e.is_due()
if activated and due:
status = "R"
review_count += 1
elif activated:
status = "A"
else:
status = "U"
new_count += 1
n = pt.Nucleon.from_data(
nucleonic_data=repo.nucleonic_data_lict.get_itemic_unit(i)
)
content = n.get("content", "") or n.get("tts_text", "") or i
atoms.append({
"ident": i,
"status": status,
"rept": e.rept(real_rept=True),
"interval": e["interval"],
"next_date": e.nextdate(),
"content": str(content)[:60],
})
schedule_num = repo.config["scheduled_num"]
return {
"repo": {
"package": repo.manifest.get("package", package),
"title": repo.manifest.get("title", package),
"author": repo.manifest.get("author", ""),
"desc": repo.manifest.get("desc", ""),
"source": str(repo.source),
"algorithm": repo.config["algorithm"],
"scheduled_num": schedule_num,
},
"progress": progress,
"preview": {"review": review_count, "new": new_count},
"today": today,
"atoms": atoms,
"total_atoms": len(atoms),
}
@router.get("/atoms/{ident}/favorite")
def get_favorite(package: str, ident: str) -> dict:
"""检查原子是否已收藏"""
repo = get_repo(package)
if repo is None:
raise HTTPException(status_code=404)
rel_path = _rel_repo_path(repo)
return {"favorited": favorite_manager.has(rel_path, ident)}
@router.post("/atoms/{ident}/favorite")
def toggle_favorite(package: str, ident: str) -> dict:
"""切换收藏状态"""
repo = get_repo(package)
if repo is None:
raise HTTPException(status_code=404)
rel_path = _rel_repo_path(repo)
if favorite_manager.has(rel_path, ident):
favorite_manager.remove(rel_path, ident)
return {"favorited": False, "action": "removed"}
else:
favorite_manager.add(rel_path, ident)
return {"favorited": True, "action": "added"}
def _rel_repo_path(repo) -> str:
"""获取仓库相对路径"""
from heurams.context import workdir
data_repo = workdir / "data" / "repo"
try:
return str(repo.source.relative_to(data_repo))
except (ValueError, AttributeError):
return str(repo.source)
+99
View File
@@ -0,0 +1,99 @@
"""配置查询与修改 REST 路由"""
from fastapi import APIRouter, HTTPException
from heurams.services.config import ConfigDict
from heurams.services.epath import epath
from ..dependencies import get_config
router = APIRouter(prefix="/api/config", tags=["config"])
@router.get("")
def get_config_tree():
"""获取完整配置树"""
config = get_config()
return _build_tree(config)
def _build_tree(node, prefix: str = "") -> list[dict]:
"""通过 ConfigDict 的 __getitem__ 展开配置树"""
result = []
for key in node:
if key.startswith("_"):
continue
full_key = f"{prefix}.{key}" if prefix else key
child = node[key]
if isinstance(child, ConfigDict):
children = _build_tree(child, full_key)
result.append({"key": full_key, "type": "branch", "children": children})
# 收集_meta数据
for mk, mv in node.items():
if mk == f"_{key}_desc" and isinstance(mv, str):
if children:
children[0]["_meta_desc"] = mv
else:
item = {"key": full_key, "type": type(child).__name__, "value": child}
cand_key = f"_{key}_candidate"
desc_key = f"_{key}_desc"
if desc_key in node:
item["desc"] = node[desc_key]
if cand_key in node:
item["candidates"] = node[cand_key]
result.append(item)
return result
@router.get("/{section:path}")
def get_config_section(section: str):
"""获取指定配置节的详细数据(含元数据)"""
config = get_config()
keys = section.split("/")
current = config
for key in keys:
if key not in current:
return {}
current = current[key]
result = {}
for k, v in current.items():
if k.startswith("_"):
continue
item = {"value": v, "type": type(v).__name__}
cand_key = f"_{k}_candidate"
desc_key = f"_{k}_desc"
if cand_key in current:
item["candidates"] = current[cand_key]
if desc_key in current:
item["desc"] = current[desc_key]
result[k] = item
return result
@router.put("/{section:path}")
def update_config(section: str, body: dict):
"""更新单个配置项
body: {"value": ..., "path": "..."}
path 是可选的完整路径默认为 section
"""
config = get_config()
# 将 / 分隔的路径转为 . 分隔的 epath
path = body.get("path", section.replace("/", "."))
new_value = body.get("value")
if new_value is None:
raise HTTPException(status_code=400, detail="缺少 value")
# 尝试类型转换:保持原类型
old_val = epath(config, path)
if old_val is not None and type(old_val) != type(new_value):
try:
new_value = type(old_val)(new_value)
except (ValueError, TypeError):
pass
epath(config, path, enable_modify=True, new_value=new_value)
# 自动持久化
config.persist()
return {"ok": True, "path": path, "value": new_value}
+196
View File
@@ -0,0 +1,196 @@
"""收藏夹、系统信息、缓存管理与分析统计 API"""
import platform
import shutil
import sys
from pathlib import Path
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from heurams.context import config_var
from heurams.services.attic import Attic
from heurams.services.favorite_service import favorite_manager
from heurams.services.logger import get_logger
from heurams.services.version import ver, stage, codename, codename_cn
logger = get_logger(__name__)
router = APIRouter(prefix="/api", tags=["misc"])
def _get_cache_dir() -> Path:
paths = config_var.get()["global"]["paths"]
cache = Path(paths.get("cache", str(Path(paths["data"]) / "cache"))) / "voice"
cache.mkdir(parents=True, exist_ok=True)
return cache
def _human_size(b: int) -> str:
for unit in ["B", "KB", "MB", "GB", "TB"]:
if b < 1024:
return f"{b:.1f} {unit}"
b /= 1024
return f"{b:.1f} PB"
@router.get("/cache")
def cache_info() -> dict:
"""缓存统计信息"""
cache_dir = _get_cache_dir()
total_size = 0
file_count = 0
if cache_dir.exists():
for f in cache_dir.rglob("*"):
if f.is_file():
total_size += f.stat().st_size
file_count += 1
return {
"path": str(cache_dir),
"file_count": file_count,
"total_size": total_size,
"human_size": _human_size(total_size),
"exists": cache_dir.exists(),
}
@router.delete("/cache")
def clear_cache() -> dict:
"""清空语音缓存"""
cache_dir = _get_cache_dir()
count = 0
if cache_dir.exists():
for f in cache_dir.rglob("*"):
if f.is_file():
f.unlink()
count += 1
# 移除空目录
for d in sorted(cache_dir.rglob("*"), key=lambda p: str(p), reverse=True):
if d.is_dir():
try:
d.rmdir()
except OSError:
pass
logger.info("清空缓存: 删除 %d 个文件", count)
return {"removed": count}
@router.get("/favorites")
def list_favorites() -> list[dict]:
"""获取所有收藏"""
items = []
for fav in favorite_manager.get_all():
items.append({
"repo_path": fav.repo_path,
"ident": fav.ident,
"added": fav.added,
"tags": fav.tags or [],
})
return items
@router.delete("/favorites/{repo_path}/{ident}")
def remove_favorite(repo_path: str, ident: str) -> dict:
"""移除收藏"""
if favorite_manager.remove(repo_path, ident):
return {"removed": True}
raise HTTPException(status_code=404, detail="收藏未找到")
@router.get("/analysis")
def analysis_stats() -> dict:
"""复习统计信息"""
a = Attic("ana", {"totaltime": 0, "openpuzzles": 0, "puzzles_err": 0})
total = a.data.get("openpuzzles", 0)
errs = a.data.get("puzzles_err", 0)
t = a.data.get("totaltime", 0)
rate = round(100 * (1 - errs / total), 2) if total else None
speed = round(total / t, 2) if t else None
return {
"total_puzzles": total,
"errors": errs,
"total_time": t,
"accuracy_pct": rate,
"speed_pps": speed,
}
@router.post("/cache/precache")
def trigger_precache() -> dict:
"""触发全量 TTS 缓存生成(后台异步)"""
import threading
from heurams.kernel.repolib import Repo
import heurams.kernel.particles as pt
from heurams.services.tts_service import convertor
from heurams.services.hasher import get_md5
paths_cfg = config_var.get()["global"]["paths"]
cache_dir = Path(paths_cfg.get("cache", str(Path(paths_cfg["data"]) / "cache"))) / "voice"
cache_dir.mkdir(parents=True, exist_ok=True)
repo_dir = Path(paths_cfg["data"]) / "repo"
def _run():
repos = Repo.probe_valid_repos_in_dir(repo_dir)
count = 0
for rp in repos:
try:
repo = Repo.from_repodir(rp)
for ident in repo.ident_index:
n = pt.Nucleon.from_data(repo.nucleonic_data_lict.get_itemic_unit(ident))
text = n.get("tts_text", "")
if not text:
continue
cache_file = cache_dir / f"{get_md5(text)}.wav"
if not cache_file.exists():
try:
convertor(text, cache_file)
count += 1
except Exception:
pass
except Exception:
continue
logger.info("预缓存完成: 生成 %d 个文件", count)
thread = threading.Thread(target=_run, daemon=True)
thread.start()
return {"started": True, "message": "缓存生成已启动"}
@router.get("/tts")
def generate_tts(text: str):
"""生成 TTS 音频并返回 wav 文件"""
from heurams.services.hasher import get_md5
from heurams.services.tts_service import convertor
paths_cfg = config_var.get()["global"]["paths"]
cache_dir = Path(paths_cfg.get("cache", str(Path(paths_cfg["data"]) / "cache"))) / "voice"
cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / f"{get_md5(text)}.wav"
if not cache_file.exists():
try:
convertor(text, cache_file)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return FileResponse(str(cache_file), media_type="audio/wav", filename="tts.wav")
@router.get("/about")
def about_info() -> dict:
"""系统信息"""
disk = shutil.disk_usage("/")
is_venv = hasattr(sys, "real_prefix") or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)
return {
"version": ver,
"stage": stage,
"codename": codename,
"codename_cn": codename_cn,
"python_version": platform.python_version(),
"python_path": sys.executable,
"os": f"{platform.system()} {platform.release()}",
"platform": platform.platform(),
"disk_free_gb": round(disk.free / (1024**3), 1),
"disk_total_gb": round(disk.total / (1024**3), 1),
"disk_free_pct": round(disk.free / disk.total * 100, 1),
"in_virtualenv": is_venv,
}
+36
View File
@@ -0,0 +1,36 @@
"""仓库信息 REST 路由"""
from fastapi import APIRouter, HTTPException
from heurams.services.logger import get_logger
from ..dependencies import compute_repo_progress, get_repo, list_repos
from ..schemas import RepoInfo
logger = get_logger(__name__)
router = APIRouter(prefix="/api/repos", tags=["repos"])
@router.get("")
def list_all_repos() -> list[RepoInfo]:
"""获取所有可用仓库"""
return list_repos()
@router.get("/{package}")
def get_repo_info(package: str) -> RepoInfo:
"""获取单个仓库详细信息"""
repo = get_repo(package)
if repo is None:
raise HTTPException(status_code=404, detail=f"仓库 '{package}' 未找到")
progress = compute_repo_progress(repo)
return RepoInfo(
package=repo.manifest.get("package", package),
title=repo.manifest.get("title", package),
author=repo.manifest.get("author", ""),
desc=repo.manifest.get("desc", ""),
source=str(repo.source),
total=progress["total"],
touched=progress["touched"],
)
+123
View File
@@ -0,0 +1,123 @@
"""复习 WebSocket 路由"""
import json
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from heurams.services.logger import get_logger
from ..dependencies import get_repo
from ..session import Session, SessionError
logger = get_logger(__name__)
router = APIRouter()
# 活跃会话存储 {repo_name: Session}
_active_sessions: dict[str, Session] = {}
@router.websocket("/api/review/{package}")
async def review_websocket(ws: WebSocket, package: str):
"""复习会话 WebSocket 端点
消息协议 (JSON):
客户端 -> 服务端:
{"action": "start", "scheduled_num": 10}
{"action": "rate", "rating": 4}
服务端 -> 客户端:
{"type": "puzzle", "data": {...}}
{"type": "progress", "data": {"phase": "...", "current": 1, "total": 10}}
{"type": "finished", "data": {"repo_name": "...", "total_atoms": 10}}
{"type": "error", "message": "..."}
"""
await ws.accept()
logger.debug("WebSocket 连接: package=%s", package)
session: Session | None = None
try:
while True:
raw = await ws.receive_text()
msg = json.loads(raw)
action = msg.get("action")
if action == "start":
if session is not None:
await ws.send_json({"type": "error", "message": "会话已开始"})
continue
repo = get_repo(package)
if repo is None:
await ws.send_json({
"type": "error",
"message": f"仓库 '{package}' 未找到",
})
continue
try:
session = Session(repo)
scheduled_num = msg.get("scheduled_num", -1)
session.start(scheduled_num)
_active_sessions[session.id] = session
except SessionError as e:
await ws.send_json({"type": "error", "message": str(e)})
continue
# 发送初始进度和第一个谜题
await ws.send_json({
"type": "progress",
"data": session.progress,
})
puzzle = session.get_current_puzzle()
if puzzle:
await ws.send_json({"type": "puzzle", "data": puzzle})
else:
await ws.send_json({
"type": "finished",
"data": {"repo_name": package, "total_atoms": 0},
})
elif action == "rate":
if session is None or session.finished:
await ws.send_json({
"type": "error",
"message": "没有活跃的复习会话",
})
continue
rating = msg.get("rating", 3)
continuing = session.rate(rating)
if not continuing:
# 复习完成
await ws.send_json({
"type": "finished",
"data": {
"repo_name": package,
"total_atoms": session.repo.data_length,
},
})
else:
await ws.send_json({
"type": "progress",
"data": session.progress,
})
puzzle = session.get_current_puzzle()
if puzzle:
await ws.send_json({"type": "puzzle", "data": puzzle})
else:
await ws.send_json({
"type": "error",
"message": f"未知动作: {action}",
})
except WebSocketDisconnect:
logger.debug("WebSocket 断开: package=%s", package)
except Exception as e:
logger.error("WebSocket 错误: %s", e)
finally:
if session:
session.cleanup()
_active_sessions.pop(session.id, None)
+67
View File
@@ -0,0 +1,67 @@
"""unifront Pydantic 模型"""
from pydantic import BaseModel
class RepoInfo(BaseModel):
"""仓库信息"""
package: str
title: str
author: str
desc: str
source: str
total: int
touched: int
class AtomInfo(BaseModel):
"""原子信息"""
ident: str
activated: bool
due: bool
rept: int
interval: int
next_date: int
last_date: int
class ConfigTree(BaseModel):
"""配置树节点"""
key: str
type: str
value: object = None
children: list["ConfigTree"] | None = None
class PuzzlePayload(BaseModel):
"""谜题数据载荷"""
category: str
alia: str
atom_ident: str
puzzle: dict
class ReviewProgress(BaseModel):
"""复习进度"""
phase: str
current: int
total: int
class WSMessage(BaseModel):
"""WebSocket 消息基类"""
type: str
data: dict | None = None
message: str | None = None
class RateAction(BaseModel):
"""评分动作"""
rating: int # 0-5
+71
View File
@@ -0,0 +1,71 @@
"""unifront FastAPI 应用工厂"""
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from heurams.services.logger import get_logger
from heurams.services.version import ver
from heurams.context import rootdir
from .routes import repos_router, config_router, atoms_router, review_router, misc_router
logger = get_logger(__name__)
def create_app() -> FastAPI:
"""创建并配置 FastAPI 应用"""
app = FastAPI(
title="HeurAMS API",
version=ver,
description="HeurAMS 启发式辅助记忆调度器 - unifront API",
)
# CORS — 允许本地开发前端
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 静态文件 (Web 前端)
static_dir = rootdir / "unifront" / "static"
if static_dir.exists():
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
# 注册路由
app.include_router(repos_router)
app.include_router(config_router)
app.include_router(atoms_router)
app.include_router(review_router)
app.include_router(misc_router)
@app.get("/api")
def api_root():
return {
"service": "HeurAMS",
"version": ver,
"endpoints": {
"repos": "/api/repos",
"config": "/api/config",
"atoms": "/api/repos/{package}/atoms",
"review_ws": "/api/review/{package}",
},
}
@app.get("/api/health")
def health():
return {"status": "ok", "version": ver}
@app.get("/")
def index():
from fastapi.responses import RedirectResponse
return RedirectResponse(url="/static/index.html")
logger.info("unifront API 应用已创建")
return app
+240 -2
View File
@@ -1,5 +1,243 @@
"""会话模块"""
"""复习会话管理"""
import uuid
import inspect
import heurams.kernel.particles as pt
import heurams.kernel.reactor as rt
import heurams.kernel.puzzles as puz
from heurams.context import config_var
from heurams.kernel.repolib.repo import Repo
from heurams.services.logger import get_logger
logger = get_logger(__name__)
# 已知谜题类型及其构造参数白名单
_PUZZLE_PARAM_MAP = {
"MCQPuzzle": {"mapping", "jammer", "max_riddles_num", "prefix"},
"ClozePuzzle": {"text", "min_denominator", "delimiter"},
"RecognitionPuzzle": set(),
}
class SessionError(Exception):
"""会话错误"""
class Session:
pass
"""管理单次复习会话的生命周期"""
def __init__(self, repo: Repo):
self.id = str(uuid.uuid4())[:8]
self.repo = repo
self.router: rt.Router | None = None
self.procession: rt.Procession | None = None
self.expander: rt.Expander | None = None
self.finished = False
def start(self, scheduled_num: int = -1):
"""开始复习,创建 Router and 处理第一个阶段"""
if scheduled_num == -1:
scheduled_num = config_var.get()["interface"]["global"]["scheduled_num"]
atoms = self._build_atoms()
atoms_to_provide = self._filter_atoms(atoms, scheduled_num)
if not atoms_to_provide:
raise SessionError("没有待复习的原子")
self.router = rt.Router(atoms_to_provide)
self._advance()
logger.debug("会话 %s 启动,原子数 %d", self.id, len(atoms_to_provide))
def _build_atoms(self) -> list[pt.Atom]:
"""从 repo 构建所有原子"""
atoms = []
for i in self.repo.ident_index:
n = pt.Nucleon.from_data(
nucleonic_data=self.repo.nucleonic_data_lict.get_itemic_unit(i)
)
e = pt.Electron.from_data(
electronic_data=self.repo.electronic_data_lict.get_itemic_unit(i),
algo_name=self.repo.config["algorithm"],
)
a = pt.Atom(n, e, self.repo.orbitic_data)
atoms.append(a)
return atoms
def _filter_atoms(self, atoms: list[pt.Atom], scheduled_num: int) -> list[pt.Atom]:
"""筛选出待复习和新记忆的原子"""
result = []
left_new = scheduled_num
for atom in atoms:
if atom.registry["electron"].is_activated():
if atom.registry["electron"].is_due():
result.append(atom)
else:
left_new -= 1
if left_new >= 0:
result.append(atom)
return result
@property
def progress(self) -> dict:
"""当前复习进度"""
if not self.procession:
return {"phase": "unknown", "current": 0, "total": 0}
return {
"phase": self.procession.route.value,
"current": self.procession.process() + 1,
"total": self.procession.total_length(),
}
def _advance(self) -> bool:
"""推进到下一个阶段/原子。返回 False 表示全部完成"""
if not self.router:
return False
self.procession = self.router.current_procession()
if self.procession.route == rt.RouterState.FINISHED:
# 全部完成
self.finished = True
self._persist()
return False
self.expander = self.procession.get_expander()
return True
def get_current_puzzle(self) -> dict | None:
"""获取当前谜题的可序列化数据"""
if self.finished or not self.expander or not self.procession:
return None
puzzle_inf = self.expander.get_current_puzzle_inf()
puzzle_class = puzzle_inf["puzzle"]
alia = puzzle_inf["alia"]
atom = self.procession.current_atom
if self.expander.state == "retronly":
return {
"category": "recognition",
"alia": alia,
"atom_ident": atom.ident,
"phase": self.procession.route.value,
"puzzle": {
"content": atom.registry["nucleon"].get("content", ""),
"tts_text": atom.registry["nucleon"].get("tts_text", ""),
},
}
# 通过 atom 配置构造并刷新谜题
puzzle_cfg = atom.registry["nucleon"]["puzzles"].get(alia, {})
try:
# 过滤出 puzzle 构造器接受的参数
sig = inspect.signature(puzzle_class.__init__)
valid_params = set(sig.parameters.keys()) - {"self"}
filtered_cfg = {}
for k, v in puzzle_cfg.items():
if k not in valid_params:
continue
# TOML/Evalizer 可能返回字符串,尝试类型转换
if isinstance(v, str):
vs = v.strip()
try:
v = int(vs) if vs.isdigit() or (vs.startswith('-') and vs[1:].isdigit()) else float(vs)
except (ValueError, TypeError):
v = vs
filtered_cfg[k] = v
puz_instance = puzzle_class(**filtered_cfg) if filtered_cfg else puzzle_class()
puz_instance.refresh()
except Exception as e:
logger.warning("谜题生成失败 %s: %s", alia, e)
return {
"category": "unknown",
"alia": alia,
"atom_ident": atom.ident,
"phase": self.procession.route.value,
"puzzle": {"error": str(e)},
}
alias = puzzle_class.__name__.lower().replace("puzzle", "")
data = {
"category": alias,
"alia": alia,
"atom_ident": atom.ident,
"phase": self.procession.route.value,
"puzzle": self._serialize_puzzle(puz_instance),
}
return data
def _serialize_puzzle(self, puz) -> dict:
"""将谜题对象序列化为字典"""
data = {}
if hasattr(puz, "wording"):
data["wording"] = puz.wording
if hasattr(puz, "answer"):
data["answer"] = puz.answer
if hasattr(puz, "options"):
data["options"] = puz.options
if hasattr(puz, "prefix"):
data["primary"] = getattr(puz, "prefix", "")
if hasattr(puz, "primary"):
# 对于 MCQ,从 atom puzzle config 获取 primary
pass
# 补充 primary/提示字段
atom = self.procession.current_atom if self.procession else None
if atom:
alia = self.expander.get_current_puzzle_inf()["alia"] if self.expander else ""
cfg = atom.registry["nucleon"]["puzzles"].get(alia, {})
if "primary" in cfg:
data["primary"] = cfg["primary"]
return data
def rate(self, rating: int) -> bool:
"""评分当前谜题并推进,返回 False 表示所有流程完成"""
if self.finished or not self.expander or not self.procession:
return False
self.expander.report(rating)
# 决定是否向前推进(SM-2 尺度:>=4 表示正确)
if rating >= 4:
self.expander.forward()
# 如果是 retronly 阶段,处理原子完成
if self.expander.state == "retronly":
quality = self.expander.get_quality()
atom = self.procession.current_atom
# 报告评分给原子
if not atom.registry["electron"].is_activated():
atom.registry["electron"].activate()
atom.lock(1)
atom.minimize(5)
else:
atom.minimize(quality)
# 若质量差则放回队列
if quality <= 3 and atom:
self.procession.append()
# 前进到下一个原子
self.procession.forward(1)
self._advance()
# 检查当前阶段的 Procession 是否已完成
if self.procession and self.procession.state == rt.ProcessionState.FINISHED.value:
self._advance()
return not self.finished
def _persist(self):
"""保存 algodata 到文件"""
try:
self.repo.persist_to_repodir()
logger.debug("会话 %s: algodata 已持久化", self.id)
except Exception as e:
logger.warning("持久化失败: %s", e)
def cleanup(self):
"""清理会话"""
self.router = None
self.procession = None
self.expander = None
logger.debug("会话 %s 已清理", self.id)
+301
View File
@@ -0,0 +1,301 @@
/* HeurAMS unifront */
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
const show = e => e.classList.remove('hidden');
const hide = e => e.classList.add('hidden');
const esc = s => { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
const API = '/api';
const get = async p => { var r = await fetch(API + p); if (!r.ok) throw Error(r.status); return r.json(); };
const post = async (p, d) => { var r = await fetch(API + p, { method:'POST', headers:{'Content-Type':'application/json'}, body:d?JSON.stringify(d):null }); if (!r.ok) throw Error(r.status); return r.json(); };
const del = async p => { var r = await fetch(API + p, { method:'DELETE' }); if (!r.ok) throw Error(r.status); return r.json(); };
const put = async (p, d) => { var r = await fetch(API + p, { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(d) }); if (!r.ok) throw Error(r.status); return r.json(); };
var App = { pkg:'', prep:null, ws:null, fav:false, prevAtom:'' };
/* === 主题 === */
function theme() { return localStorage.getItem('t') || 'light'; }
function setTheme(t) { document.body.setAttribute('data-theme', t); localStorage.setItem('t', t); $('#theme-btn').textContent = t === 'dark' ? '\u2600' : '\u263E'; }
function toggleTheme() { setTheme(theme() === 'dark' ? 'light' : 'dark'); }
/* === 布局 === */
function toggleSidebar() { $('#sidebar').classList.toggle('open'); }
function switchView(name) {
var titles = { lobby:'仪表盘', prep:'仓库详情', review:'复习中', favs:'收藏夹', cache:'缓存管理', settings:'设置', about:'关于' };
$('#nav-title').textContent = titles[name] || name;
$$('.view').forEach(function(el) { if (el.id === 'view-' + name) show(el); else hide(el); });
$$('.sidenav li').forEach(function(li) { li.classList.toggle('active', li.id === 'nav-' + name); });
$('#sidebar').classList.remove('open');
if (name === 'favs') loadFavs();
if (name === 'cache') loadCache();
if (name === 'settings') loadSettings();
if (name === 'about') loadAbout();
}
function goBack() { if (App.ws) { App.ws.close(); App.ws = null; } switchView('lobby'); loadRepos(); }
/* === 启动 === */
document.addEventListener('DOMContentLoaded', async function() {
setTheme(theme());
try { var i = await get(''); $('#nav-version').textContent = 'v' + i.version; } catch(e) {}
switchView('lobby'); loadRepos();
});
/* === 仪表盘 + 分析统计 === */
async function loadRepos() {
show($('#loading-lobby')); hide($('#empty-lobby'));
$('#repo-list').innerHTML = ''; $('#lobby-analysis').textContent = '';
try {
var repos = await get('/repos');
var tot = 0, toc = 0;
repos.forEach(function(r) { tot += r.total; toc += r.touched; });
$('#lobby-stats').innerHTML =
'<div class="stat-card"><div class="n">' + repos.length + '</div><div class="l">单元集</div></div>' +
'<div class="stat-card"><div class="n">' + tot + '</div><div class="l">总单元</div></div>' +
'<div class="stat-card"><div class="n">' + toc + '</div><div class="l">已学习</div></div>';
// 分析统计
try {
var ana = await get('/analysis');
var parts = [];
if (ana.total_puzzles > 0) {
parts.push('处理 ' + ana.total_puzzles + ' 个谜题');
if (ana.accuracy_pct !== null) parts.push('正确率 ' + ana.accuracy_pct + '%');
if (ana.speed_pps !== null) parts.push('速度 ' + ana.speed_pps + ' 个/秒');
} else { parts.push('暂无复习数据'); }
$('#lobby-analysis').textContent = parts.join(' | ');
} catch(e) {}
if (!repos.length) { hide($('#loading-lobby')); show($('#empty-lobby')); return; }
repos.forEach(function(r) {
var p = r.total > 0 ? Math.round(r.touched / r.total * 100) : 0;
var c = document.createElement('div');
c.className = 'repo-card';
c.innerHTML =
'<div style="cursor:pointer" onclick="openPrep(\'' + r.package + '\')">' +
'<div class="title">' + esc(r.title) + '</div>' +
'<div class="meta">' + esc(r.author) + ' &middot; ' + esc(r.package) + '</div>' +
(r.desc ? '<div class="meta">' + esc(r.desc) + '</div>' : '') +
'<div class="bar-wrap" style="margin:4px 0"><div class="bar-fill" style="width:' + p + '%"></div></div>' +
'<div style="font-size:11px;color:var(--dim)">' + r.touched + '/' + r.total + ' (' + p + '%)</div></div>' +
'<button class="btn sm pri" style="margin-top:6px" onclick="event.stopPropagation();quickStart(\'' + r.package + '\')">直接复习</button>';
$('#repo-list').appendChild(c);
});
hide($('#loading-lobby'));
} catch(e) { hide($('#loading-lobby')); $('#repo-list').innerHTML = '<div class="err-msg">加载失败</div>'; }
}
function quickStart(pkg) { openReview(pkg, 10); }
/* === 仓库详情 === */
async function openPrep(pkg) {
App.pkg = pkg; switchView('prep');
show($('#loading-prep')); hide($('#prep-content')); $('#prep-list').innerHTML = '';
try {
var d = await get('/repos/' + pkg + '/prepare');
App.prep = d;
var r = d.repo, p = d.progress, v = d.preview;
var pct = p.total > 0 ? Math.round(p.touched / p.total * 100) : 0;
$('#prep-title').textContent = r.title;
$('#prep-meta').innerHTML = esc(r.author) + ' &middot; ' + esc(r.package) + '<br>算法: ' + esc(r.algorithm) + ' &middot; 路径: ' + esc(r.source || '');
$('#prep-summary').innerHTML =
'<div class="stat-card"><div class="n">' + p.total + '</div><div class="l">总单元</div></div>' +
'<div class="stat-card"><div class="n">' + p.touched + '</div><div class="l">已学习</div></div>' +
'<div class="stat-card"><div class="n">' + v.review + '</div><div class="l">待复习</div></div>' +
'<div class="stat-card"><div class="n">' + v.new + '</div><div class="l">新识记</div></div>';
$('#prep-bar').style.width = pct + '%';
$('#prep-pct').textContent = '学习完成度: ' + p.touched + '/' + p.total + ' (' + pct + '%)';
$('#prep-num').value = r.scheduled_num || 10;
d.atoms.forEach(function(a) {
var el = document.createElement('div');
el.className = 'a-item';
el.innerHTML = '<span class="chip chip-' + a.status + '">' + a.status + '</span><span class="id">' + esc(a.content || a.ident) + '</span><span class="r">' + (a.rept || 0) + '</span>';
$('#prep-list').appendChild(el);
});
hide($('#loading-prep')); show($('#prep-content'));
} catch(e) { hide($('#loading-prep')); $('#prep-content').innerHTML = '<div class="err-msg">加载失败</div>'; }
}
function startReviewFromPrep() { openReview(App.pkg, parseInt($('#prep-num').value) || 10); }
/* === 复习 === */
function openReview(pkg, num) {
App.pkg = pkg; App.prevAtom = '';
switchView('review');
show($('#screen-start')); hide($('#screen-puzzle')); hide($('#screen-finished'));
$('#error-box').classList.remove('show');
$('#review-bar').style.width = '0%'; $('#review-pos').textContent = '';
$('#review-phase').textContent = ''; setStatus('断开');
App.fav = false; $('#fav-btn').textContent = '\u2606'; $('#fav-btn').classList.remove('faved');
$('#tts-btn').style.display = 'none';
$('#review-repo').textContent = (App.prep && App.prep.repo) ? App.prep.repo.title : pkg;
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
connectWS(proto + '//' + location.host + '/api/review/' + pkg, num);
}
function connectWS(url, num) {
if (App.ws) App.ws.close();
App.ws = new WebSocket(url);
App.ws.onopen = function() {
setStatus('已连接');
App.ws.send(JSON.stringify({ action:'start', scheduled_num:num }));
hide($('#screen-start')); show($('#screen-puzzle')); buildR();
};
App.ws.onclose = function() { setStatus('断开'); };
App.ws.onerror = function() { setStatus('错误'); };
App.ws.onmessage = function(ev) {
try { var m = JSON.parse(ev.data); switch(m.type) {
case 'progress': onProgress(m.data); break;
case 'puzzle': onPuzzle(m.data); break;
case 'finished': onFinished(m.data); break;
case 'error': showError(m.message); break;
}} catch(e) {}
};
}
function setStatus(l) { var el = $('#review-status'); el.textContent = l; el.className = 'tag'; if (l === '已连接') el.className = 'tag pri'; }
function showError(m) { var b = $('#error-box'); b.textContent = m; b.classList.add('show'); setTimeout(function(){ b.classList.remove('show'); }, 5000); }
function onProgress(d) {
var ls = { unsure:'准备', quick_review:'快速复习', recognition:'新记忆', final_review:'总复习', finished:'完成' };
if (d.phase) $('#review-phase').textContent = ls[d.phase] || d.phase;
if (d.total > 0) { $('#review-bar').style.width = Math.round(d.current / d.total * 100) + '%'; $('#review-pos').textContent = d.current + '/' + d.total; }
}
function buildR() {
var g = $('#rating-group'); g.innerHTML = '';
for (var i = 0; i <= 5; i++) { var b = document.createElement('button'); b.className = 'rt-btn'; b.textContent = i; b.onclick = function(){ rate(parseInt(this.textContent)); }; g.appendChild(b); }
}
function rate(r) { $$('.rt-btn').forEach(function(b){b.disabled=true;}); if (App.ws && App.ws.readyState === WebSocket.OPEN) App.ws.send(JSON.stringify({ action:'rate', rating:r })); }
function quickPass() { rate(5); }
function quickFail() { rate(2); }
/* TTS 朗读 */
function playTTS() {
var el = $('#puzzle-card .pz-ident');
var text = el ? el.textContent : '';
if (!text) return;
var audio = new Audio(API + '/tts?text=' + encodeURIComponent(text));
audio.play().catch(function() {});
}
/* 收藏 */
function toggleFav() {
var id = ''; var el = $('#puzzle-card .pz-ident'); if (el) id = el.textContent;
if (!App.pkg || !id) return;
post('/repos/' + App.pkg + '/atoms/' + encodeURIComponent(id) + '/favorite').then(function(r) {
App.fav = r.favorited; var b = $('#fav-btn'); b.textContent = r.favorited ? '\u2605' : '\u2606';
if (r.favorited) b.classList.add('faved'); else b.classList.remove('faved');
}).catch(function(){});
}
function checkFav(id) {
if (!App.pkg || !id) return;
get('/repos/' + App.pkg + '/atoms/' + encodeURIComponent(id) + '/favorite').then(function(r) {
App.fav = r.favorited; var b = $('#fav-btn'); b.textContent = r.favorited ? '\u2605' : '\u2606';
if (r.favorited) b.classList.add('faved'); else b.classList.remove('faved');
}).catch(function(){});
}
function onPuzzle(d) {
$$('.rt-btn').forEach(function(b){b.disabled=false;});
var card = $('#puzzle-card'); card.innerHTML = '';
if (d.phase) { var e = document.createElement('div'); e.className = 'pz-phase'; e.textContent = d.phase; card.appendChild(e); }
// 上一个原子(客户端追踪)
if (App.prevAtom) {
var e = document.createElement('div'); e.className = 'dim sm'; e.style.marginBottom = '6px';
e.textContent = '上一个: ' + esc(App.prevAtom); card.appendChild(e);
}
if (d.atom_ident) {
var e = document.createElement('div'); e.className = 'pz-ident'; e.textContent = d.atom_ident;
card.appendChild(e); checkFav(d.atom_ident);
$('#tts-btn').style.display = 'inline';
App.prevAtom = d.atom_ident;
}
var pz = d.puzzle || {};
if (pz.primary) { var e = document.createElement('div'); e.className = 'pz-primary'; e.textContent = pz.primary; card.appendChild(e); }
var w = wd(pz); if (w) { var e = document.createElement('div'); e.className = 'pz-wording'; e.textContent = w; card.appendChild(e); }
if (pz.options && Array.isArray(pz.options)) {
var od = document.createElement('div'); od.id = 'pz-options'; var ans = Array.isArray(pz.answer) ? pz.answer : [pz.answer];
pz.options.forEach(function(opts, qi) {
if (!Array.isArray(opts)) return; var c = ans[qi] || ans[0];
opts.forEach(function(opt) {
var b = document.createElement('button'); b.className = 'pz-opt'; b.textContent = opt;
b.onclick = function() {
$$('.pz-opt', od).forEach(function(x){x.classList.remove('correct','wrong');});
if (opt === c) { b.classList.add('correct'); } else { b.classList.add('wrong'); $$('.pz-opt', od).forEach(function(x){if(x.textContent===c)x.classList.add('correct');}); }
var a = card.querySelector('.pz-answer'); if (a) a.classList.add('show');
};
od.appendChild(b);
});
});
card.appendChild(od);
}
var at = aw(pz); if (at) { var e = document.createElement('div'); e.className = 'pz-answer'; e.innerHTML = '<strong>正确答案: </strong>' + esc(at); card.appendChild(e); if (!pz.options || !pz.options.length) e.classList.add('show'); }
}
function wd(pz) { if (!pz.wording) return ''; return Array.isArray(pz.wording) ? pz.wording.join('\n') : String(pz.wording); }
function aw(pz) { if (!pz.answer) return ''; return Array.isArray(pz.answer) ? pz.answer.join(' | ') : String(pz.answer); }
function onFinished(d) { hide($('#screen-puzzle')); show($('#screen-finished')); $('#finish-info').textContent = '共复习 ' + (d.total_atoms || 0) + ' 个原子'; $('#finish-saved').textContent = '算法数据已保存'; if (App.ws) { App.ws.close(); App.ws = null; } }
/* === 收藏夹 === */
async function loadFavs() {
show($('#loading-favs')); hide($('#empty-favs')); $('#favs-list').innerHTML = '';
try {
var items = await get('/favorites');
if (!items.length) { hide($('#loading-favs')); show($('#empty-favs')); return; }
items.forEach(function(f) {
var d = new Date(f.added * 1000);
var ts = d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0') + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
var c = document.createElement('div'); c.className = 'fav-item';
c.innerHTML = '<div class="fav-body"><div class="fav-ident">' + esc(f.ident) + '</div><div class="fav-meta">' + esc(f.repo_path) + ' &middot; ' + ts + '</div></div><button class="btn sm err" onclick="removeFav(\'' + esc(f.repo_path) + '\',\'' + esc(f.ident) + '\',this)">移除</button>';
$('#favs-list').appendChild(c);
});
hide($('#loading-favs'));
} catch(e) { hide($('#loading-favs')); $('#favs-list').innerHTML = '<div class="err-msg">加载失败</div>'; }
}
async function removeFav(rp, id, btn) { btn.disabled = true; try { await del('/favorites/' + encodeURIComponent(rp) + '/' + encodeURIComponent(id)); btn.parentElement.remove(); if (!$('#favs-list').children.length) show($('#empty-favs')); } catch(e) { showError('移除失败'); btn.disabled = false; } }
/* === 缓存管理 === */
async function loadCache() { show($('#loading-cache')); hide($('#cache-content')); $('#cache-msg').textContent = ''; try { var d = await get('/cache'); $('#cache-path').textContent = '路径: ' + esc(d.path); $('#cache-stats-cards').innerHTML = '<div class="stat-card"><div class="n">' + d.file_count + '</div><div class="l">缓存文件</div></div><div class="stat-card"><div class="n">' + d.human_size + '</div><div class="l">总大小</div></div>'; hide($('#loading-cache')); show($('#cache-content')); } catch(e) { hide($('#loading-cache')); $('#cache-content').innerHTML = '<div class="err-msg">加载失败</div>'; } }
async function clearCache() { if (!confirm('确定清空所有语音缓存?')) return; var btn = $('#cache-clear-btn'); btn.disabled = true; btn.textContent = '清空中...'; try { var r = await del('/cache'); $('#cache-msg').textContent = '已删除 ' + r.removed + ' 个缓存文件'; loadCache(); } catch(e) { $('#cache-msg').textContent = '清空失败'; } btn.disabled = false; btn.textContent = '清空缓存'; }
async function startPrecache() { var btn = $('#cache-precache-btn'); btn.disabled = true; btn.textContent = '生成中...'; try { var r = await post('/cache/precache'); $('#cache-msg').textContent = r.message; } catch(e) { $('#cache-msg').textContent = '启动失败'; } btn.disabled = false; btn.textContent = '生成缓存'; }
/* === 设置 === */
async function loadSettings() { show($('#loading-settings')); hide($('#settings-content')); var root = $('#settings-content'); root.innerHTML = ''; try { var tree = await get('/config'); renderSettingsTree(tree, root, 0); hide($('#loading-settings')); show($('#settings-content')); } catch(e) { hide($('#loading-settings')); root.innerHTML = '<div class="err-msg">加载失败</div>'; } }
function renderSettingsTree(nodes, parent, depth) {
nodes.forEach(function(node) {
if (node.type === 'branch') {
var sec = document.createElement('div'); sec.className = 'settings-section';
var display = depth < 2 ? 'block' : 'none';
sec.innerHTML = '<div class="settings-section-title" onclick="toggleSec(this)">' + esc(node.key) + ' <span class="arrow">' + (display === 'block' ? '\u25BC' : '\u25B6') + '</span></div><div class="settings-section-body" style="display:' + display + '"></div>';
parent.appendChild(sec); renderSettingsTree(node.children || [], sec.querySelector('.settings-section-body'), depth + 1);
} else {
var row = document.createElement('div'); row.className = 'settings-item'; var path = node.key; var val = node.value; var typ = node.type;
var label = '<div class="settings-label">' + esc(lastKey(node.key)) + '</div>';
if (node.desc) label += '<div class="settings-desc">' + esc(node.desc).replace(/\n/g,'<br>') + '</div>';
var inp = '';
if (node.candidates) {
inp = '<select class="settings-select" data-path="' + path + '">';
var cand = node.candidates;
if (Array.isArray(cand)) { cand.forEach(function(c){ inp += '<option value="' + esc(String(c)) + '"' + (String(c)===String(val)?' selected':'') + '>' + esc(c) + '</option>'; }); }
else if (typeof cand === 'object') { Object.keys(cand).forEach(function(k){ inp += '<option value="' + esc(k) + '"' + (String(k)===String(val)?' selected':'') + '>' + esc(cand[k]) + '</option>'; }); }
inp += '</select>';
} else if (typ === 'bool') { inp = '<label class="switch-label"><input type="checkbox" class="settings-checkbox" data-path="' + path + '"' + (val?' checked':'') + '><span class="switch-slider"></span></label>'; }
else if (typ === 'int'||typ==='number'||typ==='float') { inp = '<input type="number" class="settings-input" data-path="' + path + '" value="' + esc(String(val)) + '" step="' + (typ==='float'?'0.1':'1') + '">'; }
else { inp = '<input type="text" class="settings-input" data-path="' + path + '" value="' + esc(String(val)) + '">'; }
row.innerHTML = label + '<div class="settings-control">' + inp + '</div>'; parent.appendChild(row);
}
});
parent.querySelectorAll('.settings-input, .settings-select').forEach(function(el){ el.addEventListener('change', saveSetting); });
parent.querySelectorAll('.settings-checkbox').forEach(function(el){ el.addEventListener('change', saveSetting); });
}
function lastKey(p) { var ps = p.split('.'); return ps[ps.length - 1]; }
function toggleSec(h) { var b = h.parentElement.querySelector('.settings-section-body'); var a = h.querySelector('.arrow'); if (b.style.display === 'none') { b.style.display = 'block'; a.innerHTML = '\u25BC'; } else { b.style.display = 'none'; a.innerHTML = '\u25B6'; } }
async function saveSetting(ev) { var el = ev.target; var path = el.dataset.path; var val; if (el.type === 'checkbox') val = el.checked; else if (el.tagName === 'SELECT') val = el.value; else if (el.type === 'number') val = el.value.includes('.') ? parseFloat(el.value) : parseInt(el.value); else val = el.value; try { await put('/config/' + path.replace(/\./g,'/'), { value:val, path:path }); el.style.borderColor = 'var(--suc)'; setTimeout(function(){ el.style.borderColor = ''; }, 1500); } catch(e) { el.style.borderColor = 'var(--err)'; } }
/* === 关于 === */
async function loadAbout() { try { var d = await get('/about'); $('#about-version').textContent = '版本 ' + d.version + ' ' + d.stage; $('#about-codename').textContent = '代号: ' + d.codename + ' (' + d.codename_cn + ')'; $('#about-env').innerHTML = '<tr><td>Python</td><td>' + d.python_version + '</td></tr><tr><td>路径</td><td class="dim">' + esc(d.python_path) + '</td></tr><tr><td>OS</td><td>' + esc(d.os) + '</td></tr><tr><td>平台</td><td class="dim">' + esc(d.platform) + '</td></tr><tr><td>虚拟环境</td><td>' + (d.in_virtualenv?'是':'否') + '</td></tr><tr><td>磁盘</td><td>' + d.disk_free_gb + '/' + d.disk_total_gb + ' GB (' + d.disk_free_pct + '%)</td></tr>'; } catch(e) { $('#about-env').innerHTML = '<tr><td>加载失败</td></tr>'; } }
+168
View File
@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HeurAMS</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<!-- Top bar -->
<nav id="navbar">
<button id="nav-toggle" onclick="toggleSidebar()">&equiv;</button>
<span id="nav-title">Dashboard</span>
<span id="nav-version"></span>
<button id="theme-btn" onclick="toggleTheme()" class="btn-icon">&#9788;</button>
</nav>
<!-- Main -->
<div id="main-wrap">
<!-- Sidebar -->
<aside id="sidebar">
<ul class="sidenav">
<li class="active" id="nav-lobby"><a href="#" onclick="switchView('lobby')">Dashboard</a></li>
<li id="nav-favs"><a href="#" onclick="switchView('favs')">Favorites</a></li>
<li id="nav-cache"><a href="#" onclick="switchView('cache')">Cache</a></li>
<li id="nav-settings"><a href="#" onclick="switchView('settings')">Settings</a></li>
<li id="nav-about"><a href="#" onclick="switchView('about')">About</a></li>
</ul>
</aside>
<!-- Content area -->
<main id="main">
<!-- Error -->
<div id="error-box"></div>
<!-- ===== Dashboard ===== -->
<div id="view-lobby" class="view">
<div class="stat-row" id="lobby-stats"></div>
<div id="lobby-analysis" class="dim sm tc" style="margin-bottom:12px;"></div>
<p id="loading-lobby" class="dim tc">Loading repositories...</p>
<p id="empty-lobby" class="dim tc hidden">No repositories found</p>
<div id="repo-list"></div>
</div>
<!-- ===== Repo details ===== -->
<div id="view-prep" class="view hidden">
<p id="loading-prep" class="dim tc">Loading repo data...</p>
<div id="prep-content" class="hidden">
<h2 id="prep-title"></h2>
<p class="dim" id="prep-meta"></p>
<div class="stat-row" id="prep-summary"></div>
<div class="bar-wrap"><div class="bar-fill" id="prep-bar"></div></div>
<p class="dim sm" id="prep-pct"></p>
<div class="h-group">
<label>Study count per session:</label>
<input type="number" id="prep-num" value="10" min="1" max="200">
<button class="btn pri" onclick="startReviewFromPrep()">Start</button>
</div>
<div id="prep-list"></div>
</div>
</div>
<!-- ===== Review ===== -->
<div id="view-review" class="view hidden">
<div class="h-group sb">
<div class="h-group">
<strong id="review-repo"></strong>
<span id="review-phase" class="tag"></span>
<button id="fav-btn" class="btn-link" onclick="toggleFav()">&#9734;</button>
<button id="tts-btn" class="btn-link" onclick="playTTS()" title="Read Aloud">&#9654;</button>
</div>
<span id="review-status" class="tag">Disconnected</span>
</div>
<div class="bar-wrap sm"><div class="bar-fill" id="review-bar"></div></div>
<p class="dim sm tc" id="review-pos"></p>
<div id="screen-start" class="empty-state">
<h3>Ready</h3>
<p class="dim">Start review</p>
<button class="btn pri" onclick="startReview()">Start Review</button>
</div>
<div id="screen-puzzle" class="hidden">
<div id="puzzle-card"></div>
<div class="tc">
<p class="dim sm">Self-assessment</p>
<div id="rating-group"></div>
<div class="h-group jc">
<button class="btn sm suc" onclick="quickPass()">Correct (5)</button>
<button class="btn sm err" onclick="quickFail()">Incorrect (2)</button>
</div>
</div>
</div>
<div id="screen-finished" class="hidden empty-state">
<p class="ok-icon">&#10003;</p>
<h3>Session complete</h3>
<p class="dim" id="finish-info"></p>
<p class="dim sm" id="finish-saved"></p>
<button class="btn pri" onclick="goBack()" style="margin-top:16px;">Back</button>
</div>
</div>
<!-- ===== Favorites ===== -->
<div id="view-favs" class="view hidden">
<p id="loading-favs" class="dim tc">Loading favorites...</p>
<p id="empty-favs" class="dim tc hidden">No favorites</p>
<div id="favs-list"></div>
</div>
<!-- ===== Cache ===== -->
<div id="view-cache" class="view hidden">
<p id="loading-cache" class="dim tc">Loading cache info...</p>
<div id="cache-content" class="hidden">
<div class="cache-stats">
<div class="stat-row" id="cache-stats-cards"></div>
</div>
<div class="cache-detail">
<p class="dim sm" id="cache-path"></p>
</div>
<div class="h-group mt">
<button class="btn pri" id="cache-precache-btn" onclick="startPrecache()">Generate Cache</button>
<button class="btn" id="cache-refresh-btn" onclick="loadCache()">Refresh</button>
<button class="btn err" id="cache-clear-btn" onclick="clearCache()">Clear Cache</button>
</div>
<p id="cache-msg" class="dim sm mt"></p>
</div>
</div>
<!-- ===== Settings ===== -->
<div id="view-settings" class="view hidden">
<p id="loading-settings" class="dim tc">Loading settings...</p>
<div id="settings-content" class="hidden"></div>
</div>
<!-- ===== About ===== -->
<div id="view-about" class="view hidden">
<div class="about-section">
<h2>HeurAMS</h2>
<p class="dim" id="about-version"></p>
<p class="dim sm" id="about-codename"></p>
<p class="mt">A heuristic auxiliary memorizing scheduler based on cognitive science theories.</p>
<p class="dim sm mt">Licensed under GNU AGPL-3.0 with local API call exemption.</p>
</div>
<div class="about-section">
<h3>Developers</h3>
<p class="dim sm">Wang Zhiyu (<a href="https://github.com/pluvium27" target="_blank">@pluvium27</a>)</p>
<p class="dim sm mt">Project initiator and lead developer</p>
</div>
<div class="about-section">
<h3>Runtime Environment</h3>
<table class="info-table" id="about-env"></table>
</div>
</div>
</main>
</div>
<script src="/static/app.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+153
View File
@@ -0,0 +1,153 @@
/* HeurAMS unifront */
:root {
--bg: #fff; --bg2: #f5f5f7; --bg3: #e8e8ed; --bg-nav: #f0f0f2; --bg-side: #eaeaec;
--fg: #1d1d1f; --dim: #86868b; --bd: #d2d2d7; --accent: #1660A5; --suc: #2ecc71; --err: #e74c3c;
}
[data-theme="dark"] {
--bg: #1a1a2e; --bg2: #16213e; --bg3: #1f3056; --bg-nav: #12122a; --bg-side: #14142e;
--fg: #e0e0e0; --dim: #808080; --bd: #2a2a4a;
}
*,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }
body { font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans SC",sans-serif; background:var(--bg); color:var(--fg); min-height:100vh; }
a { color:var(--accent); }
.dim { color:var(--dim); }
.sm { font-size:12px; }
.tc { text-align:center; }
.mt { margin-top:12px; }
.hidden { display:none !important; }
/* 顶栏 */
#navbar { height:44px; display:flex; align-items:center; gap:10px; padding:0 16px; background:var(--bg-nav); border-bottom:1px solid var(--bd); position:fixed; top:0; left:0; right:0; z-index:100; }
#nav-title { font-weight:600; font-size:15px; flex:1; }
#nav-version { font-size:12px; color:var(--dim); }
#nav-toggle, #theme-btn { background:none; border:none; font-size:20px; cursor:pointer; padding:4px 8px; color:var(--fg); }
/* 主体 */
#main-wrap { display:flex; margin-top:44px; min-height:calc(100vh - 44px); }
#sidebar { width:180px; flex-shrink:0; background:var(--bg-side); border-right:1px solid var(--bd); padding:12px 0; }
#main { flex:1; padding:24px; max-width:960px; }
/* 侧边栏 */
.sidenav { list-style:none; }
.sidenav li a { display:block; padding:8px 20px; color:var(--dim); font-size:14px; text-decoration:none; }
.sidenav li a:hover { color:var(--fg); background:var(--bg3); }
.sidenav li.active a { color:var(--accent); font-weight:600; }
/* 按钮 */
.btn { display:inline-block; padding:7px 16px; border:1px solid var(--bd); border-radius:6px; font-size:13px; cursor:pointer; background:var(--bg2); color:var(--fg); }
.btn:hover { background:var(--bg3); }
.btn:disabled { opacity:0.4; cursor:default; }
.btn.pri { background:var(--accent); border-color:var(--accent); color:#fff; }
.btn.suc { background:#2e7d32; border-color:#2e7d32; color:#fff; }
.btn.err { background:#c62828; border-color:#c62828; color:#fff; }
.btn.sm { padding:4px 12px; font-size:12px; }
.btn-link { background:none; border:none; cursor:pointer; color:var(--fg); padding:0; font-size:18px; }
/* 标签 */
.tag { display:inline-block; padding:1px 8px; border-radius:4px; font-size:11px; font-weight:600; background:var(--bg2); color:var(--dim); }
.tag.pri { background:var(--accent); color:#fff; }
/* 进度条 */
.bar-wrap { height:4px; background:var(--bd); border-radius:2px; margin:8px 0; overflow:hidden; }
.bar-wrap.sm { height:3px; }
.bar-fill { height:100%; background:var(--accent); border-radius:2px; }
/* 布局 */
.h-group { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.h-group.sb { justify-content:space-between; }
.h-group.jc { justify-content:center; }
.stat-row { display:flex; gap:12px; margin-bottom:16px; flex-wrap:wrap; }
.stat-card { flex:1; min-width:80px; text-align:center; background:var(--bg2); border:1px solid var(--bd); border-radius:8px; padding:14px 10px; }
.stat-card .n { font-size:24px; font-weight:700; color:var(--accent); }
.stat-card .l { font-size:11px; color:var(--dim); }
/* 仓库卡片 */
.repo-card { background:var(--bg2); border:1px solid var(--bd); border-radius:8px; padding:14px; margin-bottom:8px; cursor:pointer; }
.repo-card:hover { background:var(--bg3); }
.repo-card .title { font-size:15px; margin-bottom:2px; }
.repo-card .meta { font-size:12px; color:var(--dim); margin-bottom:4px; }
/* 原子列表 */
#prep-list { max-height:400px; overflow-y:auto; border:1px solid var(--bd); border-radius:6px; margin-top:12px; }
.a-item { display:flex; align-items:center; gap:8px; padding:5px 10px; border-bottom:1px solid var(--bd); font-size:13px; }
.a-item:last-child { border-bottom:none; }
.a-item .id { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.a-item .r { font-size:11px; color:var(--dim); min-width:30px; text-align:right; }
.chip { display:inline-block; padding:1px 7px; border-radius:4px; font-size:11px; font-weight:600; }
.chip.R { background:#f39c12; color:#fff; }
.chip.A { background:var(--accent); color:#fff; }
.chip.U { background:#aeaeb2; color:#fff; }
/* 谜题 */
#puzzle-card { background:var(--bg2); border:1px solid var(--bd); border-radius:8px; padding:18px; margin-bottom:12px; }
.pz-phase { font-size:11px; color:var(--accent); text-transform:uppercase; letter-spacing:1px; margin-bottom:4px; font-weight:600; }
.pz-ident { font-size:12px; color:var(--dim); margin-bottom:10px; }
.pz-primary { font-size:13px; color:var(--dim); margin-bottom:8px; }
.pz-wording { font-size:15px; line-height:1.7; margin-bottom:10px; white-space:pre-wrap; }
.pz-answer { padding:8px 10px; background:rgba(46,204,113,0.08); border:1px solid rgba(46,204,113,0.3); border-radius:6px; font-size:13px; color:var(--suc); margin-bottom:10px; display:none; }
.pz-answer.show { display:block; }
#pz-options { display:flex; flex-direction:column; gap:5px; margin-bottom:10px; }
.pz-opt { padding:8px 12px; background:var(--bg); border:1px solid var(--bd); border-radius:6px; font-size:14px; text-align:left; cursor:pointer; }
.pz-opt:hover { background:var(--bg2); }
.pz-opt.correct { border-color:var(--suc); background:rgba(46,204,113,0.08); }
.pz-opt.wrong { border-color:var(--err); background:rgba(231,76,60,0.08); }
/* 评分 */
.rt-btn { width:40px; height:40px; border:2px solid var(--bd); border-radius:50%; background:var(--bg2); font-size:14px; font-weight:600; cursor:pointer; margin:0 2px; }
.rt-btn:hover { border-color:var(--accent); }
#fav-btn { color:var(--dim); }
#fav-btn.faved { color:#f39c12; }
input[type=number] { padding:6px 8px; border:1px solid var(--bd); border-radius:6px; background:var(--bg); color:var(--fg); font-size:14px; width:64px; text-align:center; }
/* 空状态 */
.empty-state { text-align:center; padding:48px 24px; }
.empty-state .ok-icon { font-size:32px; color:var(--suc); margin-bottom:8px; }
.empty-state h3 { margin-bottom:8px; }
/* 错误 */
#error-box { display:none; padding:8px 12px; background:rgba(231,76,60,0.08); border:1px solid var(--err); border-radius:6px; color:var(--err); font-size:13px; margin-bottom:10px; }
#error-box.show { display:block; }
.err-msg { padding:12px; color:var(--err); font-size:13px; }
#loading-lobby, #loading-prep, #loading-favs, #loading-cache, #loading-settings { padding:32px; }
/* 收藏夹 */
.fav-item { display:flex; align-items:center; gap:12px; background:var(--bg2); border:1px solid var(--bd); border-radius:8px; padding:12px 16px; margin-bottom:8px; }
.fav-item .fav-body { flex:1; }
.fav-item .fav-ident { font-size:14px; }
.fav-item .fav-meta { font-size:12px; color:var(--dim); margin-top:2px; }
/* 设置 */
.settings-section { background:var(--bg2); border:1px solid var(--bd); border-radius:8px; margin-bottom:8px; overflow:hidden; }
.settings-section-title { padding:10px 14px; font-weight:600; font-size:14px; cursor:pointer; display:flex; justify-content:space-between; align-items:center; }
.settings-section-title:hover { background:var(--bg3); }
.settings-section-title .arrow { font-size:10px; color:var(--dim); }
.settings-section-body { padding:0 14px 10px; }
.settings-item { display:flex; align-items:center; gap:12px; padding:6px 0; border-top:1px solid var(--bd); }
.settings-label { font-size:13px; font-weight:500; min-width:140px; }
.settings-desc { font-size:11px; color:var(--dim); }
.settings-control { margin-left:auto; flex-shrink:0; }
.settings-input { padding:4px 8px; border:1px solid var(--bd); border-radius:4px; background:var(--bg); color:var(--fg); font-size:13px; width:120px; }
.settings-select { padding:4px 8px; border:1px solid var(--bd); border-radius:4px; background:var(--bg); color:var(--fg); font-size:13px; }
.switch-label { position:relative; display:inline-block; width:36px; height:20px; cursor:pointer; }
.settings-checkbox { display:none; }
.switch-slider { position:absolute; top:0; left:0; right:0; bottom:0; background:var(--bd); border-radius:10px; transition:0.15s; }
.settings-checkbox:checked + .switch-slider { background:var(--accent); }
.switch-slider::before { content:''; position:absolute; width:16px; height:16px; border-radius:50%; background:#fff; top:2px; left:2px; transition:0.15s; }
.settings-checkbox:checked + .switch-slider::before { transform:translateX(16px); }
/* 关于 */
.about-section { background:var(--bg2); border:1px solid var(--bd); border-radius:8px; padding:16px; margin-bottom:12px; }
.about-section h2 { font-size:18px; margin-bottom:4px; }
.about-section h3 { font-size:15px; margin-bottom:6px; }
.info-table { width:100%; border-collapse:collapse; font-size:13px; }
.info-table td { padding:6px 0; border-bottom:1px solid var(--bd); }
.info-table td:first-child { width:130px; color:var(--dim); font-weight:600; }
/* 移动端 */
@media (max-width:600px) {
#sidebar { display:none; position:fixed; left:0; top:44px; bottom:0; z-index:99; width:200px; }
#sidebar.open { display:block; }
#main { padding:16px; }
}
Generated
+497 -1
View File
@@ -109,6 +109,37 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.13.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
]
[[package]]
name = "attrs"
version = "26.1.0"
@@ -184,6 +215,18 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "click"
version = "8.3.3"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -292,6 +335,22 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8c/2b/a8cb687b92a2690d2ad171f0c2fd1c8f18690363cca7618bab2bbe4cdf2b/edge_tts-7.2.8-py3-none-any.whl", hash = "sha256:361fe48ce7ef613adbe30f664e3765dd71029c6cb57427279eff8ad6df2eb211", size = 31026, upload-time = "2026-03-22T19:57:49.672Z" },
]
[[package]]
name = "fastapi"
version = "0.136.1"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
]
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -393,14 +452,26 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/67/3c/e7e140f8cdb95b042cb125ee142e7630187e8e78d21847ca81e9d1e99bb8/fsrs-6.3.1-py3-none-any.whl", hash = "sha256:ac1bf9939573592d8c9bc1e11a00bd17e04146dc9f2c913127e2bcc431b9040b", size = 22840, upload-time = "2026-03-10T14:01:01.084Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "heurams"
version = "0.5.0"
version = "0.5.1"
source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "fastapi" },
{ name = "tabulate" },
{ name = "toml" },
{ name = "transitions" },
{ name = "uvicorn", extra = ["standard"] },
]
[package.optional-dependencies]
@@ -440,9 +511,16 @@ tts-edgetts = [
{ name = "edge-tts" },
]
[package.dev-dependencies]
dev = [
{ name = "httpx" },
]
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.3.3" },
{ name = "edge-tts", marker = "extra == 'tts-edgetts'", specifier = ">=7.2.8" },
{ name = "fastapi", specifier = ">=0.136.1" },
{ name = "fsrs", marker = "extra == 'algo-fsrs'", specifier = ">=6.3.1" },
{ name = "heurams", extras = ["algo-fsrs"], marker = "extra == 'all'" },
{ name = "heurams", extras = ["algo-fsrs"], marker = "extra == 'basic'" },
@@ -461,10 +539,71 @@ requires-dist = [
{ name = "textual", marker = "extra == 'interface'", specifier = ">=8.2.5" },
{ name = "toml", specifier = ">=0.10.2" },
{ name = "transitions", specifier = ">=0.9.3" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.47.0" },
{ name = "zmq", marker = "extra == 'dev'", specifier = ">=0.0.0" },
]
provides-extras = ["interface", "algo-fsrs", "tts-edgetts", "llm", "audio-playsound", "dev", "all", "basic"]
[package.metadata.requires-dev]
dev = [{ name = "httpx", specifier = ">=0.28.1" }]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httptools"
version = "0.7.1"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.13"
@@ -776,6 +915,96 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydantic"
version = "2.13.4"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
name = "pydantic-core"
version = "2.46.4"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
@@ -815,6 +1044,15 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.2"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]]
name = "pywin32"
version = "311"
@@ -831,6 +1069,52 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "pyzmq"
version = "27.1.0"
@@ -896,6 +1180,19 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "starlette"
version = "1.0.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
]
[[package]]
name = "tabulate"
version = "0.10.0"
@@ -952,6 +1249,18 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "uc-micro-py"
version = "2.0.0"
@@ -961,6 +1270,193 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" },
]
[[package]]
name = "uvicorn"
version = "0.47.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" },
]
[package.optional-dependencies]
standard = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "httptools" },
{ name = "python-dotenv" },
{ name = "pyyaml" },
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
{ name = "watchfiles" },
{ name = "websockets" },
]
[[package]]
name = "uvloop"
version = "0.22.1"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
]
[[package]]
name = "watchfiles"
version = "1.2.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" },
]
[[package]]
name = "websockets"
version = "16.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
]
[[package]]
name = "yarl"
version = "1.23.0"