From f689e08a1da73fae4dde376127e5783ded6b6e4a Mon Sep 17 00:00:00 2001 From: david-ajax Date: Sat, 1 Nov 2025 23:13:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heurams/interface/main.py | 22 ++++ src/heurams/interface/widgets/fileexpr.py | 25 ++++ src/heurams/kernel/algorithms/__init__.py | 9 ++ src/heurams/kernel/algorithms/sm2.py | 120 +++++++++-------- src/heurams/kernel/puzzles/__init__.py | 19 +++ src/heurams/kernel/puzzles/factory.py | 77 +++++++++++ src/heurams/kernel/puzzles/loader.py | 154 ++++++++++++++++++++++ 7 files changed, 369 insertions(+), 57 deletions(-) create mode 100644 src/heurams/interface/main.py create mode 100644 src/heurams/interface/widgets/fileexpr.py create mode 100644 src/heurams/kernel/puzzles/factory.py create mode 100644 src/heurams/kernel/puzzles/loader.py diff --git a/src/heurams/interface/main.py b/src/heurams/interface/main.py new file mode 100644 index 0000000..fe36e28 --- /dev/null +++ b/src/heurams/interface/main.py @@ -0,0 +1,22 @@ +from textual.app import App, ComposeResult +from textual.widgets import Button, Header, Label, Footer + +class HeurAMSApp(App): + #CSS_PATH = "question02.tcss" + TITLE = "潜进" + SUB_TITLE = "启发式辅助记忆调度器" + + def compose(self) -> ComposeResult: + yield Header(show_clock = True) + + yield Footer(show_command_palette = True) + + def on_mount(self) -> None: + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.exit(event.button.id) + + +if __name__ == "__main__": + app = HeurAMSApp() + app.run() \ No newline at end of file diff --git a/src/heurams/interface/widgets/fileexpr.py b/src/heurams/interface/widgets/fileexpr.py new file mode 100644 index 0000000..e8dfa03 --- /dev/null +++ b/src/heurams/interface/widgets/fileexpr.py @@ -0,0 +1,25 @@ +from textual.widget import Widget + +class FileSelector(Widget): + def __init__(self, *children: Widget, 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) + def a(self): + file_list_widget = self.query_one("#file-list", ListView) + nucleon_path = pathlib.Path("./nucleon") + nucleon_files = sorted( + [f for f in nucleon_path.iterdir() if f.suffix == ".toml"], + key=lambda f: Glimpse(pt.NucleonUnion(f)).next_date, + reverse=True + ) + + if nucleon_files: + for file in nucleon_files: + text = self.item_desc_generator(pathlib.Path(file)) + file_list_widget.append(ListItem( + Label(text[0] + '\n' + text[1]), + )) + else: + file_list_widget.append( + ListItem(Static("在 ./nucleon/ 中未找到任何内容源数据文件.\n请放置文件后重启应用.\n或者新建空的单元集.")) + ) + file_list_widget.disabled = True diff --git a/src/heurams/kernel/algorithms/__init__.py b/src/heurams/kernel/algorithms/__init__.py index e69de29..1c0b3bd 100644 --- a/src/heurams/kernel/algorithms/__init__.py +++ b/src/heurams/kernel/algorithms/__init__.py @@ -0,0 +1,9 @@ +from .sm2 import SM2Algorithm + +__all__ = [ + 'SM2Algorithm', +] + +registry = { + "SM-2": SM2Algorithm, +} \ No newline at end of file diff --git a/src/heurams/kernel/algorithms/sm2.py b/src/heurams/kernel/algorithms/sm2.py index 4553638..c5d3d72 100644 --- a/src/heurams/kernel/algorithms/sm2.py +++ b/src/heurams/kernel/algorithms/sm2.py @@ -1,66 +1,72 @@ import heurams.services.timer as timer from typing import TypedDict -algo_name = "SM-2" +class SM2Algorithm: + algo_name = "SM-2" -class AlgodataDict(TypedDict): - efactor: float - real_rept: int - rept: int - interval: int - last_date: int - next_date: int - is_activated: int - last_modify: float + class AlgodataDict(TypedDict): + efactor: float + real_rept: int + rept: int + interval: int + last_date: int + next_date: int + is_activated: int + last_modify: float -defaults = { - 'efactor': 2.5, - 'real_rept': 0, - 'rept': 0, - 'interval': 0, - 'last_date': 0, - 'next_date': 0, - 'is_activated': 0, - 'last_modify': timer.get_timestamp() -} + 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() + } + + @classmethod + def revisor(cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False): + """SM-2 算法迭代决策机制实现 + 根据 quality(0 ~ 5) 进行参数迭代最佳间隔 + quality 由主程序评估 -def revisor(algodata: dict, feedback: int = 5, is_new_activation: bool = False): - """SM-2 算法迭代决策机制实现 - 根据 quality(0 ~ 5) 进行参数迭代最佳间隔 - quality 由主程序评估 + Args: + quality (int): 记忆保留率量化参数 + """ + if feedback == -1: + return - Args: - quality (int): 记忆保留率量化参数 - """ - if feedback == -1: - return - - algodata[algo_name]['efactor'] = algodata[algo_name]['efactor'] + ( - 0.1 - (5 - feedback) * (0.08 + (5 - feedback) * 0.02) - ) - algodata[algo_name]['efactor'] = max(1.3, algodata[algo_name]['efactor']) - - if feedback < 3: - algodata[algo_name]['rept'] = 0 - algodata[algo_name]['interval'] = 0 - else: - algodata[algo_name]['rept'] += 1 - - algodata[algo_name]['real_rept'] += 1 - - if is_new_activation: - algodata[algo_name]['rept'] = 0 - algodata[algo_name]['efactor'] = 2.5 - - if algodata[algo_name]['rept'] == 0: - algodata[algo_name]['interval'] = 1 - elif algodata[algo_name]['rept'] == 1: - algodata[algo_name]['interval'] = 6 - else: - algodata[algo_name]['interval'] = round( - algodata[algo_name]['interval'] * algodata[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[algo_name]['last_date'] = timer.get_daystamp() - algodata[algo_name]['next_date'] = timer.get_daystamp() + algodata[algo_name]['interval'] - algodata[algo_name]['last_modify'] = timer.get_timestamp() \ No newline at end of file + if feedback < 3: + algodata[cls.algo_name]['rept'] = 0 + algodata[cls.algo_name]['interval'] = 0 + else: + algodata[cls.algo_name]['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 + + 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]['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()) \ No newline at end of file diff --git a/src/heurams/kernel/puzzles/__init__.py b/src/heurams/kernel/puzzles/__init__.py index e69de29..58d0a16 100644 --- a/src/heurams/kernel/puzzles/__init__.py +++ b/src/heurams/kernel/puzzles/__init__.py @@ -0,0 +1,19 @@ +""" +Puzzle 模块 - 谜题生成系统 + +提供多种类型的谜题生成器,支持从字符串、字典等数据源导入题目 +""" + +from .base import BasePuzzle +from .cloze import ClozePuzzle +from .mcq import MCQPuzzle +from .factory import PuzzleFactory +from .loader import PuzzleLoader + +__all__ = [ + 'BasePuzzle', + 'ClozePuzzle', + 'MCQPuzzle', + 'PuzzleFactory', + 'PuzzleLoader', +] \ No newline at end of file diff --git a/src/heurams/kernel/puzzles/factory.py b/src/heurams/kernel/puzzles/factory.py new file mode 100644 index 0000000..0284b19 --- /dev/null +++ b/src/heurams/kernel/puzzles/factory.py @@ -0,0 +1,77 @@ +""" +谜题工厂类 - 统一创建各种类型的谜题 +""" + +from typing import Dict, List, Union, Any +from .base import BasePuzzle +from .cloze import ClozePuzzle +from .mcq import MCQPuzzle + +class PuzzleFactory: + """谜题工厂,根据配置创建不同类型的谜题""" + + @staticmethod + def create_cloze(text: str, min_denominator: int = 7) -> ClozePuzzle: + """ + 创建填空题谜题 + + Args: + text: 原始字符串,使用 "/" 分割句子 + min_denominator: 最小概率倒数,控制填空数量 + + Returns: + ClozePuzzle: 填空题实例 + """ + return ClozePuzzle(text, min_denominator) + + @staticmethod + def create_mcq( + mapping: Dict[str, str], + jammer: List[str], + max_riddles_num: int = 2, + prefix: str = "" + ) -> MCQPuzzle: + """ + 创建选择题谜题 + + Args: + mapping: 题目-答案映射字典 + jammer: 干扰项列表 + max_riddles_num: 最大题目数量 + prefix: 题目前缀 + + Returns: + MCQPuzzle: 选择题实例 + """ + return MCQPuzzle(mapping, jammer, max_riddles_num, prefix) + + @staticmethod + def from_config(config: Dict[str, Any]) -> BasePuzzle: + """ + 根据配置字典创建谜题 + + Args: + config: 配置字典,包含谜题类型和参数 + + Returns: + BasePuzzle: 谜题实例 + + Raises: + ValueError: 当配置无效时抛出 + """ + puzzle_type = config.get('type') + + if puzzle_type == 'cloze': + return PuzzleFactory.create_cloze( + text=config['text'], + min_denominator=config.get('min_denominator', 7) + ) + elif puzzle_type == 'mcq': + return PuzzleFactory.create_mcq( + mapping=config['mapping'], + jammer=config.get('jammer', []), + max_riddles_num=config.get('max_riddles_num', 2), + prefix=config.get('prefix', '') + ) + else: + raise ValueError(f"未知的谜题类型: {puzzle_type}") \ No newline at end of file diff --git a/src/heurams/kernel/puzzles/loader.py b/src/heurams/kernel/puzzles/loader.py new file mode 100644 index 0000000..b1beec7 --- /dev/null +++ b/src/heurams/kernel/puzzles/loader.py @@ -0,0 +1,154 @@ +""" +谜题加载器 - 从多种数据源加载谜题配置 +""" + +import json +import yaml +import re +from typing import Dict, List, Union, Any +from pathlib import Path +from .factory import PuzzleFactory + + +class PuzzleLoader: + """谜题加载器,支持从多种格式加载谜题配置""" + + @staticmethod + def from_json(file_path: Union[str, Path]) -> Dict[str, Any]: + """ + 从JSON文件加载谜题配置 + + Args: + file_path: JSON文件路径 + + Returns: + Dict: 谜题配置字典 + """ + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + + @staticmethod + def from_yaml(file_path: Union[str, Path]) -> Dict[str, Any]: + """ + 从YAML文件加载谜题配置 + + Args: + file_path: YAML文件路径 + + Returns: + Dict: 谜题配置字典 + """ + with open(file_path, 'r', encoding='utf-8') as f: + return yaml.safe_load(f) + + @staticmethod + def from_text(text: str, puzzle_type: str = 'cloze', **kwargs) -> Dict[str, Any]: + """ + 从纯文本字符串创建谜题配置 + + Args: + text: 原始文本 + puzzle_type: 谜题类型 ('cloze' 或 'mcq') + **kwargs: 其他参数 + + Returns: + Dict: 谜题配置字典 + """ + config = {'type': puzzle_type} + + if puzzle_type == 'cloze': + config['text'] = text + config.update(kwargs) + elif puzzle_type == 'mcq': + # 从文本解析选择题配置 + config.update(PuzzleLoader._parse_mcq_from_text(text, **kwargs)) + else: + raise ValueError(f"不支持的谜题类型: {puzzle_type}") + + return config + + @staticmethod + def _parse_mcq_from_text(text: str, **kwargs) -> Dict[str, Any]: + """ + 从文本解析选择题配置 + + Args: + text: 原始文本 + **kwargs: 其他参数 + + Returns: + Dict: 选择题配置 + """ + # 简单的解析逻辑:每行一个题目,格式为 "题目:答案" + mapping = {} + lines = text.strip().split('\n') + + for line in lines: + line = line.strip() + if ':' in line: + question, answer = line.split(':', 1) + mapping[question.strip()] = answer.strip() + + return { + 'mapping': mapping, + 'jammer': kwargs.get('jammer', []), + 'max_riddles_num': kwargs.get('max_riddles_num', 2), + 'prefix': kwargs.get('prefix', '') + } + + @staticmethod + def from_csv(file_path: Union[str, Path], **kwargs) -> Dict[str, Any]: + """ + 从CSV文件加载选择题配置 + + Args: + file_path: CSV文件路径 + **kwargs: 其他参数 + + Returns: + Dict: 选择题配置 + """ + import csv + mapping = {} + + with open(file_path, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + for row in reader: + if len(row) >= 2: + mapping[row[0].strip()] = row[1].strip() + + return { + 'type': 'mcq', + 'mapping': mapping, + 'jammer': kwargs.get('jammer', []), + 'max_riddles_num': kwargs.get('max_riddles_num', 2), + 'prefix': kwargs.get('prefix', '') + } + + @staticmethod + def load_puzzles_from_directory(directory: Union[str, Path]) -> List[Dict[str, Any]]: + """ + 从目录批量加载谜题配置 + + Args: + directory: 目录路径 + + Returns: + List[Dict]: 谜题配置列表 + """ + puzzles = [] + dir_path = Path(directory) + + # 加载JSON文件 + for json_file in dir_path.glob('*.json'): + puzzles.append(PuzzleLoader.from_json(json_file)) + + # 加载YAML文件 + for yaml_file in dir_path.glob('*.yaml'): + puzzles.append(PuzzleLoader.from_yaml(yaml_file)) + + # 加载CSV文件 + for csv_file in dir_path.glob('*.csv'): + puzzles.append(PuzzleLoader.from_csv(csv_file)) + + return puzzles \ No newline at end of file