Compare commits
7 Commits
face433660
..
0.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
| abd94d9a21 | |||
| 91d3c86871 | |||
| 66ad50c44d | |||
| 2527daa923 | |||
| 1883ca2387 | |||
| dde6f872f0 | |||
| 16c22cf207 |
+9
-9
@@ -5,20 +5,20 @@
|
||||
__pycache__/
|
||||
.idea/
|
||||
cache/
|
||||
data/repo/cngk
|
||||
data/repo/eotgk
|
||||
data/repo/evtgk
|
||||
data/misc
|
||||
data/cache
|
||||
data/session
|
||||
#nucleon/test.toml
|
||||
electron/test.toml
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
old/
|
||||
AGENT.md
|
||||
# config/
|
||||
data/cache/
|
||||
data/electron/
|
||||
data/nucleon/
|
||||
!data/nucleon/test*
|
||||
data/orbital/
|
||||
AGENTS.md
|
||||
*.log.*
|
||||
*.pkl
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
-467
@@ -1,467 +0,0 @@
|
||||
## 整体架构概览
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "用户界面层 (TUI)"
|
||||
TUI[Textual App]
|
||||
Screens[应用屏幕]
|
||||
Widgets[谜题组件]
|
||||
end
|
||||
|
||||
subgraph "内核层 Kernel"
|
||||
Reactor[调度反应器]
|
||||
Algorithms[算法模块]
|
||||
Particles[数据模型]
|
||||
Puzzles[谜题引擎]
|
||||
RepoLib[仓库系统]
|
||||
Auxiliary[辅助工具]
|
||||
end
|
||||
|
||||
subgraph "服务层"
|
||||
Config[配置管理 ConfigDict]
|
||||
Logger[日志系统]
|
||||
Timer[时间服务]
|
||||
Audio[音频服务]
|
||||
TTS[TTS 服务]
|
||||
Favorites[收藏管理]
|
||||
Attic[持久化]
|
||||
Hasher[哈希服务]
|
||||
end
|
||||
|
||||
subgraph "提供者层"
|
||||
AudioProv[音频提供者]
|
||||
TTSProv[TTS 提供者]
|
||||
LLMProv[LLM 提供者]
|
||||
end
|
||||
|
||||
subgraph "数据层"
|
||||
RepoDir[TOML/JSON 仓库目录]
|
||||
ConfigDir[TOML 配置目录]
|
||||
Logs[日志文件]
|
||||
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
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
项目以物理粒子隐喻为核心, 将记忆单元拆解为三个模型:
|
||||
|
||||
### Nucleon (核子) — 内容层
|
||||
|
||||
```
|
||||
Nucleon(ident, payload, common)
|
||||
```
|
||||
|
||||
- **只读**内容容器. 通过 `Evalizer` (基于 `eval()` 的模板系统)对 payload 和 common 进行编译展开.
|
||||
- 包含 `puzzles` 字段, 定义该记忆单元支持哪些谜题类型.
|
||||
- 从 `repo.payload` 和 `repo.typedef["common"]` 配对创建.
|
||||
- 一旦创建, 内容不可修改 (`__setitem__` 抛出 `AttributeError`).
|
||||
|
||||
### Electron (电子) — 状态层
|
||||
|
||||
```
|
||||
Electron(ident, algodata, algo_name)
|
||||
```
|
||||
|
||||
- 算法状态数据的包装器. 每个 Electron 绑定一个算法 (`algorithms[algo_name]`).
|
||||
- `algodata` 是到仓库 `algodata.lict` 中对应字典的**引用**, 修改即持久化.
|
||||
- 核心方法:`activate()` (标记激活)、`revisor()` (评分迭代)、`is_due()` (到期判断).
|
||||
|
||||
### Orbital (轨道) — 策略层
|
||||
|
||||
```
|
||||
orbital = {
|
||||
"schedule": ["quick_review", "recognition"],
|
||||
"routes": {
|
||||
"quick_review": [["MCQ", "1.0"], ["Cloze", "0.5"]],
|
||||
"recognition": [["Recognition", "1.0"]],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 定义复习阶段流程和各阶段内谜题选择策略的纯字典.
|
||||
- 每个阶段对应一组 `(谜题类型, 概率系数)` 元组列表, 概率系数 >1 的部分表示强制重复次数.
|
||||
|
||||
### Atom (原子) — 运行时组装
|
||||
|
||||
```
|
||||
Atom(nucleon, electron, orbital)
|
||||
```
|
||||
|
||||
- 三者的运行时组合, 附带 `runtime` 运行时标志 (`locked`, `min_rate`, `new_activation`).
|
||||
- 是 UI 和调度层操作的基本单位.
|
||||
- `revise()` 方法在 `locked` 为真时调用 `electron.revisor(min_rate)`, 执行最终评分迭代.
|
||||
|
||||
**关系图**:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "持久化存储"
|
||||
Payload[(payload.toml)]
|
||||
Common[(typedef.toml)]
|
||||
Algodata[(algodata.json)]
|
||||
Schedule[(schedule.toml)]
|
||||
end
|
||||
|
||||
subgraph "运行时组装"
|
||||
Nucleon -->|内容| Atom
|
||||
Electron -->|状态| Atom
|
||||
Orbital -->|策略| Atom
|
||||
end
|
||||
|
||||
Payload --> Nucleon
|
||||
Common --> Nucleon
|
||||
Algodata --> Electron
|
||||
Schedule --> Orbital
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 调度反应器 (Reactor)
|
||||
|
||||
调度反应器是核心业务流程引擎, 采用三层嵌套的有限状态机设计 (基于 `transitions` 库).
|
||||
|
||||
### 状态枚举定义
|
||||
|
||||
| 状态机 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| **RouterState** | `unsure` | 初始状态, 自动推进 |
|
||||
| | `quick_review` | 快速复习阶段 |
|
||||
| | `recognition` | 新记忆识别阶段 |
|
||||
| | `final_review` | 最终总复习阶段 |
|
||||
| | `finished` | 完成, 执行评分 |
|
||||
| **ProcessionState** | `active` | 进行中 |
|
||||
| | `finished` | 已完成 |
|
||||
| **ExpanderState** | `exammode` | 考试模式 (正面答题) |
|
||||
| | `retronly` | 回溯模式 (仅识别) |
|
||||
|
||||
### 三层嵌套结构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Router (全局路由器)"
|
||||
R[Router<br/>状态: unsure→quick_review→recognition→final_review→finished]
|
||||
P1[Procession 队列1: 快速复习]
|
||||
P2[Procession 队列2: 新记忆]
|
||||
P3[Procession 队列3: 总复习]
|
||||
R --> P1
|
||||
R --> P2
|
||||
R --> P3
|
||||
end
|
||||
|
||||
subgraph "Procession (单阶段队列)"
|
||||
P1 --> E1[Expander 原子A]
|
||||
P1 --> E2[Expander 原子B]
|
||||
P1 --> E3[Expander 原子C]
|
||||
M{forward 推进} --> |完成| Finish((FINISHED))
|
||||
end
|
||||
|
||||
subgraph "Expander (单原子展开器)"
|
||||
E1 --> S[(轨道策略)]
|
||||
S -->|概率展开| PZ1[谜题1: MCQ]
|
||||
S -->|概率展开| PZ2[谜题2: Cloze]
|
||||
PZ1 -->|评分| RPT[report]
|
||||
PZ2 -->|评分| RPT
|
||||
RPT -->|finish| RETRO[retronly 回溯模式]
|
||||
end
|
||||
```
|
||||
|
||||
### 数据流详解
|
||||
|
||||
```
|
||||
Router.__init__(atoms)
|
||||
│
|
||||
├─ 新旧原子分流
|
||||
│ ├─ old_atoms → Procession(quick_review) "初始复习"
|
||||
│ └─ new_atoms → Procession(recognition) "新记忆"
|
||||
│
|
||||
└─ 所有原子 → Procession(final_review) "总体复习"
|
||||
│
|
||||
└─ Procession.forward()
|
||||
│
|
||||
├─ cursor >= len(atoms) → finish()
|
||||
└─ cursor < len(atoms) → next_atom
|
||||
│
|
||||
└─ Procession.get_expander()
|
||||
│
|
||||
└─ Expander(atom, route)
|
||||
│
|
||||
├─ 读取 orbital.routes[route_value]
|
||||
├─ 概率展开为谜题列表 self.puzzles_inf
|
||||
├─ exammode → 依次展示谜题
|
||||
├─ report(rating) → 记录最低评分
|
||||
├─ forward() → 下一个谜题或 finish → retronly
|
||||
└─ retronly → 展示 Recognition
|
||||
│
|
||||
└─ Atom.revise()
|
||||
│
|
||||
└─ Electron.revisor(min_rate)
|
||||
│
|
||||
└─ Algorithm.revisor(algodata, feedback)
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 算法系统
|
||||
|
||||
所有算法继承自 `BaseAlgorithm`, 以类方法的风格实现, 通过 `algorithms` 字典注册.
|
||||
|
||||
| 算法 | 文件 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| **SM-2** | `sm2.py` | ✅ 完成 | 经典 SuperMemo 1987 算法 |
|
||||
| **NSP-0** | `nsp0.py` | ✅ 完成 | 非间隔过滤调度器 |
|
||||
| **SM-15M** | `sm15m.py` | ✅ 完成 | 从 CoffeeScript 移植的 SM-15 |
|
||||
| **FSRS** | `fsrs.py` | ✅ 部分完成 | 优化器不可用 |
|
||||
| **Base** | `base.py` | ✅ 基类 | 定义 `AlgodataDict` 结构和默认值 |
|
||||
|
||||
每个算法提供以下类方法:
|
||||
|
||||
| 方法 | 功能 |
|
||||
|------|------|
|
||||
| `revisor(algodata, feedback, is_new_activation)` | 根据评分迭代记忆数据 |
|
||||
| `is_due(algodata)` | 判断是否到期复习 |
|
||||
| `get_rating(algodata)` | 获取评分信息 |
|
||||
| `nextdate(algodata)` | 获取下一次复习时间戳 |
|
||||
| `check_integrity(algodata)` | 校验 algodata 数据结构完整性 |
|
||||
|
||||
### 算法数据结构 (AlgodataDict)
|
||||
|
||||
```python
|
||||
{
|
||||
"real_rept": int, # 实际复习次数
|
||||
"rept": int, # 当前重复计数
|
||||
"interval": int, # 间隔天数
|
||||
"last_date": int, # 上次复习日期
|
||||
"next_date": int, # 下次到期日期
|
||||
"is_activated": int, # 是否已激活 (0/1)
|
||||
"last_modify": float, # 最后修改时间戳
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 仓库系统 (Repo)
|
||||
|
||||
仓库是 TOML/JSON 文件目录, 无数据库依赖.
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
data/repo/<package_name>/
|
||||
├── manifest.toml # 元信息: title, author, package, desc
|
||||
├── typedef.toml # 通用元数据、谜题定义、注解
|
||||
├── payload.toml # 记忆条目 (key=ident)
|
||||
├── algodata.json # 算法状态 (key=ident)
|
||||
└── schedule.toml # 轨道/复习策略
|
||||
```
|
||||
|
||||
### Repo 类设计
|
||||
|
||||
```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` 和 `algodata` 使用 `Lict` (列表+字典混合容器), 支持双模式访问.
|
||||
- `_generate_particles_data()` 在初始化时自动将 payload 数据转换为 `Nucleon` 所需的格式.
|
||||
- 默认保存列表 `default_save_list = ["algodata"]`, 仅持久化算法状态.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Lict 集合
|
||||
|
||||
`Lict` 继承 `MutableSequence`, 同时维护列表和字典访问:
|
||||
|
||||
```python
|
||||
lict = Lict()
|
||||
lict.append(("key1", value1)) # 列表追加
|
||||
lict["key1"] # 字典访问
|
||||
lict[0] # 索引访问
|
||||
lict.keys() # 所有键
|
||||
lict.dicted_data # 纯字典导出
|
||||
```
|
||||
|
||||
脏同步机制:修改列表时自动同步字典, 修改字典时自动同步列表. 用于 `payload` 和 `algodata` 的双模式访问需求.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 配置系统 (ConfigDict)
|
||||
|
||||
`ConfigDict` 继承 `UserDict`, 是**单例模式**的 TOML 懒加载配置管理器.
|
||||
|
||||
### 配置目录约定
|
||||
|
||||
```
|
||||
data/config/
|
||||
├── _.toml # 顶层默认值 (递归合并)
|
||||
├── interface/
|
||||
│ ├── _.toml # interface 层默认值
|
||||
│ ├── global.toml
|
||||
│ └── puzzles.toml
|
||||
├── services/
|
||||
│ ├── _.toml # services 层默认值
|
||||
│ ├── audio.toml
|
||||
│ └── tts.toml
|
||||
└── repo/
|
||||
└── _.toml
|
||||
```
|
||||
|
||||
- `_.toml` 文件 = 该目录层级的默认值, 合并到父级
|
||||
- 带后缀文件 = 按需懒加载
|
||||
- 子目录 = 递归子配置
|
||||
|
||||
### 上下文管理
|
||||
|
||||
```python
|
||||
from heurams.context import config_var, ConfigContext
|
||||
|
||||
# 全局访问
|
||||
config = config_var.get()
|
||||
algo = config["interface"]["global"]["algorithm"]
|
||||
|
||||
# 作用域覆盖
|
||||
with ConfigContext(test_config):
|
||||
... # 临时使用测试配置
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 提供者系统 (Providers)
|
||||
|
||||
可插拔的后端实现, 通过 `providers/__init__.py` 中的字典注册.
|
||||
|
||||
| 类别 | 提供者 | 说明 |
|
||||
|------|--------|------|
|
||||
| **TTS** | `edge_tts` | Microsoft Edge TTS (在线) |
|
||||
| | `basetts` | 桩基类 (未实现) |
|
||||
| **Audio** | `playsound` | 跨平台音频播放 |
|
||||
| | `termux` | Android Termux 环境 |
|
||||
| **LLM** | `openai` | OpenAI 兼容 API (未完整实现) |
|
||||
|
||||
选择方式:`services/*.toml` 中的 `provider` 字段.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 谜题系统 (Puzzles)
|
||||
|
||||
谜题引擎用于在复习阶段生成评估视图:
|
||||
|
||||
| 谜题 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| **MCQ** | `mcq.py` | 选择题 (Multiple Choice) |
|
||||
| **Cloze** | `cloze.py` | 完形填空 (Cloze Deletion) |
|
||||
| **Recognition** | `recognition.py` | 认读识别 |
|
||||
| **Guess** | `guess.py` | 猜测词义 |
|
||||
| **Base** | `base.py` | 抽象基类 |
|
||||
|
||||
谜题通过轨道策略 (Orbital)在 `Expander` 中按概率展开, 每个原子可产生多个谜题, 每个谜题独立评分.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 服务层
|
||||
|
||||
| 服务 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| **Config** | `config.py` | `ConfigDict(UserDict)` TOML 懒加载单例 |
|
||||
| **Logger** | `logger.py` | `get_logger(name)` → 层级日志 (`heurams.*`) |
|
||||
| **Timer** | `timer.py` | `get_daystamp()` / `get_timestamp()`, 支持可配置覆盖 |
|
||||
| **Audio** | `audio_service.py` | 音频播放, 路由到配置的音频提供者 |
|
||||
| **TTS** | `tts_service.py` | 文本转语音, 路由到配置的 TTS 提供者 |
|
||||
| **Favorites** | `favorite_service.py` | JSON5 持久化的收藏管理器 (单例) |
|
||||
| **Attic** | `attic.py` | 结构化 pickle 持久化, 支持 `<DAYSTAMP>`/`<TIMESTAMP>` 占位符 |
|
||||
| **Hasher** | `hasher.py` | MD5 哈希 |
|
||||
| **Epath** | `epath.py` | 点符号嵌套字典访问 (`epath(dct, "a.b.c")`) |
|
||||
| **TextProc** | `textproc.py` | `truncate()`, `domize()`, `undomize()` |
|
||||
|
||||
日志系统:每个模块通过 `get_logger(__name__)` 创建自己的日志器, 日志文件 10MB 轮转, 最多 5 个备份, 追加到 `heurams.log`.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 复习全流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 用户
|
||||
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: 开始复习
|
||||
UI->>Router: Router(atoms)
|
||||
Router->>Procession: 创建复习队列
|
||||
Router->>Procession: 创建新记忆队列
|
||||
Router->>Procession: 创建总复习队列
|
||||
Procession->>Expander: 展开当前原子
|
||||
Expander->>Expander: 解析轨道策略
|
||||
Expander-->>UI: 展示谜题
|
||||
User->>UI: 评分 (1-5)
|
||||
UI->>Expander: report(rating)
|
||||
Expander->>Expander: forward() 下一个谜题
|
||||
Expander-->>UI: 下一个谜题或回溯
|
||||
Expander->>Expander: finish() → retronly
|
||||
Expander-->>UI: Recognition 回溯
|
||||
User->>UI: 最终评分
|
||||
UI->>Atom: revise()
|
||||
Atom->>Electron: revisor(min_rate)
|
||||
Electron->>Algo: revisor(algodata, feedback)
|
||||
Algo-->>Electron: 更新 algodata
|
||||
Algo-->>Atom: 更新 interval, next_date
|
||||
Procession->>Procession: forward() 下一原子
|
||||
Procession-->>Router: 队列完成
|
||||
Router->>Router: 切换阶段
|
||||
Router-->>UI: 完成 (finished)
|
||||
UI->>User: 显示总结
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
1. **无数据库** — 所有持久化基于 TOML/JSON 文件目录, 方便版本管理和手动编辑.
|
||||
2. **Lict 双模式访问** — payload 和 algodata 同时支持列表迭代和字典查找, 兼顾批处理和随机访问.
|
||||
3. **物理隐喻分离** — 内容 (Nucleon)、状态 (Electron)、策略 (Orbital) 三者正交, 可独立替换, 便于组合不同算法和内容类型.
|
||||
4. **transitions 状态机** — 使用 `transitions` 库实现 Router → Procession → Expander 三层嵌套状态机, 每个层次职责明确.
|
||||
5. **Evalizer eval 模板** — 使用 `eval()` 实现动态模板替换, 功能灵活但存在安全风险 (标记为待替换).
|
||||
6. **配置单例** — `ConfigDict` 以规范化路径为键实现单例, 避免多实例导致的配置不一致问题.
|
||||
7. **评分累积** — 原子在多谜题阶段的最终评分取所有谜题的最低评分 (`min_rate`), 确保严格评估.
|
||||
+22
-99
@@ -1,116 +1,39 @@
|
||||
# 贡献指南与二次开发
|
||||
# 贡献指南
|
||||
|
||||
欢迎为此项目做出贡献!
|
||||
本项目是一个开源项目, 我们鼓励社区成员参与改进.
|
||||
|
||||
## 开发规范
|
||||
|
||||
分支划分:
|
||||
|
||||
- `main` 分支: 稳定版本, 仅当稳定版本释出或修补版本时将 `dev` 合并到 `main` 上
|
||||
- `dev` 分支: 主线开发版本, 自身仅用于非重构的问题修复和整合功能分支
|
||||
- 功能与重构分支: 从 `dev` 分支创建, 命名格式为 `feature/描述` 或 `fix/描述` 或 `refactor/v版本号`
|
||||
- 不要将功能与重构分支先应被合并至 `dev` 后在 `dev` 完成文档开发后再释出至 `main`
|
||||
|
||||
代码格式化:
|
||||
|
||||
- 安装工具:
|
||||
```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` 提交, 但 `main` 和 `dev` 分支上的代码应尽量整洁, 以便合并时审查
|
||||
|
||||
提交消息:
|
||||
## 开发流程
|
||||
|
||||
1. **分支划分**:
|
||||
- `main` 分支: 稳定版本
|
||||
- `dev` 分支: 开发版本
|
||||
- 功能分支: 从 `dev` 分支创建, 命名格式为 `feature/描述` 或 `fix/描述` 或 `refactor/描述`
|
||||
2. **代码风格**:
|
||||
- 请使用 Black 格式化代码
|
||||
- 遵循 PEP 8 规范
|
||||
- 添加适当的文档字符串
|
||||
3. **提交消息**:
|
||||
- 使用简体中文或英文撰写清晰的提交消息
|
||||
- 提交消息格式: 遵循 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 规范, 建议使用 `koji` 工具
|
||||
|
||||
合并方式:
|
||||
|
||||
- 为了一致性和可追溯性, 项目自 v0.4.0 重构后重新初始化仓库起就禁止使用 Fast-forward 合并
|
||||
- 可以设置 `git config merge.ff false`
|
||||
- 格式: 遵循 Conventional Commits 规范
|
||||
|
||||
## 设置开发环境
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://git.pluv27.top/pluv/HeurAMS
|
||||
git clone https://gitea.imwangzhiyu.xyz/ajax/HeurAMS
|
||||
|
||||
cd HeurAMS
|
||||
|
||||
# 可能需要切换到 dev 分支
|
||||
git checkout dev
|
||||
# 你可能需要切换分支
|
||||
|
||||
# 如果决定使用 uv (推荐)
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
## 首先要安装uv, 例如通过 pip 或者其他包管理器
|
||||
python3 -m pip install uv
|
||||
|
||||
uv sync # 同步开发运行环境
|
||||
|
||||
uv run heurams # 验证包安装
|
||||
|
||||
uv run tui # 启动 TUI
|
||||
|
||||
# 如果决定使用原生 python 环境 (不推荐, 但我们保留了这种方式以便在不便支持 uv 与硬链接的环境和文件系统(例如 termux)运行 HeurAMS)
|
||||
|
||||
## 安装依赖并将 HeurAMS 安装为本地包
|
||||
python3 -m pip install -r requirements.txt
|
||||
python3 -m pip install -e .
|
||||
|
||||
python3 -m heurams # 验证安装
|
||||
python3 -m heurams.__interface__ # 启动 TUI
|
||||
# 安装开发版本
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## 许可证与外部引用
|
||||
## 许可证
|
||||
|
||||
贡献者拥有其贡献部分的版权同意其贡献将在 AGPL-3.0 许可证下发布.
|
||||
|
||||
如果您认为有必要引入其他开源的 vendor, 请在 PR 中注明或手动联系以便我们审查 vendor 许可证并更改此处和网站上的关于与版权声明
|
||||
|
||||
如果您认为有必要引入其他专有的网络服务(就像现在项目中的 edgetts), 请也在 PR 中注明
|
||||
|
||||
如果您认为有必要升级某个依赖或运行环境的版本, 请也在 PR 中注明
|
||||
|
||||
## 新的用户界面前端与其他语言移植
|
||||
|
||||
HeurAMS 被设计为一个可独立于前端的程序库, 这意味着:
|
||||
|
||||
- 我们的内置 Textual TUI 前端不是唯一可用的前端
|
||||
|
||||
- 您可以在自己的项目中以独立进程/服务调用 HeurAMS (但不能在代码中链接), 而免于受 AGPL-3.0 "污染". 为了这点, 我们正在完善可选择启用的跨进程 RPC 模块, 这将成为潜进内核的跨平台标准件.
|
||||
|
||||
- 如果您有一个自己开发的且可用的 HeurAMS 前端 (例如我们暂未实现的 flutter 前端), 并且以 AGPL-3.0/GPL-3.0 开放源代码, 可以联系我们将它转移到 HeurAMS 的官方仓库中以便共同维护, 您将保留您的版权并可主导该仓库下的开发工作 :)
|
||||
|
||||
- 如果您通过独立进程/服务调用方式开发了另外的软件, 开源但不愿使用 AGPL-3.0/GPL-3.0 许可证, 也可以联系我们, 我们乐于将您的项目链接添加到友链中
|
||||
|
||||
- 如果您想创建程序库的其他语言 (例如 dart 或 rust) 版本以协助此语言下的方便集成, 并且同样以 AGPL-3.0/GPL-3.0 开放源代码, 也可以联系我们将它转移到 HeurAMS 的官方仓库中以便共同维护, 您将保留您的版权并可主导该仓库下的开发工作 :)
|
||||
|
||||
## 软件开发之外的贡献
|
||||
|
||||
即使您不是软件开发人员, 我们也欢迎您加入贡献!
|
||||
|
||||
您可以:
|
||||
|
||||
- 协助创建各种语言的翻译来翻译软件的界面 (但我们目前还没有 i18n 平台, 所以如果您想贡献翻译, 可能需要手动联系我们)
|
||||
- 制作图像、主题、音效乃至制作开放的记忆单元集给其他用户使用
|
||||
- 改进软件配套的文档
|
||||
- 维护软件的开发/交流群组
|
||||
- 给其他用户答疑解惑或分享自己的经验
|
||||
- 在讨论区提出新想法或反馈问题
|
||||
|
||||
您的角色您来定!
|
||||
贡献者同意其贡献将在 AGPL-3.0 许可证下发布.
|
||||
|
||||
@@ -1,142 +1,182 @@
|
||||
# 潜进 (HeurAMS) - 启发式辅助记忆调度器
|
||||
# 潜进 (HeurAMS) - 启发式辅助记忆程序
|
||||
|
||||
## 概述
|
||||
"潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是为习题册, 古诗词, 及其他问答/记忆/理解型知识设计的多用途辅助记忆软件, 提供动态规划的优化记忆方案
|
||||
|
||||
"潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划, 也是一个开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
|
||||
## 关于此仓库
|
||||
"潜进" 软件组项目包含多个子项目
|
||||
此仓库包含了 "潜进" 项目的核心和基于 Textual 的基本用户界面的实现
|
||||
|
||||
## 项目结构
|
||||
|
||||
这个仓库是 "潜进" 的核心程序库在 python 语言下的实现\
|
||||
包含数据模型与框架, 并内置了基于 textual 框架的前端实现 (interface 子模块)\
|
||||
除了通过内置前端进行学习外, 开发者也能在 python 环境中导入 `heurams` 库, 使用框架构建其他辅助记忆功能前端或其他应用程序
|
||||
## 开发进程
|
||||
- 0.0.x: 简易调度器实现与最小原型.
|
||||
- 0.1.x: 命令行操作的调度器.
|
||||
- 0.2.x: 使用 Textual 构建富文本终端用户界面, 项目可行性验证, 采用 SM-2 原始算法, 评估方式为用户自评估的原型.
|
||||
- 0.3.x: 简单的多文件项目, 创建了记忆内容/算法数据结构, 基于 SM-2 改进算法的自动复习测评评估. 重点设计古诗文记忆理解功能, 以及 TUI 界面实现, 简单的 TTS 集成.
|
||||
- 0.4.x: 使用模块管理解耦设计, 增加文档与类型标注, 采用上下文设计模式的隐式依赖注入与遵从 IoC, 注册器设计的算法与功能实现, 支持其他调度算法模块 (SM-2, FSRS) 与谜题模块, 采用日志调试, 更新文件格式, 引入动态数据模式(宏驱动的动态内容生成), 与基于文件的策略调控, 更佳的用户数据处理, 加入模块化扩展集成, 将算法数据格式换为 json 提高性能, 采用 provider-service 抽象架构, 支持切换服务提供者, 整体兼容性改进.
|
||||
> 下一步?
|
||||
> 使用 Flutter 构建酷酷的现代化前端, 增加云同步/文档源服务...
|
||||
|
||||
## 特性
|
||||
|
||||
### 间隔重复调度器
|
||||
### 间隔迭代算法
|
||||
> 许多出版物都广泛讨论了不同重复间隔对学习效果的影响. 特别是, 间隔效应被认为是一种普遍现象. 间隔效应是指, 如果重复的间隔是分散/稀疏的, 而不是集中重复, 那么学习任务的表现会更好. 因此, 有观点提出, 学习中使用的最佳重复间隔是**最长的、但不会导致遗忘的间隔**.
|
||||
- 采用经实证的 SM-2 间隔迭代算法, 此算法亦用作 Anki 闪卡记忆软件的默认闪卡调度器
|
||||
- 动态规划每个记忆单元的记忆间隔时间表
|
||||
- 动态跟踪记忆反馈数据, 优化长期记忆保留率与稳定性
|
||||
|
||||
> 许多出版物都广泛讨论了不同重复间隔对学习效果的影响. 特别是, 间隔效应被认为是一种普遍现象. 间隔效应是指, 如果重复的间隔是分散/稀疏的, 而不是集中重复, 那么学习任务的表现会更好. 因此, 有观点提出, 学习中使用的最佳重复间隔是**最长的, 但不会导致遗忘的间隔**.
|
||||
|
||||
- 软件开箱即用, 无需多加配置即可使用默认的 `SM-2` 算法进行学习
|
||||
- 此外, 算法模块是 "潜进" 内核 (heurams.kernel) 中的一等公民, 内核天然支持插拔各型算法
|
||||
- 无需安装繁杂的插件即可分单元集完成算法快速切换与调优, 研究者可以方便地修改算法模块以便捷地进行研究与测试
|
||||
- 内置 `SM-2` 简单间隔重复算法, 此算法亦用作 `Anki` 闪卡记忆软件的默认闪卡调度器
|
||||
- 还内置 `NSP-0` 筛选用非间隔重复算法以便快速筛选记忆内容, `FSRS` 先进间隔重复算法作为效率更高的调度器, 与 `SM-15M (移植自 sm.js 项目)` 复杂间隔重复算法(逆向工程)
|
||||
- 算法模块可以标记记忆项目, 也可以动态规划每个记忆单元的记忆间隔时间表, 动态跟踪记忆反馈数据, 以优化长期记忆保留率与稳定性
|
||||
- 得益于项目的模块化架构与单元集结构设计, 一个项目甚至可以与任意种算法共存并互通, 这对研究者及想探索/实验高效率方法的用户极其友好
|
||||
|
||||
### 多模态学习进程
|
||||
|
||||
与 `Anki` 的 SQLite `apkg` 包不同, 潜进项目坚持使用人类可读的文件夹组织单元集, 这带来了若干好处, 包括:
|
||||
|
||||
- 人类可读: 您可以用任意工具, 乃至一个记事本自由修改记忆载荷数据而无需打开软件
|
||||
- 元数据配置: 配置自由度极高, 可以任意组合, 重造, 乃至创造新内容
|
||||
- 测验, 算法与知识互相隔离: 您的记忆项目不再是单一的闪卡, 而是 `载荷(payload)` 和 `谜题(puzzle)` 通过 `元数据(typedef)` 抽象成的 `核子(nucleon)` 对象, 在程序内部和 `算法数据(algodata)` 抽象成的 `电子`, `调度设置(schedule)` 定义的 `轨道(orbital)` 共同有机组合成的运行时对象 `原子(atom)`! 这意味着一条知识不仅可以用若干不同的算法规划, 还可以用多种并行的谜题类型测验, 极大地提升您的学习效果和丰富度. 作为学习者, 您无需担忧这些概念复杂--仅需从云端下载单元集即可开箱即用上述特性!
|
||||
- 多模态学习
|
||||
- 软件自身集成了文本转语音 (TTS) , 音频与语言模型 (LLM) 模块, 这些功能乃至功能本身都是可插拔, 可扩展, 可切换驱动的, 这为内容创建了极大的丰富度
|
||||
- 软件内置多种谜题类型, 包括选择题 (MCQ), 填空题 (Cloze) 与识别题 (Recognition), 您可在同一单元应用多种, 或是选择启用
|
||||
- 软件天然支持动态内容生成, 支持宏驱动的模板系统, 根据上下文乃至语言模型动态生成知识点的解析
|
||||
- 在间隔重复研究尚被 SuperMemo 系列独占的时代, Wozniak 就早已表示 "如果不能理解知识, 就无需记忆它". 今天, 我们依然相信理解是记忆的基石
|
||||
- 云同步与分享优化: 由于我们的记忆数据和单元集文件都是文本文件, 故可进行快速的增量同步而无需完整地上传所有文件, 并且设计天然支持分享内容的版本控制, 如果您想分享单文件, 我们也支持 .zip/.tar.gz/.tar.xz 导入与导出
|
||||
- 性能提升: 得益于现代且支持分块的文件组织结构, 潜进能在保持高自由度的同时仅使用 python 就能达到敏捷且低占用的用户体验
|
||||
### 学习进程优化
|
||||
- 逐字解析: 支持逐字详细释义解析
|
||||
- 语法分析: 接入生成式人工智能, 支持古文结构交互式解析
|
||||
- 自然语音: 集成微软神经网络文本转语音 (TTS) 技术
|
||||
- 多种谜题类型: 选择题 (MCQ)、填空题 (Cloze)、识别题 (Recognition)
|
||||
- 动态内容生成: 支持宏驱动的模板系统, 根据上下文动态生成题目
|
||||
|
||||
### 实用用户界面
|
||||
|
||||
- 响应式 Textual 框架构建的跨平台 TUI 界面
|
||||
- 支持触屏/鼠标/键盘多操作模式
|
||||
- 简洁直观的复习流程设计
|
||||
|
||||
## 快速开始
|
||||
### 架构特性
|
||||
- 模块化设计: 算法、谜题、服务提供者可插拔替换
|
||||
- 上下文管理: 使用 ContextVar 实现隐式依赖注入
|
||||
- 数据持久化: TOML 配置与内容, JSON 算法状态
|
||||
- 服务抽象: 音频播放、TTS、LLM 通过 provider 架构支持多种后端
|
||||
- 完整日志系统: 带轮转的日志记录, 便于调试
|
||||
|
||||
### 从包管理器安装
|
||||
|
||||
潜进(heurams) 处于早期开发考虑, 尚未上架 PyPI, 但您可以用我们的基础设施安装稳定版和开发版本.
|
||||
|
||||
> [!CAUTION]
|
||||
> 对于部分 Linux 发行版和 Android Termux 用户:\
|
||||
> 您需要先行安装 `cmake` 和 `libzmq` 才能正确安装项目的 `zmq` 依赖\
|
||||
> 例如在 termux 上先运行 `pkg install cmake clang libzmq`\
|
||||
> 项目功能本身不依赖它, 但需要该依赖用于启动可选的调试服务器
|
||||
|
||||
#### 稳定版本
|
||||
|
||||
```
|
||||
python -m pip install heurams[all] -i https://pypi.pluv27.top/root/stable/+simple/ # 安装全部可选依赖(推荐)
|
||||
```
|
||||
|
||||
#### 开发版本
|
||||
|
||||
```
|
||||
python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/ # 安装全部可选依赖(推荐)
|
||||
```
|
||||
|
||||
#### 依赖组说明
|
||||
|
||||
由于部分依赖只被少数功能需要, 所以我们把可选依赖分得比较细, 前面的命令会安装所有可选依赖, 以下是依赖组列表
|
||||
|
||||
- 基础依赖: (只能驱动程序库)
|
||||
- tabulate: 终端表格
|
||||
- toml: TOML 文件加载
|
||||
- transitions: 状态机依赖
|
||||
- `interface` 依赖组: (基本用户界面依赖)
|
||||
- textual: 终端用户界面
|
||||
- psutil: 获取系统信息
|
||||
- `algo-fsrs` 依赖组:
|
||||
- py-fsrs: FSRS 算法模块需要
|
||||
- `tts-edgetts` 依赖组:
|
||||
- edge-tts:微软文本转语音
|
||||
- `misc-jieba` 依赖组:
|
||||
- jieba: 中文智能分词所需
|
||||
- `llm-openai` 依赖组:
|
||||
- openai: OpenAI API 所需
|
||||
- `audio-playsound` 依赖组:
|
||||
- playsound: 通用音频播放
|
||||
- pygobject: playsound 依赖
|
||||
- `dev` 依赖组:
|
||||
- zmq: 远程调试服务器所需
|
||||
- pytest: 测试所需
|
||||
- pytest-cov: 测试所需
|
||||
- `all` 依赖组:
|
||||
- 包含以上所有依赖
|
||||
## 安装
|
||||
|
||||
### 从源码安装
|
||||
1. 克隆仓库:
|
||||
```bash
|
||||
git clone https://gitea.imwangzhiyu.xyz/ajax/HeurAMS
|
||||
cd HeurAMS
|
||||
```
|
||||
|
||||
我们提供原生 python 和 uv 两种安装方式.\
|
||||
详见[贡献指南](CONTRIBUTING.md).
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. 以开发模式安装包:
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
### 启动应用
|
||||
```bash
|
||||
# 在任一目录(建议是空目录或者包根目录, 将被用作存放数据)下运行
|
||||
python -m heurams.interface
|
||||
```
|
||||
|
||||
### 数据目录结构
|
||||
应用会在工作目录下创建以下数据目录:
|
||||
- `data/nucleon/`: 记忆内容 (TOML 格式)
|
||||
- `data/electron/`: 算法状态 (JSON 格式)
|
||||
- `data/orbital/`: 策略配置 (TOML 格式)
|
||||
- `data/cache/`: 音频缓存文件
|
||||
- `data/template/`: 内容模板
|
||||
|
||||
首次运行时会自动创建这些目录.
|
||||
|
||||
## 配置
|
||||
|
||||
配置文件位于 `config/config.toml`(相对于工作目录). 如果不存在, 会使用内置的默认配置.
|
||||
|
||||
## 项目结构
|
||||
|
||||
详见[架构说明](ARCHITECTURE.md).
|
||||
### 架构图
|
||||
|
||||
## 参与项目
|
||||
以下 Mermaid 图展示了 HeurAMS 的主要组件及其关系:
|
||||
|
||||
欢迎参与到项目协作中! 请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) 了解贡献指南.
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "用户界面层 (TUI)"
|
||||
TUI[Textual TUI]
|
||||
Widgets[界面组件]
|
||||
Screens[应用屏幕]
|
||||
end
|
||||
|
||||
subgraph "服务层"
|
||||
Config[配置管理]
|
||||
Logger[日志系统]
|
||||
Timer[时间服务]
|
||||
AudioService[音频服务]
|
||||
TTSService[TTS服务]
|
||||
OtherServices[其他服务]
|
||||
end
|
||||
|
||||
subgraph "内核层"
|
||||
Algorithms[算法模块]
|
||||
Particles[数据模型]
|
||||
Puzzles[谜题模块]
|
||||
Reactor[调度反应器]
|
||||
end
|
||||
|
||||
subgraph "提供者层"
|
||||
AudioProvider[音频提供者]
|
||||
TTSProvider[TTS提供者]
|
||||
OtherProviders[其他提供者]
|
||||
end
|
||||
|
||||
subgraph "数据层"
|
||||
Files[本地文件数据]
|
||||
end
|
||||
|
||||
subgraph "上下文管理"
|
||||
Context[ConfigContext]
|
||||
CtxVar[config_var]
|
||||
end
|
||||
|
||||
TUI --> Config
|
||||
TUI --> Logger
|
||||
TUI --> AudioService
|
||||
TUI --> TTSService
|
||||
TUI --> OtherServices
|
||||
Config --> Files
|
||||
Config --> Context
|
||||
AudioService --> AudioProvider
|
||||
TTSService --> TTSProvider
|
||||
OtherServices --> OtherProviders
|
||||
Reactor --> Algorithms
|
||||
Reactor --> Particles
|
||||
Reactor --> Puzzles
|
||||
Particles --> Files
|
||||
Algorithms --> Files
|
||||
```
|
||||
|
||||
### 目录结构
|
||||
```
|
||||
src/heurams/
|
||||
├── __init__.py # 包入口点
|
||||
├── context.py # 全局上下文、路径、配置上下文管理器
|
||||
├── services/ # 核心服务
|
||||
│ ├── config.py # 配置管理
|
||||
│ ├── logger.py # 日志系统
|
||||
│ ├── timer.py # 时间服务
|
||||
│ ├── audio_service.py # 音频播放抽象
|
||||
│ └── tts_service.py # 文本转语音抽象
|
||||
├── kernel/ # 核心业务逻辑
|
||||
│ ├── algorithms/ # 间隔重复算法 (FSRS, SM2)
|
||||
│ ├── particles/ # 数据模型 (Atom, Electron, Nucleon, Orbital)
|
||||
│ ├── puzzles/ # 谜题类型 (MCQ, cloze, recognition)
|
||||
│ └── reactor/ # 调度和处理逻辑
|
||||
├── providers/ # 外部服务提供者
|
||||
│ ├── audio/ # 音频播放实现
|
||||
│ ├── tts/ # 文本转语音实现
|
||||
│ └── llm/ # LLM 集成
|
||||
├── interface/ # Textual TUI 界面
|
||||
│ ├── widgets/ # UI 组件
|
||||
│ ├── screens/ # 应用屏幕
|
||||
│ └── __main__.py # 应用入口点
|
||||
└── default/ # 默认配置和数据模板
|
||||
```
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献!请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) 了解贡献指南.
|
||||
|
||||
## 许可证
|
||||
|
||||
### 项目本身
|
||||
|
||||
本项目基于 AGPL-3.0 许可证开源. 详见根目录下 [LICENSE](LICENSE) 文件.
|
||||
|
||||
### 第三方代码
|
||||
|
||||
项目在 `src/heurams/vendor/` 目录下嵌入或在其他位置间接使用了以下第三方代码(可能有修改):
|
||||
|
||||
#### py-fsrs (open-spaced-repetition)
|
||||
|
||||
- 上游版本: 6.3.1
|
||||
- 引用方式: vendor
|
||||
- 位置: `src/heurams/vendor/pyfsrs/`
|
||||
- 原项目: [py-fsrs](https://github.com/open-spaced-repetition/py-fsrs)
|
||||
- 原版权: Copyright (c) 2026 Open Spaced Repetition Contributors
|
||||
- 原许可证: MIT License
|
||||
|
||||
#### SM.js (slaypni)
|
||||
|
||||
- 上游版本: commit `6e3bb4afaf484426deb4a9fa3bcffe42ac066b45` (2015年2月4日上游已停止维护)
|
||||
- 引用方式: 将 coffeescript 重写为 python 并间接引用, 数学原理一致; 并对重写后代码进行逻辑, 性能与标准化 API 改进
|
||||
- 位置: `src/heurams/kernel/algorithms/sm15m*.py`
|
||||
- 原项目: [SM.js](https://github.com/slaypni/SM-15)
|
||||
- 原版权: Copyright (c) 2014 Kazuaki Tanida
|
||||
- 原许可证: MIT License
|
||||
|
||||
本项目受益于他们无私且优秀的工作.
|
||||
本项目基于 AGPL-3.0 许可证开源. 详见 [LICENSE](LICENSE) 文件.
|
||||
@@ -0,0 +1,39 @@
|
||||
# [调试] 将更改保存到文件
|
||||
persist_to_file = 1
|
||||
|
||||
# [调试] 覆写时间, 设为 -1 以禁用
|
||||
daystamp_override = -1
|
||||
timestamp_override = -1
|
||||
|
||||
# [调试] 一键通过
|
||||
quick_pass = 1
|
||||
|
||||
# 对于每个项目的默认新记忆原子数量
|
||||
scheduled_num = 8
|
||||
|
||||
# UTC 时间戳修正 仅用于 UNIX 日时间戳的生成修正, 单位为秒
|
||||
timezone_offset = +28800 # 中国标准时间 (UTC+8)
|
||||
|
||||
[puzzles] # 谜题默认配置
|
||||
|
||||
[puzzles.mcq]
|
||||
max_riddles_num = 2
|
||||
|
||||
[puzzles.cloze]
|
||||
min_denominator = 3
|
||||
|
||||
[paths] # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径
|
||||
nucleon_dir = "./data/nucleon"
|
||||
electron_dir = "./data/electron"
|
||||
orbital_dir = "./data/orbital"
|
||||
cache_dir = "./data/cache"
|
||||
template_dir = "./data/template"
|
||||
|
||||
[services] # 定义服务到提供者的映射
|
||||
audio = "playsound" # 可选项: playsound(通用), termux(仅用于支持 Android Termux), mpg123(TODO)
|
||||
tts = "edgetts" # 可选项: edgetts
|
||||
llm = "openai" # 可选项: openai
|
||||
|
||||
[providers.llm.openai] # 与 OpenAI 相容的语言模型接口服务设置
|
||||
url = ""
|
||||
key = ""
|
||||
@@ -1,5 +0,0 @@
|
||||
_services_desc = '服务模块设置'
|
||||
_providers_desc = '驱动模块设置'
|
||||
_repo_desc = '单元集独立设置'
|
||||
_interface_desc = '基本用户界面设置'
|
||||
_global_desc = '底层设置'
|
||||
@@ -1,21 +0,0 @@
|
||||
zmq_debug = false
|
||||
_zmq_debug_desc = "[调试] ZeroMQ 调试服务器, 这会在 zmq_debug_port 上打开一个服务器\n调试工具可远程在 HeurAMS 内执行任意 python 代码, 无必要请关闭"
|
||||
zmq_debug_port = 5555
|
||||
_zmq_debug_port_desc = "[调试] ZeroMQ 调试服务器端口"
|
||||
enable_built_in_interface = true
|
||||
_enable_built_in_interface_desc = "启用内置基本用户界面\n(当且仅当 HeurAMS 作为程序库时禁用, 以跳过用户界面逻辑)"
|
||||
_paths_desc = "用户数据路径定义"
|
||||
|
||||
[paths]
|
||||
data = "./data"
|
||||
_data_desc = "用户数据根目录"
|
||||
cache = "./data/cache"
|
||||
_cache_desc = "缓存根目录\n(如音频缓存在 voice 子目录)"
|
||||
config = "./data/config"
|
||||
_config_desc = "配置文件根目录"
|
||||
repo = "./data/repo"
|
||||
_repo_desc = "记忆单元集根目录"
|
||||
misc = "./data/misc"
|
||||
_misc_desc = "扩展程序和 whisper 等模块的数据存储根目录"
|
||||
_addons = "./data/addons"
|
||||
__addons_desc = "扩展程序根目录"
|
||||
@@ -1,4 +0,0 @@
|
||||
_global_desc = "用户界面通用设置"
|
||||
_widgets_desc = "组件设置"
|
||||
_screens_desc = "界面设置"
|
||||
_puzzles_desc = "谜题生成器设置"
|
||||
@@ -1,25 +0,0 @@
|
||||
show_header = true
|
||||
_show_header_desc = "展示界面顶部的标题栏\n如果您想节省这一行空间, 可以禁用它"
|
||||
clock_on_header = true
|
||||
_clock_on_header_desc = "在界面顶部的标题栏显示时间"
|
||||
change_window_title = true
|
||||
_change_window_title_desc = "更改终端模拟器窗口的标题\n如果禁用了 header, 则建议启用"
|
||||
persist_to_file = true
|
||||
_persist_to_file_desc = "[调试] 将记忆更改保存到文件"
|
||||
quick_pass = true
|
||||
_quick_pass_desc = "[调试] 启用快速应答功能(跳过测验)"
|
||||
auto_pass = false
|
||||
_auto_pass_desc = "[调试] 自动通过测试模式"
|
||||
scheduled_num = 420
|
||||
_scheduled_num_desc = "默认记忆单元数量\n可被单元集设置覆盖"
|
||||
refresh_on_resume = true
|
||||
_refresh_on_resume_desc = "[调试] 每当 Screen 激活后都刷新状态"
|
||||
algorithm = "SM-2"
|
||||
_algorithm_desc = "默认记忆调度算法\n可被单元集设置覆盖"
|
||||
|
||||
[_algorithm_candidate]
|
||||
NSP-0 = "筛选用非间隔重复调度器"
|
||||
none = "不设置默认调度器"
|
||||
SM-2 = "第二代 SuperMemo 简单间隔重复调度器\nWozniak 于 1987 年提出, Anki 的默认算法"
|
||||
SM-15M = "类第15代 SuperMemo 复杂间隔重复调度器\n不稳定且逆向工程"
|
||||
FSRS = "先进开放间隔重复调度器"
|
||||
@@ -1,2 +0,0 @@
|
||||
_cloze_desc = "填空题"
|
||||
_mcq_desc = "选择题"
|
||||
@@ -1,2 +0,0 @@
|
||||
min_denominator = "2"
|
||||
_min_denominator_desc = "设空比例系数的倒数"
|
||||
@@ -1,2 +0,0 @@
|
||||
max_riddles_num = "2"
|
||||
_max_riddles_num_desc = "单次生成的最大谜题数量"
|
||||
@@ -1 +0,0 @@
|
||||
_recognition_desc = "用于 '辨识' 组件的设置"
|
||||
@@ -1,2 +0,0 @@
|
||||
autovoice = true
|
||||
_autovoice_desc = "自动语音播放"
|
||||
@@ -1 +0,0 @@
|
||||
_tts_desc = '文本转语音驱动'
|
||||
@@ -1 +0,0 @@
|
||||
_edgetts_desc = "微软文本转语音驱动"
|
||||
@@ -1,25 +0,0 @@
|
||||
voice = "zh-CN-XiaoxiaoNeural"
|
||||
_voice_desc = "音色"
|
||||
|
||||
[_voice_candidate]
|
||||
zh-CN-XiaoxiaoNeural = "晓晓: 中文温柔女声"
|
||||
zh-CN-XiaoyiNeural = "晓伊: 中文甜美女声"
|
||||
zh-CN-XiaochenNeural = "晓辰: 中文知性女声"
|
||||
zh-CN-XiaohanNeural = "晓涵: 中文优雅女声"
|
||||
zh-CN-XiaomengNeural = "晓梦: 中文梦幻女声"
|
||||
zh-CN-XiaomoNeural = "晓墨: 中文文艺女声"
|
||||
zh-CN-XiaoqiuNeural = "晓秋: 中文成熟女声"
|
||||
zh-CN-XiaoruiNeural = "晓睿: 中文智慧女声"
|
||||
zh-CN-XiaoshuangNeural = "晓双: 中文活泼女声"
|
||||
zh-CN-XiaoxuanNeural = "晓萱: 中文清新女声"
|
||||
zh-CN-XiaoyanNeural = "晓颜: 中文柔美女声"
|
||||
zh-CN-XiaoyouNeural = "晓悠: 中文悠扬女声"
|
||||
zh-CN-XiaozhenNeural = "晓甄: 中文端庄女声"
|
||||
zh-CN-YunxiNeural = "云希: 中文清朗男声"
|
||||
zh-CN-YunyangNeural = "云扬: 中文阳光男声"
|
||||
zh-CN-YunjianNeural = "云健: 中文稳重男声"
|
||||
zh-CN-YunfengNeural = "云枫: 中文磁性男声"
|
||||
zh-CN-YunhaoNeural = "云皓: 中文豪迈男声"
|
||||
zh-CN-YunxiaNeural = "云夏: 中文热情男声"
|
||||
zh-CN-YunyeNeural = "云野: 中文野性男声"
|
||||
zh-CN-YunzeNeural = "云泽: 中文深沉男声"
|
||||
@@ -1,2 +0,0 @@
|
||||
_cngk-t_desc = "高考必备古诗文-测试"
|
||||
_cngk_desc = "高考必备古诗文"
|
||||
@@ -1,11 +0,0 @@
|
||||
algorithm = "NSP-0"
|
||||
_algorithm_desc = "记忆调度算法"
|
||||
scheduled_num = 420
|
||||
_scheduled_num_desc = "单次记忆单元数量"
|
||||
|
||||
[_algorithm_candidate]
|
||||
NSP-0 = "筛选用非间隔重复调度器"
|
||||
none = "不设置默认调度器"
|
||||
SM-2 = "第二代 SuperMemo 简单间隔重复调度器"
|
||||
SM-15M = "第15代 SuperMemo 复杂间隔重复调度器 (不稳定且逆向工程)"
|
||||
FSRS = "先进开放间隔重复调度器"
|
||||
@@ -1,11 +0,0 @@
|
||||
algorithm = "SM-2"
|
||||
_algorithm_desc = "记忆调度算法"
|
||||
scheduled_num = 20
|
||||
_scheduled_num_desc = "单次记忆单元数量"
|
||||
|
||||
[_algorithm_candidate]
|
||||
NSP-0 = "筛选用非间隔重复调度器"
|
||||
none = "不设置默认调度器"
|
||||
SM-2 = "第二代 SuperMemo 简单间隔重复调度器"
|
||||
SM-15M = "第15代 SuperMemo 复杂间隔重复调度器 (不稳定且逆向工程)"
|
||||
FSRS = "先进开放间隔重复调度器"
|
||||
@@ -1,5 +0,0 @@
|
||||
_audio_desc = '音频服务'
|
||||
_llm_desc = '语言模型服务'
|
||||
_sync_desc = '数据同步服务'
|
||||
_timer_desc = '时间服务'
|
||||
_tts_desc = '文本转语音服务'
|
||||
@@ -1,9 +0,0 @@
|
||||
provider = "playsound"
|
||||
_provider_desc = "音频驱动类型"
|
||||
|
||||
[_provider_candidate]
|
||||
playsound = "python 跨平台音频系统"
|
||||
termux = "Android Termux 音频系统"
|
||||
mpg123 = "通用音频系统, 依赖系统 mpg123"
|
||||
pulseaudio = "高级音频路由系统"
|
||||
none = "不使用音频"
|
||||
@@ -1,6 +0,0 @@
|
||||
provider = "openai"
|
||||
_provider_desc = "模型接口类型"
|
||||
|
||||
[_provider_candidate]
|
||||
openai = "OpenAI 风格 API, 同时支持与其相容的模型服务 (如 deepseek)"
|
||||
none = "不使用语言大模型"
|
||||
@@ -1,7 +0,0 @@
|
||||
provider = "webdav"
|
||||
_provider_desc = "同步服务驱动类型"
|
||||
|
||||
[_provider_candidate]
|
||||
webdav = "WebDAV 兼容网络文件系统 (包括 webdavs)"
|
||||
official = "官方同步服务器"
|
||||
none = "不使用同步服务器"
|
||||
@@ -1,6 +0,0 @@
|
||||
daystamp_override = -1
|
||||
_daystamp_override_desc = "[调试] 覆写 UNIX 日时间戳, 单位为日\n(设为 -1 禁用)"
|
||||
timestamp_override = -1
|
||||
_timestamp_override_desc = "[调试] 覆写 UNIX 时间戳, 单位为秒\n(设为 -1 禁用)"
|
||||
timezone_offset = 28800
|
||||
_timezone_offset_desc = "时区偏移设置, 用于取消跨天时区误差, 单位为秒\n(如 28800 为 UTC+8.0, 中国标准时间)"
|
||||
@@ -1,7 +0,0 @@
|
||||
provider = "edgetts"
|
||||
_provider_desc = "文本转语音驱动类型"
|
||||
|
||||
[_provider_candidate]
|
||||
edgetts = "微软神经网络语音合成, 依赖微软网络服务"
|
||||
espeak = "低保真度本地语音合成"
|
||||
none = "不使用文本转语音"
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,4 +0,0 @@
|
||||
title = "高考必背古诗文-筛选"
|
||||
package = "cngk-t"
|
||||
author = "__heurams__"
|
||||
desc = "高考古诗文 60 篇"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
schedule = ["quick_review", "recognition", "final_review"]
|
||||
|
||||
[routes]
|
||||
quick_review = [["FillBlank", "1.0"], ["Recognition", "1.0"]]
|
||||
recognition = [["FillBlank", "1.0"]]
|
||||
final_review = [["FillBlank", "1.0"], ["Recognition", "1.0"]]
|
||||
|
||||
[annotation]
|
||||
"quick_review" = "复习旧知"
|
||||
"recognition" = "新知识"
|
||||
"final_review" = "总复习"
|
||||
@@ -1,17 +0,0 @@
|
||||
[annotation]
|
||||
note = "笔记"
|
||||
keyword_note = "关键词翻译"
|
||||
translation = "语句翻译"
|
||||
delimiter = "分隔符"
|
||||
content = "内容"
|
||||
tts_text = "文本转语音文本"
|
||||
|
||||
[common]
|
||||
delimiter = "/"
|
||||
tts_text = "eval:payload['content'].replace('/', '')"
|
||||
|
||||
[common.puzzles] # 谜题定义
|
||||
# 我们称 "Recognition" 为 recognition 谜题的 alia
|
||||
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:payload['content']", secondary = ["eval:payload['keyword_note']", "eval:payload['note']"], top_dim = ["eval:payload['translation']"] }
|
||||
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:payload['content']", primary = "eval:payload['content']", mapping = "eval:payload['keyword_note']", jammer = "eval:list(payload['keyword_note'].values())", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
|
||||
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:payload['content']", delimiter = "eval:nucleon['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Nucleon 是 HeurAMS 软件项目使用的基于 TOML 的专有源文件格式, 版本 5
|
||||
# 建议使用的 MIME 类型: application/vnd.xyz.imwangzhiyu.heurams-nucleon.v5+toml
|
||||
|
||||
[__metadata__]
|
||||
[__metadata__.attribution] # 元信息
|
||||
desc = "带有宏支持的空白模板"
|
||||
|
||||
[__metadata__.annotation] # 键批注
|
||||
|
||||
[__metadata__.formation] # 文件配置
|
||||
#delimiter = "/"
|
||||
#tts_text = "eval:nucleon['content'].replace('/', '')"
|
||||
|
||||
[__metadata__.orbital.puzzles] # 谜题定义
|
||||
# 我们称 "Recognition" 为 recognition 谜题的 alia
|
||||
#"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:nucleon['content']", secondary = ["eval:nucleon['keyword_note']", "eval:nucleon['note']"], top_dim = ["eval:nucleon['translation']"] }
|
||||
#"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:nucleon['content']", mapping = "eval:nucleon['keyword_note']", jammer = "eval:nucleon['keyword_note']", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
|
||||
#"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:nucleon['content']", delimiter = "eval:metadata['formation']['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}
|
||||
|
||||
[__metadata__.orbital.schedule] # 内置的推荐学习方案
|
||||
#quick_review = [["FillBlank", "1.0"], ["SelectMeaning", "0.5"], ["recognition", "1.0"]]
|
||||
#recognition = [["Recognition", "1.0"]]
|
||||
#final_review = [["FillBlank", "0.7"], ["SelectMeaning", "0.7"], ["recognition", "1.0"]]
|
||||
+18
-67
@@ -1,76 +1,27 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "heurams"
|
||||
version = "0.5.0"
|
||||
authors = [{ name = "pluvium27", email = "pluvium27@outlook.com" }]
|
||||
description = "Heuristic Auxiliary Memory Scheduler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
version = "0.4.0"
|
||||
description = "Heuristic Assisted Memory Scheduler"
|
||||
license = {file = "LICENSE"}
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Operating System :: OS Independent",
|
||||
"Topic :: Education",
|
||||
"Intended Audience :: Education",
|
||||
]
|
||||
license = "AGPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
dependencies = [ # 这些依赖只能驱动 kernel 程序库
|
||||
"tabulate>=0.10.0",
|
||||
"toml>=0.10.2",
|
||||
"transitions>=0.9.3",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
interface = [ # 基本用户界面依赖
|
||||
"textual>=8.2.3",
|
||||
"psutil>=7.2.2",
|
||||
]
|
||||
algo-fsrs = [
|
||||
"fsrs>=6.3.1", # FSRS 算法底层依赖
|
||||
]
|
||||
tts-edgetts = [
|
||||
"edge-tts>=7.2.8", # 微软 TTS
|
||||
]
|
||||
misc-jieba = [
|
||||
"jieba>=0.42.1", # 用于中文智能分词
|
||||
]
|
||||
llm-openai = [
|
||||
"openai>=2.32.0",
|
||||
]
|
||||
audio-playsound = [
|
||||
keywords = ["spaced-repetition", "memory", "learning", "tui", "textual", "flashcards", "education"]
|
||||
dependencies = [
|
||||
"bidict==0.23.1",
|
||||
"playsound==1.2.2",
|
||||
"pygobject>=3.56.2", # playsound 依赖它
|
||||
]
|
||||
dev = [ # 调试所需依赖
|
||||
"zmq>=0.0.0", # 用于 ZMQ 远程调试服务器, 在 linux 上建议先安装 libzmq
|
||||
"pytest>=8.0.0", # 用于普通测试
|
||||
"pytest-cov>=6.0.0",
|
||||
]
|
||||
all = [
|
||||
"heurams[algo-fsrs]",
|
||||
"heurams[tts-edgetts]",
|
||||
"heurams[misc-jieba]",
|
||||
"heurams[llm-openai]",
|
||||
"heurams[audio-playsound]",
|
||||
"heurams[dev]",
|
||||
"textual==5.3.0",
|
||||
"toml==0.10.2",
|
||||
]
|
||||
readme = "README.md"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://ams.pluv.top"
|
||||
Issues = "https://github.com/heurams/heurams/issues"
|
||||
|
||||
[project.scripts]
|
||||
heurams = "heurams.__main__:main"
|
||||
tui = "heurams.interface.__main__:main"
|
||||
heurams-tui = "heurams.interface.__main__:main"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py", "*_test.py"]
|
||||
pythonpath = ["src"]
|
||||
markers = [
|
||||
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||
"integration: marks tests as integration tests",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.7.19"]
|
||||
build-backend = "uv_build"
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
+2
-6
@@ -1,8 +1,4 @@
|
||||
edge-tts==7.0.2
|
||||
jieba==0.42.1
|
||||
openai==1.0.0
|
||||
bidict==0.23.1
|
||||
playsound==1.2.2
|
||||
tabulate>=0.9.0
|
||||
textual==7.0.0
|
||||
textual==5.3.0
|
||||
toml==0.10.2
|
||||
transitions==0.9.3
|
||||
|
||||
+3
-19
@@ -1,23 +1,7 @@
|
||||
import heurams.services.version as ver
|
||||
|
||||
|
||||
# __main__.py
|
||||
def main():
|
||||
prompt = f"""HeurAMS {ver.ver} 已经被成功地安装在系统中.
|
||||
HeurAMS 被设计为一个带有辅助记忆调度器功能的软件包, 无法直接被执行, 但可被其他 Python 程序调用.
|
||||
若您想启动内置的基本用户界面:
|
||||
prompt = """HeurAMS 已经被成功地安装在系统中.
|
||||
但 HeurAMS 被设计为一个带有辅助记忆调度器功能的软件包, 无法直接被执行, 但可被其他 Python 程序调用.
|
||||
若您想启动内置的基本用户界面,
|
||||
请运行 python -m heurams.interface,
|
||||
或者 python -m heurams.interface.__main__
|
||||
python 代指您使用的解释器, 在某些发行版中可能是 python3, 而 python 命令被指向了 python2.
|
||||
尽管项目保留了 requirements.txt, 我们仍不推荐使用系统 python 和原始 venv 进行开发.
|
||||
项目的推荐开发环境工具是 uv.
|
||||
如果你的环境已经安装了 uv:
|
||||
先运行 uv sync 同步环境, 此命令只需要执行一遍, uv 会自动处理依赖.
|
||||
然后通过运行 uv run tui 启动内置基本用户界面.
|
||||
此时您的解释器在项目目录里的 .venv/bin 中, 使用 IDE 开发前, 务必切换解释器!
|
||||
注意: 一个常见的误区是, 执行 interface 下的 __main__.py 运行基本用户界面, 这会导致 Python 上下文环境异常, 请不要这样做."""
|
||||
print(prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+24
-19
@@ -6,35 +6,40 @@
|
||||
import pathlib
|
||||
from contextvars import ContextVar
|
||||
|
||||
from heurams.services.config import ConfigDict
|
||||
from heurams.services.config import ConfigFile
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
# 默认数据目录, 以包目录下的 data 为准
|
||||
# 用户数据目录, 以运行目录下的 data 为准
|
||||
|
||||
rootdir: pathlib.Path = pathlib.Path(__file__).parent
|
||||
"""包目录路径, 也就是 heurams 目录."""
|
||||
|
||||
workdir = pathlib.Path.cwd()
|
||||
"""工作目录路径."""
|
||||
# 默认配置文件路径规定: 以包目录为准
|
||||
# 用户配置文件路径规定: 以运行目录为准
|
||||
# 数据文件路径规定: 以运行目录为准
|
||||
|
||||
rootdir = pathlib.Path(__file__).parent
|
||||
print(f"rootdir: {rootdir}")
|
||||
logger = get_logger(__name__)
|
||||
logger.debug(f"包目录: {rootdir}")
|
||||
logger.debug(f"项目根目录: {rootdir}")
|
||||
workdir = pathlib.Path.cwd()
|
||||
print(f"workdir: {workdir}")
|
||||
logger.debug(f"工作目录: {workdir}")
|
||||
|
||||
(workdir / "data" / "config").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
config_var: ContextVar[ConfigDict].get = ContextVar(
|
||||
"config_var",
|
||||
default=ConfigDict(workdir / "data" / "config"),
|
||||
config_var: ContextVar[ConfigFile] = ContextVar(
|
||||
"config_var", default=ConfigFile(rootdir / "default" / "config" / "config.toml")
|
||||
)
|
||||
"""配置对象的全局引用对象."""
|
||||
try:
|
||||
config_var: ContextVar[ConfigFile] = ContextVar(
|
||||
"config_var", default=ConfigFile(workdir / "config" / "config.toml")
|
||||
) # 配置文件
|
||||
print("已加载自定义用户配置")
|
||||
logger.info("已加载自定义用户配置, 路径: %s", workdir / "config" / "config.toml")
|
||||
except Exception as e:
|
||||
print("未能加载自定义用户配置")
|
||||
logger.warning("未能加载自定义用户配置, 错误: %s", e)
|
||||
|
||||
# runtime_var: ContextVar = ContextVar('runtime_var', default=dict()) # 运行时共享数据
|
||||
|
||||
|
||||
class ConfigContext:
|
||||
"""
|
||||
功能完备的上下文管理器
|
||||
用于临时切换配置引用对象的作用域, 支持嵌套使用
|
||||
用于临时切换配置的作用域, 支持嵌套使用
|
||||
|
||||
Example:
|
||||
>>> with ConfigContext(test_config):
|
||||
@@ -42,7 +47,7 @@ class ConfigContext:
|
||||
>>> get_daystamp() # 恢复原配置
|
||||
"""
|
||||
|
||||
def __init__(self, config_provider: ConfigDict):
|
||||
def __init__(self, config_provider: ConfigFile):
|
||||
self.config_provider = config_provider
|
||||
self._token = None
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# [调试] 将更改保存到文件
|
||||
persist_to_file = 1
|
||||
|
||||
# [调试] 覆写时间, 设为 -1 以禁用
|
||||
daystamp_override = -1
|
||||
timestamp_override = -1
|
||||
|
||||
# [调试] 一键通过
|
||||
quick_pass = 1
|
||||
|
||||
# 对于每个项目的默认新记忆原子数量
|
||||
scheduled_num = 8
|
||||
|
||||
# UTC 时间戳修正 仅用于 UNIX 日时间戳的生成修正, 单位为秒
|
||||
timezone_offset = +28800 # 中国标准时间 (UTC+8)
|
||||
|
||||
[puzzles] # 谜题默认配置
|
||||
|
||||
[puzzles.mcq]
|
||||
max_riddles_num = 2
|
||||
|
||||
[puzzles.cloze]
|
||||
min_denominator = 3
|
||||
|
||||
[paths] # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径
|
||||
nucleon_dir = "./data/nucleon"
|
||||
electron_dir = "./data/electron"
|
||||
orbital_dir = "./data/orbital"
|
||||
cache_dir = "./data/cache"
|
||||
template_dir = "./data/template"
|
||||
|
||||
[services] # 定义服务到提供者的映射
|
||||
audio = "playsound" # 可选项: playsound(通用), termux(仅用于支持 Android Termux), mpg123(TODO)
|
||||
tts = "edgetts" # 可选项: edgetts
|
||||
llm = "openai" # 可选项: openai
|
||||
|
||||
[providers.llm.openai] # 与 OpenAI 相容的语言模型接口服务设置
|
||||
url = ""
|
||||
key = ""
|
||||
@@ -1,3 +1,2 @@
|
||||
# Interface - 用户界面
|
||||
|
||||
与界面系统**强绑定**的相关代码文件, "界面系统" 在此处是基本界面实现相关的 Textual 框架
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
from time import sleep, perf_counter
|
||||
|
||||
# import gc
|
||||
# gc.set_threshold(100, 1, 1)
|
||||
print("欢迎使用基本用户界面!")
|
||||
print("加载配置与上下文... ", end="", flush=True)
|
||||
_start_all = perf_counter()
|
||||
_start = _start_all
|
||||
from heurams.context import rootdir, workdir, config_var
|
||||
|
||||
_end = perf_counter()
|
||||
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
|
||||
|
||||
print("加载用户界面框架... ", end="", flush=True)
|
||||
_start = perf_counter()
|
||||
from textual.app import App
|
||||
from textual.widgets import Button
|
||||
|
||||
_end = perf_counter()
|
||||
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
|
||||
|
||||
print("加载用户界面布局... ", 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 . import shim
|
||||
|
||||
_end = perf_counter()
|
||||
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
|
||||
|
||||
print(f"组件目录: {rootdir}")
|
||||
print(f"工作目录: {workdir}")
|
||||
_end_all = perf_counter()
|
||||
print(f"前置工作共计耗时: {round(1000 * (_end_all - _start_all))}ms")
|
||||
|
||||
|
||||
class HeurAMSApp(App):
|
||||
TITLE = "潜进"
|
||||
CSS_PATH = rootdir / "interface" / "css" / "main.tcss"
|
||||
SUB_TITLE = "启发式辅助记忆调度器"
|
||||
BINDINGS = [
|
||||
("q", "go_back", "退出"),
|
||||
("d", "toggle_dark", "主题"),
|
||||
("n", "app.push_screen('navigator')", "导航"),
|
||||
("s", "app.push_screen('setting')", "设置"),
|
||||
("z", "app.push_screen('about')", "关于"),
|
||||
]
|
||||
SCREENS = {
|
||||
"dashboard": DashboardScreen,
|
||||
"precache_all": PrecachingScreen,
|
||||
"synctool": SyncScreen,
|
||||
"about": AboutScreen,
|
||||
"navigator": NavigatorScreen,
|
||||
"setting": SettingScreen,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.push_screen("dashboard")
|
||||
|
||||
def action_go_back(self) -> None:
|
||||
self.exit() # go_back 在最顶层是退出, Screen 会再次定义为返回, 键位都是 q, 免得不一样
|
||||
|
||||
def action_do_nothing(
|
||||
self,
|
||||
) -> None: # 用来给没使用/禁用的快捷键占位, 因为 Binding 删除不了
|
||||
pass
|
||||
|
||||
# 移除烦人的 "rich traceback"
|
||||
# Textual 官方不会管这破事, 写 Rich 写入脑了导致的
|
||||
# 不知道哪来的自信改标准库的 traceback
|
||||
# https://github.com/Textualize/textual/discussions/6255
|
||||
# NOTE: 进行 textual 版本升级时, 确保查看过上游代码, 尤其是 App 的 _exception
|
||||
# 如果行为变了就把下面的删了 (虽然有 fallback)
|
||||
def _fatal_error(self):
|
||||
if hasattr(self, "_exception"):
|
||||
self._close_messages_no_wait()
|
||||
raise self._exception
|
||||
super()._fatal_error() # fallback
|
||||
|
||||
def panic(self, *args):
|
||||
if hasattr("_exception"):
|
||||
self._close_messages_no_wait()
|
||||
raise self._exception
|
||||
super().panic(*args) # ditto
|
||||
@@ -1,20 +1,53 @@
|
||||
from heurams.interface import *
|
||||
from heurams.context import config_var
|
||||
from textual.app import App
|
||||
from textual.widgets import Button
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
import threading
|
||||
import zmq
|
||||
import pickle
|
||||
|
||||
from .screens.about import AboutScreen
|
||||
from .screens.dashboard import DashboardScreen
|
||||
from .screens.nucreator import NucleonCreatorScreen
|
||||
from .screens.precache import PrecachingScreen
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HeurAMSApp(App):
|
||||
TITLE = "潜进"
|
||||
CSS_PATH = "css/main.tcss"
|
||||
SUB_TITLE = "启发式辅助记忆调度器"
|
||||
BINDINGS = [
|
||||
("q", "quit", "退出"),
|
||||
("d", "toggle_dark", "切换色调"),
|
||||
("1", "app.push_screen('dashboard')", "仪表盘"),
|
||||
("2", "app.push_screen('precache_all')", "缓存管理器"),
|
||||
("3", "app.push_screen('nucleon_creator')", "创建新单元"),
|
||||
("0", "app.push_screen('about')", "版本信息"),
|
||||
]
|
||||
SCREENS = {
|
||||
"dashboard": DashboardScreen,
|
||||
"nucleon_creator": NucleonCreatorScreen,
|
||||
"precache_all": PrecachingScreen,
|
||||
"about": AboutScreen,
|
||||
}
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.push_screen("dashboard")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.exit(event.button.id)
|
||||
|
||||
def action_do_nothing(self):
|
||||
print("DO NOTHING")
|
||||
self.refresh()
|
||||
|
||||
|
||||
def environment_check():
|
||||
from pathlib import Path
|
||||
|
||||
logger.debug("检查环境路径")
|
||||
subdir = ["cache/voice", "repo", "global", "config"]
|
||||
for i in subdir:
|
||||
i = Path(config_var.get()["global"]["paths"]["data"]) / i
|
||||
|
||||
for i in config_var.get()["paths"].values():
|
||||
i = Path(i)
|
||||
if not i.exists():
|
||||
logger.info("创建目录: %s", i)
|
||||
print(f"创建 {i}")
|
||||
@@ -25,46 +58,32 @@ def environment_check():
|
||||
logger.debug("环境检查完成")
|
||||
|
||||
|
||||
def start_debug_server(app):
|
||||
logger = get_logger("zmq_debug")
|
||||
context = zmq.Context()
|
||||
socket = context.socket(zmq.REP)
|
||||
port = config_var.get()["global"].get("zmq_debug_port", 5555)
|
||||
socket.bind(f"tcp://*:{port}")
|
||||
logger.info(f"ZMQ Debug server started on port {port}")
|
||||
first = 1
|
||||
while True:
|
||||
msg = socket.recv()
|
||||
code = pickle.loads(msg)
|
||||
namespace = {"app": app, "logger": logger, "config_var": config_var}
|
||||
if first:
|
||||
app.title += " [调试已连接]"
|
||||
first = 0
|
||||
def is_subdir(parent, child):
|
||||
try:
|
||||
# 先尝试 eval
|
||||
result = eval(code, namespace)
|
||||
socket.send(pickle.dumps(f"成功: {result}"))
|
||||
except SyntaxError:
|
||||
# 再尝试 exec
|
||||
try:
|
||||
exec(code, namespace)
|
||||
socket.send(pickle.dumps(f"成功: 执行完成"))
|
||||
except Exception as e:
|
||||
socket.send(pickle.dumps(f"错误: {e}"))
|
||||
except Exception as e:
|
||||
socket.send(pickle.dumps(f"错误: {e}"))
|
||||
child.relative_to(parent)
|
||||
logger.debug("is_subdir: %s 是 %s 的子目录", child, parent)
|
||||
return 1
|
||||
except:
|
||||
logger.debug("is_subdir: %s 不是 %s 的子目录", child, parent)
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 开发模式
|
||||
from heurams.context import config_var, rootdir, workdir
|
||||
|
||||
if is_subdir(Path(rootdir), Path(os.getcwd())):
|
||||
os.chdir(Path(rootdir) / ".." / "..")
|
||||
print(f'转入开发数据目录: {Path(rootdir)/".."/".."}')
|
||||
|
||||
environment_check()
|
||||
|
||||
app = HeurAMSApp()
|
||||
|
||||
if config_var.get()["global"].get("zmq_debug", False):
|
||||
threading.Thread(target=start_debug_server, args=(app,), daemon=True).start()
|
||||
|
||||
app.run(inline=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
app.run()
|
||||
|
||||
|
||||
def main():
|
||||
app.run()
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
NavigatorScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
|
||||
#dialog {
|
||||
grid-size: 2;
|
||||
grid-gutter: 1 1;
|
||||
grid-rows: 1fr 3;
|
||||
padding: 0 1;
|
||||
width: 46;
|
||||
height: 12;
|
||||
border: thick $background 80%;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
.repo-list {
|
||||
|
||||
}
|
||||
|
||||
#header {
|
||||
height: 3;
|
||||
}
|
||||
|
||||
#analysis {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.repo-list-item {
|
||||
layout: grid;
|
||||
grid-size: 1;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
.repo-list-item-shortcut {
|
||||
dock: right;
|
||||
offset: -5% 0
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#puzzle_container > * {
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#operations {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0 1 0 0;
|
||||
padding: 0 1 0 1;
|
||||
}
|
||||
|
||||
#main_container {
|
||||
|
||||
}
|
||||
|
||||
#previewer_container {
|
||||
|
||||
}
|
||||
|
||||
.unit-statline {
|
||||
|
||||
}
|
||||
|
||||
#title {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
.foot {
|
||||
align-vertical: bottom;
|
||||
}
|
||||
|
||||
.setting-switch {
|
||||
dock: right;
|
||||
}
|
||||
|
||||
Select {
|
||||
width: 55%;
|
||||
dock: right;
|
||||
}
|
||||
|
||||
Input {
|
||||
width: 55%;
|
||||
dock: right;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
width: 100%;
|
||||
height: 4;
|
||||
padding: 0 0 1 0;
|
||||
}
|
||||
@@ -1,105 +1,79 @@
|
||||
"""关于界面"""
|
||||
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Footer, Header, Label, Markdown
|
||||
|
||||
from textual import events, on
|
||||
from textual.widgets import Button, Footer, Header, Label, Markdown, Static
|
||||
|
||||
import heurams.services.version as version
|
||||
from heurams.context import *
|
||||
import platform
|
||||
import shutil
|
||||
import psutil
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
class AboutScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("z", "go_back", "关于"),
|
||||
]
|
||||
SUB_TITLE = "关于"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="about_container"):
|
||||
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()
|
||||
memory_info = self._get_memory_info()
|
||||
|
||||
about_text = f"""
|
||||
# 关于 "潜进"
|
||||
|
||||
主程序库版本: `{version.ver}-python`
|
||||
用户界面分支: `Textual TUI (基本用户界面)`
|
||||
用户界面版本: `{version.ver}`
|
||||
API 版本代号: `{version.codename.capitalize()}`
|
||||
版本 {version.ver} {version.stage.capitalize()}
|
||||
|
||||
一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.
|
||||
一个开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
|
||||
开发代号: {version.codename.capitalize()}
|
||||
|
||||
以 AGPL-3.0 开放源代码, 这直接意味着任何个体直接基于此代码对外或内部提供的应用和服务, 无论本地或网络, 必须向所有用户公开完整修改后的源代码, 且继续沿用 AGPL-3.0 协议.
|
||||
一个基于启发式算法的开放源代码记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.
|
||||
|
||||
您正使用的 TUI 用户界面是 python 版本程序库自带的基本用户界面, 以作为第一个全功能前端实现与程序库测试套件, 位于程序库根目录中的 interface 文件夹.
|
||||
以 AGPL-3.0 开放源代码
|
||||
|
||||
您可在项目主页 https://ams.pluv27.top 获取用户指南, 开发文档与软件更新.
|
||||
开发人员:
|
||||
|
||||
如果您觉得这个软件有用, 可以考虑参与贡献, 或在它的源代码仓库给它添加一个星标 :)
|
||||
- Wang Zhiyu([@pluvium27](https://github.com/pluvium27)): 项目作者
|
||||
|
||||
您的慷慨支持, 我们必当涌泉相报.
|
||||
特别感谢:
|
||||
|
||||
开发人员列表:
|
||||
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SuperMemo-2 算法
|
||||
- [Thoughts Memo](https://www.zhihu.com/people/L.M.Sherlock): 文献参考
|
||||
|
||||
- Wang Zhiyu([@pluvium27](https://github.com/pluvium27)): 发起项目与主要维护者
|
||||
# 参与贡献
|
||||
|
||||
特别感谢以下人士, 他们的算法与理论构成了此软件现有算法的基石:
|
||||
我们是一个年轻且包容的社区, 由技术人员, 设计师, 文书工作者, 以及创意人员共同构成,
|
||||
|
||||
- [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): 中文文献参考
|
||||
通过我们协力开发的软件为所有人谋取福祉.
|
||||
|
||||
上述工作不可避免地让我们确立了下列价值观 (取自 KDE 宣言):
|
||||
|
||||
# 运行环境信息
|
||||
- 开放治理 确保更多人能参与我们的领导和决策进程;
|
||||
|
||||
Python 解释器版本: {python_version}
|
||||
Python 解释器路径: {sys.executable}
|
||||
Textual 框架版本: {textual_version}
|
||||
终端模拟器: {terminal_info}
|
||||
操作系统版本: {os_version}
|
||||
存储余量: {disk_usage}
|
||||
内存大小: {memory_info}
|
||||
- 自由软件 确保我们的工作成果随时能为所有人所用;
|
||||
|
||||
报告问题时, 请复制这些信息到问题描述, 并上传软件日志 `heurams.log` 作为附件, 以协助开发者定位错误
|
||||
- 多样包容 确保所有人都能加入社区并参加工作;
|
||||
|
||||
- 创新精神 确保新思路能不断涌现并服务于所有人;
|
||||
|
||||
- 共同产权 确保我们能团结一致;
|
||||
|
||||
- 迎合用户 确保我们的成果对所有人有用.
|
||||
|
||||
综上所述, 在为我们共同目标奋斗的过程中, 我们认为上述价值观反映了我们社区的本质, 是我们始终如一地保持初心的关键所在.
|
||||
|
||||
这是一项立足于协作精神的事业, 它的运作和产出不受任何单一个人或者机构的操纵.
|
||||
|
||||
我们的共同目标是为人人带来高品质的辅助记忆 & 学习软件.
|
||||
|
||||
不管您来自何方, 我们都欢迎您加入社区并做出贡献.
|
||||
"""
|
||||
|
||||
# """
|
||||
# 学术数据
|
||||
|
||||
# "潜进" 的用户数据可用于科学方面的研究, 我们将在未来版本添加学术数据的收集和展示平台
|
||||
# """
|
||||
yield Markdown(about_text, classes="about-markdown")
|
||||
|
||||
yield Button(
|
||||
"返回主界面",
|
||||
id="back_button",
|
||||
variant="primary",
|
||||
flat=True,
|
||||
classes="back-button",
|
||||
)
|
||||
yield Footer()
|
||||
@@ -111,72 +85,3 @@ Textual 框架版本: {textual_version}
|
||||
event.stop()
|
||||
if event.button.id == "back_button":
|
||||
self.action_go_back()
|
||||
|
||||
def _get_textual_version(self) -> str:
|
||||
"""获取 Textual 框架版本"""
|
||||
try:
|
||||
import textual
|
||||
|
||||
return textual.__version__
|
||||
except (ImportError, AttributeError):
|
||||
return "未知"
|
||||
|
||||
def _get_terminal_info(self) -> str:
|
||||
"""获取终端模拟器信息"""
|
||||
terminal = shutil.which("terminal")
|
||||
if terminal:
|
||||
return terminal
|
||||
# 尝试从环境变量获取
|
||||
terminal_env = os.environ.get("TERM_PROGRAM") or os.environ.get("TERM")
|
||||
return terminal_env or "未知"
|
||||
|
||||
def _get_python_version(self) -> str:
|
||||
"""获取 Python 解释器版本"""
|
||||
return platform.python_version()
|
||||
|
||||
def _get_os_version(self) -> str:
|
||||
"""获取操作系统版本"""
|
||||
try:
|
||||
if platform.system() == "Darwin":
|
||||
# macOS
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
["sw_vers", "-productVersion"], capture_output=True, text=True
|
||||
)
|
||||
return f"macOS {result.stdout.strip()}"
|
||||
elif platform.system() == "Windows":
|
||||
# Windows
|
||||
return f"Windows {platform.release()}"
|
||||
elif platform.system() == "Linux":
|
||||
# Linux - 尝试获取发行版信息
|
||||
try:
|
||||
import distro
|
||||
|
||||
return f"{distro.name()} {distro.version()}"
|
||||
except (ImportError, AttributeError):
|
||||
return platform.platform()
|
||||
else:
|
||||
return platform.platform()
|
||||
except Exception:
|
||||
return platform.platform()
|
||||
|
||||
def _get_disk_usage(self) -> str:
|
||||
"""获取磁盘使用情况"""
|
||||
try:
|
||||
usage = psutil.disk_usage("/")
|
||||
free_gb = usage.free / (1024**3)
|
||||
total_gb = usage.total / (1024**3)
|
||||
percent_free = (free_gb / total_gb) * 100
|
||||
return f"{free_gb:.1f} GB ({percent_free:.1f}%)"
|
||||
except Exception:
|
||||
return "未知"
|
||||
|
||||
def _get_memory_info(self) -> str:
|
||||
"""获取内存信息"""
|
||||
try:
|
||||
memory = psutil.virtual_memory()
|
||||
total_gb = memory.total / (1024**3)
|
||||
return f"{total_gb:.1f} GB"
|
||||
except Exception:
|
||||
return "未知"
|
||||
|
||||
@@ -1,218 +1,147 @@
|
||||
"""仪表盘界面"""
|
||||
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
#!/usr/bin/env python3
|
||||
import pathlib
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer, Horizontal, Vertical
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static
|
||||
from textual import events, on
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import (Button, Footer, Header, Label, ListItem, ListView,
|
||||
Static)
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.timer as timer
|
||||
import heurams.services.version as version
|
||||
from heurams.context import *
|
||||
from heurams.kernel.particles import *
|
||||
from heurams.kernel.repolib import *
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .navigator import NavigatorScreen
|
||||
from .about import AboutScreen
|
||||
from .preparation import PreparationScreen
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DashboardScreen(Screen):
|
||||
"""主仪表盘屏幕"""
|
||||
|
||||
SUB_TITLE = "仪表盘"
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
]
|
||||
|
||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "dashboard.tcss"
|
||||
|
||||
repolink = reactive({})
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name, id, classes)
|
||||
self._load_data()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""组合界面组件"""
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
yield Header(show_clock=True)
|
||||
yield ScrollableContainer(
|
||||
Label(f'欢迎使用 "潜进" 启发式辅助记忆调度器', classes="title-label"),
|
||||
Label(f"当前 UNIX 日时间戳: {timer.get_daystamp()}"),
|
||||
Label(f'时区修正: UTC+{config_var.get()["timezone_offset"] / 3600}'),
|
||||
Label("选择待学习或待修改的记忆单元集:", classes="title-label"),
|
||||
ListView(id="union-list", classes="union-list-view"),
|
||||
Label(
|
||||
f'"潜进" 启发式辅助记忆调度器 | 版本 {version.ver} {version.codename.capitalize()} 2025'
|
||||
),
|
||||
)
|
||||
with ScrollableContainer():
|
||||
yield Horizontal( # 顶部的状态
|
||||
Vertical(
|
||||
Label(f"当前日时间戳: {timer.get_daystamp()}"),
|
||||
Label(
|
||||
f"应用时区修正: UTC+{str(config_var.get()['services']['timer']['timezone_offset'] / 3600).removesuffix('.0')}"
|
||||
),
|
||||
Label(
|
||||
f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}",
|
||||
),
|
||||
classes="left",
|
||||
),
|
||||
Vertical(
|
||||
Label(f"已加载 {len(self.repos)} 个单元集"),
|
||||
Label(
|
||||
f"共计 {reduce(lambda x, y: x + y, map(lambda x: x.progress['total'], self.repos))} 个单元"
|
||||
),
|
||||
Label(
|
||||
f"已激活 {reduce(lambda x, y: x + y, map(lambda x: x.progress['touched'], self.repos))} 个单元"
|
||||
),
|
||||
Label(f""),
|
||||
classes="right",
|
||||
),
|
||||
id="header",
|
||||
)
|
||||
yield ListView(id="repo_list", classes="repo-list") # 单元集选择
|
||||
from heurams.services.attic import Attic
|
||||
|
||||
a = Attic("ana", {"totaltime": 0, "openpuzzles": 0, "puzzles_err": 0})
|
||||
yield Label(f"版本 {version.ver} {version.stage.capitalize()}") # 版本信息
|
||||
yield Label(
|
||||
f"在 {round(a.data['totaltime'], 2)} 秒内处理了 {a.data['openpuzzles']} 个谜题, 正确率{'无法求解' if not a.data['openpuzzles'] else ' ' + str(round(100 * (1 - a.data['puzzles_err']/a.data['openpuzzles']), 2)) + '%'}, 平均速度{'无法求解' if not a.data['totaltime'] else ' ' + str(round(a.data['openpuzzles']/a.data['totaltime'], 2)) + ' 个每秒'}",
|
||||
id="analysis",
|
||||
) # 版本信息
|
||||
yield Footer()
|
||||
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
def item_desc_generator(self, filename) -> dict:
|
||||
"""简单分析以生成项目项显示文本
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
# https://github.com/Textualize/textual/discussions/4268
|
||||
# self.refresh(recompose=True) 此函数有问题且官方不管 而且性能低
|
||||
Returns:
|
||||
dict: 以数字为列表, 分别呈现单行字符串
|
||||
"""
|
||||
res = dict()
|
||||
filestem = pathlib.Path(filename).stem
|
||||
res[0] = f"{filename}\0"
|
||||
import heurams.kernel.particles as pt
|
||||
from heurams.kernel.particles.loader import load_electron
|
||||
|
||||
def _load_data(self):
|
||||
repo_dirs = Repo.probe_valid_repos_in_dir(
|
||||
Path(config_var.get()["global"]["paths"]["repo"])
|
||||
electron_file_path = pathlib.Path(config_var.get()["paths"]["electron_dir"]) / (
|
||||
filestem + ".json"
|
||||
)
|
||||
self.repos = list(map(Repo.from_repodir, repo_dirs))
|
||||
for repo in self.repos:
|
||||
self._analyse_repo(repo)
|
||||
|
||||
def _analyse_repo(self, repo: Repo):
|
||||
# need_review: 需要/不需要学习
|
||||
# nearest_review_time: 最近下次学习时间
|
||||
# progress: 进度
|
||||
## initial_time: 起始时间
|
||||
# package: 包名
|
||||
# prompt: 最终呈现信息
|
||||
repo.package = repo.manifest["package"]
|
||||
repo.nearest_review_time = float("inf")
|
||||
repo.progress = {
|
||||
"total": repo.data_length,
|
||||
"touched": 0,
|
||||
"have_activated_ever": False,
|
||||
}
|
||||
repo.preview = {
|
||||
"review": 0,
|
||||
"new": repo.config[
|
||||
"scheduled_num"
|
||||
], # TODO: 考虑之后在这里加点运算避免 SM-2 积压, 但现在需要的是直观!
|
||||
}
|
||||
initial_time = float("inf")
|
||||
for i in range(
|
||||
repo.data_length
|
||||
): # TODO: 增加异步性能优化, 但是学习数据属实规模小...
|
||||
e = pt.Electron.from_data(
|
||||
electronic_data=repo.electronic_data_lict[i],
|
||||
algo_name=repo.config["algorithm"],
|
||||
)
|
||||
# n = pt.Nucleon.from_data(repo.nucleonic_data_lict[i])
|
||||
if e.is_activated():
|
||||
repo.progress["have_activated_ever"] = True # 被激活过~
|
||||
repo.progress["touched"] += 1
|
||||
repo.nearest_review_time = min(repo.nearest_review_time, e.nextdate())
|
||||
if timer.get_daystamp() >= e.nextdate():
|
||||
repo.preview["review"] += 1
|
||||
# initial_time = min(initial_time, e.)
|
||||
repo.need_review = timer.get_daystamp() >= repo.nearest_review_time
|
||||
repo.prompt = f"""{repo.manifest['title']} \\[{repo.config['algorithm']}]
|
||||
[d]进度: {repo.progress['touched']}/{repo.progress['total']} ({round(repo.progress['touched']/repo.progress['total']*100, 1)}%)[/d]
|
||||
[d]{f'需要学习: {repo.preview['review']}R + {repo.preview['new']}U' if repo.need_review else (f"暂未开始: 0R + {repo.preview['new']}U" if not repo.progress['have_activated_ever'] else '无需操作')}[/d]"""
|
||||
logger.debug(f"电子文件路径: {electron_file_path}")
|
||||
|
||||
if electron_file_path.exists(): # 未找到则创建电子文件 (json)
|
||||
pass
|
||||
else:
|
||||
electron_file_path.touch()
|
||||
with open(electron_file_path, "w") as f:
|
||||
f.write("{}")
|
||||
electron_dict = load_electron(path=electron_file_path) # TODO: 取消硬编码扩展名
|
||||
logger.debug(electron_dict)
|
||||
is_due = 0
|
||||
is_activated = 0
|
||||
nextdate = 0x3F3F3F3F
|
||||
for i in electron_dict.values():
|
||||
i: pt.Electron
|
||||
logger.debug(i, i.is_due())
|
||||
if i.is_due():
|
||||
is_due = 1
|
||||
if i.is_activated():
|
||||
is_activated = 1
|
||||
nextdate = min(nextdate, i.nextdate())
|
||||
res[1] = f"下一次复习: {nextdate}\n"
|
||||
res[1] += f"{is_due if "需要复习" else "当前无需复习"}"
|
||||
if not is_activated:
|
||||
res[1] = " 尚未激活"
|
||||
return res
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""挂载组件时初始化"""
|
||||
repo_list_widget = self.query_one("#repo_list", ListView)
|
||||
union_list_widget = self.query_one("#union-list", ListView)
|
||||
|
||||
# 按下次复习时间排序
|
||||
repodirs = sorted(
|
||||
self.repos,
|
||||
key=lambda r: r.nearest_review_time,
|
||||
reverse=True, # 紧张的先复习
|
||||
probe = probe_all(0)
|
||||
|
||||
if len(probe["nucleon"]):
|
||||
for file in probe["nucleon"]:
|
||||
text = self.item_desc_generator(file)
|
||||
union_list_widget.append(
|
||||
ListItem(
|
||||
Label(text[0] + "\n" + text[1]),
|
||||
)
|
||||
|
||||
# 填充列表
|
||||
if not repodirs:
|
||||
repo_list_widget.append(
|
||||
)
|
||||
else:
|
||||
union_list_widget.append(
|
||||
ListItem(
|
||||
Static(
|
||||
f"在 {config_var.get()['global']['paths']['repo']} 中未找到任何仓库.\n"
|
||||
"请导入仓库后重启应用, 或者新建空的仓库."
|
||||
),
|
||||
id="not-found",
|
||||
"在 ./nucleon/ 中未找到任何内容源数据文件.\n请放置文件后重启应用.\n或者新建空的单元集."
|
||||
)
|
||||
)
|
||||
repo_list_widget.disabled = True
|
||||
return
|
||||
|
||||
for r in self.repos:
|
||||
self.repolink[str(r.manifest["package"])] = r # 用于规避 ctype id 对象还原
|
||||
# NOTE: 上一行不要使用 id(), id 可能被重用!
|
||||
list_item = ListItem(
|
||||
*[Label(line) for line in r.prompt.splitlines()],
|
||||
Button(
|
||||
f"开始学习",
|
||||
flat=True,
|
||||
variant="primary",
|
||||
id=f"slaunch_repo_{r.manifest['package']}",
|
||||
classes="repo-list-item-shortcut",
|
||||
),
|
||||
classes="repo-list-item",
|
||||
id=f"launch_repo_{r.manifest['package']}",
|
||||
)
|
||||
repo_list_widget.append(list_item)
|
||||
union_list_widget.disabled = True
|
||||
|
||||
def on_list_view_selected(self, event) -> None:
|
||||
"""处理列表项选择事件"""
|
||||
if not isinstance(event.item, ListItem):
|
||||
return
|
||||
|
||||
if "not-found" == event.item.id:
|
||||
selected_label = event.item.query_one(Label)
|
||||
if "未找到任何 .toml 文件" in str(selected_label.renderable): # type: ignore
|
||||
return
|
||||
|
||||
# 还原对象
|
||||
selected_repo = self.repolink[event.item.id.removeprefix("launch_repo_")]
|
||||
selected_filename = pathlib.Path(
|
||||
str(selected_label.renderable)
|
||||
.partition("\0")[0] # 文件名末尾截断, 保留文件名
|
||||
.replace("*", "")
|
||||
) # 去除markdown加粗
|
||||
|
||||
# 跳转到准备屏幕
|
||||
self.app.push_screen(PreparationScreen(selected_repo))
|
||||
nucleon_file_path = (
|
||||
pathlib.Path(config_var.get()["paths"]["nucleon_dir"]) / selected_filename
|
||||
)
|
||||
electron_file_path = pathlib.Path(config_var.get()["paths"]["electron_dir"]) / (
|
||||
str(selected_filename.stem) + ".json"
|
||||
)
|
||||
self.app.push_screen(PreparationScreen(nucleon_file_path, electron_file_path))
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
if event.button.id == "new_nucleon_button":
|
||||
# 切换到创建单元
|
||||
from .nucreator import NucleonCreatorScreen
|
||||
|
||||
newscr = NucleonCreatorScreen()
|
||||
self.app.push_screen(newscr)
|
||||
elif event.button.id == "precache_all_button":
|
||||
# 切换到缓存管理器
|
||||
from .precache import PrecachingScreen
|
||||
|
||||
precache_screen = PrecachingScreen()
|
||||
self.app.push_screen(precache_screen)
|
||||
elif event.button.id == "about_button":
|
||||
from .about import AboutScreen
|
||||
|
||||
about_screen = AboutScreen()
|
||||
self.app.push_screen(about_screen)
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
"""退出应用程序"""
|
||||
self.app.exit()
|
||||
|
||||
def action_open_navigator(self) -> None:
|
||||
"""打开导航器"""
|
||||
self.app.push_screen(NavigatorScreen())
|
||||
|
||||
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
|
||||
|
||||
launch(repo=self.repolink[event.button.id.removeprefix("slaunch_repo_")], app=self.app, scheduled_num=-1) # type: ignore
|
||||
# TODO: 这样启动的记忆实例的状态机无法绑定到 PreparationScreen 中
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
"""收藏夹管理器界面"""
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from textual import events, on
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import (
|
||||
Button,
|
||||
Footer,
|
||||
Header,
|
||||
Label,
|
||||
ListItem,
|
||||
ListView,
|
||||
Markdown,
|
||||
Static,
|
||||
)
|
||||
|
||||
from textual import events, on
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.repolib import Repo
|
||||
from heurams.services.favorite_service import FavoriteItem, favorite_manager
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class FavoriteManagerScreen(Screen):
|
||||
"""收藏夹管理器屏幕"""
|
||||
|
||||
SUB_TITLE = "收藏夹"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("d", "toggle_dark", ""),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name, id, classes)
|
||||
self.favorites: List[FavoriteItem] = []
|
||||
self._load_favorites()
|
||||
|
||||
def _load_favorites(self) -> None:
|
||||
"""加载收藏列表"""
|
||||
self.favorites = favorite_manager.get_all()
|
||||
logger.debug("加载 %d 个收藏项", len(self.favorites))
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""组合界面组件"""
|
||||
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
with ScrollableContainer(id="favorites-container"):
|
||||
if not self.favorites:
|
||||
yield Label("暂无收藏", classes="empty-label")
|
||||
yield Static("使用 * 键在记忆界面中添加收藏.")
|
||||
else:
|
||||
yield Label(f"共 {len(self.favorites)} 个收藏项", classes="count-label")
|
||||
yield ListView(id="favorites-list")
|
||||
yield Footer()
|
||||
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""挂载后填充列表"""
|
||||
if self.favorites:
|
||||
list_view = self.query_one("#favorites-list")
|
||||
for fav in self.favorites:
|
||||
list_view.append(self._create_favorite_item(fav)) # type: ignore
|
||||
|
||||
def _encode_favorite_key(self, repo_path: str, ident: str) -> str:
|
||||
"""编码仓库路径和标识符为安全的按钮 ID 部分"""
|
||||
# 使用 \x00 分隔两部分,然后进行 base64 编码
|
||||
combined = f"{repo_path}\x00{ident}"
|
||||
encoded = base64.urlsafe_b64encode(combined.encode()).decode()
|
||||
# 去掉填充的等号
|
||||
return encoded.rstrip("=")
|
||||
|
||||
def _decode_favorite_key(self, key: str) -> tuple[str, str]:
|
||||
"""解码按钮 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:
|
||||
"""创建收藏项列表项"""
|
||||
# 尝试获取仓库信息
|
||||
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
|
||||
content_preview = repo_info.get("content_preview", "") if repo_info else ""
|
||||
added_time = self._format_time(fav.added)
|
||||
|
||||
# 构建显示文本
|
||||
display_text = f"[b]{title}[/b] ({fav.ident})\n"
|
||||
if content_preview:
|
||||
display_text += f"{content_preview}\n"
|
||||
display_text += f"添加于: {added_time}"
|
||||
if fav.tags:
|
||||
display_text += f" 标签: {', '.join(fav.tags)}"
|
||||
|
||||
# 创建安全的按钮 ID
|
||||
button_key = self._encode_favorite_key(fav.repo_path, fav.ident)
|
||||
# 创建列表项,包含移除按钮
|
||||
container = ScrollableContainer(
|
||||
Markdown(display_text, classes="favorite-content"),
|
||||
Button("移除", id=f"remove-{button_key}", variant="error"),
|
||||
classes="favorite-item",
|
||||
)
|
||||
return ListItem(container)
|
||||
|
||||
def _get_repo_info(self, repo_path: str, fav: FavoriteItem) -> Optional[dict]:
|
||||
"""获取仓库信息(标题、原子内容预览)"""
|
||||
try:
|
||||
data_repo = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
||||
repo_dir = data_repo / repo_path
|
||||
if not repo_dir.exists():
|
||||
logger.warning("仓库目录不存在: %s", repo_dir)
|
||||
return None
|
||||
repo = Repo.from_repodir(repo_dir)
|
||||
# 获取原子内容预览
|
||||
content_preview = ""
|
||||
payload = repo.payload
|
||||
# 查找对应 ident 的 payload 条目
|
||||
for ident_key, content in payload:
|
||||
if ident_key == fav.ident:
|
||||
# 截断过长的内容
|
||||
if isinstance(content, dict) and "content" in content:
|
||||
text = content["content"]
|
||||
else:
|
||||
text = str(content)
|
||||
if len(text) > 100:
|
||||
content_preview = text[:100] + "..."
|
||||
else:
|
||||
content_preview = text
|
||||
break
|
||||
return {
|
||||
"title": repo.manifest["title"],
|
||||
"content_preview": content_preview,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("获取仓库信息失败: %s", e)
|
||||
return None
|
||||
|
||||
def _format_time(self, timestamp: int) -> str:
|
||||
"""格式化时间戳"""
|
||||
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:
|
||||
"""处理按钮点击事件"""
|
||||
button_id = event.button.id
|
||||
if button_id and button_id.startswith("remove-"):
|
||||
# 提取编码后的键
|
||||
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("解析按钮 ID 失败: %s", e)
|
||||
self.app.notify("操作失败: 无效的按钮标识", severity="error")
|
||||
|
||||
def _remove_favorite(self, repo_path: str, ident: str) -> None:
|
||||
"""移除收藏项"""
|
||||
if favorite_manager.remove(repo_path, ident):
|
||||
self.app.notify(f"已移除收藏: {ident}", severity="information")
|
||||
# 重新加载列表
|
||||
self._load_favorites()
|
||||
# 刷新界面
|
||||
self._refresh_list()
|
||||
else:
|
||||
self.app.notify(f"移除失败: {ident}", severity="error")
|
||||
|
||||
def _refresh_list(self) -> None:
|
||||
"""刷新列表显示"""
|
||||
container = self.query_one("#favorites-container")
|
||||
# 清空容器
|
||||
for child in container.children:
|
||||
child.remove()
|
||||
# 重新组合
|
||||
if not self.favorites:
|
||||
container.mount(Label("暂无收藏", classes="empty-label"))
|
||||
container.mount(Static("使用 * 键在记忆界面中添加收藏。"))
|
||||
else:
|
||||
container.mount(
|
||||
Label(f"共 {len(self.favorites)} 个收藏项", classes="count-label")
|
||||
)
|
||||
list_view = ListView(id="favorites-list")
|
||||
container.mount(list_view)
|
||||
for fav in self.favorites:
|
||||
list_view.append(self._create_favorite_item(fav))
|
||||
|
||||
def action_go_back(self) -> None:
|
||||
"""返回上一屏幕"""
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_toggle_dark(self) -> None:
|
||||
"""切换暗黑模式"""
|
||||
self.app.dark = not self.app.dark # type: ignore
|
||||
@@ -1 +0,0 @@
|
||||
"""整体式记忆工作界面"""
|
||||
@@ -1,278 +0,0 @@
|
||||
"""队列式记忆工作界面"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.reactive import reactive
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Footer, Header, Label, Static
|
||||
|
||||
from textual import events, on
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
from heurams.context import config_var, rootdir
|
||||
from heurams.kernel.reactor import *
|
||||
from heurams.services.favorite_service import favorite_manager
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .. import shim
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MemScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("p", "prev", "查看上一个"),
|
||||
("d", "toggle_dark", ""),
|
||||
("v", "play_voice", "朗读"),
|
||||
("*", "toggle_favorite", "收藏"),
|
||||
("r", "resume_mark"),
|
||||
("n", "block_prompt"),
|
||||
("s", "block_prompt"),
|
||||
("z", "block_prompt"),
|
||||
]
|
||||
|
||||
SUB_TITLE = "学习中"
|
||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "memoqueue.tcss"
|
||||
|
||||
if config_var.get()["interface"]["global"]["quick_pass"]:
|
||||
BINDINGS.append(("k", "quick_pass", "正确应答"))
|
||||
BINDINGS.append(("f", "quick_fail", "错误应答"))
|
||||
|
||||
rating = reactive(-1)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
router: Router,
|
||||
repo=None,
|
||||
name=None,
|
||||
id=None,
|
||||
classes=None,
|
||||
) -> None:
|
||||
super().__init__(name, id, classes)
|
||||
self.router = router
|
||||
self.repo = repo
|
||||
self.update_state()
|
||||
self.expander: Expander
|
||||
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
from heurams.services.attic import Attic
|
||||
|
||||
a = Attic("ana", {"openqueue": 0})
|
||||
a.data["openqueue"] += 1
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
with ScrollableContainer():
|
||||
yield Label(self._get_progress_text(), id="head_stat")
|
||||
yield ScrollableContainer(id="puzzle_container")
|
||||
yield Footer()
|
||||
|
||||
def update_state(self):
|
||||
"""更新状态机"""
|
||||
self.procession: Procession = self.router.current_procession() # type: ignore
|
||||
self.atom: pt.Atom = self.procession.current_atom # type: ignore
|
||||
|
||||
def on_mount(self):
|
||||
self.expander = self.procession.get_expander()
|
||||
from heurams.services.attic import Attic
|
||||
import time
|
||||
|
||||
a = Attic("ana", {"last": time.time()})
|
||||
a.data["last"] = time.time()
|
||||
self.mount_puzzle()
|
||||
self.update_display()
|
||||
|
||||
def puzzle_widget(self):
|
||||
try:
|
||||
puzzle = self.expander.get_current_puzzle_inf()
|
||||
return shim.puzzle2widget[puzzle["puzzle"]]( # type: ignore
|
||||
atom=self.atom, alia=puzzle["alia"] # type: ignore
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"调度展开出错: {e}")
|
||||
return Static(f"无法生成谜题 {e}")
|
||||
|
||||
def _get_progress_text(self):
|
||||
s = f"阶段: {self.procession.route.name}\n"
|
||||
# 收藏状态
|
||||
if self.repo is not None:
|
||||
fav_status = "已收藏" if self._is_current_atom_favorited() else "未收藏"
|
||||
s += f"收藏: {fav_status}\n"
|
||||
s += f"进度: {self.procession.process() + 1}/{self.procession.total_length()}"
|
||||
return s
|
||||
|
||||
def update_display(self):
|
||||
"""更新进度显示"""
|
||||
progress_widget = self.query_one("#head_stat")
|
||||
progress_widget.update(self._get_progress_text()) # type: ignore
|
||||
|
||||
def mount_puzzle(self):
|
||||
"""挂载当前谜题组件"""
|
||||
if self.procession.route == RouterState.FINISHED:
|
||||
self.mount_finished_widget()
|
||||
return
|
||||
container = self.query_one("#puzzle_container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
container.mount(self.puzzle_widget())
|
||||
|
||||
def mount_finished_widget(self):
|
||||
"""挂载已完成组件"""
|
||||
a = Attic("ana", {"finished": 0})
|
||||
a.data["finished"] += 1
|
||||
container = self.query_one("#puzzle_container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
from heurams.interface.widgets.finished import Finished
|
||||
|
||||
if config_var.get()["interface"]["global"]["persist_to_file"]:
|
||||
self.save_func()
|
||||
container.mount(Finished(is_saved=["interface"]["global"]["persist_to_file"]))
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
event.stop()
|
||||
|
||||
def action_play_voice(self):
|
||||
self.run_worker(self.play_voice, exclusive=True, thread=True)
|
||||
|
||||
def play_voice(self):
|
||||
"""朗读当前内容"""
|
||||
from pathlib import Path
|
||||
|
||||
from heurams.services.audio_service import play_by_path
|
||||
from heurams.services.hasher import get_md5
|
||||
|
||||
path = Path(config_var.get()["global"]["paths"]["data"]) / "cache" / "voice"
|
||||
path = path / f"{get_md5(self.atom.registry['nucleon']["tts_text"])}.wav"
|
||||
if path.exists():
|
||||
play_by_path(path)
|
||||
else:
|
||||
from heurams.services.tts_service import convertor
|
||||
|
||||
convertor(self.atom.registry["nucleon"]["tts_text"], path)
|
||||
play_by_path(path)
|
||||
|
||||
def watch_rating(self, old_rating, new_rating) -> None:
|
||||
if new_rating == -1: # 安全值
|
||||
return
|
||||
self.update_state()
|
||||
if self.procession.route == RouterState.FINISHED:
|
||||
rating = -1
|
||||
return
|
||||
self.expander.report(new_rating)
|
||||
self.forward(new_rating)
|
||||
self.rating = -1
|
||||
|
||||
def forward(self, rating):
|
||||
self.update_state()
|
||||
allow_forward = 1 if rating >= 4 else 0
|
||||
if allow_forward:
|
||||
self.expander.forward()
|
||||
if self.expander.state == "retronly":
|
||||
self.forward_atom(self.expander.get_quality())
|
||||
self.update_state()
|
||||
from heurams.services.attic import Attic
|
||||
|
||||
a = Attic("ana", {"openpuzzles": 0})
|
||||
a = Attic("ana", {"totaltime": 0})
|
||||
a.data["openpuzzles"] += 1
|
||||
import time
|
||||
|
||||
a.data["totaltime"] += time.time() - a.data["last"]
|
||||
a.data["last"] = time.time()
|
||||
self.mount_puzzle()
|
||||
self.update_display()
|
||||
|
||||
def atom_reporter(self, quality):
|
||||
if not self.atom.registry["runtime"]["locked"]:
|
||||
if not self.atom.registry["electron"].is_activated():
|
||||
self.atom.registry["electron"].activate()
|
||||
logger.debug(f"激活原子 {self.atom}")
|
||||
self.atom.lock(1)
|
||||
self.atom.minimize(5)
|
||||
else:
|
||||
self.atom.minimize(quality)
|
||||
else:
|
||||
pass
|
||||
|
||||
def forward_atom(self, quality):
|
||||
logger.debug(f"Quality: {quality}")
|
||||
self.atom_reporter(quality)
|
||||
if quality <= 3:
|
||||
from heurams.services.attic import Attic
|
||||
|
||||
a = Attic("ana", {"puzzles_err": 0})
|
||||
a.data["puzzles_err"] += 1
|
||||
self.procession.append()
|
||||
self.update_state() # 刷新状态
|
||||
self.procession.forward(1)
|
||||
self.update_state() # 刷新状态
|
||||
self.expander = self.procession.get_expander()
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quick_pass(self):
|
||||
self.rating = 5
|
||||
|
||||
def action_quick_fail(self):
|
||||
self.rating = 3
|
||||
|
||||
def _get_repo_rel_path(self) -> str:
|
||||
"""获取仓库相对路径(相对于 data/repo)"""
|
||||
if self.repo is None:
|
||||
return ""
|
||||
# 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:
|
||||
# 如果不在 data/repo 下,则返回完整路径(字符串形式)
|
||||
return str(repo_full_path)
|
||||
|
||||
def _is_current_atom_favorited(self) -> bool:
|
||||
"""检查当前原子是否已收藏"""
|
||||
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):
|
||||
"""切换收藏状态"""
|
||||
if self.repo is None:
|
||||
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(f"已取消收藏:{ident}", severity="information")
|
||||
else:
|
||||
favorite_manager.add(repo_path, ident)
|
||||
self.app.notify(f"已收藏:{ident}", severity="information")
|
||||
# 更新显示(如果需要)
|
||||
self.update_display()
|
||||
|
||||
def action_block_prompt(self):
|
||||
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", severity="error")
|
||||
|
||||
def action_resume_mark(self):
|
||||
from heurams.services.attic import Attic
|
||||
import time
|
||||
|
||||
a = Attic("ana")
|
||||
l = a.data["last"]
|
||||
a.data["last"] = time.time()
|
||||
self.app.notify(f"时间恢复已修正: {l} -> {a.data['last']}")
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
from enum import Enum, auto
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Center, ScrollableContainer
|
||||
from textual.reactive import reactive
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Footer, Header, Label, Static
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.reactor import *
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .. import shim
|
||||
|
||||
|
||||
class AtomState(Enum):
|
||||
FAILED = auto()
|
||||
NORMAL = auto()
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MemScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "pop_screen", "返回"),
|
||||
# ("p", "prev", "复习上一个"),
|
||||
("d", "toggle_dark", ""),
|
||||
("v", "play_voice", "朗读"),
|
||||
("0,1,2,3", "app.push_screen('about')", ""),
|
||||
]
|
||||
|
||||
if config_var.get()["quick_pass"]:
|
||||
BINDINGS.append(("k", "quick_pass", "跳过"))
|
||||
rating = reactive(-1)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
atoms: list,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name, id, classes)
|
||||
self.atoms = atoms
|
||||
self.phaser = Phaser(atoms)
|
||||
# logger.debug(self.phaser.state)
|
||||
self.procession: Procession = self.phaser.current_procession() # type: ignore
|
||||
self.atom: pt.Atom = self.procession.current_atom
|
||||
# logger.debug(self.phaser.state)
|
||||
# self.procession.forward(1)
|
||||
for i in atoms:
|
||||
i.do_eval()
|
||||
|
||||
def on_mount(self):
|
||||
self.load_puzzle()
|
||||
pass
|
||||
|
||||
def puzzle_widget(self):
|
||||
try:
|
||||
logger.debug(self.phaser.state)
|
||||
logger.debug(self.procession.cursor)
|
||||
logger.debug(self.atom)
|
||||
self.fission = Fission(self.atom, self.phaser.state)
|
||||
puzzle_debug = next(self.fission.generate())
|
||||
# logger.debug(puzzle_debug)
|
||||
return shim.puzzle2widget[puzzle_debug["puzzle"]](
|
||||
atom=self.atom, alia=puzzle_debug["alia"]
|
||||
)
|
||||
except (KeyError, StopIteration, AttributeError) as e:
|
||||
logger.debug(f"调度展开出错: {e}")
|
||||
return Static("无法生成谜题")
|
||||
# logger.debug(shim.puzzle2widget[puzzle_debug["puzzle"]])
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer():
|
||||
yield Label(self._get_progress_text(), id="progress")
|
||||
|
||||
# self.mount(self.current_widget()) # type: ignore
|
||||
yield ScrollableContainer(id="puzzle-container")
|
||||
# yield Button("重新学习此单元", id="re-recognize", variant="warning")
|
||||
yield Footer()
|
||||
|
||||
def _get_progress_text(self):
|
||||
return f"当前进度: {self.procession.process() + 1}/{self.procession.total_length()}"
|
||||
|
||||
def update_display(self):
|
||||
progress_widget = self.query_one("#progress")
|
||||
progress_widget.update(self._get_progress_text()) # type: ignore
|
||||
|
||||
def load_puzzle(self):
|
||||
self.atom: pt.Atom = self.procession.current_atom
|
||||
container = self.query_one("#puzzle-container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
container.mount(self.puzzle_widget())
|
||||
|
||||
def load_finished_widget(self):
|
||||
container = self.query_one("#puzzle-container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
from heurams.interface.widgets.finished import Finished
|
||||
|
||||
container.mount(Finished())
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
event.stop()
|
||||
|
||||
def watch_rating(self, old_rating, new_rating) -> None:
|
||||
if self.procession == 0:
|
||||
return
|
||||
if new_rating == -1:
|
||||
return
|
||||
forwards = 1 if new_rating >= 4 else 0
|
||||
self.rating = -1
|
||||
logger.debug(f"试图前进: {"允许" if forwards else "禁止"}")
|
||||
if forwards:
|
||||
ret = self.procession.forward(1)
|
||||
if ret == 0: # 若结束了此次队列
|
||||
self.procession = self.phaser.current_procession() # type: ignore
|
||||
if self.procession == 0: # 若所有队列都结束了
|
||||
logger.debug(f"记忆进程结束")
|
||||
for i in self.atoms:
|
||||
i: pt.Atom
|
||||
i.revise()
|
||||
i.persist("electron")
|
||||
self.load_finished_widget()
|
||||
return
|
||||
else:
|
||||
logger.debug(f"建立新队列 {self.procession.phase}")
|
||||
self.load_puzzle()
|
||||
else: # 若不通过
|
||||
self.procession.append()
|
||||
self.update_display()
|
||||
|
||||
def action_quick_pass(self):
|
||||
self.rating = 5
|
||||
self.atom.minimize(5)
|
||||
self.atom.registry["electron"].activate()
|
||||
self.atom.lock(1)
|
||||
|
||||
def action_play_voice(self):
|
||||
"""朗读当前内容"""
|
||||
pass
|
||||
|
||||
def action_toggle_dark(self):
|
||||
self.app.action_toggle_dark()
|
||||
|
||||
def action_pop_screen(self):
|
||||
self.app.pop_screen()
|
||||
@@ -1,94 +0,0 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Grid
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Label, ListItem, ListView, Static
|
||||
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .favmgr import FavoriteManagerScreen
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class NavigatorScreen(ModalScreen):
|
||||
"""导航器模态窗口"""
|
||||
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("escape", "go_back", "返回"),
|
||||
("n", "go_back", "切换"),
|
||||
]
|
||||
|
||||
SCREENS = [
|
||||
("仪表盘", "dashboard"),
|
||||
# ("创建仓库", "repo_creator"),
|
||||
("缓存管理器", "precache_all"),
|
||||
("收藏夹", FavoriteManagerScreen),
|
||||
("设置页面", "setting"),
|
||||
# ("调试日志", "logviewer"),
|
||||
("同步工具", "synctool"),
|
||||
("关于此软件", "about"),
|
||||
# ("仓库编辑器", "repo_editor"),
|
||||
]
|
||||
|
||||
OTHERS = [
|
||||
("退出程序", "self.app.exit()"),
|
||||
("项目主页", "webbrowser.open('https://ams.imwangzhiyu.xyz')"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""组合界面组件"""
|
||||
with Grid(id="dialog"):
|
||||
yield Label(
|
||||
"[b]请选择要跳转的功能\n或记忆会话实例[/b]\n\n将在此处显示提示",
|
||||
classes="title-label",
|
||||
)
|
||||
yield ListView(
|
||||
*[ListItem(Label(title)) for title, _ in (self.SCREENS + self.OTHERS)],
|
||||
id="nav-list",
|
||||
classes="nav-list-view",
|
||||
)
|
||||
yield Static("按下回车以完成切换\n所有会话将被保存")
|
||||
yield Button(
|
||||
"关闭 (n)",
|
||||
id="close_button",
|
||||
variant="primary",
|
||||
classes="close-button",
|
||||
flat=True,
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
# 设置焦点到列表
|
||||
nav_list = self.query_one("#nav-list", ListView)
|
||||
nav_list.focus()
|
||||
|
||||
def on_list_view_selected(self, event) -> None:
|
||||
if not isinstance(event.item, ListItem):
|
||||
return
|
||||
selected_label = event.item.query_one(Label)
|
||||
label_text = str(selected_label.render())
|
||||
# 查找对应的屏幕标识
|
||||
for title, screen_id in self.SCREENS:
|
||||
if title == label_text:
|
||||
self.app.pop_screen()
|
||||
# 跳转到目标屏幕
|
||||
if isinstance(screen_id, str):
|
||||
# 已注册的字符串标识符
|
||||
self.app.push_screen(screen_id)
|
||||
else:
|
||||
self.app.push_screen(screen_id())
|
||||
return
|
||||
for title, cmd in self.OTHERS:
|
||||
if title == label_text:
|
||||
exec(cmd)
|
||||
return
|
||||
return
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
event.stop()
|
||||
if event.button.id == "close_button":
|
||||
self.action_go_back()
|
||||
|
||||
def action_go_back(self) -> None:
|
||||
self.app.pop_screen()
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
import toml
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import (Button, Footer, Header, Input, Label, Markdown,
|
||||
Select)
|
||||
|
||||
from heurams.context import config_var
|
||||
from heurams.services.version import ver
|
||||
|
||||
|
||||
class NucleonCreatorScreen(Screen):
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
SUB_TITLE = "单元集创建向导"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
|
||||
def search_templates(self):
|
||||
from pathlib import Path
|
||||
|
||||
from heurams.context import config_var
|
||||
|
||||
template_dir = Path(config_var.get()["paths"]["template_dir"])
|
||||
templates = list()
|
||||
for i in template_dir.iterdir():
|
||||
if i.name.endswith(".toml"):
|
||||
try:
|
||||
import toml
|
||||
|
||||
with open(i, "r") as f:
|
||||
dic = toml.load(f)
|
||||
desc = dic["__metadata__.attribution"]["desc"]
|
||||
templates.append(desc + " (" + i.name + ")")
|
||||
except Exception as e:
|
||||
templates.append(f"无描述模板 ({i.name})")
|
||||
print(e)
|
||||
return templates
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="vice_container"):
|
||||
yield Label(f"[b]空白单元集创建向导\n")
|
||||
yield Markdown(
|
||||
"> 提示: 你可能注意到当选中文本框时底栏和操作按键绑定将被覆盖 \n只需选中(使用鼠标或 Tab)选择框即可恢复底栏功能"
|
||||
)
|
||||
yield Markdown("1. 键入单元集名称")
|
||||
yield Input(placeholder="单元集名称", id="name_input")
|
||||
yield Markdown(
|
||||
"> 单元集名称不应与现有单元集重复. \n> 新的单元集文件将创建在 ./nucleon/你输入的名称.toml"
|
||||
)
|
||||
yield Label(f"\n")
|
||||
yield Markdown("2. 选择单元集模板")
|
||||
LINES = self.search_templates()
|
||||
"""带有宏支持的空白单元集 ({ver})
|
||||
古诗词模板单元集 ({ver})
|
||||
英语词汇和短语模板单元集 ({ver})
|
||||
"""
|
||||
yield Select.from_values(LINES, prompt="选择类型", id="template_select")
|
||||
yield Markdown("> 新单元集的版本号将和主程序版本保持同步")
|
||||
yield Label(f"\n")
|
||||
yield Markdown("3. 输入常见附加元数据 (可选)")
|
||||
yield Input(placeholder="作者", id="author_input")
|
||||
yield Input(placeholder="内容描述", id="desc_input")
|
||||
yield Button(
|
||||
"新建空白单元集",
|
||||
id="submit_button",
|
||||
variant="primary",
|
||||
classes="start-button",
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
self.query_one("#submit_button").focus()
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self):
|
||||
self.app.exit()
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
event.stop()
|
||||
if event.button.id == "submit_button":
|
||||
# 获取输入值
|
||||
name_input = self.query_one("#name_input")
|
||||
template_select = self.query_one("#template_select")
|
||||
author_input = self.query_one("#author_input")
|
||||
desc_input = self.query_one("#desc_input")
|
||||
|
||||
name = name_input.value.strip() # type: ignore
|
||||
author = author_input.value.strip() # type: ignore
|
||||
desc = desc_input.value.strip() # type: ignore
|
||||
selected = template_select.value # type: ignore
|
||||
|
||||
# 验证
|
||||
if not name:
|
||||
self.notify("单元集名称不能为空", severity="error")
|
||||
return
|
||||
|
||||
# 获取配置路径
|
||||
config = config_var.get()
|
||||
nucleon_dir = Path(config["paths"]["nucleon_dir"])
|
||||
template_dir = Path(config["paths"]["template_dir"])
|
||||
|
||||
# 检查文件是否已存在
|
||||
nucleon_path = nucleon_dir / f"{name}.toml"
|
||||
if nucleon_path.exists():
|
||||
self.notify(f"单元集 '{name}' 已存在", severity="error")
|
||||
return
|
||||
|
||||
# 确定模板文件
|
||||
if selected is None:
|
||||
self.notify("请选择一个模板", severity="error")
|
||||
return
|
||||
# selected 是描述字符串, 格式如 "描述 (filename.toml)"
|
||||
# 提取文件名
|
||||
import re
|
||||
|
||||
match = re.search(r"\(([^)]+)\)$", selected)
|
||||
if not match:
|
||||
self.notify("模板选择格式无效", severity="error")
|
||||
return
|
||||
template_filename = match.group(1)
|
||||
template_path = template_dir / template_filename
|
||||
if not template_path.exists():
|
||||
self.notify(f"模板文件不存在: {template_filename}", severity="error")
|
||||
return
|
||||
|
||||
# 加载模板
|
||||
try:
|
||||
with open(template_path, "r", encoding="utf-8") as f:
|
||||
template_data = toml.load(f)
|
||||
except Exception as e:
|
||||
self.notify(f"加载模板失败: {e}", severity="error")
|
||||
return
|
||||
|
||||
# 更新元数据
|
||||
metadata = template_data.get("__metadata__", {})
|
||||
attribution = metadata.get("attribution", {})
|
||||
if author:
|
||||
attribution["author"] = author
|
||||
if desc:
|
||||
attribution["desc"] = desc
|
||||
attribution["name"] = name
|
||||
# 可选: 设置版本
|
||||
attribution["version"] = ver
|
||||
metadata["attribution"] = attribution
|
||||
template_data["__metadata__"] = metadata
|
||||
|
||||
# 确保 nucleon_dir 存在
|
||||
nucleon_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 写入新文件
|
||||
try:
|
||||
with open(nucleon_path, "w", encoding="utf-8") as f:
|
||||
toml.dump(template_data, f)
|
||||
except Exception as e:
|
||||
self.notify(f"保存单元集失败: {e}", severity="error")
|
||||
return
|
||||
|
||||
self.notify(f"单元集 '{name}' 创建成功")
|
||||
self.app.pop_screen()
|
||||
@@ -1,32 +1,16 @@
|
||||
"""缓存工具界面"""
|
||||
|
||||
#!/usr/bin/env python3
|
||||
import pathlib
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal, ScrollableContainer, Container
|
||||
from textual.containers import Horizontal, ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Footer, Header, Label, ProgressBar, Static
|
||||
from textual.worker import get_current_worker
|
||||
|
||||
from textual import events, on
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
|
||||
# 兼容性缓存路径:优先使用 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:
|
||||
"""将字节数格式化为人类可读的字符串"""
|
||||
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||
if bytes_num < 1024.0:
|
||||
return f"{bytes_num:.2f} {unit}"
|
||||
bytes_num /= 1024.0 # type: ignore
|
||||
return f"{bytes_num:.2f} PB"
|
||||
|
||||
|
||||
class PrecachingScreen(Screen):
|
||||
"""预缓存音频文件屏幕
|
||||
@@ -39,9 +23,7 @@ class PrecachingScreen(Screen):
|
||||
"""
|
||||
|
||||
SUB_TITLE = "缓存管理器"
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
]
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
|
||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
@@ -55,109 +37,34 @@ class PrecachingScreen(Screen):
|
||||
self.precache_worker = None
|
||||
self.cancel_flag = 0
|
||||
self.desc = desc
|
||||
# 不再需要缓存配置,保留配置读取以兼容
|
||||
self.cache_stats = {
|
||||
"total_size": 0,
|
||||
"file_count": 0,
|
||||
"human_size": "0 B",
|
||||
"cached_units": 0,
|
||||
"total_units": 0,
|
||||
"cache_rate": 0,
|
||||
}
|
||||
self._update_cache_stats()
|
||||
|
||||
def _get_total_units(self) -> int:
|
||||
"""获取所有仓库的总单元数"""
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.repolib import Repo
|
||||
|
||||
repo_path = pathlib.Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
||||
repo_dirs = Repo.probe_valid_repos_in_dir(repo_path)
|
||||
repos = map(Repo.from_repodir, repo_dirs)
|
||||
total = 0
|
||||
for repo in repos:
|
||||
try:
|
||||
total += len(repo.ident_index)
|
||||
except:
|
||||
continue
|
||||
return total
|
||||
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
|
||||
def _update_cache_stats(self) -> None:
|
||||
"""更新缓存统计信息"""
|
||||
total_size = 0
|
||||
file_count = 0
|
||||
cached_units = 0
|
||||
if cache_dir.exists():
|
||||
for file in cache_dir.rglob("*"):
|
||||
if file.is_file():
|
||||
total_size += file.stat().st_size
|
||||
file_count += 1
|
||||
if file.suffix.lower() == ".wav":
|
||||
cached_units += 1
|
||||
total_units = self._get_total_units()
|
||||
cache_rate = (cached_units / total_units * 100) if total_units > 0 else 0
|
||||
|
||||
self.cache_stats["total_size"] = total_size
|
||||
self.cache_stats["file_count"] = file_count
|
||||
self.cache_stats["human_size"] = human_size(total_size)
|
||||
self.cache_stats["cached_units"] = cached_units
|
||||
self.cache_stats["total_units"] = total_units
|
||||
self.cache_stats["cache_rate"] = cache_rate
|
||||
for i in nucleons:
|
||||
i: pt.Nucleon
|
||||
i.do_eval()
|
||||
# print("完成 EVAL")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="precache_container"):
|
||||
yield Label("[b]音频预缓存[/b]", classes="title-label")
|
||||
with Container():
|
||||
yield Static(
|
||||
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% (已缓存 {self.cache_stats.get('cached_units', 0)} / {self.cache_stats.get('total_units', 0)} 个单元)",
|
||||
classes="cache-usage-text",
|
||||
)
|
||||
|
||||
if self.nucleons:
|
||||
yield Static(
|
||||
f"目标单元归属: [b]{self.desc}[/b]", classes="target-info"
|
||||
)
|
||||
yield Static(
|
||||
f"单元数量: {len(self.nucleons)}", classes="target-info"
|
||||
)
|
||||
yield Static(f"目标单元归属: [b]{self.desc}[/b]", classes="target-info")
|
||||
yield Static(f"单元数量: {len(self.nucleons)}", classes="target-info")
|
||||
else:
|
||||
yield Static("目标: 所有单元", classes="target-info")
|
||||
|
||||
yield Static(id="status", classes="status-info")
|
||||
yield Static(id="current_item", classes="current-item")
|
||||
yield ProgressBar(total=100, show_eta=False, id="progress_bar")
|
||||
|
||||
with Horizontal(classes="button-group"):
|
||||
if not self.is_precaching:
|
||||
yield Button(
|
||||
"开始预缓存", id="start_precache", variant="primary"
|
||||
)
|
||||
yield Button("开始预缓存", id="start_precache", variant="primary")
|
||||
else:
|
||||
yield Button(
|
||||
"取消预缓存", id="cancel_precache", variant="error"
|
||||
)
|
||||
yield Button("取消预缓存", id="cancel_precache", variant="error")
|
||||
yield Button("清空缓存", id="clear_cache", variant="warning")
|
||||
yield Button("返回", id="go_back", variant="default")
|
||||
with Container(classes="cache-info"):
|
||||
yield Static(f"缓存路径: {cache_dir}", classes="cache-path")
|
||||
yield Static(
|
||||
f"文件数: {self.cache_stats['file_count']}", classes="cache-count"
|
||||
)
|
||||
yield Static(
|
||||
f"总大小: {self.cache_stats['human_size']}", classes="cache-size"
|
||||
)
|
||||
yield Button(
|
||||
"刷新", id="refresh_cache_stats", variant="default", flat=True
|
||||
)
|
||||
|
||||
yield Static("若您离开此界面, 未完成的缓存进程会自动停止.")
|
||||
yield Static('缓存程序支持 "断点续传".')
|
||||
|
||||
@@ -166,7 +73,6 @@ class PrecachingScreen(Screen):
|
||||
def on_mount(self):
|
||||
"""挂载时初始化状态"""
|
||||
self.update_status("就绪", "等待开始...")
|
||||
self._update_cache_display()
|
||||
|
||||
def update_status(self, status, current_item="", progress=None):
|
||||
"""更新状态显示"""
|
||||
@@ -181,35 +87,19 @@ class PrecachingScreen(Screen):
|
||||
progress_bar.progress = progress
|
||||
progress_bar.advance(0) # 刷新显示
|
||||
|
||||
def _update_cache_display(self) -> None:
|
||||
"""更新缓存信息显示"""
|
||||
# 更新统计信息
|
||||
self._update_cache_stats()
|
||||
# 更新缓存率进度条
|
||||
# 更新缓存大小和文件数显示
|
||||
cache_count_widget = self.query_one(".cache-count", Static)
|
||||
cache_size_widget = self.query_one(".cache-size", Static)
|
||||
cache_usage_text = self.query_one(".cache-usage-text", Static)
|
||||
if cache_count_widget:
|
||||
cache_count_widget.update(f"文件数: {self.cache_stats['file_count']}")
|
||||
if cache_size_widget:
|
||||
cache_size_widget.update(f"总大小: {self.cache_stats['human_size']}")
|
||||
if cache_usage_text:
|
||||
cache_usage_text.update(
|
||||
f"缓存率: {self.cache_stats.get('cache_rate', 0):.1f}% "
|
||||
f"(已缓存 {self.cache_stats.get('cached_units', 0)} / {self.cache_stats.get('total_units', 0)} 个单元)"
|
||||
)
|
||||
|
||||
def precache_by_text(self, text: str):
|
||||
"""预缓存单段文本的音频"""
|
||||
from heurams.context import config_var, rootdir, workdir
|
||||
|
||||
cache_dir = pathlib.Path(config_var.get()["paths"]["cache_dir"])
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
cache_file = cache_dir / f"{hasher.get_md5(text)}.wav"
|
||||
if not cache_file.exists():
|
||||
try:
|
||||
from heurams.services.tts_service import convertor
|
||||
try: # TODO: 调用模块消除tts耦合
|
||||
import edge_tts as tts
|
||||
|
||||
convertor(text, cache_file)
|
||||
communicate = tts.Communicate(text, "zh-CN-XiaoxiaoNeural")
|
||||
communicate.save_sync(str(cache_file))
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"预缓存失败 '{text}': {e}")
|
||||
@@ -218,8 +108,10 @@ class PrecachingScreen(Screen):
|
||||
|
||||
def precache_by_nucleon(self, nucleon: pt.Nucleon):
|
||||
"""依据 Nucleon 缓存"""
|
||||
ret = self.precache_by_text(nucleon["tts_text"])
|
||||
# print(nucleon.metadata['formation']['tts_text'])
|
||||
ret = self.precache_by_text(nucleon.metadata["formation"]["tts_text"])
|
||||
return ret
|
||||
# print(f"TTS 缓存: {nucleon.metadata['formation']['tts_text']}")
|
||||
|
||||
def precache_by_list(self, nucleons: list):
|
||||
"""依据 Nucleons 列表缓存"""
|
||||
@@ -228,7 +120,7 @@ class PrecachingScreen(Screen):
|
||||
worker = get_current_worker()
|
||||
if worker and worker.is_cancelled: # 函数在worker中执行且已被取消
|
||||
return False
|
||||
text = nucleon["tts_text"]
|
||||
text = nucleon.metadata["formation"]["tts_text"]
|
||||
# self.current_item = text[:30] + "..." if len(text) > 50 else text
|
||||
# print(text)
|
||||
self.processed += 1
|
||||
@@ -258,30 +150,36 @@ class PrecachingScreen(Screen):
|
||||
# print(f"返回 {ret}")
|
||||
return ret
|
||||
|
||||
def precache_by_filepath(self, path: pathlib.Path):
|
||||
"""预缓存单个文件的所有内容"""
|
||||
lst = list()
|
||||
for i in pt.load_nucleon(path):
|
||||
lst.append(i[0])
|
||||
return self.precache_by_list(lst)
|
||||
|
||||
def precache_all_files(self):
|
||||
"""预缓存所有文件"""
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.repolib import Repo
|
||||
from heurams.context import config_var, rootdir, workdir
|
||||
|
||||
repo_path = pathlib.Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
||||
repo_dirs = Repo.probe_valid_repos_in_dir(repo_path)
|
||||
repos = map(Repo.from_repodir, repo_dirs)
|
||||
nucleon_path = pathlib.Path(config_var.get()["paths"]["nucleon_dir"])
|
||||
nucleon_files = [
|
||||
f for f in nucleon_path.iterdir() if f.suffix == ".toml"
|
||||
] # TODO: 解耦合
|
||||
|
||||
# 计算总项目数
|
||||
self.total = 0
|
||||
nucleon_list = list()
|
||||
for repo in repos:
|
||||
nu = list()
|
||||
for file in nucleon_files:
|
||||
try:
|
||||
for i in repo.ident_index:
|
||||
nucleon_list.append(
|
||||
pt.Nucleon.from_data(
|
||||
repo.nucleonic_data_lict.get_itemic_unit(i)
|
||||
)
|
||||
)
|
||||
for i in pt.load_nucleon(file):
|
||||
nu.append(i[0])
|
||||
except:
|
||||
continue
|
||||
self.total = len(nucleon_list)
|
||||
return self.precache_by_list(nucleon_list)
|
||||
self.total = len(nu)
|
||||
for i in nu:
|
||||
i: pt.Nucleon
|
||||
i.do_eval()
|
||||
return self.precache_by_list(nu)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop()
|
||||
@@ -316,19 +214,18 @@ class PrecachingScreen(Screen):
|
||||
try:
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||
from heurams.context import config_var, rootdir, workdir
|
||||
|
||||
shutil.rmtree(
|
||||
f"{config_var.get()["paths"]["cache_dir"]}", ignore_errors=True
|
||||
)
|
||||
self.update_status("已清空", "音频缓存已清空", 0)
|
||||
self._update_cache_display() # 更新缓存统计显示
|
||||
except Exception as e:
|
||||
self.update_status("错误", f"清空缓存失败: {e}")
|
||||
self.cancel_flag = 1
|
||||
self.processed = 0
|
||||
self.progress = 0
|
||||
|
||||
elif event.button.id == "refresh_cache_stats":
|
||||
# 刷新缓存统计信息
|
||||
self._update_cache_display()
|
||||
self.app.notify("缓存信息已刷新", severity="information")
|
||||
elif event.button.id == "go_back":
|
||||
self.action_go_back()
|
||||
|
||||
@@ -336,3 +233,8 @@ class PrecachingScreen(Screen):
|
||||
if self.is_precaching and self.precache_worker:
|
||||
self.precache_worker.cancel()
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self):
|
||||
if self.is_precaching and self.precache_worker:
|
||||
self.precache_worker.cancel()
|
||||
self.app.exit()
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
"""记忆准备界面"""
|
||||
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.reactive import reactive
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import (
|
||||
Button,
|
||||
Footer,
|
||||
Header,
|
||||
Label,
|
||||
Markdown,
|
||||
Static,
|
||||
Sparkline,
|
||||
)
|
||||
from textual.lazy import Reveal
|
||||
|
||||
from textual import events, on
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Footer, Header, Label, Markdown, Static
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.repolib import *
|
||||
from heurams.kernel.algorithms import algorithms
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -32,71 +21,48 @@ class PreparationScreen(Screen):
|
||||
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("p", "precache", "缓存"),
|
||||
("p", "precache", "预缓存音频"),
|
||||
("d", "toggle_dark", ""),
|
||||
("0,1,2,3", "app.push_screen('about')", ""),
|
||||
]
|
||||
|
||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "preparation.tcss"
|
||||
scheduled_num = reactive(config_var.get()["scheduled_num"])
|
||||
|
||||
def __init__(self, repo: Repo) -> None:
|
||||
def __init__(self, nucleon_file: pathlib.Path, electron_file: pathlib.Path) -> None:
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
self.repo = repo
|
||||
self.load_data()
|
||||
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
self.nucleon_file = nucleon_file
|
||||
self.electron_file = electron_file
|
||||
self.nucleons_with_orbital = pt.load_nucleon(self.nucleon_file)
|
||||
self.electrons = pt.load_electron(self.electron_file)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
from heurams.services.attic import Attic
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="vice_container"):
|
||||
yield Label(f"准备就绪: [b]{self.nucleon_file.stem}[/b]\n")
|
||||
yield Label(
|
||||
f"内容源文件: {config_var.get()['paths']['nucleon_dir']}/[b]{self.nucleon_file.name}[/b]"
|
||||
)
|
||||
yield Label(
|
||||
f"元数据文件: {config_var.get()['paths']['electron_dir']}/[b]{self.electron_file.name}[/b]"
|
||||
)
|
||||
yield Label(f"\n单元数量: {len(self.nucleons_with_orbital)}\n")
|
||||
yield Label(f"单次记忆数量: {self.scheduled_num}", id="schnum_label")
|
||||
|
||||
a = Attic("ana", {"openpre": 0})
|
||||
a.data["openpre"] += 1
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
with ScrollableContainer(id="main_container"):
|
||||
yield Markdown(
|
||||
f"**准备就绪**: `{self.repo.manifest['title']}`\n", id="title"
|
||||
)
|
||||
yield Label(f"单元集路径: {self.repo.source}")
|
||||
yield Label(
|
||||
f"学习完成度: {self.repo.progress['touched']}/{len(self.repo)} [d]\\[{round(self.repo.progress['touched']/self.repo.progress['total']*100, 1)}%][/d]"
|
||||
)
|
||||
yield Label(
|
||||
f"调度算法: {self.repo.config["algorithm"]} {algorithms[self.repo.config["algorithm"]].desc}"
|
||||
)
|
||||
yield Label(
|
||||
f"学习数量: {self.repo.preview['review'] + self.scheduled_num} = {self.repo.preview['review']} [d][复习][/d] + {self.scheduled_num} [d][新识记][/d]\n",
|
||||
id="schnum_label",
|
||||
)
|
||||
|
||||
yield Horizontal(
|
||||
Button(
|
||||
yield Button(
|
||||
"开始记忆",
|
||||
id="start_memorizing_button",
|
||||
variant="primary",
|
||||
classes="btn",
|
||||
),
|
||||
Button(
|
||||
"管理缓存",
|
||||
classes="start-button",
|
||||
)
|
||||
yield Button(
|
||||
"预缓存音频",
|
||||
id="precache_button",
|
||||
variant="success",
|
||||
classes="btn",
|
||||
),
|
||||
id="operations",
|
||||
classes="precache-button",
|
||||
)
|
||||
|
||||
yield Static()
|
||||
yield Sparkline(self.spark_line_arr, summary_function=max)
|
||||
# yield Static(str(self.spark_line_arr))
|
||||
with Reveal(ScrollableContainer(id="previewer_container")):
|
||||
for i in self.content.splitlines():
|
||||
yield Static(i, classes="unit-statline")
|
||||
yield Static(f"\n单元预览:\n")
|
||||
yield Markdown(self._get_full_content().replace("/", ""), classes="full")
|
||||
yield Footer()
|
||||
|
||||
# def watch_scheduled_num(self, old_scheduled_num, new_scheduled_num):
|
||||
@@ -107,31 +73,13 @@ class PreparationScreen(Screen):
|
||||
# except:
|
||||
# pass
|
||||
|
||||
def load_data(self):
|
||||
self.scheduled_num = self.repo.config["scheduled_num"]
|
||||
def _get_full_content(self):
|
||||
content = ""
|
||||
spark_line_arr = []
|
||||
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"],
|
||||
)
|
||||
statstr = ""
|
||||
|
||||
if e.is_activated():
|
||||
statstr = "[#00ff00]A[/]"
|
||||
if e.is_due():
|
||||
statstr = "[#ffff00]R[/]"
|
||||
# statstr += ('[dim]' + str(e.rept(real_rept=True)).zfill(2)+'[/]')
|
||||
else:
|
||||
statstr = "[#ff0000]U[/]"
|
||||
spark_line_arr.append(e.rept(real_rept=True))
|
||||
content += f" {statstr} {n['content'].replace('/', '')} \n"
|
||||
self.content = content
|
||||
self.spark_line_arr = spark_line_arr
|
||||
for nucleon, orbital in self.nucleons_with_orbital:
|
||||
nucleon: pt.Nucleon
|
||||
# print(nucleon.payload)
|
||||
content += " - " + nucleon["content"] + " \n"
|
||||
return content
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
@@ -140,13 +88,9 @@ class PreparationScreen(Screen):
|
||||
from ..screens.precache import PrecachingScreen
|
||||
|
||||
lst = list()
|
||||
for i in self.repo.ident_index:
|
||||
lst.append(
|
||||
pt.Nucleon.from_data(self.repo.nucleonic_data_lict.get_itemic_unit(i))
|
||||
)
|
||||
precache_screen = PrecachingScreen(
|
||||
nucleons=lst, desc=self.repo.manifest["title"]
|
||||
)
|
||||
for i in self.nucleons_with_orbital:
|
||||
lst.append(i[0])
|
||||
precache_screen = PrecachingScreen(lst)
|
||||
self.app.push_screen(precache_screen)
|
||||
|
||||
def action_quit_app(self):
|
||||
@@ -156,42 +100,39 @@ class PreparationScreen(Screen):
|
||||
event.stop()
|
||||
logger.debug("按下按钮")
|
||||
if event.button.id == "start_memorizing_button":
|
||||
launch(repo=self.repo, app=self.app, scheduled_num=self.scheduled_num)
|
||||
|
||||
elif event.button.id == "precache_button":
|
||||
self.action_precache()
|
||||
|
||||
|
||||
def launch(repo, app, scheduled_num):
|
||||
if scheduled_num == -1:
|
||||
scheduled_num = config_var.get()["interface"]["global"]["scheduled_num"]
|
||||
atoms = list()
|
||||
for i in repo.ident_index:
|
||||
n = pt.Nucleon.from_data(
|
||||
nucleonic_data=repo.nucleonic_data_lict.get_itemic_unit(i)
|
||||
)
|
||||
e = pt.Electron.from_data(
|
||||
electronic_data=repo.electronic_data_lict.get_itemic_unit(i),
|
||||
algo_name=repo.config["algorithm"],
|
||||
)
|
||||
a = pt.Atom(n, e, repo.orbitic_data)
|
||||
atoms.append(a)
|
||||
|
||||
for nucleon, orbital in self.nucleons_with_orbital:
|
||||
atom = pt.Atom(nucleon.ident)
|
||||
atom.link("nucleon", nucleon)
|
||||
try:
|
||||
atom.link("electron", self.electrons[nucleon.ident])
|
||||
except KeyError:
|
||||
atom.link("electron", pt.Electron(nucleon.ident))
|
||||
atom.link("orbital", orbital)
|
||||
atom.link("nucleon_fmt", "toml")
|
||||
atom.link("electron_fmt", "json")
|
||||
atom.link("orbital_fmt", "toml")
|
||||
atom.link("nucleon_path", self.nucleon_file)
|
||||
atom.link("electron_path", self.electron_file)
|
||||
atom.link("orbital_path", None)
|
||||
atoms.append(atom)
|
||||
atoms_to_provide = list()
|
||||
left_new = scheduled_num
|
||||
left_new = self.scheduled_num
|
||||
for i in atoms:
|
||||
i: pt.Atom
|
||||
if i.registry["electron"].is_activated():
|
||||
if i.registry["electron"].is_due():
|
||||
atoms_to_provide.append(i)
|
||||
else:
|
||||
if i.registry["electron"].is_activated():
|
||||
pass
|
||||
else:
|
||||
left_new -= 1
|
||||
if left_new >= 0:
|
||||
atoms_to_provide.append(i)
|
||||
import heurams.kernel.reactor as rt
|
||||
logger.debug(f"ATP: {atoms_to_provide}")
|
||||
from .memorizor import MemScreen
|
||||
|
||||
from .memoqueue import MemScreen
|
||||
|
||||
router = rt.Router(atoms_to_provide)
|
||||
memscreen = MemScreen(router=router, repo=repo)
|
||||
app.push_screen(memscreen)
|
||||
memscreen = MemScreen(atoms_to_provide)
|
||||
self.app.push_screen(memscreen)
|
||||
elif event.button.id == "precache_button":
|
||||
self.action_precache()
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
"""设置页面"""
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import (
|
||||
Footer,
|
||||
Header,
|
||||
Label,
|
||||
Collapsible,
|
||||
Input,
|
||||
Switch,
|
||||
Select,
|
||||
)
|
||||
|
||||
from textual import events, on
|
||||
|
||||
from heurams.context import *
|
||||
from heurams.kernel.particles import *
|
||||
from heurams.kernel.repolib import *
|
||||
from heurams.services.logger import get_logger
|
||||
from heurams.services.textproc import domize, undomize
|
||||
from heurams.services.epath import epath
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SettingScreen(Screen):
|
||||
"""设置页面屏幕"""
|
||||
|
||||
SUB_TITLE = "设置"
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("s", "go_back", "设置"),
|
||||
]
|
||||
CSS_PATH = rootdir / "interface" / "css" / "screens" / "setting.tcss"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name, id, classes)
|
||||
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""组合界面组件"""
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
with ScrollableContainer():
|
||||
yield Label("[b]设置页面[/b]")
|
||||
for i in config_var.get():
|
||||
if i.startswith("_"):
|
||||
continue
|
||||
a = self._get_subcfg(f"{i}")
|
||||
if a:
|
||||
yield Collapsible(
|
||||
*a,
|
||||
title=i + f'\n[d]{config_var.get().get(f"_{i}_desc", "")}[/d]',
|
||||
)
|
||||
yield Label(
|
||||
"退出页面时, 所作的更改会立即保存, 但仍建议重启软件以确保新的配置得到应用",
|
||||
classes="foot",
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def _get_subcfg(self, parent_epath: str):
|
||||
parent = epath(config_var.get(), parent_epath)
|
||||
if isinstance(parent, ConfigDict):
|
||||
if parent.is_dir:
|
||||
lst = list()
|
||||
for i in parent:
|
||||
if i.startswith("_"):
|
||||
continue
|
||||
a = self._get_subcfg(f"{parent_epath}.{i}")
|
||||
if a:
|
||||
lst.append(
|
||||
Collapsible(
|
||||
*a, title=i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'
|
||||
)
|
||||
)
|
||||
return lst
|
||||
if isinstance(parent, dict) or (
|
||||
isinstance(parent, ConfigDict) and not parent.is_dir
|
||||
):
|
||||
lst = list()
|
||||
for i in parent:
|
||||
if i.startswith("_"):
|
||||
continue
|
||||
if isinstance(parent[i], dict):
|
||||
a = self._get_subcfg(f"{parent_epath}.{i}")
|
||||
if a:
|
||||
lst.append(
|
||||
Collapsible(
|
||||
*a, title=i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'
|
||||
)
|
||||
)
|
||||
elif f"_{i}_candidate" in parent: # 选择框模式
|
||||
if isinstance(parent[f"_{i}_candidate"], dict):
|
||||
lst.append(
|
||||
Horizontal(
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Select(
|
||||
(
|
||||
(f"{j}\n[d]{k}[/d]", j)
|
||||
for j, k in parent[f"_{i}_candidate"].items()
|
||||
),
|
||||
prompt=f'{parent.get(f"{i}", "")}',
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
),
|
||||
classes="setting-item",
|
||||
)
|
||||
)
|
||||
elif isinstance(parent[f"_{i}_candidate"], list):
|
||||
lst.append(
|
||||
Horizontal(
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Select(
|
||||
((j, j) for j in parent[f"_{i}_candidate"]),
|
||||
prompt=f'{parent.get(f"{i}", "")}',
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
),
|
||||
classes="setting-item",
|
||||
)
|
||||
)
|
||||
else:
|
||||
if isinstance(parent[i], float):
|
||||
lst.append(
|
||||
Horizontal(
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Input(
|
||||
value=str(parent[i]),
|
||||
placeholder="要求一个浮点数",
|
||||
type="number",
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
),
|
||||
classes="setting-item",
|
||||
)
|
||||
)
|
||||
elif isinstance(parent[i], str):
|
||||
lst.append(
|
||||
Horizontal(
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Input(
|
||||
value=parent[i],
|
||||
placeholder="要求一个字符串",
|
||||
type="text",
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
),
|
||||
classes="setting-item",
|
||||
)
|
||||
)
|
||||
elif isinstance(parent[i], bool):
|
||||
lst.append(
|
||||
Horizontal(
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Switch(
|
||||
value=parent[i],
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
classes="setting-switch",
|
||||
),
|
||||
classes="setting-item",
|
||||
)
|
||||
)
|
||||
elif isinstance(parent[i], int):
|
||||
lst.append(
|
||||
Horizontal(
|
||||
Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
|
||||
Input(
|
||||
value=str(parent[i]),
|
||||
placeholder="要求一个整数",
|
||||
type="integer",
|
||||
id=domize(f"{parent_epath}.{i}"),
|
||||
),
|
||||
classes="setting-item",
|
||||
)
|
||||
)
|
||||
elif isinstance(parent[i], list):
|
||||
pass
|
||||
else:
|
||||
lst.append(Label("未知类型"))
|
||||
return lst
|
||||
return [Label("无子项")]
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""挂载组件时初始化"""
|
||||
|
||||
def action_go_back(self) -> None:
|
||||
"""返回上一屏幕"""
|
||||
config_var.get().persist()
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
"""退出应用程序"""
|
||||
self.app.exit()
|
||||
|
||||
def action_open_navigator(self) -> None:
|
||||
"""打开导航器"""
|
||||
self.app.push_screen(NavigatorScreen())
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
widget_id = event.input.id
|
||||
if not widget_id:
|
||||
return
|
||||
eepath = undomize(widget_id)
|
||||
value = event.value
|
||||
epath(
|
||||
config_var.get(),
|
||||
eepath,
|
||||
enable_modify=True,
|
||||
new_value=type(epath(config_var.get(), eepath))(value),
|
||||
)
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
widget_id = event.switch.id
|
||||
if not widget_id:
|
||||
return
|
||||
eepath = undomize(widget_id)
|
||||
value = event.value
|
||||
epath(
|
||||
config_var.get(),
|
||||
eepath,
|
||||
enable_modify=True,
|
||||
new_value=type(epath(config_var.get(), eepath))(value),
|
||||
)
|
||||
|
||||
def on_select_changed(self, event: Select.Changed) -> None:
|
||||
widget_id = event.select.id
|
||||
if not widget_id:
|
||||
return
|
||||
eepath = undomize(widget_id)
|
||||
value = event.value
|
||||
epath(
|
||||
config_var.get(),
|
||||
eepath,
|
||||
enable_modify=True,
|
||||
new_value=type(epath(config_var.get(), eepath))(value),
|
||||
)
|
||||
@@ -1,16 +1,14 @@
|
||||
"""同步工具界面"""
|
||||
|
||||
#!/usr/bin/env python3
|
||||
import pathlib
|
||||
import time
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal, ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Footer, Header, ProgressBar, Static
|
||||
from textual.widgets import Button, Footer, Header, Label, ProgressBar, Static
|
||||
from textual.worker import get_current_worker
|
||||
|
||||
from textual import events, on
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
|
||||
|
||||
@@ -20,297 +18,22 @@ class SyncScreen(Screen):
|
||||
|
||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
self.sync_service = None
|
||||
self.is_syncing = False
|
||||
self.is_paused = False
|
||||
self.log_messages = []
|
||||
self.max_log_lines = 50
|
||||
|
||||
@on(events.ScreenResume)
|
||||
def post_active(self, event):
|
||||
from heurams.interface import shim
|
||||
|
||||
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
if config_var.get()["interface"]["global"]["show_header"]:
|
||||
yield Header(
|
||||
show_clock=config_var.get()["interface"]["global"]["clock_on_header"]
|
||||
)
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="sync_container"):
|
||||
# 标题和连接状态
|
||||
yield Static("同步工具", classes="title")
|
||||
yield Static("", id="status_label", classes="status")
|
||||
|
||||
# 配置信息
|
||||
yield Static(f"同步协议: {config_var.get()['services']['sync']}")
|
||||
yield Static("服务器配置:", classes="section_title")
|
||||
with Horizontal(classes="config_info"):
|
||||
yield Static("远程服务器:", classes="config_label")
|
||||
yield Static("", id="server_url", classes="config_value")
|
||||
with Horizontal(classes="config_info"):
|
||||
yield Static("远程路径:", classes="config_label")
|
||||
yield Static("", id="remote_path", classes="config_value")
|
||||
|
||||
with Horizontal(classes="control_buttons"):
|
||||
yield Button("测试连接", id="test_connection", variant="primary")
|
||||
yield Button("开始同步", id="start_sync", variant="success")
|
||||
yield Button("暂停", id="pause_sync", variant="warning", disabled=True)
|
||||
yield Button("取消", id="cancel_sync", variant="error", disabled=True)
|
||||
|
||||
yield Static("同步进度", classes="section_title")
|
||||
yield ProgressBar(id="progress_bar", show_percentage=True, total=100)
|
||||
yield Static("", id="progress_label", classes="progress_text")
|
||||
|
||||
yield Static("同步日志", classes="section_title")
|
||||
yield Static("", id="log_output", classes="log_output")
|
||||
|
||||
pass
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
"""挂载时初始化状态"""
|
||||
self.update_ui_from_config()
|
||||
self.log_message("同步工具已启动")
|
||||
|
||||
def update_ui_from_config(self):
|
||||
"""更新 UI 显示配置信息"""
|
||||
try:
|
||||
sync_cfg: dict = config_var.get()["providers"]["sync"]["webdav"]
|
||||
# 更新服务器 URL
|
||||
url = sync_cfg.get("url", "未配置")
|
||||
url_widget = self.query_one("#server_url")
|
||||
url_widget.update(url) # type: ignore
|
||||
# 更新远程路径
|
||||
remote_path = sync_cfg.get("remote_path", "/")
|
||||
path_widget = self.query_one("#remote_path")
|
||||
path_widget.update(remote_path) # type: ignore
|
||||
|
||||
# 更新状态标签
|
||||
status_widget = self.query_one("#status_label")
|
||||
if self.sync_service and self.sync_service.client:
|
||||
status_widget.update("✅ 同步服务已就绪") # type: ignore
|
||||
status_widget.add_class("ready")
|
||||
else:
|
||||
status_widget.update("❌ 同步服务未配置或未启用") # type: ignore
|
||||
status_widget.add_class("error")
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(f"更新 UI 失败: {e}", is_error=True)
|
||||
|
||||
def update_status(self, status, current_item="", progress=None):
|
||||
"""更新状态显示"""
|
||||
try:
|
||||
status_widget = self.query_one("#status_label")
|
||||
status_widget.update(status) # type: ignore
|
||||
|
||||
if progress is not None:
|
||||
progress_bar = self.query_one("#progress_bar")
|
||||
progress_bar.progress = progress # type: ignore
|
||||
|
||||
progress_label = self.query_one("#progress_label")
|
||||
progress_label.update(f"{progress}% - {current_item}" if current_item else f"{progress}%") # type: ignore
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(f"更新状态失败: {e}", is_error=True)
|
||||
|
||||
def log_message(self, message: str, is_error: bool = False):
|
||||
"""添加日志消息并更新显示"""
|
||||
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)
|
||||
# 保持日志行数不超过最大值
|
||||
if len(self.log_messages) > self.max_log_lines:
|
||||
self.log_messages = self.log_messages[-self.max_log_lines :]
|
||||
|
||||
# 更新日志显示
|
||||
try:
|
||||
log_widget = self.query_one("#log_output")
|
||||
log_widget.update("\n".join(self.log_messages)) # type: ignore
|
||||
except Exception:
|
||||
pass # 如果组件未就绪,忽略错误
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""处理按钮点击事件"""
|
||||
button_id = event.button.id
|
||||
|
||||
if button_id == "test_connection":
|
||||
self.test_connection()
|
||||
elif button_id == "start_sync":
|
||||
self.start_sync()
|
||||
elif button_id == "pause_sync":
|
||||
self.pause_sync()
|
||||
elif button_id == "cancel_sync":
|
||||
self.cancel_sync()
|
||||
|
||||
event.stop()
|
||||
|
||||
def test_connection(self):
|
||||
"""测试 WebDAV 服务器连接"""
|
||||
if not self.sync_service:
|
||||
self.log_message("同步服务未初始化,请检查配置", is_error=True)
|
||||
self.update_status("❌ 同步服务未初始化")
|
||||
return
|
||||
|
||||
self.log_message("正在测试 WebDAV 连接...")
|
||||
self.update_status("正在测试连接...")
|
||||
|
||||
try:
|
||||
success = self.sync_service.test_connection()
|
||||
if success:
|
||||
self.log_message("连接测试成功")
|
||||
self.update_status("✅ 连接正常")
|
||||
else:
|
||||
self.log_message("连接测试失败", is_error=True)
|
||||
self.update_status("❌ 连接失败")
|
||||
except Exception as e:
|
||||
self.log_message(f"连接测试异常: {e}", is_error=True)
|
||||
self.update_status("❌ 连接异常")
|
||||
|
||||
def start_sync(self):
|
||||
"""开始同步"""
|
||||
if not self.sync_service:
|
||||
self.log_message("同步服务未初始化,无法开始同步", is_error=True)
|
||||
return
|
||||
|
||||
if self.is_syncing:
|
||||
self.log_message("同步已在进行中", is_error=True)
|
||||
return
|
||||
|
||||
self.is_syncing = True
|
||||
self.is_paused = False
|
||||
self.update_button_states()
|
||||
|
||||
self.log_message("开始同步数据...")
|
||||
self.update_status("正在同步...", progress=0)
|
||||
|
||||
# 启动后台同步任务
|
||||
self.run_worker(self.perform_sync, thread=True)
|
||||
|
||||
def perform_sync(self):
|
||||
"""执行同步任务(在后台线程中运行)"""
|
||||
worker = get_current_worker()
|
||||
|
||||
try:
|
||||
# 获取需要同步的本地目录
|
||||
from heurams.context import config_var
|
||||
|
||||
config = config_var.get()
|
||||
paths = config.get("paths", {})
|
||||
|
||||
# 同步 nucleon 目录
|
||||
nucleon_dir = pathlib.Path(paths.get("nucleon_dir", "./data/nucleon"))
|
||||
if nucleon_dir.exists():
|
||||
self.log_message(f"同步 nucleon 目录: {nucleon_dir}")
|
||||
self.update_status(f"同步 nucleon 目录...", progress=10)
|
||||
|
||||
result = self.sync_service.sync_directory(nucleon_dir) # type: ignore
|
||||
if result.get("success"):
|
||||
self.log_message(
|
||||
f"nucleon 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)} 个"
|
||||
)
|
||||
else:
|
||||
self.log_message(
|
||||
f"nucleon 同步失败: {result.get('error', '未知错误')}",
|
||||
is_error=True,
|
||||
)
|
||||
|
||||
# 同步 electron 目录
|
||||
electron_dir = pathlib.Path(paths.get("electron_dir", "./data/electron"))
|
||||
if electron_dir.exists():
|
||||
self.log_message(f"同步 electron 目录: {electron_dir}")
|
||||
self.update_status(f"同步 electron 目录...", progress=60)
|
||||
|
||||
result = self.sync_service.sync_directory(electron_dir) # type: ignore
|
||||
if result.get("success"):
|
||||
self.log_message(
|
||||
f"electron 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)} 个"
|
||||
)
|
||||
else:
|
||||
self.log_message(
|
||||
f"electron 同步失败: {result.get('error', '未知错误')}",
|
||||
is_error=True,
|
||||
)
|
||||
|
||||
# 同步 orbital 目录(如果存在)
|
||||
orbital_dir = pathlib.Path(paths.get("orbital_dir", "./data/orbital"))
|
||||
if orbital_dir.exists():
|
||||
self.log_message(f"同步 orbital 目录: {orbital_dir}")
|
||||
self.update_status(f"同步 orbital 目录...", progress=80)
|
||||
|
||||
result = self.sync_service.sync_directory(orbital_dir) # type: ignore
|
||||
if result.get("success"):
|
||||
self.log_message(
|
||||
f"orbital 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)} 个"
|
||||
)
|
||||
else:
|
||||
self.log_message(
|
||||
f"orbital 同步失败: {result.get('error', '未知错误')}",
|
||||
is_error=True,
|
||||
)
|
||||
|
||||
# 同步完成
|
||||
self.update_status("同步完成", progress=100)
|
||||
self.log_message("所有目录同步完成")
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(f"同步过程中发生错误: {e}", is_error=True)
|
||||
self.update_status("同步失败")
|
||||
finally:
|
||||
# 重置同步状态
|
||||
self.is_syncing = False
|
||||
self.is_paused = False
|
||||
self.update_button_states() # type: ignore
|
||||
|
||||
def pause_sync(self):
|
||||
"""暂停同步"""
|
||||
if not self.is_syncing:
|
||||
return
|
||||
|
||||
self.is_paused = not self.is_paused
|
||||
self.update_button_states()
|
||||
|
||||
if self.is_paused:
|
||||
self.log_message("同步已暂停")
|
||||
self.update_status("同步已暂停")
|
||||
else:
|
||||
self.log_message("同步已恢复")
|
||||
self.update_status("正在同步...")
|
||||
|
||||
def cancel_sync(self):
|
||||
"""取消同步"""
|
||||
if not self.is_syncing:
|
||||
return
|
||||
|
||||
self.is_syncing = False
|
||||
self.is_paused = False
|
||||
self.update_button_states()
|
||||
|
||||
self.log_message("同步已取消")
|
||||
self.update_status("同步已取消")
|
||||
|
||||
def update_button_states(self):
|
||||
"""更新按钮状态"""
|
||||
try:
|
||||
start_button = self.query_one("#start_sync")
|
||||
pause_button = self.query_one("#pause_sync")
|
||||
cancel_button = self.query_one("#cancel_sync")
|
||||
|
||||
if self.is_syncing:
|
||||
start_button.disabled = True
|
||||
pause_button.disabled = False
|
||||
cancel_button.disabled = False
|
||||
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(f"更新按钮状态失败: {e}", is_error=True)
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
"""Kernel 操作辅助函数库"""
|
||||
|
||||
import random
|
||||
from typing import TypedDict
|
||||
|
||||
import heurams.interface.widgets as pzw
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
import platform
|
||||
import os
|
||||
from heurams.context import config_var
|
||||
|
||||
staging = {} # 细粒度缓存区, 是 ident -> quality 的封装
|
||||
|
||||
|
||||
def report_to_staging(atom: pt.Atom, quality):
|
||||
staging[atom.ident] = min(quality, staging[atom.ident])
|
||||
|
||||
|
||||
def clear():
|
||||
staging = dict()
|
||||
|
||||
|
||||
def deploy_to_electron():
|
||||
for atom_ident, quality in staging.items():
|
||||
if pt.atom_registry[atom_ident].registry["electron"].is_activated:
|
||||
pt.atom_registry[atom_ident].registry["electron"].revisor(quality=quality)
|
||||
else:
|
||||
pt.atom_registry[atom_ident].registry["electron"].revisor(
|
||||
quality=quality, is_new_activation=True
|
||||
)
|
||||
clear()
|
||||
|
||||
|
||||
puzzle2widget = {
|
||||
pz.RecognitionPuzzle: pzw.Recognition,
|
||||
@@ -12,13 +35,3 @@ puzzle2widget = {
|
||||
pz.MCQPuzzle: pzw.MCQPuzzle,
|
||||
pz.BasePuzzle: pzw.BasePuzzleWidget,
|
||||
}
|
||||
|
||||
|
||||
def set_term_title(title):
|
||||
if not config_var.get()["interface"]["global"]["change_window_title"]:
|
||||
return
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
os.system(f"title {title}")
|
||||
else: # Linux, Mac, etc.
|
||||
os.write(2, f"\033]2;{title}\007".encode("utf-8"))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Iterable
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.widget import Widget
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
@@ -14,7 +15,7 @@ class BasePuzzleWidget(Widget):
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True,
|
||||
markup: bool = True
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children,
|
||||
@@ -22,7 +23,7 @@ class BasePuzzleWidget(Widget):
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup,
|
||||
markup=markup
|
||||
)
|
||||
self.atom = atom
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ import copy
|
||||
import random
|
||||
from typing import TypedDict
|
||||
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.containers import Container
|
||||
from textual.message import Message
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Label, Markdown
|
||||
from textual.events import Key
|
||||
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.services.logger import get_logger
|
||||
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
@@ -51,11 +50,10 @@ class ClozePuzzle(BasePuzzleWidget):
|
||||
self.hashtable = {}
|
||||
self.alia = alia
|
||||
self._load()
|
||||
self.btn_shortcuts = {}
|
||||
self.hashmap = dict()
|
||||
|
||||
def _load(self):
|
||||
setting = self.atom.registry["nucleon"]["puzzles"][self.alia]
|
||||
setting = self.atom.registry["orbital"]["puzzles"][self.alia]
|
||||
self.puzzle = pz.ClozePuzzle(
|
||||
text=setting["text"],
|
||||
delimiter=setting["delimiter"],
|
||||
@@ -67,39 +65,20 @@ class ClozePuzzle(BasePuzzleWidget):
|
||||
|
||||
def compose(self):
|
||||
yield Label(self.puzzle.wording, id="sentence")
|
||||
yield Markdown(f"> {self.listprint(self.inputlist)}", id="inputpreview")
|
||||
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
|
||||
# 渲染当前问题的选项
|
||||
with ScrollableContainer(id="btn-container") as s:
|
||||
c = 0
|
||||
with Container(id="btn-container"):
|
||||
for i in self.ans:
|
||||
h = str(hash(i))
|
||||
if hash(i) in self.hashmap.keys():
|
||||
continue
|
||||
c += 1
|
||||
self.hashmap[h] = i
|
||||
btnid = f"sel000-{h}"
|
||||
self.hashmap[str(hash(i))] = i
|
||||
btnid = f"sel000-{hash(i)}"
|
||||
logger.debug(f"建立按钮 {btnid}")
|
||||
self.btn_shortcuts[f"{c}"] = btnid
|
||||
yield Button(f"[{c}] {i}", id=f"{btnid}")
|
||||
s.focus()
|
||||
yield Button(i, id=f"{btnid}")
|
||||
|
||||
yield Button("退格", id="delete")
|
||||
self.btn_shortcuts[f"0"] = "delete"
|
||||
self.btn_shortcuts[f"backspace"] = "delete"
|
||||
self.btn_shortcuts[f"delete"] = "delete"
|
||||
|
||||
def listprint(self, lst):
|
||||
s = ""
|
||||
if lst:
|
||||
lastone = lst[-1]
|
||||
for i in lst[:-1]:
|
||||
s += i + " "
|
||||
s += f" `{lastone}`"
|
||||
return s
|
||||
|
||||
def update_display(self):
|
||||
preview = self.query_one("#inputpreview")
|
||||
preview.update(f"> {self.listprint(self.inputlist)}") # type: ignore
|
||||
preview.update(f"当前输入: {self.inputlist}") # type: ignore
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
button_id = event.button.id
|
||||
@@ -128,10 +107,3 @@ class ClozePuzzle(BasePuzzleWidget):
|
||||
pass
|
||||
else:
|
||||
self.atom.minimize(rating)
|
||||
|
||||
def on_key(self, event: Key) -> None:
|
||||
self.notify(event.key)
|
||||
if event.key in self.btn_shortcuts:
|
||||
btn_id = self.btn_shortcuts.get(event.key)
|
||||
btn_id = "#" + btn_id
|
||||
self.query_one(btn_id, Button).press()
|
||||
|
||||
@@ -7,28 +7,25 @@ class Finished(Widget):
|
||||
self,
|
||||
*children: Widget,
|
||||
alia="",
|
||||
is_saved=0,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True,
|
||||
markup: bool = True
|
||||
) -> None:
|
||||
self.alia = alia
|
||||
self.is_saved = is_saved
|
||||
super().__init__(
|
||||
*children,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup,
|
||||
markup=markup
|
||||
)
|
||||
|
||||
def compose(self):
|
||||
yield Label("本次记忆进程结束", id="finished_msg")
|
||||
yield Label(f"算法数据{'已保存' if self.is_saved else "未能保存"}")
|
||||
yield Button("返回上一级", flat=True, id="back-to-menu")
|
||||
yield Button("返回上一级", id="back-to-menu")
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
button_id = event.button.id
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 单项选择题
|
||||
from typing import TypedDict
|
||||
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.containers import Container, ScrollableContainer
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Label
|
||||
|
||||
@@ -9,7 +9,7 @@ import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from heurams.services.hasher import hash
|
||||
from heurams.services.logger import get_logger
|
||||
from textual.events import Key
|
||||
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -51,49 +51,35 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
self.hashmap = dict()
|
||||
self.cursor = 0
|
||||
self.atom = atom
|
||||
self.btn_shortcuts = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
cfg = self.atom.registry["nucleon"]["puzzles"][self.alia]
|
||||
if cfg["mapping"] == {}:
|
||||
self.screen.rating = 5 # type: ignore
|
||||
cfg = self.atom.registry["orbital"]["puzzles"][self.alia]
|
||||
self.puzzle = pz.MCQPuzzle(
|
||||
cfg["mapping"], cfg["jammer"], int(cfg["max_riddles_num"]), cfg["prefix"]
|
||||
)
|
||||
self.puzzle.refresh()
|
||||
|
||||
def compose(self):
|
||||
setting: Setting = self.atom.registry["nucleon"]["puzzles"][self.alia]
|
||||
if len(self.inputlist) > len(self.puzzle.options):
|
||||
logger.debug("ERR IDX")
|
||||
logger.debug(self.inputlist)
|
||||
logger.debug(self.puzzle.options)
|
||||
else:
|
||||
self.atom.registry["nucleon"].do_eval()
|
||||
setting: Setting = self.atom.registry["nucleon"].metadata["orbital"]["puzzles"][
|
||||
self.alia
|
||||
]
|
||||
logger.debug(f"Puzzle Setting: {setting}")
|
||||
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
|
||||
with ScrollableContainer(id="btn-container") as s:
|
||||
with Container(id="btn-container"):
|
||||
for i in current_options:
|
||||
if i in [" ", ""]:
|
||||
continue
|
||||
c += 1
|
||||
h = str(hash(i))
|
||||
self.hashmap[h] = i
|
||||
btnid = f"sel{str(self.cursor).zfill(3)}-{h}"
|
||||
self.hashmap[str(hash(i))] = i
|
||||
btnid = f"sel{str(self.cursor).zfill(3)}-{hash(i)}"
|
||||
logger.debug(f"建立按钮 {btnid}")
|
||||
self.btn_shortcuts[f"{c}"] = f"{btnid}"
|
||||
yield Button(f"[{c}] " + i, id=f"{btnid}")
|
||||
s.focus()
|
||||
yield Button("退格", id="delete")
|
||||
yield Button(i, id=f"{btnid}")
|
||||
|
||||
self.btn_shortcuts["0"] = f"delete"
|
||||
self.btn_shortcuts["delete"] = f"delete"
|
||||
self.btn_shortcuts["backspace"] = f"delete"
|
||||
yield Button("退格", id="delete")
|
||||
|
||||
def update_display(self, error=0):
|
||||
# 更新预览标签
|
||||
@@ -151,25 +137,20 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
for child in container.children
|
||||
if hasattr(child, "id") and child.id and child.id.startswith("sel")
|
||||
]
|
||||
container.focus()
|
||||
|
||||
for button in buttons_to_remove:
|
||||
logger.info(button)
|
||||
container.remove_children("#" + button.id) # type: ignore
|
||||
|
||||
# 添加当前题目的选项按钮
|
||||
c = 0
|
||||
current_question_index = len(self.inputlist)
|
||||
if current_question_index < len(self.puzzle.options):
|
||||
current_options = self.puzzle.options[current_question_index]
|
||||
for option in current_options:
|
||||
if option in ["", " "]:
|
||||
continue
|
||||
c += 1
|
||||
button_id = f"sel{str(self.cursor).zfill(3)}-{hash(option)}"
|
||||
if button_id not in self.hashmap:
|
||||
self.hashmap[button_id[7:]] = option
|
||||
new_button = Button(f"[{c}] " + option, id=button_id)
|
||||
self.btn_shortcuts[f"{c}"] = button_id
|
||||
self.hashmap[button_id] = option
|
||||
new_button = Button(option, id=button_id)
|
||||
container.mount(new_button)
|
||||
|
||||
def handler(self, rating):
|
||||
@@ -177,10 +158,3 @@ class MCQPuzzle(BasePuzzleWidget):
|
||||
pass
|
||||
else:
|
||||
self.atom.minimize(rating)
|
||||
|
||||
def on_key(self, event: Key) -> None:
|
||||
self.notify(event.key)
|
||||
if event.key in self.btn_shortcuts:
|
||||
btn_id = self.btn_shortcuts.get(event.key)
|
||||
btn_id = "#" + btn_id
|
||||
self.query_one(btn_id, Button).press()
|
||||
|
||||
@@ -11,7 +11,7 @@ class Placeholder(Widget):
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True,
|
||||
markup: bool = True
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children,
|
||||
@@ -19,7 +19,7 @@ class Placeholder(Widget):
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup,
|
||||
markup=markup
|
||||
)
|
||||
|
||||
def compose(self):
|
||||
|
||||
@@ -2,6 +2,8 @@ import re
|
||||
from typing import Dict, List, TypedDict
|
||||
|
||||
from textual.containers import Center
|
||||
from textual.message import Message
|
||||
from textual.reactive import reactive
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Label, Markdown, Static
|
||||
|
||||
@@ -47,13 +49,8 @@ class Recognition(BasePuzzleWidget):
|
||||
self.alia = alia
|
||||
|
||||
def compose(self):
|
||||
from heurams.context import config_var
|
||||
|
||||
autovoice = config_var.get()["interface"]["widgets"]["recognition"]["autovoice"]
|
||||
if autovoice:
|
||||
self.screen.action_play_voice() # type: ignore
|
||||
cfg: RecognitionConfig = self.atom.registry["nucleon"]["puzzles"][self.alia]
|
||||
delim = self.atom.registry["nucleon"]["delimiter"]
|
||||
cfg: RecognitionConfig = self.atom.registry["orbital"]["puzzles"][self.alia]
|
||||
delim = self.atom.registry["nucleon"].metadata["formation"]["delimiter"]
|
||||
replace_dict = {
|
||||
", ": ",",
|
||||
". ": ".",
|
||||
@@ -66,11 +63,12 @@ class Recognition(BasePuzzleWidget):
|
||||
f":{delim}": ":",
|
||||
}
|
||||
|
||||
nucleon = self.atom.registry["nucleon"]
|
||||
metadata = self.atom.registry["nucleon"].metadata
|
||||
primary = cfg["primary"]
|
||||
|
||||
with Center():
|
||||
for i in cfg["top_dim"]:
|
||||
yield Static(f"[dim]{i}[/]")
|
||||
yield Static(f"[dim]{cfg['top_dim']}[/]")
|
||||
yield Label("")
|
||||
|
||||
for old, new in replace_dict.items():
|
||||
@@ -86,21 +84,30 @@ class Recognition(BasePuzzleWidget):
|
||||
for item in cfg["secondary"]:
|
||||
if isinstance(item, list):
|
||||
for j in item:
|
||||
yield Markdown(f"### 笔记: {j}") # TODO ANNOTATION
|
||||
yield Markdown(f"### {metadata['annotation'][item]}: {j}")
|
||||
continue
|
||||
if isinstance(item, Dict):
|
||||
total = ""
|
||||
for j, k in item.items(): # type: ignore
|
||||
total += f"> {j}: {k} \n"
|
||||
total += f"> **{j}**: {k} \n"
|
||||
yield Markdown(total)
|
||||
if isinstance(item, str):
|
||||
yield Markdown(item)
|
||||
|
||||
with Center() as c:
|
||||
with Button("我已知晓", id="ok") as b:
|
||||
b.focus()
|
||||
with Center():
|
||||
yield Button("我已知晓", id="ok")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "ok":
|
||||
self.screen.rating = 5 # type: ignore
|
||||
self.handler(5)
|
||||
|
||||
def handler(self, rating):
|
||||
if not self.atom.registry["runtime"]["locked"]:
|
||||
if not self.atom.registry["electron"].is_activated():
|
||||
self.atom.registry["electron"].activate()
|
||||
logger.debug(f"激活原子 {self.atom}")
|
||||
self.atom.lock(1)
|
||||
self.atom.minimize(5)
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# Kernel - HeurAMS 核心
|
||||
|
||||
记忆规划相关算法与数据结构, 可脱离业务层
|
||||
@@ -1,21 +1,18 @@
|
||||
from .base import BaseAlgorithm
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .sm2 import SM2Algorithm
|
||||
from .sm15m import SM15MAlgorithm
|
||||
from .nsp0 import NSP0Algorithm
|
||||
from .fsrs import FSRSAlgorithm
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"SM2Algorithm",
|
||||
"BaseAlgorithm",
|
||||
"SM15MAlgorithm",
|
||||
"NSP0Algorithm",
|
||||
"FSRSAlgorithm",
|
||||
]
|
||||
|
||||
algorithms = {
|
||||
"SM-2": SM2Algorithm,
|
||||
"NSP-0": NSP0Algorithm,
|
||||
"SM-15M": SM15MAlgorithm,
|
||||
"FSRS": FSRSAlgorithm,
|
||||
"Base": BaseAlgorithm,
|
||||
# "SM-15M": SM15MAlgorithm,
|
||||
}
|
||||
|
||||
logger.debug("算法模块初始化完成, 注册的算法: %s", list(algorithms.keys()))
|
||||
|
||||
@@ -8,9 +8,9 @@ logger = get_logger(__name__)
|
||||
|
||||
class BaseAlgorithm:
|
||||
algo_name = "BaseAlgorithm"
|
||||
desc = "算法基类"
|
||||
|
||||
class AlgodataDict(TypedDict):
|
||||
efactor: float
|
||||
real_rept: int
|
||||
rept: int
|
||||
interval: int
|
||||
@@ -40,6 +40,7 @@ class BaseAlgorithm:
|
||||
feedback,
|
||||
is_new_activation,
|
||||
)
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def is_due(cls, algodata) -> int:
|
||||
@@ -51,7 +52,7 @@ class BaseAlgorithm:
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def get_rating(cls, algodata) -> str:
|
||||
def rate(cls, algodata) -> str:
|
||||
"""获取评分信息"""
|
||||
logger.debug(
|
||||
"BaseAlgorithm.rate 被调用, algodata keys: %s",
|
||||
@@ -67,11 +68,3 @@ class BaseAlgorithm:
|
||||
list(algodata.keys()) if algodata else [],
|
||||
)
|
||||
return -1
|
||||
|
||||
@classmethod
|
||||
def check_integrity(cls, algodata):
|
||||
try:
|
||||
cls.AlgodataDict(**algodata[cls.algo_name])
|
||||
return 1
|
||||
except:
|
||||
return 0
|
||||
|
||||
@@ -1,246 +1,6 @@
|
||||
"""
|
||||
FSRS 算法模块 — 基于 py-fsrs 的现代间隔重复调度器
|
||||
|
||||
基于: https://github.com/open-spaced-repetition/py-fsrs
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import TypedDict
|
||||
|
||||
from fsrs import Scheduler, Card, Rating
|
||||
|
||||
from heurams.context import config_var
|
||||
# FSRS 算法模块, 尚未就绪
|
||||
from heurams.services.logger import get_logger
|
||||
from heurams.services.timer import get_daystamp, get_timestamp
|
||||
|
||||
from .base import BaseAlgorithm
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 全局 Scheduler 状态文件路径
|
||||
_SCHEDULER_STATE_FILE = pathlib.Path(
|
||||
config_var.get()["global"]["paths"]["data"]
|
||||
) / "global" / "fsrs_scheduler_state.json"
|
||||
|
||||
|
||||
def _get_global_scheduler():
|
||||
"""获取全局 FSRS Scheduler 实例,从文件加载或创建新的"""
|
||||
if os.path.exists(_SCHEDULER_STATE_FILE):
|
||||
try:
|
||||
with open(_SCHEDULER_STATE_FILE, "r", encoding="utf-8") as f:
|
||||
return Scheduler.from_json(f.read())
|
||||
except Exception:
|
||||
logger.warning("FSRS Scheduler 状态文件加载失败,创建新实例")
|
||||
return Scheduler()
|
||||
|
||||
|
||||
def _save_global_scheduler(scheduler):
|
||||
"""保存全局 FSRS Scheduler 实例到文件"""
|
||||
try:
|
||||
_SCHEDULER_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = scheduler.to_json()
|
||||
with open(_SCHEDULER_STATE_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(data)
|
||||
except Exception:
|
||||
logger.exception("FSRS Scheduler 状态保存失败")
|
||||
|
||||
|
||||
def _feedback_to_rating(feedback: int) -> Rating:
|
||||
"""将 SM-2 风格 feedback (0-5) 映射为 FSRS Rating (1-4)"""
|
||||
if feedback <= 2:
|
||||
return Rating.Again
|
||||
elif feedback == 3:
|
||||
return Rating.Hard
|
||||
elif feedback == 4:
|
||||
return Rating.Good
|
||||
else:
|
||||
return Rating.Easy
|
||||
|
||||
|
||||
def _datetime_to_daystamp(dt: datetime) -> int:
|
||||
"""将 datetime 转换为天数戳(从 1970-01-01)"""
|
||||
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
delta = dt - epoch
|
||||
return delta.days
|
||||
|
||||
|
||||
def _daystamp_to_datetime(daystamp: int) -> datetime:
|
||||
"""将天数戳转换为 UTC datetime"""
|
||||
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
return epoch + timedelta(days=daystamp)
|
||||
|
||||
|
||||
class FSRSAlgorithm(BaseAlgorithm):
|
||||
algo_name = "FSRS"
|
||||
desc = "基于 py-fsrs 的现代间隔重复调度器"
|
||||
|
||||
class AlgodataDict(TypedDict):
|
||||
# FSRS 特有字段
|
||||
fsrs_state: int # State 枚举值: 1=Learning, 2=Review, 3=Relearning
|
||||
fsrs_step: int # 当前学习步进索引, -1 表示 None (Review 状态)
|
||||
fsrs_stability: float # 稳定性(秒),0.0 表示尚未计算
|
||||
fsrs_difficulty: float # 难度 [1.0, 10.0], 0.0 表示尚未计算
|
||||
# 标准 BaseAlgorithm 兼容字段
|
||||
real_rept: int
|
||||
rept: int
|
||||
interval: int
|
||||
last_date: int
|
||||
next_date: int
|
||||
is_activated: int
|
||||
last_modify: float
|
||||
|
||||
defaults = {
|
||||
"fsrs_state": 1, # State.Learning
|
||||
"fsrs_step": 0,
|
||||
"fsrs_stability": 0.0,
|
||||
"fsrs_difficulty": 0.0,
|
||||
"real_rept": 0,
|
||||
"rept": 0,
|
||||
"interval": 0,
|
||||
"last_date": 0,
|
||||
"next_date": 0,
|
||||
"is_activated": 0,
|
||||
"last_modify": get_timestamp(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _algodata_to_card(cls, algodata):
|
||||
"""从 algodata 恢复 Card 实例"""
|
||||
data = algodata.get(cls.algo_name, cls.defaults.copy())
|
||||
|
||||
card = Card()
|
||||
|
||||
# State: int → IntEnum
|
||||
card.state = data.get("fsrs_state", 1)
|
||||
|
||||
# Step: -1 表示 None(Review 状态下的 card.step 为 None)
|
||||
step = data.get("fsrs_step", -1)
|
||||
card.step = None if step == -1 else step
|
||||
|
||||
# Stability: 0.0 表示尚未计算(新卡片)
|
||||
stability = data.get("fsrs_stability", 0.0)
|
||||
card.stability = None if stability == 0.0 else stability
|
||||
|
||||
# Difficulty: 0.0 表示尚未计算
|
||||
difficulty = data.get("fsrs_difficulty", 0.0)
|
||||
card.difficulty = None if difficulty == 0.0 else difficulty
|
||||
|
||||
# due: 新卡片(next_date ≤ 0)设为当前时间
|
||||
next_date = data.get("next_date", 0)
|
||||
if next_date <= 0:
|
||||
card.due = datetime.now(timezone.utc)
|
||||
else:
|
||||
card.due = _daystamp_to_datetime(next_date)
|
||||
|
||||
# last_review
|
||||
last_date = data.get("last_date", 0)
|
||||
card.last_review = (
|
||||
_daystamp_to_datetime(last_date) if last_date > 0 else None
|
||||
)
|
||||
|
||||
return card
|
||||
|
||||
@classmethod
|
||||
def _card_to_algodata(cls, card, algodata):
|
||||
"""将 Card 实例状态写回 algodata"""
|
||||
if cls.algo_name not in algodata:
|
||||
algodata[cls.algo_name] = cls.defaults.copy()
|
||||
|
||||
data = algodata[cls.algo_name]
|
||||
data["fsrs_state"] = int(card.state)
|
||||
data["fsrs_step"] = card.step if card.step is not None else -1
|
||||
data["fsrs_stability"] = card.stability if card.stability is not None else 0.0
|
||||
data["fsrs_difficulty"] = (
|
||||
card.difficulty if card.difficulty is not None else 0.0
|
||||
)
|
||||
data["last_date"] = (
|
||||
_datetime_to_daystamp(card.last_review)
|
||||
if card.last_review
|
||||
else data.get("last_date", 0)
|
||||
)
|
||||
data["next_date"] = (
|
||||
_datetime_to_daystamp(card.due) if card.due else 0
|
||||
)
|
||||
data["interval"] = max(0, data["next_date"] - data["last_date"])
|
||||
data["last_modify"] = get_timestamp()
|
||||
return algodata
|
||||
|
||||
@classmethod
|
||||
def revisor(
|
||||
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
||||
):
|
||||
"""FSRS 算法迭代决策机制实现
|
||||
|
||||
将 feedback (0-5) 映射为 FSRS Rating 后交由 py-fsrs 调度器处理。
|
||||
|
||||
Args:
|
||||
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, 跳过更新")
|
||||
return
|
||||
|
||||
scheduler = _get_global_scheduler()
|
||||
rating = _feedback_to_rating(feedback)
|
||||
|
||||
if is_new_activation:
|
||||
card = Card()
|
||||
logger.debug("新激活,创建新 Card")
|
||||
else:
|
||||
card = cls._algodata_to_card(algodata)
|
||||
|
||||
card, review_log = scheduler.review_card(card, rating)
|
||||
_save_global_scheduler(scheduler)
|
||||
|
||||
cls._card_to_algodata(card, algodata)
|
||||
# real_rept: 总复习次数
|
||||
algodata[cls.algo_name]["real_rept"] += 1
|
||||
# rept: 成功回忆次数(feedback ≥ 3 视为成功)
|
||||
if feedback >= 3:
|
||||
algodata[cls.algo_name]["rept"] += 1
|
||||
|
||||
logger.debug(
|
||||
"FSRS.revisor 完成: stability=%s, difficulty=%s, state=%s, "
|
||||
"next_date=%d",
|
||||
card.stability,
|
||||
card.difficulty,
|
||||
card.state,
|
||||
algodata[cls.algo_name]["next_date"],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_due(cls, algodata):
|
||||
data = algodata.get(cls.algo_name, cls.defaults.copy())
|
||||
next_date = data.get("next_date", 0)
|
||||
current = get_daystamp()
|
||||
result = next_date <= current
|
||||
logger.debug(
|
||||
"FSRS.is_due: next_date=%d, current=%d, result=%s",
|
||||
next_date,
|
||||
current,
|
||||
result,
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_rating(cls, algodata):
|
||||
data = algodata.get(cls.algo_name, cls.defaults.copy())
|
||||
difficulty = data.get("fsrs_difficulty", 0.0)
|
||||
logger.debug("FSRS.get_rating: difficulty=%f", difficulty)
|
||||
return str(difficulty)
|
||||
|
||||
@classmethod
|
||||
def nextdate(cls, algodata) -> int:
|
||||
data = algodata.get(cls.algo_name, cls.defaults.copy())
|
||||
next_date = data.get("next_date", 0)
|
||||
logger.debug("FSRS.nextdate: %d", next_date)
|
||||
return next_date
|
||||
logger.info("FSRS算法模块尚未实现")
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
from typing import TypedDict
|
||||
|
||||
import heurams.services.timer as timer
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .base import BaseAlgorithm
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class NSP0Algorithm(BaseAlgorithm):
|
||||
algo_name = "NSP-0"
|
||||
desc = "快速筛选用特殊调度器"
|
||||
|
||||
class AlgodataDict(TypedDict):
|
||||
real_rept: int
|
||||
rept: int
|
||||
interval: int
|
||||
important: int
|
||||
last_date: int
|
||||
next_date: int
|
||||
is_activated: int
|
||||
last_modify: float
|
||||
|
||||
defaults = {
|
||||
"real_rept": 0,
|
||||
"important": 0,
|
||||
"rept": 0,
|
||||
"interval": 0,
|
||||
"last_date": 0,
|
||||
"next_date": 0,
|
||||
"is_activated": 0,
|
||||
"last_modify": timer.get_timestamp(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def revisor(
|
||||
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
||||
):
|
||||
"""NSP-0 算法迭代决策机制实现
|
||||
根据 quality(0 ~ 5) 进行参数迭代最佳间隔
|
||||
quality 由主程序评估
|
||||
|
||||
Args:
|
||||
quality (int): 记忆保留率量化参数
|
||||
"""
|
||||
logger.debug(
|
||||
"NSP0.revisor 开始, feedback: %d, is_new_activation: %s",
|
||||
feedback,
|
||||
is_new_activation,
|
||||
)
|
||||
|
||||
if feedback == -1:
|
||||
logger.debug("feedback 为 -1, 跳过更新")
|
||||
return
|
||||
algodata[cls.algo_name]["interval"] = 1 if feedback <= 3 else float("inf")
|
||||
algodata[cls.algo_name]["important"] = (
|
||||
1 if feedback <= 3 else algodata[cls.algo_name]["important"]
|
||||
)
|
||||
algodata[cls.algo_name]["last_date"] = timer.get_daystamp()
|
||||
algodata[cls.algo_name]["next_date"] = (
|
||||
timer.get_daystamp() + algodata[cls.algo_name]["interval"]
|
||||
)
|
||||
algodata[cls.algo_name]["last_modify"] = timer.get_timestamp()
|
||||
|
||||
logger.debug(
|
||||
"更新日期: 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"],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_due(cls, algodata):
|
||||
result = algodata[cls.algo_name]["next_date"] <= timer.get_daystamp()
|
||||
logger.debug(
|
||||
"NSP0.is_due: next_date=%d, current_daystamp=%d, result=%s",
|
||||
algodata[cls.algo_name]["next_date"],
|
||||
timer.get_daystamp(),
|
||||
result,
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_rating(cls, algodata):
|
||||
efactor = algodata[cls.algo_name]["efactor"]
|
||||
logger.debug("NSP0.rate: efactor=%f", efactor)
|
||||
return str(efactor)
|
||||
|
||||
@classmethod
|
||||
def nextdate(cls, algodata) -> int:
|
||||
next_date = algodata[cls.algo_name]["next_date"]
|
||||
logger.debug("NSP0.nextdate: %d", next_date)
|
||||
return next_date
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,6 @@ logger = get_logger(__name__)
|
||||
|
||||
class SM2Algorithm(BaseAlgorithm):
|
||||
algo_name = "SM-2"
|
||||
desc = "经典间隔重复算法"
|
||||
|
||||
class AlgodataDict(TypedDict):
|
||||
efactor: float
|
||||
@@ -117,7 +116,7 @@ class SM2Algorithm(BaseAlgorithm):
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_rating(cls, algodata):
|
||||
def rate(cls, algodata):
|
||||
efactor = algodata[cls.algo_name]["efactor"]
|
||||
logger.debug("SM2.rate: efactor=%f", efactor)
|
||||
return str(efactor)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from .evalizor import Evalizer
|
||||
from .lict import Lict
|
||||
|
||||
__all__ = ["Evalizer", "Lict"]
|
||||
@@ -1,33 +0,0 @@
|
||||
class Evalizer:
|
||||
"""几乎无副作用的模板系统
|
||||
|
||||
接受环境信息并创建一个模板解析工具, 工具传入参数支持list, dict及其嵌套
|
||||
副作用问题: 仅存在于 eval 函数
|
||||
"""
|
||||
|
||||
# TODO: 弃用风险极高的 eval
|
||||
# TODO: 异步/多线程执行避免堵塞
|
||||
def __init__(self, environment: dict) -> None:
|
||||
self.env = environment
|
||||
|
||||
def __call__(self, anyobj):
|
||||
return self.travel(anyobj)
|
||||
|
||||
def travel(self, anyobj):
|
||||
if isinstance(anyobj, list):
|
||||
return list(map(self.travel, anyobj))
|
||||
elif isinstance(anyobj, dict):
|
||||
return dict(map(self.travel, anyobj.items()))
|
||||
elif isinstance(anyobj, tuple):
|
||||
return tuple(map(self.travel, anyobj))
|
||||
elif isinstance(anyobj, str):
|
||||
if anyobj.startswith("eval:"):
|
||||
return self.eval_with_env(anyobj[5:])
|
||||
else:
|
||||
return anyobj
|
||||
else:
|
||||
return anyobj
|
||||
|
||||
def eval_with_env(self, s: str):
|
||||
ret = eval(s, globals(), self.env)
|
||||
return ret
|
||||
@@ -1,173 +0,0 @@
|
||||
from collections.abc import MutableSequence
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class Lict(MutableSequence):
|
||||
""" "列典" 对象
|
||||
|
||||
同时兼容字典和列表大多数 API, 两边数据同步的容器, 性能与 list/dict 大体相当
|
||||
列表数据是 dictobj.items() 的格式
|
||||
支持根据字典或列表初始化
|
||||
限制要求:
|
||||
- 键名一定唯一, 且仅能为字符串
|
||||
- append 的元组中, 表示键名的元素不能重复, 否则会导致覆盖行为
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initlist: Optional[list] = None,
|
||||
initdict: Optional[dict] = None,
|
||||
forced_order: bool = False,
|
||||
):
|
||||
self._dict = {}
|
||||
self._list = []
|
||||
self._dict_dirty = False
|
||||
self._list_dirty = False
|
||||
self.forced_order = forced_order
|
||||
|
||||
if initdict: # initdict 更优先
|
||||
self._dict = initdict.copy()
|
||||
self._list_dirty = True
|
||||
elif initlist:
|
||||
self._list = initlist.copy()
|
||||
self._dict_dirty = True
|
||||
|
||||
self._sync_if_needed()
|
||||
|
||||
def _sync_if_needed(self):
|
||||
"""只在需要时同步"""
|
||||
if self._dict_dirty:
|
||||
self._dict = {k: v for k, v in self._list}
|
||||
if self.forced_order:
|
||||
self._list.sort()
|
||||
self._dict_dirty = False
|
||||
|
||||
if self._list_dirty:
|
||||
self._list = list(self._dict.items())
|
||||
if self.forced_order:
|
||||
self._list.sort()
|
||||
self._list_dirty = False
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, str):
|
||||
return self._dict[key]
|
||||
else:
|
||||
self._sync_if_needed()
|
||||
return self._list[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""传入键值对时等同于操作字典, 传入索引+元组时等用于替换某索引的列表值为新元组"""
|
||||
if isinstance(key, str):
|
||||
self._dict[key] = value
|
||||
self._list_dirty = True
|
||||
else:
|
||||
if value != (value[0], value[1]):
|
||||
raise NotImplementedError
|
||||
self._sync_if_needed()
|
||||
old_key = self._list[key][0]
|
||||
self._dict.pop(old_key)
|
||||
self._list[key] = value
|
||||
self._dict[value[0]] = value[1] # 避免全量同步
|
||||
|
||||
def __delitem__(self, key):
|
||||
if isinstance(key, str): # 字符串键
|
||||
del self._dict[key]
|
||||
self._list_dirty = True
|
||||
else: # 数字索引
|
||||
self._sync_if_needed()
|
||||
del_key = self._list[key][0]
|
||||
del self._list[key]
|
||||
del self._dict[del_key]
|
||||
|
||||
def keys(self):
|
||||
self._sync_if_needed()
|
||||
return self._dict.keys()
|
||||
|
||||
def values(self):
|
||||
self._sync_if_needed()
|
||||
return self._dict.values()
|
||||
|
||||
def items(self):
|
||||
self._sync_if_needed()
|
||||
return self._list
|
||||
|
||||
def __len__(self):
|
||||
self._sync_if_needed()
|
||||
return len(self._list)
|
||||
|
||||
def __iter__(self):
|
||||
self._sync_if_needed()
|
||||
return iter(self._list)
|
||||
|
||||
def __contains__(self, item):
|
||||
self._sync_if_needed()
|
||||
return item in self._list or item in self.keys() or item in self.values()
|
||||
|
||||
def append(self, item):
|
||||
if item != (item[0], item[1]):
|
||||
raise NotImplementedError
|
||||
self._sync_if_needed() # 以防 forced_order
|
||||
key, value = item
|
||||
self._dict[key] = value
|
||||
self._list.append(item) # 两端都已同步
|
||||
self._sync_if_needed() # 以防 forced_order
|
||||
|
||||
def append_if_it_doesnt_exist_before(self, item: Any):
|
||||
if item != (item[0], item[1]):
|
||||
raise NotImplementedError
|
||||
self._sync_if_needed()
|
||||
if item[0] not in self:
|
||||
self.append(item)
|
||||
self._sync_if_needed()
|
||||
|
||||
def insert(self, i: int, item: Any) -> None:
|
||||
if item != (item[0], item[1]):
|
||||
raise NotImplementedError
|
||||
self._sync_if_needed()
|
||||
self._list.insert(i, item)
|
||||
self._dict_dirty = True
|
||||
|
||||
def pop(self, i: int = -1) -> Any:
|
||||
self._sync_if_needed()
|
||||
res = self._list.pop(i)
|
||||
self._dict_dirty = True
|
||||
return res
|
||||
|
||||
def remove(self, item: Any) -> None:
|
||||
if isinstance(item, str):
|
||||
item = (item, self._dict[item])
|
||||
if item != (item[0], item[1]):
|
||||
raise NotImplementedError
|
||||
self._sync_if_needed()
|
||||
self._list.remove(item)
|
||||
self._dict_dirty = True
|
||||
|
||||
def clear(self) -> None:
|
||||
self._dict.clear()
|
||||
self._list.clear()
|
||||
self._dict_dirty = False
|
||||
self._list_dirty = False
|
||||
|
||||
def index(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def extend(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def sort(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def reverse(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_itemic_unit(self, ident):
|
||||
return (ident, self._dict[ident])
|
||||
|
||||
def keys_equal_with(self, other):
|
||||
self._sync_if_needed()
|
||||
return self.key_equality(self, other)
|
||||
|
||||
@classmethod
|
||||
def key_equality(cls, a, b):
|
||||
return a.keys() == b.keys()
|
||||
@@ -1,21 +1,29 @@
|
||||
from .atom import Atom
|
||||
from .electron import Electron
|
||||
from .nucleon import Nucleon
|
||||
from .placeholders import (
|
||||
AtomPlaceholder,
|
||||
ElectronPlaceholder,
|
||||
NucleonPlaceholder,
|
||||
orbital_placeholder,
|
||||
)
|
||||
"""
|
||||
Particle 模块 - 粒子对象系统
|
||||
|
||||
# from .orbital import Orbital
|
||||
提供闪卡所需对象, 使用物理学粒子的领域驱动设计
|
||||
"""
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger.debug("粒子模块已加载")
|
||||
|
||||
from .atom import Atom, atom_registry
|
||||
from .electron import Electron
|
||||
from .loader import load_electron, load_nucleon
|
||||
from .nucleon import Nucleon
|
||||
from .orbital import Orbital
|
||||
from .probe import probe_all, probe_by_filename
|
||||
|
||||
__all__ = [
|
||||
"Atom",
|
||||
"Electron",
|
||||
"Nucleon",
|
||||
"AtomPlaceholder",
|
||||
"NucleonPlaceholder",
|
||||
"ElectronPlaceholder",
|
||||
"orbital_placeholder",
|
||||
"Orbital",
|
||||
"Atom",
|
||||
"probe_all",
|
||||
"probe_by_filename",
|
||||
"load_nucleon",
|
||||
"load_electron",
|
||||
"atom_registry",
|
||||
]
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import json
|
||||
import pathlib
|
||||
import typing
|
||||
from typing import TypedDict
|
||||
|
||||
import bidict
|
||||
import toml
|
||||
|
||||
from heurams.context import config_var
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .electron import Electron
|
||||
from .nucleon import Nucleon
|
||||
from .orbital import Orbital
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -11,13 +19,19 @@ logger = get_logger(__name__)
|
||||
class AtomRegister_runtime(TypedDict):
|
||||
locked: bool # 只读锁定标识符
|
||||
min_rate: int # 最低评分
|
||||
new_activation: bool # 新激活
|
||||
newact: bool # 新激活
|
||||
|
||||
|
||||
class AtomRegister(TypedDict):
|
||||
nucleon: Nucleon
|
||||
nucleon_path: pathlib.Path
|
||||
nucleon_fmt: str
|
||||
electron: Electron
|
||||
orbital: dict
|
||||
electron_path: pathlib.Path
|
||||
electron_fmt: str
|
||||
orbital: Orbital
|
||||
orbital_path: pathlib.Path
|
||||
orbital_fmt: str
|
||||
runtime: AtomRegister_runtime
|
||||
|
||||
|
||||
@@ -30,27 +44,39 @@ class Atom:
|
||||
以及关联路径
|
||||
"""
|
||||
|
||||
default_runtime = {
|
||||
"locked": False,
|
||||
"min_rate": float("inf"),
|
||||
"new_activation": False,
|
||||
}
|
||||
|
||||
def __init__(self, nucleon_obj=None, electron_obj=None, orbital_obj=None):
|
||||
self.ident = nucleon_obj["ident"] # type: ignore
|
||||
def __init__(self, ident=""):
|
||||
logger.debug("创建 Atom 实例, ident: '%s'", ident)
|
||||
self.ident = ident
|
||||
atom_registry[ident] = self
|
||||
logger.debug("Atom 已注册到全局注册表, 当前注册表大小: %d", len(atom_registry))
|
||||
# self.is_evaled = False
|
||||
self.registry: AtomRegister = { # type: ignore
|
||||
"ident": nucleon_obj["ident"], # type: ignore
|
||||
"nucleon": nucleon_obj,
|
||||
"electron": electron_obj,
|
||||
"orbital": orbital_obj,
|
||||
"runtime": dict(),
|
||||
"nucleon": None,
|
||||
"nucleon_path": None,
|
||||
"nucleon_fmt": "toml",
|
||||
"electron": None,
|
||||
"electron_path": None,
|
||||
"electron_fmt": "json",
|
||||
"orbital": None,
|
||||
"orbital_path": None, # 允许设置为 None, 此时使用 nucleon 文件内的推荐配置
|
||||
"orbital_fmt": "toml",
|
||||
"runtime": {"locked": False, "min_rate": 0x3F3F3F3F, "newact": False},
|
||||
}
|
||||
self.init_runtime()
|
||||
if self.registry["electron"].is_activated() == 0:
|
||||
self.registry["runtime"]["new_activation"] = True
|
||||
self.do_eval()
|
||||
logger.debug("Atom 初始化完成")
|
||||
|
||||
def init_runtime(self):
|
||||
self.registry["runtime"] = AtomRegister_runtime(**self.default_runtime)
|
||||
def link(self, key, value):
|
||||
logger.debug("Atom.link: key='%s', value type: %s", key, type(value).__name__)
|
||||
if key in self.registry.keys():
|
||||
self.registry[key] = value
|
||||
logger.debug("键 '%s' 已链接, 触发 do_eval", key)
|
||||
self.do_eval()
|
||||
if key == "electron":
|
||||
if self.registry["electron"].is_activated() == 0:
|
||||
self.registry["runtime"]["newact"] = True
|
||||
else:
|
||||
logger.error("尝试链接不受支持的键: '%s'", key)
|
||||
raise ValueError("不受支持的原子元数据链接操作")
|
||||
|
||||
def minimize(self, rating):
|
||||
"""效果等同于 self.registry['runtime']['min_rate'] = min(rating, self.registry['runtime']['min_rate'])
|
||||
@@ -83,21 +109,136 @@ class Atom:
|
||||
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"],
|
||||
is_new_activation=self.registry["runtime"]["newact"],
|
||||
)
|
||||
else:
|
||||
logger.debug("禁止总评分")
|
||||
|
||||
def do_eval(self):
|
||||
"""
|
||||
执行并以结果替换当前单元的所有 eval 语句
|
||||
TODO: 带有限制的 eval, 异步/多线程执行避免堵塞
|
||||
"""
|
||||
logger.debug("Atom.do_eval 开始")
|
||||
|
||||
# eval 环境设置
|
||||
def eval_with_env(s: str):
|
||||
# 初始化默认值
|
||||
nucleon = self.registry["nucleon"]
|
||||
default = {}
|
||||
metadata = {}
|
||||
try:
|
||||
default = config_var.get()["puzzles"]
|
||||
metadata = nucleon.metadata
|
||||
except Exception:
|
||||
# 如果无法获取配置或元数据, 使用空字典
|
||||
logger.debug("无法获取配置或元数据, 使用空字典")
|
||||
pass
|
||||
try:
|
||||
eval_value = eval(s)
|
||||
if isinstance(eval_value, (list, dict)):
|
||||
ret = eval_value
|
||||
else:
|
||||
ret = str(eval_value)
|
||||
logger.debug(
|
||||
"eval 执行成功: '%s' -> '%s'",
|
||||
s,
|
||||
str(ret)[:50] + "..." if len(ret) > 50 else ret,
|
||||
)
|
||||
except Exception as e:
|
||||
ret = f"此 eval 实例发生错误: {e}"
|
||||
logger.warning("eval 执行错误: '%s' -> %s", s, e)
|
||||
return ret
|
||||
|
||||
def traverse(data, modifier):
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
data[key] = traverse(value, modifier)
|
||||
return data
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
data[i] = traverse(item, modifier)
|
||||
return data
|
||||
elif isinstance(data, tuple):
|
||||
return tuple(traverse(item, modifier) for item in data)
|
||||
else:
|
||||
if isinstance(data, str):
|
||||
if data.startswith("eval:"):
|
||||
logger.debug("发现 eval 表达式: '%s'", data[5:])
|
||||
return modifier(data[5:])
|
||||
return data
|
||||
|
||||
# 如果 nucleon 存在且有 do_eval 方法, 调用它
|
||||
nucleon = self.registry["nucleon"]
|
||||
if nucleon is not None and hasattr(nucleon, "do_eval"):
|
||||
nucleon.do_eval()
|
||||
logger.debug("已调用 nucleon.do_eval")
|
||||
|
||||
# 如果 electron 存在且其 algodata 包含 eval 字符串, 遍历它
|
||||
electron = self.registry["electron"]
|
||||
if electron is not None and hasattr(electron, "algodata"):
|
||||
traverse(electron.algodata, eval_with_env)
|
||||
logger.debug("已处理 electron algodata eval")
|
||||
|
||||
# 如果 orbital 存在且是字典, 遍历它
|
||||
orbital = self.registry["orbital"]
|
||||
if orbital is not None and isinstance(orbital, dict):
|
||||
traverse(orbital, eval_with_env)
|
||||
logger.debug("orbital eval 完成")
|
||||
|
||||
logger.debug("Atom.do_eval 完成")
|
||||
|
||||
def persist(self, key):
|
||||
logger.debug("Atom.persist: key='%s'", key)
|
||||
path: pathlib.Path | None = self.registry[key + "_path"]
|
||||
if isinstance(path, pathlib.Path):
|
||||
path = typing.cast(pathlib.Path, path)
|
||||
logger.debug("持久化路径: %s, 格式: %s", path, self.registry[key + "_fmt"])
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if self.registry[key + "_fmt"] == "toml":
|
||||
with open(path, "r+") as f:
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
toml.dump(self.registry[key], f)
|
||||
logger.debug("TOML 数据已保存到: %s", path)
|
||||
elif self.registry[key + "_fmt"] == "json":
|
||||
with open(path, "r+") as f:
|
||||
origin = json.load(f)
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
origin[self.ident] = self.registry[key].algodata
|
||||
json.dump(origin, f, indent=2, ensure_ascii=False)
|
||||
logger.debug("JSON 数据已保存到: %s", path)
|
||||
else:
|
||||
logger.error("不受支持的持久化格式: %s", self.registry[key + "_fmt"])
|
||||
raise KeyError("不受支持的持久化格式")
|
||||
else:
|
||||
logger.error("路径未初始化: %s_path", key)
|
||||
raise TypeError("对未初始化的路径对象操作")
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.registry[key]
|
||||
logger.debug("Atom.__getitem__: key='%s'", key)
|
||||
if key in self.registry:
|
||||
value = self.registry[key]
|
||||
logger.debug("返回 value type: %s", type(value).__name__)
|
||||
return value
|
||||
logger.error("不支持的键: '%s'", key)
|
||||
raise KeyError(f"不支持的键: {key}")
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key == "ident":
|
||||
raise AttributeError("应为只读")
|
||||
logger.debug(
|
||||
"Atom.__setitem__: key='%s', value type: %s", key, type(value).__name__
|
||||
)
|
||||
if key in self.registry:
|
||||
self.registry[key] = value
|
||||
logger.debug("键 '%s' 已设置", key)
|
||||
else:
|
||||
logger.error("不支持的键: '%s'", key)
|
||||
raise KeyError(f"不支持的键: {key}")
|
||||
|
||||
def __repr__(self):
|
||||
from pprint import pformat
|
||||
@staticmethod
|
||||
def placeholder():
|
||||
return (Electron.placeholder(), Nucleon.placeholder(), {})
|
||||
|
||||
s = pformat(self.registry, indent=4)
|
||||
return s
|
||||
|
||||
atom_registry: bidict.bidict[str, Atom] = bidict.bidict()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from copy import deepcopy
|
||||
|
||||
import heurams.kernel.algorithms as algolib
|
||||
import heurams.services.timer as timer
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.algorithms import algorithms
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
@@ -9,76 +7,87 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Electron:
|
||||
"""电子: 单算法支持的记忆数据包装"""
|
||||
"""电子: 记忆分析元数据及算法"""
|
||||
|
||||
def __init__(self, ident: str, algodata: dict = {}, algo_name: str = ""):
|
||||
def __init__(self, ident: str, algodata: dict = {}, algo_name: str = "SM-2"):
|
||||
"""初始化电子对象 (记忆数据)
|
||||
|
||||
Args:
|
||||
ident: 算法的唯一标识符, 用于区分不同的算法实例, 使用 algodata[ident] 获取
|
||||
algodata: 算法数据字典引用, 包含算法的各项参数和设置
|
||||
algo_name: 使用的算法模块标识
|
||||
algodata: 算法数据字典, 包含算法的各项参数和设置
|
||||
algo: 使用的算法模块标识
|
||||
"""
|
||||
if algo_name == "":
|
||||
algo_name = "SM-2"
|
||||
logger.debug(
|
||||
"创建 Electron 实例, ident: '%s', algo_name: '%s'", ident, algo_name
|
||||
)
|
||||
self.algodata = algodata
|
||||
self.ident = ident
|
||||
self.algoname = algo_name
|
||||
self.algo: algolib.BaseAlgorithm = algorithms[algo_name]
|
||||
self.algo = algorithms[algo_name]
|
||||
logger.debug("使用的算法类: %s", self.algo.__name__)
|
||||
|
||||
if not self.algo.check_integrity(self.algodata):
|
||||
self.algodata[self.algo.algo_name] = deepcopy(self.algo.defaults)
|
||||
if self.algo not in self.algodata.keys():
|
||||
self.algodata[self.algo.algo_name] = {}
|
||||
logger.debug("算法键 '%s' 不存在, 已创建空字典", self.algo)
|
||||
if not self.algodata[self.algo.algo_name]:
|
||||
logger.debug("算法数据为空, 使用默认值初始化")
|
||||
self._default_init(self.algo.defaults)
|
||||
else:
|
||||
logger.debug("算法数据已存在, 跳过默认初始化")
|
||||
logger.debug(
|
||||
"Electron 初始化完成, algodata keys: %s", list(self.algodata.keys())
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
from pprint import pformat
|
||||
|
||||
s = pformat(self.algodata, indent=4)
|
||||
return s
|
||||
def _default_init(self, defaults: dict):
|
||||
"""默认初始化包装"""
|
||||
logger.debug(
|
||||
"Electron._default_init: 使用默认值, keys: %s", list(defaults.keys())
|
||||
)
|
||||
self.algodata[self.algo.algo_name] = defaults.copy()
|
||||
|
||||
def activate(self):
|
||||
"""激活此电子"""
|
||||
logger.debug("Electron.activate: 激活 ident='%s'", self.ident)
|
||||
self.algodata[self.algo.algo_name]["is_activated"] = 1
|
||||
self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp()
|
||||
logger.debug("电子已激活, is_activated=1")
|
||||
|
||||
def modify(self, key, value):
|
||||
def modify(self, var: str, value):
|
||||
"""修改 algodata[algo] 中子字典数据"""
|
||||
if key in self.algodata[self.algo.algo_name]:
|
||||
self.algodata[self.algo.algo_name][key] = value
|
||||
logger.debug("Electron.modify: var='%s', value=%s", var, value)
|
||||
if var in self.algodata[self.algo.algo_name]:
|
||||
self.algodata[self.algo.algo_name][var] = value
|
||||
self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp()
|
||||
logger.debug("变量 '%s' 已修改, 更新 last_modify", var)
|
||||
else:
|
||||
raise AttributeError("不存在的子键")
|
||||
logger.warning("'%s' 非已知元数据字段", var)
|
||||
print(f"警告: '{var}' 非已知元数据字段")
|
||||
|
||||
def is_due(self):
|
||||
"""是否应该复习"""
|
||||
logger.debug("Electron.is_due: 检查 ident='%s'", self.ident)
|
||||
result = self.algo.is_due(self.algodata)
|
||||
logger.debug("is_due 结果: %s", result)
|
||||
return result and self.is_activated()
|
||||
|
||||
def rept(self, real_rept=False):
|
||||
if real_rept:
|
||||
return self.algodata[self.algo.algo_name]["real_rept"]
|
||||
return self.algodata[self.algo.algo_name]["rept"]
|
||||
|
||||
def is_activated(self):
|
||||
result = self.algodata[self.algo.algo_name]["is_activated"]
|
||||
logger.debug("Electron.is_activated: ident='%s', 结果: %d", self.ident, result)
|
||||
return result
|
||||
|
||||
def last_modify(self):
|
||||
result = self.algodata[self.algo.algo_name]["last_modify"]
|
||||
return result
|
||||
|
||||
def get_rating(self):
|
||||
def get_rate(self):
|
||||
"评价"
|
||||
try:
|
||||
result = self.algo.get_rating(self.algodata)
|
||||
logger.debug("Electron.rate: ident='%s'", self.ident)
|
||||
result = self.algo.rate(self.algodata)
|
||||
logger.debug("rate 结果: %s", result)
|
||||
return result
|
||||
except:
|
||||
return 0
|
||||
|
||||
def nextdate(self) -> int:
|
||||
logger.debug("Electron.nextdate: ident='%s'", self.ident)
|
||||
result = self.algo.nextdate(self.algodata)
|
||||
return result
|
||||
|
||||
def lastdate(self) -> int:
|
||||
result = self.algodata[self.algo.algo_name]["lastdate"]
|
||||
logger.debug("nextdate 结果: %d", result)
|
||||
return result
|
||||
|
||||
def revisor(self, quality: int = 5, is_new_activation: bool = False):
|
||||
@@ -88,7 +97,32 @@ class Electron:
|
||||
quality (int): 记忆保留率量化参数 (0-5)
|
||||
is_new_activation (bool): 是否为初次激活
|
||||
"""
|
||||
logger.debug(
|
||||
"Electron.revisor: ident='%s', quality=%d, is_new_activation=%s",
|
||||
self.ident,
|
||||
quality,
|
||||
is_new_activation,
|
||||
)
|
||||
self.algo.revisor(self.algodata, quality, is_new_activation)
|
||||
logger.debug(
|
||||
"revisor 完成, 更新后的 algodata: %s", self.algodata.get(self.algo, {})
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"记忆单元预览 \n"
|
||||
f"标识符: '{self.ident}' \n"
|
||||
f"算法: {self.algo} \n"
|
||||
f"易度系数: {self.algodata[self.algo.algo_name]['efactor']:.2f} \n"
|
||||
f"已经重复的次数: {self.algodata[self.algo.algo_name]['rept']} \n"
|
||||
f"下次间隔: {self.algodata[self.algo.algo_name]['interval']} 天 \n"
|
||||
f"下次复习日期时间戳: {self.algodata[self.algo.algo_name]['next_date']}"
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.ident == other.ident:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.ident)
|
||||
@@ -112,9 +146,6 @@ class Electron:
|
||||
return len(self.algodata[self.algo.algo_name])
|
||||
|
||||
@staticmethod
|
||||
def from_data(electronic_data: tuple, algo_name: str = ""):
|
||||
_data = electronic_data
|
||||
ident = _data[0]
|
||||
algodata = _data[1]
|
||||
ident = ident
|
||||
return Electron(ident, algodata, algo_name)
|
||||
def placeholder():
|
||||
"""生成一个电子占位符"""
|
||||
return Electron("电子对象样例内容", {})
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import json
|
||||
import pathlib
|
||||
from copy import deepcopy
|
||||
|
||||
import toml
|
||||
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .electron import Electron
|
||||
from .nucleon import Nucleon
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def load_nucleon(path: pathlib.Path, fmt="toml"):
|
||||
logger.debug("load_nucleon: 加载文件 %s, 格式: %s", path, fmt)
|
||||
with open(path, "r") as f:
|
||||
dictdata = dict()
|
||||
dictdata = toml.load(f) # type: ignore
|
||||
logger.debug("TOML 解析成功, keys: %s", list(dictdata.keys()))
|
||||
lst = list()
|
||||
nested_data = dict()
|
||||
# 修正 toml 解析器的不管嵌套行为
|
||||
for key, value in dictdata.items():
|
||||
if "__metadata__" in key: # 以免影响句号
|
||||
if "." in key:
|
||||
parts = key.split(".")
|
||||
current = nested_data
|
||||
for part in parts[:-1]:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
current[parts[-1]] = value
|
||||
logger.debug("处理元数据键: %s", key)
|
||||
else:
|
||||
nested_data[key] = value
|
||||
logger.debug("嵌套数据处理完成, keys: %s", list(nested_data.keys()))
|
||||
# print(nested_data)
|
||||
for item, attr in nested_data.items():
|
||||
if item == "__metadata__":
|
||||
continue
|
||||
logger.debug("处理项目: %s", item)
|
||||
lst.append(
|
||||
(
|
||||
Nucleon(item, attr, deepcopy(nested_data["__metadata__"])),
|
||||
deepcopy(nested_data["__metadata__"]["orbital"]),
|
||||
)
|
||||
)
|
||||
logger.debug("load_nucleon 完成, 加载了 %d 个 Nucleon 对象", len(lst))
|
||||
return lst
|
||||
|
||||
|
||||
def load_electron(path: pathlib.Path, fmt="json") -> dict:
|
||||
"""从文件路径加载电子对象
|
||||
|
||||
Args:
|
||||
path (pathlib.Path): 路径
|
||||
fmt (str): 文件格式(可选, 默认 json)
|
||||
|
||||
Returns:
|
||||
dict: 键名是电子对象名称, 值是电子对象
|
||||
"""
|
||||
logger.debug("load_electron: 加载文件 %s, 格式: %s", path, fmt)
|
||||
with open(path, "r") as f:
|
||||
dictdata = dict()
|
||||
dictdata = json.load(f) # type: ignore
|
||||
logger.debug("JSON 解析成功, keys: %s", list(dictdata.keys()))
|
||||
dic = dict()
|
||||
for item, attr in dictdata.items():
|
||||
logger.debug("处理电子项目: %s", item)
|
||||
dic[item] = Electron(item, attr)
|
||||
logger.debug("load_electron 完成, 加载了 %d 个 Electron 对象", len(dic))
|
||||
return dic
|
||||
@@ -1,78 +1,104 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from heurams.context import config_var
|
||||
from heurams.services.logger import get_logger
|
||||
from heurams.kernel.auxiliary.evalizor import Evalizer
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Nucleon:
|
||||
"""原子核: 带有运行时隔离的模板化只读材料元数据容器"""
|
||||
"""原子核: 材料元数据"""
|
||||
|
||||
def __init__(self, ident, payload, common):
|
||||
def __init__(self, ident: str, payload: dict, metadata: dict = {}):
|
||||
"""初始化原子核 (记忆内容)
|
||||
|
||||
Args:
|
||||
ident: 唯一标识符
|
||||
payload: 记忆内容信息
|
||||
metadata: 可选元数据信息
|
||||
"""
|
||||
logger.debug(
|
||||
"创建 Nucleon 实例, ident: '%s', payload keys: %s, metadata keys: %s",
|
||||
ident,
|
||||
list(payload.keys()) if payload else [],
|
||||
list(metadata.keys()) if metadata else [],
|
||||
)
|
||||
self.metadata = metadata
|
||||
self.payload = payload
|
||||
self.ident = ident
|
||||
try:
|
||||
data_safe = deepcopy((payload | common))
|
||||
data_puz = deepcopy(data_safe["puzzles"])
|
||||
data_safe["puzzles"] = {}
|
||||
env = {
|
||||
"payload": data_safe,
|
||||
"default": config_var.get()["interface"]["puzzles"],
|
||||
"nucleon": data_safe,
|
||||
}
|
||||
self.evalizer = Evalizer(environment=env)
|
||||
data_safe = self.evalizer(deepcopy(data_safe))
|
||||
env = {
|
||||
"payload": data_safe,
|
||||
"default": config_var.get()["interface"]["puzzles"],
|
||||
"nucleon": data_safe,
|
||||
}
|
||||
self.evalizer = Evalizer(environment=env)
|
||||
data_puz = self.evalizer(deepcopy(data_puz))
|
||||
data_safe["puzzles"] = data_puz # type: ignore
|
||||
self.data: dict = data_safe # type: ignore
|
||||
except Exception:
|
||||
self.data = payload | common
|
||||
logger.debug("Nucleon 初始化完成")
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, str):
|
||||
logger.debug("Nucleon.__getitem__: key='%s'", key)
|
||||
if key == "ident":
|
||||
logger.debug("返回 ident: '%s'", self.ident)
|
||||
return self.ident
|
||||
return self.data[key]
|
||||
if key in self.payload:
|
||||
value = self.payload[key]
|
||||
logger.debug(
|
||||
"返回 payload['%s'], value type: %s", key, type(value).__name__
|
||||
)
|
||||
return value
|
||||
else:
|
||||
raise AttributeError
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
raise AttributeError("应为只读")
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise AttributeError("应为只读")
|
||||
logger.error("键 '%s' 未在 payload 中找到", key)
|
||||
raise KeyError(f"Key '{key}' not found in payload.")
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.data)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in (self.data)
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key in self:
|
||||
return self[key]
|
||||
return default
|
||||
yield from self.payload.keys()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
return len(self.payload)
|
||||
|
||||
def __repr__(self):
|
||||
from pprint import pformat
|
||||
def __hash__(self):
|
||||
return hash(self.ident)
|
||||
|
||||
s = pformat(self.data, indent=4)
|
||||
return s
|
||||
def do_eval(self):
|
||||
"""
|
||||
执行并以结果替换当前单元的所有 eval 语句
|
||||
TODO: 带有限制的 eval, 异步/多线程执行避免堵塞
|
||||
"""
|
||||
logger.debug("Nucleon.do_eval 开始")
|
||||
|
||||
# eval 环境设置
|
||||
def eval_with_env(s: str):
|
||||
try:
|
||||
nucleon = self
|
||||
eval_value = eval(s)
|
||||
if isinstance(eval_value, (int, float)):
|
||||
ret = str(eval_value)
|
||||
else:
|
||||
ret = eval_value
|
||||
logger.debug(
|
||||
"eval 执行成功: '%s' -> '%s'",
|
||||
s,
|
||||
str(ret)[:50] + "..." if len(ret) > 50 else ret,
|
||||
)
|
||||
except Exception as e:
|
||||
ret = f"此 eval 实例发生错误: {e}"
|
||||
logger.warning("eval 执行错误: '%s' -> %s", s, e)
|
||||
return ret
|
||||
|
||||
def traverse(data, modifier):
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
data[key] = traverse(value, modifier)
|
||||
return data
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
data[i] = traverse(item, modifier)
|
||||
return data
|
||||
elif isinstance(data, tuple):
|
||||
return tuple(traverse(item, modifier) for item in data)
|
||||
else:
|
||||
if isinstance(data, str):
|
||||
if data.startswith("eval:"):
|
||||
logger.debug("发现 eval 表达式: '%s'", data[5:])
|
||||
return modifier(data[5:])
|
||||
return data
|
||||
|
||||
traverse(self.payload, eval_with_env)
|
||||
traverse(self.metadata, eval_with_env)
|
||||
logger.debug("Nucleon.do_eval 完成")
|
||||
|
||||
@staticmethod
|
||||
def from_data(nucleonic_data: tuple):
|
||||
_data = nucleonic_data
|
||||
payload = _data[1][0]
|
||||
common = _data[1][1]
|
||||
ident = _data[0] # TODO:实现eval
|
||||
return Nucleon(ident, payload, common)
|
||||
def placeholder():
|
||||
"""生成一个占位原子核"""
|
||||
logger.debug("创建 Nucleon 占位符")
|
||||
return Nucleon("核子对象样例内容", {})
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
"""轨道对象"""
|
||||
from typing import TypedDict
|
||||
|
||||
# 似乎没有实现这个类的必要...
|
||||
# 那不妨在这儿写点文档
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
"""
|
||||
orbital, 即轨道, 是定义队列式复习阶段流程的数据结构, 其实就是个字典, 至于为何不用typeddict, 因为懒.
|
||||
|
||||
orbital_example = {
|
||||
"schedule": [列表 存储阶段(routes)名称]
|
||||
"routes":{
|
||||
阶段名称 = [["谜题(puzzle 现称 Puzzles 评估器)名称", "概率系数 可大于1(整数部分为重复次数) 注意使用字符串包裹(toml 规范)"], ...],
|
||||
...
|
||||
}
|
||||
}
|
||||
至于谜题定义 放在 nucleon['puzzles'], 这样设计是为了兼容多种不同谜题实现的记忆单元, 尽管如此, 你也可见其谜题调度方式必须是相同的.
|
||||
logger = get_logger(__name__)
|
||||
logger.debug("Orbital 类型定义模块已加载")
|
||||
|
||||
|
||||
class OrbitalSchedule(TypedDict):
|
||||
quick_review: list
|
||||
recognition: list
|
||||
final_review: list
|
||||
|
||||
|
||||
class Orbital(TypedDict):
|
||||
schedule: OrbitalSchedule
|
||||
puzzles: dict
|
||||
|
||||
|
||||
"""一份示例
|
||||
["__metadata__.orbital.puzzles"] # 谜题定义
|
||||
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:nucleon['content']", secondery = ["eval:nucleon['keyword_note']", "eval:nucleon['note']"], top_dim = ["eval:nucleon['translation']"] }
|
||||
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:nucleon['content']", jammer = "eval:nucleon['keyword_note']", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
|
||||
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:nucleon['content']", delimiter = "eval:metadata['formation']['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}
|
||||
|
||||
["__metadata__.orbital.schedule"] # 内置的推荐学习方案
|
||||
quick_review = [["FillBlank", "1.0"], ["SelectMeaning", "0.5"], ["recognition", "1.0"]]
|
||||
recognition = [["recognition", "1.0"]]
|
||||
final_review = [["FillBlank", "0.7"], ["SelectMeaning", "0.7"], ["recognition", "1.0"]]
|
||||
"""
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
from .atom import Atom
|
||||
from .electron import Electron
|
||||
from .nucleon import Nucleon
|
||||
|
||||
orbital_placeholder = {
|
||||
"schedule": ["quick_review", "recognition", "final_review"],
|
||||
"routes": {
|
||||
"quick_review": [
|
||||
["FillBlank", 1.0],
|
||||
["SelectMeaning", 0.5],
|
||||
["Recognition", 1.0],
|
||||
],
|
||||
"recognition": [["Recognition", 1.0]],
|
||||
"final_review": [
|
||||
["FillBlank", 0.7],
|
||||
["SelectMeaning", 0.7],
|
||||
["Recognition", 1.0],
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class NucleonPlaceholder(Nucleon):
|
||||
def __init__(self):
|
||||
super().__init__("__placeholder__", {}, {})
|
||||
|
||||
def __getitem__(self, key):
|
||||
return f"__placeholder__ attempted {key}"
|
||||
|
||||
|
||||
class ElectronPlaceholder(Electron):
|
||||
def __init__(self):
|
||||
super().__init__("__placeholder__", {"": {"": ""}}, "")
|
||||
|
||||
|
||||
class AtomPlaceholder(Atom):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
NucleonPlaceholder(), ElectronPlaceholder(), orbital_placeholder
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
import pathlib
|
||||
|
||||
from heurams.context import config_var
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def probe_by_filename(filename):
|
||||
"""探测指定文件 (无扩展名) 的所有信息"""
|
||||
logger.debug("probe_by_filename: 探测文件 '%s'", filename)
|
||||
paths: dict = config_var.get().get("paths")
|
||||
logger.debug("配置路径: %s", paths)
|
||||
formats = ["toml", "json"]
|
||||
result = {}
|
||||
for item, attr in paths.items():
|
||||
for i in formats:
|
||||
attr: pathlib.Path = pathlib.Path(attr) / filename + "." + i
|
||||
if attr.exists():
|
||||
logger.debug("找到文件: %s", attr)
|
||||
result[item.replace("_dir", "")] = str(attr)
|
||||
else:
|
||||
logger.debug("文件不存在: %s", attr)
|
||||
logger.debug("probe_by_filename 结果: %s", result)
|
||||
return result
|
||||
|
||||
|
||||
def probe_all(is_stem=1):
|
||||
"""依据目录探测所有信息
|
||||
|
||||
Args:
|
||||
is_stem (boolean): 是否**删除**文件扩展名
|
||||
|
||||
Returns:
|
||||
dict: 有三项, 每一项的键名都是文件组类型, 值都是文件组列表, 只包含文件名
|
||||
"""
|
||||
logger.debug("probe_all: 开始探测, is_stem=%d", is_stem)
|
||||
paths: dict = config_var.get().get("paths")
|
||||
logger.debug("配置路径: %s", paths)
|
||||
result = {}
|
||||
for item, attr in paths.items():
|
||||
attr: pathlib.Path = pathlib.Path(attr)
|
||||
result[item.replace("_dir", "")] = list()
|
||||
logger.debug("扫描目录: %s", attr)
|
||||
file_count = 0
|
||||
for i in attr.iterdir():
|
||||
if not i.is_dir():
|
||||
file_count += 1
|
||||
if is_stem:
|
||||
result[item.replace("_dir", "")].append(str(i.stem))
|
||||
else:
|
||||
result[item.replace("_dir", "")].append(str(i.name))
|
||||
logger.debug("目录 %s 中找到 %d 个文件", attr, file_count)
|
||||
logger.debug("probe_all 完成, 结果 keys: %s", list(result.keys()))
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
|
||||
print(os.getcwd())
|
||||
print(probe_all())
|
||||
@@ -1,11 +1,13 @@
|
||||
"""
|
||||
Puzzles 模块 - 生成评估模块
|
||||
Puzzle 模块 - 谜题生成系统
|
||||
|
||||
提供多种类型的辅助评估生成器, 支持从字符串、字典等数据源导入题目
|
||||
提供多种类型的谜题生成器, 支持从字符串、字典等数据源导入题目
|
||||
"""
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
from .base import BasePuzzle
|
||||
from .cloze import ClozePuzzle
|
||||
from .mcq import MCQPuzzle
|
||||
@@ -24,3 +26,38 @@ puzzles = {
|
||||
"recognition": RecognitionPuzzle,
|
||||
"base": BasePuzzle,
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_by_dict(config_dict: dict) -> BasePuzzle:
|
||||
"""
|
||||
根据配置字典创建谜题
|
||||
|
||||
Args:
|
||||
config_dict: 配置字典, 包含谜题类型和参数
|
||||
|
||||
Returns:
|
||||
BasePuzzle: 谜题实例
|
||||
|
||||
Raises:
|
||||
ValueError: 当配置无效时抛出
|
||||
"""
|
||||
logger.debug(
|
||||
"puzzles.create_by_dict: config_dict keys=%s", list(config_dict.keys())
|
||||
)
|
||||
puzzle_type = config_dict.get("type")
|
||||
|
||||
if puzzle_type == "cloze":
|
||||
return puzzles["cloze"](
|
||||
text=config_dict["text"],
|
||||
min_denominator=config_dict.get("min_denominator", 7),
|
||||
)
|
||||
elif puzzle_type == "mcq":
|
||||
return puzzles["mcq"](
|
||||
mapping=config_dict["mapping"],
|
||||
jammer=config_dict.get("jammer", []),
|
||||
max_riddles_num=config_dict.get("max_riddles_num", 2),
|
||||
prefix=config_dict.get("prefix", ""),
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"未知的谜题类型: {puzzle_type}")
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .base import BasePuzzle
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class GuessPuzzle(BasePuzzle):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -72,7 +72,7 @@ class MCQPuzzle(BasePuzzle):
|
||||
|
||||
# 确保至少有4个干扰项
|
||||
while len(self.jammer) < 4:
|
||||
self.jammer.append("")
|
||||
self.jammer.append(" " * (4 - len(self.jammer)))
|
||||
|
||||
unique_jammers = set(jammer + list(self.mapping.values()))
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# mcq.py
|
||||
import random
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
@@ -16,3 +17,4 @@ class RecognitionPuzzle(BasePuzzle):
|
||||
|
||||
def refresh(self):
|
||||
logger.debug("RecognitionPuzzle.refresh(空实现)")
|
||||
pass
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
# Reactor - 记忆流程状态机模块
|
||||
|
||||
Reactor 是 HeurAMS 的记忆流程状态机模块, 和界面 (interface) 的实现是解耦的, 以便后期与其他框架的适配.\
|
||||
得益于 Pickle, 状态机模块支持快照!
|
||||
|
||||
## Router - 全局阶段控制器
|
||||
|
||||
在一次队列记忆流程中, Router 代表记忆流程本身.
|
||||
|
||||
### 属性
|
||||
|
||||
#### 状态属性
|
||||
|
||||
其有状态属性:
|
||||
|
||||
- unsure - 用于初始化
|
||||
- \*quick_review - 复习逾期的单元
|
||||
- \*recognition - 辨识新单元
|
||||
- \*final_review - 复习所有逾期的和新辨认的单元
|
||||
- finished - 表示完成
|
||||
|
||||
> 逾期的: 指 SM-2 算法间隔显示应该复习的单元
|
||||
|
||||
带 * 的属性表示实际的记忆阶段, 由 repo 中 schedule.toml 中 schedule 列表显式声明, 运行过程中可以选择性执行, "空的" Procession 会被直接跳过.
|
||||
|
||||
在初始化 Procession 时, 每个 Procession 被赋予一个不重复的状态属性 作为"阶段状态"属性, 以此标识 Procession 的阶段属性, 因为每个 Procession 管理一个阶段下的复习进程.
|
||||
|
||||
你可以用 state 属性获取 Router 的当前状态.
|
||||
|
||||
#### Procession 属性
|
||||
|
||||
储存一个顺序列表, 保存所有构造的 Procession.\
|
||||
顺序与 repo 中 schedule.toml 中 schedule 列表中的顺序完全相同
|
||||
|
||||
### 初始化
|
||||
|
||||
Router 接受一个存储 Atom 对象的列表, 作为组织记忆流程的材料\
|
||||
在内部, 根据是否激活将其分为 new_atoms 与 old_atoms.\
|
||||
因此, 如果你传入的列表中有算法上"无所事事"的 Atom, 流程会对其进行"加强复习"
|
||||
由此创建 Procession.
|
||||
|
||||
### 直接输出呈现形式
|
||||
|
||||
Router 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
|
||||
其以 ascii 表格形式输出, 格式也符合 markdown 表格规范, 你可以直接复制到 markdown.\
|
||||
示例:
|
||||
|
||||
```text
|
||||
| Type | State | Processions | Current Procession |
|
||||
| :----- | :----- | :--------------------- | :----------------- |
|
||||
| Router | unsure | ['新记忆', '总体复习'] | 新记忆 |
|
||||
```
|
||||
|
||||
| Type | State | Processions | Current Procession |
|
||||
| :----- | :----- | :--------------------- | :----------------- |
|
||||
| Router | unsure | ['新记忆', '总体复习'] | 新记忆 |
|
||||
|
||||
### 方法
|
||||
|
||||
作为一个 Transition Machine 对象的继承, 其拥有 Machine 对象拥有的所有方法.\
|
||||
除此之外, 它也拥有一些其他方法.
|
||||
|
||||
#### current_procession(self)
|
||||
|
||||
用于查询当前的 Procession, 并且根据当前 Procession 更新自身状态.\
|
||||
返回一个 Procession 对象, 是当前阶段的 Procession.\
|
||||
内部运作是返回第一个状态不为 finished 的 Procession, 并将自身状态变更为 Procession 的"阶段状态"属性\
|
||||
若所有 Procession 都已完成, 将返回一个"阶段状态"为 finished 的 Procession 占位符对象(它不在 procession 属性中), 并更新自身状态为 finished.
|
||||
|
||||
## Procession - 阶段管理器
|
||||
|
||||
### 属性
|
||||
|
||||
#### 状态属性
|
||||
|
||||
其有状态属性:
|
||||
|
||||
- active - 标识未完成, 初始化的默认属性
|
||||
- finished - 完成了
|
||||
|
||||
#### 其他属性
|
||||
|
||||
- current_atom: 当前记忆原子的引用
|
||||
- atoms: 队列中所有原子列表
|
||||
- cursor: 指针, 是当前原子在 atoms 列表中的索引
|
||||
- route: "阶段属性"
|
||||
|
||||
> 注意区分 "Router" 和 "Route", 其中 "Route" 表示 "Router State".
|
||||
|
||||
- name\_: 阶段的命名
|
||||
- state: 当前状态属性
|
||||
|
||||
### 初始化
|
||||
|
||||
接受一个 atoms 列表与 route_state (RouterState Enum 类型)对象
|
||||
|
||||
### 直接输出呈现形式
|
||||
|
||||
同 Router, 但显示数据有所不同\
|
||||
与 Router 不同, Procession 显示队列会对过长的 atom.ident 进行缩略(末尾 `>` 符号)
|
||||
|
||||
```text
|
||||
| Type | Name | State | Progress | Procession | Current Atom |
|
||||
| :--------- | :----- | :----- | :------- | :--------------------- | :---------------------------- |
|
||||
| Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, |
|
||||
```
|
||||
|
||||
| Type | Name | State | Progress | Procession | Current Atom |
|
||||
| :--------- | :----- | :----- | :------- | :--------------------- | :---------------------------- |
|
||||
| Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, |
|
||||
|
||||
### 方法
|
||||
|
||||
作为一个 Transition Machine 对象的继承, 其拥有 Machine 对象拥有的所有方法.\
|
||||
除此之外, 它也拥有一些其他方法.
|
||||
|
||||
#### forward(self, step=1)
|
||||
|
||||
移动 cursor 并依情况更新 current_atom 和状态属性\
|
||||
无论 Procession 是否处于完成状态, forward 操作都是可逆的, 你可以传入负数, 此时已完成的 Procession 会自动"重启".
|
||||
|
||||
#### append(self, atom=None)
|
||||
|
||||
追加(回忆失败的)原子(默认为当前原子, 传入 None 会自动转化为当前原子)到队列末端\
|
||||
如果这个原子已经处于队列末端, 不会重复追加, 除非队列只剩下这个原子还没完成(此时最多重复两个)
|
||||
|
||||
#### process(self)
|
||||
|
||||
返回 cursor 值
|
||||
|
||||
#### __len__(self)
|
||||
|
||||
返回剩余原子量(而不是原子总量)\
|
||||
可以使用 len 函数调用
|
||||
获取原子总量请用 len(obj.atoms), 或者 total_length(self) 方法
|
||||
|
||||
#### total_length(self)
|
||||
|
||||
返回队列原子总量
|
||||
|
||||
#### is_empty(self)
|
||||
|
||||
判断是否为空队列(传入原子列表对象是空列表的队列)
|
||||
|
||||
#### get_expander(self)
|
||||
|
||||
获取当前原子的 Expander 对象, 用于单原子调度展开
|
||||
|
||||
## Expander - 单原子调度控制器
|
||||
|
||||
### 属性
|
||||
|
||||
#### 状态属性
|
||||
|
||||
- exammode: 测试模式(默认)
|
||||
- retronly: 仅回顾模式
|
||||
|
||||
#### 其他属性
|
||||
|
||||
- cursor
|
||||
- atom
|
||||
- current_puzzle
|
||||
- orbital_schedule
|
||||
- orbital_puzzles
|
||||
- puzzles
|
||||
|
||||
### 初始化
|
||||
|
||||
接受 atom 对象和 route 参数
|
||||
|
||||
### 方法
|
||||
|
||||
#### get_puzzles(self)
|
||||
@@ -1,8 +1,12 @@
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .expander import Expander
|
||||
from .router import Router
|
||||
from .fission import Fission
|
||||
from .phaser import Phaser
|
||||
from .procession import Procession
|
||||
from .states import RouterState, ProcessionState
|
||||
from .states import PhaserState, ProcessionState
|
||||
|
||||
__all__ = ["RouterState", "ProcessionState", "Procession", "Expander", "Router"]
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = ["PhaserState", "ProcessionState", "Procession", "Fission", "Phaser"]
|
||||
|
||||
logger.debug("反应堆模块已加载")
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import random
|
||||
from functools import reduce
|
||||
|
||||
from tabulate import tabulate as tabu
|
||||
from transitions import Machine
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as puz
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .states import ExpanderState, RouterState
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Expander(Machine):
|
||||
"""单原子调度展开器"""
|
||||
|
||||
def __init__(self, atom: pt.Atom, route=RouterState.RECOGNITION):
|
||||
self.route = route
|
||||
self.cursor = 0
|
||||
self.atom = atom
|
||||
self.current_puzzle_inf: dict
|
||||
# route 为 RouterState 枚举实例, 需要获取其value
|
||||
route_value = route.value
|
||||
states = [
|
||||
{"name": ExpanderState.EXAMMODE.value},
|
||||
{"name": ExpanderState.RETRONLY.value},
|
||||
]
|
||||
|
||||
transitions = [
|
||||
{
|
||||
"trigger": "finish",
|
||||
"source": ExpanderState.EXAMMODE.value,
|
||||
"dest": ExpanderState.RETRONLY.value,
|
||||
},
|
||||
]
|
||||
if route == RouterState.FINISHED:
|
||||
Machine.__init__(
|
||||
self,
|
||||
states=states,
|
||||
transitions=transitions,
|
||||
initial=ExpanderState.EXAMMODE.value,
|
||||
)
|
||||
return
|
||||
orbital_schedule = atom.registry["orbital"]["routes"][route_value] # type: ignore
|
||||
orbital_puzzles = atom.registry["nucleon"]["puzzles"]
|
||||
self.puzzles_inf = list()
|
||||
self.min_ratings = []
|
||||
for item, possibility in orbital_schedule: # type: ignore
|
||||
logger.debug(f"开始处理: {item}")
|
||||
|
||||
puzzle = puz.puzzles[orbital_puzzles[item]["__origin__"]]
|
||||
|
||||
if not isinstance(possibility, float):
|
||||
possibility = float(possibility)
|
||||
|
||||
while possibility > 1:
|
||||
self.puzzles_inf.append(
|
||||
{
|
||||
"puzzle": puzzle,
|
||||
"alia": item,
|
||||
}
|
||||
)
|
||||
possibility -= 1
|
||||
|
||||
if random.random() <= possibility:
|
||||
self.puzzles_inf.append(
|
||||
{
|
||||
"puzzle": puzzle,
|
||||
"alia": item,
|
||||
}
|
||||
)
|
||||
if self.puzzles_inf:
|
||||
self.current_puzzle_inf = self.puzzles_inf[0]
|
||||
|
||||
for i in range(len(self.puzzles_inf)):
|
||||
self.min_ratings.append(float("inf"))
|
||||
|
||||
Machine.__init__(
|
||||
self,
|
||||
states=states,
|
||||
transitions=transitions,
|
||||
initial=ExpanderState.EXAMMODE.value,
|
||||
)
|
||||
|
||||
def get_puzzles_inf(self):
|
||||
if self.state == "retronly":
|
||||
return [{"puzzle": puz.puzzles["recognition"], "alia": "Recognition"}]
|
||||
return self.puzzles_inf
|
||||
|
||||
def get_current_puzzle_inf(self):
|
||||
if self.state == "retronly":
|
||||
return {"puzzle": puz.puzzles["recognition"], "alia": "Recognition"}
|
||||
return self.current_puzzle_inf
|
||||
|
||||
def report(self, rating):
|
||||
if self.puzzles_inf:
|
||||
self.min_ratings[self.cursor] = min(rating, self.min_ratings[self.cursor])
|
||||
|
||||
def get_quality(self):
|
||||
if self.puzzles_inf:
|
||||
if self.is_state("retronly", self):
|
||||
return reduce(lambda x, y: min(x, y), self.min_ratings)
|
||||
raise IndexError
|
||||
|
||||
def forward(self, step=1):
|
||||
"""将谜题指针向前移动并依情况更新或完成"""
|
||||
self.cursor += step
|
||||
if self.cursor >= len(self.puzzles_inf):
|
||||
if self.state != "retronly":
|
||||
self.finish()
|
||||
else:
|
||||
self.current_puzzle_inf = self.puzzles_inf[self.cursor]
|
||||
|
||||
def __repr__(self, style="pipe", ends="\n") -> str:
|
||||
from heurams.services.textproc import truncate
|
||||
|
||||
dic = [
|
||||
{
|
||||
"Type": "Expander",
|
||||
"Atom": truncate(self.atom.ident),
|
||||
"State": self.state,
|
||||
"Progress": f"{self.cursor + 1} / {len(self.puzzles_inf)}",
|
||||
"Procession": list(
|
||||
map(lambda f: truncate(f["alia"]), self.puzzles_inf)
|
||||
),
|
||||
"Current Puzzle": f"{self.current_puzzle_inf['alia']}@{self.current_puzzle_inf['puzzle'].__name__}", # type: ignore
|
||||
}
|
||||
]
|
||||
return str(tabu(dic, headers="keys", tablefmt=style)) + ends
|
||||
@@ -0,0 +1,45 @@
|
||||
import random
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as puz
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .states import PhaserState
|
||||
|
||||
|
||||
class Fission:
|
||||
"""裂变器: 单原子调度展开器"""
|
||||
|
||||
def __init__(self, atom: pt.Atom, phase=PhaserState.RECOGNITION):
|
||||
self.logger = get_logger(__name__)
|
||||
self.atom = atom
|
||||
# print(f"{phase.value}")
|
||||
self.orbital_schedule = atom.registry["orbital"]["schedule"][phase.value] # type: ignore
|
||||
self.orbital_puzzles = atom.registry["orbital"]["puzzles"]
|
||||
# print(self.orbital_schedule)
|
||||
self.puzzles = list()
|
||||
for item, possibility in self.orbital_schedule: # type: ignore
|
||||
print(f"ad:{item}")
|
||||
self.logger.debug(f"开始处理 orbital 项: {item}")
|
||||
if not isinstance(possibility, float):
|
||||
possibility = float(possibility)
|
||||
while possibility > 1:
|
||||
self.puzzles.append(
|
||||
{
|
||||
"puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]],
|
||||
"alia": item,
|
||||
}
|
||||
)
|
||||
possibility -= 1
|
||||
if random.random() <= possibility:
|
||||
self.puzzles.append(
|
||||
{
|
||||
"puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]],
|
||||
"alia": item,
|
||||
}
|
||||
)
|
||||
print(f"ok:{item}")
|
||||
self.logger.debug(f"orbital 项处理完成: {item}")
|
||||
|
||||
def generate(self):
|
||||
yield from self.puzzles
|
||||
@@ -0,0 +1,51 @@
|
||||
# 移相器类定义
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .procession import Procession
|
||||
from .states import PhaserState, ProcessionState
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Phaser:
|
||||
"""移相器: 全局调度阶段管理器"""
|
||||
|
||||
def __init__(self, atoms: list[pt.Atom]) -> None:
|
||||
logger.debug("Phaser.__init__: 原子数量=%d", len(atoms))
|
||||
new_atoms = list()
|
||||
old_atoms = list()
|
||||
self.state = PhaserState.UNSURE
|
||||
for i in atoms:
|
||||
if not i.registry["electron"].is_activated():
|
||||
new_atoms.append(i)
|
||||
else:
|
||||
old_atoms.append(i)
|
||||
logger.debug("新原子数量=%d, 旧原子数量=%d", len(new_atoms), len(old_atoms))
|
||||
self.processions = list()
|
||||
if len(old_atoms):
|
||||
self.processions.append(
|
||||
Procession(old_atoms, PhaserState.QUICK_REVIEW, "初始复习")
|
||||
)
|
||||
logger.debug("创建初始复习 Procession")
|
||||
if len(new_atoms):
|
||||
self.processions.append(
|
||||
Procession(new_atoms, PhaserState.RECOGNITION, "新记忆")
|
||||
)
|
||||
logger.debug("创建新记忆 Procession")
|
||||
self.processions.append(Procession(atoms, PhaserState.FINAL_REVIEW, "总体复习"))
|
||||
logger.debug("创建总体复习 Procession")
|
||||
logger.debug("Phaser 初始化完成, processions 数量=%d", len(self.processions))
|
||||
|
||||
def current_procession(self):
|
||||
logger.debug("Phaser.current_procession 被调用")
|
||||
for i in self.processions:
|
||||
i: Procession
|
||||
if not i.state == ProcessionState.FINISHED:
|
||||
self.state = i.phase
|
||||
logger.debug("找到未完成的 Procession: phase=%s", i.phase)
|
||||
return i
|
||||
self.state = PhaserState.FINISHED
|
||||
logger.debug("所有 Procession 已完成, 状态设置为 FINISHED")
|
||||
return 0
|
||||
@@ -1,101 +1,61 @@
|
||||
from tabulate import tabulate as tabu
|
||||
from transitions import Machine
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .expander import Expander
|
||||
from .states import RouterState, ProcessionState
|
||||
from .states import PhaserState, ProcessionState
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Procession(Machine):
|
||||
class Procession:
|
||||
"""队列: 标识单次记忆流程"""
|
||||
|
||||
def __init__(self, atoms: list, route_state: RouterState, name_: str = ""):
|
||||
def __init__(self, atoms: list, phase: PhaserState, name: str = ""):
|
||||
logger.debug(
|
||||
"Procession.__init__: 原子数量=%d, route=%s, name='%s'",
|
||||
"Procession.__init__: 原子数量=%d, phase=%s, name='%s'",
|
||||
len(atoms),
|
||||
route_state.value,
|
||||
name_,
|
||||
phase.value,
|
||||
name,
|
||||
)
|
||||
self.current_atom: pt.Atom | None
|
||||
self.atoms = atoms
|
||||
self.current_atom = atoms[0] if atoms else None
|
||||
self.queue = atoms.copy()
|
||||
self.current_atom = atoms[0]
|
||||
self.cursor = 0
|
||||
self.name_ = name_
|
||||
self.route = route_state
|
||||
|
||||
states = [
|
||||
{"name": ProcessionState.ACTIVE.value, "on_enter": "on_active"},
|
||||
{"name": ProcessionState.FINISHED.value, "on_enter": "on_finished"},
|
||||
]
|
||||
|
||||
transitions = [
|
||||
{
|
||||
"trigger": "finish",
|
||||
"source": ProcessionState.ACTIVE.value,
|
||||
"dest": ProcessionState.FINISHED.value,
|
||||
},
|
||||
{
|
||||
"trigger": "restart",
|
||||
"source": ProcessionState.FINISHED.value,
|
||||
"dest": ProcessionState.ACTIVE.value,
|
||||
},
|
||||
]
|
||||
|
||||
Machine.__init__(
|
||||
self,
|
||||
states=states,
|
||||
transitions=transitions,
|
||||
initial=ProcessionState.ACTIVE.value,
|
||||
)
|
||||
|
||||
logger.debug("Procession 初始化完成, 队列长度=%d", len(self.atoms))
|
||||
|
||||
def on_active(self):
|
||||
"""进入active状态时的回调"""
|
||||
logger.debug("Procession 进入 active 状态")
|
||||
|
||||
def on_finished(self):
|
||||
"""进入FINISHED状态时的回调"""
|
||||
logger.debug("Procession 进入 FINISHED 状态")
|
||||
self.name = name
|
||||
self.phase = phase
|
||||
self.state: ProcessionState = ProcessionState.RUNNING
|
||||
logger.debug("Procession 初始化完成, 队列长度=%d", len(self.queue))
|
||||
|
||||
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() # 触发状态转换
|
||||
if self.cursor == len(self.queue):
|
||||
self.state = ProcessionState.FINISHED
|
||||
logger.debug("Procession 已完成")
|
||||
else:
|
||||
if self.state != ProcessionState.ACTIVE.value:
|
||||
self.restart() # 确保在active状态
|
||||
self.current_atom = self.atoms[self.cursor]
|
||||
self.state = ProcessionState.RUNNING
|
||||
try:
|
||||
logger.debug("cursor 更新为: %d", self.cursor)
|
||||
logger.debug(
|
||||
"当前原子更新为: %s",
|
||||
self.current_atom.ident if self.current_atom else "None",
|
||||
)
|
||||
self.current_atom = self.queue[self.cursor]
|
||||
logger.debug("当前原子更新为: %s", self.current_atom.ident)
|
||||
return 1 # 成功
|
||||
except IndexError as e:
|
||||
logger.debug("IndexError: %s", e)
|
||||
self.state = ProcessionState.FINISHED
|
||||
logger.debug("Procession 因索引错误而完成")
|
||||
return 0
|
||||
|
||||
def append(self, atom=None):
|
||||
"""追加(回忆失败的)原子(默认为当前原子)到队列末端"""
|
||||
if atom is None:
|
||||
if atom == 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))
|
||||
if self.queue[len(self.queue) - 1] != atom or len(self) <= 1:
|
||||
self.queue.append(atom)
|
||||
logger.debug("原子已追加到队列, 新队列长度=%d", len(self.queue))
|
||||
else:
|
||||
logger.debug("原子未追加(重复或队列长度<=1)")
|
||||
|
||||
def __len__(self):
|
||||
if not self.atoms:
|
||||
return 0
|
||||
length = len(self.atoms) - self.cursor
|
||||
length = len(self.queue) - self.cursor
|
||||
logger.debug("Procession.__len__: 剩余长度=%d", length)
|
||||
return length
|
||||
|
||||
@@ -104,29 +64,11 @@ class Procession(Machine):
|
||||
return self.cursor
|
||||
|
||||
def total_length(self):
|
||||
total = len(self.atoms)
|
||||
total = len(self.queue)
|
||||
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)
|
||||
empty = len(self.queue)
|
||||
logger.debug("Procession.is_empty: %d", empty)
|
||||
return empty
|
||||
|
||||
def get_expander(self):
|
||||
return Expander(atom=self.current_atom, route=self.route) # type: ignore
|
||||
|
||||
def __repr__(self, style="pipe", ends="\n"):
|
||||
from heurams.services.textproc import truncate
|
||||
|
||||
dic = [
|
||||
{
|
||||
"Type": "Procession",
|
||||
"Name": self.name_,
|
||||
"State": self.state,
|
||||
"Progress": f"{self.cursor + 1} / {len(self.atoms)}",
|
||||
"Procession": list(map(lambda f: truncate(f.ident), self.atoms)),
|
||||
"Current Atom": self.current_atom.ident, # type: ignore
|
||||
}
|
||||
]
|
||||
return str(tabu(dic, headers="keys", tablefmt=style)) + ends
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
from transitions import Machine
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
from heurams.kernel.particles.placeholders import AtomPlaceholder
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
from .procession import Procession
|
||||
from .states import RouterState, ProcessionState
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Router(Machine):
|
||||
"""全局调度阶段路由器"""
|
||||
|
||||
def __init__(self, atoms: list[pt.Atom]) -> None:
|
||||
logger.debug(f"Router.__init__: 原子数量={len(atoms)}")
|
||||
|
||||
self.atoms = atoms
|
||||
new_atoms = list()
|
||||
old_atoms = list()
|
||||
|
||||
for i in atoms:
|
||||
if not i.registry["electron"].is_activated():
|
||||
new_atoms.append(i)
|
||||
else:
|
||||
old_atoms.append(i)
|
||||
|
||||
logger.debug(f"新原子数量={len(new_atoms)}, 旧原子数量={len(old_atoms)}")
|
||||
|
||||
self.processions = list()
|
||||
"""路由中的所有队列"""
|
||||
# TODO: 改进为基于配置文件的可选复习阶段
|
||||
if len(old_atoms):
|
||||
self.processions.append(
|
||||
Procession(old_atoms, RouterState.QUICK_REVIEW, "初始复习")
|
||||
)
|
||||
logger.debug("创建初始复习 Procession")
|
||||
|
||||
if len(new_atoms):
|
||||
self.processions.append(
|
||||
Procession(new_atoms, RouterState.RECOGNITION, "新记忆")
|
||||
)
|
||||
logger.debug("创建新记忆 Procession")
|
||||
|
||||
self.processions.append(Procession(atoms, RouterState.FINAL_REVIEW, "总体复习"))
|
||||
logger.debug("创建总体复习 Procession")
|
||||
logger.debug("Router 初始化完成, processions 数量=%d", len(self.processions))
|
||||
|
||||
# 设置transitions状态机
|
||||
states = [
|
||||
{"name": RouterState.UNSURE.value, "on_enter": "on_unsure"},
|
||||
{"name": RouterState.QUICK_REVIEW.value, "on_enter": "on_quick_review"},
|
||||
{"name": RouterState.RECOGNITION.value, "on_enter": "on_recognition"},
|
||||
{"name": RouterState.FINAL_REVIEW.value, "on_enter": "on_final_review"},
|
||||
{"name": RouterState.FINISHED.value, "on_enter": "on_finished"},
|
||||
]
|
||||
|
||||
transitions = [
|
||||
{"trigger": "to_unsure", "source": "*", "dest": RouterState.UNSURE.value},
|
||||
{
|
||||
"trigger": "to_quick_review",
|
||||
"source": "*",
|
||||
"dest": RouterState.QUICK_REVIEW.value,
|
||||
},
|
||||
{
|
||||
"trigger": "to_recognition",
|
||||
"source": "*",
|
||||
"dest": RouterState.RECOGNITION.value,
|
||||
},
|
||||
{
|
||||
"trigger": "to_final_review",
|
||||
"source": "*",
|
||||
"dest": RouterState.FINAL_REVIEW.value,
|
||||
},
|
||||
{
|
||||
"trigger": "to_finished",
|
||||
"source": "*",
|
||||
"dest": RouterState.FINISHED.value,
|
||||
},
|
||||
]
|
||||
|
||||
Machine.__init__(
|
||||
self,
|
||||
states=states,
|
||||
transitions=transitions,
|
||||
initial=RouterState.UNSURE.value,
|
||||
)
|
||||
|
||||
self.to_unsure()
|
||||
|
||||
def on_unsure(self):
|
||||
"""进入UNSURE状态时的回调"""
|
||||
logger.debug("Router 进入 UNSURE 状态")
|
||||
|
||||
def on_quick_review(self):
|
||||
"""进入QUICK_REVIEW状态时的回调"""
|
||||
logger.debug("Router 进入 QUICK_REVIEW 状态")
|
||||
|
||||
def on_recognition(self):
|
||||
"""进入RECOGNITION状态时的回调"""
|
||||
logger.debug("Router 进入 RECOGNITION 状态")
|
||||
|
||||
def on_final_review(self):
|
||||
"""进入FINAL_REVIEW状态时的回调"""
|
||||
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:
|
||||
# if i.route == RouterState.UNSURE: 此判断是不必要的 因为没有这种 Procession
|
||||
if i.route == RouterState.QUICK_REVIEW:
|
||||
self.to_quick_review()
|
||||
elif i.route == RouterState.RECOGNITION:
|
||||
self.to_recognition()
|
||||
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"):
|
||||
from tabulate import tabulate as tabu
|
||||
|
||||
lst = [
|
||||
{
|
||||
"Type": "Router",
|
||||
"State": self.state,
|
||||
"Processions": list(map(lambda f: (f.name_), self.processions)),
|
||||
"Current Procession": "None" if not self.current_procession() else self.current_procession().name_, # type: ignore
|
||||
},
|
||||
]
|
||||
return str(tabu(tabular_data=lst, headers="keys", tablefmt=style)) + ends
|
||||
@@ -1,11 +1,11 @@
|
||||
from enum import Enum
|
||||
from enum import Enum, auto
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RouterState(Enum):
|
||||
class PhaserState(Enum):
|
||||
UNSURE = "unsure"
|
||||
QUICK_REVIEW = "quick_review"
|
||||
RECOGNITION = "recognition"
|
||||
@@ -14,13 +14,8 @@ class RouterState(Enum):
|
||||
|
||||
|
||||
class ProcessionState(Enum):
|
||||
ACTIVE = "active"
|
||||
FINISHED = "finished"
|
||||
|
||||
|
||||
class ExpanderState(Enum):
|
||||
EXAMMODE = "exammode"
|
||||
RETRONLY = "retronly"
|
||||
RUNNING = auto()
|
||||
FINISHED = auto()
|
||||
|
||||
|
||||
logger.debug("状态枚举定义已加载")
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .repo import Repo, RepoManifest
|
||||
|
||||
__all__ = ["Repo", "RepoManifest"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user