0.4.1 版本合并

This commit is contained in:
2025-12-21 03:03:06 +08:00
64 changed files with 2110 additions and 242 deletions

2
.gitignore vendored
View File

@@ -11,8 +11,6 @@ electron/test.toml
build/
dist/
old/
# Project specific directories
# config/
data/cache/
data/electron/

View File

@@ -82,7 +82,7 @@ python -m heurams.interface
## 配置
配置文件位于 `config/config.toml`相对于工作目录. 如果不存在, 会使用内置的默认配置.
配置文件位于 `config/config.toml`(相对于工作目录). 如果不存在, 会使用内置的默认配置.
## 项目结构

View File

@@ -1,7 +1,7 @@
prompt = """HeurAMS 已经被成功地安装在系统中.
prompt = """HeurAMS 已经被成功地安装在系统中.
但 HeurAMS 被设计为一个带有辅助记忆调度器功能的软件包, 无法直接被执行, 但可被其他 Python 程序调用.
若您想启动内置的基本用户界面,
请运行 python -m heurams.interface,
或者 python -m heurams.interface.__main__
注意: 一个常见的误区是, 执行 interface 下的 __main__.py 运行基本用户界面, 这会导致 Python 上下文环境异常, 请不要这样做."""
print(prompt)
print(prompt)

View File

@@ -3,8 +3,9 @@
以及基准路径
"""
from contextvars import ContextVar
import pathlib
from contextvars import ContextVar
from heurams.services.config import ConfigFile
from heurams.services.logger import get_logger

View File

@@ -6,10 +6,10 @@ daystamp_override = -1
timestamp_override = -1
# [调试] 一键通过
quick_pass = 0
quick_pass = 1
# 对于每个项目的默认新记忆原子数量
tasked_number = 8
scheduled_num = 8
# UTC 时间戳修正 仅用于 UNIX 日时间戳的生成修正, 单位为秒
timezone_offset = +28800 # 中国标准时间 (UTC+8)
@@ -22,8 +22,18 @@ max_riddles_num = 2
[puzzles.cloze]
min_denominator = 3
[paths] # 相对于工作目录而言 或绝对路径
[paths] # 相对于配置文件的 ".." (即工作目录) 而言 或绝对路径
nucleon_dir = "./data/nucleon"
electron_dir = "./data/electron"
orbital_dir = "./data/orbital"
cache_dir = "./data/cache"
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 = ""

View File

@@ -1,10 +1,12 @@
from textual.app import App
from textual.widgets import Button
from heurams.services.logger import get_logger
from .screens.about import AboutScreen
from .screens.dashboard import DashboardScreen
from .screens.nucreator import NucleonCreatorScreen
from .screens.precache import PrecachingScreen
from .screens.about import AboutScreen
from heurams.services.logger import get_logger
logger = get_logger(__name__)
@@ -66,11 +68,11 @@ def is_subdir(parent, child):
return 0
# 开发模式
from heurams.context import rootdir, workdir, config_var
from pathlib import Path
from heurams.context import rootdir
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) / ".." / "..")

View File

@@ -1,15 +1,8 @@
#!/usr/bin/env python3
from textual.app import ComposeResult
from textual.widgets import (
Header,
Footer,
Label,
Static,
Button,
Markdown,
)
from textual.containers import ScrollableContainer, ScrollableContainer
from textual.containers import ScrollableContainer
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, Markdown, Static
import heurams.services.version as version
from heurams.context import *

View File

