Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7e668863a | |||
| cfa85a3cac | |||
| 6502efd84f | |||
| b3bc5b1a09 | |||
| 6adf1c62f6 | |||
| abd94d9a21 |
@@ -1,50 +1,50 @@
|
||||
# AI Coding Assistant Guide
|
||||
# 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 工具以及在使用 AI 辅助向 HeurAMS 项目贡献代码的开发者提供指导, 一般而言此文件会被自动读入多种 AI 工具的上下文.
|
||||
|
||||
AI tools should read this `/AGENTS.md` file in its entirety.
|
||||
AI 工具应当完整阅读此 `/AGENTS.md` 文件.
|
||||
|
||||
## Reference Documents
|
||||
## 查阅开发文档
|
||||
|
||||
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:
|
||||
在帮助进行 HeurAMS 开发时,AI 工具应遵循标准的开发规范与流程, 应当自动查看或是在用户发出"初始化/init"指令后查看:
|
||||
|
||||
- [Contributing Guide](/CONTRIBUTING.md)
|
||||
- [README](/README.md)
|
||||
- [Architecture Overview](/docs/ARCHITECTURE.md)
|
||||
- [贡献指南](/CONTRIBUTING.md)
|
||||
- [自述文件](/README.md)
|
||||
- [项目架构](/ARCHITECTURE.md)
|
||||
|
||||
## Prohibited Actions
|
||||
## 明确禁止行为
|
||||
|
||||
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**
|
||||
1. 禁止 AI 自动生成 PR 或 patch 文件
|
||||
2. 禁止 AI 在未经人工确认的情况下修改现有代码
|
||||
3. 禁止 AI 不使用格式化工具而生成格式化文件的行为
|
||||
4. 禁止 AI 修复任何"bug", 而不经人工确认
|
||||
5. 禁止一切不遵循项目设计原则, 另造独立库的 "糊屎" 行为
|
||||
6. 禁止 AI 直接操作 pip, uv, apt 等工具修改外部依赖或工具, 而应让人类开发者自己操作依赖
|
||||
7. 禁止使用不同于任何现有文件的现有注释语言的其他语言写新注释
|
||||
8. 禁止不读文件就直接覆写
|
||||
9. 绝对禁止修改此 `/AGENTS.md` 文件
|
||||
|
||||
## License & Legal Requirements
|
||||
## 许可证与法律要求
|
||||
|
||||
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).
|
||||
所有贡献必须符合许可要求, 所有代码必须与 AGPL-3.0-or-later 许可以及项目附加豁免条款(位于 LICENSE 文件尾部 237 至 245 行)兼容.
|
||||
|
||||
## Signed-off-by & DCO
|
||||
## Signed-off-by 与 DCO
|
||||
|
||||
AI agents are **strictly forbidden** from adding Signed-off-by tags.
|
||||
AI 代理**严禁添加** Signed-off-by 标签.
|
||||
|
||||
Only humans can legally certify the DCO.
|
||||
只有人类能够合法地认证 DCO.
|
||||
|
||||
Human committers are responsible for:
|
||||
人类提交者负责:
|
||||
|
||||
- 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 生成的代码
|
||||
- 确保符合许可要求
|
||||
- 添加自己的 Signed-off-by 标签以认证 DCO
|
||||
- 对贡献负责任
|
||||
|
||||
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
|
||||
- 了解运行环境, 例如操作系统或具体发行版
|
||||
- 遵循此文档所述规则
|
||||
- 主动提醒使用 AI 工具的开发者
|
||||
|
||||
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>
|
||||
本文档参考自 <a href="https://docs.kernel.org/process/coding-assistants.html" target="_blank" rel="noopener noreferrer">AI Coding Assistants — The Linux Kernel documentation</a>
|
||||
|
||||
+61
-61
@@ -1,119 +1,119 @@
|
||||
# Contributing Guide & Development
|
||||
# 贡献指南与二次开发
|
||||
|
||||
Welcome and thank you for supporting this project!
|
||||
欢迎支持此项目!
|
||||
|
||||
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://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> 设置了镜像同步.
|
||||
|
||||
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.
|
||||
这丝毫不影响项目接受来自 <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]
|
||||
> 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.
|
||||
> 我们已经开始着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
|
||||
> 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS 和 Plasma Mobile 提供现代用户界面
|
||||
> 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发
|
||||
|
||||
## Development Conventions
|
||||
## 开发规范
|
||||
|
||||
Branch structure:
|
||||
分支划分:
|
||||
|
||||
- `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`.
|
||||
- `dev` 分支(仓库默认分支): 主线开发分支, 自身仅用于非重构的问题修复和整合功能分支, 拉取请求在该分支合并
|
||||
- `master` 分支: 主线稳定版本, 仅当稳定版本释出或修补版本时将 `dev` 合并到 `master` 上
|
||||
- 功能与重构分支: 从 `dev` 分支创建, 命名格式为 `feature/描述` 或 `fix/描述` 或 `refactor/描述` 或 `next/版本号`
|
||||
- 功能与重构分支应先合并至 `dev`, 再合并至 `master`
|
||||
|
||||
Code formatting:
|
||||
代码格式化:
|
||||
|
||||
- Install tools:
|
||||
- 安装工具:
|
||||
```bash
|
||||
python -m pip install black autoflake mdformat
|
||||
```
|
||||
- For Python, use `black` and `autoflake`.\
|
||||
Commands:
|
||||
- 对于 Python, 使用 `black` 与 `autoflake` 格式化\
|
||||
命令:
|
||||
```bash
|
||||
# black's multi-threading may have compatibility issues in some environments
|
||||
# black 的多线程在某些环境下有兼容性问题
|
||||
black . --workers=1
|
||||
```
|
||||
```bash
|
||||
# autoflake: note to exclude __init__.py
|
||||
# autoflake 注意排除 __init__.py
|
||||
autoflake --in-place --remove-all-unused-imports --recursive ./src/ --exclude __init__.py
|
||||
```
|
||||
- For Markdown, use `mdformat`.\
|
||||
Command:
|
||||
- 对于 Markdown, 使用 `mdformat` 格式化
|
||||
命令:
|
||||
```bash
|
||||
mdformat --number .
|
||||
```
|
||||
- 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.
|
||||
- 对于 Textual CSS, 可以使用 `prettier` 格式化
|
||||
- 格式化不是必需的, 可以整合入一次 `style` 提交, 但 `master` 和 `dev` 分支上的代码应尽量整洁, 以便合并时审查
|
||||
|
||||
Commit messages:
|
||||
提交消息:
|
||||
|
||||
- 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.
|
||||
- 使用简体中文或英文撰写清晰的提交消息
|
||||
- 提交消息格式: 遵循 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 规范, 建议使用 `koji` 工具
|
||||
|
||||
Merge method:
|
||||
合并方式:
|
||||
|
||||
- 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`.
|
||||
- 为了一致性和可追溯性, 项目自 v0.4.0 重构后重新初始化仓库起就禁止使用 Fast-forward 合并
|
||||
- 可以设置 `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.
|
||||
提交署名方式:
|
||||
由于 KDE Invent 设施的奇怪 git hook, commit 的 Author 字段需要看起来像个真名(例如 Wang Zhiyu 不能写为 wangzhiyu, 否则 KDE Invent 的 hook 会拒绝 push),
|
||||
所以请确保您的 git 配置使用了类似正式姓名的格式 (例如 git config user.name "Li Hua", 也即中间需要空格), 不一定要真实姓名, 邮箱无要求, 也可以将单名重复使用两次 (例如 Thura Thura) 以通过检测.
|
||||
|
||||
## Setting Up Development Environment
|
||||
## 设置开发环境
|
||||
|
||||
```bash
|
||||
git clone https://git.pluv27.top/pluv/HeurAMS # Default branch is dev, no need to switch
|
||||
git clone https://git.pluv27.top/pluv/HeurAMS # 默认分支为 dev, 所以不必切换分支
|
||||
|
||||
cd HeurAMS
|
||||
|
||||
# If using uv (recommended)
|
||||
# 如果决定使用 uv (推荐)
|
||||
|
||||
python3 -m pip install uv
|
||||
|
||||
uv sync --all-extras # Sync development environment
|
||||
uv sync --all-extras # 同步开发运行环境
|
||||
|
||||
uv run heurams
|
||||
|
||||
# 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 # Launch TUI
|
||||
python3 -m heurams.interface # 启动 TUI
|
||||
```
|
||||
|
||||
## License & External References
|
||||
## 许可证与外部引用
|
||||
|
||||
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).
|
||||
贡献者拥有其贡献部分的版权同意其贡献将在 AGPL-3.0 许可证(包括附加的本机 API 调用豁免条款)下发布.
|
||||
|
||||
Please note in your PR description if:
|
||||
如有以下情况, 请在 PR 描述中注明:
|
||||
|
||||
- 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
|
||||
- 如果需要引入其他开源 vendor
|
||||
- 如果需要引入其他专有的网络服务(例如当前项目中的 edgetts)
|
||||
- 如果需要升级某个依赖或运行环境的版本
|
||||
|
||||
## New User Interface Frontends
|
||||
## 新的用户界面前端
|
||||
|
||||
HeurAMS is designed as a frontend-independent library, meaning:
|
||||
HeurAMS 被设计为一个可独立于前端的程序库, 这意味着:
|
||||
|
||||
- Our built-in Textual TUI frontend is not the only available frontend.
|
||||
- 我们的内置 Textual TUI 前端不是唯一可用的前端
|
||||
|
||||
- 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 前端 (例如未实现的 Flutter 前端), 并且以 AGPL-3.0/GPL-3.0 开放源代码, 可以联系我们将它转移到 HeurAMS 的官方仓库中以便共同维护, 您将保留您的版权并可主导该仓库下的开发工作 :)
|
||||
|
||||
- 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.
|
||||
- 您还可以在自己的项目中以独立进程/服务调用 HeurAMS, 根据 AGPL-3.0 及本项目的附加许可条款, 如果调用发生在同一主机上且不涉及外部网络转发, 则可豁免许可证规定的特定义务而免于受 AGPL-3.0 "污染". 为了这点, 我们正在完善可选择启用的跨进程 RPC 模块, 这将成为潜进内核的跨平台标准件.
|
||||
|
||||
- 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.
|
||||
- 如果您通过独立进程/服务调用方式开发了另外的软件, 开源但不愿使用 AGPL-3.0/GPL-3.0 许可证, 也可以联系我们, 我们乐于将您的项目链接添加到友链中
|
||||
|
||||
## 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!
|
||||
您的角色您来定!
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
# 贡献指南与二次开发
|
||||
|
||||
欢迎支持此项目!
|
||||
|
||||
目前, 项目仓库主服务器为<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 许可证, 也可以联系我们, 我们乐于将您的项目链接添加到友链中
|
||||
|
||||
## 软件开发之外的贡献
|
||||
|
||||
即使您不是软件开发人员, 我们也欢迎您加入贡献!
|
||||
|
||||
您可以:
|
||||
|
||||
- 协助创建或核对各种语言的翻译来翻译软件的界面和文档
|
||||
- 制作开放的记忆单元集(包括但不限于文字、图像、音效)给其他用户使用
|
||||
- 改进软件配套的文档
|
||||
- 给其他用户答疑解惑或分享自己的经验
|
||||
- 在讨论区提出新想法或反馈问题
|
||||
|
||||
您的角色您来定!
|
||||
@@ -333,7 +333,7 @@ Flutter 是构建跨平台图形界面的优秀框架, HeurAMS 的设计目标
|
||||
|
||||
当前我们优先开发了基于 Textual 的 TUI 前端和基于 Kirigami 的原生前端, 但这不排除未来出现 Flutter 或其他框架前端的可能性.
|
||||
|
||||
如果您有兴趣开发 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).
|
||||
如果您有兴趣开发 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).
|
||||
|
||||
## 软件需要联网吗?
|
||||
|
||||
@@ -411,6 +411,6 @@ HeurAMS 的间隔重复算法基于相同的认知科学原理, 且算法透明
|
||||
|
||||
## 如何参与项目?
|
||||
|
||||
详见[贡献指南](../CONTRIBUTING_zh.md).
|
||||
详见[贡献指南](CONTRIBUTING.md).
|
||||
|
||||
即使不是开发者, 您也可以通过编写文档、制作记忆单元集、翻译界面、答疑等方式参与.
|
||||
@@ -44,7 +44,7 @@
|
||||
- 资源占用小, 运行流畅, 不拖泥带水
|
||||
- 便于测试与调试程序库
|
||||
|
||||
查看[屏幕截图](SCREENSHOTS_zh.md).
|
||||
查看[屏幕截图](SCREENSHOTS.md).
|
||||
|
||||
|
||||
## 包依赖组说明
|
||||
@@ -66,11 +66,11 @@
|
||||
|
||||
## 关于此仓库
|
||||
|
||||
此仓库为 HeurAMS "潜进" 的核心程序库在 python 语言下的实现\
|
||||
此仓库为 "潜进" 的核心程序库在 python 语言下的实现\
|
||||
包含数据模型与框架, 并内置了基于 textual 框架的前端实现 (interface 子模块)\
|
||||
除了通过内置前端进行学习外, 开发者也能在 python 环境中导入 `heurams` 库或使用 `RPC` 与 `heurams` 程序库实例通讯, 使用框架构建其他辅助记忆功能前端或其他应用程序
|
||||
|
||||
项目组的所有仓库如下:
|
||||
潜进项目的所有仓库如下:
|
||||
|
||||
| 项目名称 | 状态 | 说明 | 包名 | 技术栈 | 目标平台 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
@@ -1,143 +1,116 @@
|
||||
# HeurAMS - Heuristic Auxiliary Memorizing Scheduler
|
||||
# 潜进 (HeurAMS) - 启发式辅助记忆调度器
|
||||
|
||||
[中文](README_zh.md) | English
|
||||
## 概述
|
||||
|
||||
## Overview
|
||||
"潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是一种基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划,
|
||||
也是一种开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的调查实验与研究.
|
||||
|
||||
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.
|
||||
|
||||
[Detailed Introduction](docs/INTRODUCTION.md) | [Screenshots](docs/SCREENSHOTS.md)
|
||||
[详细介绍](INTRODUCTION.md) [屏幕截图](SCREENSHOTS.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>
|
||||
<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
|
||||
潜进 (包名是 `heurams`) 处于早期开发考虑, 尚未上架 PyPI, 但您可以用 pip 支持的 git 协议安装稳定版和开发版本, 这要求您的电脑上安装了 python 环境 (建议 3.12.13 及之后版本).
|
||||
|
||||
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).
|
||||
#### 面向用户的安装
|
||||
|
||||
Install from the stable `master` branch with optional dependencies for user experience (recommended):
|
||||
从稳定的 `master` 分支安装, 并安装适用于用户体验的可选依赖(推荐):
|
||||
|
||||
```
|
||||
pip install --upgrade 'heurams[basic] @ https://git.pluv27.top/pluv/HeurAMS/archive/master.zip'
|
||||
```
|
||||
|
||||
Install from the more recent, roughly stable `dev` branch with optional dependencies (if you want cutting-edge improvements):
|
||||
从较前沿, 大致稳定的 `dev` 分支安装, 并安装适用于用户体验的可选依赖(如果您追求较前沿的改进):
|
||||
|
||||
```
|
||||
pip install --force-reinstall --no-deps 'heurams[basic] @ https://git.pluv27.top/pluv/HeurAMS/archive/dev.zip'
|
||||
```
|
||||
|
||||
Install the general audio module for desktop computers (based on playsound3):\
|
||||
(Not applicable for termux environments; termux has built-in audio support)
|
||||
安装适用于一般计算机的通用音频模块(基于 playsound3):\
|
||||
(此项不适用于 termux 环境, termux 的音频支持是内建的)
|
||||
|
||||
```
|
||||
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)
|
||||
> [!CAUTION]
|
||||
> 对于部分 Linux 发行版和 Android Termux 用户:\
|
||||
> 您需要先行安装 `cmake` 和 `libzmq` 才能正确安装项目的 `zmq` 依赖.\
|
||||
> 例如在 termux 上先运行 `pkg install cmake clang libzmq`.\
|
||||
> 项目功能本身不依赖它, 但需要该依赖用于启动可选的调试服务器.
|
||||
|
||||
#### 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.
|
||||
|
||||
### Usage
|
||||
|
||||
Run `heurams` in your terminal, and you will see help information:
|
||||
|
||||
```plain
|
||||
~ $ heurams
|
||||
Usage: heurams [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
HeurAMS 0.5.1 - Heuristic Auxiliary Memorizing Scheduler
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
(Your terminal will now display the TUI)
|
||||
```
|
||||
|
||||
Check the version with `heurams -v`:
|
||||
从 `dev` 分支进行基于 git 的可编辑安装, 并安装全部可选依赖(推荐):
|
||||
|
||||
```
|
||||
~ $ heurams -v
|
||||
HeurAMS 0.5.1 stable (fulcrum/支点), Linux
|
||||
pip install --force-reinstall --no-deps 'heurams[all] @ https://git.pluv27.top/pluv/HeurAMS/archive/dev.zip'
|
||||
```
|
||||
|
||||
## Frequently Asked Questions (FAQ)
|
||||
> 您也可以从 `refactor/...` 等特定分支安装
|
||||
|
||||
See [FAQ](docs/FAQ.md).
|
||||
[依赖组说明](INTRODUCTION.md#包依赖组说明)
|
||||
|
||||
## Project Architecture
|
||||
### 从源码安装
|
||||
|
||||
See [Architecture Overview](docs/ARCHITECTURE.md).
|
||||
我们提供原生 python 和 uv 两种安装方式.\
|
||||
详见[贡献指南](CONTRIBUTING.md).
|
||||
|
||||
## Contributing
|
||||
## 常见问题 (FAQ)
|
||||
|
||||
Contributions are welcome!
|
||||
See the [Contributing Guide](CONTRIBUTING.md).
|
||||
For AI-assisted development guidelines, see [AGENTS.md](AGENTS.md).
|
||||
详见[常见问题](FAQ.md).
|
||||
|
||||
## Project Identity
|
||||
## 项目架构
|
||||
|
||||
HeurAMS project identity assets are located in `./src/heurams/assets/art/` directory:
|
||||
详见[架构说明](ARCHITECTURE.md).
|
||||
|
||||
<img src="src/heurams/assets/art/banner128-light.png" height="96px" title="Bitmap Banner (Opaque)">
|
||||
## 参与项目
|
||||
|
||||
欢迎参与到项目协作中!\
|
||||
详见[贡献指南](CONTRIBUTING.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="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">
|
||||
<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>
|
||||
|
||||
Colors: `#1660A5 (Ocean Blue)` `#545F70 (Blue Gray)` `#FFFFFF (Monochrome Light Icon White)` `#1A1A1A (Monochrome Dark Icon Deep Black)` `#2f2f35 (Text Color)`.
|
||||
颜色分别是: `#1660A5 (海蓝色)` `#545F70 (蓝灰色)` `#FFFFFF (单色明亮图标白色)` `#1A1A1A (单色暗色图标深黑色)` `#2f2f35 (文字颜色)`.
|
||||
|
||||
## 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.
|
||||
### 项目本身
|
||||
|
||||
See the [LICENSE](LICENSE) file in the root directory.
|
||||
本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款, 较标准 AGPL-3.0 更松.
|
||||
|
||||
### Third-Party Code
|
||||
详见根目录下 [LICENSE](LICENSE) 文件.
|
||||
|
||||
The project embeds or directly uses the following third-party code or its derivatives (possibly with modifications) in `src/heurams/vendor/` or other locations:
|
||||
### 第三方代码
|
||||
|
||||
#### SM.js
|
||||
项目在 `src/heurams/vendor/` 目录下嵌入或在其他位置间接使用了以下第三方代码(可能有修改):
|
||||
|
||||
- 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
|
||||
#### SM.js (slaypni)
|
||||
|
||||
This project benefits from their selfless and excellent work.
|
||||
- 上游版本: 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
|
||||
|
||||
本项目受益于他们无私且优秀的工作.
|
||||
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
# 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
|
||||
|
||||
本项目受益于他们无私且优秀的工作.
|
||||
@@ -4,10 +4,9 @@
|
||||
|
||||
- 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_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).
|
||||
详见[贡献指南](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).
|
||||
|
||||
## 基本用户界面前端的截图
|
||||
|
||||
@@ -21,9 +20,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>
|
||||
|
||||
### 准备界面与预缓存工具
|
||||
@@ -32,8 +31,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>
|
||||
|
||||
### 记忆队列界面
|
||||
@@ -42,9 +41,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>
|
||||
|
||||
### 设置界面
|
||||
@@ -52,8 +51,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>
|
||||
|
||||
### 其他界面
|
||||
@@ -62,8 +61,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 前端的截图
|
||||
@@ -1,438 +0,0 @@
|
||||
## 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
@@ -1,416 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,83 +0,0 @@
|
||||
## 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.
|
||||
@@ -1,73 +0,0 @@
|
||||
# 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,11 +14,9 @@ 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]
|
||||
@@ -76,8 +74,3 @@ markers = [
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"httpx>=0.28.1",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#print("欢迎使用 HeurAMS 及其组件!")
|
||||
|
||||
# 补充日志记录
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger.info("HeurAMS is imported")
|
||||
logger.info("欢迎使用 HeurAMS 及其组件!")
|
||||
|
||||
+25
-79
@@ -1,118 +1,64 @@
|
||||
"""命令行入口
|
||||
"""
|
||||
|
||||
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,
|
||||
help=(
|
||||
f"HeurAMS {ver} - 启发式辅助记忆调度器"
|
||||
),
|
||||
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):
|
||||
def cli(ctx):
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo(cli.get_help(ctx))
|
||||
ctx.exit(0)
|
||||
|
||||
|
||||
@cli.command(cls=_I18nCommand)
|
||||
@cli.command()
|
||||
def tui():
|
||||
"""Launch the built-in user interface (TUI)"""
|
||||
"""启动内置基本用户界面 (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()
|
||||
def _print_version():
|
||||
click.echo(
|
||||
_("unifront API service started: http://{host}:{port}").format(
|
||||
host=host, port=port
|
||||
)
|
||||
f"HeurAMS {ver} ({codename}/{codename_cn}), 阶段: {stage}"
|
||||
)
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host=host, port=port, reload=reload, log_level="info")
|
||||
|
||||
|
||||
@cli.command(cls=_I18nCommand, name="help")
|
||||
@cli.command()
|
||||
def version():
|
||||
"""输出版本信息"""
|
||||
_print_version()
|
||||
|
||||
|
||||
@cli.command(name="ver", hidden=True)
|
||||
def ver_cmd():
|
||||
"""输出版本信息"""
|
||||
_print_version()
|
||||
|
||||
|
||||
@cli.command(name="help")
|
||||
@click.pass_context
|
||||
def help_cmd(ctx):
|
||||
"""Show this help message"""
|
||||
"""显示此帮助信息"""
|
||||
click.echo(cli.get_help(ctx.parent))
|
||||
|
||||
|
||||
def main():
|
||||
cli()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("HeurAMS cmdline entrypoint invoked")
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""全局上下文模块
|
||||
|
||||
初始化并管理基准路径, 程序配置对象, 并提供调试所需上下文管理器
|
||||
"""
|
||||
全局上下文管理模块
|
||||
以及基准路径
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
@@ -19,14 +19,13 @@ workdir = pathlib.Path.cwd()
|
||||
"""工作目录路径."""
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger.info(f"rootdir: {rootdir}")
|
||||
logger.info(f"workdir: {workdir}")
|
||||
logger.debug(f"包目录: {rootdir}")
|
||||
logger.debug(f"工作目录: {workdir}")
|
||||
|
||||
default_data = rootdir / "assets" / "data"
|
||||
user_data = workdir / "data"
|
||||
|
||||
if not user_data.exists():
|
||||
logger.info("Create a new data directory: %s", user_data)
|
||||
logger.info("初始化数据目录: %s", user_data)
|
||||
import shutil
|
||||
|
||||
shutil.copytree(default_data, user_data)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"""国际化支持模块
|
||||
|
||||
基于 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()
|
||||
@@ -2,53 +2,52 @@ from time import sleep, perf_counter
|
||||
|
||||
# import gc
|
||||
# gc.set_threshold(100, 1, 1)
|
||||
from heurams.i18n import _
|
||||
|
||||
print(_("Welcome to the basic user interface!"))
|
||||
print(_("Loading config and context... "), end="", flush=True)
|
||||
print("欢迎使用基本用户界面!")
|
||||
print("加载配置与上下文... ", end="", flush=True)
|
||||
_start_all = perf_counter()
|
||||
_start = _start_all
|
||||
from heurams.context import rootdir, workdir, config_var
|
||||
|
||||
_end = perf_counter()
|
||||
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
|
||||
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
|
||||
|
||||
print(_("Loading UI framework... "), end="", flush=True)
|
||||
print("加载用户界面框架... ", end="", flush=True)
|
||||
_start = perf_counter()
|
||||
from textual.app import App
|
||||
from textual.widgets import Button
|
||||
|
||||
_end = perf_counter()
|
||||
print(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
|
||||
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
|
||||
|
||||
print(_("Loading UI layout... "), end="", flush=True)
|
||||
print("加载用户界面布局... ", 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(_("Done! ({time}ms)").format(time=round(1000 * (_end - _start))))
|
||||
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
|
||||
|
||||
print(_("Component directory: {path}").format(path=rootdir))
|
||||
print(_("Working directory: {path}").format(path=workdir))
|
||||
print(f"组件目录: {rootdir}")
|
||||
print(f"工作目录: {workdir}")
|
||||
_end_all = perf_counter()
|
||||
print(_("Pre-work total: {time}ms").format(time=round(1000 * (_end_all - _start_all))))
|
||||
print(f"前置工作共计耗时: {round(1000 * (_end_all - _start_all))}ms")
|
||||
|
||||
|
||||
class HeurAMSApp(App):
|
||||
TITLE = "HeurAMS"
|
||||
TITLE = "潜进"
|
||||
CSS_PATH = rootdir / "interface" / "css" / "main.tcss"
|
||||
SUB_TITLE = _("Heuristic Auxiliary Memorizing Scheduler")
|
||||
SUB_TITLE = "启发式辅助记忆调度器"
|
||||
BINDINGS = [
|
||||
("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")),
|
||||
("q", "go_back", "退出"),
|
||||
("d", "toggle_dark", "主题"),
|
||||
("n", "app.push_screen('navigator')", "导航"),
|
||||
("s", "app.push_screen('setting')", "设置"),
|
||||
("z", "app.push_screen('about')", "关于"),
|
||||
]
|
||||
SCREENS = {
|
||||
"dashboard": DashboardScreen,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from heurams.interface import *
|
||||
from heurams.context import config_var
|
||||
from heurams.i18n import _
|
||||
from heurams.services.logger import get_logger
|
||||
import threading
|
||||
import pickle
|
||||
@@ -22,7 +21,7 @@ def start_debug_server(app):
|
||||
code = pickle.loads(msg)
|
||||
namespace = {"app": app, "logger": logger, "config_var": config_var}
|
||||
if first:
|
||||
app.title += _(" [Debug Connected]")
|
||||
app.title += " [调试已连接]"
|
||||
first = 0
|
||||
try:
|
||||
# 先尝试 eval
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""About screen"""
|
||||
"""关于界面"""
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -17,10 +16,10 @@ import sys
|
||||
|
||||
class AboutScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "go_back", _("Back")),
|
||||
("z", "go_back", _("About")),
|
||||
("q", "go_back", "返回"),
|
||||
("z", "go_back", "关于"),
|
||||
]
|
||||
SUB_TITLE = _("About")
|
||||
SUB_TITLE = "关于"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -38,64 +37,55 @@ class AboutScreen(Screen):
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
with ScrollableContainer(id="about_container"):
|
||||
yield Label(_("[b]About & Version Info[/b]"))
|
||||
# Get system info
|
||||
yield Label("[b]关于与版本信息[/b]")
|
||||
# 获取系统信息
|
||||
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 = _(
|
||||
"""# About HeurAMS
|
||||
about_text = f"""
|
||||
# 关于 HeurAMS "潜进"
|
||||
|
||||
Main library version: `{ver}-python`
|
||||
UI frontend: `Textual TUI (Basic UI)`
|
||||
UI version: `{ver}`
|
||||
API codename: `{codename}`
|
||||
主程序库版本: `{version.ver}-python`
|
||||
用户界面分支: `Textual TUI (基本用户界面)`
|
||||
用户界面版本: `{version.ver}`
|
||||
API 版本代号: `{version.codename.capitalize()}`
|
||||
|
||||
> 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.
|
||||
> 一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.
|
||||
> 一个开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
|
||||
|
||||
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.
|
||||
您可在项目主页 https://ams.pluv27.top 获取用户指南, 开发文档与软件更新, 并参与到软件的开发与改进工作.
|
||||
|
||||
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.
|
||||
以 GNU Affero 通用公共许可证 (第3版) 开放源代码, 并有一条豁免本机 API 调用的附加条款, 用于其他前端到程序库的接口调用.
|
||||
|
||||
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.
|
||||
您正使用程序库内置的终端用户界面, 它是第一个全功能前端实现与程序库测试套件, 位于程序库的 interface 子目录.
|
||||
|
||||
Developers:
|
||||
- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): Project initiator and lead developer
|
||||
开发人员列表:
|
||||
- Wang Zhiyu ([@pluvium27](https://github.com/pluvium27)): 项目发起与主要开发者
|
||||
|
||||
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 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
|
||||
- [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 算法底层实现
|
||||
|
||||
# Runtime Environment
|
||||
# 运行环境信息
|
||||
|
||||
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}
|
||||
Python 解释器版本: {python_version}
|
||||
Python 解释器路径: {sys.executable}
|
||||
Textual 框架版本: {textual_version}
|
||||
终端模拟器: {terminal_info}
|
||||
操作系统版本: {os_version}
|
||||
存储余量: {disk_usage}
|
||||
|
||||
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,
|
||||
)
|
||||
报告问题时, 请复制这些信息到问题描述, 并上传软件日志 `heurams.log` 作为附件, 以协助开发者定位错误
|
||||
"""
|
||||
yield Markdown(about_text, classes="about-markdown")
|
||||
yield Button(
|
||||
_("Back to Main"),
|
||||
"返回主界面",
|
||||
id="back_button",
|
||||
variant="primary",
|
||||
flat=True,
|
||||
@@ -112,20 +102,22 @@ When reporting issues, please copy this information into the issue description a
|
||||
self.action_go_back()
|
||||
|
||||
def _get_textual_version(self) -> str:
|
||||
"""获取 Textual 框架版本"""
|
||||
try:
|
||||
import textual
|
||||
|
||||
return textual.__version__
|
||||
except (ImportError, AttributeError):
|
||||
return _("Unknown")
|
||||
return "未知"
|
||||
|
||||
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 _("Unknown")
|
||||
return terminal_env or "未知"
|
||||
|
||||
def _get_python_version(self) -> str:
|
||||
"""获取 Python 解释器版本"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Dashboard screen"""
|
||||
"""仪表盘界面"""
|
||||
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
@@ -14,7 +14,6 @@ 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
|
||||
@@ -26,11 +25,11 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DashboardScreen(Screen):
|
||||
"""Main dashboard screen"""
|
||||
"""主仪表盘屏幕"""
|
||||
|
||||
SUB_TITLE = _("Dashboard")
|
||||
SUB_TITLE = "仪表盘"
|
||||
BINDINGS = [
|
||||
("q", "go_back", _("Back")),
|
||||
("q", "go_back", "返回"),
|
||||
]
|
||||
|
||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "dashboard.tcss"
|
||||
@@ -47,50 +46,45 @@ 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(_("Current daystamp: {ds}").format(ds=timer.get_daystamp())),
|
||||
Label(f"当前日时间戳: {timer.get_daystamp()}"),
|
||||
Label(
|
||||
_("Timezone offset: UTC+{offset}").format(offset=str(config_var.get()['services']['timer']['timezone_offset'] / 3600).removesuffix('.0'))
|
||||
f"应用时区修正: UTC+{str(config_var.get()['services']['timer']['timezone_offset'] / 3600).removesuffix('.0')}"
|
||||
),
|
||||
Label(
|
||||
_("Default algorithm: {algo}").format(algo=config_var.get()['interface']['global']['algorithm']),
|
||||
f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}",
|
||||
),
|
||||
classes="left",
|
||||
),
|
||||
Vertical(
|
||||
Label(_("Loaded {n} repo(s)").format(n=len(self.repos))),
|
||||
Label(f"已加载 {len(self.repos)} 个单元集"),
|
||||
Label(
|
||||
_("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)
|
||||
f"共计 {reduce(lambda x, y: x + y, map(lambda x: x.progress['total'], self.repos)) if self.repos else 0} 个单元"
|
||||
),
|
||||
Label(
|
||||
_("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)
|
||||
f"已激活 {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(_("Version {ver}-{stage}").format(ver=version.ver, stage=version.stage))
|
||||
yield Label(f"版本 {version.ver}-{version.stage}") # 版本信息
|
||||
yield Label(
|
||||
_("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)),
|
||||
),
|
||||
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)) + ' 个每秒'}",
|
||||
id="analysis",
|
||||
) # Version info
|
||||
) # 版本信息
|
||||
yield Footer()
|
||||
|
||||
@on(events.ScreenResume)
|
||||
@@ -146,28 +140,9 @@ 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 = _(
|
||||
"""{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")
|
||||
)
|
||||
),
|
||||
)
|
||||
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]"""
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""挂载组件时初始化"""
|
||||
@@ -185,7 +160,8 @@ class DashboardScreen(Screen):
|
||||
repo_list_widget.append(
|
||||
ListItem(
|
||||
Static(
|
||||
_("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'])
|
||||
f"在 {config_var.get()['global']['paths']['repo']} 中未找到任何单元集仓库目录.\n"
|
||||
"请导入单元集后重启应用, 或者新建单元集."
|
||||
),
|
||||
id="not-found",
|
||||
)
|
||||
@@ -199,7 +175,7 @@ class DashboardScreen(Screen):
|
||||
list_item = ListItem(
|
||||
*[Label(line) for line in r.prompt.splitlines()],
|
||||
Button(
|
||||
_("Start Learning"),
|
||||
f"开始学习",
|
||||
flat=True,
|
||||
variant="primary",
|
||||
id=f"slaunch_repo_{r.manifest['package']}",
|
||||
@@ -234,6 +210,7 @@ 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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Favorites manager screen"""
|
||||
"""收藏夹管理器界面"""
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
@@ -21,7 +21,6 @@ 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
|
||||
@@ -30,12 +29,12 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class FavoriteManagerScreen(Screen):
|
||||
"""Favorites manager screen"""
|
||||
"""收藏夹管理器屏幕"""
|
||||
|
||||
SUB_TITLE = _("Favorites")
|
||||
SUB_TITLE = "收藏夹"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "go_back", _("Back")),
|
||||
("q", "go_back", "返回"),
|
||||
("d", "toggle_dark", ""),
|
||||
]
|
||||
|
||||
@@ -50,12 +49,12 @@ class FavoriteManagerScreen(Screen):
|
||||
self._load_favorites()
|
||||
|
||||
def _load_favorites(self) -> None:
|
||||
"""Load favorites list"""
|
||||
"""加载收藏列表"""
|
||||
self.favorites = favorite_manager.get_all()
|
||||
logger.info("Loaded %d favorites", len(self.favorites))
|
||||
logger.debug("加载 %d 个收藏项", len(self.favorites))
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose UI components"""
|
||||
"""组合界面组件"""
|
||||
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
@@ -63,10 +62,10 @@ class FavoriteManagerScreen(Screen):
|
||||
)
|
||||
with ScrollableContainer(id="favorites-container"):
|
||||
if not self.favorites:
|
||||
yield Label(_("No favorites"), classes="empty-label")
|
||||
yield Static(_("Press * in the memorization screen to add favorites."))
|
||||
yield Label("暂无收藏", classes="empty-label")
|
||||
yield Static("使用 * 键在记忆界面中添加收藏.")
|
||||
else:
|
||||
yield Label(_("Total {n} favorite(s)").format(n=len(self.favorites)), classes="count-label")
|
||||
yield Label(f"共 {len(self.favorites)} 个收藏项", classes="count-label")
|
||||
yield ListView(id="favorites-list")
|
||||
yield Footer()
|
||||
|
||||
@@ -84,41 +83,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:
|
||||
"""Encode repo path and identifier into a safe button ID part"""
|
||||
# Use \x00 as separator between the two parts, then base64 encode
|
||||
"""编码仓库路径和标识符为安全的按钮 ID 部分"""
|
||||
# 使用 \x00 分隔两部分, 然后进行 base64 编码
|
||||
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]:
|
||||
"""Decode button ID part back to repo path and identifier"""
|
||||
# Pad to make length multiple of 4
|
||||
"""解码按钮 ID 部分为仓库路径和标识符"""
|
||||
# 补全等号以使长度是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 += _(" [d]Added: {time}\n From {title}[/d]").format(time=added_time, title=title)
|
||||
display_text += f" [d]添加于: {added_time}\n 来自 {title}[/d]"
|
||||
if fav.tags:
|
||||
display_text += f"{', '.join(fav.tags)}"
|
||||
|
||||
# Create safe button ID
|
||||
# 创建安全的按钮 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,
|
||||
@@ -129,21 +128,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("Repo directory does not exist: %s", repo_dir)
|
||||
logger.warning("仓库目录不存在: %s", repo_dir)
|
||||
return None
|
||||
repo = Repo.from_repodir(repo_dir)
|
||||
# Get atom content preview
|
||||
# 获取原子内容预览
|
||||
content_preview = ""
|
||||
payload = repo.payload
|
||||
# Find the payload entry matching ident
|
||||
# 查找对应 ident 的 payload 条目
|
||||
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:
|
||||
@@ -158,53 +157,53 @@ class FavoriteManagerScreen(Screen):
|
||||
"content_preview": content_preview,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get repo info: %s", e)
|
||||
logger.error("获取仓库信息失败: %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-"):
|
||||
# Extract encoded key
|
||||
key = button_id[7:] # Remove "remove-" prefix
|
||||
# 提取编码后的键
|
||||
key = button_id[7:] # 去掉 "remove-" 前缀
|
||||
try:
|
||||
repo_path, ident = self._decode_favorite_key(key)
|
||||
self._remove_favorite(repo_path, ident)
|
||||
except Exception as e:
|
||||
logger.error("Failed to parse button ID: %s", e)
|
||||
self.app.notify(_("Operation failed: invalid button identifier"), severity="error")
|
||||
logger.error("解析按钮 ID 失败: %s", e)
|
||||
self.app.notify("操作失败: 无效的按钮标识", 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(_("Removed favorite: {ident}").format(ident=ident), severity="information")
|
||||
# Reload list
|
||||
self.app.notify(f"已移除收藏: {ident}", severity="information")
|
||||
# 重新加载列表
|
||||
self._load_favorites()
|
||||
# Refresh UI
|
||||
# 刷新界面
|
||||
self._refresh_list()
|
||||
else:
|
||||
self.app.notify(_("Failed to remove: {ident}").format(ident=ident), severity="error")
|
||||
self.app.notify(f"移除失败: {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(_("No favorites"), classes="empty-label"))
|
||||
container.mount(Static(_("Press * in the memorization screen to add favorites.")))
|
||||
container.mount(Label("暂无收藏", classes="empty-label"))
|
||||
container.mount(Static("使用 * 键在记忆界面中添加收藏。"))
|
||||
else:
|
||||
container.mount(
|
||||
Label(_("Total {n} favorite(s)").format(n=len(self.favorites)), classes="count-label")
|
||||
Label(f"共 {len(self.favorites)} 个收藏项", classes="count-label")
|
||||
)
|
||||
list_view = ListView(id="favorites-list")
|
||||
container.mount(list_view)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Queue-based memorization screen"""
|
||||
"""队列式记忆工作界面"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
@@ -25,11 +24,11 @@ logger = get_logger(__name__)
|
||||
|
||||
class MemScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "go_back_notif", _("Back")),
|
||||
("p", "prev", _("Previous")),
|
||||
("q", "go_back_notif", "返回"),
|
||||
("p", "prev", "查看上一个"),
|
||||
("d", "toggle_dark", ""),
|
||||
("v", "play_voice", _("Read Aloud")),
|
||||
("*", "toggle_favorite", _("Favorite")),
|
||||
("v", "play_voice", "朗读"),
|
||||
("*", "toggle_favorite", "收藏"),
|
||||
("r", "resume_mark"),
|
||||
("Q", "go_back"),
|
||||
("n", "block_prompt"),
|
||||
@@ -37,12 +36,12 @@ class MemScreen(Screen):
|
||||
("z", "block_prompt"),
|
||||
]
|
||||
|
||||
SUB_TITLE = _("Learning")
|
||||
SUB_TITLE = "学习中"
|
||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "memoqueue.tcss"
|
||||
|
||||
if config_var.get()["interface"]["global"]["quick_pass"]:
|
||||
BINDINGS.append(("k", "quick_pass", _("Correct")))
|
||||
BINDINGS.append(("f", "quick_fail", _("Incorrect")))
|
||||
BINDINGS.append(("k", "quick_pass", "正确应答"))
|
||||
BINDINGS.append(("f", "quick_fail", "错误应答"))
|
||||
|
||||
rating = reactive(-1)
|
||||
|
||||
@@ -81,7 +80,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
|
||||
|
||||
@@ -102,17 +101,17 @@ class MemScreen(Screen):
|
||||
atom=self.atom, alia=puzzle["alia"] # type: ignore
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to expand puzzle: {e}")
|
||||
return Static(_("Failed to generate puzzle: {e}").format(e=e))
|
||||
logger.debug(f"调度展开出错: {e}")
|
||||
return Static(f"无法生成谜题 {e}")
|
||||
|
||||
def _get_progress_text(self):
|
||||
s = ""
|
||||
if self.repo is not None:
|
||||
fav_status = _("Favorited") if self._is_current_atom_favorited() else _("Not favorited")
|
||||
fav_status = "已收藏" if self._is_current_atom_favorited() else "未收藏"
|
||||
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 += _("Previous: {ident}").format(ident=f"[d]{self.procession.atoms[self.procession.cursor - 1]['ident']}[/d]")
|
||||
s += f"上一个: [d]{self.procession.atoms[self.procession.cursor - 1]['ident']}[/d]"
|
||||
return s
|
||||
|
||||
def update_display(self):
|
||||
@@ -121,7 +120,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
|
||||
@@ -131,7 +130,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")
|
||||
@@ -154,7 +153,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
|
||||
@@ -162,6 +161,7 @@ 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"Activated atom: {self.atom}")
|
||||
logger.debug(f"激活原子 {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(_("Are you sure? Press uppercase Q to go back."))
|
||||
self.notify("确定吗? 按下大写 Q 以返回")
|
||||
|
||||
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:
|
||||
"""Get repo relative path (relative to data/repo)"""
|
||||
"""获取仓库相对路径(相对于 data/repo)"""
|
||||
if self.repo is None:
|
||||
return ""
|
||||
# self.repo.source is the Path object pointing to the repo directory
|
||||
# self.repo.source 是 Path 对象, 指向仓库目录
|
||||
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:
|
||||
# If not under data/repo, return the full path as string
|
||||
# 如果不在 data/repo 下, 则返回完整路径(字符串形式)
|
||||
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(_("Cannot favorite: no repo associated"), severity="error")
|
||||
self.app.notify("无法收藏:未关联仓库", 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(_("Unfavorited: {ident}").format(ident=ident), severity="information")
|
||||
self.app.notify(f"已取消收藏:{ident}", severity="information")
|
||||
else:
|
||||
favorite_manager.add(repo_path, ident)
|
||||
self.app.notify(_("Favorited: {ident}").format(ident=ident), severity="information")
|
||||
# Update display if needed
|
||||
self.app.notify(f"已收藏:{ident}", severity="information")
|
||||
# 更新显示(如果需要)
|
||||
self.update_display()
|
||||
|
||||
def action_block_prompt(self):
|
||||
self.app.notify(_("This function is not available during memorization. Please finish or go back first."), severity="error")
|
||||
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", 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(_("Time resume corrected: {old} -> {new}").format(old=l, new=a.data['last']))
|
||||
self.app.notify(f"时间恢复已修正: {l} -> {a.data['last']}")
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -13,33 +12,36 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class NavigatorScreen(ModalScreen):
|
||||
"""Navigator modal screen"""
|
||||
"""导航器模态窗口"""
|
||||
|
||||
BINDINGS = [
|
||||
("q", "go_back", _("Back")),
|
||||
("escape", "go_back", _("Back")),
|
||||
("n", "go_back", _("Switch")),
|
||||
("q", "go_back", "返回"),
|
||||
("escape", "go_back", "返回"),
|
||||
("n", "go_back", "切换"),
|
||||
]
|
||||
|
||||
SCREENS = [
|
||||
(_("Dashboard"), "dashboard"),
|
||||
(_("Cache Manager"), "precache_all"),
|
||||
(_("Favorites"), FavoriteManagerScreen),
|
||||
(_("Settings Page"), "setting"),
|
||||
(_("Sync Tool"), "synctool"),
|
||||
(_("About"), "about"),
|
||||
("仪表盘", "dashboard"),
|
||||
# ("创建仓库", "repo_creator"),
|
||||
("缓存管理器", "precache_all"),
|
||||
("收藏夹", FavoriteManagerScreen),
|
||||
("设置页面", "setting"),
|
||||
# ("调试日志", "logviewer"),
|
||||
("同步工具", "synctool"),
|
||||
("关于此软件", "about"),
|
||||
# ("仓库编辑器", "repo_editor"),
|
||||
]
|
||||
|
||||
OTHERS = [
|
||||
(_("Exit"), "self.app.exit()"),
|
||||
(_("Project Homepage"), "webbrowser.open('https://ams.pluv27.top')"),
|
||||
("退出程序", "self.app.exit()"),
|
||||
("项目主页", "webbrowser.open('https://ams.pluv27.top')"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose UI components"""
|
||||
"""组合界面组件"""
|
||||
with Grid(id="dialog"):
|
||||
yield Label(
|
||||
_("[b]Select a function to navigate to\nor a memorization session instance[/b]\n\nTips will be displayed here"),
|
||||
"[b]请选择要跳转的功能\n或记忆会话实例[/b]\n\n将在此处显示提示",
|
||||
classes="title-label",
|
||||
)
|
||||
yield ListView(
|
||||
@@ -47,9 +49,9 @@ class NavigatorScreen(ModalScreen):
|
||||
id="nav-list",
|
||||
classes="nav-list-view",
|
||||
)
|
||||
yield Static(_("Press Enter to switch\nAll sessions will be saved"))
|
||||
yield Static("按下回车以完成切换\n所有会话将被保存")
|
||||
yield Button(
|
||||
_("Close (n)"),
|
||||
"关闭 (n)",
|
||||
id="close_button",
|
||||
variant="primary",
|
||||
classes="close-button",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Cache tool screen"""
|
||||
"""缓存工具界面"""
|
||||
|
||||
import pathlib
|
||||
|
||||
@@ -13,15 +13,14 @@ 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 _
|
||||
|
||||
# Compatibility cache path: prefer paths.cache, otherwise data/cache
|
||||
# 兼容性缓存路径:优先使用 paths.cache, 否则使用 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}"
|
||||
@@ -30,18 +29,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): Optional list containing Nucleon objects only
|
||||
desc (list): Optional string containing description of this call
|
||||
nucleons (list): 可选列表, 仅包含 Nucleon 对象
|
||||
desc (list): 可选字符串, 包含对此次调用的文字描述
|
||||
"""
|
||||
|
||||
SUB_TITLE = _("Cache Manager")
|
||||
SUB_TITLE = "缓存管理器"
|
||||
BINDINGS = [
|
||||
("q", "go_back", _("Back")),
|
||||
("q", "go_back", "返回"),
|
||||
]
|
||||
|
||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||
@@ -68,7 +67,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
|
||||
|
||||
@@ -90,7 +89,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
|
||||
@@ -118,25 +117,21 @@ class PrecachingScreen(Screen):
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
with ScrollableContainer(id="precache_container"):
|
||||
yield Label(_("[b]Audio Pre-cache[/b]"), classes="title-label")
|
||||
yield Label("[b]音频预缓存[/b]", classes="title-label")
|
||||
with Container():
|
||||
yield Static(
|
||||
_("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),
|
||||
),
|
||||
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% (已缓存 {self.cache_stats.get('cached_units', 0)} / {self.cache_stats.get('total_units', 0)} 个单元)",
|
||||
classes="cache-usage-text",
|
||||
)
|
||||
if self.nucleons:
|
||||
yield Static(
|
||||
_("Target units from: [b]{desc}[/b]").format(desc=self.desc), classes="target-info"
|
||||
f"目标单元归属: [b]{self.desc}[/b]", classes="target-info"
|
||||
)
|
||||
yield Static(
|
||||
_("Unit count: {n}").format(n=len(self.nucleons)), classes="target-info"
|
||||
f"单元数量: {len(self.nucleons)}", classes="target-info"
|
||||
)
|
||||
else:
|
||||
yield Static(_("Target: all units"), classes="target-info")
|
||||
yield Static("目标: 所有单元", classes="target-info")
|
||||
|
||||
yield Static(id="status", classes="status-info")
|
||||
yield Static(id="current_item", classes="current-item")
|
||||
@@ -144,72 +139,69 @@ class PrecachingScreen(Screen):
|
||||
with Horizontal(classes="button-group"):
|
||||
if not self.is_precaching:
|
||||
yield Button(
|
||||
_("Start Pre-cache"), id="start_precache", variant="primary"
|
||||
"开始预缓存", id="start_precache", variant="primary"
|
||||
)
|
||||
else:
|
||||
yield Button(
|
||||
_("Cancel Pre-cache"), id="cancel_precache", variant="error"
|
||||
"取消预缓存", id="cancel_precache", variant="error"
|
||||
)
|
||||
yield Button(_("Clear Cache"), id="clear_cache", variant="warning")
|
||||
yield Button(_("Back"), id="go_back", variant="default")
|
||||
yield Button("清空缓存", id="clear_cache", variant="warning")
|
||||
yield Button("返回", id="go_back", variant="default")
|
||||
with Container(classes="cache-info"):
|
||||
yield Static(_("Cache path: {path}").format(path=cache_dir), classes="cache-path")
|
||||
yield Static(f"缓存路径: {cache_dir}", classes="cache-path")
|
||||
yield Static(
|
||||
_("Files: {n}").format(n=self.cache_stats['file_count']), classes="cache-count"
|
||||
f"文件数: {self.cache_stats['file_count']}", classes="cache-count"
|
||||
)
|
||||
yield Static(
|
||||
_("Total size: {size}").format(size=self.cache_stats['human_size']), classes="cache-size"
|
||||
f"总大小: {self.cache_stats['human_size']}", classes="cache-size"
|
||||
)
|
||||
yield Button(
|
||||
_("Refresh"), id="refresh_cache_stats", variant="default", flat=True
|
||||
"刷新", id="refresh_cache_stats", variant="default", flat=True
|
||||
)
|
||||
yield Static(_("If you leave this screen, ongoing cache processes will stop automatically."))
|
||||
yield Static(_('Cache supports "resume from break".'))
|
||||
yield Static("若您离开此界面, 未完成的缓存进程会自动停止.")
|
||||
yield Static('缓存程序支持 "断点续传".')
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
"""Initialise state on mount"""
|
||||
self.update_status(_("Ready"), _("Waiting to start..."))
|
||||
"""挂载时初始化状态"""
|
||||
self.update_status("就绪", "等待开始...")
|
||||
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(_("Status: {s}").format(s=status))
|
||||
item_widget.update(_("Current item: {item}").format(item=current_item) if current_item else "")
|
||||
status_widget.update(f"状态: {status}")
|
||||
item_widget.update(f"当前项目: {current_item}" if current_item else "")
|
||||
|
||||
if progress is not None:
|
||||
progress_bar.progress = progress
|
||||
progress_bar.advance(0) # Refresh display
|
||||
progress_bar.advance(0) # 刷新显示
|
||||
|
||||
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(_("Files: {n}").format(n=self.cache_stats['file_count']))
|
||||
cache_count_widget.update(f"文件数: {self.cache_stats['file_count']}")
|
||||
if cache_size_widget:
|
||||
cache_size_widget.update(_("Total size: {size}").format(size=self.cache_stats['human_size']))
|
||||
cache_size_widget.update(f"总大小: {self.cache_stats['human_size']}")
|
||||
if cache_usage_text:
|
||||
cache_usage_text.update(
|
||||
_("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),
|
||||
)
|
||||
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% "
|
||||
f"(已缓存 {self.cache_stats.get('cached_units', 0)} / {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"
|
||||
@@ -220,21 +212,21 @@ class PrecachingScreen(Screen):
|
||||
convertor(text, cache_file)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Pre-cache failed '{text}': {e}")
|
||||
print(f"预缓存失败 '{text}': {e}")
|
||||
return 0
|
||||
return 1
|
||||
|
||||
def precache_by_nucleon(self, nucleon: pt.Nucleon):
|
||||
"""Cache based on Nucleon"""
|
||||
"""依据 Nucleon 缓存"""
|
||||
ret = self.precache_by_text(nucleon["tts_text"])
|
||||
return ret
|
||||
|
||||
def precache_by_list(self, nucleons: list):
|
||||
"""Cache based on Nucleons list"""
|
||||
"""依据 Nucleons 列表缓存"""
|
||||
for idx, nucleon in enumerate(nucleons):
|
||||
# print(f"PROC: {nucleon}")
|
||||
worker = get_current_worker()
|
||||
if worker and worker.is_cancelled: # Function running in worker and has been cancelled
|
||||
if worker and worker.is_cancelled: # 函数在worker中执行且已被取消
|
||||
return False
|
||||
text = nucleon["tts_text"]
|
||||
# self.current_item = text[:30] + "..." if len(text) > 50 else text
|
||||
@@ -244,12 +236,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(_("Processing ({i}/{total})").format(i=idx + 1, total=len(nucleons)), text, progress)
|
||||
self.update_status(f"正处理 ({idx + 1}/{len(nucleons)})", text, progress)
|
||||
ret = self.precache_by_nucleon(nucleon)
|
||||
if not ret:
|
||||
self.update_status(
|
||||
_("Error"),
|
||||
_("Failed, skipping: {item}").format(item=self.current_item),
|
||||
"出错",
|
||||
f"处理失败, 跳过: {self.current_item}",
|
||||
)
|
||||
import time
|
||||
|
||||
@@ -294,7 +286,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,
|
||||
@@ -311,32 +303,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(_("Cancelled"), _("Pre-cache cancelled by user"), 0)
|
||||
self.update_status("已取消", "预缓存操作被用户取消", 0)
|
||||
|
||||
elif event.button.id == "clear_cache":
|
||||
# Clear cache
|
||||
# 清空缓存
|
||||
try:
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||
self.update_status(_("Cleared"), _("Audio cache cleared"), 0)
|
||||
self._update_cache_display() # Update cache stats display
|
||||
self.update_status("已清空", "音频缓存已清空", 0)
|
||||
self._update_cache_display() # 更新缓存统计显示
|
||||
except Exception as e:
|
||||
self.update_status(_("Error"), _("Failed to clear cache: {error}").format(error=e))
|
||||
self.update_status("错误", f"清空缓存失败: {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(_("Cache info refreshed"), severity="information")
|
||||
self.app.notify("缓存信息已刷新", severity="information")
|
||||
elif event.button.id == "go_back":
|
||||
self.action_go_back()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Memorization preparation screen"""
|
||||
"""记忆准备界面"""
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
@@ -19,7 +19,6 @@ 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
|
||||
@@ -29,11 +28,11 @@ logger = get_logger(__name__)
|
||||
|
||||
class PreparationScreen(Screen):
|
||||
|
||||
SUB_TITLE = _("Prepare Repository")
|
||||
SUB_TITLE = "准备记忆集"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "go_back", _("Back")),
|
||||
("p", "precache", _("Cache")),
|
||||
("q", "go_back", "返回"),
|
||||
("p", "precache", "缓存"),
|
||||
("d", "toggle_dark", ""),
|
||||
("0,1,2,3", "app.push_screen('about')", ""),
|
||||
]
|
||||
@@ -62,40 +61,29 @@ class PreparationScreen(Screen):
|
||||
)
|
||||
with ScrollableContainer(id="main_container"):
|
||||
yield Markdown(
|
||||
_("**Ready**: `{title}`\n").format(title=self.repo.manifest['title']), id="title"
|
||||
f"**准备就绪**: `{self.repo.manifest['title']}`\n", id="title"
|
||||
)
|
||||
yield Label(_("Repo path: {path}").format(path=self.repo.source))
|
||||
yield Label(f"单元集路径: {self.repo.source}")
|
||||
yield Label(
|
||||
_("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)
|
||||
)
|
||||
f"学习完成度: {self.repo.progress['touched']}/{len(self.repo)} [d]\\[{round(self.repo.progress['touched']/self.repo.progress['total']*100, 1)}%][/d]"
|
||||
)
|
||||
yield Label(
|
||||
_("Scheduling algorithm: {algo} {desc}").format(
|
||||
algo=self.repo.config["algorithm"],
|
||||
desc=algorithms[self.repo.config["algorithm"]].desc
|
||||
)
|
||||
f"调度算法: {self.repo.config["algorithm"]} {algorithms[self.repo.config["algorithm"]].desc}"
|
||||
)
|
||||
yield Label(
|
||||
_("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,
|
||||
),
|
||||
f"学习数量: {self.repo.preview['review'] + self.scheduled_num} = {self.repo.preview['review']} [d][复习][/d] + {self.scheduled_num} [d][新识记][/d]\n",
|
||||
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",
|
||||
@@ -111,6 +99,14 @@ 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 = ""
|
||||
@@ -158,6 +154,7 @@ 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)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Settings screen"""
|
||||
"""设置页面"""
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
@@ -16,7 +16,6 @@ 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
|
||||
@@ -27,12 +26,12 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SettingScreen(Screen):
|
||||
"""Settings screen"""
|
||||
"""设置页面屏幕"""
|
||||
|
||||
SUB_TITLE = _("Settings")
|
||||
SUB_TITLE = "设置"
|
||||
BINDINGS = [
|
||||
("q", "go_back", _("Back")),
|
||||
("s", "go_back", _("Settings")),
|
||||
("q", "go_back", "返回"),
|
||||
("s", "go_back", "设置"),
|
||||
]
|
||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "setting.tcss"
|
||||
|
||||
@@ -51,13 +50,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]" + _("Settings") + "[/b]")
|
||||
yield Label("[b]设置页面[/b]")
|
||||
for i in config_var.get():
|
||||
if i.startswith("_"):
|
||||
continue
|
||||
@@ -68,7 +67,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()
|
||||
@@ -139,7 +138,7 @@ class SettingScreen(Screen):
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Input(
|
||||
value=str(parent[i]),
|
||||
placeholder=_("Requires a float"),
|
||||
placeholder="要求一个浮点数",
|
||||
type="number",
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
),
|
||||
@@ -152,7 +151,7 @@ class SettingScreen(Screen):
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Input(
|
||||
value=parent[i],
|
||||
placeholder=_("Requires a string"),
|
||||
placeholder="要求一个字符串",
|
||||
type="text",
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
),
|
||||
@@ -177,7 +176,7 @@ class SettingScreen(Screen):
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Input(
|
||||
value=str(parent[i]),
|
||||
placeholder=_("Requires an integer"),
|
||||
placeholder="要求一个整数",
|
||||
type="integer",
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
),
|
||||
@@ -187,9 +186,9 @@ class SettingScreen(Screen):
|
||||
elif isinstance(parent[i], list):
|
||||
pass
|
||||
else:
|
||||
lst.append(Label(_("Unknown type")))
|
||||
lst.append(Label("未知类型"))
|
||||
return lst
|
||||
return [Label(_("No sub-items"))]
|
||||
return [Label("无子项")]
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""挂载组件时初始化"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Sync tool screen"""
|
||||
"""同步工具界面"""
|
||||
|
||||
import pathlib
|
||||
import time
|
||||
@@ -12,12 +12,11 @@ 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", _("Back"))]
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
|
||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
@@ -40,67 +39,67 @@ class SyncScreen(Screen):
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
with ScrollableContainer(id="sync_container"):
|
||||
# Title and connection status
|
||||
yield Static(_("Sync Tool"), classes="title")
|
||||
# 标题和连接状态
|
||||
yield Static("同步工具", classes="title")
|
||||
yield Static("", id="status_label", classes="status")
|
||||
|
||||
# Config info
|
||||
yield Static(_("Sync protocol: {proto}").format(proto=config_var.get()['services']['sync']))
|
||||
yield Static(_("Server Configuration:"), classes="section_title")
|
||||
# 配置信息
|
||||
yield Static(f"同步协议: {config_var.get()['services']['sync']}")
|
||||
yield Static("服务器配置:", classes="section_title")
|
||||
with Horizontal(classes="config_info"):
|
||||
yield Static(_("Remote server:"), classes="config_label")
|
||||
yield Static("远程服务器:", classes="config_label")
|
||||
yield Static("", id="server_url", classes="config_value")
|
||||
with Horizontal(classes="config_info"):
|
||||
yield Static(_("Remote path:"), classes="config_label")
|
||||
yield Static("远程路径:", classes="config_label")
|
||||
yield Static("", id="remote_path", classes="config_value")
|
||||
|
||||
with Horizontal(classes="control_buttons"):
|
||||
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 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 Static(_("Sync Progress"), classes="section_title")
|
||||
yield Static("同步进度", classes="section_title")
|
||||
yield ProgressBar(id="progress_bar", show_percentage=True, total=100)
|
||||
yield Static("", id="progress_label", classes="progress_text")
|
||||
|
||||
yield Static(_("Sync Log"), classes="section_title")
|
||||
yield Static("同步日志", 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(_("Sync tool started"))
|
||||
self.log_message("同步工具已启动")
|
||||
|
||||
def update_ui_from_config(self):
|
||||
"""Update UI with config info"""
|
||||
"""更新 UI 显示配置信息"""
|
||||
try:
|
||||
sync_cfg: dict = config_var.get()["providers"]["sync"]["webdav"]
|
||||
# Update server URL
|
||||
url = sync_cfg.get("url", _("Not configured"))
|
||||
# 更新服务器 URL
|
||||
url = sync_cfg.get("url", "未配置")
|
||||
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(_("✅ Sync service ready")) # type: ignore
|
||||
status_widget.update("✅ 同步服务已就绪") # type: ignore
|
||||
status_widget.add_class("ready")
|
||||
else:
|
||||
status_widget.update(_("❌ Sync service not configured or not enabled")) # type: ignore
|
||||
status_widget.update("❌ 同步服务未配置或未启用") # type: ignore
|
||||
status_widget.add_class("error")
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(_("Failed to update UI: {error}").format(error=e), is_error=True)
|
||||
self.log_message(f"更新 UI 失败: {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
|
||||
@@ -113,28 +112,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(_("Failed to update status: {error}").format(error=e), is_error=True)
|
||||
self.log_message(f"更新状态失败: {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 # Ignore if widget not ready
|
||||
pass # 如果组件未就绪, 忽略错误
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button press events"""
|
||||
"""处理按钮点击事件"""
|
||||
button_id = event.button.id
|
||||
|
||||
if button_id == "test_connection":
|
||||
@@ -149,133 +148,124 @@ class SyncScreen(Screen):
|
||||
event.stop()
|
||||
|
||||
def test_connection(self):
|
||||
"""Test WebDAV server connection"""
|
||||
"""测试 WebDAV 服务器连接"""
|
||||
if not self.sync_service:
|
||||
self.log_message(_("Sync service not initialised, please check configuration"), is_error=True)
|
||||
self.update_status(_("❌ Sync service not initialised"))
|
||||
self.log_message("同步服务未初始化, 请检查配置", is_error=True)
|
||||
self.update_status("❌ 同步服务未初始化")
|
||||
return
|
||||
|
||||
self.log_message(_("Testing WebDAV connection..."))
|
||||
self.update_status(_("Testing connection..."))
|
||||
self.log_message("正在测试 WebDAV 连接...")
|
||||
self.update_status("正在测试连接...")
|
||||
|
||||
try:
|
||||
success = self.sync_service.test_connection()
|
||||
if success:
|
||||
self.log_message(_("Connection test successful"))
|
||||
self.update_status(_("✅ Connection OK"))
|
||||
self.log_message("连接测试成功")
|
||||
self.update_status("✅ 连接正常")
|
||||
else:
|
||||
self.log_message(_("Connection test failed"), is_error=True)
|
||||
self.update_status(_("❌ Connection failed"))
|
||||
self.log_message("连接测试失败", is_error=True)
|
||||
self.update_status("❌ 连接失败")
|
||||
except Exception as e:
|
||||
self.log_message(_("Connection test error: {error}").format(error=e), is_error=True)
|
||||
self.update_status(_("❌ Connection error"))
|
||||
self.log_message(f"连接测试异常: {e}", is_error=True)
|
||||
self.update_status("❌ 连接异常")
|
||||
|
||||
def start_sync(self):
|
||||
"""Start syncing"""
|
||||
"""开始同步"""
|
||||
if not self.sync_service:
|
||||
self.log_message(_("Sync service not initialised, cannot start sync"), is_error=True)
|
||||
self.log_message("同步服务未初始化, 无法开始同步", is_error=True)
|
||||
return
|
||||
|
||||
if self.is_syncing:
|
||||
self.log_message(_("Sync already in progress"), is_error=True)
|
||||
self.log_message("同步已在进行中", is_error=True)
|
||||
return
|
||||
|
||||
self.is_syncing = True
|
||||
self.is_paused = False
|
||||
self.update_button_states()
|
||||
|
||||
self.log_message(_("Starting data sync..."))
|
||||
self.update_status(_("Syncing..."), progress=0)
|
||||
self.log_message("开始同步数据...")
|
||||
self.update_status("正在同步...", 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", {})
|
||||
|
||||
# Sync nucleon directory
|
||||
# 同步 nucleon 目录
|
||||
nucleon_dir = pathlib.Path(paths.get("nucleon_dir", "./data/nucleon"))
|
||||
if nucleon_dir.exists():
|
||||
self.log_message(_("Syncing nucleon directory: {dir}").format(dir=nucleon_dir))
|
||||
self.update_status(_("Syncing nucleon directory..."), progress=10)
|
||||
self.log_message(f"同步 nucleon 目录: {nucleon_dir}")
|
||||
self.update_status(f"同步 nucleon 目录...", progress=10)
|
||||
|
||||
result = self.sync_service.sync_directory(nucleon_dir) # type: ignore
|
||||
if result.get("success"):
|
||||
self.log_message(
|
||||
_("nucleon sync complete: uploaded {up}, downloaded {down}").format(
|
||||
up=result.get('uploaded', 0),
|
||||
down=result.get('downloaded', 0),
|
||||
)
|
||||
f"nucleon 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)} 个"
|
||||
)
|
||||
else:
|
||||
self.log_message(
|
||||
_("nucleon sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
|
||||
f"nucleon 同步失败: {result.get('error', '未知错误')}",
|
||||
is_error=True,
|
||||
)
|
||||
|
||||
# Sync electron directory
|
||||
# 同步 electron 目录
|
||||
electron_dir = pathlib.Path(paths.get("electron_dir", "./data/electron"))
|
||||
if electron_dir.exists():
|
||||
self.log_message(_("Syncing electron directory: {dir}").format(dir=electron_dir))
|
||||
self.update_status(_("Syncing electron directory..."), progress=60)
|
||||
self.log_message(f"同步 electron 目录: {electron_dir}")
|
||||
self.update_status(f"同步 electron 目录...", progress=60)
|
||||
|
||||
result = self.sync_service.sync_directory(electron_dir) # type: ignore
|
||||
if result.get("success"):
|
||||
self.log_message(
|
||||
_("electron sync complete: uploaded {up}, downloaded {down}").format(
|
||||
up=result.get('uploaded', 0),
|
||||
down=result.get('downloaded', 0),
|
||||
)
|
||||
f"electron 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)} 个"
|
||||
)
|
||||
else:
|
||||
self.log_message(
|
||||
_("electron sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
|
||||
f"electron 同步失败: {result.get('error', '未知错误')}",
|
||||
is_error=True,
|
||||
)
|
||||
|
||||
# Sync orbital directory (if exists)
|
||||
# 同步 orbital 目录(如果存在)
|
||||
orbital_dir = pathlib.Path(paths.get("orbital_dir", "./data/orbital"))
|
||||
if orbital_dir.exists():
|
||||
self.log_message(_("Syncing orbital directory: {dir}").format(dir=orbital_dir))
|
||||
self.update_status(_("Syncing orbital directory..."), progress=80)
|
||||
self.log_message(f"同步 orbital 目录: {orbital_dir}")
|
||||
self.update_status(f"同步 orbital 目录...", progress=80)
|
||||
|
||||
result = self.sync_service.sync_directory(orbital_dir) # type: ignore
|
||||
if result.get("success"):
|
||||
self.log_message(
|
||||
_("orbital sync complete: uploaded {up}, downloaded {down}").format(
|
||||
up=result.get('uploaded', 0),
|
||||
down=result.get('downloaded', 0),
|
||||
)
|
||||
f"orbital 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)} 个"
|
||||
)
|
||||
else:
|
||||
self.log_message(
|
||||
_("orbital sync failed: {err}").format(err=result.get('error', _('Unknown error'))),
|
||||
f"orbital 同步失败: {result.get('error', '未知错误')}",
|
||||
is_error=True,
|
||||
)
|
||||
|
||||
# Sync complete
|
||||
self.update_status(_("Sync complete"), progress=100)
|
||||
self.log_message(_("All directories synced"))
|
||||
# 同步完成
|
||||
self.update_status("同步完成", progress=100)
|
||||
self.log_message("所有目录同步完成")
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(_("Error during sync: {error}").format(error=e), is_error=True)
|
||||
self.update_status(_("Sync failed"))
|
||||
self.log_message(f"同步过程中发生错误: {e}", is_error=True)
|
||||
self.update_status("同步失败")
|
||||
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
|
||||
|
||||
@@ -283,14 +273,14 @@ class SyncScreen(Screen):
|
||||
self.update_button_states()
|
||||
|
||||
if self.is_paused:
|
||||
self.log_message(_("Sync paused"))
|
||||
self.update_status(_("Sync paused"))
|
||||
self.log_message("同步已暂停")
|
||||
self.update_status("同步已暂停")
|
||||
else:
|
||||
self.log_message(_("Sync resumed"))
|
||||
self.update_status(_("Syncing..."))
|
||||
self.log_message("同步已恢复")
|
||||
self.update_status("正在同步...")
|
||||
|
||||
def cancel_sync(self):
|
||||
"""Cancel sync"""
|
||||
"""取消同步"""
|
||||
if not self.is_syncing:
|
||||
return
|
||||
|
||||
@@ -298,11 +288,11 @@ class SyncScreen(Screen):
|
||||
self.is_paused = False
|
||||
self.update_button_states()
|
||||
|
||||
self.log_message(_("Sync cancelled"))
|
||||
self.update_status(_("Sync cancelled"))
|
||||
self.log_message("同步已取消")
|
||||
self.update_status("同步已取消")
|
||||
|
||||
def update_button_states(self):
|
||||
"""Update button states"""
|
||||
"""更新按钮状态"""
|
||||
try:
|
||||
start_button = self.query_one("#start_sync")
|
||||
pause_button = self.query_one("#pause_sync")
|
||||
@@ -312,14 +302,14 @@ class SyncScreen(Screen):
|
||||
start_button.disabled = True
|
||||
pause_button.disabled = False
|
||||
cancel_button.disabled = False
|
||||
pause_button.label = _("Resume") if self.is_paused else _("Pause") # type: ignore
|
||||
pause_button.label = "继续" if self.is_paused else "暂停" # type: ignore
|
||||
else:
|
||||
start_button.disabled = False
|
||||
pause_button.disabled = True
|
||||
cancel_button.disabled = True
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(_("Failed to update button state: {error}").format(error=e), is_error=True)
|
||||
self.log_message(f"更新按钮状态失败: {e}", is_error=True)
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
@@ -5,7 +5,6 @@ from textual.widgets import Button, Label, Static
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
|
||||
from heurams.i18n import _
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
|
||||
|
||||
@@ -33,49 +32,49 @@ class BasicEvaluation(BasePuzzleWidget):
|
||||
|
||||
class RatingChanged(Message):
|
||||
def __init__(self, rating: int) -> None:
|
||||
self.rating = rating # Rating value (0-5)
|
||||
self.rating = rating # 评分值 (0-5)
|
||||
super().__init__()
|
||||
|
||||
# Feedback mapping
|
||||
# 反馈映射表
|
||||
feedback_mapping = {
|
||||
"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")},
|
||||
"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": "完全空白"},
|
||||
}
|
||||
|
||||
def compose(self):
|
||||
# Show main content
|
||||
# 显示主要内容
|
||||
yield Label(self.atom.registry["nucleon"]["content"], id="main")
|
||||
|
||||
# Show instruction (optional)
|
||||
yield Static(_("Evaluate how well you remember this content: "), classes="instruction")
|
||||
# 显示评估说明(可选)
|
||||
yield Static("请评估你对这个内容的记忆程度: ", classes="instruction")
|
||||
|
||||
# Button container
|
||||
# 按钮容器
|
||||
with ScrollableContainer(id="button_container"):
|
||||
btn = {}
|
||||
btn["5"] = Button(
|
||||
_("Perfect recall"), variant="success", id="feedback_5", classes="choice"
|
||||
"完美回想", variant="success", id="feedback_5", classes="choice"
|
||||
)
|
||||
btn["4"] = Button(
|
||||
_("Correct after hesitation"), variant="success", id="feedback_4", classes="choice"
|
||||
"犹豫后正确", variant="success", id="feedback_4", classes="choice"
|
||||
)
|
||||
btn["3"] = Button(
|
||||
_("Correct with difficulty"), variant="warning", id="feedback_3", classes="choice"
|
||||
"困难地正确", variant="warning", id="feedback_3", classes="choice"
|
||||
)
|
||||
btn["2"] = Button(
|
||||
_("Wrong but familiar"), variant="warning", id="feedback_2", classes="choice"
|
||||
"错误但熟悉", variant="warning", id="feedback_2", classes="choice"
|
||||
)
|
||||
btn["1"] = Button(
|
||||
_("Wrong and unfamiliar"), variant="error", id="feedback_1", classes="choice"
|
||||
"错误且不熟", variant="error", id="feedback_1", classes="choice"
|
||||
)
|
||||
btn["0"] = Button(
|
||||
_("Complete blank"), variant="error", id="feedback_0", classes="choice"
|
||||
"完全空白", 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,7 +10,6 @@ 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
|
||||
@@ -80,6 +79,7 @@ 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(_("Backspace"), id="delete")
|
||||
yield Button("退格", id="delete")
|
||||
self.btn_shortcuts[f"0"] = "delete"
|
||||
self.btn_shortcuts[f"backspace"] = "delete"
|
||||
self.btn_shortcuts[f"delete"] = "delete"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Label
|
||||
|
||||
from heurams.i18n import _
|
||||
|
||||
|
||||
class Finished(Widget):
|
||||
def __init__(
|
||||
@@ -28,9 +26,9 @@ class Finished(Widget):
|
||||
)
|
||||
|
||||
def compose(self):
|
||||
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")
|
||||
yield Label("本次记忆进程结束", id="finished_msg")
|
||||
yield Label(f"算法数据{'已保存' if self.is_saved else "未能保存"}")
|
||||
yield Button("返回上一级", flat=True, id="back-to-menu")
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
button_id = event.button.id
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Multiple-choice puzzle
|
||||
# 单项选择题
|
||||
from typing import TypedDict
|
||||
|
||||
from textual.containers import ScrollableContainer
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -66,10 +65,15 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
|
||||
def compose(self):
|
||||
setting: Setting = self.atom.registry["nucleon"]["puzzles"][self.alia]
|
||||
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")
|
||||
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")
|
||||
|
||||
# 渲染当前问题的选项
|
||||
c = 0
|
||||
@@ -81,19 +85,21 @@ 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(_("Backspace"), id="delete")
|
||||
yield Button("退格", 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(_("Current input: {input}").format(input=self.inputlist)) # type: ignore
|
||||
preview.update(f"当前输入: {self.inputlist}") # type: ignore
|
||||
logger.debug("已经更新预览标签")
|
||||
# 更新问题标签
|
||||
puzzle_label = self.query_one("#puzzle")
|
||||
current_question_index = len(self.inputlist)
|
||||
@@ -116,7 +122,7 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
# 选项选择处理
|
||||
answer_text = self.hashmap[button_id[7:]] # type: ignore
|
||||
self.inputlist.append(answer_text)
|
||||
logger.debug(f"Input list: {self.inputlist}")
|
||||
logger.debug(f"{self.inputlist}")
|
||||
# 检查是否完成所有题目
|
||||
if len(self.inputlist) >= len(self.puzzle.answer):
|
||||
is_correct = self.inputlist == self.puzzle.answer
|
||||
@@ -137,6 +143,7 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
def refresh_buttons(self):
|
||||
"""刷新按钮显示(用于题目切换)"""
|
||||
# 移除所有选项按钮
|
||||
logger.debug("刷新按钮")
|
||||
self.cursor += 1
|
||||
container = self.query_one("#btn-container")
|
||||
buttons_to_remove = [
|
||||
@@ -146,6 +153,7 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
]
|
||||
container.focus()
|
||||
for button in buttons_to_remove:
|
||||
logger.info(button)
|
||||
container.remove_children("#" + button.id) # type: ignore
|
||||
|
||||
# 添加当前题目的选项按钮
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Label
|
||||
|
||||
from heurams.i18n import _
|
||||
|
||||
|
||||
class Placeholder(Widget):
|
||||
def __init__(
|
||||
@@ -25,8 +23,8 @@ class Placeholder(Widget):
|
||||
)
|
||||
|
||||
def compose(self):
|
||||
yield Label(_("Sample Label"), id="testlabel")
|
||||
yield Button(_("Sample Button"), id="testbtn", classes="choice")
|
||||
yield Label("示例标签", id="testlabel")
|
||||
yield Button("示例按钮", id="testbtn", classes="choice")
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
pass
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -87,7 +86,7 @@ class Recognition(BasePuzzleWidget):
|
||||
for item in cfg["secondary"]:
|
||||
if isinstance(item, list):
|
||||
for j in item:
|
||||
yield Markdown(_("### Note: {note}").format(note=j)) # TODO ANNOTATION
|
||||
yield Markdown(f"### 笔记: {j}") # TODO ANNOTATION
|
||||
continue
|
||||
if isinstance(item, Dict):
|
||||
total = ""
|
||||
@@ -98,7 +97,7 @@ class Recognition(BasePuzzleWidget):
|
||||
yield Markdown(item)
|
||||
|
||||
with Center() as c:
|
||||
with Button(_("I know this"), id="ok") as b:
|
||||
with Button("我已知晓", id="ok") as b:
|
||||
b.focus()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
|
||||
@@ -44,21 +44,38 @@ 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
|
||||
|
||||
@@ -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("No former FSRS Scheduler file founded, creating new instance")
|
||||
logger.warning("FSRS Scheduler 状态文件加载失败, 创建新实例")
|
||||
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.error("Failed to persist FSRS Scheduler state")
|
||||
logger.exception("FSRS Scheduler 状态保存失败")
|
||||
|
||||
|
||||
def _feedback_to_rating(feedback: int) -> Rating:
|
||||
@@ -176,9 +176,14 @@ 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, update skipped")
|
||||
logger.debug("feedback 为 -1, 跳过更新")
|
||||
return
|
||||
|
||||
scheduler = _get_global_scheduler()
|
||||
@@ -186,7 +191,7 @@ class FSRSAlgorithm(BaseAlgorithm):
|
||||
|
||||
if is_new_activation:
|
||||
card = Card()
|
||||
logger.debug("New activation, create new Card")
|
||||
logger.debug("新激活, 创建新 Card")
|
||||
else:
|
||||
card = cls._algodata_to_card(algodata)
|
||||
|
||||
@@ -201,7 +206,7 @@ class FSRSAlgorithm(BaseAlgorithm):
|
||||
algodata[cls.algo_name]["rept"] += 1
|
||||
|
||||
logger.debug(
|
||||
"FSRS.revisor finished: stability=%s, difficulty=%s, state=%s, " "next_date=%d",
|
||||
"FSRS.revisor 完成: stability=%s, difficulty=%s, state=%s, " "next_date=%d",
|
||||
card.stability,
|
||||
card.difficulty,
|
||||
card.state,
|
||||
|
||||
@@ -51,7 +51,7 @@ class NSP0Algorithm(BaseAlgorithm):
|
||||
)
|
||||
|
||||
if feedback == -1:
|
||||
logger.debug("feedback = -1, update skipped")
|
||||
logger.debug("feedback 为 -1, 跳过更新")
|
||||
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(
|
||||
"Update date: last_date=%d, next_date=%d, last_modify=%f",
|
||||
"更新日期: 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"],
|
||||
|
||||
@@ -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("Failed to load SM-15M global state file, creating new instance")
|
||||
logger.warning("SM-15M 全局状态文件加载失败, 创建新实例")
|
||||
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.error("Failed to save SM-15M global state")
|
||||
logger.exception("SM-15M 全局状态保存失败")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -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"],
|
||||
|
||||
@@ -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, update skipped")
|
||||
logger.debug("feedback 为 -1, 跳过更新")
|
||||
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("Update efactor: %f", algodata[cls.algo_name]["efactor"])
|
||||
logger.debug("更新 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("Increase rept: %d", algodata[cls.algo_name]["rept"])
|
||||
logger.debug("递增 rept: %d", algodata[cls.algo_name]["rept"])
|
||||
|
||||
algodata[cls.algo_name]["real_rept"] += 1
|
||||
logger.debug("Increase real_rept: %d", algodata[cls.algo_name]["real_rept"])
|
||||
logger.debug("递增 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("New activation, reset rept and efactor")
|
||||
logger.debug("新激活, 重置 rept 和 efactor")
|
||||
|
||||
if algodata[cls.algo_name]["rept"] == 0:
|
||||
algodata[cls.algo_name]["interval"] = 1
|
||||
logger.debug("rept=0, set interval=1")
|
||||
logger.debug("rept=0, 设置 interval=1")
|
||||
elif algodata[cls.algo_name]["rept"] == 1:
|
||||
algodata[cls.algo_name]["interval"] = 6
|
||||
logger.debug("rept=1, set interval=6")
|
||||
logger.debug("rept=1, 设置 interval=6")
|
||||
else:
|
||||
algodata[cls.algo_name]["interval"] = round(
|
||||
algodata[cls.algo_name]["interval"] * algodata[cls.algo_name]["efactor"]
|
||||
)
|
||||
logger.debug(
|
||||
"rept>1, providing interval: %d", algodata[cls.algo_name]["interval"]
|
||||
"rept>1, 计算 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(
|
||||
"Update date: last_date=%d, next_date=%d, last_modify=%f",
|
||||
"更新日期: 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"],
|
||||
|
||||
@@ -63,7 +63,7 @@ class Atom:
|
||||
)
|
||||
|
||||
def lock(self, locked=-1):
|
||||
logger.debug(f"Lock atom: {locked}")
|
||||
logger.debug(f"锁定参数 {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"Rating allowed: {self.registry['runtime']['min_rate']}")
|
||||
logger.debug(f"允许总评分: {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("Rating disallowed")
|
||||
logger.debug("禁止总评分")
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.registry[key]
|
||||
|
||||
@@ -8,7 +8,9 @@ class BasePuzzle:
|
||||
"""谜题基类"""
|
||||
|
||||
def refresh(self):
|
||||
raise NotImplementedError("Method refresh not implemented")
|
||||
logger.debug("BasePuzzle.refresh 被调用(未实现)")
|
||||
raise NotImplementedError("谜题对象未实现 refresh 方法")
|
||||
|
||||
def __str__(self):
|
||||
return f"Puzzle: {type(self).__name__}"
|
||||
logger.debug("BasePuzzle.__str__ 被调用")
|
||||
return f"谜题: {type(self).__name__}"
|
||||
|
||||
@@ -27,20 +27,20 @@ class ClozePuzzle(BasePuzzle):
|
||||
self.wording = "填空题 - 尚未刷新谜题"
|
||||
self.answer = ["填空题 - 尚未刷新谜题"]
|
||||
self.delimiter = delimiter
|
||||
logger.debug("ClozePuzzle inited")
|
||||
logger.debug("ClozePuzzle 初始化完成")
|
||||
|
||||
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: nothing to proceed")
|
||||
logger.warning("ClozePuzzle.refresh: 无单词可处理")
|
||||
return
|
||||
words = [word for word in words if word]
|
||||
logger.debug("ClozePuzzle.refresh: %d words splited", len(words))
|
||||
logger.debug("ClozePuzzle.refresh: 分割出 %d 个单词", len(words))
|
||||
num_blanks = min(max(1, len(words) // self.min_denominator), len(words))
|
||||
logger.debug("ClozePuzzle.refresh: %d blank required", num_blanks)
|
||||
logger.debug("ClozePuzzle.refresh: 需要生成 %d 个填空", num_blanks)
|
||||
indices_to_blank = random.sample(range(len(words)), num_blanks)
|
||||
indices_to_blank.sort()
|
||||
blanked_words = list(words)
|
||||
@@ -50,7 +50,8 @@ class ClozePuzzle(BasePuzzle):
|
||||
answer.append(words[index])
|
||||
self.answer = answer
|
||||
self.wording = "".join(blanked_words)
|
||||
logger.debug("ClozePuzzle.refresh, %d blanks generated", len(answer))
|
||||
logger.debug("ClozePuzzle.refresh 完成, 生成 %d 个填空", len(answer))
|
||||
|
||||
def __str__(self):
|
||||
logger.debug("ClozePuzzle.__str__ 被调用")
|
||||
return f"{self.wording}\n{str(self.answer)}"
|
||||
|
||||
@@ -65,8 +65,8 @@ class MCQPuzzle(BasePuzzle):
|
||||
jammer: 传入的干扰项列表
|
||||
"""
|
||||
# 合并正确答案和传入的干扰项, 并去重
|
||||
logger.debug(f"Answer table: {self.mapping}, {type(self.mapping)}")
|
||||
logger.debug(f"Jammers: {jammer}, {type(jammer)}")
|
||||
logger.debug(f"答案映射: {self.mapping}, {type(self.mapping)}")
|
||||
logger.debug(f"干扰项: {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
|
||||
|
||||
@@ -11,7 +11,8 @@ class RecognitionPuzzle(BasePuzzle):
|
||||
"""识别占位符"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
logger.debug("RecognitionPuzzle.__init__")
|
||||
super().__init__()
|
||||
|
||||
def refresh(self):
|
||||
pass
|
||||
logger.debug("RecognitionPuzzle.refresh(空实现)")
|
||||
|
||||
@@ -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"Process: {item}")
|
||||
logger.debug(f"开始处理: {item}")
|
||||
|
||||
puzzle = puz.puzzles[orbital_puzzles[item]["__origin__"]]
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ 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
|
||||
@@ -46,48 +52,65 @@ class Procession(Machine):
|
||||
initial=ProcessionState.ACTIVE.value,
|
||||
)
|
||||
|
||||
logger.debug("Procession 初始化完成, 队列长度=%d", len(self.atoms))
|
||||
|
||||
def on_active(self):
|
||||
"""进入active状态时的回调"""
|
||||
pass
|
||||
logger.debug("Procession 进入 active 状态")
|
||||
|
||||
def on_finished(self):
|
||||
"""进入FINISHED状态时的回调"""
|
||||
pass
|
||||
logger.debug("Procession 进入 FINISHED 状态")
|
||||
|
||||
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):
|
||||
|
||||
@@ -14,7 +14,7 @@ class Router(Machine):
|
||||
"""全局调度阶段路由器"""
|
||||
|
||||
def __init__(self, atoms: list[pt.Atom]) -> None:
|
||||
logger.debug(f"number of atoms={len(atoms)}")
|
||||
logger.debug(f"Router.__init__: 原子数量={len(atoms)}")
|
||||
|
||||
self.atoms = atoms
|
||||
new_atoms = list()
|
||||
@@ -26,7 +26,7 @@ class Router(Machine):
|
||||
else:
|
||||
old_atoms.append(i)
|
||||
|
||||
logger.debug(f"number of new atoms={len(new_atoms)}, number of old atoms={len(old_atoms)}")
|
||||
logger.debug(f"新原子数量={len(new_atoms)}, 旧原子数量={len(old_atoms)}")
|
||||
|
||||
self.processions = list()
|
||||
"""路由中的所有队列"""
|
||||
@@ -35,14 +35,17 @@ 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("Router inited, number of processions =%d", len(self.processions))
|
||||
logger.debug("创建总体复习 Procession")
|
||||
logger.debug("Router 初始化完成, processions 数量=%d", len(self.processions))
|
||||
|
||||
# 设置transitions状态机
|
||||
states = [
|
||||
@@ -88,27 +91,29 @@ class Router(Machine):
|
||||
|
||||
def on_unsure(self):
|
||||
"""进入UNSURE状态时的回调"""
|
||||
pass
|
||||
logger.debug("Router 进入 UNSURE 状态")
|
||||
|
||||
def on_quick_review(self):
|
||||
"""进入QUICK_REVIEW状态时的回调"""
|
||||
pass
|
||||
logger.debug("Router 进入 QUICK_REVIEW 状态")
|
||||
|
||||
def on_recognition(self):
|
||||
"""进入RECOGNITION状态时的回调"""
|
||||
pass
|
||||
logger.debug("Router 进入 RECOGNITION 状态")
|
||||
|
||||
def on_final_review(self):
|
||||
"""进入FINAL_REVIEW状态时的回调"""
|
||||
pass
|
||||
logger.debug("Router 进入 FINAL_REVIEW 状态")
|
||||
|
||||
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:
|
||||
@@ -120,10 +125,12 @@ 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"):
|
||||
|
||||
@@ -20,4 +20,7 @@ class ProcessionState(Enum):
|
||||
|
||||
class ExpanderState(Enum):
|
||||
EXAMMODE = "exammode"
|
||||
RETRONLY = "retronly"
|
||||
RETRONLY = "retronly"
|
||||
|
||||
|
||||
logger.debug("状态枚举定义已加载")
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[General]
|
||||
LangCode=zh_CN
|
||||
TargetLangCode=zh_CN
|
||||
Binary file not shown.
@@ -1,670 +0,0 @@
|
||||
# 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 "内蔵 UI(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 "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.
@@ -1,669 +0,0 @@
|
||||
# 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 "Кнопка"
|
||||
@@ -1,7 +0,0 @@
|
||||
<?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.
@@ -1,679 +0,0 @@
|
||||
# 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 "示例按钮"
|
||||
@@ -11,4 +11,4 @@ __all__ = [
|
||||
]
|
||||
|
||||
providers = {"termux": termux_audio, "playsound": playsound_audio}
|
||||
logger.debug("Audio providers registered: %s", list(providers.keys()))
|
||||
logger.debug("音频 providers 已注册: %s", list(providers.keys()))
|
||||
|
||||
@@ -11,10 +11,10 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
def play_by_path(path: pathlib.Path):
|
||||
logger.debug("playsound_audio.play_by_path: playing %s", path)
|
||||
logger.debug("playsound_audio.play_by_path: 开始播放 %s", path)
|
||||
try:
|
||||
import playsound3
|
||||
playsound3.playsound(str(path))
|
||||
logger.debug("Audio playing finished: %s", path)
|
||||
logger.debug("播放完成: %s", path)
|
||||
except Exception as e:
|
||||
logger.error("Failed to play: %s, error: %s", path, e)
|
||||
logger.error("播放失败: %s, 错误: %s", path, e)
|
||||
|
||||
@@ -13,9 +13,9 @@ logger = get_logger(__name__)
|
||||
# from .protocol import PlayFunctionProtocol
|
||||
|
||||
def play_by_path(path: pathlib.Path):
|
||||
logger.debug("termux_audio.play_by_path: playing %s", path)
|
||||
logger.debug("termux_audio.play_by_path: 开始播放 %s", path)
|
||||
try:
|
||||
os.system(f"play-audio {path.resolve()}")
|
||||
logger.debug("Play audio: %s", path)
|
||||
logger.debug("播放命令已执行: %s", path)
|
||||
except Exception as e:
|
||||
logger.error("Failed to play audio: %s, error: %s", path, e)
|
||||
logger.error("播放失败: %s, 错误: %s", path, e)
|
||||
|
||||
@@ -16,4 +16,4 @@ providers = {
|
||||
"openai": OpenAILLM,
|
||||
}
|
||||
|
||||
logger.debug("LLM providers registered: %s", list(providers.keys()))
|
||||
logger.debug("LLM providers 已注册: %s", list(providers.keys()))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""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 未实现流式功能"
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""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
|
||||
@@ -15,4 +15,4 @@ providers = {
|
||||
"edgetts": EdgeTTS,
|
||||
}
|
||||
|
||||
logger.debug("TTS providers registered: %s", list(providers.keys()))
|
||||
logger.debug("TTS providers 已注册: %s", list(providers.keys()))
|
||||
|
||||
@@ -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 is not a functional implementation")
|
||||
logger.warning("BaseTTS.convert 是基类方法, 未实现具体功能")
|
||||
return path # type: ignore
|
||||
|
||||
@@ -21,10 +21,10 @@ class EdgeTTS(BaseTTS):
|
||||
text,
|
||||
config_var.get()["providers"]["tts"]["edgetts"]["voice"],
|
||||
)
|
||||
logger.debug("EdgeTTS object created, saving audio")
|
||||
logger.debug("EdgeTTS 通信对象创建成功, 正在保存音频")
|
||||
communicate.save_sync(str(path))
|
||||
logger.debug("EdgeTTS audio saved as %s", path)
|
||||
logger.debug("EdgeTTS 音频已保存到: %s", path)
|
||||
return path # type: ignore
|
||||
except Exception as e:
|
||||
logger.error("EdgeTTS.convert failed: %s", e)
|
||||
logger.error("EdgeTTS.convert 失败: %s", e)
|
||||
raise
|
||||
|
||||
@@ -11,7 +11,7 @@ logger = get_logger(__name__)
|
||||
play_by_path: Callable = prov[
|
||||
config_var.get()["services"]["audio"]["provider"]
|
||||
].play_by_path
|
||||
logger.info(
|
||||
"TTS Service inited, using provider %s",
|
||||
logger.debug(
|
||||
"音频服务初始化完成, 使用 Provider: %s",
|
||||
config_var.get()["services"]["audio"]["provider"],
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ class ConfigDict(UserDict):
|
||||
if i.suffix == ".toml":
|
||||
self.data[i.stem] = i
|
||||
else:
|
||||
logger.error(f"Illegal file detected in config: {i.stem}") # what's up bro
|
||||
logger.debug(f"配置目录中有无效的文件 {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.info("Data persisted")
|
||||
logger.debug("完成配置持久化")
|
||||
return
|
||||
|
||||
with open(self.path, "w+") as f:
|
||||
|
||||
@@ -19,12 +19,14 @@ def epath(
|
||||
path = path.lstrip(".")
|
||||
target = dct
|
||||
keys = path.split(".")
|
||||
logger.debug(f"Proceeding EPATH {path}, {new_value}")
|
||||
logger.debug(f"处理 EPATH {path}, {new_value}")
|
||||
for idx, i in enumerate(keys):
|
||||
is_last = idx == len(keys) - 1
|
||||
|
||||
# 处理字典键
|
||||
logger.debug(f"Proceeding in detail: {i}, {(isinstance(target, dict) or isinstance(target, ConfigDict))} {i in target}")
|
||||
logger.debug(
|
||||
f"处理 {i}, {(isinstance(target, dict) or isinstance(target, ConfigDict))} {i in target}"
|
||||
)
|
||||
|
||||
if is_last and enable_modify:
|
||||
# 最后一次循环执行修改
|
||||
|
||||
@@ -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.info("Finished loading favlist, %d items in total", len(self._favorites))
|
||||
logger.debug("收藏列表加载成功, 共 %d 项", len(self._favorites))
|
||||
except Exception as e:
|
||||
logger.error("Filed to load favlist: %s", e)
|
||||
logger.error("加载收藏列表失败: %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.info("Finished saving favlist, %d items in total", len(self._favorites))
|
||||
logger.debug("收藏列表保存成功, 共 %d 项", len(self._favorites))
|
||||
except Exception as e:
|
||||
logger.error("Failed to save favlist: %s", e)
|
||||
logger.error("保存收藏列表失败: %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.info("Favitem already exists: %s/%s", repo_path, ident)
|
||||
logger.debug("收藏已存在: %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("Add favitem: %s/%s", repo_path, ident)
|
||||
logger.info("添加收藏: %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("Remove favitem: %s/%s", repo_path, ident)
|
||||
logger.info("移除收藏: %s/%s", repo_path, ident)
|
||||
return True
|
||||
logger.error("Non-existed favitem: %s/%s", repo_path, ident)
|
||||
logger.debug("收藏不存在: %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("Clear favlist")
|
||||
logger.info("清空收藏列表")
|
||||
|
||||
def count(self) -> int:
|
||||
"""收藏总数"""
|
||||
|
||||
@@ -6,11 +6,15 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_md5(text):
|
||||
logger.debug(f"MD5 hash input`{text}`")
|
||||
logger.debug(f"计算MD5哈希, 输入`{text}`")
|
||||
result = hashlib.md5(text.encode("utf-8")).hexdigest()
|
||||
logger.debug("Providing MD5 hash: %s...", result[:8])
|
||||
logger.debug("哈希结果: %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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""日志服务模块
|
||||
|
||||
基于 logging 库, 提供统一日志记录功能
|
||||
"""
|
||||
HeurAMS 日志服务模块
|
||||
基于Python标准logging库, 提供统一的日志记录功能
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -8,24 +8,26 @@ 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 - [%(filename)s:%(lineno)d:%(funcName)s] - %(message)s"
|
||||
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)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 = 16 * 1024 * 1024, # 16MB
|
||||
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
||||
backup_count: int = 5,
|
||||
) -> None:
|
||||
"""
|
||||
设置全局日志服务
|
||||
配置全局日志系统
|
||||
|
||||
Args:
|
||||
log_file: 日志文件路径
|
||||
@@ -42,7 +44,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,
|
||||
@@ -52,6 +54,7 @@ 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
|
||||
|
||||
@@ -59,28 +62,40 @@ def setup_logging(
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
# 创建 heurams logger 并单独设置 DEBUG 级别
|
||||
# 创建自己的应用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.debug("HeurAMS logger inited, path: %s", log_path.resolve())
|
||||
app_logger.info("日志系统已初始化, 日志文件: %s", log_path)
|
||||
|
||||
|
||||
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()
|
||||
@@ -91,12 +106,49 @@ 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]
|
||||
|
||||
# 初始化日志系统
|
||||
setup_logging()
|
||||
|
||||
# 便捷函数
|
||||
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日志服务模块已加载")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""时间服务
|
||||
"""
|
||||
# 时间服务
|
||||
import datetime
|
||||
import time
|
||||
|
||||
@@ -8,40 +7,32 @@ 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:
|
||||
"""获取当前日戳(以天为单位的整数时间戳)"""
|
||||
if daystamp_override != -1:
|
||||
logger.debug("Daystamp overrode: %d", daystamp_override)
|
||||
return int(daystamp_override)
|
||||
time_override = config_var.get()["services"]["timer"]["daystamp_override"]
|
||||
if time_override != -1:
|
||||
logger.debug("使用覆盖的日戳: %d", time_override)
|
||||
return int(time_override)
|
||||
|
||||
result = int(
|
||||
(time.time() + config_var.get()["services"]["timer"]["timezone_offset"])
|
||||
// (24 * 3600)
|
||||
)
|
||||
global last_daystamp
|
||||
if last_daystamp != result: # 用于避免日志泛洪
|
||||
logger.debug("Providing new daystamp: %d", result)
|
||||
last_daystamp = result
|
||||
logger.debug("计算日戳: %d", result)
|
||||
return result
|
||||
|
||||
|
||||
def get_timestamp() -> float:
|
||||
"""获取 UNIX 时间戳"""
|
||||
# 搞这个函数的原因是要支持可复现操作
|
||||
if timestamp_override != -1:
|
||||
logger.debug("Timestamp overrode: %f", timestamp_override)
|
||||
return float(timestamp_override)
|
||||
time_override = config_var.get()["services"]["timer"]["timestamp_override"]
|
||||
if time_override != -1:
|
||||
logger.debug("使用覆盖的时间戳: %f", time_override)
|
||||
return float(time_override)
|
||||
|
||||
result = time.time()
|
||||
global last_timestamp
|
||||
if last_timestamp != result:
|
||||
logger.debug("Providing new timestamp: %d", result)
|
||||
last_timestamp = result
|
||||
logger.debug("获取当前时间戳: %f", result)
|
||||
return result
|
||||
|
||||
|
||||
@@ -58,9 +49,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 的 datetime (naive 视为 UTC)
|
||||
接受带时区或 naive 的 datetime(naive 视为 UTC)。
|
||||
"""
|
||||
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
||||
if dt.tzinfo is None:
|
||||
@@ -70,5 +61,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)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""文本转语音服务
|
||||
"""
|
||||
# 文本转语音服务
|
||||
from typing import Callable
|
||||
|
||||
from heurams.context import config_var
|
||||
@@ -9,7 +8,7 @@ from heurams.services.logger import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
convertor: Callable = prov[config_var.get()["services"]["tts"]["provider"]].convert
|
||||
logger.info(
|
||||
"TTS Service inited, using provider: %s",
|
||||
logger.debug(
|
||||
"TTS 服务初始化完成, 使用 provider: %s",
|
||||
config_var.get()["services"]["tts"]["provider"],
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""版本服务
|
||||
"""
|
||||
# 版本控制集成服务
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -7,4 +6,6 @@ logger = get_logger(__name__)
|
||||
ver = "0.5.1"
|
||||
stage = "stable"
|
||||
codename = "fulcrum"
|
||||
codename_cn = "支点"
|
||||
codename_cn = "支点"
|
||||
|
||||
logger.info("HeurAMS 版本: %s (%s), 阶段: %s", ver, codename, stage)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
"""unifront - HeurAMS API 前端模块"""
|
||||
|
||||
from .server import create_app
|
||||
from .session import Session
|
||||
|
||||
__all__ = ["create_app", "Session"]
|
||||
@@ -1,92 +0,0 @@
|
||||
"""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
|
||||
@@ -1,9 +0,0 @@
|
||||
"""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"]
|
||||
@@ -1,192 +0,0 @@
|
||||
"""原子信息 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)
|
||||
@@ -1,99 +0,0 @@
|
||||
"""配置查询与修改 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}
|
||||
@@ -1,196 +0,0 @@
|
||||
"""收藏夹、系统信息、缓存管理与分析统计 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,
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
"""仓库信息 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"],
|
||||
)
|
||||
@@ -1,123 +0,0 @@
|
||||
"""复习 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)
|
||||
@@ -1,67 +0,0 @@
|
||||
"""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
|
||||
@@ -1,71 +0,0 @@
|
||||
"""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
|
||||
@@ -1,243 +1,5 @@
|
||||
"""复习会话管理"""
|
||||
|
||||
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:
|
||||
"""管理单次复习会话的生命周期"""
|
||||
|
||||
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)
|
||||
pass
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
/* 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) + ' · ' + 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) + ' · ' + esc(r.package) + '<br>算法: ' + esc(r.algorithm) + ' · 路径: ' + 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) + ' · ' + 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>'; } }
|
||||
@@ -1,168 +0,0 @@
|
||||
<!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()">≡</button>
|
||||
<span id="nav-title">Dashboard</span>
|
||||
<span id="nav-version"></span>
|
||||
<button id="theme-btn" onclick="toggleTheme()" class="btn-icon">☼</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()">☆</button>
|
||||
<button id="tts-btn" class="btn-link" onclick="playTTS()" title="Read Aloud">▶</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">✓</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
-1
File diff suppressed because one or more lines are too long
@@ -1,153 +0,0 @@
|
||||
/* 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; }
|
||||
}
|
||||
@@ -109,37 +109,6 @@ 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"
|
||||
@@ -335,22 +304,6 @@ 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"
|
||||
@@ -452,26 +405,15 @@ 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.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "tabulate" },
|
||||
{ name = "toml" },
|
||||
{ name = "transitions" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -511,16 +453,10 @@ 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'" },
|
||||
@@ -539,71 +475,10 @@ 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"
|
||||
@@ -915,96 +790,6 @@ 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"
|
||||
@@ -1044,15 +829,6 @@ 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"
|
||||
@@ -1069,52 +845,6 @@ 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"
|
||||
@@ -1180,19 +910,6 @@ 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"
|
||||
@@ -1249,18 +966,6 @@ 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"
|
||||
@@ -1270,193 +975,6 @@ 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"
|
||||
|
||||
Reference in New Issue
Block a user