style: 格式化代码

This commit is contained in:
2026-05-07 19:48:07 +08:00
parent fcda88488b
commit 048e74ad7f
19 changed files with 164 additions and 132 deletions

View File

@@ -1,12 +1,12 @@
# AI 编程工具说明 # AI 编程工具说明
本文档为 AI 工具以及在使用 AI 辅助向 HeurAMS 项目贡献代码的开发者提供指导, 一般而言此文件会被自动读入多种 AI 工具的上下文. 本文档为 AI 工具以及在使用 AI 辅助向 HeurAMS 项目贡献代码的开发者提供指导, 一般而言此文件会被自动读入多种 AI 工具的上下文.
AI 工具应当完整阅读此 `/AGENTS.md` 文件. AI 工具应当完整阅读此 `/AGENTS.md` 文件.
## 查阅开发文档 ## 查阅开发文档
在帮助进行 HeurAMS 开发时AI 工具应遵循标准的开发规范与流程, 应当自动查看或是在用户发出"初始化/init"指令后查看: 在帮助进行 HeurAMS 开发时AI 工具应遵循标准的开发规范与流程, 应当自动查看或是在用户发出"初始化/init"指令后查看:
- [贡献指南](/CONTRIBUTING.md) - [贡献指南](/CONTRIBUTING.md)
- [自述文件](/README.md) - [自述文件](/README.md)
@@ -26,22 +26,22 @@ AI 工具应当完整阅读此 `/AGENTS.md` 文件.
## 许可证与法律要求 ## 许可证与法律要求
所有贡献必须符合许可要求, 所有代码必须与 AGPL-3.0-or-later 许可以及项目附加豁免条款(位于 LICENSE 文件尾部 237 至 245 行)兼容. 所有贡献必须符合许可要求, 所有代码必须与 AGPL-3.0-or-later 许可以及项目附加豁免条款(位于 LICENSE 文件尾部 237 至 245 行)兼容.
## Signed-off-by 与 DCO ## Signed-off-by 与 DCO
AI 代理**严禁添加** Signed-off-by 标签. AI 代理**严禁添加** Signed-off-by 标签.
只有人类能够合法地认证 DCO. 只有人类能够合法地认证 DCO.
人类提交者负责: 人类提交者负责:
- 审阅所有 AI 生成的代码 - 审阅所有 AI 生成的代码
- 确保符合许可要求 - 确保符合许可要求
- 添加自己的 Signed-off-by 标签以认证 DCO - 添加自己的 Signed-off-by 标签以认证 DCO
- 对贡献负责任 - 对贡献负责任
AI 助手负责: AI 助手负责:
- 了解运行环境, 例如操作系统或具体发行版 - 了解运行环境, 例如操作系统或具体发行版
- 遵循此文档所述规则 - 遵循此文档所述规则

View File

@@ -3,7 +3,7 @@
欢迎为此项目做出贡献! 欢迎为此项目做出贡献!
> [!NOTE] > [!NOTE]
> 我们已经开始着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目 > 我们已经开始着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
> 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS 和 Plasma Mobile 提供现代用户界面 > 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS 和 Plasma Mobile 提供现代用户界面
> 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发 > 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发
@@ -81,6 +81,7 @@ python3 -m heurams.interface # 启动 TUI
贡献者拥有其贡献部分的版权同意其贡献将在 AGPL-3.0 许可证(包括附加的本机 API 调用豁免条款)下发布. 贡献者拥有其贡献部分的版权同意其贡献将在 AGPL-3.0 许可证(包括附加的本机 API 调用豁免条款)下发布.
如有以下情况, 请在 PR 描述中注明: 如有以下情况, 请在 PR 描述中注明:
- 如果需要引入其他开源 vendor - 如果需要引入其他开源 vendor
- 如果需要引入其他专有的网络服务(例如当前项目中的 edgetts) - 如果需要引入其他专有的网络服务(例如当前项目中的 edgetts)
- 如果需要升级某个依赖或运行环境的版本 - 如果需要升级某个依赖或运行环境的版本
@@ -96,7 +97,7 @@ HeurAMS 被设计为一个可独立于前端的程序库, 这意味着:
- 您还可以在自己的项目中以独立进程/服务调用 HeurAMS, 根据 AGPL-3.0 及本项目的附加许可条款, 如果调用发生在同一主机上且不涉及外部网络转发, 则可豁免许可证规定的特定义务而免于受 AGPL-3.0 "污染". 为了这点, 我们正在完善可选择启用的跨进程 RPC 模块, 这将成为潜进内核的跨平台标准件. - 您还可以在自己的项目中以独立进程/服务调用 HeurAMS, 根据 AGPL-3.0 及本项目的附加许可条款, 如果调用发生在同一主机上且不涉及外部网络转发, 则可豁免许可证规定的特定义务而免于受 AGPL-3.0 "污染". 为了这点, 我们正在完善可选择启用的跨进程 RPC 模块, 这将成为潜进内核的跨平台标准件.
- 如果您通过独立进程/服务调用方式开发了另外的软件, 开源但不愿使用 AGPL-3.0/GPL-3.0 许可证, 也可以联系我们, 我们乐于将您的项目链接添加到友链中 - 如果您通过独立进程/服务调用方式开发了另外的软件, 开源但不愿使用 AGPL-3.0/GPL-3.0 许可证, 也可以联系我们, 我们乐于将您的项目链接添加到友链中
## 软件开发之外的贡献 ## 软件开发之外的贡献
即使您不是软件开发人员, 我们也欢迎您加入贡献! 即使您不是软件开发人员, 我们也欢迎您加入贡献!

87
FAQ.md
View File

