style: 格式化代码
This commit is contained in:
@@ -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)
|
||||||
- 如果需要升级某个依赖或运行环境的版本
|
- 如果需要升级某个依赖或运行环境的版本
|
||||||
|
|||||||
7
FAQ.md
7
FAQ.md
@@ -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,9 +68,11 @@ python -m heurams.interface
|
|||||||
```
|
```
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
打开"终端"应用程序, 输入以上命令.
|
打开"终端"应用程序, 输入以上命令.
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
打开您的终端模拟器 (一般是按 Ctrl + Alt + T), 输入以上命令.
|
打开您的终端模拟器 (一般是按 Ctrl + Alt + T), 输入以上命令.
|
||||||
|
|
||||||
如果您觉得每次输入命令太麻烦, 可以创建一个桌面快捷方式或脚本文件, 详见网上的相关教程.
|
如果您觉得每次输入命令太麻烦, 可以创建一个桌面快捷方式或脚本文件, 详见网上的相关教程.
|
||||||
@@ -324,7 +329,7 @@ Flutter 是构建跨平台图形界面的优秀框架, HeurAMS 的设计目标
|
|||||||
|
|
||||||
当前我们优先开发了基于 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).
|
||||||
|
|
||||||
## 软件需要联网吗?
|
## 软件需要联网吗?
|
||||||
|
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
"潜进" (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).
|
||||||
|
|
||||||
## 项目标识
|
## 项目标识
|
||||||
|
|||||||
@@ -5,18 +5,18 @@
|
|||||||
- 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;">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
### 准备界面与预缓存工具
|
### 准备界面与预缓存工具
|
||||||
|
|
||||||
学习准备界面展示了单元集基本信息和每个单元的学习状态, 并提供了学习和预缓存的入口.
|
学习准备界面展示了单元集基本信息和每个单元的学习状态, 并提供了学习和预缓存的入口.\
|
||||||
预缓存工具使您能提前预缓存文本转语音资源以确保复习流程的顺畅体验和离线复习能力, 但即使您不预先缓存, 资源也会在复习播放时被自动加载.
|
预缓存工具使您能提前预缓存文本转语音资源以确保复习流程的顺畅体验和离线复习能力, 但即使您不预先缓存, 资源也会在复习播放时被自动加载.
|
||||||
|
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
### 记忆队列界面
|
### 记忆队列界面
|
||||||
|
|
||||||
队列式学习记忆的主要界面.
|
队列式学习记忆的主要界面.\
|
||||||
同一知识点可产生多种谜题类型的评估方式, 软件内置完形填空与识别题等多种测试类型, 您可在复习流程中按顺序完成不同测试.
|
同一知识点可产生多种谜题类型的评估方式, 软件内置完形填空与识别题等多种测试类型, 您可在复习流程中按顺序完成不同测试.
|
||||||
|
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||||
@@ -46,7 +46,6 @@
|
|||||||
<img src="screenshots/memoqueue_recognition_2.png" width="48%">
|
<img src="screenshots/memoqueue_recognition_2.png" width="48%">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
### 设置界面
|
### 设置界面
|
||||||
|
|
||||||
配置界面包含算法选择、音频与多种服务的提供者切换、以及界面与算法设置等选项.
|
配置界面包含算法选择、音频与多种服务的提供者切换、以及界面与算法设置等选项.
|
||||||
@@ -58,7 +57,7 @@
|
|||||||
|
|
||||||
### 其他界面
|
### 其他界面
|
||||||
|
|
||||||
收藏管理器可管理您手动标记的个人收藏集.
|
收藏管理器可管理您手动标记的个人收藏集.\
|
||||||
关于页面提供了程序版本号、许可协议等信息.
|
关于页面提供了程序版本号、许可协议等信息.
|
||||||
|
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||||
@@ -69,4 +68,5 @@
|
|||||||
## KiriMemo 前端的截图
|
## KiriMemo 前端的截图
|
||||||
|
|
||||||
截图将在 KiriMemo 前端开发趋于稳定后补充.
|
截图将在 KiriMemo 前端开发趋于稳定后补充.
|
||||||
|
|
||||||
<!-- TODO: 补充截图 -->
|
<!-- TODO: 补充截图 -->
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = "算法基类"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user