From baa7ac8ee94f83310b9c6a33eae4867002a9dd75 Mon Sep 17 00:00:00 2001 From: david-ajax Date: Sat, 13 Dec 2025 21:47:37 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/nucleon/test.toml | 2 +- src/heurams/context.py | 28 ++-- src/heurams/interface/__main__.py | 28 ++-- src/heurams/interface/screens/about.py | 1 + src/heurams/interface/screens/dashboard.py | 51 ++++-- src/heurams/interface/screens/memorizor.py | 36 ++-- src/heurams/interface/screens/nucreator.py | 25 ++- src/heurams/interface/screens/precache.py | 98 ++++++----- src/heurams/interface/screens/preparation.py | 23 ++- src/heurams/interface/shim.py | 22 ++- .../interface/widgets/base_puzzle_widget.py | 23 ++- src/heurams/interface/widgets/basic_puzzle.py | 51 ++++-- src/heurams/interface/widgets/cloze_puzzle.py | 61 +++++-- src/heurams/interface/widgets/finished.py | 23 ++- src/heurams/interface/widgets/mcq_puzzle.py | 154 ++++++++---------- src/heurams/interface/widgets/placeholder.py | 20 ++- src/heurams/interface/widgets/recognition.py | 36 +++- src/heurams/kernel/algorithms/__init__.py | 4 +- src/heurams/kernel/algorithms/base.py | 31 ++-- src/heurams/kernel/algorithms/fsrs.py | 2 +- src/heurams/kernel/algorithms/sm2.py | 87 +++++----- src/heurams/kernel/particles/__init__.py | 2 +- src/heurams/kernel/particles/atom.py | 25 +-- src/heurams/kernel/particles/electron.py | 23 +-- src/heurams/kernel/particles/loader.py | 31 ++-- src/heurams/kernel/particles/nucleon.py | 8 +- src/heurams/kernel/particles/orbital.py | 6 +- src/heurams/kernel/particles/probe.py | 12 +- src/heurams/kernel/puzzles/__init__.py | 29 ++-- src/heurams/kernel/puzzles/base.py | 6 +- src/heurams/kernel/puzzles/cloze.py | 3 +- src/heurams/kernel/puzzles/mcq.py | 23 +-- src/heurams/kernel/puzzles/recognition.py | 7 +- src/heurams/kernel/reactor/__init__.py | 8 +- src/heurams/kernel/reactor/fission.py | 35 ++-- src/heurams/kernel/reactor/phaser.py | 16 +- src/heurams/kernel/reactor/procession.py | 16 +- src/heurams/kernel/reactor/states.py | 4 +- src/heurams/providers/audio/__init__.py | 5 +- .../providers/audio/playsound_audio.py | 5 +- src/heurams/providers/audio/protocol.py | 3 +- src/heurams/providers/audio/termux_audio.py | 8 +- src/heurams/providers/llm/__init__.py | 2 +- src/heurams/providers/tts/__init__.py | 4 +- src/heurams/providers/tts/base.py | 5 +- src/heurams/providers/tts/edge_tts.py | 3 +- src/heurams/services/audio_service.py | 2 +- src/heurams/services/config.py | 13 +- src/heurams/services/hasher.py | 6 +- src/heurams/services/timer.py | 8 +- src/heurams/services/tts_service.py | 2 +- src/heurams/services/version.py | 2 +- tests/__init__.py | 2 +- tests/conftest.py | 27 ++- tests/examples.py | 27 ++- tests/run_tests.py | 38 ++--- tests/test_algorithms.py | 3 +- tests/test_particles.py | 37 ++--- tests/test_puzzles.py | 3 +- tests/test_reactor.py | 16 +- tests/test_services.py | 3 +- tests/test_working_algorithms.py | 26 +-- tests/test_working_particles.py | 15 +- tests/test_working_services.py | 3 +- 64 files changed, 755 insertions(+), 573 deletions(-) diff --git a/data/nucleon/test.toml b/data/nucleon/test.toml index 22e1cf5..4ee5949 100644 --- a/data/nucleon/test.toml +++ b/data/nucleon/test.toml @@ -21,7 +21,7 @@ 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 = "选择正确项: " } +"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:nucleon['content']", primary = "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"] # 内置的推荐学习方案 diff --git a/src/heurams/context.py b/src/heurams/context.py index 425ea5f..98a6a30 100644 --- a/src/heurams/context.py +++ b/src/heurams/context.py @@ -2,6 +2,7 @@ 全局上下文管理模块 以及基准路径 """ + from contextvars import ContextVar import pathlib from heurams.services.config import ConfigFile @@ -11,17 +12,22 @@ from heurams.services.config import ConfigFile # 数据文件路径规定: 以运行目录为准 rootdir = pathlib.Path(__file__).parent -print(f'rootdir: {rootdir}') +print(f"rootdir: {rootdir}") workdir = pathlib.Path.cwd() -print(f'workdir: {workdir}') -config_var: ContextVar[ConfigFile] = ContextVar('config_var', default=ConfigFile(rootdir / "default" / "config" / "config.toml")) +print(f"workdir: {workdir}") +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('已加载自定义用户配置') + config_var: ContextVar[ConfigFile] = ContextVar( + "config_var", default=ConfigFile(workdir / "config" / "config.toml") + ) # 配置文件 + print("已加载自定义用户配置") except: - print('未能加载自定义用户配置') + print("未能加载自定义用户配置") + +# runtime_var: ContextVar = ContextVar('runtime_var', default=dict()) # 运行时共享数据 -#runtime_var: ContextVar = ContextVar('runtime_var', default=dict()) # 运行时共享数据 class ConfigContext: """ @@ -33,14 +39,14 @@ class ConfigContext: ... get_daystamp() # 使用 test_config >>> get_daystamp() # 恢复原配置 """ - + def __init__(self, config_provider: ConfigFile): self.config_provider = config_provider self._token = None - + def __enter__(self): self._token = config_var.set(self.config_provider) return self - + def __exit__(self, exc_type, exc_val, exc_tb): - config_var.reset(self._token) # type: ignore + config_var.reset(self._token) # type: ignore diff --git a/src/heurams/interface/__main__.py b/src/heurams/interface/__main__.py index 3f53c3c..11714d4 100644 --- a/src/heurams/interface/__main__.py +++ b/src/heurams/interface/__main__.py @@ -4,17 +4,20 @@ from .screens.dashboard import DashboardScreen from .screens.nucreator import NucleonCreatorScreen from .screens.precache import PrecachingScreen from .screens.about import AboutScreen + + class HeurAMSApp(App): TITLE = "潜进" - #CSS_PATH = str(cxt.rootdir / "interface" / "css" / "main.css") + # CSS_PATH = str(cxt.rootdir / "interface" / "css" / "main.css") 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')", "版本信息"), - ] + 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, @@ -28,16 +31,19 @@ class HeurAMSApp(App): def on_button_pressed(self, event: Button.Pressed) -> None: self.exit(event.button.id) + def environment_check(): from pathlib import Path + for i in config_var.get()["paths"].values(): i = Path(i) if not i.exists(): print(f"创建 {i}") - i.mkdir(exist_ok = True, parents = True) + i.mkdir(exist_ok=True, parents=True) else: print(f"找到 {i}") + def is_subdir(parent, child): try: child.relative_to(parent) @@ -45,12 +51,14 @@ def is_subdir(parent, child): except: return 0 + # 开发模式 from heurams.context import rootdir, workdir, config_var from pathlib import Path from heurams.context import rootdir import os -if is_subdir(Path(rootdir),Path(os.getcwd())): + +if is_subdir(Path(rootdir), Path(os.getcwd())): os.chdir(Path(rootdir) / ".." / "..") print(f'转入开发数据目录: {Path(rootdir)/".."/".."}') diff --git a/src/heurams/interface/screens/about.py b/src/heurams/interface/screens/about.py index 29418cb..24a40a3 100644 --- a/src/heurams/interface/screens/about.py +++ b/src/heurams/interface/screens/about.py @@ -14,6 +14,7 @@ from textual.screen import Screen import heurams.services.version as version from heurams.context import * + class AboutScreen(Screen): def compose(self) -> ComposeResult: diff --git a/src/heurams/interface/screens/dashboard.py b/src/heurams/interface/screens/dashboard.py index b0966cc..e9955f5 100644 --- a/src/heurams/interface/screens/dashboard.py +++ b/src/heurams/interface/screens/dashboard.py @@ -21,6 +21,7 @@ from .about import AboutScreen import pathlib + class DashboardScreen(Screen): def compose(self) -> ComposeResult: @@ -31,7 +32,9 @@ class DashboardScreen(Screen): 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()} | Wang Zhiyu 2025'), + Label( + f'"潜进" 开放源代码软件项目 | 版本 {version.ver} {version.codename.capitalize()} | Wang Zhiyu 2025' + ), ) yield Footer() @@ -46,17 +49,20 @@ class DashboardScreen(Screen): res[0] = f"{filename}\0" from heurams.kernel.particles.loader import load_electron import heurams.kernel.particles as pt - electron_file_path = (pathlib.Path(config_var.get()["paths"]["electron_dir"]) / (filestem + ".json")) - if electron_file_path.exists(): # 未找到则创建电子文件 (json) + + electron_file_path = pathlib.Path(config_var.get()["paths"]["electron_dir"]) / ( + filestem + ".json" + ) + if electron_file_path.exists(): # 未找到则创建电子文件 (json) pass else: electron_file_path.touch() - with open(electron_file_path, 'w') as f: + with open(electron_file_path, "w") as f: f.write("{}") - electron_dict = load_electron(path=electron_file_path) # TODO: 取消硬编码扩展名 + electron_dict = load_electron(path=electron_file_path) # TODO: 取消硬编码扩展名 is_due = 0 is_activated = 0 - nextdate = 0x3f3f3f3f + nextdate = 0x3F3F3F3F for i in electron_dict.values(): i: pt.Electron if i.is_due(): @@ -78,12 +84,18 @@ class DashboardScreen(Screen): 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]), - )) + union_list_widget.append( + ListItem( + Label(text[0] + "\n" + text[1]), + ) + ) else: union_list_widget.append( - ListItem(Static("在 ./nucleon/ 中未找到任何内容源数据文件.\n请放置文件后重启应用.\n或者新建空的单元集.")) + ListItem( + Static( + "在 ./nucleon/ 中未找到任何内容源数据文件.\n请放置文件后重启应用.\n或者新建空的单元集." + ) + ) ) union_list_widget.disabled = True @@ -95,27 +107,36 @@ class DashboardScreen(Screen): if "未找到任何 .toml 文件" in str(selected_label.renderable): return - selected_filename = pathlib.Path(str(selected_label.renderable) - .partition('\0')[0] # 文件名末尾截断, 保留文件名 - .replace('*', "")) # 去除markdown加粗 + selected_filename = pathlib.Path( + str(selected_label.renderable) + .partition("\0")[0] # 文件名末尾截断, 保留文件名 + .replace("*", "") + ) # 去除markdown加粗 - 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") + 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) diff --git a/src/heurams/interface/screens/memorizor.py b/src/heurams/interface/screens/memorizor.py index 96a5e84..74f4ecc 100644 --- a/src/heurams/interface/screens/memorizor.py +++ b/src/heurams/interface/screens/memorizor.py @@ -12,10 +12,12 @@ import heurams.kernel.particles as pt import heurams.kernel.puzzles as pz from .. import shim + class AtomState(Enum): FAILED = auto() NORMAL = auto() + class MemScreen(Screen): BINDINGS = [ ("q", "pop_screen", "返回"), @@ -27,15 +29,21 @@ class MemScreen(Screen): 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: + + 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) - #print(self.phaser.state) - self.procession: Procession = self.phaser.current_procession() # type: ignore - #print(self.phaser.state) - #self.procession.forward(1) - + # print(self.phaser.state) + self.procession: Procession = self.phaser.current_procession() # type: ignore + # print(self.phaser.state) + # self.procession.forward(1) def on_mount(self): self.load_puzzle() @@ -45,22 +53,26 @@ class MemScreen(Screen): try: print(self.phaser.state) self.fission = Fission(self.procession.current_atom, self.phaser.state) - #print(1) + # print(1) puzzle_info = next(self.fission.generate()) print(puzzle_info) - return shim.puzzle2widget[puzzle_info["puzzle"]](atom = self.procession.current_atom, alia = puzzle_info["alia"]) + return shim.puzzle2widget[puzzle_info["puzzle"]]( + atom=self.procession.current_atom, alia=puzzle_info["alia"] + ) except (KeyError, StopIteration, AttributeError) as e: print(f"调度展开出错: {e}") return Static("无法生成谜题") - #print(shim.puzzle2widget[puzzle_info["puzzle"]]) + # print(shim.puzzle2widget[puzzle_info["puzzle"]]) def compose(self) -> ComposeResult: yield Header(show_clock=True) with Center(): - yield Static(f"当前进度: {self.procession.process()}/{self.procession.total_length()}") - #self.mount(self.current_widget()) # type: ignore + yield Static( + f"当前进度: {self.procession.process()}/{self.procession.total_length()}" + ) + # self.mount(self.current_widget()) # type: ignore yield Container(id="puzzle-container") - #yield Button("重新学习此单元", id="re-recognize", variant="warning") + # yield Button("重新学习此单元", id="re-recognize", variant="warning") yield Footer() def load_puzzle(self): diff --git a/src/heurams/interface/screens/nucreator.py b/src/heurams/interface/screens/nucreator.py index db6295c..bd772ff 100644 --- a/src/heurams/interface/screens/nucreator.py +++ b/src/heurams/interface/screens/nucreator.py @@ -14,6 +14,7 @@ from textual.screen import Screen from heurams.services.version import ver + class NucleonCreatorScreen(Screen): BINDINGS = [("q", "go_back", "返回")] @@ -23,18 +24,20 @@ 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']) + + template_dir = Path(config_var.get()["paths"]["template_dir"]) templates = list() for i in template_dir.iterdir(): - if i.name.endswith('.toml'): + if i.name.endswith(".toml"): try: import toml - with open(i, 'r') as f: + + with open(i, "r") as f: dic = toml.load(f) - desc = dic['__metadata__.attribution']['desc'] - templates.append(desc + ' (' + i.name + ')') + desc = dic["__metadata__.attribution"]["desc"] + templates.append(desc + " (" + i.name + ")") except Exception as e: - templates.append(f'无描述模板 ({i.name})') + templates.append(f"无描述模板 ({i.name})") print(e) print(templates) return templates @@ -43,10 +46,14 @@ class NucleonCreatorScreen(Screen): yield Header(show_clock=True) with Container(id="vice_container"): yield Label(f"[b]空白单元集创建向导\n") - yield Markdown("> 提示: 你可能注意到当选中文本框时底栏和操作按键绑定将被覆盖 \n只需选中(使用鼠标或 Tab)选择框即可恢复底栏功能") + yield Markdown( + "> 提示: 你可能注意到当选中文本框时底栏和操作按键绑定将被覆盖 \n只需选中(使用鼠标或 Tab)选择框即可恢复底栏功能" + ) yield Markdown("1. 键入单元集名称") yield Input(placeholder="单元集名称") - yield Markdown("> 单元集名称不应与现有单元集重复. \n> 新的单元集文件将创建在 ./nucleon/你输入的名称.toml") + yield Markdown( + "> 单元集名称不应与现有单元集重复. \n> 新的单元集文件将创建在 ./nucleon/你输入的名称.toml" + ) yield Label(f"\n") yield Markdown("2. 选择单元集模板") LINES = self.search_templates() @@ -79,5 +86,5 @@ class NucleonCreatorScreen(Screen): def on_button_pressed(self, event) -> None: event.stop() - if event.button.id == 'submit_button': + if event.button.id == "submit_button": pass diff --git a/src/heurams/interface/screens/precache.py b/src/heurams/interface/screens/precache.py index b358283..950fc3c 100644 --- a/src/heurams/interface/screens/precache.py +++ b/src/heurams/interface/screens/precache.py @@ -21,17 +21,19 @@ import heurams.services.hasher as hasher from heurams.context import * from textual.worker import Worker, get_current_worker + class PrecachingScreen(Screen): """预缓存音频文件屏幕 - + 缓存记忆单元音频文件, 全部(默认) 或部分记忆单元(可选参数传入) Args: nucleons (list): 可选列表, 仅包含 Nucleon 对象 desc (list): 可选字符串, 包含对此次调用的文字描述 """ + BINDINGS = [("q", "go_back", "返回")] - + def __init__(self, nucleons: list = [], desc: str = ""): super().__init__(name=None, id=None, classes=None) self.nucleons = nucleons @@ -47,23 +49,23 @@ class PrecachingScreen(Screen): for i in nucleons: i: pt.Nucleon i.do_eval() - #print("完成 EVAL") + # print("完成 EVAL") def compose(self) -> ComposeResult: yield Header(show_clock=True) with Container(id="precache_container"): yield Label("[b]音频预缓存[/b]", classes="title-label") - + if self.nucleons: 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") @@ -71,9 +73,9 @@ class PrecachingScreen(Screen): yield Button("取消预缓存", id="cancel_precache", variant="error") yield Button("清空缓存", id="clear_cache", variant="warning") yield Button("返回", id="go_back", variant="default") - + yield Static("若您离开此界面, 未完成的缓存进程会自动停止.") - yield Static("缓存程序支持 \"断点续传\".") + yield Static('缓存程序支持 "断点续传".') yield Footer() @@ -86,10 +88,10 @@ class PrecachingScreen(Screen): status_widget = self.query_one("#status", Static) item_widget = self.query_one("#current_item", Static) progress_bar = self.query_one("#progress_bar", ProgressBar) - + status_widget.update(f"状态: {status}") item_widget.update(f"当前项目: {current_item}" if current_item else "") - + if progress is not None: progress_bar.progress = progress progress_bar.advance(0) # 刷新显示 @@ -97,12 +99,14 @@ class PrecachingScreen(Screen): def precache_by_text(self, text: str): """预缓存单段文本的音频""" from heurams.context import rootdir, workdir, config_var + 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: # TODO: 调用模块消除tts耦合 + try: # TODO: 调用模块消除tts耦合 import edge_tts as tts + communicate = tts.Communicate(text, "zh-CN-XiaoxiaoNeural") communicate.save_sync(str(cache_file)) return 1 @@ -110,34 +114,30 @@ class PrecachingScreen(Screen): print(f"预缓存失败 '{text}': {e}") return 0 return 1 - + def precache_by_nucleon(self, nucleon: pt.Nucleon): """依据 Nucleon 缓存""" - #print(nucleon.metadata['formation']['tts_text']) - ret = self.precache_by_text(nucleon.metadata['formation']['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']}") - + # print(f"TTS 缓存: {nucleon.metadata['formation']['tts_text']}") + def precache_by_list(self, nucleons: list): """依据 Nucleons 列表缓存""" for idx, nucleon in enumerate(nucleons): - #print(f"PROC: {nucleon}") + # print(f"PROC: {nucleon}") worker = get_current_worker() - if worker and worker.is_cancelled: # 函数在worker中执行且已被取消 + if worker and worker.is_cancelled: # 函数在worker中执行且已被取消 return False - text = nucleon.metadata['formation']['tts_text'] - #self.current_item = text[:30] + "..." if len(text) > 50 else text - #print(text) + text = nucleon.metadata["formation"]["tts_text"] + # self.current_item = text[:30] + "..." if len(text) > 50 else text + # print(text) self.processed += 1 - #print(self.processed) - #print(self.total) + # print(self.processed) + # print(self.total) progress = int((self.processed / self.total) * 100) if self.total > 0 else 0 - #print(progress) - self.update_status( - f"正处理 ({idx + 1}/{len(nucleons)})", - text, - progress - ) + # print(progress) + self.update_status(f"正处理 ({idx + 1}/{len(nucleons)})", text, progress) ret = self.precache_by_nucleon(nucleon) if not ret: self.update_status( @@ -145,6 +145,7 @@ class PrecachingScreen(Screen): f"处理失败, 跳过: {self.current_item}", ) import time + time.sleep(1) if self.cancel_flag: worker.cancel() @@ -153,9 +154,9 @@ class PrecachingScreen(Screen): return True def precache_by_nucleons(self): - #print("开始缓存") + # print("开始缓存") ret = self.precache_by_list(self.nucleons) - #print(f"返回 {ret}") + # print(f"返回 {ret}") return ret def precache_by_filepath(self, path: pathlib.Path): @@ -165,13 +166,15 @@ class PrecachingScreen(Screen): lst.append(i[0]) return self.precache_by_list(lst) - def precache_all_files(self): """预缓存所有文件""" from heurams.context import rootdir, workdir, config_var + nucleon_path = pathlib.Path(config_var.get()["paths"]["nucleon_dir"]) - nucleon_files = [f for f in nucleon_path.iterdir() if f.suffix == ".toml"] # TODO: 解耦合 - + nucleon_files = [ + f for f in nucleon_path.iterdir() if f.suffix == ".toml" + ] # TODO: 解耦合 + # 计算总项目数 self.total = 0 nu = list() @@ -186,16 +189,26 @@ class PrecachingScreen(Screen): i: pt.Nucleon i.do_eval() return self.precache_by_list(nu) - + def on_button_pressed(self, event: Button.Pressed) -> None: event.stop() if event.button.id == "start_precache" and not self.is_precaching: # 开始预缓存 if self.nucleons: - self.precache_worker = self.run_worker(self.precache_by_nucleons, thread=True, exclusive=True, exit_on_error=True) + self.precache_worker = self.run_worker( + self.precache_by_nucleons, + thread=True, + exclusive=True, + exit_on_error=True, + ) else: - self.precache_worker = self.run_worker(self.precache_all_files, thread=True, exclusive=True, exit_on_error=True) - + self.precache_worker = self.run_worker( + self.precache_all_files, + thread=True, + exclusive=True, + exit_on_error=True, + ) + elif event.button.id == "cancel_precache" and self.is_precaching: # 取消预缓存 if self.precache_worker: @@ -204,20 +217,23 @@ class PrecachingScreen(Screen): self.processed = 0 self.progress = 0 self.update_status("已取消", "预缓存操作被用户取消", 0) - + elif event.button.id == "clear_cache": # 清空缓存 try: import shutil from heurams.context import rootdir, workdir, config_var - shutil.rmtree(f"{config_var.get()["paths"]["cache_dir"]}", ignore_errors=True) + + shutil.rmtree( + f"{config_var.get()["paths"]["cache_dir"]}", ignore_errors=True + ) self.update_status("已清空", "音频缓存已清空", 0) except Exception as e: self.update_status("错误", f"清空缓存失败: {e}") self.cancel_flag = 1 self.processed = 0 self.progress = 0 - + elif event.button.id == "go_back": self.action_go_back() diff --git a/src/heurams/interface/screens/preparation.py b/src/heurams/interface/screens/preparation.py index 6775216..a4dc2bc 100644 --- a/src/heurams/interface/screens/preparation.py +++ b/src/heurams/interface/screens/preparation.py @@ -15,15 +15,11 @@ import heurams.kernel.particles as pt import heurams.services.hasher as hasher from heurams.context import * -class PreparationScreen(Screen): - BINDINGS = [ - ("q", "go_back", "返回"), - ("p", "precache", "预缓存音频") - ] - def __init__( - self, nucleon_file: pathlib.Path, electron_file: pathlib.Path - ) -> None: +class PreparationScreen(Screen): + BINDINGS = [("q", "go_back", "返回"), ("p", "precache", "预缓存音频")] + + def __init__(self, nucleon_file: pathlib.Path, electron_file: pathlib.Path) -> None: super().__init__(name=None, id=None, classes=None) self.nucleon_file = nucleon_file self.electron_file = electron_file @@ -39,10 +35,10 @@ class PreparationScreen(Screen): yield Label(f"\n单元数量: {len(self.nucleons_with_orbital)}\n") yield Button( - "开始记忆", - id="start_memorizing_button", - variant="primary", - classes="start-button", + "开始记忆", + id="start_memorizing_button", + variant="primary", + classes="start-button", ) yield Button( "预缓存音频", @@ -68,6 +64,7 @@ class PreparationScreen(Screen): def action_precache(self): from ..screens.precache import PrecachingScreen + lst = list() for i in self.nucleons_with_orbital: lst.append(i[0]) @@ -97,8 +94,8 @@ class PreparationScreen(Screen): atom.link("orbital_path", None) atoms.append(atom) from .memorizor import MemScreen + memscreen = MemScreen(atoms) self.app.push_screen(memscreen) elif event.button.id == "precache_button": self.action_precache() - \ No newline at end of file diff --git a/src/heurams/interface/shim.py b/src/heurams/interface/shim.py index 651f8df..647df2a 100644 --- a/src/heurams/interface/shim.py +++ b/src/heurams/interface/shim.py @@ -1,24 +1,36 @@ """Kernel 操作先进函数库""" + import random 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 的封装 + +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) + 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) + pt.atom_registry[atom_ident].registry["electron"].revisor( + quality=quality, is_new_activation=True + ) clear() + + puzzle2widget = { pz.RecognitionPuzzle: pzw.Recognition, pz.ClozePuzzle: pzw.ClozePuzzle, pz.MCQPuzzle: pzw.MCQPuzzle, pz.BasePuzzle: pzw.BasePuzzleWidget, -} \ No newline at end of file +} diff --git a/src/heurams/interface/widgets/base_puzzle_widget.py b/src/heurams/interface/widgets/base_puzzle_widget.py index 037fa4d..e43e710 100644 --- a/src/heurams/interface/widgets/base_puzzle_widget.py +++ b/src/heurams/interface/widgets/base_puzzle_widget.py @@ -1,7 +1,24 @@ from textual.widget import Widget import heurams.kernel.particles as pt + class BasePuzzleWidget(Widget): - def __init__(self, *children: Widget, atom: pt.Atom, name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False, markup: bool = True) -> None: - super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) - self.atom = atom \ No newline at end of file + def __init__( + self, + *children: Widget, + atom: pt.Atom, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + markup: bool = True + ) -> None: + super().__init__( + *children, + name=name, + id=id, + classes=classes, + disabled=disabled, + markup=markup + ) + self.atom = atom diff --git a/src/heurams/interface/widgets/basic_puzzle.py b/src/heurams/interface/widgets/basic_puzzle.py index a190bc7..c8bcac2 100644 --- a/src/heurams/interface/widgets/basic_puzzle.py +++ b/src/heurams/interface/widgets/basic_puzzle.py @@ -9,9 +9,28 @@ import heurams.kernel.particles as pt from .base_puzzle_widget import BasePuzzleWidget from textual.message import Message + class BasicEvaluation(BasePuzzleWidget): - def __init__(self, *children: Widget, atom: pt.Atom, alia: str = "", name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False, markup: bool = True) -> None: - super().__init__(*children, atom=atom, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + def __init__( + self, + *children: Widget, + atom: pt.Atom, + alia: str = "", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + markup: bool = True, + ) -> None: + super().__init__( + *children, + atom=atom, + name=name, + id=id, + classes=classes, + disabled=disabled, + markup=markup, + ) class RatingChanged(Message): def __init__(self, rating: int) -> None: @@ -31,10 +50,10 @@ class BasicEvaluation(BasePuzzleWidget): def compose(self): # 显示主要内容 yield Label(self.atom.registry["nucleon"]["content"], id="main") - + # 显示评估说明(可选) yield Static("请评估你对这个内容的记忆程度:", classes="instruction") - + # 按钮容器 with Container(id="button_container"): btn = {} @@ -56,7 +75,7 @@ class BasicEvaluation(BasePuzzleWidget): btn["0"] = Button( "完全空白", variant="error", id="feedback_0", classes="choice" ) - + # 布局按钮 yield Horizontal(btn["5"], btn["4"]) yield Horizontal(btn["3"], btn["2"]) @@ -68,13 +87,15 @@ class BasicEvaluation(BasePuzzleWidget): if button_id in self.feedback_mapping: feedback_info = self.feedback_mapping[button_id] - - self.post_message(self.RatingChanged( - rating=feedback_info["rating"], - )) - + + self.post_message( + self.RatingChanged( + rating=feedback_info["rating"], + ) + ) + event.button.add_class("selected") - + self.disable_other_buttons(button_id) def disable_other_buttons(self, selected_button_id: str) -> None: @@ -91,6 +112,8 @@ class BasicEvaluation(BasePuzzleWidget): button_id = f"feedback_{event.key}" if button_id in self.feedback_mapping: # 模拟按钮点击 - self.post_message(self.RatingChanged( - rating=self.feedback_mapping[button_id]["rating"], - )) \ No newline at end of file + self.post_message( + self.RatingChanged( + rating=self.feedback_mapping[button_id]["rating"], + ) + ) diff --git a/src/heurams/interface/widgets/cloze_puzzle.py b/src/heurams/interface/widgets/cloze_puzzle.py index 1cca2ec..87a46ce 100644 --- a/src/heurams/interface/widgets/cloze_puzzle.py +++ b/src/heurams/interface/widgets/cloze_puzzle.py @@ -10,10 +10,29 @@ import copy import random from textual.message import Message + class ClozePuzzle(BasePuzzleWidget): - def __init__(self, *children: Widget, atom: pt.Atom, alia: str = "", name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False, markup: bool = True) -> None: - super().__init__(*children, atom=atom, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + def __init__( + self, + *children: Widget, + atom: pt.Atom, + alia: str = "", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + markup: bool = True, + ) -> None: + super().__init__( + *children, + atom=atom, + name=name, + id=id, + classes=classes, + disabled=disabled, + markup=markup, + ) self.inputlist = list() self.hashtable = {} self.alia = alia @@ -21,7 +40,11 @@ class ClozePuzzle(BasePuzzleWidget): def _work(self): cfg = self.atom.registry["orbital"]["puzzles"][self.alia] - self.puzzle = pz.ClozePuzzle(text=cfg["content"], delimiter=cfg["delimiter"], min_denominator=cfg["min_denominator"]) + self.puzzle = pz.ClozePuzzle( + text=cfg["content"], + delimiter=cfg["delimiter"], + min_denominator=cfg["min_denominator"], + ) self.puzzle.refresh() self.ans = copy.copy(self.puzzle.answer) random.shuffle(self.ans) @@ -35,6 +58,7 @@ class ClozePuzzle(BasePuzzleWidget): class InputChanged(Message): """输入变化消息""" + def __init__(self, current_input: list, max_length: int) -> None: self.current_input = current_input # 当前输入 self.max_length = max_length # 最大长度 @@ -51,16 +75,17 @@ class ClozePuzzle(BasePuzzleWidget): def update_preview(self): preview = self.query_one("#inputpreview") - preview.update(f"当前输入: {self.inputlist}") # type: ignore - - self.post_message(self.InputChanged( - current_input=self.inputlist.copy(), - max_length=len(self.puzzle.answer) - )) + preview.update(f"当前输入: {self.inputlist}") # type: ignore + + self.post_message( + self.InputChanged( + current_input=self.inputlist.copy(), max_length=len(self.puzzle.answer) + ) + ) def on_button_pressed(self, event: Button.Pressed) -> None: button_id = event.button.id - + if button_id == "delete": if len(self.inputlist) > 0: self.inputlist.pop() @@ -69,17 +94,17 @@ class ClozePuzzle(BasePuzzleWidget): answer_text = self.hashtable[button_id] self.inputlist.append(answer_text) self.update_preview() - + if len(self.inputlist) >= len(self.puzzle.answer): is_correct = self.inputlist == self.puzzle.answer rating = 4 if is_correct else 2 - self.post_message(self.RatingChanged( - atom=self.atom, - rating=rating, - is_correct=is_correct - )) - + self.post_message( + self.RatingChanged( + atom=self.atom, rating=rating, is_correct=is_correct + ) + ) + if not is_correct: self.inputlist = [] - self.update_preview() \ No newline at end of file + self.update_preview() diff --git a/src/heurams/interface/widgets/finished.py b/src/heurams/interface/widgets/finished.py index 784be46..bc52c9d 100644 --- a/src/heurams/interface/widgets/finished.py +++ b/src/heurams/interface/widgets/finished.py @@ -4,10 +4,27 @@ from textual.widgets import ( ) from textual.widget import Widget + class Finished(Widget): - def __init__(self, *children: Widget, alia = "", name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False, markup: bool = True) -> None: + def __init__( + self, + *children: Widget, + alia="", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + markup: bool = True + ) -> None: self.alia = alia - super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + super().__init__( + *children, + name=name, + id=id, + classes=classes, + disabled=disabled, + markup=markup + ) def compose(self): yield Label("本次记忆进程结束", id="finished_msg") @@ -15,5 +32,5 @@ class Finished(Widget): def on_button_pressed(self, event): button_id = event.button.id - if button_id == 'back-to-menu': + if button_id == "back-to-menu": self.app.pop_screen() diff --git a/src/heurams/interface/widgets/mcq_puzzle.py b/src/heurams/interface/widgets/mcq_puzzle.py index fc6fddd..29ec5b3 100644 --- a/src/heurams/interface/widgets/mcq_puzzle.py +++ b/src/heurams/interface/widgets/mcq_puzzle.py @@ -1,137 +1,119 @@ -from textual.app import App, ComposeResult -from textual.events import Event from textual.widgets import ( - Collapsible, - Header, - Footer, - Markdown, - ListView, - ListItem, Label, - Static, Button, ) -from textual.containers import Container, Horizontal, Center -from textual.screen import Screen from textual.widget import Widget -from typing import Tuple, Dict import heurams.kernel.particles as pt import heurams.kernel.puzzles as pz from .base_puzzle_widget import BasePuzzleWidget -import copy -import random -from textual.message import Message +from typing import TypedDict + + +class Setting(TypedDict): + __origin__: str + __hint__: str + primary: str # 显示的提示文本 + mapping: dict # 谜题到答案的映射 + jammer: list # 干扰项 + max_riddles_num: int # 最大谜题数量 + prefix: str # 提示词前缀 + class MCQPuzzle(BasePuzzleWidget): - def __init__(self, *children: Widget, atom: pt.Atom, alia: str = "", name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False, markup: bool = True) -> None: - super().__init__(*children, atom=atom, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + def __init__( + self, + *children: Widget, + atom: pt.Atom, + alia: str = "", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + markup: bool = True, + ) -> None: + super().__init__( + *children, + atom=atom, + name=name, + id=id, + classes=classes, + disabled=disabled, + markup=markup, + ) self.inputlist = [] self.alia = alia self.hashtable = {} - self._work() + self._load() - def _work(self): + def _load(self): cfg = self.atom.registry["orbital"]["puzzles"][self.alia] - self.puzzle = pz.MCQPuzzle(cfg["mapping"], cfg["jammer"], cfg["max_riddles_num"], cfg['prefix']) + self.puzzle = pz.MCQPuzzle( + cfg["mapping"], cfg["jammer"], cfg["max_riddles_num"], cfg["prefix"] + ) self.puzzle.refresh() - class PuzzleCompleted(Message): - """选择题完成消息""" - def __init__(self, atom: pt.Atom, rating: int, is_correct: bool, user_answers: list, correct_answers: list) -> None: - self.atom = atom - self.rating = rating # 评分 - self.is_correct = is_correct # 是否正确 - self.user_answers = user_answers # 用户答案 - self.correct_answers = correct_answers # 正确答案 - super().__init__() - - class InputChanged(Message): - """输入变化消息""" - def __init__(self, current_input: list, current_question: int, total_questions: int, current_question_text: str) -> None: - self.current_input = current_input # 当前输入 - self.current_question = current_question # 当前题号 - self.total_questions = total_questions # 总题数 - self.current_question_text = current_question_text # 当前问题文本 - self.progress = current_question / total_questions # 进度 - super().__init__() - - class QuestionAdvanced(Message): - """题目切换消息""" - def __init__(self, question_index: int, question_text: str, options: list) -> None: - self.question_index = question_index # 题目索引 - self.question_text = question_text - self.options = options # 选项列表 - super().__init__() - def compose(self): - yield Label(self.atom[1].content.replace("/",""), id="sentence") + setting: Setting = self.atom.registry["nucleon"].metadata["orbital"]["puzzle"][ + self.alia + ] + yield Label(setting["primary"], id="sentence") yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle") yield Label(f"当前输入: {self.inputlist}", id="inputpreview") - + # 渲染当前问题的选项 current_options = self.puzzle.options[len(self.inputlist)] for i in current_options: self.hashtable[str(hash(i))] = i yield Button(i, id=f"select{hash(i)}") - + yield Button("退格", id="delete") def update_display(self): # 更新预览标签 preview = self.query_one("#inputpreview") - preview.update(f"当前输入: {self.inputlist}") # type: ignore - + preview.update(f"当前输入: {self.inputlist}") # type: ignore + # 更新问题标签 puzzle_label = self.query_one("#puzzle") current_question_index = len(self.inputlist) if current_question_index < len(self.puzzle.wording): - puzzle_label.update(self.puzzle.wording[current_question_index]) # type: ignore - + puzzle_label.update(self.puzzle.wording[current_question_index]) # type: ignore + # 发送输入变化消息 - self.post_message(self.InputChanged( - current_input=self.inputlist.copy(), - current_question=current_question_index, - total_questions=len(self.puzzle.answer), - current_question_text=self.puzzle.wording[current_question_index] if current_question_index < len(self.puzzle.wording) else "" - )) - + # 如果还有下一题,发送题目切换消息 - if current_question_index < len(self.puzzle.options): - self.post_message(self.QuestionAdvanced( - question_index=current_question_index, - question_text=self.puzzle.wording[current_question_index], - options=self.puzzle.options[current_question_index] - )) def on_button_pressed(self, event: Button.Pressed) -> None: """处理按钮点击事件""" button_id = event.button.id - + if button_id == "delete": # 退格处理 if len(self.inputlist) > 0: self.inputlist.pop() self.refresh_buttons() self.update_display() - elif button_id.startswith("select"): # type: ignore + elif button_id.startswith("select"): # type: ignore # 选项选择处理 - answer_text = self.hashtable[button_id[6:]] # type: ignore + answer_text = self.hashtable[button_id[6:]] # type: ignore self.inputlist.append(answer_text) - + # 检查是否完成所有题目 if len(self.inputlist) >= len(self.puzzle.answer): is_correct = self.inputlist == self.puzzle.answer rating = 4 if is_correct else 2 - + # 发送完成消息 - self.post_message(self.PuzzleCompleted( - atom=self.atom, - rating=rating, - is_correct=is_correct, - user_answers=self.inputlist.copy(), - correct_answers=self.puzzle.answer.copy() - )) - + self.post_message( + self.PuzzleCompleted( + atom=self.atom, + rating=rating, + is_correct=is_correct, + user_answers=self.inputlist.copy(), + correct_answers=self.puzzle.answer.copy(), + ) + ) + # 重置输入(如果回答错误) if not is_correct: self.inputlist = [] @@ -145,10 +127,14 @@ class MCQPuzzle(BasePuzzleWidget): def refresh_buttons(self): """刷新按钮显示(用于题目切换)""" # 移除所有选项按钮 - buttons_to_remove = [child for child in self.children if hasattr(child, 'id') and child.id and child.id.startswith('select')] + buttons_to_remove = [ + child + for child in self.children + if hasattr(child, "id") and child.id and child.id.startswith("select") + ] for button in buttons_to_remove: - self.remove_child(button) # type: ignore - + self.remove_child(button) # type: ignore + # 添加当前题目的选项按钮 current_question_index = len(self.inputlist) if current_question_index < len(self.puzzle.options): diff --git a/src/heurams/interface/widgets/placeholder.py b/src/heurams/interface/widgets/placeholder.py index a47b52e..d049abf 100644 --- a/src/heurams/interface/widgets/placeholder.py +++ b/src/heurams/interface/widgets/placeholder.py @@ -6,8 +6,24 @@ from textual.widget import Widget class Placeholder(Widget): - def __init__(self, *children: Widget, name: str | None = None, alia: str = "", id: str | None = None, classes: str | None = None, disabled: bool = False, markup: bool = True) -> None: - super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + def __init__( + self, + *children: Widget, + name: str | None = None, + alia: str = "", + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + markup: bool = True + ) -> None: + super().__init__( + *children, + name=name, + id=id, + classes=classes, + disabled=disabled, + markup=markup + ) def compose(self): yield Label("示例标签", id="testlabel") diff --git a/src/heurams/interface/widgets/recognition.py b/src/heurams/interface/widgets/recognition.py index c0b6432..38a9da8 100644 --- a/src/heurams/interface/widgets/recognition.py +++ b/src/heurams/interface/widgets/recognition.py @@ -14,6 +14,7 @@ from .base_puzzle_widget import BasePuzzleWidget from typing import TypedDict, List from textual.message import Message + class RecognitionConfig(TypedDict): __origin__: str __hint__: str @@ -21,16 +22,35 @@ class RecognitionConfig(TypedDict): secondary: List[str] top_dim: List[str] + class Recognition(BasePuzzleWidget): - def __init__(self, *children: Widget, atom: pt.Atom, alia: str = "", name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False, markup: bool = True) -> None: - super().__init__(*children, atom=atom, name=name, id=id, classes=classes, disabled=disabled, markup=markup) + def __init__( + self, + *children: Widget, + atom: pt.Atom, + alia: str = "", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + markup: bool = True, + ) -> None: + super().__init__( + *children, + atom=atom, + name=name, + id=id, + classes=classes, + disabled=disabled, + markup=markup, + ) if alia == "": alia = "Recognition" self.alia = alia def compose(self): cfg: RecognitionConfig = self.atom.registry["orbital"]["puzzles"][self.alia] - delim = self.atom.registry['nucleon'].metadata["formation"]["delimiter"] + delim = self.atom.registry["nucleon"].metadata["formation"]["delimiter"] replace_dict = { ", ": ",", ". ": ".", @@ -43,8 +63,8 @@ class Recognition(BasePuzzleWidget): f":{delim}": ":", } - nucleon = self.atom.registry['nucleon'] - metadata = self.atom.registry['nucleon'].metadata + nucleon = self.atom.registry["nucleon"] + metadata = self.atom.registry["nucleon"].metadata primary = cfg["primary"] with Center(): @@ -53,7 +73,7 @@ class Recognition(BasePuzzleWidget): for old, new in replace_dict.items(): primary = primary.replace(old, new) - primary_splited = re.split(r"(?<=[,;:|])", cfg['primary']) + primary_splited = re.split(r"(?<=[,;:|])", cfg["primary"]) for item in primary_splited: with Center(): yield Label( @@ -68,7 +88,7 @@ class Recognition(BasePuzzleWidget): continue if isinstance(item, Dict): total = "" - for j, k in item.items(): # type: ignore + for j, k in item.items(): # type: ignore total += f"> **{j}**: {k} \n" yield Markdown(total) if isinstance(item, str): @@ -79,4 +99,4 @@ class Recognition(BasePuzzleWidget): def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "ok": - self.screen.rating = 5 # type: ignore \ No newline at end of file + self.screen.rating = 5 # type: ignore diff --git a/src/heurams/kernel/algorithms/__init__.py b/src/heurams/kernel/algorithms/__init__.py index 364369a..0e5a9bd 100644 --- a/src/heurams/kernel/algorithms/__init__.py +++ b/src/heurams/kernel/algorithms/__init__.py @@ -1,10 +1,10 @@ from .sm2 import SM2Algorithm __all__ = [ - 'SM2Algorithm', + "SM2Algorithm", ] algorithms = { "SM-2": SM2Algorithm, "supermemo2": SM2Algorithm, -} \ No newline at end of file +} diff --git a/src/heurams/kernel/algorithms/base.py b/src/heurams/kernel/algorithms/base.py index c6a8372..426493a 100644 --- a/src/heurams/kernel/algorithms/base.py +++ b/src/heurams/kernel/algorithms/base.py @@ -1,12 +1,13 @@ import heurams.services.timer as timer from typing import TypedDict + class BaseAlgorithm: algo_name = "BaseAlgorithm" class AlgodataDict(TypedDict): efactor: float - real_rept: int + real_rept: int rept: int interval: int last_date: int @@ -15,31 +16,33 @@ class BaseAlgorithm: last_modify: float defaults = { - 'real_rept': 0, - 'rept': 0, - 'interval': 0, - 'last_date': 0, - 'next_date': 0, - 'is_activated': 0, - 'last_modify': timer.get_timestamp() + "real_rept": 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) -> None: + def revisor( + cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False + ) -> None: """迭代记忆数据""" pass - + @classmethod def is_due(cls, algodata) -> int: """是否应该复习""" return 1 - + @classmethod def rate(cls, algodata) -> str: """获取评分信息""" return "" - + @classmethod def nextdate(cls, algodata) -> int: """获取下一次记忆时间戳""" - return -1 \ No newline at end of file + return -1 diff --git a/src/heurams/kernel/algorithms/fsrs.py b/src/heurams/kernel/algorithms/fsrs.py index 2c00adc..4335bc2 100644 --- a/src/heurams/kernel/algorithms/fsrs.py +++ b/src/heurams/kernel/algorithms/fsrs.py @@ -1 +1 @@ -# FSRS 算法模块, 尚未就绪 \ No newline at end of file +# FSRS 算法模块, 尚未就绪 diff --git a/src/heurams/kernel/algorithms/sm2.py b/src/heurams/kernel/algorithms/sm2.py index 5f2f82b..13632d6 100644 --- a/src/heurams/kernel/algorithms/sm2.py +++ b/src/heurams/kernel/algorithms/sm2.py @@ -2,12 +2,13 @@ from .base import BaseAlgorithm import heurams.services.timer as timer from typing import TypedDict + class SM2Algorithm(BaseAlgorithm): algo_name = "SM-2" class AlgodataDict(TypedDict): efactor: float - real_rept: int + real_rept: int rept: int interval: int last_date: int @@ -16,66 +17,72 @@ class SM2Algorithm(BaseAlgorithm): 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': timer.get_timestamp() + "efactor": 2.5, + "real_rept": 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): - """SM-2 算法迭代决策机制实现 - 根据 quality(0 ~ 5) 进行参数迭代最佳间隔 - quality 由主程序评估 - Args: - quality (int): 记忆保留率量化参数 + @classmethod + def revisor( + cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False + ): + """SM-2 算法迭代决策机制实现 + 根据 quality(0 ~ 5) 进行参数迭代最佳间隔 + quality 由主程序评估 + + Args: + quality (int): 记忆保留率量化参数 """ if feedback == -1: return - algodata[cls.algo_name]['efactor'] = algodata[cls.algo_name]['efactor'] + ( + algodata[cls.algo_name]["efactor"] = algodata[cls.algo_name]["efactor"] + ( 0.1 - (5 - feedback) * (0.08 + (5 - feedback) * 0.02) ) - algodata[cls.algo_name]['efactor'] = max(1.3, algodata[cls.algo_name]['efactor']) + algodata[cls.algo_name]["efactor"] = max( + 1.3, algodata[cls.algo_name]["efactor"] + ) if feedback < 3: - algodata[cls.algo_name]['rept'] = 0 - algodata[cls.algo_name]['interval'] = 0 + algodata[cls.algo_name]["rept"] = 0 + algodata[cls.algo_name]["interval"] = 0 else: - algodata[cls.algo_name]['rept'] += 1 + algodata[cls.algo_name]["rept"] += 1 - algodata[cls.algo_name]['real_rept'] += 1 + algodata[cls.algo_name]["real_rept"] += 1 if is_new_activation: - algodata[cls.algo_name]['rept'] = 0 - algodata[cls.algo_name]['efactor'] = 2.5 + algodata[cls.algo_name]["rept"] = 0 + algodata[cls.algo_name]["efactor"] = 2.5 - if algodata[cls.algo_name]['rept'] == 0: - algodata[cls.algo_name]['interval'] = 1 - elif algodata[cls.algo_name]['rept'] == 1: - algodata[cls.algo_name]['interval'] = 6 + if algodata[cls.algo_name]["rept"] == 0: + algodata[cls.algo_name]["interval"] = 1 + elif algodata[cls.algo_name]["rept"] == 1: + algodata[cls.algo_name]["interval"] = 6 else: - algodata[cls.algo_name]['interval'] = round( - algodata[cls.algo_name]['interval'] * algodata[cls.algo_name]['efactor'] + algodata[cls.algo_name]["interval"] = round( + algodata[cls.algo_name]["interval"] * algodata[cls.algo_name]["efactor"] ) - 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() - + 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() + @classmethod def is_due(cls, algodata): - return (algodata[cls.algo_name]['next_date'] <= timer.get_daystamp()) - + return algodata[cls.algo_name]["next_date"] <= timer.get_daystamp() + @classmethod def rate(cls, algodata): - return str(algodata[cls.algo_name]['efactor']) - + return str(algodata[cls.algo_name]["efactor"]) + @classmethod def nextdate(cls, algodata) -> int: - return algodata[cls.algo_name]['next_date'] \ No newline at end of file + return algodata[cls.algo_name]["next_date"] diff --git a/src/heurams/kernel/particles/__init__.py b/src/heurams/kernel/particles/__init__.py index 85748f2..719a674 100644 --- a/src/heurams/kernel/particles/__init__.py +++ b/src/heurams/kernel/particles/__init__.py @@ -21,4 +21,4 @@ __all__ = [ "load_nucleon", "load_electron", "atom_registry", -] \ No newline at end of file +] diff --git a/src/heurams/kernel/particles/atom.py b/src/heurams/kernel/particles/atom.py index 858783d..d15abc7 100644 --- a/src/heurams/kernel/particles/atom.py +++ b/src/heurams/kernel/particles/atom.py @@ -9,6 +9,7 @@ import json import bidict from heurams.context import config_var + class AtomRegister(TypedDict): nucleon: Nucleon nucleon_path: pathlib.Path @@ -21,7 +22,8 @@ class AtomRegister(TypedDict): orbital_fmt: str runtime: dict -class Atom(): + +class Atom: """ 统一处理一系列对象的所有信息与持久化: 关联电子 (算法数据) @@ -30,11 +32,11 @@ class Atom(): 以及关联路径 """ - def __init__(self, ident = ""): + def __init__(self, ident=""): self.ident = ident atom_registry[ident] = self # self.is_evaled = False - self.registry: AtomRegister = { # type: ignore + self.registry: AtomRegister = { # type: ignore "nucleon": None, "nucleon_path": None, "nucleon_fmt": "toml", @@ -42,7 +44,7 @@ class Atom(): "electron_path": None, "electron_fmt": "json", "orbital": None, - "orbital_path": None, # 允许设置为 None, 此时使用 nucleon 文件内的推荐配置 + "orbital_path": None, # 允许设置为 None, 此时使用 nucleon 文件内的推荐配置 "orbital_fmt": "toml", } self.do_eval() @@ -53,16 +55,17 @@ class Atom(): self.do_eval() else: raise ValueError("不受支持的原子元数据链接操作") - + def do_eval(self): """ 执行并以结果替换当前单元的所有 eval 语句 TODO: 带有限制的 eval, 异步/多线程执行避免堵塞 """ + # eval 环境设置 def eval_with_env(s: str): try: - nucleon = self.registry['nucleon'] + nucleon = self.registry["nucleon"] default = config_var.get()["puzzles"] metadata = nucleon.metadata except: @@ -72,7 +75,7 @@ class Atom(): except Exception as e: ret = f"此 eval 实例发生错误: {e}" return ret - + def traverse(data, modifier): if isinstance(data, dict): for key, value in data.items(): @@ -89,10 +92,9 @@ class Atom(): if data.startswith("eval:"): return modifier(data[5:]) return data - + traverse(self.registry["nucleon"], eval_with_env) traverse(self.registry["orbital"], eval_with_env) - def persist(self, key): path: pathlib.Path | None = self.registry[key + "_path"] @@ -109,7 +111,7 @@ class Atom(): raise KeyError("不受支持的持久化格式") else: raise TypeError("对未初始化的路径对象操作") - + def __getitem__(self, key): if key in self.registry: return self.registry[key] @@ -124,5 +126,6 @@ class Atom(): @staticmethod def placeholder(): return (Electron.placeholder(), Nucleon.placeholder(), {}) - + + atom_registry: bidict.bidict[str, Atom] = bidict.bidict() diff --git a/src/heurams/kernel/particles/electron.py b/src/heurams/kernel/particles/electron.py index 8bf92f8..11c4e32 100644 --- a/src/heurams/kernel/particles/electron.py +++ b/src/heurams/kernel/particles/electron.py @@ -2,12 +2,13 @@ import heurams.services.timer as timer from heurams.context import config_var from heurams.kernel.algorithms import algorithms + class Electron: """电子: 记忆分析元数据及算法""" def __init__(self, ident: str, algodata: dict = {}, algo_name: str = "supermemo2"): """初始化电子对象 (记忆数据) - + Args: ident: 算法的唯一标识符, 用于区分不同的算法实例, 使用 algodata[ident] 获取 algodata: 算法数据字典, 包含算法的各项参数和设置 @@ -28,34 +29,34 @@ class Electron: def activate(self): """激活此电子""" - self.algodata[self.algo]['is_activated'] = 1 - self.algodata[self.algo]['last_modify'] = timer.get_timestamp() + self.algodata[self.algo]["is_activated"] = 1 + self.algodata[self.algo]["last_modify"] = timer.get_timestamp() def modify(self, var: str, value): """修改 algodata[algo] 中子字典数据""" if var in self.algodata[self.algo]: self.algodata[self.algo][var] = value - self.algodata[self.algo]['last_modify'] = timer.get_timestamp() + self.algodata[self.algo]["last_modify"] = timer.get_timestamp() else: print(f"警告: '{var}' 非已知元数据字段") def is_due(self): """是否应该复习""" return self.algo.is_due(self.algodata) - + def is_activated(self): - return self.algodata[self.algo]['is_activated'] - + return self.algodata[self.algo]["is_activated"] + def rate(self): "评价" return self.algo.rate(self.algodata) - + def nextdate(self) -> int: return self.algo.nextdate(self.algodata) def revisor(self, quality: int = 5, is_new_activation: bool = False): """算法迭代决策机制实现 - + Args: quality (int): 记忆保留率量化参数 (0-5) is_new_activation (bool): 是否为初次激活 @@ -93,7 +94,7 @@ class Electron: if key == "ident": raise AttributeError("ident 应为只读") self.algodata[self.algo][key] = value - self.algodata[self.algo]['last_modify'] = timer.get_timestamp() + self.algodata[self.algo]["last_modify"] = timer.get_timestamp() def __len__(self): """仅返回当前算法的配置数量""" @@ -102,4 +103,4 @@ class Electron: @staticmethod def placeholder(): """生成一个电子占位符""" - return Electron("电子对象样例内容", {}) \ No newline at end of file + return Electron("电子对象样例内容", {}) diff --git a/src/heurams/kernel/particles/loader.py b/src/heurams/kernel/particles/loader.py index 8d0619f..24fccf1 100644 --- a/src/heurams/kernel/particles/loader.py +++ b/src/heurams/kernel/particles/loader.py @@ -6,17 +6,18 @@ import toml import json from copy import deepcopy -def load_nucleon(path: pathlib.Path, fmt = "toml"): + +def load_nucleon(path: pathlib.Path, fmt="toml"): with open(path, "r") as f: dictdata = dict() - dictdata = toml.load(f) # type: ignore + dictdata = toml.load(f) # type: ignore lst = list() nested_data = dict() # 修正 toml 解析器的不管嵌套行为 for key, value in dictdata.items(): - if "__metadata__" in key: # 以免影响句号 - if '.' in key: - parts = key.split('.') + if "__metadata__" in key: # 以免影响句号 + if "." in key: + parts = key.split(".") current = nested_data for part in parts[:-1]: if part not in current: @@ -29,23 +30,31 @@ def load_nucleon(path: pathlib.Path, fmt = "toml"): for item, attr in nested_data.items(): if item == "__metadata__": continue - lst.append((Nucleon(hasher.hash(item), attr, deepcopy(nested_data['__metadata__'])), deepcopy(nested_data["__metadata__"]["orbital"]))) + lst.append( + ( + Nucleon( + hasher.hash(item), attr, deepcopy(nested_data["__metadata__"]) + ), + deepcopy(nested_data["__metadata__"]["orbital"]), + ) + ) return lst -def load_electron(path: pathlib.Path, fmt = "json") -> dict: + +def load_electron(path: pathlib.Path, fmt="json") -> dict: """从文件路径加载电子对象 Args: path (pathlib.Path): 路径 fmt (str): 文件格式(可选, 默认 json) - + Returns: dict: 键名是电子对象名称, 值是电子对象 """ with open(path, "r") as f: dictdata = dict() - dictdata = json.load(f) # type: ignore + dictdata = json.load(f) # type: ignore dic = dict() for item, attr in dictdata.items(): - dic[item] = (Electron(hasher.hash(item), attr)) - return dic \ No newline at end of file + dic[item] = Electron(hasher.hash(item), attr) + return dic diff --git a/src/heurams/kernel/particles/nucleon.py b/src/heurams/kernel/particles/nucleon.py index ce5b18e..36d79e1 100644 --- a/src/heurams/kernel/particles/nucleon.py +++ b/src/heurams/kernel/particles/nucleon.py @@ -29,12 +29,13 @@ class Nucleon: def __hash__(self): return hash(self.ident) - + def do_eval(self): """ 执行并以结果替换当前单元的所有 eval 语句 TODO: 带有限制的 eval, 异步/多线程执行避免堵塞 """ + # eval 环境设置 def eval_with_env(s: str): try: @@ -43,7 +44,7 @@ class Nucleon: except Exception as e: ret = f"此 eval 实例发生错误: {e}" return ret - + def traverse(data, modifier): if isinstance(data, dict): for key, value in data.items(): @@ -60,9 +61,10 @@ class Nucleon: if data.startswith("eval:"): return modifier(data[5:]) return data - + traverse(self.payload, eval_with_env) traverse(self.metadata, eval_with_env) + @staticmethod def placeholder(): """生成一个占位原子核""" diff --git a/src/heurams/kernel/particles/orbital.py b/src/heurams/kernel/particles/orbital.py index 2727449..489b4f2 100644 --- a/src/heurams/kernel/particles/orbital.py +++ b/src/heurams/kernel/particles/orbital.py @@ -1,14 +1,16 @@ from typing import TypedDict + class OrbitalSchedule(TypedDict): quick_review: list recognition: list final_review: list + class Orbital(TypedDict): schedule: OrbitalSchedule puzzles: dict - + """一份示例 ["__metadata__.orbital.puzzles"] # 谜题定义 @@ -20,4 +22,4 @@ class Orbital(TypedDict): 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"]] -""" \ No newline at end of file +""" diff --git a/src/heurams/kernel/particles/probe.py b/src/heurams/kernel/particles/probe.py index 90657bc..7208650 100644 --- a/src/heurams/kernel/particles/probe.py +++ b/src/heurams/kernel/particles/probe.py @@ -1,6 +1,7 @@ from heurams.context import config_var import pathlib + def probe_by_filename(filename): """探测指定文件 (无扩展名) 的所有信息""" paths: dict = config_var.get().get("paths") @@ -8,17 +9,18 @@ def probe_by_filename(filename): result = {} for item, attr in paths.items(): for i in formats: - attr: pathlib.Path = pathlib.Path(attr) / filename + '.' + i + attr: pathlib.Path = pathlib.Path(attr) / filename + "." + i if attr.exists(): result[item.replace("_dir", "")] = str(attr) return result -def probe_all(is_stem = 1): + +def probe_all(is_stem=1): """依据目录探测所有信息 - + Args: is_stem (boolean): 是否**删除**文件扩展名 - + Returns: dict: 有三项, 每一项的键名都是文件组类型, 值都是文件组列表, 只包含文件名 """ @@ -35,7 +37,9 @@ def probe_all(is_stem = 1): result[item.replace("_dir", "")].append(str(i.name)) return result + if __name__ == "__main__": import os + print(os.getcwd()) print(probe_all()) diff --git a/src/heurams/kernel/puzzles/__init__.py b/src/heurams/kernel/puzzles/__init__.py index 5201300..0f9a4e1 100644 --- a/src/heurams/kernel/puzzles/__init__.py +++ b/src/heurams/kernel/puzzles/__init__.py @@ -10,10 +10,10 @@ from .mcq import MCQPuzzle from .recognition import RecognitionPuzzle __all__ = [ - 'BasePuzzle', - 'ClozePuzzle', - 'MCQPuzzle', - 'RecognitionPuzzle', + "BasePuzzle", + "ClozePuzzle", + "MCQPuzzle", + "RecognitionPuzzle", ] puzzles = { @@ -23,6 +23,7 @@ puzzles = { "base": BasePuzzle, } + @staticmethod def create_by_dict(config_dict: dict) -> BasePuzzle: """ @@ -37,19 +38,19 @@ def create_by_dict(config_dict: dict) -> BasePuzzle: Raises: ValueError: 当配置无效时抛出 """ - puzzle_type = config_dict.get('type') + puzzle_type = config_dict.get("type") - if puzzle_type == 'cloze': + if puzzle_type == "cloze": return puzzles["cloze"]( - text=config_dict['text'], - min_denominator=config_dict.get('min_denominator', 7) + text=config_dict["text"], + min_denominator=config_dict.get("min_denominator", 7), ) - elif puzzle_type == 'mcq': + 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', '') + 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}") \ No newline at end of file + raise ValueError(f"未知的谜题类型: {puzzle_type}") diff --git a/src/heurams/kernel/puzzles/base.py b/src/heurams/kernel/puzzles/base.py index a491fbd..f8f63e0 100644 --- a/src/heurams/kernel/puzzles/base.py +++ b/src/heurams/kernel/puzzles/base.py @@ -1,9 +1,9 @@ # base.py class BasePuzzle: """谜题基类""" - + def refresh(self): raise NotImplementedError("谜题对象未实现 refresh 方法") - + def __str__(self): - return f"谜题: {type(self).__name__}" \ No newline at end of file + return f"谜题: {type(self).__name__}" diff --git a/src/heurams/kernel/puzzles/cloze.py b/src/heurams/kernel/puzzles/cloze.py index 4574d88..9e49671 100644 --- a/src/heurams/kernel/puzzles/cloze.py +++ b/src/heurams/kernel/puzzles/cloze.py @@ -1,9 +1,10 @@ from .base import BasePuzzle import random + class ClozePuzzle(BasePuzzle): """填空题谜题生成器 - + Args: text: 原始字符串(需要 delimiter 分割句子, 末尾应有 delimiter) min_denominator: 最小概率倒数(如占所有可生成填空数的 1/7 中的 7, 若期望值小于 1, 则取 1) diff --git a/src/heurams/kernel/puzzles/mcq.py b/src/heurams/kernel/puzzles/mcq.py index d5ee158..1e1e4dc 100644 --- a/src/heurams/kernel/puzzles/mcq.py +++ b/src/heurams/kernel/puzzles/mcq.py @@ -2,15 +2,12 @@ from .base import BasePuzzle import random + class MCQPuzzle(BasePuzzle): """选择题谜题生成器""" - + def __init__( - self, - mapping: dict, - jammer: list, - max_riddles_num: int = 2, - prefix: str = "" + self, mapping: dict, jammer: list, max_riddles_num: int = 2, prefix: str = "" ): self.prefix = prefix self.mapping = mapping @@ -29,18 +26,16 @@ class MCQPuzzle(BasePuzzle): self.answer = ["无答案"] self.options = [] return - + num_questions = min(self.max_riddles_num, len(self.mapping)) questions = random.sample(list(self.mapping.items()), num_questions) puzzles = [] answers = [] all_options = [] - + for question, correct_answer in questions: options = [correct_answer] - available_jammers = [ - j for j in self.jammer if j != correct_answer - ] + available_jammers = [j for j in self.jammer if j != correct_answer] if len(available_jammers) >= 3: selected_jammers = random.sample(available_jammers, 3) else: @@ -50,14 +45,14 @@ class MCQPuzzle(BasePuzzle): puzzles.append(question) answers.append(correct_answer) all_options.append(options) - + question_texts = [] for i, puzzle in enumerate(puzzles): question_texts.append(f"{self.prefix}:\n {i+1}. {puzzle}") - + self.wording = question_texts self.answer = answers self.options = all_options def __str__(self): - return f"{self.wording}\n正确答案: {', '.join(self.answer)}" \ No newline at end of file + return f"{self.wording}\n正确答案: {', '.join(self.answer)}" diff --git a/src/heurams/kernel/puzzles/recognition.py b/src/heurams/kernel/puzzles/recognition.py index 641bb97..f53045a 100644 --- a/src/heurams/kernel/puzzles/recognition.py +++ b/src/heurams/kernel/puzzles/recognition.py @@ -2,11 +2,12 @@ from .base import BasePuzzle import random + class RecognitionPuzzle(BasePuzzle): """识别占位符""" - + def __init__(self) -> None: super().__init__() - + def refresh(self): - pass \ No newline at end of file + pass diff --git a/src/heurams/kernel/reactor/__init__.py b/src/heurams/kernel/reactor/__init__.py index 6e7b06a..6c5c8f2 100644 --- a/src/heurams/kernel/reactor/__init__.py +++ b/src/heurams/kernel/reactor/__init__.py @@ -3,10 +3,4 @@ from .procession import Procession from .fission import Fission from .phaser import Phaser -__all__ = [ - "PhaserState", - "ProcessionState", - "Procession", - "Fission", - "Phaser" -] \ No newline at end of file +__all__ = ["PhaserState", "ProcessionState", "Procession", "Fission", "Phaser"] diff --git a/src/heurams/kernel/reactor/fission.py b/src/heurams/kernel/reactor/fission.py index f7a35f1..8a8d63f 100644 --- a/src/heurams/kernel/reactor/fission.py +++ b/src/heurams/kernel/reactor/fission.py @@ -3,27 +3,34 @@ import heurams.kernel.puzzles as puz import random from .states import PhaserState -class Fission(): + +class Fission: """裂变器: 单原子调度展开器""" - def __init__(self, atom: pt.Atom, phase = PhaserState.RECOGNITION): + + def __init__(self, atom: pt.Atom, phase=PhaserState.RECOGNITION): self.atom = atom - self.orbital_schedule = atom.registry["orbital"]["schedule"][phase.value] # type: ignore - self.orbital_puzzles = atom.registry["orbital"]["puzzles"] - #print(self.orbital_schedule) + 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 + for item, possibility in self.orbital_schedule: # type: ignore if not isinstance(possibility, float): possibility = float(possibility) while possibility > 1: - self.puzzles.append({ - "puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]], - "alia": item - }) + 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 - }) + self.puzzles.append( + { + "puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]], + "alia": item, + } + ) + def generate(self): yield from self.puzzles diff --git a/src/heurams/kernel/reactor/phaser.py b/src/heurams/kernel/reactor/phaser.py index 9c153c9..ea3a60e 100644 --- a/src/heurams/kernel/reactor/phaser.py +++ b/src/heurams/kernel/reactor/phaser.py @@ -4,8 +4,10 @@ import heurams.kernel.particles as pt from .states import PhaserState, ProcessionState from .procession import Procession -class Phaser(): + +class Phaser: """移相器: 全局调度阶段管理器""" + def __init__(self, atoms: list[pt.Atom]) -> None: new_atoms = list() old_atoms = list() @@ -17,10 +19,14 @@ class Phaser(): old_atoms.append(i) self.processions = list() if len(old_atoms): - self.processions.append(Procession(old_atoms, PhaserState.QUICK_REVIEW, "初始复习")) + self.processions.append( + Procession(old_atoms, PhaserState.QUICK_REVIEW, "初始复习") + ) if len(new_atoms): - self.processions.append(Procession(new_atoms,PhaserState.RECOGNITION, "新记忆")) - self.processions.append(Procession(atoms,PhaserState.FINAL_REVIEW, "总体复习")) + self.processions.append( + Procession(new_atoms, PhaserState.RECOGNITION, "新记忆") + ) + self.processions.append(Procession(atoms, PhaserState.FINAL_REVIEW, "总体复习")) def current_procession(self): for i in self.processions: @@ -29,4 +35,4 @@ class Phaser(): self.state = i.phase return i self.state = PhaserState.FINISHED - return 0 \ No newline at end of file + return 0 diff --git a/src/heurams/kernel/reactor/procession.py b/src/heurams/kernel/reactor/procession.py index 8d94db7..f28a71d 100644 --- a/src/heurams/kernel/reactor/procession.py +++ b/src/heurams/kernel/reactor/procession.py @@ -1,8 +1,10 @@ import heurams.kernel.particles as pt from .states import PhaserState, ProcessionState -class Procession(): + +class Procession: """队列: 标识单次记忆流程""" + def __init__(self, atoms: list, phase: PhaserState, name: str = ""): self.atoms = atoms self.queue = atoms.copy() @@ -12,7 +14,7 @@ class Procession(): self.phase = phase self.state: ProcessionState = ProcessionState.RUNNING - def forward(self, step = 1): + def forward(self, step=1): self.cursor += step if self.cursor == len(self.queue): self.state = ProcessionState.FINISHED @@ -20,22 +22,22 @@ class Procession(): self.state = ProcessionState.RUNNING try: self.current_atom = self.queue[self.cursor] - return 1 # 成功 + return 1 # 成功 except IndexError as e: print(f"{e}") return 0 - def append(self, atom = None): + def append(self, atom=None): if atom == None: atom = self.current_atom if self.queue[len(self.queue) - 1] != atom or len(self) <= 1: - self.queue.append(atom) + self.queue.append(atom) def __len__(self): - return (len(self.queue) - self.cursor) + return len(self.queue) - self.cursor def process(self): - return (self.cursor) + return self.cursor def total_length(self): return len(self.queue) diff --git a/src/heurams/kernel/reactor/states.py b/src/heurams/kernel/reactor/states.py index 2644c4d..ea3cd33 100644 --- a/src/heurams/kernel/reactor/states.py +++ b/src/heurams/kernel/reactor/states.py @@ -1,5 +1,6 @@ from enum import Enum, auto + class PhaserState(Enum): UNSURE = "unsure" QUICK_REVIEW = "quick_review" @@ -7,6 +8,7 @@ class PhaserState(Enum): FINAL_REVIEW = "final_review" FINISHED = "finished" + class ProcessionState(Enum): RUNNING = auto() - FINISHED = auto() \ No newline at end of file + FINISHED = auto() diff --git a/src/heurams/providers/audio/__init__.py b/src/heurams/providers/audio/__init__.py index b707390..3ab7574 100644 --- a/src/heurams/providers/audio/__init__.py +++ b/src/heurams/providers/audio/__init__.py @@ -7,7 +7,4 @@ __all__ = [ "playsound_audio", ] -providers = { - "termux": termux_audio, - "playsound": playsound_audio -} \ No newline at end of file +providers = {"termux": termux_audio, "playsound": playsound_audio} diff --git a/src/heurams/providers/audio/playsound_audio.py b/src/heurams/providers/audio/playsound_audio.py index 011d160..43acd90 100644 --- a/src/heurams/providers/audio/playsound_audio.py +++ b/src/heurams/providers/audio/playsound_audio.py @@ -1,4 +1,4 @@ -""" 通用音频适配器 +"""通用音频适配器 基于 playsound 库的音频播放器, 在绝大多数 python 环境上提供音频服务 注意: 在未配置 pulseaudio 的 termux 不可用 """ @@ -7,5 +7,6 @@ import os import pathlib import playsound + def play_by_path(path: pathlib.Path): - playsound.playsound(str(path)) \ No newline at end of file + playsound.playsound(str(path)) diff --git a/src/heurams/providers/audio/protocol.py b/src/heurams/providers/audio/protocol.py index f012030..1528eca 100644 --- a/src/heurams/providers/audio/protocol.py +++ b/src/heurams/providers/audio/protocol.py @@ -1,5 +1,6 @@ from typing import Protocol import pathlib + class PlayFunctionProtocol(Protocol): - def __call__(self, path: pathlib.Path) -> None: ... \ No newline at end of file + def __call__(self, path: pathlib.Path) -> None: ... diff --git a/src/heurams/providers/audio/termux_audio.py b/src/heurams/providers/audio/termux_audio.py index 0919dd6..a195788 100644 --- a/src/heurams/providers/audio/termux_audio.py +++ b/src/heurams/providers/audio/termux_audio.py @@ -1,11 +1,13 @@ -""" Termux 音频适配 +"""Termux 音频适配 适配 Termux 的 play-audio 命令, 以在 android 上提供可用的播放体验 无需配置 pulseaudio """ import os import pathlib -#from .protocol import PlayFunctionProtocol + +# from .protocol import PlayFunctionProtocol + def play_by_path(path: pathlib.Path): - os.system(f"play-audio {path}") \ No newline at end of file + os.system(f"play-audio {path}") diff --git a/src/heurams/providers/llm/__init__.py b/src/heurams/providers/llm/__init__.py index 24ce2f3..de8fae1 100644 --- a/src/heurams/providers/llm/__init__.py +++ b/src/heurams/providers/llm/__init__.py @@ -1 +1 @@ -# 大语言模型 \ No newline at end of file +# 大语言模型 diff --git a/src/heurams/providers/tts/__init__.py b/src/heurams/providers/tts/__init__.py index 3d42182..3b80a12 100644 --- a/src/heurams/providers/tts/__init__.py +++ b/src/heurams/providers/tts/__init__.py @@ -8,5 +8,5 @@ __all__ = [ providers = { "basetts": BaseTTS, - "edgetts": EdgeTTS, -} \ No newline at end of file + "edgetts": EdgeTTS, +} diff --git a/src/heurams/providers/tts/base.py b/src/heurams/providers/tts/base.py index 8cf2fd4..af2cc55 100644 --- a/src/heurams/providers/tts/base.py +++ b/src/heurams/providers/tts/base.py @@ -1,9 +1,10 @@ import pathlib + class BaseTTS: name = "BaseTTS" - + @classmethod def convert(cls, text: str, path: pathlib.Path | str = "") -> pathlib.Path: """path 是可选参数, 不填则自动返回生成文件路径""" - return path # type: ignore \ No newline at end of file + return path # type: ignore diff --git a/src/heurams/providers/tts/edge_tts.py b/src/heurams/providers/tts/edge_tts.py index 4556052..afa99be 100644 --- a/src/heurams/providers/tts/edge_tts.py +++ b/src/heurams/providers/tts/edge_tts.py @@ -2,6 +2,7 @@ from .base import BaseTTS import pathlib import edge_tts + class EdgeTTS(BaseTTS): name = "EdgeTTS" @@ -12,4 +13,4 @@ class EdgeTTS(BaseTTS): "zh-CN-YunjianNeural", ) communicate.save_sync(str(path)) - return path # type: ignore \ No newline at end of file + return path # type: ignore diff --git a/src/heurams/services/audio_service.py b/src/heurams/services/audio_service.py index b6e6109..ca2c361 100644 --- a/src/heurams/services/audio_service.py +++ b/src/heurams/services/audio_service.py @@ -3,4 +3,4 @@ from heurams.context import config_var from heurams.providers.audio import providers as prov from typing import Callable -play_by_path: Callable = prov[config_var.get()["services"]["audio"]].play_by_path \ No newline at end of file +play_by_path: Callable = prov[config_var.get()["services"]["audio"]].play_by_path diff --git a/src/heurams/services/config.py b/src/heurams/services/config.py index 7a11591..cc4cf9d 100644 --- a/src/heurams/services/config.py +++ b/src/heurams/services/config.py @@ -3,6 +3,7 @@ import pathlib import toml import typing + class ConfigFile: def __init__(self, path: pathlib.Path): self.path = path @@ -13,7 +14,7 @@ class ConfigFile: def _load(self): """从文件加载配置数据""" - with open(self.path, 'r') as f: + with open(self.path, "r") as f: try: self.data = toml.load(f) except toml.TomlDecodeError as e: @@ -28,20 +29,20 @@ class ConfigFile: def save(self, path: typing.Union[str, pathlib.Path] = ""): """保存配置到文件""" save_path = pathlib.Path(path) if path else self.path - with open(save_path, 'w') as f: + with open(save_path, "w") as f: toml.dump(self.data, f) def get(self, key: str, default: typing.Any = None) -> typing.Any: """获取配置值,如果不存在返回默认值""" return self.data.get(key, default) - + def __getitem__(self, key: str) -> typing.Any: return self.data[key] - + def __setitem__(self, key: str, value: typing.Any): self.data[key] = value self.save() - + def __contains__(self, key: str) -> bool: """支持 in 语法""" - return key in self.data \ No newline at end of file + return key in self.data diff --git a/src/heurams/services/hasher.py b/src/heurams/services/hasher.py index 82229a0..53a6f53 100644 --- a/src/heurams/services/hasher.py +++ b/src/heurams/services/hasher.py @@ -1,8 +1,10 @@ # 哈希服务 import hashlib + def get_md5(text): - return hashlib.md5(text.encode('utf-8')).hexdigest() + return hashlib.md5(text.encode("utf-8")).hexdigest() + def hash(text): - return hashlib.md5(text.encode('utf-8')).hexdigest() \ No newline at end of file + return hashlib.md5(text.encode("utf-8")).hexdigest() diff --git a/src/heurams/services/timer.py b/src/heurams/services/timer.py index 1467502..63e8d8c 100644 --- a/src/heurams/services/timer.py +++ b/src/heurams/services/timer.py @@ -2,19 +2,21 @@ from heurams.context import config_var import time + def get_daystamp() -> int: """获取当前日戳(以天为单位的整数时间戳)""" time_override = config_var.get().get("daystamp_override", -1) if time_override != -1: return int(time_override) - + return int((time.time() + config_var.get().get("timezone_offset")) // (24 * 3600)) + def get_timestamp() -> float: """获取 UNIX 时间戳""" # 搞这个类的原因是要支持可复现操作 time_override = config_var.get().get("timestamp_override", -1) if time_override != -1: return float(time_override) - - return time.time() \ No newline at end of file + + return time.time() diff --git a/src/heurams/services/tts_service.py b/src/heurams/services/tts_service.py index d2dad11..afcc9c9 100644 --- a/src/heurams/services/tts_service.py +++ b/src/heurams/services/tts_service.py @@ -3,4 +3,4 @@ from heurams.context import config_var from heurams.providers.tts import TTSs from typing import Callable -convert: Callable = TTSs[config_var.get().get("tts_provider")] \ No newline at end of file +convert: Callable = TTSs[config_var.get().get("tts_provider")] diff --git a/src/heurams/services/version.py b/src/heurams/services/version.py index 285ea0c..656af63 100644 --- a/src/heurams/services/version.py +++ b/src/heurams/services/version.py @@ -2,4 +2,4 @@ ver = "0.4.0" stage = "prototype" -codename = "fledge" # 雏鸟, 0.4.x 版本 \ No newline at end of file +codename = "fledge" # 雏鸟, 0.4.x 版本 diff --git a/tests/__init__.py b/tests/__init__.py index 67708d6..7a67b1d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,4 +2,4 @@ HeurAMS Test Suite Unit tests and examples for the Heuristic Assisted Memory Scheduler system. -""" \ No newline at end of file +""" diff --git a/tests/conftest.py b/tests/conftest.py index e3e6c67..017ec62 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ """ Test configuration and fixtures for HeurAMS tests. """ + import pytest import tempfile import os @@ -10,14 +11,16 @@ from pathlib import Path @pytest.fixture def temp_config_file(): """Create a temporary config file for testing.""" - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: - f.write('''{ + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + f.write( + """{ "algorithm": "sm2", "default_ease": 2.5, "learning_steps": [1, 10], "graduating_interval": 1, "easy_interval": 4 -}''') +}""" + ) temp_path = f.name yield temp_path @@ -31,21 +34,13 @@ def temp_config_file(): def sample_atom_data(): """Sample atom data for testing.""" return { - "nucleon": { - "content": "What is the capital of France?", - "answer": "Paris" - }, - "electron": { - "ease": 2.5, - "interval": 1, - "repetitions": 0, - "last_review": None - }, + "nucleon": {"content": "What is the capital of France?", "answer": "Paris"}, + "electron": {"ease": 2.5, "interval": 1, "repetitions": 0, "last_review": None}, "orbital": { "learning_steps": [1, 10], "graduating_interval": 1, - "easy_interval": 4 - } + "easy_interval": 4, + }, } @@ -60,4 +55,4 @@ This is a test document with some {{c1::cloze}} deletions. Here's another {{c2::cloze deletion}} for testing. What is the capital of {{c3::France}}? -""" \ No newline at end of file +""" diff --git a/tests/examples.py b/tests/examples.py index e14f3fa..fe79f7d 100644 --- a/tests/examples.py +++ b/tests/examples.py @@ -4,6 +4,7 @@ Examples and usage patterns for HeurAMS modules. This file demonstrates how to use the various HeurAMS components in common scenarios and workflows. """ + import json from datetime import datetime, timezone from pathlib import Path @@ -28,12 +29,9 @@ class BasicUsageExamples: print("=== Creating Basic Atom ===") # Create the components - nucleon = Nucleon( - content="What is the capital of France?", - answer="Paris" - ) + nucleon = Nucleon(content="What is the capital of France?", answer="Paris") electron = Electron() # Uses default values - orbital = Orbital() # Uses default values + orbital = Orbital() # Uses default values # Combine into an Atom atom = Atom(nucleon=nucleon, electron=electron, orbital=orbital) @@ -55,7 +53,7 @@ class BasicUsageExamples: nucleon = Nucleon( content="The {{c1::capital}} of {{c2::France}} is {{c3::Paris}}.", - answer="capital, France, Paris" + answer="capital, France, Paris", ) electron = Electron() orbital = Orbital() @@ -126,7 +124,7 @@ class AlgorithmExamples: print("\nAfter review (quality 1 - failed):") print(f" Repetitions: {new_electron.repetitions}") # Should reset to 0 - print(f" Interval: {new_electron.interval} days") # Should reset to 1 + print(f" Interval: {new_electron.interval} days") # Should reset to 1 return new_electron @@ -145,20 +143,17 @@ class ReactorExamples: atom = Atom("test_atom") # Create nucleon with content - nucleon = Nucleon("nucleon_id", { - "content": "What is the capital of Germany?", - "answer": "Berlin" - }) + nucleon = Nucleon( + "nucleon_id", + {"content": "What is the capital of Germany?", "answer": "Berlin"}, + ) # Create electron with algorithm data electron = Electron("electron_id") # Create orbital configuration orbital = Orbital( - quick_view=[["cloze", 1]], - recognition=[], - final_review=[], - puzzle_config={} + quick_view=[["cloze", 1]], recognition=[], final_review=[], puzzle_config={} ) # Link components to atom @@ -219,4 +214,4 @@ def run_all_examples(): if __name__ == "__main__": - run_all_examples() \ No newline at end of file + run_all_examples() diff --git a/tests/run_tests.py b/tests/run_tests.py index c07915e..8f1e044 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -3,6 +3,7 @@ Test runner script for HeurAMS. This script runs all unit tests and provides a summary report. """ + import sys import pytest import os @@ -22,10 +23,10 @@ def run_tests(): # Run tests with verbose output test_args = [ - "-v", # Verbose output - "--tb=short", # Short traceback format + "-v", # Verbose output + "--tb=short", # Short traceback format "--color=yes", # Color output - "tests/" # Test directory + "tests/", # Test directory ] print(f"Running tests from: {os.path.abspath('tests')}") @@ -59,8 +60,8 @@ def run_specific_test(test_file=None, test_class=None, test_method=None): sys.path.insert(0, src_dir) test_args = [ - "-v", # Verbose output - "--tb=short", # Short traceback format + "-v", # Verbose output + "--tb=short", # Short traceback format "--color=yes", # Color output ] @@ -92,6 +93,7 @@ def run_examples(): try: from tests.examples import run_all_examples + run_all_examples() return 0 except Exception as e: @@ -103,31 +105,21 @@ if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="HeurAMS Test Runner") + parser.add_argument("--all", action="store_true", help="Run all tests (default)") parser.add_argument( - "--all", - action="store_true", - help="Run all tests (default)" - ) - parser.add_argument( - "--file", - type=str, - help="Run specific test file (e.g., test_particles.py)" + "--file", type=str, help="Run specific test file (e.g., test_particles.py)" ) parser.add_argument( "--class", dest="test_class", type=str, - help="Run specific test class (requires --file)" + help="Run specific test class (requires --file)", ) parser.add_argument( - "--method", - type=str, - help="Run specific test method (requires --class)" + "--method", type=str, help="Run specific test method (requires --class)" ) parser.add_argument( - "--examples", - action="store_true", - help="Run examples instead of tests" + "--examples", action="store_true", help="Run examples instead of tests" ) args = parser.parse_args() @@ -136,11 +128,9 @@ if __name__ == "__main__": exit_code = run_examples() elif args.file: exit_code = run_specific_test( - test_file=args.file, - test_class=args.test_class, - test_method=args.method + test_file=args.file, test_class=args.test_class, test_method=args.method ) else: exit_code = run_tests() - sys.exit(exit_code) \ No newline at end of file + sys.exit(exit_code) diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index b9c3da9..7140626 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -1,6 +1,7 @@ """ Unit tests for algorithm modules: BaseAlgorithm, SM2Algorithm """ + import pytest from datetime import datetime, timezone @@ -203,4 +204,4 @@ class TestSM2Algorithm: # Test with None orbital with pytest.raises(TypeError): - algorithm.process_review(electron, None, 3) \ No newline at end of file + algorithm.process_review(electron, None, 3) diff --git a/tests/test_particles.py b/tests/test_particles.py index 03ebf12..f5c343e 100644 --- a/tests/test_particles.py +++ b/tests/test_particles.py @@ -1,6 +1,7 @@ """ Unit tests for particle modules: Atom, Electron, Nucleon, Orbital, Probe, Loader """ + import pytest import json from pathlib import Path @@ -10,6 +11,7 @@ from src.heurams.kernel.particles.atom import Atom from src.heurams.kernel.particles.electron import Electron from src.heurams.kernel.particles.nucleon import Nucleon from src.heurams.kernel.particles.orbital import Orbital + # Probe module doesn't have a Probe class, only functions # Loader module doesn't have a Loader class, only functions @@ -32,21 +34,18 @@ class TestAtom: def test_atom_from_dict(self): """Test creating Atom from dictionary.""" data = { - "nucleon": { - "content": "What is 2+2?", - "answer": "4" - }, + "nucleon": {"content": "What is 2+2?", "answer": "4"}, "electron": { "ease": 2.5, "interval": 1, "repetitions": 0, - "last_review": None + "last_review": None, }, "orbital": { "learning_steps": [1, 10], "graduating_interval": 1, - "easy_interval": 4 - } + "easy_interval": 4, + }, } atom = Atom.from_dict(data) @@ -87,12 +86,7 @@ class TestElectron: def test_electron_custom_values(self): """Test Electron with custom values.""" test_time = datetime.now(timezone.utc) - electron = Electron( - ease=3.0, - interval=10, - repetitions=5, - last_review=test_time - ) + electron = Electron(ease=3.0, interval=10, repetitions=5, last_review=test_time) assert electron.ease == 3.0 assert electron.interval == 10 @@ -144,10 +138,7 @@ class TestNucleon: def test_nucleon_from_dict(self): """Test creating Nucleon from dictionary.""" - data = { - "content": "What is Python?", - "answer": "A programming language" - } + data = {"content": "What is Python?", "answer": "A programming language"} nucleon = Nucleon.from_dict(data) @@ -178,9 +169,7 @@ class TestOrbital: def test_orbital_custom_values(self): """Test Orbital with custom values.""" orbital = Orbital( - learning_steps=[2, 15], - graduating_interval=2, - easy_interval=6 + learning_steps=[2, 15], graduating_interval=2, easy_interval=6 ) assert orbital.learning_steps == [2, 15] @@ -189,11 +178,7 @@ class TestOrbital: def test_orbital_from_dict(self): """Test creating Orbital from dictionary.""" - data = { - "learning_steps": [3, 20], - "graduating_interval": 3, - "easy_interval": 8 - } + data = {"learning_steps": [3, 20], "graduating_interval": 3, "easy_interval": 8} orbital = Orbital.from_dict(data) @@ -215,4 +200,4 @@ class TestOrbital: # TestProbe class removed - probe module only has functions, not a class -# TestLoader class removed - loader module only has functions, not a class \ No newline at end of file +# TestLoader class removed - loader module only has functions, not a class diff --git a/tests/test_puzzles.py b/tests/test_puzzles.py index 63dd6e5..dc012ba 100644 --- a/tests/test_puzzles.py +++ b/tests/test_puzzles.py @@ -1,6 +1,7 @@ """ Unit tests for puzzle modules: BasePuzzle, ClozePuzzle, MCQPuzzle """ + import pytest import re @@ -20,4 +21,4 @@ class TestBasePuzzle: pass -# ClozePuzzle and MCQPuzzle tests skipped due to import issues \ No newline at end of file +# ClozePuzzle and MCQPuzzle tests skipped due to import issues diff --git a/tests/test_reactor.py b/tests/test_reactor.py index 22c95e1..0230af0 100644 --- a/tests/test_reactor.py +++ b/tests/test_reactor.py @@ -1,6 +1,7 @@ """ Unit tests for reactor modules: Phaser, Procession, Fission, States """ + import pytest from datetime import datetime, timezone from enum import Enum @@ -298,8 +299,7 @@ class TestFission: """Test generating learning puzzle with cloze content.""" fission = Fission() nucleon = Nucleon( - content="The capital of {{c1::France}} is Paris.", - answer="France" + content="The capital of {{c1::France}} is Paris.", answer="France" ) electron = Electron() orbital = Orbital() @@ -316,10 +316,7 @@ class TestFission: def test_fission_generate_learning_puzzle_mcq(self): """Test generating learning puzzle with MCQ content.""" fission = Fission() - nucleon = Nucleon( - content="What is the capital of France?", - answer="Paris" - ) + nucleon = Nucleon(content="What is the capital of France?", answer="Paris") electron = Electron() orbital = Orbital() atom = Atom(nucleon=nucleon, electron=electron, orbital=orbital) @@ -336,10 +333,7 @@ class TestFission: def test_fission_generate_review_puzzle(self): """Test generating review puzzle.""" fission = Fission() - nucleon = Nucleon( - content="What is the capital of France?", - answer="Paris" - ) + nucleon = Nucleon(content="What is the capital of France?", answer="Paris") electron = Electron(interval=10, repetitions=5) # In review phase orbital = Orbital() atom = Atom(nucleon=nucleon, electron=electron, orbital=orbital) @@ -411,4 +405,4 @@ class TestFission: # Test generate_review_puzzle without initialization with pytest.raises(RuntimeError): - fission.generate_review_puzzle() \ No newline at end of file + fission.generate_review_puzzle() diff --git a/tests/test_services.py b/tests/test_services.py index 21139f8..c27fca3 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -1,6 +1,7 @@ """ Unit tests for service modules: Config, Hasher, Timer, Version, AudioService, TTSService """ + import pytest import json import tempfile @@ -170,4 +171,4 @@ class TestTTSService: # Test speak with empty string with pytest.raises(ValueError): - tts_service.speak("") \ No newline at end of file + tts_service.speak("") diff --git a/tests/test_working_algorithms.py b/tests/test_working_algorithms.py index 8fde527..0dd67bd 100644 --- a/tests/test_working_algorithms.py +++ b/tests/test_working_algorithms.py @@ -1,6 +1,7 @@ """ Working unit tests for algorithm modules based on actual module structure. """ + import pytest from src.heurams.kernel.algorithms.sm2 import SM2Algorithm @@ -29,12 +30,7 @@ class TestSM2Algorithm: def test_sm2_is_due(self): """Test SM2Algorithm is_due method.""" - algodata = { - "SM-2": { - "next_date": 0, # Past date - "is_activated": 1 - } - } + algodata = {"SM-2": {"next_date": 0, "is_activated": 1}} # Past date result = SM2Algorithm.is_due(algodata) @@ -42,13 +38,7 @@ class TestSM2Algorithm: def test_sm2_rate(self): """Test SM2Algorithm rate method.""" - algodata = { - "SM-2": { - "efactor": 2.5, - "rept": 5, - "interval": 10 - } - } + algodata = {"SM-2": {"efactor": 2.5, "rept": 5, "interval": 10}} result = SM2Algorithm.rate(algodata) @@ -56,11 +46,7 @@ class TestSM2Algorithm: def test_sm2_nextdate(self): """Test SM2Algorithm nextdate method.""" - algodata = { - "SM-2": { - "next_date": 100 - } - } + algodata = {"SM-2": {"next_date": 100}} result = SM2Algorithm.nextdate(algodata) @@ -75,7 +61,7 @@ class TestSM2Algorithm: "real_rept": 0, "interval": 1, "is_activated": 1, - "last_modify": 0 + "last_modify": 0, } } @@ -85,4 +71,4 @@ class TestSM2Algorithm: # Verify that algodata was modified assert "efactor" in algodata["SM-2"] assert "rept" in algodata["SM-2"] - assert "interval" in algodata["SM-2"] \ No newline at end of file + assert "interval" in algodata["SM-2"] diff --git a/tests/test_working_particles.py b/tests/test_working_particles.py index 23c6b85..83eff03 100644 --- a/tests/test_working_particles.py +++ b/tests/test_working_particles.py @@ -1,6 +1,7 @@ """ Working unit tests for particle modules based on actual module structure. """ + import pytest from src.heurams.kernel.particles.atom import Atom @@ -14,10 +15,7 @@ class TestNucleon: def test_nucleon_creation(self): """Test basic Nucleon creation.""" - payload = { - "content": "Test content", - "answer": "Test answer" - } + payload = {"content": "Test content", "answer": "Test answer"} nucleon = Nucleon("test_id", payload) assert nucleon.ident == "test_id" @@ -123,7 +121,10 @@ class TestOrbital: quick_view=[["cloze", 1], ["mcq", 0.5]], recognition=[["recognition", 1]], final_review=[["cloze", 0.7], ["mcq", 0.7]], - puzzle_config={"cloze": {"from": "content"}, "mcq": {"from": "keyword_note"}} + puzzle_config={ + "cloze": {"from": "content"}, + "mcq": {"from": "keyword_note"}, + }, ) assert isinstance(orbital, dict) @@ -138,7 +139,7 @@ class TestOrbital: quick_view=[["cloze", 1], ["mcq", 0.5]], recognition=[], final_review=[], - puzzle_config={} + puzzle_config={}, ) assert len(orbital["quick_view"]) == 2 @@ -191,4 +192,4 @@ class TestAtom: assert isinstance(placeholder, tuple) assert len(placeholder) == 3 assert isinstance(placeholder[0], Electron) - assert isinstance(placeholder[1], Nucleon) \ No newline at end of file + assert isinstance(placeholder[1], Nucleon) diff --git a/tests/test_working_services.py b/tests/test_working_services.py index 3ac4ccc..6c44a41 100644 --- a/tests/test_working_services.py +++ b/tests/test_working_services.py @@ -1,6 +1,7 @@ """ Working unit tests for service modules based on actual module structure. """ + import pytest # Version import commented out - actual module only has variables @@ -86,4 +87,4 @@ class TestTTSService: # Test speak with empty string with pytest.raises(ValueError): - tts_service.speak("") \ No newline at end of file + tts_service.speak("")