@@ -2,15 +2,15 @@
## 什么是终端模拟器? ## 什么是终端模拟器?
终端模拟器是在图形桌面环境中模拟并使用终端的应用程序, 例如 KDE Konsole, GNOME Terminal, Windows Terminal, iTerm2 等. 终端模拟器是在图形桌面环境中模拟并使用终端的应用程序, 例如 KDE Konsole, GNOME Terminal, Windows Terminal, iTerm2 等.
较旧 Windows 的那个很寒酸的小黑窗口也是终端模拟器(conhost.exe), 但它对此软件基本用户界面(以及一切现代终端应用)支持不佳, 建议在 Windows 平台使用 WezTerm (支持 sixel) 或 Windows Terminal (不支持 sixel). 较旧 Windows 的那个很寒酸的小黑窗口也是终端模拟器(conhost.exe), 但它对此软件基本用户界面(以及一切现代终端应用)支持不佳, 建议在 Windows 平台使用 WezTerm (支持 sixel) 或 Windows Terminal (不支持 sixel).
## 软件支持移动设备吗? ## 软件支持移动设备吗?
基本用户界面 (Textual TUI) 可在 Android Termux 中良好运行. 基本用户界面 (Textual TUI) 可在 Android Termux 中良好运行.
此外, 正在开发的 KiriMemo 前端基于 KDE Kirigami 框架, 将原生支持 Android 和 iOS. 此外, 正在开发的 KiriMemo 前端基于 KDE Kirigami 框架, 将原生支持 Android 和 iOS.
## HeurAMS 和 Anki 有什么区别? ## HeurAMS 和 Anki 有什么区别?
@@ -29,13 +29,13 @@
## 软件是免费的吗? ## 软件是免费的吗?
是的, 完全免费, 且开源. 您无需支付任何费用即可使用全部功能. 是的, 完全免费, 且开源. 您无需支付任何费用即可使用全部功能.
## 黑乎乎的这个界面我怎么用? ## 黑乎乎的这个界面我怎么用?
得益于微软几十年对用户进行的"命令行即落后"教育, 以及 `conhost.exe``cmd.exe` 的糟糕体验, 您对终端用户界面感到不适应是完全正常的. 得益于微软几十年对用户进行的"命令行即落后"教育, 以及 `conhost.exe``cmd.exe` 的糟糕体验, 您对终端用户界面感到不适应是完全正常的.
但实际上, 虽然看起来像老式电脑屏幕, Textual 和终端标准其实比您想象得要现代一些. 但实际上, 虽然看起来像老式电脑屏幕, Textual 和终端标准其实比您想象得要现代一些.
### 可以用鼠标 ### 可以用鼠标
@@ -44,6 +44,7 @@
所以可能和您的想象不同, 您事实上可以直接用鼠标点击按钮, 就像使用普通软件一样. 所以可能和您的想象不同, 您事实上可以直接用鼠标点击按钮, 就像使用普通软件一样.
### 也可以用键盘 ### 也可以用键盘
- `Tab` 键在不同区域之间切换焦点 - `Tab` 键在不同区域之间切换焦点
- `方向键` 在列表中上下移动 - `方向键` 在列表中上下移动
- `Enter` 确认选择 - `Enter` 确认选择
@@ -51,6 +52,7 @@
- 屏幕上会有按键提示, 例如 `[n] 导航器` 表示按 `n` 键打开导航器 - 屏幕上会有按键提示, 例如 `[n] 导航器` 表示按 `n` 键打开导航器
### 触屏也可以 ### 触屏也可以
在平板或手机 Termux 中, 您可以触摸或者滑动屏幕操作. 在平板或手机 Termux 中, 您可以触摸或者滑动屏幕操作.
## 我怎么启动这个软件? ## 我怎么启动这个软件?
@@ -58,6 +60,7 @@
首先需要确保系统中安装了 Python (推荐 3.12.13 版本) 并安装了 HeurAMS 的所需组件. 首先需要确保系统中安装了 Python (推荐 3.12.13 版本) 并安装了 HeurAMS 的所需组件.
### Windows ### Windows
打开"命令提示符"或"PowerShell", 输入以下命令后按回车, 或者把这玩意另存为快捷方式: 打开"命令提示符"或"PowerShell", 输入以下命令后按回车, 或者把这玩意另存为快捷方式:
``` ```
@@ -65,22 +68,24 @@ python -m heurams.interface
``` ```
### macOS ### macOS
打开"终端"应用程序, 输入以上命令. 打开"终端"应用程序, 输入以上命令.
### Linux ### Linux
打开您的终端模拟器 (一般是按 Ctrl + Alt + T), 输入以上命令. 打开您的终端模拟器 (一般是按 Ctrl + Alt + T), 输入以上命令.
如果您觉得每次输入命令太麻烦, 可以创建一个桌面快捷方式或脚本文件, 详见网上的相关教程. 如果您觉得每次输入命令太麻烦, 可以创建一个桌面快捷方式或脚本文件, 详见网上的相关教程.
## 我怎么退出软件? ## 我怎么退出软件?
按键盘上的 `q` 键返回主界面后退出. 按键盘上的 `q` 键返回主界面后退出.
您的学习进度会自动保存, 不会丢失. 您的学习进度会自动保存, 不会丢失.
## 我看不到图片怎么办? ## 我看不到图片怎么办?
终端模拟器需要支持 sixel 图像协议才能显示图片. 终端模拟器需要支持 sixel 图像协议才能显示图片.
- WezTerm (适用于几乎所有操作系统): 支持 - WezTerm (适用于几乎所有操作系统): 支持
- KDE Konsole: 支持 - KDE Konsole: 支持
@@ -124,15 +129,15 @@ python -m heurams.interface
## 字体太小/太大怎么办? ## 字体太小/太大怎么办?
在您的终端模拟器设置中找到"字体大小"选项进行调整. 在您的终端模拟器设置中找到"字体大小"选项进行调整.
软件会跟随终端的字体设置. 软件会跟随终端的字体设置.
## 为什么我的界面和截图不一样? ## 为什么我的界面和截图不一样?
截图使用的是 KDE Plasma 桌面上的 Konsole, 80x25 字符尺寸, Cascadia Code 和 Noto Sans SC 字体. 截图使用的是 KDE Plasma 桌面上的 Konsole, 80x25 字符尺寸, Cascadia Code 和 Noto Sans SC 字体.
如果您的终端尺寸更大, 界面会更宽裕; 如果使用不同字体或者不同操作系统, 视觉效果会略有差异. 如果您的终端尺寸更大, 界面会更宽裕; 如果使用不同字体或者不同操作系统, 视觉效果会略有差异.
功能上完全一致. 功能上完全一致.
@@ -144,9 +149,9 @@ python -m heurams.interface
并且这种方式于学术研究与实验不利, 用户自评分产生的数据是不可靠的. 并且这种方式于学术研究与实验不利, 用户自评分产生的数据是不可靠的.
因此 HeurAMS 的前端内建了基于用户行为分析的自动评分系统, 也就是"谜题". 因此 HeurAMS 的前端内建了基于用户行为分析的自动评分系统, 也就是"谜题".
它会根据题目本身难度和您的答题行为(包括但不限于正确性, 操作回退次数, 有效答题时间)并自动为您评分. 它会根据题目本身难度和您的答题行为(包括但不限于正确性, 操作回退次数, 有效答题时间)并自动为您评分.
但如果您或者某个单元集选择使用 `basic_puzzle`, 或者打算自己实现自动评分系统, 分数含义如下: 但如果您或者某个单元集选择使用 `basic_puzzle`, 或者打算自己实现自动评分系统, 分数含义如下:
@@ -170,7 +175,7 @@ python -m heurams.interface
## 我每天都要打开软件吗? 不学会怎样? ## 我每天都要打开软件吗? 不学会怎样?
理论上不需要每天打开. 软件会自动记录每个知识点下次该复习的时间. 理论上不需要每天打开. 软件会自动记录每个知识点下次该复习的时间.
但建议您每天打开软件看下状态. 但建议您每天打开软件看下状态.
@@ -183,9 +188,9 @@ python -m heurams.interface
## 能同时学多个科目吗? ## 能同时学多个科目吗?
可以. 可以.
每个科目或课程可以做成独立的"单元集". 每个科目或课程可以做成独立的"单元集".
## 我换电脑了, 怎么迁移数据? ## 我换电脑了, 怎么迁移数据?
@@ -215,13 +220,13 @@ python -m heurams.interface
## 同时用 Anki 和 HeurAMS 会冲突吗? ## 同时用 Anki 和 HeurAMS 会冲突吗?
不会. 不会.
两者是独立的软件, 数据互不影响. 您可以逐步将内容迁移到 HeurAMS, 也可以两个一起用. 两者是独立的软件, 数据互不影响. 您可以逐步将内容迁移到 HeurAMS, 也可以两个一起用.
## 我需要安装 Python 吗? ## 我需要安装 Python 吗?
需要的, HeurAMS 是基于 Python 的软件. 需要的, HeurAMS 是基于 Python 的软件.
- Windows/macOS: 从 python.org 下载安装即可 - Windows/macOS: 从 python.org 下载安装即可
- Linux: 系统通常已自带 Python - Linux: 系统通常已自带 Python
@@ -233,9 +238,9 @@ HeurAMS 建议的 Python 版本是 3.12.13.
## 软件安全吗? 会不会有病毒? ## 软件安全吗? 会不会有病毒?
HeurAMS 是开源软件, 所有代码公开可查阅, 不会有病毒或后门. HeurAMS 是开源软件, 所有代码公开可查阅, 不会有病毒或后门.
它只读写自己的 `data/` 文件夹, 不会动您电脑上的其他文件. 它只读写自己的 `data/` 文件夹, 不会动您电脑上的其他文件.
## 软件报错, 出现一堆我看不懂的英文怎么办? ## 软件报错, 出现一堆我看不懂的英文怎么办?
@@ -249,13 +254,13 @@ HeurAMS 是开源软件, 所有代码公开可查阅, 不会有病毒或后门.
## 怎么看我学了多少? 有统计吗? ## 怎么看我学了多少? 有统计吗?
仪表盘界面会显示统计信息. 仪表盘界面会显示统计信息.
您可以通过导航器随时回到仪表盘查看. 您可以通过导航器随时回到仪表盘查看.
## 我觉得复习太快/太慢了, 能调吗? ## 我觉得复习太快/太慢了, 能调吗?
可以. 您可以通过切换算法或调整算法参数或改变记忆单元数来改变复习节奏. 可以. 您可以通过切换算法或调整算法参数或改变记忆单元数来改变复习节奏.
在设置界面可以找到相关设置. 在设置界面可以找到相关设置.
@@ -277,13 +282,13 @@ HeurAMS 是开源软件, 所有代码公开可查阅, 不会有病毒或后门.
## 哪里可以下载别人做好的单元集? ## 哪里可以下载别人做好的单元集?
目前项目还没有官方的单元集市场. 目前项目还没有官方的单元集市场.
但随着社区发展, 未来可能会有用户分享的单元集, 您也可以和朋友互相分享. 但随着社区发展, 未来可能会有用户分享的单元集, 您也可以和朋友互相分享.
## 我能把学习内容导出打印吗? ## 我能把学习内容导出打印吗?
可以. 可以.
软件本身支持将单元集导出为单一文本文件, 您可以用任何文本编辑器打开并打印. 也可以直接复制内容到 Word 等软件. 软件本身支持将单元集导出为单一文本文件, 您可以用任何文本编辑器打开并打印. 也可以直接复制内容到 Word 等软件.
@@ -318,13 +323,13 @@ data/repo/my_pack/
## 为什么不用 Flutter? ## 为什么不用 Flutter?
Flutter 是构建跨平台图形界面的优秀框架, HeurAMS 的设计目标之一是保持核心程序库独立于特定前端. Flutter 是构建跨平台图形界面的优秀框架, HeurAMS 的设计目标之一是保持核心程序库独立于特定前端.
但 Flutter 在 "集成 python" 方面不如 PyOtherSide, 只能通过 RPC 标准件和程序库通讯, 并且 Flutter 的桌面多窗口一直以来没有被官方稳定支持, 所以我们暂时放弃了 Flutter 而选择了 Kirigami. 但 Flutter 在 "集成 python" 方面不如 PyOtherSide, 只能通过 RPC 标准件和程序库通讯, 并且 Flutter 的桌面多窗口一直以来没有被官方稳定支持, 所以我们暂时放弃了 Flutter 而选择了 Kirigami.
当前我们优先开发了基于 Textual 的 TUI 前端和基于 Kirigami 的原生前端, 但这不排除未来出现 Flutter 或其他框架前端的可能性. 当前我们优先开发了基于 Textual 的 TUI 前端和基于 Kirigami 的原生前端, 但这不排除未来出现 Flutter 或其他框架前端的可能性.
如果您有兴趣开发 Flutter 前端, 欢迎参考[贡献指南](CONTRIBUTING.md#新的用户界面前端). 如果您有兴趣开发 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).
## 软件需要联网吗? ## 软件需要联网吗?
@@ -336,11 +341,11 @@ Flutter 是构建跨平台图形界面的优秀框架, HeurAMS 的设计目标
## 许可证中的"本机 API 调用豁免"是什么? ## 许可证中的"本机 API 调用豁免"是什么?
简言之, 如果您在自己的程序中通过本地进程间 API 方式的通信 (如同一主机上的 RPC 调用) 使用 HeurAMS, 而无需通过网络转发, 则您的程序不受 AGPL-3.0 许可证的约束. 简言之, 如果您在自己的程序中通过本地进程间 API 方式的通信 (如同一主机上的 RPC 调用) 使用 HeurAMS, 而无需通过网络转发, 则您的程序不受 AGPL-3.0 许可证的约束.
这项附加条款旨在鼓励第三方前端和工具的开发. 这项附加条款旨在鼓励第三方前端和工具的开发.
所以 HeurAMS 的许可证实质上是比原始的 AGPL-3.0 松一点的. 所以 HeurAMS 的许可证实质上是比原始的 AGPL-3.0 松一点的.
## HeurAMS 和百词斩有什么区别? ## HeurAMS 和百词斩有什么区别?
@@ -360,29 +365,29 @@ Flutter 是构建跨平台图形界面的优秀框架, HeurAMS 的设计目标
## 百词斩有图片联想记忆, HeurAMS 也有吗? ## 百词斩有图片联想记忆, HeurAMS 也有吗?
支持. 支持.
如果您的终端支持图片显示 (如 Konsole 或者 WezTerm), 单元集中可以包含图片, 复习时会直接展示. 如果您的终端支持图片显示 (如 Konsole 或者 WezTerm), 单元集中可以包含图片, 复习时会直接展示.
但图片需要您自己放入单元集. 但图片需要您自己放入单元集.
## 百词斩有打卡和排行榜, HeurAMS 有吗? ## 百词斩有打卡和排行榜, HeurAMS 有吗?
目前没有. 目前没有.
HeurAMS 不设打卡、排行榜或社交功能, 也不向任何人收集您的学习数据. HeurAMS 不设打卡、排行榜或社交功能, 也不向任何人收集您的学习数据.
## 百词斩有现成的词书, HeurAMS 去哪找内容? ## 百词斩有现成的词书, HeurAMS 去哪找内容?
百词斩的课程是官方制作好的, HeurAMS 的内容需要您自己创建或从社区获取. 百词斩的课程是官方制作好的, HeurAMS 的内容需要您自己创建或从社区获取.
详见"如何创建自己的单元集?". 详见"如何创建自己的单元集?".
## 百词斩在手机上用很方便, HeurAMS 能在手机用吗? ## 百词斩在手机上用很方便, HeurAMS 能在手机用吗?
可以, 但现阶段需要您"折腾"一下. 可以, 但现阶段需要您"折腾"一下.
Android 手机安装 Termux 后可运行 HeurAMS 的基本用户界面. Android 手机安装 Termux 后可运行 HeurAMS 的基本用户界面.
此外, 正在开发的 KiriMemo 前端将原生支持 Android 和 iOS, 这就不需要用户去折腾了. 此外, 正在开发的 KiriMemo 前端将原生支持 Android 和 iOS, 这就不需要用户去折腾了.
@@ -396,12 +401,12 @@ Android 手机安装 Termux 后可运行 HeurAMS 的基本用户界面.
考虑到百词斩的算法和词库是事实上闭源的, 我们无从得知算法来源. 考虑到百词斩的算法和词库是事实上闭源的, 我们无从得知算法来源.
但 HeurAMS 的架构设计可保证单元集制成后效果不比百词斩差, 甚至优于百词斩. 但 HeurAMS 的架构设计可保证单元集制成后效果不比百词斩差, 甚至优于百词斩.
HeurAMS 的间隔重复算法基于相同的认知科学原理, 且算法透明可调, 您可以自由选择最适合自己的调度策略. HeurAMS 的间隔重复算法基于相同的认知科学原理, 且算法透明可调, 您可以自由选择最适合自己的调度策略.
## 如何参与项目? ## 如何参与项目?
详见[贡献指南](CONTRIBUTING.md). 详见[贡献指南](CONTRIBUTING.md).
即使不是开发者, 您也可以通过编写文档、制作记忆单元集、翻译界面、答疑等方式参与. 即使不是开发者, 您也可以通过编写文档、制作记忆单元集、翻译界面、答疑等方式参与.

View File

@@ -2,19 +2,19 @@
## 概述 ## 概述
"潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是一种基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划, "潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是一种基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划,\
也是一种开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究. 也是一种开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
## 关于此仓库 ## 关于此仓库
此仓库为 "潜进" 的核心程序库在 python 语言下的实现 此仓库为 "潜进" 的核心程序库在 python 语言下的实现\
包含数据模型与框架, 并内置了基于 textual 框架的前端实现 (interface 子模块) 包含数据模型与框架, 并内置了基于 textual 框架的前端实现 (interface 子模块)\
除了通过内置前端进行学习外, 开发者也能在 python 环境中导入 `heurams` 库或使用 `RPC``heurams` 程序库实例通讯, 使用框架构建其他辅助记忆功能前端或其他应用程序 除了通过内置前端进行学习外, 开发者也能在 python 环境中导入 `heurams` 库或使用 `RPC``heurams` 程序库实例通讯, 使用框架构建其他辅助记忆功能前端或其他应用程序
> [!NOTE] > [!NOTE]
> 我们已经着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目 > 我们已经着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
> 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS 和 Plasma Mobile 提供现代用户界面 > 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS 和 Plasma Mobile 提供现代用户界面\
> 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发 > 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发
## 特性 ## 特性
@@ -72,11 +72,14 @@
#### 稳定版本 #### 稳定版本
安装适用于用户体验的可选依赖(推荐): 安装适用于用户体验的可选依赖(推荐):
``` ```
python -m pip install heurams[basic] -i https://pypi.pluv27.top/root/stable/+simple/ python -m pip install heurams[basic] -i https://pypi.pluv27.top/root/stable/+simple/
``` ```
安装适用于一般计算机的通用音频模块(基于 playsound3):
安装适用于一般计算机的通用音频模块(基于 playsound3):\
(此项不适用于 termux 环境, termux 的音频支持是内建的) (此项不适用于 termux 环境, termux 的音频支持是内建的)
``` ```
python -m pip install heurams[audio-playsound] -i https://pypi.pluv27.top/root/stable/+simple/ python -m pip install heurams[audio-playsound] -i https://pypi.pluv27.top/root/stable/+simple/
``` ```
@@ -84,12 +87,13 @@ python -m pip install heurams[audio-playsound] -i https://pypi.pluv27.top/root/s
#### 开发版本 #### 开发版本
> [!CAUTION] > [!CAUTION]
> 对于部分 Linux 发行版和 Android Termux 用户: > 对于部分 Linux 发行版和 Android Termux 用户:\
> 您需要先行安装 `cmake` 和 `libzmq` 才能正确安装项目的 `zmq` 依赖. > 您需要先行安装 `cmake` 和 `libzmq` 才能正确安装项目的 `zmq` 依赖.\
> 例如在 termux 上先运行 `pkg install cmake clang libzmq`. > 例如在 termux 上先运行 `pkg install cmake clang libzmq`.\
> 项目功能本身不依赖它, 但需要该依赖用于启动可选的调试服务器. > 项目功能本身不依赖它, 但需要该依赖用于启动可选的调试服务器.
安装全部可选依赖(推荐): 安装全部可选依赖(推荐):
``` ```
python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/ python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/
``` ```
@@ -112,7 +116,7 @@ python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/
### 从源码安装 ### 从源码安装
我们提供原生 python 和 uv 两种安装方式. 我们提供原生 python 和 uv 两种安装方式.\
详见[贡献指南](CONTRIBUTING.md). 详见[贡献指南](CONTRIBUTING.md).
## 常见问题 (FAQ) ## 常见问题 (FAQ)
@@ -125,8 +129,8 @@ python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/
## 参与项目 ## 参与项目
欢迎参与到项目协作中! 欢迎参与到项目协作中!\
详见[贡献指南](CONTRIBUTING.md). 详见[贡献指南](CONTRIBUTING.md).\
关于 AI 辅助开发的说明, 请参阅 [AGENTS.md](AGENTS.md). 关于 AI 辅助开发的说明, 请参阅 [AGENTS.md](AGENTS.md).
## 项目标识 ## 项目标识
@@ -145,9 +149,9 @@ HeurAMS 项目标识如下, 矢量图文件位于 `./src/heurams/assets/art/`
### 项目本身 ### 项目本身
本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款, 较标准 AGPL-3.0 更松. 本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款, 较标准 AGPL-3.0 更松.
详见根目录下 [LICENSE](LICENSE) 文件. 详见根目录下 [LICENSE](LICENSE) 文件.
### 第三方代码 ### 第三方代码

View File

@@ -1,23 +1,23 @@
# 用户界面屏幕截图 # 用户界面屏幕截图
潜进 (HeurAMS) 项目目前有两个前端实现, 此文档用于呈现它们的截图 (尽量与最新版本同步): 潜进 (HeurAMS) 项目目前有两个前端实现, 此文档用于呈现它们的截图 (尽量与最新版本同步):
- Textual 基本用户界面 (heurams.interface): 基于 Python Textual 框架构建的程序库内置跨平台 TUI 界面, 支持触屏、鼠标、键盘多操作模式, 是当前开箱即用的默认前端. - Textual 基本用户界面 (heurams.interface): 基于 Python Textual 框架构建的程序库内置跨平台 TUI 界面, 支持触屏、鼠标、键盘多操作模式, 是当前开箱即用的默认前端.
- KiriMemo (org.kde.kirimemo): 基于 KDE Kirigami 框架的现代跨平台前端, 使用 C++ 和 QML 构建, 通过 `PyOtherSide` 直接复用 Python 内核, 为多种平台提供原生体验 (尚未稳定). - KiriMemo (org.kde.kirimemo): 基于 KDE Kirigami 框架的现代跨平台前端, 使用 C++ 和 QML 构建, 通过 `PyOtherSide` 直接复用 Python 内核, 为多种平台提供原生体验 (尚未稳定).
欢迎为现有前端贡献代码, 或开发您自己的前端. 欢迎为现有前端贡献代码, 或开发您自己的前端.\
详见[贡献指南](CONTRIBUTING.md#新的用户界面前端). 详见[贡献指南](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).
## 基本用户界面前端的截图 ## 基本用户界面前端的截图
> 截图所使用的终端模拟器为 KDE Konsole > 截图所使用的终端模拟器为 KDE Konsole\
> 字体为 Cascadia Code 和 Noto Sans SC > 字体为 Cascadia Code 和 Noto Sans SC\
> 终端尺寸设置为 80x25 (软件也支持更大的终端尺寸) > 终端尺寸设置为 80x25 (软件也支持更大的终端尺寸)
### 仪表盘与导航器 ### 仪表盘与导航器
仪表盘包含学习面板的总体视图, 包括不同功能区域的操作入口, 统计信息, 以及单元集概览. 仪表盘包含学习面板的总体视图, 包括不同功能区域的操作入口, 统计信息, 以及单元集概览.\
导航器是一个实用的模态窗口, 能带您在多种功能间自如切换, 按 `n` 键或单击下方按钮可在任意界面迅速打开/关闭导航器. 导航器是一个实用的模态窗口, 能带您在多种功能间自如切换, 按 `n` 键或单击下方按钮可在任意界面迅速打开/关闭导航器.
<div style="display: flex; flex-wrap: wrap; gap: 10px;"> <div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/dashboard_1.png" width="48%"> <img src="screenshots/dashboard_1.png" width="48%">
@@ -27,8 +27,8 @@
### 准备界面与预缓存工具 ### 准备界面与预缓存工具
学习准备界面展示了单元集基本信息和每个单元的学习状态, 并提供了学习和预缓存的入口. 学习准备界面展示了单元集基本信息和每个单元的学习状态, 并提供了学习和预缓存的入口.\
预缓存工具使您能提前预缓存文本转语音资源以确保复习流程的顺畅体验和离线复习能力, 但即使您不预先缓存, 资源也会在复习播放时被自动加载. 预缓存工具使您能提前预缓存文本转语音资源以确保复习流程的顺畅体验和离线复习能力, 但即使您不预先缓存, 资源也会在复习播放时被自动加载.
<div style="display: flex; flex-wrap: wrap; gap: 10px;"> <div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/preparation.png" width="48%"> <img src="screenshots/preparation.png" width="48%">
@@ -37,8 +37,8 @@
### 记忆队列界面 ### 记忆队列界面
队列式学习记忆的主要界面. 队列式学习记忆的主要界面.\
同一知识点可产生多种谜题类型的评估方式, 软件内置完形填空与识别题等多种测试类型, 您可在复习流程中按顺序完成不同测试. 同一知识点可产生多种谜题类型的评估方式, 软件内置完形填空与识别题等多种测试类型, 您可在复习流程中按顺序完成不同测试.
<div style="display: flex; flex-wrap: wrap; gap: 10px;"> <div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/memoqueue_cloze_1.png" width="48%"> <img src="screenshots/memoqueue_cloze_1.png" width="48%">
@@ -46,10 +46,9 @@
<img src="screenshots/memoqueue_recognition_2.png" width="48%"> <img src="screenshots/memoqueue_recognition_2.png" width="48%">
</div> </div>
### 设置界面 ### 设置界面
配置界面包含算法选择、音频与多种服务的提供者切换、以及界面与算法设置等选项. 配置界面包含算法选择、音频与多种服务的提供者切换、以及界面与算法设置等选项.
<div style="display: flex; flex-wrap: wrap; gap: 10px;"> <div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/setting_1.png" width="48%"> <img src="screenshots/setting_1.png" width="48%">
@@ -58,8 +57,8 @@
### 其他界面 ### 其他界面
收藏管理器可管理您手动标记的个人收藏集. 收藏管理器可管理您手动标记的个人收藏集.\
关于页面提供了程序版本号、许可协议等信息. 关于页面提供了程序版本号、许可协议等信息.
<div style="display: flex; flex-wrap: wrap; gap: 10px;"> <div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img src="screenshots/about_1.png" width="48%"> <img src="screenshots/about_1.png" width="48%">
@@ -68,5 +67,6 @@
## KiriMemo 前端的截图 ## KiriMemo 前端的截图
截图将在 KiriMemo 前端开发趋于稳定后补充. 截图将在 KiriMemo 前端开发趋于稳定后补充.
<!-- TODO: 补充截图 --> <!-- TODO: 补充截图 -->

View File

@@ -20,4 +20,4 @@ python 代指您使用的解释器, 在某些发行版中可能是 python3, 而
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -27,6 +27,7 @@ user_data = workdir / "data"
if not user_data.exists(): if not user_data.exists():
logger.info("初始化数据目录: %s", user_data) logger.info("初始化数据目录: %s", user_data)
import shutil import shutil
shutil.copytree(default_data, user_data) shutil.copytree(default_data, user_data)
else: else:
(workdir / "data" / "config").mkdir(parents=True, exist_ok=True) (workdir / "data" / "config").mkdir(parents=True, exist_ok=True)

View File

@@ -7,6 +7,7 @@ import pickle
logger = get_logger(__name__) logger = get_logger(__name__)
def start_debug_server(app): def start_debug_server(app):
logger = get_logger("zmq_debug") logger = get_logger("zmq_debug")
context = zmq.Context() context = zmq.Context()

View File

@@ -16,7 +16,6 @@ from textual.widgets import (
Label, Label,
ListItem, ListItem,
ListView, ListView,
Markdown,
Static, Static,
) )
@@ -117,7 +116,13 @@ class FavoriteManagerScreen(Screen):
# 创建列表项, 包含移除按钮 # 创建列表项, 包含移除按钮
container = Horizontal( container = Horizontal(
Label(display_text, classes="favorite-content"), Label(display_text, classes="favorite-content"),
Button("移除", id=f"remove-{button_key}", variant="error", flat=True, classes="favorite-item-btn"), Button(
"移除",
id=f"remove-{button_key}",
variant="error",
flat=True,
classes="favorite-item-btn",
),
classes="favorite-item", classes="favorite-item",
) )
return ListItem(container) return ListItem(container)

View File

@@ -140,7 +140,11 @@ class MemScreen(Screen):
if config_var.get()["interface"]["global"]["persist_to_file"]: if config_var.get()["interface"]["global"]["persist_to_file"]:
self.repo.persist_to_repodir() self.repo.persist_to_repodir()
container.mount(Finished(is_saved=config_var.get()["interface"]["global"]["persist_to_file"])) container.mount(
Finished(
is_saved=config_var.get()["interface"]["global"]["persist_to_file"]
)
)
def on_button_pressed(self, event): def on_button_pressed(self, event):
event.stop() event.stop()
@@ -162,6 +166,7 @@ class MemScreen(Screen):
play_by_path(path) play_by_path(path)
else: else:
from heurams.services.tts_service import convertor from heurams.services.tts_service import convertor
convertor(self.atom.registry["nucleon"]["tts_text"], path) convertor(self.atom.registry["nucleon"]["tts_text"], path)
play_by_path(path) play_by_path(path)
@@ -224,6 +229,7 @@ class MemScreen(Screen):
def action_go_back_notif(self): def action_go_back_notif(self):
self.notify("确定吗? 按下大写 Q 以返回") self.notify("确定吗? 按下大写 Q 以返回")
def action_go_back(self): def action_go_back(self):
self.app.pop_screen() self.app.pop_screen()

View File

@@ -81,10 +81,10 @@ class ClozePuzzle(BasePuzzleWidget):
btnid = f"sel000-{h}" btnid = f"sel000-{h}"
logger.debug(f"建立按钮 {btnid}") logger.debug(f"建立按钮 {btnid}")
self.btn_shortcuts[f"{c}"] = btnid self.btn_shortcuts[f"{c}"] = btnid
btns.append(Button(f"{i}", id=f"{btnid}", classes='cloze-option-btn')) btns.append(Button(f"{i}", id=f"{btnid}", classes="cloze-option-btn"))
for i in range((len(btns)+1)//2): for i in range((len(btns) + 1) // 2):
if 2 * i + 1 + 1 <= len(btns): if 2 * i + 1 + 1 <= len(btns):
yield Horizontal(btns[i], btns[len(btns) - 1 - i], classes='hori') yield Horizontal(btns[i], btns[len(btns) - 1 - i], classes="hori")
else: else:
yield btns[i] yield btns[i]
s.focus() s.focus()
@@ -136,7 +136,7 @@ class ClozePuzzle(BasePuzzleWidget):
self.atom.minimize(rating) self.atom.minimize(rating)
def on_key(self, event: Key) -> None: def on_key(self, event: Key) -> None:
#self.notify(event.key) # self.notify(event.key)
if event.key in self.btn_shortcuts: if event.key in self.btn_shortcuts:
btn_id = self.btn_shortcuts.get(event.key) btn_id = self.btn_shortcuts.get(event.key)
btn_id = "#" + btn_id btn_id = "#" + btn_id

View File

@@ -11,4 +11,4 @@ for _finder, _name, _ispkg in pkgutil.iter_modules(__path__):
continue continue
importlib.import_module(f".{_name}", __package__) importlib.import_module(f".{_name}", __package__)
algorithms = BaseAlgorithm.get_registry() algorithms = BaseAlgorithm.get_registry()

View File

@@ -7,6 +7,7 @@ logger = get_logger(__name__)
_registry: dict[str, type["BaseAlgorithm"]] = {} _registry: dict[str, type["BaseAlgorithm"]] = {}
class BaseAlgorithm: class BaseAlgorithm:
algo_name = "BaseAlgorithm" algo_name = "BaseAlgorithm"
desc = "算法基类" desc = "算法基类"

View File

@@ -3,7 +3,7 @@ FSRS 算法模块 — 基于 py-fsrs 的现代间隔重复调度器
基于: https://github.com/open-spaced-repetition/py-fsrs 基于: https://github.com/open-spaced-repetition/py-fsrs
""" """
import json
import os import os
import pathlib import pathlib
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
@@ -20,9 +20,10 @@ from .base import BaseAlgorithm
logger = get_logger(__name__) logger = get_logger(__name__)
# 全局 Scheduler 状态文件路径 # 全局 Scheduler 状态文件路径
_SCHEDULER_STATE_FILE = pathlib.Path( _SCHEDULER_STATE_FILE = (
config_var.get()["global"]["paths"]["misc"] pathlib.Path(config_var.get()["global"]["paths"]["misc"])
) / "fsrs_scheduler_state.json" / "fsrs_scheduler_state.json"
)
def _get_global_scheduler(): def _get_global_scheduler():
@@ -78,10 +79,10 @@ class FSRSAlgorithm(BaseAlgorithm):
class AlgodataDict(TypedDict): class AlgodataDict(TypedDict):
# FSRS 特有字段 # FSRS 特有字段
fsrs_state: int # State 枚举值: 1=Learning, 2=Review, 3=Relearning fsrs_state: int # State 枚举值: 1=Learning, 2=Review, 3=Relearning
fsrs_step: int # 当前学习步进索引, -1 表示 None (Review 状态) fsrs_step: int # 当前学习步进索引, -1 表示 None (Review 状态)
fsrs_stability: float # 稳定性(秒), 0.0 表示尚未计算 fsrs_stability: float # 稳定性(秒), 0.0 表示尚未计算
fsrs_difficulty: float # 难度 [1.0, 10.0], 0.0 表示尚未计算 fsrs_difficulty: float # 难度 [1.0, 10.0], 0.0 表示尚未计算
# 标准 BaseAlgorithm 兼容字段 # 标准 BaseAlgorithm 兼容字段
real_rept: int real_rept: int
rept: int rept: int
@@ -92,7 +93,7 @@ class FSRSAlgorithm(BaseAlgorithm):
last_modify: float last_modify: float
defaults = { defaults = {
"fsrs_state": 1, # State.Learning "fsrs_state": 1, # State.Learning
"fsrs_step": 0, "fsrs_step": 0,
"fsrs_stability": 0.0, "fsrs_stability": 0.0,
"fsrs_difficulty": 0.0, "fsrs_difficulty": 0.0,
@@ -136,9 +137,7 @@ class FSRSAlgorithm(BaseAlgorithm):
# last_review # last_review
last_date = data.get("last_date", 0) last_date = data.get("last_date", 0)
card.last_review = ( card.last_review = _daystamp_to_datetime(last_date) if last_date > 0 else None
_daystamp_to_datetime(last_date) if last_date > 0 else None
)
return card return card
@@ -160,9 +159,7 @@ class FSRSAlgorithm(BaseAlgorithm):
if card.last_review if card.last_review
else data.get("last_date", 0) else data.get("last_date", 0)
) )
data["next_date"] = ( data["next_date"] = _datetime_to_daystamp(card.due) if card.due else 0
_datetime_to_daystamp(card.due) if card.due else 0
)
data["interval"] = max(0, data["next_date"] - data["last_date"]) data["interval"] = max(0, data["next_date"] - data["last_date"])
data["last_modify"] = get_timestamp() data["last_modify"] = get_timestamp()
return algodata return algodata
@@ -209,8 +206,7 @@ class FSRSAlgorithm(BaseAlgorithm):
algodata[cls.algo_name]["rept"] += 1 algodata[cls.algo_name]["rept"] += 1
logger.debug( logger.debug(
"FSRS.revisor 完成: stability=%s, difficulty=%s, state=%s, " "FSRS.revisor 完成: stability=%s, difficulty=%s, state=%s, " "next_date=%d",
"next_date=%d",
card.stability, card.stability,
card.difficulty, card.difficulty,
card.state, card.state,

View File

@@ -4,6 +4,7 @@ SM-15M — 基于 sm.js 的间隔重复算法
基于: https://github.com/slaypni/sm.js 基于: https://github.com/slaypni/sm.js
原始 CoffeeScript (c) 2014 Kazuaki Tanida, MIT 许可证 原始 CoffeeScript (c) 2014 Kazuaki Tanida, MIT 许可证
""" """
import datetime import datetime
import json import json
import math import math
@@ -19,7 +20,6 @@ from heurams.services.timer import (
get_timestamp_ms, get_timestamp_ms,
daystamp_to_datetime, daystamp_to_datetime,
datetime_to_daystamp, datetime_to_daystamp,
get_now_datetime,
) )
from .base import BaseAlgorithm from .base import BaseAlgorithm
@@ -111,7 +111,7 @@ def power_law_model(a, b):
"""y = a * x^b""" """y = a * x^b"""
def y_func(x): def y_func(x):
return a * (x ** b) return a * (x**b)
def x_func(y): def x_func(y):
if a == 0 or b == 0: if a == 0 or b == 0:
@@ -134,7 +134,7 @@ def fixed_point_power_law_regression(points, fixed_point):
sum_sqX = sum(x * x for x in X) sum_sqX = sum(x * x for x in X)
b = sumXY / sum_sqX if sum_sqX else 0 b = sumXY / sum_sqX if sum_sqX else 0
return power_law_model(q / (p ** b), b) return power_law_model(q / (p**b), b)
# ============================================================================ # ============================================================================
@@ -161,7 +161,7 @@ class FI_G:
def _register_point(self, fi, grade): def _register_point(self, fi, grade):
self.points.append([fi, grade + self.GRADE_OFFSET]) self.points.append([fi, grade + self.GRADE_OFFSET])
if len(self.points) > self.MAX_POINTS_COUNT: if len(self.points) > self.MAX_POINTS_COUNT:
self.points = self.points[-self.MAX_POINTS_COUNT:] self.points = self.points[-self.MAX_POINTS_COUNT :]
self._graph = None self._graph = None
def update(self, grade, item, now): def update(self, grade, item, now):
@@ -203,7 +203,7 @@ class ForgettingCurve:
is_remembered = grade >= THRESHOLD_RECALL is_remembered = grade >= THRESHOLD_RECALL
self.points.append([uf, self.REMEMBERED if is_remembered else self.FORGOTTEN]) self.points.append([uf, self.REMEMBERED if is_remembered else self.FORGOTTEN])
if len(self.points) > self.MAX_POINTS_COUNT: if len(self.points) > self.MAX_POINTS_COUNT:
self.points = self.points[-self.MAX_POINTS_COUNT:] self.points = self.points[-self.MAX_POINTS_COUNT :]
self._curve = None self._curve = None
def retention(self, uf): def retention(self, uf):
@@ -253,9 +253,9 @@ class ForgettingCurves:
pts = [] pts = []
for i in range(21): for i in range(21):
v = MIN_AF + NOTCH_AF * i v = MIN_AF + NOTCH_AF * i
y = math.exp( y = math.exp(-1.0 / (10 + 1 * (a + 1)) * (i - a**0.6)) * (
-1.0 / (10 + 1 * (a + 1)) * (i - a ** 0.6) self.REMEMBERED - self.sm.requested_fi
) * (self.REMEMBERED - self.sm.requested_fi) )
pts.append([v, min(self.REMEMBERED, y)]) pts.append([v, min(self.REMEMBERED, y)])
partial = [[0, self.REMEMBERED]] + pts partial = [[0, self.REMEMBERED]] + pts
row.append(ForgettingCurve(partial)) row.append(ForgettingCurve(partial))
@@ -427,7 +427,9 @@ class Item:
now = datetime.datetime.now() now = datetime.datetime.now()
af_idx = self.lapse if self.repetition == 0 else self.af_index() af_idx = self.lapse if self.repetition == 0 else self.af_index()
of_val = self.sm.ofm.of(self.repetition, af_idx) of_val = self.sm.ofm.of(self.repetition, af_idx)
self.of = max(1.0, (of_val - 1) * (self.interval(now) / self.optimum_interval) + 1) self.of = max(
1.0, (of_val - 1) * (self.interval(now) / self.optimum_interval) + 1
)
self.optimum_interval = round(self.optimum_interval * self.of) self.optimum_interval = round(self.optimum_interval * self.of)
self.previous_date = now self.previous_date = now
self.due_date = now + datetime.timedelta(milliseconds=self.optimum_interval) self.due_date = now + datetime.timedelta(milliseconds=self.optimum_interval)
@@ -443,7 +445,7 @@ class Item:
estimated_af = max(MIN_AF, min(MAX_AF, corrected_uf)) estimated_af = max(MIN_AF, min(MAX_AF, corrected_uf))
self._afs.append(estimated_af) self._afs.append(estimated_af)
if len(self._afs) > self.MAX_AFS_COUNT: if len(self._afs) > self.MAX_AFS_COUNT:
self._afs = self._afs[-self.MAX_AFS_COUNT:] self._afs = self._afs[-self.MAX_AFS_COUNT :]
wsum = sum(af * (i + 1) for i, af in enumerate(self._afs)) wsum = sum(af * (i + 1) for i, af in enumerate(self._afs))
wtotal = sum(range(1, len(self._afs) + 1)) wtotal = sum(range(1, len(self._afs) + 1))
self.af(wsum / wtotal if wtotal else estimated_af) self.af(wsum / wtotal if wtotal else estimated_af)
@@ -600,7 +602,10 @@ class SM:
# Global state management # Global state management
# ============================================================================ # ============================================================================
_GLOBAL_STATE_FILE = pathlib.Path(config_var.get()["global"]["paths"]["misc"]) / "sm15m_global_state.json" _GLOBAL_STATE_FILE = (
pathlib.Path(config_var.get()["global"]["paths"]["misc"])
/ "sm15m_global_state.json"
)
def _get_global_sm(): def _get_global_sm():
@@ -702,7 +707,8 @@ class SM15MAlgorithm(BaseAlgorithm):
last_date = data.get("last_date", 0) last_date = data.get("last_date", 0)
item.previous_date = ( item.previous_date = (
daystamp_to_datetime(last_date).replace(tzinfo=None) daystamp_to_datetime(last_date).replace(tzinfo=None)
if last_date > 0 else None if last_date > 0
else None
) )
next_date_ms = data.get("next_date_ms", 0) next_date_ms = data.get("next_date_ms", 0)
@@ -743,9 +749,7 @@ class SM15MAlgorithm(BaseAlgorithm):
data["last_date"] = datetime_to_daystamp(item.previous_date) data["last_date"] = datetime_to_daystamp(item.previous_date)
data["next_date_ms"] = int(item.due_date.timestamp() * 1000) data["next_date_ms"] = int(item.due_date.timestamp() * 1000)
data["next_date"] = datetime_to_daystamp(item.due_date) data["next_date"] = datetime_to_daystamp(item.due_date)
data["interval"] = max( data["interval"] = max(0, data["next_date"] - (data.get("last_date", 0) or 0))
0, data["next_date"] - (data.get("last_date", 0) or 0)
)
data["last_modify"] = get_timestamp() data["last_modify"] = get_timestamp()
return algodata return algodata

View File

@@ -11,6 +11,7 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
def play_by_path(path: pathlib.Path): def play_by_path(path: pathlib.Path):
logger.debug("playsound_audio.play_by_path: 开始播放 %s", path) logger.debug("playsound_audio.play_by_path: 开始播放 %s", path)
try: try:

View File

@@ -43,9 +43,9 @@ def get_timestamp_ms() -> int:
def daystamp_to_datetime(daystamp: int) -> datetime.datetime: def daystamp_to_datetime(daystamp: int) -> datetime.datetime:
"""将日戳转换为 UTC datetime (当日午夜)""" """将日戳转换为 UTC datetime (当日午夜)"""
return datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta( return datetime.datetime(
days=daystamp 1970, 1, 1, tzinfo=datetime.timezone.utc
) ) + datetime.timedelta(days=daystamp)
def datetime_to_daystamp(dt: datetime.datetime) -> int: def datetime_to_daystamp(dt: datetime.datetime) -> int:

View File

@@ -25,9 +25,18 @@ class TestElectronInit:
assert e.algodata["SM-2"]["efactor"] == 2.5 assert e.algodata["SM-2"]["efactor"] == 2.5
def test_existing_data_preserved(self, timer_context): def test_existing_data_preserved(self, timer_context):
data = {"SM-2": {"efactor": 1.5, "rept": 3, "real_rept": 5, "interval": 10, data = {
"last_date": 100, "next_date": 200, "is_activated": 1, "SM-2": {
"last_modify": 1e9}} "efactor": 1.5,
"rept": 3,
"real_rept": 5,
"interval": 10,
"last_date": 100,
"next_date": 200,
"is_activated": 1,
"last_modify": 1e9,
}
}
e = Electron("test-id", data) e = Electron("test-id", data)
assert e.algodata["SM-2"]["efactor"] == 1.5 assert e.algodata["SM-2"]["efactor"] == 1.5
assert e.algodata["SM-2"]["rept"] == 3 assert e.algodata["SM-2"]["rept"] == 3

View File

@@ -65,8 +65,6 @@ class TestEpathModify:
def test_modify_list_index_with_parents(self): def test_modify_list_index_with_parents(self):
d = {"items": []} d = {"items": []}
result = epath( result = epath(d, "items.[3]", enable_modify=True, new_value=42, parents=True)
d, "items.[3]", enable_modify=True, new_value=42, parents=True
)
assert result == 42 assert result == 42
assert d["items"] == [None, None, None, 42] assert d["items"] == [None, None, None, 42]