@@ -1,26 +1,20 @@
#!/usr/bin/env python3
import pathlib
from textual.app import ComposeResult
from textual.widgets import (
Header,
Footer,
Label,
ListView,
ListItem,
Button,
Static,
)
from textual.containers import ScrollableContainer
from textual.screen import Screen
from textual.widgets import (Button, Footer, Header, Label, ListItem, ListView,
Static)
from heurams.kernel.particles import *
from heurams.context import *
import heurams.services.version as version
import heurams.services.timer as timer
from .preparation import PreparationScreen
from .about import AboutScreen
import heurams.services.version as version
from heurams.context import *
from heurams.kernel.particles import *
from heurams.services.logger import get_logger
import pathlib
from .about import AboutScreen
from .preparation import PreparationScreen
logger = get_logger(__name__)
@@ -51,8 +45,8 @@ class DashboardScreen(Screen):
res = dict()
filestem = pathlib.Path(filename).stem
res[0] = f"{filename}\0"
from heurams.kernel.particles.loader import load_electron
import heurams.kernel.particles as pt
from heurams.kernel.particles.loader import load_electron
electron_file_path = pathlib.Path(config_var.get()["paths"]["electron_dir"]) / (
filestem + ".json"

View File

@@ -1,16 +1,18 @@
#!/usr/bin/env python3
from textual.app import ComposeResult
from textual.widgets import Header, Footer, Label, Static, Button
from textual.containers import Center, ScrollableContainer
from textual.screen import Screen
from textual.reactive import reactive
from enum import Enum, auto
from heurams.services.logger import get_logger
from heurams.context import config_var
from heurams.kernel.reactor import *
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

View File

@@ -1,21 +1,15 @@
#!/usr/bin/env python3
from pathlib import Path
import toml
from textual.app import ComposeResult
from textual.widgets import (
Header,
Footer,
Label,
Input,
Select,
Button,
Markdown,
)
from textual.containers import ScrollableContainer
from textual.screen import Screen
from textual.widgets import (Button, Footer, Header, Input, Label, Markdown,
Select)
from heurams.services.version import ver
import toml
from pathlib import Path
from heurams.context import config_var
from heurams.services.version import ver
class NucleonCreatorScreen(Screen):
@@ -27,6 +21,7 @@ class NucleonCreatorScreen(Screen):
def search_templates(self):
from pathlib import Path
from heurams.context import config_var
template_dir = Path(config_var.get()["paths"]["template_dir"])

View File

@@ -1,22 +1,15 @@
#!/usr/bin/env python3
from textual.app import ComposeResult
from textual.widgets import (
Header,
Footer,
Label,
Button,
Static,
ProgressBar,
)
from textual.containers import ScrollableContainer, Horizontal
from textual.containers import ScrollableContainer
from textual.screen import Screen
import pathlib
from textual.app import ComposeResult
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
import heurams.kernel.particles as pt
import heurams.services.hasher as hasher
from heurams.context import *
from textual.worker import get_current_worker
class PrecachingScreen(Screen):
@@ -96,7 +89,7 @@ class PrecachingScreen(Screen):
def precache_by_text(self, text: str):
"""预缓存单段文本的音频"""
from heurams.context import rootdir, workdir, config_var
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)
@@ -166,7 +159,7 @@ class PrecachingScreen(Screen):
def precache_all_files(self):
"""预缓存所有文件"""
from heurams.context import rootdir, workdir, config_var
from heurams.context import config_var, rootdir, workdir
nucleon_path = pathlib.Path(config_var.get()["paths"]["nucleon_dir"])
nucleon_files = [
@@ -220,7 +213,8 @@ class PrecachingScreen(Screen):
# 清空缓存
try:
import shutil
from heurams.context import rootdir, workdir, config_var
from heurams.context import config_var, rootdir, workdir
shutil.rmtree(
f"{config_var.get()["paths"]["cache_dir"]}", ignore_errors=True

View File

@@ -1,21 +1,15 @@
#!/usr/bin/env python3
from textual.app import ComposeResult
from textual.widgets import (
Header,
Footer,
Label,
Static,
Button,
Markdown,
)
from textual.containers import ScrollableContainer
from textual.reactive import reactive
from textual.screen import Screen
from heurams.context import config_var
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 textual.reactive import reactive
from textual.widget import Widget
from heurams.context import config_var
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -1,22 +1,15 @@
#!/usr/bin/env python3
from textual.app import ComposeResult
from textual.widgets import (
Header,
Footer,
Label,
Button,
Static,
ProgressBar,
)
from textual.containers import ScrollableContainer, Horizontal
from textual.containers import ScrollableContainer
from textual.screen import Screen
import pathlib
from textual.app import ComposeResult
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
import heurams.kernel.particles as pt
import heurams.services.hasher as hasher
from heurams.context import *
from textual.worker import get_current_worker
class SyncScreen(Screen):

View File

@@ -1,10 +1,11 @@
"""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 heurams.interface.widgets as pzw
from typing import TypedDict
staging = {} # 细粒度缓存区, 是 ident -> quality 的封装

View File

@@ -1,6 +1,8 @@
from typing import Iterable
from textual.app import ComposeResult
from textual.widget import Widget
import heurams.kernel.particles as pt

View File

@@ -1,13 +1,11 @@
from textual.widgets import (
Label,
Static,
Button,
)
from textual.containers import ScrollableContainer, Horizontal
from textual.widget import Widget
import heurams.kernel.particles as pt
from .base_puzzle_widget import BasePuzzleWidget
from textual.containers import Horizontal, ScrollableContainer
from textual.message import Message
from textual.widget import Widget
from textual.widgets import Button, Label, Static
import heurams.kernel.particles as pt
from .base_puzzle_widget import BasePuzzleWidget
class BasicEvaluation(BasePuzzleWidget):
@@ -51,7 +49,7 @@ class BasicEvaluation(BasePuzzleWidget):
# 显示主要内容
yield Label(self.atom.registry["nucleon"]["content"], id="main")
# 显示评估说明可选
# 显示评估说明(可选)
yield Static("请评估你对这个内容的记忆程度: ", classes="instruction")
# 按钮容器

View File

@@ -1,17 +1,17 @@
from textual.widgets import (
Label,
Button,
)
from textual.widget import Widget
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from .base_puzzle_widget import BasePuzzleWidget
import copy
import random
from typing import TypedDict
from textual.containers import Container
from textual.message import Message
from textual.widget import Widget
from textual.widgets import Button, Label
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.services.logger import get_logger
from typing import TypedDict
from .base_puzzle_widget import BasePuzzleWidget
logger = get_logger(__name__)

View File

@@ -1,8 +1,5 @@
from textual.widgets import (
Label,
Button,
)
from textual.widget import Widget
from textual.widgets import Button, Label
class Finished(Widget):

View File

@@ -1,17 +1,17 @@
# 单项选择题
from textual.widgets import (
Label,
Button,
)
from textual.containers import ScrollableContainer, Container
from typing import TypedDict
from textual.containers import Container, ScrollableContainer
from textual.widget import Widget
from textual.widgets import Button, Label
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from .base_puzzle_widget import BasePuzzleWidget
from typing import TypedDict
from heurams.services.hasher import hash
from heurams.services.logger import get_logger
from .base_puzzle_widget import BasePuzzleWidget
logger = get_logger(__name__)
@@ -116,7 +116,7 @@ class MCQPuzzle(BasePuzzleWidget):
self.screen.rating = rating # type: ignore
self.handler(rating)
# 重置输入如果回答错误
# 重置输入(如果回答错误)
if not is_correct:
self.inputlist = []
self.refresh_buttons()
@@ -127,7 +127,7 @@ class MCQPuzzle(BasePuzzleWidget):
self.update_display()
def refresh_buttons(self):
"""刷新按钮显示用于题目切换"""
"""刷新按钮显示(用于题目切换)"""
# 移除所有选项按钮
logger.debug("刷新按钮")
self.cursor += 1

View File

@@ -1,8 +1,5 @@
from textual.widgets import (
Label,
Button,
)
from textual.widget import Widget
from textual.widgets import Button, Label
class Placeholder(Widget):

View File

@@ -1,20 +1,17 @@
from textual.reactive import reactive
from textual.widgets import (
Markdown,
Label,
Static,
Button,
)
from textual.containers import Center
from textual.widget import Widget
from typing import Dict
import heurams.kernel.particles as pt
import re
from .base_puzzle_widget import BasePuzzleWidget
from typing import TypedDict, List
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
import heurams.kernel.particles as pt
from heurams.services.logger import get_logger
from .base_puzzle_widget import BasePuzzleWidget
logger = get_logger(__name__)

View File

@@ -1,6 +1,8 @@
from .sm2 import SM2Algorithm
from heurams.services.logger import get_logger
from .sm2 import SM2Algorithm
from .sm15m import SM15MAlgorithm
logger = get_logger(__name__)
__all__ = [
@@ -9,7 +11,8 @@ __all__ = [
algorithms = {
"SM-2": SM2Algorithm,
"supermemo2": SM2Algorithm,
"SM-15M": SM15MAlgorithm,
# "SM-15M": SM15MAlgorithm,
}
logger.debug("算法模块初始化完成, 注册的算法: %s", list(algorithms.keys()))

View File

@@ -1,5 +1,6 @@
import heurams.services.timer as timer
from typing import TypedDict
import heurams.services.timer as timer
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -0,0 +1,281 @@
"""
SM-15 接口兼容实现, 基于 SM-15 算法的逆向工程
全局状态保存在文件中, 项目状态通过 algodata 字典传递
基于: https://github.com/kazuaki/sm.js
原始 CoffeeScript 代码: (c) 2014 Kazuaki Tanida (MIT 许可证)
"""
import datetime
import json
import os
from typing import TypedDict
from heurams.kernel.algorithms.sm15m_calc import (MAX_AF, MIN_AF, NOTCH_AF,
RANGE_AF, RANGE_REPETITION,
SM, THRESHOLD_RECALL, Item)
# 全局状态文件路径
_GLOBAL_STATE_FILE = os.path.expanduser("~/.sm15_global_state.json")
def _get_global_sm():
"""获取全局 SM 实例, 从文件加载或创建新的"""
if os.path.exists(_GLOBAL_STATE_FILE):
try:
with open(_GLOBAL_STATE_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
sm_instance = SM.load(data)
return sm_instance
except Exception:
# 如果加载失败, 创建新的实例
pass
# 创建新的 SM 实例
sm_instance = SM()
# 保存初始状态
_save_global_sm(sm_instance)
return sm_instance
def _save_global_sm(sm_instance):
"""保存全局 SM 实例到文件"""
try:
data = sm_instance.data()
with open(_GLOBAL_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
except Exception:
# 忽略保存错误
pass
class SM15MAlgorithm:
algo_name = "SM-15M"
class AlgodataDict(TypedDict):
efactor: float
real_rept: int
rept: int
interval: int
last_date: int
next_date: int
is_activated: int
last_modify: float
defaults = {
"efactor": 2.5,
"real_rept": 0,
"rept": 0,
"interval": 0,
"last_date": 0,
"next_date": 0,
"is_activated": 0,
"last_modify": 0.0,
}
@classmethod
def _get_timestamp(cls):
"""获取当前时间戳(秒)"""
return datetime.datetime.now().timestamp()
@classmethod
def _get_daystamp(cls):
"""获取当前天数戳(从某个纪元开始的天数)"""
# 使用与原始 SM-2 相同的纪元1970-01-01
now = datetime.datetime.now()
epoch = datetime.datetime(1970, 1, 1)
delta = now - epoch
return delta.days
@classmethod
def _algodata_to_item(cls, algodata, sm_instance):
"""将 algodata 转换为 Item 实例"""
# 从 algodata 获取 SM-2 数据
sm15_data = algodata.get(cls.algo_name, cls.defaults.copy())
# 创建 Item 实例
item = Item(sm_instance)
# 映射字段
# efactor -> A-Factor (需要转换)
efactor = sm15_data.get("efactor", 2.5)
# SM-2 的 efactor 范围 [1.3, 2.5+], SM-15 的 A-Factor 范围 [1.2, 6.9]
# 简单线性映射af = (efactor - 1.3) * (MAX_AF - MIN_AF) / (2.5 - 1.3) + MIN_AF
# 但 efactor 可能大于 2.5, 所以需要限制
af = max(MIN_AF, min(MAX_AF, efactor * 2.0)) # 粗略映射
# 调试
# print(f"DEBUG: efactor={efactor}, af before set={af}")
item.af(af)
# print(f"DEBUG: item.af() after set={item.af()}")
# rept -> repetition (成功回忆次数)
rept = sm15_data.get("rept", 0)
item.repetition = (
rept - 1 if rept > 0 else -1
) # SM-15 中 repetition=-1 表示新项目
# real_rept -> lapse? 或者忽略
real_rept = sm15_data.get("real_rept", 0)
# 可以存储在 value 中或忽略
# interval -> optimum_interval (需要从天数转换为毫秒)
interval_days = sm15_data.get("interval", 0)
if interval_days == 0:
item.optimum_interval = sm_instance.interval_base
else:
item.optimum_interval = interval_days * 24 * 60 * 60 * 1000 # 天转毫秒
# last_date -> previous_date
last_date_days = sm15_data.get("last_date", 0)
if last_date_days > 0:
epoch = datetime.datetime(1970, 1, 1)
item.previous_date = epoch + datetime.timedelta(days=last_date_days)
# next_date -> due_date
next_date_days = sm15_data.get("next_date", 0)
if next_date_days > 0:
epoch = datetime.datetime(1970, 1, 1)
item.due_date = epoch + datetime.timedelta(days=next_date_days)
# is_activated 和 last_modify 忽略
# 将原始 algodata 保存在 value 中以便恢复
item.value = {
"front": "SM-15 item",
"back": "SM-15 item",
"_sm15_data": sm15_data,
}
return item
@classmethod
def _item_to_algodata(cls, item, algodata):
"""将 Item 实例状态写回 algodata"""
if cls.algo_name not in algodata:
algodata[cls.algo_name] = cls.defaults.copy()
sm15_data = algodata[cls.algo_name]
# A-Factor -> efactor (反向映射)
af = item.af()
if af is None:
af = MIN_AF
# 反向粗略映射
efactor = max(1.3, min(af / 2.0, 10.0)) # 限制范围
# 调试
# print(f"DEBUG: item.af()={af}, computed efactor={efactor}")
sm15_data["efactor"] = efactor
# repetition -> rept
rept = item.repetition + 1 if item.repetition >= 0 else 0
sm15_data["rept"] = rept
# real_rept: 递增在 revisor 中处理, 这里保持不变
# 但如果没有 real_rept 字段, 则初始化为0
if "real_rept" not in sm15_data:
sm15_data["real_rept"] = 0
# optimum_interval -> interval (毫秒转天)
interval_ms = item.optimum_interval
if interval_ms == item.sm.interval_base:
sm15_data["interval"] = 0
else:
interval_days = max(0, round(interval_ms / (24 * 60 * 60 * 1000)))
sm15_data["interval"] = interval_days
# previous_date -> last_date
if item.previous_date:
epoch = datetime.datetime(1970, 1, 1)
last_date_days = (item.previous_date - epoch).days
sm15_data["last_date"] = last_date_days
else:
sm15_data["last_date"] = 0
# due_date -> next_date
if item.due_date:
epoch = datetime.datetime(1970, 1, 1)
next_date_days = (item.due_date - epoch).days
sm15_data["next_date"] = next_date_days
else:
sm15_data["next_date"] = 0
# is_activated: 保持不变或设为1
if "is_activated" not in sm15_data:
sm15_data["is_activated"] = 1
# last_modify: 更新时间戳
sm15_data["last_modify"] = cls._get_timestamp()
return algodata
@classmethod
def revisor(
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
):
"""SM-15 算法迭代决策机制实现"""
# 获取全局 SM 实例
sm_instance = _get_global_sm()
# 将 algodata 转换为 Item
item = cls._algodata_to_item(algodata, sm_instance)
# 处理 is_new_activation
if is_new_activation:
# 重置为初始状态
item.repetition = -1
item.lapse = 0
item.optimum_interval = sm_instance.interval_base
item.previous_date = None
item.due_date = datetime.datetime.fromtimestamp(0)
item.af(2.5) # 重置 efactor
# 将项目临时添加到 SM 实例(以便 answer 更新共享状态)
sm_instance.q.append(item)
# 处理反馈(评分)
# SM-2 的 feedback 是 0-5, SM-15 的 grade 也是 0-5
grade = feedback
now = datetime.datetime.now()
# 调用 answer 方法
item.answer(grade, now)
# 更新共享状态(FI-Graph, ForgettingCurves, OFM)
if item.repetition >= 0:
sm_instance.forgetting_curves.register_point(grade, item, now)
sm_instance.ofm.update()
sm_instance.fi_g.update(grade, item, now)
# 从队列中移除项目
sm_instance.q.remove(item)
# 保存全局状态
_save_global_sm(sm_instance)
# 将更新后的 Item 状态写回 algodata
cls._item_to_algodata(item, algodata)
# 更新 real_rept(总复习次数)
algodata[cls.algo_name]["real_rept"] += 1
@classmethod
def is_due(cls, algodata):
"""检查项目是否到期"""
sm15_data = algodata.get(cls.algo_name, cls.defaults.copy())
next_date_days = sm15_data.get("next_date", 0)
current_daystamp = cls._get_daystamp()
return next_date_days <= current_daystamp
@classmethod
def rate(cls, algodata):
"""获取项目的评分(返回 efactor 字符串)"""
sm15_data = algodata.get(cls.algo_name, cls.defaults.copy())
efactor = sm15_data.get("efactor", 2.5)
return str(efactor)
@classmethod
def nextdate(cls, algodata) -> int:
"""获取下次复习日期(天数戳)"""
sm15_data = algodata.get(cls.algo_name, cls.defaults.copy())
next_date_days = sm15_data.get("next_date", 0)
return next_date_days

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
from .base import BaseAlgorithm
import heurams.services.timer as timer
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__)

View File

@@ -9,12 +9,12 @@ 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 .atom import Atom, atom_registry
from .probe import probe_all, probe_by_filename
from .loader import load_nucleon, load_electron
__all__ = [
"Electron",

View File

@@ -1,14 +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
from typing import TypedDict
import pathlib
import typing
import toml
import json
import bidict
from heurams.context import config_var
from heurams.services.logger import get_logger
logger = get_logger(__name__)
@@ -16,7 +19,7 @@ logger = get_logger(__name__)
class AtomRegister_runtime(TypedDict):
locked: bool # 只读锁定标识符
min_rate: int # 最低评分
newact: bool # 新激活
newact: bool # 新激活
class AtomRegister(TypedDict):
@@ -68,9 +71,9 @@ class Atom:
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
if key == "electron":
if self.registry["electron"].is_activated() == 0:
self.registry["runtime"]["newact"] = True
else:
logger.error("尝试链接不受支持的键: '%s'", key)
raise ValueError("不受支持的原子元数据链接操作")
@@ -104,7 +107,10 @@ class Atom:
"""
if self.registry["runtime"]["locked"]:
logger.debug(f"允许总评分: {self.registry['runtime']['min_rate']}")
self.registry["electron"].revisor(self.registry["runtime"]["min_rate"], is_new_activation=self.registry["runtime"]["newact"])
self.registry["electron"].revisor(
self.registry["runtime"]["min_rate"],
is_new_activation=self.registry["runtime"]["newact"],
)
else:
logger.debug("禁止总评分")

View File

@@ -9,7 +9,7 @@ logger = get_logger(__name__)
class Electron:
"""电子: 记忆分析元数据及算法"""
def __init__(self, ident: str, algodata: dict = {}, algo_name: str = "supermemo2"):
def __init__(self, ident: str, algodata: dict = {}, algo_name: str = "SM-2"):
"""初始化电子对象 (记忆数据)
Args:

View File

@@ -1,12 +1,15 @@
from .nucleon import Nucleon
from .electron import Electron
import heurams.services.hasher as hasher
import pathlib
import toml
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__)

View File

@@ -1,4 +1,5 @@
from typing import TypedDict
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -1,5 +1,6 @@
from heurams.context import config_var
import pathlib
from heurams.context import config_var
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -8,7 +8,7 @@ class BasePuzzle:
"""谜题基类"""
def refresh(self):
logger.debug("BasePuzzle.refresh 被调用未实现")
logger.debug("BasePuzzle.refresh 被调用(未实现)")
raise NotImplementedError("谜题对象未实现 refresh 方法")
def __str__(self):

View File

@@ -1,7 +1,9 @@
from .base import BasePuzzle
import random
from heurams.services.logger import get_logger
from .base import BasePuzzle
logger = get_logger(__name__)

View File

@@ -1,9 +1,11 @@
# mcq.py
from .base import BasePuzzle
import random
from typing import List, Dict, Optional, Union
from typing import Dict, List, Optional, Union
from heurams.services.logger import get_logger
from .base import BasePuzzle
logger = get_logger(__name__)

View File

@@ -1,8 +1,10 @@
# mcq.py
from .base import BasePuzzle
import random
from heurams.services.logger import get_logger
from .base import BasePuzzle
logger = get_logger(__name__)
@@ -14,5 +16,5 @@ class RecognitionPuzzle(BasePuzzle):
super().__init__()
def refresh(self):
logger.debug("RecognitionPuzzle.refresh空实现")
logger.debug("RecognitionPuzzle.refresh(空实现)")
pass

View File

@@ -1,8 +1,9 @@
from .states import PhaserState, ProcessionState
from .procession import Procession
from heurams.services.logger import get_logger
from .fission import Fission
from .phaser import Phaser
from heurams.services.logger import get_logger
from .procession import Procession
from .states import PhaserState, ProcessionState
logger = get_logger(__name__)

View File

@@ -1,9 +1,11 @@
import random
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as puz
import random
from .states import PhaserState
from heurams.services.logger import get_logger
from .states import PhaserState
class Fission:
"""裂变器: 单原子调度展开器"""

View File

@@ -1,10 +1,11 @@
# 移相器类定义
import heurams.kernel.particles as pt
from .states import PhaserState, ProcessionState
from .procession import Procession
from heurams.services.logger import get_logger
from .procession import Procession
from .states import PhaserState, ProcessionState
logger = get_logger(__name__)

View File

@@ -1,7 +1,8 @@
import heurams.kernel.particles as pt
from .states import PhaserState, ProcessionState
from heurams.services.logger import get_logger
from .states import PhaserState, ProcessionState
logger = get_logger(__name__)
@@ -51,7 +52,7 @@ class Procession:
self.queue.append(atom)
logger.debug("原子已追加到队列, 新队列长度=%d", len(self.queue))
else:
logger.debug("原子未追加重复或队列长度<=1")
logger.debug("原子未追加(重复或队列长度<=1)")
def __len__(self):
length = len(self.queue) - self.cursor

View File

@@ -1,4 +1,5 @@
from enum import Enum, auto
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -1,8 +1,8 @@
# 音频播放器, 必须基于文件操作
from . import termux_audio
from . import playsound_audio
from heurams.services.logger import get_logger
from . import playsound_audio, termux_audio
logger = get_logger(__name__)
__all__ = [

View File

@@ -5,7 +5,9 @@
import os
import pathlib
import playsound
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -1,5 +1,6 @@
from typing import Protocol
import pathlib
from typing import Protocol
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -5,6 +5,7 @@
import os
import pathlib
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -2,4 +2,4 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__)
logger.debug("OpenAI provider 模块已加载未实现")
logger.debug("OpenAI provider 模块已加载(未实现)")

View File

@@ -1,6 +1,7 @@
from heurams.services.logger import get_logger
from .base import BaseTTS
from .edge_tts import EdgeTTS
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -1,4 +1,5 @@
import pathlib
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -1,8 +1,11 @@
from .base import BaseTTS
import pathlib
import edge_tts
from heurams.services.logger import get_logger
from .base import BaseTTS
logger = get_logger(__name__)

View File

@@ -1,7 +1,8 @@
# 音频服务
from typing import Callable
from heurams.context import config_var
from heurams.providers.audio import providers as prov
from typing import Callable
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -1,7 +1,9 @@
# 配置文件服务
import pathlib
import toml
import typing
import toml
from heurams.services.logger import get_logger

View File

@@ -1,5 +1,6 @@
# 哈希服务
import hashlib
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -45,7 +45,7 @@ def setup_logging(
# 创建formatter
formatter = logging.Formatter(log_format, date_format)
# 创建文件handler使用RotatingFileHandler防止日志过大
# 创建文件handler(使用RotatingFileHandler防止日志过大)
file_handler = logging.handlers.RotatingFileHandler(
filename=log_path,
maxBytes=max_bytes,
@@ -55,7 +55,7 @@ def setup_logging(
file_handler.setFormatter(formatter)
file_handler.setLevel(log_level)
# 配置root logger - 设置为 WARNING 级别只记录重要信息
# 配置root logger - 设置为 WARNING 级别(只记录重要信息)
root_logger = logging.getLogger()
root_logger.setLevel(logging.WARNING) # 这里改为 WARNING
@@ -63,7 +63,7 @@ def setup_logging(
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 创建自己的应用logger单独设置DEBUG级别
# 创建自己的应用logger(单独设置DEBUG级别)
app_logger = logging.getLogger("heurams")
app_logger.setLevel(log_level) # 保持DEBUG级别
app_logger.addHandler(file_handler)
@@ -146,7 +146,7 @@ def exception(msg: str, *args, **kwargs) -> None:
get_logger().exception(msg, *args, **kwargs)
# 初始化日志系统硬编码配置
# 初始化日志系统(硬编码配置)
setup_logging()

View File

@@ -1,6 +1,7 @@
# 时间服务
from heurams.context import config_var
import time
from heurams.context import config_var
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -1,7 +1,8 @@
# 文本转语音服务
from typing import Callable
from heurams.context import config_var
from heurams.providers.tts import TTSs
from typing import Callable
from heurams.services.logger import get_logger
logger = get_logger(__name__)

View File

@@ -3,7 +3,7 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__)
ver = "0.4.0"
ver = "0.4.1"
stage = "prototype"
codename = "fledge" # 雏鸟, 0.4.x 版本

View File

@@ -1,25 +1,26 @@
#!/usr/bin/env python3
"""
DashboardScreen 的测试, 包括单元测试和 pilot 测试.
DashboardScreen 的测试, 包括单元测试和 pilot 测试.
"""
import unittest
import tempfile
import pathlib
import tempfile
import time
from unittest.mock import patch, MagicMock
import unittest
from unittest.mock import MagicMock, patch
from textual.pilot import Pilot
from heurams.context import ConfigContext
from heurams.services.config import ConfigFile
from heurams.interface.__main__ import HeurAMSApp
from heurams.interface.screens.dashboard import DashboardScreen
from heurams.services.config import ConfigFile
class TestDashboardScreenUnit(unittest.TestCase):
"""DashboardScreen 的单元测试不启动完整应用. """
"""DashboardScreen 的单元测试(不启动完整应用)."""
def setUp(self):
"""在每个测试之前运行, 设置临时目录和配置. """
"""在每个测试之前运行, 设置临时目录和配置."""
# 创建临时目录用于测试数据
self.temp_dir = tempfile.TemporaryDirectory()
self.temp_path = pathlib.Path(self.temp_dir.name)
@@ -53,12 +54,12 @@ class TestDashboardScreenUnit(unittest.TestCase):
self.config_ctx.__enter__()
def tearDown(self):
"""在每个测试之后清理. """
"""在每个测试之后清理."""
self.config_ctx.__exit__(None, None, None)
self.temp_dir.cleanup()
def test_compose(self):
"""测试 compose 方法返回正确的部件. """
"""测试 compose 方法返回正确的部件."""
screen = DashboardScreen()
# 手动调用 compose 并收集部件
from textual.app import ComposeResult
@@ -66,7 +67,7 @@ class TestDashboardScreenUnit(unittest.TestCase):
result = screen.compose()
widgets = list(result)
# 检查是否包含 Header 和 Footer
from textual.widgets import Header, Footer
from textual.widgets import Footer, Header
header_present = any(isinstance(w, Header) for w in widgets)
footer_present = any(isinstance(w, Footer) for w in widgets)
@@ -84,7 +85,7 @@ class TestDashboardScreenUnit(unittest.TestCase):
self.assertEqual(list_view.__class__.__name__, "ListView")
def test_item_desc_generator(self):
"""测试 item_desc_generator 函数. """
"""测试 item_desc_generator 函数."""
screen = DashboardScreen()
# 模拟一个文件名
filename = "test.toml"
@@ -100,10 +101,10 @@ class TestDashboardScreenUnit(unittest.TestCase):
@unittest.skip("Pilot 测试需要进一步配置, 暂不运行")
class TestDashboardScreenPilot(unittest.TestCase):
"""使用 Textual Pilot 的集成测试. """
"""使用 Textual Pilot 的集成测试."""
def setUp(self):
"""配置临时目录和配置. """
"""配置临时目录和配置."""
self.temp_dir = tempfile.TemporaryDirectory()
self.temp_path = pathlib.Path(self.temp_dir.name)
@@ -134,7 +135,7 @@ class TestDashboardScreenPilot(unittest.TestCase):
self.temp_dir.cleanup()
def test_dashboard_loads_with_pilot(self):
"""使用 Pilot 测试 DashboardScreen 加载. """
"""使用 Pilot 测试 DashboardScreen 加载."""
with patch("heurams.interface.__main__.environment_check"):
app = HeurAMSApp()
# 注意: Pilot 在 Textual 6.9.0 中的用法可能不同

View File

@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
from heurams.kernel.algorithms.sm2 import SM2Algorithm
@@ -94,7 +94,7 @@ class TestSM2Algorithm(unittest.TestCase):
SM2Algorithm.revisor(algodata, feedback=5, is_new_activation=True)
self.assertEqual(algodata[SM2Algorithm.algo_name]["rept"], 0)
self.assertEqual(algodata[SM2Algorithm.algo_name]["efactor"], 2.5)
# interval 应为 1因为 rept=0
# interval 应为 1(因为 rept=0)
self.assertEqual(algodata[SM2Algorithm.algo_name]["interval"], 1)
def test_revisor_efactor_calculation(self):

View File

@@ -1,15 +1,16 @@
import unittest
from unittest.mock import patch, MagicMock
import json
import pathlib
import tempfile
import toml
import json
import unittest
from unittest.mock import MagicMock, patch
import toml
from heurams.context import ConfigContext
from heurams.kernel.particles.atom import Atom, atom_registry
from heurams.kernel.particles.electron import Electron
from heurams.kernel.particles.nucleon import Nucleon
from heurams.kernel.particles.orbital import Orbital
from heurams.context import ConfigContext
from heurams.services.config import ConfigFile

View File

@@ -1,9 +1,9 @@
import unittest
from unittest.mock import patch, MagicMock
import sys
import unittest
from unittest.mock import MagicMock, patch
from heurams.kernel.particles.electron import Electron
from heurams.kernel.algorithms import algorithms
from heurams.kernel.particles.electron import Electron
class TestElectron(unittest.TestCase):
@@ -27,7 +27,7 @@ class TestElectron(unittest.TestCase):
self.assertEqual(electron.algo, algorithms["supermemo2"])
self.assertIn(electron.algo, electron.algodata)
self.assertIsInstance(electron.algodata[electron.algo], dict)
# 检查默认值排除动态字段
# 检查默认值(排除动态字段)
defaults = electron.algo.defaults
for key, value in defaults.items():
if key == "last_modify":

View File

@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
from heurams.kernel.particles.nucleon import Nucleon

View File

@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
from heurams.kernel.puzzles.cloze import ClozePuzzle

View File

@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, MagicMock, call
from unittest.mock import MagicMock, call, patch
from heurams.kernel.puzzles.mcq import MCQPuzzle
@@ -48,7 +48,7 @@ class TestMCQPuzzle(unittest.TestCase):
# 模拟 random.sample 返回前两个映射项
mock_sample.side_effect = [
[("q1", "a1"), ("q2", "a2")], # 选择问题
["j1", "j2", "j3"], # 为每个问题选择干扰项实际调用两次
["j1", "j2", "j3"], # 为每个问题选择干扰项(实际调用两次)
]
puzzle.refresh()
@@ -59,7 +59,7 @@ class TestMCQPuzzle(unittest.TestCase):
self.assertEqual(puzzle.answer, ["a1", "a2"])
# 检查 options 列表
self.assertEqual(len(puzzle.options), 2)
# 每个选项列表应包含 4 个选项正确答案 + 3 个干扰项
# 每个选项列表应包含 4 个选项(正确答案 + 3 个干扰项)
self.assertEqual(len(puzzle.options[0]), 4)
self.assertEqual(len(puzzle.options[1]), 4)
# random.shuffle 应被调用

View File

@@ -1,10 +1,10 @@
import unittest
from unittest.mock import Mock, patch, MagicMock
from unittest.mock import MagicMock, Mock, patch
from heurams.kernel.reactor.phaser import Phaser
from heurams.kernel.reactor.states import PhaserState, ProcessionState
from heurams.kernel.particles.atom import Atom
from heurams.kernel.particles.electron import Electron
from heurams.kernel.reactor.phaser import Phaser
from heurams.kernel.reactor.states import PhaserState, ProcessionState
class TestPhaser(unittest.TestCase):