refactor: 完成 0.4.0 版本更新
完成 0.4.0 版本更新, 为了消除此前提交消息风格不一致与错误提交超大文件的问题, 维持代码统计数据的准确性和提交消息风格的一致性, 重新初始化仓库; 旧的提交历史在 HeurAMS-legacy 仓库(https://gitea.imwangzhiyu.xyz/ajax/HeurAMS-legacy)
This commit is contained in:
7
src/heurams/__init__.py
Normal file
7
src/heurams/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
print("欢迎使用 HeurAMS 及其组件!")
|
||||
|
||||
# 补充日志记录
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger.info("欢迎使用 HeurAMS 及其组件!")
|
||||
7
src/heurams/__main__.py
Normal file
7
src/heurams/__main__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
prompt = """HeurAMS 已经被成功地安装在系统中.
|
||||
但 HeurAMS 被设计为一个带有辅助记忆调度器功能的软件包, 无法直接被执行, 但可被其他 Python 程序调用.
|
||||
若您想启动内置的基本用户界面,
|
||||
请运行 python -m heurams.interface,
|
||||
或者 python -m heurams.interface.__main__
|
||||
注意: 一个常见的误区是, 执行 interface 下的 __main__.py 运行基本用户界面, 这会导致 Python 上下文环境异常, 请不要这样做."""
|
||||
print(prompt)
|
||||
58
src/heurams/context.py
Normal file
58
src/heurams/context.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
全局上下文管理模块
|
||||
以及基准路径
|
||||
"""
|
||||
|
||||
from contextvars import ContextVar
|
||||
import pathlib
|
||||
from heurams.services.config import ConfigFile
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
# 默认配置文件路径规定: 以包目录为准
|
||||
# 用户配置文件路径规定: 以运行目录为准
|
||||
# 数据文件路径规定: 以运行目录为准
|
||||
|
||||
rootdir = pathlib.Path(__file__).parent
|
||||
print(f"rootdir: {rootdir}")
|
||||
logger = get_logger(__name__)
|
||||
logger.debug(f"项目根目录: {rootdir}")
|
||||
workdir = pathlib.Path.cwd()
|
||||
print(f"workdir: {workdir}")
|
||||
logger.debug(f"工作目录: {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("已加载自定义用户配置")
|
||||
logger.info("已加载自定义用户配置, 路径: %s", workdir / "config" / "config.toml")
|
||||
except Exception as e:
|
||||
print("未能加载自定义用户配置")
|
||||
logger.warning("未能加载自定义用户配置, 错误: %s", e)
|
||||
|
||||
# runtime_var: ContextVar = ContextVar('runtime_var', default=dict()) # 运行时共享数据
|
||||
|
||||
|
||||
class ConfigContext:
|
||||
"""
|
||||
功能完备的上下文管理器
|
||||
用于临时切换配置的作用域, 支持嵌套使用
|
||||
|
||||
Example:
|
||||
>>> with ConfigContext(test_config):
|
||||
... 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
|
||||
29
src/heurams/default/config/config.toml
Normal file
29
src/heurams/default/config/config.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
# [调试] 将更改保存到文件
|
||||
persist_to_file = 1
|
||||
|
||||
# [调试] 覆写时间, 设为 -1 以禁用
|
||||
daystamp_override = -1
|
||||
timestamp_override = -1
|
||||
|
||||
# [调试] 一键通过
|
||||
quick_pass = 0
|
||||
|
||||
# 对于每个项目的默认新记忆原子数量
|
||||
tasked_number = 8
|
||||
|
||||
# UTC 时间戳修正 仅用于 UNIX 日时间戳的生成修正, 单位为秒
|
||||
timezone_offset = +28800 # 中国标准时间 (UTC+8)
|
||||
|
||||
[puzzles] # 谜题默认配置
|
||||
|
||||
[puzzles.mcq]
|
||||
max_riddles_num = 2
|
||||
|
||||
[puzzles.cloze]
|
||||
min_denominator = 3
|
||||
|
||||
[paths] # 相对于工作目录而言 或绝对路径
|
||||
nucleon_dir = "./data/nucleon"
|
||||
electron_dir = "./data/electron"
|
||||
orbital_dir = "./data/orbital"
|
||||
cache_dir = "./data/cache"
|
||||
2
src/heurams/interface/README.md
Normal file
2
src/heurams/interface/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Interface - 用户界面
|
||||
与界面系统**强绑定**的相关代码文件, "界面系统" 在此处是基本界面实现相关的 Textual 框架
|
||||
87
src/heurams/interface/__main__.py
Normal file
87
src/heurams/interface/__main__.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from textual.app import App
|
||||
from textual.widgets import Button
|
||||
from .screens.dashboard import DashboardScreen
|
||||
from .screens.nucreator import NucleonCreatorScreen
|
||||
from .screens.precache import PrecachingScreen
|
||||
from .screens.about import AboutScreen
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HeurAMSApp(App):
|
||||
TITLE = "潜进"
|
||||
CSS_PATH = "css/main.tcss"
|
||||
SUB_TITLE = "启发式辅助记忆调度器"
|
||||
BINDINGS = [
|
||||
("q", "quit", "退出"),
|
||||
("d", "toggle_dark", "切换色调"),
|
||||
("1", "app.push_screen('dashboard')", "仪表盘"),
|
||||
("2", "app.push_screen('precache_all')", "缓存管理器"),
|
||||
("3", "app.push_screen('nucleon_creator')", "创建新单元"),
|
||||
("0", "app.push_screen('about')", "版本信息"),
|
||||
]
|
||||
SCREENS = {
|
||||
"dashboard": DashboardScreen,
|
||||
"nucleon_creator": NucleonCreatorScreen,
|
||||
"precache_all": PrecachingScreen,
|
||||
"about": AboutScreen,
|
||||
}
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.push_screen("dashboard")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.exit(event.button.id)
|
||||
|
||||
def action_do_nothing(self):
|
||||
print("DO NOTHING")
|
||||
self.refresh()
|
||||
|
||||
|
||||
def environment_check():
|
||||
from pathlib import Path
|
||||
|
||||
logger.debug("检查环境路径")
|
||||
|
||||
for i in config_var.get()["paths"].values():
|
||||
i = Path(i)
|
||||
if not i.exists():
|
||||
logger.info("创建目录: %s", i)
|
||||
print(f"创建 {i}")
|
||||
i.mkdir(exist_ok=True, parents=True)
|
||||
else:
|
||||
logger.debug("目录已存在: %s", i)
|
||||
print(f"找到 {i}")
|
||||
logger.debug("环境检查完成")
|
||||
|
||||
|
||||
def is_subdir(parent, child):
|
||||
try:
|
||||
child.relative_to(parent)
|
||||
logger.debug("is_subdir: %s 是 %s 的子目录", child, parent)
|
||||
return 1
|
||||
except:
|
||||
logger.debug("is_subdir: %s 不是 %s 的子目录", child, parent)
|
||||
return 0
|
||||
|
||||
|
||||
# 开发模式
|
||||
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())):
|
||||
os.chdir(Path(rootdir) / ".." / "..")
|
||||
print(f'转入开发数据目录: {Path(rootdir)/".."/".."}')
|
||||
|
||||
environment_check()
|
||||
|
||||
app = HeurAMSApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
|
||||
def main():
|
||||
app.run()
|
||||
0
src/heurams/interface/css/main.tcss
Normal file
0
src/heurams/interface/css/main.tcss
Normal file
94
src/heurams/interface/screens/about.py
Normal file
94
src/heurams/interface/screens/about.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Static,
|
||||
Button,
|
||||
Markdown,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
|
||||
import heurams.services.version as version
|
||||
from heurams.context import *
|
||||
|
||||
|
||||
class AboutScreen(Screen):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="about_container"):
|
||||
yield Label("[b]关于与版本信息[/b]")
|
||||
about_text = f"""
|
||||
# 关于 "潜进"
|
||||
|
||||
版本 {version.ver} {version.stage.capitalize()}
|
||||
|
||||
开发代号: {version.codename.capitalize()}
|
||||
|
||||
一个基于启发式算法的开放源代码记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.
|
||||
|
||||
以 AGPL-3.0 开放源代码
|
||||
|
||||
开发人员:
|
||||
|
||||
- Wang Zhiyu([@pluvium27](https://github.com/pluvium27)): 项目作者
|
||||
|
||||
特别感谢:
|
||||
|
||||
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SuperMemo-2 算法
|
||||
- [Thoughts Memo](https://www.zhihu.com/people/L.M.Sherlock): 文献参考
|
||||
|
||||
# 参与贡献
|
||||
|
||||
我们是一个年轻且包容的社区, 由技术人员, 设计师, 文书工作者, 以及创意人员共同构成,
|
||||
|
||||
通过我们协力开发的软件为所有人谋取福祉.
|
||||
|
||||
上述工作不可避免地让我们确立了下列价值观 (取自 KDE 宣言):
|
||||
|
||||
- 开放治理 确保更多人能参与我们的领导和决策进程;
|
||||
|
||||
- 自由软件 确保我们的工作成果随时能为所有人所用;
|
||||
|
||||
- 多样包容 确保所有人都能加入社区并参加工作;
|
||||
|
||||
- 创新精神 确保新思路能不断涌现并服务于所有人;
|
||||
|
||||
- 共同产权 确保我们能团结一致;
|
||||
|
||||
- 迎合用户 确保我们的成果对所有人有用.
|
||||
|
||||
综上所述, 在为我们共同目标奋斗的过程中, 我们认为上述价值观反映了我们社区的本质, 是我们始终如一地保持初心的关键所在.
|
||||
|
||||
这是一项立足于协作精神的事业, 它的运作和产出不受任何单一个人或者机构的操纵.
|
||||
|
||||
我们的共同目标是为人人带来高品质的辅助记忆 & 学习软件.
|
||||
|
||||
不管您来自何方, 我们都欢迎您加入社区并做出贡献.
|
||||
"""
|
||||
|
||||
# """
|
||||
# 学术数据
|
||||
|
||||
# "潜进" 的用户数据可用于科学方面的研究, 我们将在未来版本添加学术数据的收集和展示平台
|
||||
# """
|
||||
yield Markdown(about_text, classes="about-markdown")
|
||||
|
||||
yield Button(
|
||||
"返回主界面",
|
||||
id="back_button",
|
||||
variant="primary",
|
||||
classes="back-button",
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
event.stop()
|
||||
if event.button.id == "back_button":
|
||||
self.action_go_back()
|
||||
153
src/heurams/interface/screens/dashboard.py
Normal file
153
src/heurams/interface/screens/dashboard.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
ListView,
|
||||
ListItem,
|
||||
Button,
|
||||
Static,
|
||||
)
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
|
||||
from heurams.kernel.particles import *
|
||||
from heurams.context import *
|
||||
import heurams.services.version as version
|
||||
import heurams.services.timer as timer
|
||||
from .preparation import PreparationScreen
|
||||
from .about import AboutScreen
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
import pathlib
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DashboardScreen(Screen):
|
||||
SUB_TITLE = "仪表盘"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
yield ScrollableContainer(
|
||||
Label(f'欢迎使用 "潜进" 启发式辅助记忆调度器', classes="title-label"),
|
||||
Label(f"当前 UNIX 日时间戳: {timer.get_daystamp()}"),
|
||||
Label(f'时区修正: UTC+{config_var.get()["timezone_offset"] / 3600}'),
|
||||
Label("选择待学习或待修改的记忆单元集:", classes="title-label"),
|
||||
ListView(id="union-list", classes="union-list-view"),
|
||||
Label(
|
||||
f'"潜进" 启发式辅助记忆调度器 | 版本 {version.ver} {version.codename.capitalize()} 2025'
|
||||
),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def item_desc_generator(self, filename) -> dict:
|
||||
"""简单分析以生成项目项显示文本
|
||||
|
||||
Returns:
|
||||
dict: 以数字为列表, 分别呈现单行字符串
|
||||
"""
|
||||
res = dict()
|
||||
filestem = pathlib.Path(filename).stem
|
||||
res[0] = f"{filename}\0"
|
||||
from heurams.kernel.particles.loader import load_electron
|
||||
import heurams.kernel.particles as pt
|
||||
|
||||
electron_file_path = pathlib.Path(config_var.get()["paths"]["electron_dir"]) / (
|
||||
filestem + ".json"
|
||||
)
|
||||
|
||||
logger.debug(f"电子文件路径: {electron_file_path}")
|
||||
|
||||
if electron_file_path.exists(): # 未找到则创建电子文件 (json)
|
||||
pass
|
||||
else:
|
||||
electron_file_path.touch()
|
||||
with open(electron_file_path, "w") as f:
|
||||
f.write("{}")
|
||||
electron_dict = load_electron(path=electron_file_path) # TODO: 取消硬编码扩展名
|
||||
logger.debug(electron_dict)
|
||||
is_due = 0
|
||||
is_activated = 0
|
||||
nextdate = 0x3F3F3F3F
|
||||
for i in electron_dict.values():
|
||||
i: pt.Electron
|
||||
logger.debug(i, i.is_due())
|
||||
if i.is_due():
|
||||
is_due = 1
|
||||
if i.is_activated():
|
||||
is_activated = 1
|
||||
nextdate = min(nextdate, i.nextdate())
|
||||
res[1] = f"下一次复习: {nextdate}\n"
|
||||
res[1] += f"{is_due if "需要复习" else "当前无需复习"}"
|
||||
if not is_activated:
|
||||
res[1] = " 尚未激活"
|
||||
return res
|
||||
|
||||
def on_mount(self) -> None:
|
||||
union_list_widget = self.query_one("#union-list", ListView)
|
||||
|
||||
probe = probe_all(0)
|
||||
|
||||
if len(probe["nucleon"]):
|
||||
for file in probe["nucleon"]:
|
||||
text = self.item_desc_generator(file)
|
||||
union_list_widget.append(
|
||||
ListItem(
|
||||
Label(text[0] + "\n" + text[1]),
|
||||
)
|
||||
)
|
||||
else:
|
||||
union_list_widget.append(
|
||||
ListItem(
|
||||
Static(
|
||||
"在 ./nucleon/ 中未找到任何内容源数据文件.\n请放置文件后重启应用.\n或者新建空的单元集."
|
||||
)
|
||||
)
|
||||
)
|
||||
union_list_widget.disabled = True
|
||||
|
||||
def on_list_view_selected(self, event) -> None:
|
||||
if not isinstance(event.item, ListItem):
|
||||
return
|
||||
|
||||
selected_label = event.item.query_one(Label)
|
||||
if "未找到任何 .toml 文件" in str(selected_label.renderable): # type: ignore
|
||||
return
|
||||
|
||||
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"
|
||||
)
|
||||
self.app.push_screen(PreparationScreen(nucleon_file_path, electron_file_path))
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
if event.button.id == "new_nucleon_button":
|
||||
# 切换到创建单元
|
||||
from .nucreator import NucleonCreatorScreen
|
||||
|
||||
newscr = NucleonCreatorScreen()
|
||||
self.app.push_screen(newscr)
|
||||
elif event.button.id == "precache_all_button":
|
||||
# 切换到缓存管理器
|
||||
from .precache import PrecachingScreen
|
||||
|
||||
precache_screen = PrecachingScreen()
|
||||
self.app.push_screen(precache_screen)
|
||||
elif event.button.id == "about_button":
|
||||
from .about import AboutScreen
|
||||
|
||||
about_screen = AboutScreen()
|
||||
self.app.push_screen(about_screen)
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
self.app.exit()
|
||||
0
src/heurams/interface/screens/intelinote.py
Normal file
0
src/heurams/interface/screens/intelinote.py
Normal file
152
src/heurams/interface/screens/memorizor.py
Normal file
152
src/heurams/interface/screens/memorizor.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Header, Footer, Label, Static, Button
|
||||
from textual.containers import Center, ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.reactive import reactive
|
||||
from enum import Enum, auto
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.reactor import *
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from .. import shim
|
||||
|
||||
|
||||
class AtomState(Enum):
|
||||
FAILED = auto()
|
||||
NORMAL = auto()
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MemScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "pop_screen", "返回"),
|
||||
# ("p", "prev", "复习上一个"),
|
||||
("d", "toggle_dark", ""),
|
||||
("v", "play_voice", "朗读"),
|
||||
("0,1,2,3", "app.push_screen('about')", ""),
|
||||
]
|
||||
|
||||
if config_var.get()["quick_pass"]:
|
||||
BINDINGS.append(("k", "quick_pass", "跳过"))
|
||||
rating = reactive(-1)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
atoms: list,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name, id, classes)
|
||||
self.atoms = atoms
|
||||
self.phaser = Phaser(atoms)
|
||||
# logger.debug(self.phaser.state)
|
||||
self.procession: Procession = self.phaser.current_procession() # type: ignore
|
||||
self.atom: pt.Atom = self.procession.current_atom
|
||||
# logger.debug(self.phaser.state)
|
||||
# self.procession.forward(1)
|
||||
for i in atoms:
|
||||
i.do_eval()
|
||||
|
||||
def on_mount(self):
|
||||
self.load_puzzle()
|
||||
pass
|
||||
|
||||
def puzzle_widget(self):
|
||||
try:
|
||||
logger.debug(self.phaser.state)
|
||||
logger.debug(self.procession.cursor)
|
||||
logger.debug(self.atom)
|
||||
self.fission = Fission(self.atom, self.phaser.state)
|
||||
puzzle_debug = next(self.fission.generate())
|
||||
# logger.debug(puzzle_debug)
|
||||
return shim.puzzle2widget[puzzle_debug["puzzle"]](
|
||||
atom=self.atom, alia=puzzle_debug["alia"]
|
||||
)
|
||||
except (KeyError, StopIteration, AttributeError) as e:
|
||||
logger.debug(f"调度展开出错: {e}")
|
||||
return Static("无法生成谜题")
|
||||
# logger.debug(shim.puzzle2widget[puzzle_debug["puzzle"]])
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer():
|
||||
yield Label(self._get_progress_text(), id="progress")
|
||||
|
||||
# self.mount(self.current_widget()) # type: ignore
|
||||
yield ScrollableContainer(id="puzzle-container")
|
||||
# yield Button("重新学习此单元", id="re-recognize", variant="warning")
|
||||
yield Footer()
|
||||
|
||||
def _get_progress_text(self):
|
||||
return f"当前进度: {self.procession.process() + 1}/{self.procession.total_length()}"
|
||||
|
||||
def update_display(self):
|
||||
progress_widget = self.query_one("#progress")
|
||||
progress_widget.update(self._get_progress_text()) # type: ignore
|
||||
|
||||
def load_puzzle(self):
|
||||
self.atom: pt.Atom = self.procession.current_atom
|
||||
container = self.query_one("#puzzle-container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
container.mount(self.puzzle_widget())
|
||||
|
||||
def load_finished_widget(self):
|
||||
container = self.query_one("#puzzle-container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
from heurams.interface.widgets.finished import Finished
|
||||
|
||||
container.mount(Finished())
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
event.stop()
|
||||
|
||||
def watch_rating(self, old_rating, new_rating) -> None:
|
||||
if self.procession == 0:
|
||||
return
|
||||
if new_rating == -1:
|
||||
return
|
||||
forwards = 1 if new_rating >= 4 else 0
|
||||
self.rating = -1
|
||||
logger.debug(f"试图前进: {"允许" if forwards else "禁止"}")
|
||||
if forwards:
|
||||
ret = self.procession.forward(1)
|
||||
if ret == 0: # 若结束了此次队列
|
||||
self.procession = self.phaser.current_procession() # type: ignore
|
||||
if self.procession == 0: # 若所有队列都结束了
|
||||
logger.debug(f"记忆进程结束")
|
||||
for i in self.atoms:
|
||||
i: pt.Atom
|
||||
i.revise()
|
||||
i.persist("electron")
|
||||
self.load_finished_widget()
|
||||
return
|
||||
else:
|
||||
logger.debug(f"建立新队列 {self.procession.phase}")
|
||||
self.load_puzzle()
|
||||
else: # 若不通过
|
||||
self.procession.append()
|
||||
self.update_display()
|
||||
|
||||
def action_quick_pass(self):
|
||||
self.rating = 5
|
||||
self.atom.minimize(5)
|
||||
self.atom.registry["electron"].activate()
|
||||
self.atom.lock(1)
|
||||
|
||||
def action_play_voice(self):
|
||||
"""朗读当前内容"""
|
||||
pass
|
||||
|
||||
def action_toggle_dark(self):
|
||||
self.app.action_toggle_dark()
|
||||
|
||||
def action_pop_screen(self):
|
||||
self.app.pop_screen()
|
||||
171
src/heurams/interface/screens/nucreator.py
Normal file
171
src/heurams/interface/screens/nucreator.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Markdown,
|
||||
)
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
|
||||
from heurams.services.version import ver
|
||||
import toml
|
||||
from pathlib import Path
|
||||
from heurams.context import config_var
|
||||
|
||||
|
||||
class NucleonCreatorScreen(Screen):
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
SUB_TITLE = "单元集创建向导"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
|
||||
def search_templates(self):
|
||||
from pathlib import Path
|
||||
from heurams.context import config_var
|
||||
|
||||
template_dir = Path(config_var.get()["paths"]["template_dir"])
|
||||
templates = list()
|
||||
for i in template_dir.iterdir():
|
||||
if i.name.endswith(".toml"):
|
||||
try:
|
||||
import toml
|
||||
|
||||
with open(i, "r") as f:
|
||||
dic = toml.load(f)
|
||||
desc = dic["__metadata__.attribution"]["desc"]
|
||||
templates.append(desc + " (" + i.name + ")")
|
||||
except Exception as e:
|
||||
templates.append(f"无描述模板 ({i.name})")
|
||||
print(e)
|
||||
return templates
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="vice_container"):
|
||||
yield Label(f"[b]空白单元集创建向导\n")
|
||||
yield Markdown(
|
||||
"> 提示: 你可能注意到当选中文本框时底栏和操作按键绑定将被覆盖 \n只需选中(使用鼠标或 Tab)选择框即可恢复底栏功能"
|
||||
)
|
||||
yield Markdown("1. 键入单元集名称")
|
||||
yield Input(placeholder="单元集名称", id="name_input")
|
||||
yield Markdown(
|
||||
"> 单元集名称不应与现有单元集重复. \n> 新的单元集文件将创建在 ./nucleon/你输入的名称.toml"
|
||||
)
|
||||
yield Label(f"\n")
|
||||
yield Markdown("2. 选择单元集模板")
|
||||
LINES = self.search_templates()
|
||||
"""带有宏支持的空白单元集 ({ver})
|
||||
古诗词模板单元集 ({ver})
|
||||
英语词汇和短语模板单元集 ({ver})
|
||||
"""
|
||||
yield Select.from_values(LINES, prompt="选择类型", id="template_select")
|
||||
yield Markdown("> 新单元集的版本号将和主程序版本保持同步")
|
||||
yield Label(f"\n")
|
||||
yield Markdown("3. 输入常见附加元数据 (可选)")
|
||||
yield Input(placeholder="作者", id="author_input")
|
||||
yield Input(placeholder="内容描述", id="desc_input")
|
||||
yield Button(
|
||||
"新建空白单元集",
|
||||
id="submit_button",
|
||||
variant="primary",
|
||||
classes="start-button",
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
self.query_one("#submit_button").focus()
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self):
|
||||
self.app.exit()
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
event.stop()
|
||||
if event.button.id == "submit_button":
|
||||
# 获取输入值
|
||||
name_input = self.query_one("#name_input")
|
||||
template_select = self.query_one("#template_select")
|
||||
author_input = self.query_one("#author_input")
|
||||
desc_input = self.query_one("#desc_input")
|
||||
|
||||
name = name_input.value.strip() # type: ignore
|
||||
author = author_input.value.strip() # type: ignore
|
||||
desc = desc_input.value.strip() # type: ignore
|
||||
selected = template_select.value # type: ignore
|
||||
|
||||
# 验证
|
||||
if not name:
|
||||
self.notify("单元集名称不能为空", severity="error")
|
||||
return
|
||||
|
||||
# 获取配置路径
|
||||
config = config_var.get()
|
||||
nucleon_dir = Path(config["paths"]["nucleon_dir"])
|
||||
template_dir = Path(config["paths"]["template_dir"])
|
||||
|
||||
# 检查文件是否已存在
|
||||
nucleon_path = nucleon_dir / f"{name}.toml"
|
||||
if nucleon_path.exists():
|
||||
self.notify(f"单元集 '{name}' 已存在", severity="error")
|
||||
return
|
||||
|
||||
# 确定模板文件
|
||||
if selected is None:
|
||||
self.notify("请选择一个模板", severity="error")
|
||||
return
|
||||
# selected 是描述字符串, 格式如 "描述 (filename.toml)"
|
||||
# 提取文件名
|
||||
import re
|
||||
|
||||
match = re.search(r"\(([^)]+)\)$", selected)
|
||||
if not match:
|
||||
self.notify("模板选择格式无效", severity="error")
|
||||
return
|
||||
template_filename = match.group(1)
|
||||
template_path = template_dir / template_filename
|
||||
if not template_path.exists():
|
||||
self.notify(f"模板文件不存在: {template_filename}", severity="error")
|
||||
return
|
||||
|
||||
# 加载模板
|
||||
try:
|
||||
with open(template_path, "r", encoding="utf-8") as f:
|
||||
template_data = toml.load(f)
|
||||
except Exception as e:
|
||||
self.notify(f"加载模板失败: {e}", severity="error")
|
||||
return
|
||||
|
||||
# 更新元数据
|
||||
metadata = template_data.get("__metadata__", {})
|
||||
attribution = metadata.get("attribution", {})
|
||||
if author:
|
||||
attribution["author"] = author
|
||||
if desc:
|
||||
attribution["desc"] = desc
|
||||
attribution["name"] = name
|
||||
# 可选: 设置版本
|
||||
attribution["version"] = ver
|
||||
metadata["attribution"] = attribution
|
||||
template_data["__metadata__"] = metadata
|
||||
|
||||
# 确保 nucleon_dir 存在
|
||||
nucleon_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 写入新文件
|
||||
try:
|
||||
with open(nucleon_path, "w", encoding="utf-8") as f:
|
||||
toml.dump(template_data, f)
|
||||
except Exception as e:
|
||||
self.notify(f"保存单元集失败: {e}", severity="error")
|
||||
return
|
||||
|
||||
self.notify(f"单元集 '{name}' 创建成功")
|
||||
self.app.pop_screen()
|
||||
246
src/heurams/interface/screens/precache.py
Normal file
246
src/heurams/interface/screens/precache.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Button,
|
||||
Static,
|
||||
ProgressBar,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
import pathlib
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
from textual.worker import get_current_worker
|
||||
|
||||
|
||||
class PrecachingScreen(Screen):
|
||||
"""预缓存音频文件屏幕
|
||||
|
||||
缓存记忆单元音频文件, 全部(默认) 或部分记忆单元(可选参数传入)
|
||||
|
||||
Args:
|
||||
nucleons (list): 可选列表, 仅包含 Nucleon 对象
|
||||
desc (list): 可选字符串, 包含对此次调用的文字描述
|
||||
"""
|
||||
|
||||
SUB_TITLE = "缓存管理器"
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
|
||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
self.nucleons = nucleons
|
||||
self.is_precaching = False
|
||||
self.current_file = ""
|
||||
self.current_item = ""
|
||||
self.progress = 0
|
||||
self.total = len(nucleons)
|
||||
self.processed = 0
|
||||
self.precache_worker = None
|
||||
self.cancel_flag = 0
|
||||
self.desc = desc
|
||||
for i in nucleons:
|
||||
i: pt.Nucleon
|
||||
i.do_eval()
|
||||
# print("完成 EVAL")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(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")
|
||||
else:
|
||||
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 Footer()
|
||||
|
||||
def on_mount(self):
|
||||
"""挂载时初始化状态"""
|
||||
self.update_status("就绪", "等待开始...")
|
||||
|
||||
def update_status(self, status, current_item="", progress=None):
|
||||
"""更新状态显示"""
|
||||
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) # 刷新显示
|
||||
|
||||
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耦合
|
||||
import edge_tts as tts
|
||||
|
||||
communicate = tts.Communicate(text, "zh-CN-XiaoxiaoNeural")
|
||||
communicate.save_sync(str(cache_file))
|
||||
return 1
|
||||
except Exception as e:
|
||||
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"])
|
||||
return ret
|
||||
# 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}")
|
||||
worker = get_current_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)
|
||||
self.processed += 1
|
||||
# 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)
|
||||
ret = self.precache_by_nucleon(nucleon)
|
||||
if not ret:
|
||||
self.update_status(
|
||||
"出错",
|
||||
f"处理失败, 跳过: {self.current_item}",
|
||||
)
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
if self.cancel_flag:
|
||||
worker.cancel()
|
||||
self.cancel_flag = 0
|
||||
return False
|
||||
return True
|
||||
|
||||
def precache_by_nucleons(self):
|
||||
# print("开始缓存")
|
||||
ret = self.precache_by_list(self.nucleons)
|
||||
# print(f"返回 {ret}")
|
||||
return ret
|
||||
|
||||
def precache_by_filepath(self, path: pathlib.Path):
|
||||
"""预缓存单个文件的所有内容"""
|
||||
lst = list()
|
||||
for i in pt.load_nucleon(path):
|
||||
lst.append(i[0])
|
||||
return self.precache_by_list(lst)
|
||||
|
||||
def precache_all_files(self):
|
||||
"""预缓存所有文件"""
|
||||
from heurams.context import 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: 解耦合
|
||||
|
||||
# 计算总项目数
|
||||
self.total = 0
|
||||
nu = list()
|
||||
for file in nucleon_files:
|
||||
try:
|
||||
for i in pt.load_nucleon(file):
|
||||
nu.append(i[0])
|
||||
except:
|
||||
continue
|
||||
self.total = len(nu)
|
||||
for i in nu:
|
||||
i: pt.Nucleon
|
||||
i.do_eval()
|
||||
return self.precache_by_list(nu)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop()
|
||||
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,
|
||||
)
|
||||
else:
|
||||
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:
|
||||
self.precache_worker.cancel()
|
||||
self.is_precaching = False
|
||||
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
|
||||
)
|
||||
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()
|
||||
|
||||
def action_go_back(self):
|
||||
if self.is_precaching and self.precache_worker:
|
||||
self.precache_worker.cancel()
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self):
|
||||
if self.is_precaching and self.precache_worker:
|
||||
self.precache_worker.cancel()
|
||||
self.app.exit()
|
||||
144
src/heurams/interface/screens/preparation.py
Normal file
144
src/heurams/interface/screens/preparation.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Static,
|
||||
Button,
|
||||
Markdown,
|
||||
)
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from heurams.context import config_var
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
from textual.reactive import reactive
|
||||
from textual.widget import Widget
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PreparationScreen(Screen):
|
||||
|
||||
SUB_TITLE = "准备记忆集"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("p", "precache", "预缓存音频"),
|
||||
("d", "toggle_dark", ""),
|
||||
("0,1,2,3", "app.push_screen('about')", ""),
|
||||
]
|
||||
|
||||
scheduled_num = reactive(config_var.get()["scheduled_num"])
|
||||
|
||||
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
|
||||
self.nucleons_with_orbital = pt.load_nucleon(self.nucleon_file)
|
||||
self.electrons = pt.load_electron(self.electron_file)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="vice_container"):
|
||||
yield Label(f"准备就绪: [b]{self.nucleon_file.stem}[/b]\n")
|
||||
yield Label(
|
||||
f"内容源文件: {config_var.get()['paths']['nucleon_dir']}/[b]{self.nucleon_file.name}[/b]"
|
||||
)
|
||||
yield Label(
|
||||
f"元数据文件: {config_var.get()['paths']['electron_dir']}/[b]{self.electron_file.name}[/b]"
|
||||
)
|
||||
yield Label(f"\n单元数量: {len(self.nucleons_with_orbital)}\n")
|
||||
yield Label(f"单次记忆数量: {self.scheduled_num}", id="schnum_label")
|
||||
|
||||
yield Button(
|
||||
"开始记忆",
|
||||
id="start_memorizing_button",
|
||||
variant="primary",
|
||||
classes="start-button",
|
||||
)
|
||||
yield Button(
|
||||
"预缓存音频",
|
||||
id="precache_button",
|
||||
variant="success",
|
||||
classes="precache-button",
|
||||
)
|
||||
|
||||
yield Static(f"\n单元预览:\n")
|
||||
yield Markdown(self._get_full_content().replace("/", ""), classes="full")
|
||||
yield Footer()
|
||||
|
||||
# def watch_scheduled_num(self, old_scheduled_num, new_scheduled_num):
|
||||
# logger.debug("响应", old_scheduled_num, "->", new_scheduled_num)
|
||||
# try:
|
||||
# one = self.query_one("#schnum_label")
|
||||
# one.update(f"单次记忆数量: {new_scheduled_num}") # type: ignore
|
||||
# except:
|
||||
# pass
|
||||
|
||||
def _get_full_content(self):
|
||||
content = ""
|
||||
for nucleon, orbital in self.nucleons_with_orbital:
|
||||
nucleon: pt.Nucleon
|
||||
# print(nucleon.payload)
|
||||
content += " - " + nucleon["content"] + " \n"
|
||||
return content
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_precache(self):
|
||||
from ..screens.precache import PrecachingScreen
|
||||
|
||||
lst = list()
|
||||
for i in self.nucleons_with_orbital:
|
||||
lst.append(i[0])
|
||||
precache_screen = PrecachingScreen(lst)
|
||||
self.app.push_screen(precache_screen)
|
||||
|
||||
def action_quit_app(self):
|
||||
self.app.exit()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop()
|
||||
logger.debug("按下按钮")
|
||||
if event.button.id == "start_memorizing_button":
|
||||
atoms = list()
|
||||
for nucleon, orbital in self.nucleons_with_orbital:
|
||||
atom = pt.Atom(nucleon.ident)
|
||||
atom.link("nucleon", nucleon)
|
||||
try:
|
||||
atom.link("electron", self.electrons[nucleon.ident])
|
||||
except KeyError:
|
||||
atom.link("electron", pt.Electron(nucleon.ident))
|
||||
atom.link("orbital", orbital)
|
||||
atom.link("nucleon_fmt", "toml")
|
||||
atom.link("electron_fmt", "json")
|
||||
atom.link("orbital_fmt", "toml")
|
||||
atom.link("nucleon_path", self.nucleon_file)
|
||||
atom.link("electron_path", self.electron_file)
|
||||
atom.link("orbital_path", None)
|
||||
atoms.append(atom)
|
||||
atoms_to_provide = list()
|
||||
left_new = self.scheduled_num
|
||||
for i in atoms:
|
||||
i: pt.Atom
|
||||
if i.registry["electron"].is_due():
|
||||
atoms_to_provide.append(i)
|
||||
else:
|
||||
if i.registry["electron"].is_activated():
|
||||
pass
|
||||
else:
|
||||
left_new -= 1
|
||||
if left_new >= 0:
|
||||
atoms_to_provide.append(i)
|
||||
logger.debug(f"ATP: {atoms_to_provide}")
|
||||
from .memorizor import MemScreen
|
||||
|
||||
memscreen = MemScreen(atoms_to_provide)
|
||||
self.app.push_screen(memscreen)
|
||||
elif event.button.id == "precache_button":
|
||||
self.action_precache()
|
||||
0
src/heurams/interface/screens/register.py
Normal file
0
src/heurams/interface/screens/register.py
Normal file
48
src/heurams/interface/screens/synctool.py
Normal file
48
src/heurams/interface/screens/synctool.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Button,
|
||||
Static,
|
||||
ProgressBar,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
import pathlib
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
from textual.worker import get_current_worker
|
||||
|
||||
|
||||
class SyncScreen(Screen):
|
||||
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
|
||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="sync_container"):
|
||||
pass
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
"""挂载时初始化状态"""
|
||||
|
||||
def update_status(self, status, current_item="", progress=None):
|
||||
"""更新状态显示"""
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop()
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self):
|
||||
self.app.exit()
|
||||
0
src/heurams/interface/screens/type43.py
Normal file
0
src/heurams/interface/screens/type43.py
Normal file
36
src/heurams/interface/shim.py
Normal file
36
src/heurams/interface/shim.py
Normal file
@@ -0,0 +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 的封装
|
||||
|
||||
|
||||
def report_to_staging(atom: pt.Atom, quality):
|
||||
staging[atom.ident] = min(quality, staging[atom.ident])
|
||||
|
||||
|
||||
def clear():
|
||||
staging = dict()
|
||||
|
||||
|
||||
def deploy_to_electron():
|
||||
for atom_ident, quality in staging.items():
|
||||
if pt.atom_registry[atom_ident].registry["electron"].is_activated:
|
||||
pt.atom_registry[atom_ident].registry["electron"].revisor(quality=quality)
|
||||
else:
|
||||
pt.atom_registry[atom_ident].registry["electron"].revisor(
|
||||
quality=quality, is_new_activation=True
|
||||
)
|
||||
clear()
|
||||
|
||||
|
||||
puzzle2widget = {
|
||||
pz.RecognitionPuzzle: pzw.Recognition,
|
||||
pz.ClozePuzzle: pzw.ClozePuzzle,
|
||||
pz.MCQPuzzle: pzw.MCQPuzzle,
|
||||
pz.BasePuzzle: pzw.BasePuzzleWidget,
|
||||
}
|
||||
7
src/heurams/interface/widgets/__init__.py
Normal file
7
src/heurams/interface/widgets/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
from .basic_puzzle import BasicEvaluation
|
||||
from .cloze_puzzle import ClozePuzzle
|
||||
from .finished import Finished
|
||||
from .mcq_puzzle import MCQPuzzle
|
||||
from .placeholder import Placeholder
|
||||
from .recognition import Recognition
|
||||
32
src/heurams/interface/widgets/base_puzzle_widget.py
Normal file
32
src/heurams/interface/widgets/base_puzzle_widget.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from typing import Iterable
|
||||
from textual.app import ComposeResult
|
||||
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
|
||||
|
||||
def compose(self) -> Iterable[Widget]:
|
||||
return super().compose()
|
||||
|
||||
def handler(self, rating) -> None:
|
||||
pass
|
||||
119
src/heurams/interface/widgets/basic_puzzle.py
Normal file
119
src/heurams/interface/widgets/basic_puzzle.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Static,
|
||||
Button,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
from textual.widget import Widget
|
||||
import heurams.kernel.particles as pt
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
from textual.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,
|
||||
)
|
||||
|
||||
class RatingChanged(Message):
|
||||
def __init__(self, rating: int) -> None:
|
||||
self.rating = rating # 评分值 (0-5)
|
||||
super().__init__()
|
||||
|
||||
# 反馈映射表
|
||||
feedback_mapping = {
|
||||
"feedback_5": {"rating": 5, "text": "完美回想"},
|
||||
"feedback_4": {"rating": 4, "text": "犹豫后正确"},
|
||||
"feedback_3": {"rating": 3, "text": "困难地正确"},
|
||||
"feedback_2": {"rating": 2, "text": "错误但熟悉"},
|
||||
"feedback_1": {"rating": 1, "text": "错误且不熟"},
|
||||
"feedback_0": {"rating": 0, "text": "完全空白"},
|
||||
}
|
||||
|
||||
def compose(self):
|
||||
# 显示主要内容
|
||||
yield Label(self.atom.registry["nucleon"]["content"], id="main")
|
||||
|
||||
# 显示评估说明(可选)
|
||||
yield Static("请评估你对这个内容的记忆程度: ", classes="instruction")
|
||||
|
||||
# 按钮容器
|
||||
with ScrollableContainer(id="button_container"):
|
||||
btn = {}
|
||||
btn["5"] = Button(
|
||||
"完美回想", variant="success", id="feedback_5", classes="choice"
|
||||
)
|
||||
btn["4"] = Button(
|
||||
"犹豫后正确", variant="success", id="feedback_4", classes="choice"
|
||||
)
|
||||
btn["3"] = Button(
|
||||
"困难地正确", variant="warning", id="feedback_3", classes="choice"
|
||||
)
|
||||
btn["2"] = Button(
|
||||
"错误但熟悉", variant="warning", id="feedback_2", classes="choice"
|
||||
)
|
||||
btn["1"] = Button(
|
||||
"错误且不熟", variant="error", id="feedback_1", classes="choice"
|
||||
)
|
||||
btn["0"] = Button(
|
||||
"完全空白", variant="error", id="feedback_0", classes="choice"
|
||||
)
|
||||
|
||||
# 布局按钮
|
||||
yield Horizontal(btn["5"], btn["4"])
|
||||
yield Horizontal(btn["3"], btn["2"])
|
||||
yield Horizontal(btn["1"], btn["0"])
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""处理按钮点击事件"""
|
||||
button_id = event.button.id
|
||||
|
||||
if button_id in self.feedback_mapping:
|
||||
feedback_info = self.feedback_mapping[button_id]
|
||||
|
||||
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:
|
||||
for button in self.query("Button.choice"):
|
||||
if button.id != selected_button_id:
|
||||
button.disabled = True
|
||||
|
||||
def enable_all_buttons(self) -> None:
|
||||
for button in self.query("Button.choice"):
|
||||
button.disabled = False
|
||||
|
||||
def on_key(self, event) -> None:
|
||||
if event.key in ["0", "1", "2", "3", "4", "5"]:
|
||||
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"],
|
||||
)
|
||||
)
|
||||
109
src/heurams/interface/widgets/cloze_puzzle.py
Normal file
109
src/heurams/interface/widgets/cloze_puzzle.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Button,
|
||||
)
|
||||
from textual.widget import Widget
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
import copy
|
||||
import random
|
||||
from textual.containers import Container
|
||||
from textual.message import Message
|
||||
from heurams.services.logger import get_logger
|
||||
from typing import TypedDict
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Setting(TypedDict):
|
||||
__origin__: str
|
||||
__hint__: str
|
||||
text: str
|
||||
delimiter: str
|
||||
min_denominator: str
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
self.inputlist = list()
|
||||
self.hashtable = {}
|
||||
self.alia = alia
|
||||
self._load()
|
||||
self.hashmap = dict()
|
||||
|
||||
def _load(self):
|
||||
setting = self.atom.registry["orbital"]["puzzles"][self.alia]
|
||||
self.puzzle = pz.ClozePuzzle(
|
||||
text=setting["text"],
|
||||
delimiter=setting["delimiter"],
|
||||
min_denominator=int(setting["min_denominator"]),
|
||||
)
|
||||
self.puzzle.refresh()
|
||||
self.ans = copy.copy(self.puzzle.answer) # 乱序
|
||||
random.shuffle(self.ans)
|
||||
|
||||
def compose(self):
|
||||
yield Label(self.puzzle.wording, id="sentence")
|
||||
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
|
||||
# 渲染当前问题的选项
|
||||
with Container(id="btn-container"):
|
||||
for i in self.ans:
|
||||
self.hashmap[str(hash(i))] = i
|
||||
btnid = f"sel000-{hash(i)}"
|
||||
logger.debug(f"建立按钮 {btnid}")
|
||||
yield Button(i, id=f"{btnid}")
|
||||
|
||||
yield Button("退格", id="delete")
|
||||
|
||||
def update_display(self):
|
||||
preview = self.query_one("#inputpreview")
|
||||
preview.update(f"当前输入: {self.inputlist}") # type: ignore
|
||||
|
||||
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.update_display()
|
||||
else:
|
||||
answer_text = self.hashmap[button_id[7:]] # type: ignore
|
||||
self.inputlist.append(answer_text)
|
||||
self.update_display()
|
||||
|
||||
if len(self.inputlist) >= len(self.puzzle.answer):
|
||||
is_correct = self.inputlist == self.puzzle.answer
|
||||
rating = 4 if is_correct else 2
|
||||
self.handler(rating)
|
||||
self.screen.rating = rating # type: ignore
|
||||
|
||||
if not is_correct:
|
||||
self.inputlist = []
|
||||
self.update_display()
|
||||
|
||||
def handler(self, rating):
|
||||
if self.atom.lock():
|
||||
pass
|
||||
else:
|
||||
self.atom.minimize(rating)
|
||||
36
src/heurams/interface/widgets/finished.py
Normal file
36
src/heurams/interface/widgets/finished.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Button,
|
||||
)
|
||||
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:
|
||||
self.alia = alia
|
||||
super().__init__(
|
||||
*children,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup
|
||||
)
|
||||
|
||||
def compose(self):
|
||||
yield Label("本次记忆进程结束", id="finished_msg")
|
||||
yield Button("返回上一级", id="back-to-menu")
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
button_id = event.button.id
|
||||
if button_id == "back-to-menu":
|
||||
self.app.pop_screen()
|
||||
160
src/heurams/interface/widgets/mcq_puzzle.py
Normal file
160
src/heurams/interface/widgets/mcq_puzzle.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# 单项选择题
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Button,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, Container
|
||||
from textual.widget import Widget
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
from typing import TypedDict
|
||||
from heurams.services.hasher import hash
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
self.inputlist = []
|
||||
self.alia = alia
|
||||
self.hashmap = dict()
|
||||
self.cursor = 0
|
||||
self.atom = atom
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
cfg = self.atom.registry["orbital"]["puzzles"][self.alia]
|
||||
self.puzzle = pz.MCQPuzzle(
|
||||
cfg["mapping"], cfg["jammer"], int(cfg["max_riddles_num"]), cfg["prefix"]
|
||||
)
|
||||
self.puzzle.refresh()
|
||||
|
||||
def compose(self):
|
||||
self.atom.registry["nucleon"].do_eval()
|
||||
setting: Setting = self.atom.registry["nucleon"].metadata["orbital"]["puzzles"][
|
||||
self.alia
|
||||
]
|
||||
logger.debug(f"Puzzle Setting: {setting}")
|
||||
current_options = self.puzzle.options[len(self.inputlist)]
|
||||
yield Label(setting["primary"], id="sentence")
|
||||
yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle")
|
||||
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
|
||||
|
||||
# 渲染当前问题的选项
|
||||
with Container(id="btn-container"):
|
||||
for i in current_options:
|
||||
self.hashmap[str(hash(i))] = i
|
||||
btnid = f"sel{str(self.cursor).zfill(3)}-{hash(i)}"
|
||||
logger.debug(f"建立按钮 {btnid}")
|
||||
yield Button(i, id=f"{btnid}")
|
||||
|
||||
yield Button("退格", id="delete")
|
||||
|
||||
def update_display(self, error=0):
|
||||
# 更新预览标签
|
||||
preview = self.query_one("#inputpreview")
|
||||
preview.update(f"当前输入: {self.inputlist}") # type: ignore
|
||||
logger.debug("已经更新预览标签")
|
||||
# 更新问题标签
|
||||
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
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""处理按钮点击事件"""
|
||||
event.stop()
|
||||
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("sel"): # type: ignore
|
||||
# 选项选择处理
|
||||
answer_text = self.hashmap[button_id[7:]] # type: ignore
|
||||
self.inputlist.append(answer_text)
|
||||
logger.debug(f"{self.inputlist}")
|
||||
# 检查是否完成所有题目
|
||||
if len(self.inputlist) >= len(self.puzzle.answer):
|
||||
is_correct = self.inputlist == self.puzzle.answer
|
||||
rating = 4 if is_correct else 2
|
||||
|
||||
self.screen.rating = rating # type: ignore
|
||||
self.handler(rating)
|
||||
# 重置输入(如果回答错误)
|
||||
if not is_correct:
|
||||
self.inputlist = []
|
||||
self.refresh_buttons()
|
||||
self.update_display()
|
||||
else:
|
||||
# 进入下一题
|
||||
self.refresh_buttons()
|
||||
self.update_display()
|
||||
|
||||
def refresh_buttons(self):
|
||||
"""刷新按钮显示(用于题目切换)"""
|
||||
# 移除所有选项按钮
|
||||
logger.debug("刷新按钮")
|
||||
self.cursor += 1
|
||||
container = self.query_one("#btn-container")
|
||||
buttons_to_remove = [
|
||||
child
|
||||
for child in container.children
|
||||
if hasattr(child, "id") and child.id and child.id.startswith("sel")
|
||||
]
|
||||
|
||||
for button in buttons_to_remove:
|
||||
logger.info(button)
|
||||
container.remove_children("#" + button.id) # type: ignore
|
||||
|
||||
# 添加当前题目的选项按钮
|
||||
current_question_index = len(self.inputlist)
|
||||
if current_question_index < len(self.puzzle.options):
|
||||
current_options = self.puzzle.options[current_question_index]
|
||||
for option in current_options:
|
||||
button_id = f"sel{str(self.cursor).zfill(3)}-{hash(option)}"
|
||||
if button_id not in self.hashmap:
|
||||
self.hashmap[button_id] = option
|
||||
new_button = Button(option, id=button_id)
|
||||
container.mount(new_button)
|
||||
|
||||
def handler(self, rating):
|
||||
if self.atom.lock():
|
||||
pass
|
||||
else:
|
||||
self.atom.minimize(rating)
|
||||
33
src/heurams/interface/widgets/placeholder.py
Normal file
33
src/heurams/interface/widgets/placeholder.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Button,
|
||||
)
|
||||
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 compose(self):
|
||||
yield Label("示例标签", id="testlabel")
|
||||
yield Button("示例按钮", id="testbtn", classes="choice")
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
pass
|
||||
116
src/heurams/interface/widgets/recognition.py
Normal file
116
src/heurams/interface/widgets/recognition.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import (
|
||||
Markdown,
|
||||
Label,
|
||||
Static,
|
||||
Button,
|
||||
)
|
||||
from textual.containers import Center
|
||||
from textual.widget import Widget
|
||||
from typing import Dict
|
||||
import heurams.kernel.particles as pt
|
||||
import re
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
from typing import TypedDict, List
|
||||
from textual.message import Message
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RecognitionConfig(TypedDict):
|
||||
__origin__: str
|
||||
__hint__: str
|
||||
primary: str
|
||||
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,
|
||||
)
|
||||
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"]
|
||||
replace_dict = {
|
||||
", ": ",",
|
||||
". ": ".",
|
||||
"; ": ";",
|
||||
": ": ":",
|
||||
f"{delim},": ",",
|
||||
f".{delim}": ".",
|
||||
f"{delim};": ";",
|
||||
f";{delim}": ";",
|
||||
f":{delim}": ":",
|
||||
}
|
||||
|
||||
nucleon = self.atom.registry["nucleon"]
|
||||
metadata = self.atom.registry["nucleon"].metadata
|
||||
primary = cfg["primary"]
|
||||
|
||||
with Center():
|
||||
yield Static(f"[dim]{cfg['top_dim']}[/]")
|
||||
yield Label("")
|
||||
|
||||
for old, new in replace_dict.items():
|
||||
primary = primary.replace(old, new)
|
||||
primary_splited = re.split(r"(?<=[,;:|])", cfg["primary"])
|
||||
for item in primary_splited:
|
||||
with Center():
|
||||
yield Label(
|
||||
f"[b][b]{item.replace(delim, ' ')}[/][/]",
|
||||
id="sentence" + str(hash(item)),
|
||||
)
|
||||
|
||||
for item in cfg["secondary"]:
|
||||
if isinstance(item, list):
|
||||
for j in item:
|
||||
yield Markdown(f"### {metadata['annotation'][item]}: {j}")
|
||||
continue
|
||||
if isinstance(item, Dict):
|
||||
total = ""
|
||||
for j, k in item.items(): # type: ignore
|
||||
total += f"> **{j}**: {k} \n"
|
||||
yield Markdown(total)
|
||||
if isinstance(item, str):
|
||||
yield Markdown(item)
|
||||
|
||||
with Center():
|
||||
yield Button("我已知晓", id="ok")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "ok":
|
||||
self.screen.rating = 5 # type: ignore
|
||||
self.handler(5)
|
||||
|
||||
def handler(self, rating):
|
||||
if not self.atom.registry["runtime"]["locked"]:
|
||||
if not self.atom.registry["electron"].is_activated():
|
||||
self.atom.registry["electron"].activate()
|
||||
logger.debug(f"激活原子 {self.atom}")
|
||||
self.atom.lock(1)
|
||||
self.atom.minimize(5)
|
||||
else:
|
||||
pass
|
||||
2
src/heurams/kernel/README.md
Normal file
2
src/heurams/kernel/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Kernel - HeurAMS 核心
|
||||
记忆规划相关算法与数据结构, 可脱离业务层
|
||||
15
src/heurams/kernel/algorithms/__init__.py
Normal file
15
src/heurams/kernel/algorithms/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from .sm2 import SM2Algorithm
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"SM2Algorithm",
|
||||
]
|
||||
|
||||
algorithms = {
|
||||
"SM-2": SM2Algorithm,
|
||||
"supermemo2": SM2Algorithm,
|
||||
}
|
||||
|
||||
logger.debug("算法模块初始化完成, 注册的算法: %s", list(algorithms.keys()))
|
||||
69
src/heurams/kernel/algorithms/base.py
Normal file
69
src/heurams/kernel/algorithms/base.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import heurams.services.timer as timer
|
||||
from typing import TypedDict
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BaseAlgorithm:
|
||||
algo_name = "BaseAlgorithm"
|
||||
|
||||
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 = {
|
||||
"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:
|
||||
"""迭代记忆数据"""
|
||||
logger.debug(
|
||||
"BaseAlgorithm.revisor 被调用, algodata keys: %s, feedback: %d, is_new_activation: %s",
|
||||
list(algodata.keys()) if algodata else [],
|
||||
feedback,
|
||||
is_new_activation,
|
||||
)
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def is_due(cls, algodata) -> int:
|
||||
"""是否应该复习"""
|
||||
logger.debug(
|
||||
"BaseAlgorithm.is_due 被调用, algodata keys: %s",
|
||||
list(algodata.keys()) if algodata else [],
|
||||
)
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def rate(cls, algodata) -> str:
|
||||
"""获取评分信息"""
|
||||
logger.debug(
|
||||
"BaseAlgorithm.rate 被调用, algodata keys: %s",
|
||||
list(algodata.keys()) if algodata else [],
|
||||
)
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def nextdate(cls, algodata) -> int:
|
||||
"""获取下一次记忆时间戳"""
|
||||
logger.debug(
|
||||
"BaseAlgorithm.nextdate 被调用, algodata keys: %s",
|
||||
list(algodata.keys()) if algodata else [],
|
||||
)
|
||||
return -1
|
||||
6
src/heurams/kernel/algorithms/fsrs.py
Normal file
6
src/heurams/kernel/algorithms/fsrs.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# FSRS 算法模块, 尚未就绪
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
logger.info("FSRS算法模块尚未实现")
|
||||
126
src/heurams/kernel/algorithms/sm2.py
Normal file
126
src/heurams/kernel/algorithms/sm2.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from .base import BaseAlgorithm
|
||||
import heurams.services.timer as timer
|
||||
from typing import TypedDict
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SM2Algorithm(BaseAlgorithm):
|
||||
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
|
||||
|
||||
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 由主程序评估
|
||||
|
||||
Args:
|
||||
quality (int): 记忆保留率量化参数
|
||||
"""
|
||||
logger.debug(
|
||||
"SM2.revisor 开始, feedback: %d, is_new_activation: %s",
|
||||
feedback,
|
||||
is_new_activation,
|
||||
)
|
||||
|
||||
if feedback == -1:
|
||||
logger.debug("feedback 为 -1, 跳过更新")
|
||||
return
|
||||
|
||||
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"]
|
||||
)
|
||||
logger.debug("更新 efactor: %f", algodata[cls.algo_name]["efactor"])
|
||||
|
||||
if feedback < 3:
|
||||
algodata[cls.algo_name]["rept"] = 0
|
||||
algodata[cls.algo_name]["interval"] = 0
|
||||
logger.debug("feedback < 3, 重置 rept 和 interval")
|
||||
else:
|
||||
algodata[cls.algo_name]["rept"] += 1
|
||||
logger.debug("递增 rept: %d", algodata[cls.algo_name]["rept"])
|
||||
|
||||
algodata[cls.algo_name]["real_rept"] += 1
|
||||
logger.debug("递增 real_rept: %d", algodata[cls.algo_name]["real_rept"])
|
||||
|
||||
if is_new_activation:
|
||||
algodata[cls.algo_name]["rept"] = 0
|
||||
algodata[cls.algo_name]["efactor"] = 2.5
|
||||
logger.debug("新激活, 重置 rept 和 efactor")
|
||||
|
||||
if algodata[cls.algo_name]["rept"] == 0:
|
||||
algodata[cls.algo_name]["interval"] = 1
|
||||
logger.debug("rept=0, 设置 interval=1")
|
||||
elif algodata[cls.algo_name]["rept"] == 1:
|
||||
algodata[cls.algo_name]["interval"] = 6
|
||||
logger.debug("rept=1, 设置 interval=6")
|
||||
else:
|
||||
algodata[cls.algo_name]["interval"] = round(
|
||||
algodata[cls.algo_name]["interval"] * algodata[cls.algo_name]["efactor"]
|
||||
)
|
||||
logger.debug(
|
||||
"rept>1, 计算 interval: %d", algodata[cls.algo_name]["interval"]
|
||||
)
|
||||
|
||||
algodata[cls.algo_name]["last_date"] = timer.get_daystamp()
|
||||
algodata[cls.algo_name]["next_date"] = (
|
||||
timer.get_daystamp() + algodata[cls.algo_name]["interval"]
|
||||
)
|
||||
algodata[cls.algo_name]["last_modify"] = timer.get_timestamp()
|
||||
|
||||
logger.debug(
|
||||
"更新日期: last_date=%d, next_date=%d, last_modify=%f",
|
||||
algodata[cls.algo_name]["last_date"],
|
||||
algodata[cls.algo_name]["next_date"],
|
||||
algodata[cls.algo_name]["last_modify"],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_due(cls, algodata):
|
||||
result = algodata[cls.algo_name]["next_date"] <= timer.get_daystamp()
|
||||
logger.debug(
|
||||
"SM2.is_due: next_date=%d, current_daystamp=%d, result=%s",
|
||||
algodata[cls.algo_name]["next_date"],
|
||||
timer.get_daystamp(),
|
||||
result,
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def rate(cls, algodata):
|
||||
efactor = algodata[cls.algo_name]["efactor"]
|
||||
logger.debug("SM2.rate: efactor=%f", efactor)
|
||||
return str(efactor)
|
||||
|
||||
@classmethod
|
||||
def nextdate(cls, algodata) -> int:
|
||||
next_date = algodata[cls.algo_name]["next_date"]
|
||||
logger.debug("SM2.nextdate: %d", next_date)
|
||||
return next_date
|
||||
29
src/heurams/kernel/particles/__init__.py
Normal file
29
src/heurams/kernel/particles/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Particle 模块 - 粒子对象系统
|
||||
|
||||
提供闪卡所需对象, 使用物理学粒子的领域驱动设计
|
||||
"""
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger.debug("粒子模块已加载")
|
||||
|
||||
from .electron import Electron
|
||||
from .nucleon import Nucleon
|
||||
from .orbital import Orbital
|
||||
from .atom import Atom, atom_registry
|
||||
from .probe import probe_all, probe_by_filename
|
||||
from .loader import load_nucleon, load_electron
|
||||
|
||||
__all__ = [
|
||||
"Electron",
|
||||
"Nucleon",
|
||||
"Orbital",
|
||||
"Atom",
|
||||
"probe_all",
|
||||
"probe_by_filename",
|
||||
"load_nucleon",
|
||||
"load_electron",
|
||||
"atom_registry",
|
||||
]
|
||||
238
src/heurams/kernel/particles/atom.py
Normal file
238
src/heurams/kernel/particles/atom.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from .electron import Electron
|
||||
from .nucleon import Nucleon
|
||||
from .orbital import Orbital
|
||||
from typing import TypedDict
|
||||
import pathlib
|
||||
import typing
|
||||
import toml
|
||||
import json
|
||||
import bidict
|
||||
from heurams.context import config_var
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AtomRegister_runtime(TypedDict):
|
||||
locked: bool # 只读锁定标识符
|
||||
min_rate: int # 最低评分
|
||||
newact: bool # 新激活
|
||||
|
||||
|
||||
class AtomRegister(TypedDict):
|
||||
nucleon: Nucleon
|
||||
nucleon_path: pathlib.Path
|
||||
nucleon_fmt: str
|
||||
electron: Electron
|
||||
electron_path: pathlib.Path
|
||||
electron_fmt: str
|
||||
orbital: Orbital
|
||||
orbital_path: pathlib.Path
|
||||
orbital_fmt: str
|
||||
runtime: AtomRegister_runtime
|
||||
|
||||
|
||||
class Atom:
|
||||
"""
|
||||
统一处理一系列对象的所有信息与持久化:
|
||||
关联电子 (算法数据)
|
||||
关联核子 (内容数据)
|
||||
关联轨道 (策略数据)
|
||||
以及关联路径
|
||||
"""
|
||||
|
||||
def __init__(self, ident=""):
|
||||
logger.debug("创建 Atom 实例, ident: '%s'", ident)
|
||||
self.ident = ident
|
||||
atom_registry[ident] = self
|
||||
logger.debug("Atom 已注册到全局注册表, 当前注册表大小: %d", len(atom_registry))
|
||||
# self.is_evaled = False
|
||||
self.registry: AtomRegister = { # type: ignore
|
||||
"nucleon": None,
|
||||
"nucleon_path": None,
|
||||
"nucleon_fmt": "toml",
|
||||
"electron": None,
|
||||
"electron_path": None,
|
||||
"electron_fmt": "json",
|
||||
"orbital": None,
|
||||
"orbital_path": None, # 允许设置为 None, 此时使用 nucleon 文件内的推荐配置
|
||||
"orbital_fmt": "toml",
|
||||
"runtime": {"locked": False, "min_rate": 0x3F3F3F3F, "newact": False},
|
||||
}
|
||||
self.do_eval()
|
||||
logger.debug("Atom 初始化完成")
|
||||
|
||||
def link(self, key, value):
|
||||
logger.debug("Atom.link: key='%s', value type: %s", key, type(value).__name__)
|
||||
if key in self.registry.keys():
|
||||
self.registry[key] = value
|
||||
logger.debug("键 '%s' 已链接, 触发 do_eval", key)
|
||||
self.do_eval()
|
||||
if key == 'electron':
|
||||
if self.registry['electron'].is_activated() == 0:
|
||||
self.registry['runtime']['newact'] = True
|
||||
else:
|
||||
logger.error("尝试链接不受支持的键: '%s'", key)
|
||||
raise ValueError("不受支持的原子元数据链接操作")
|
||||
|
||||
def minimize(self, rating):
|
||||
"""效果等同于 self.registry['runtime']['min_rate'] = min(rating, self.registry['runtime']['min_rate'])
|
||||
|
||||
Args:
|
||||
rating (int): 评分
|
||||
"""
|
||||
self.registry["runtime"]["min_rate"] = min(
|
||||
rating, self.registry["runtime"]["min_rate"]
|
||||
)
|
||||
|
||||
def lock(self, locked=-1):
|
||||
logger.debug(f"锁定参数 {locked}")
|
||||
"""锁定, 效果等同于 self.registry['runtime']['locked'] = locked 或者返回是否锁定"""
|
||||
if locked == 1:
|
||||
self.registry["runtime"]["locked"] = True
|
||||
return 1
|
||||
elif locked == 0:
|
||||
self.registry["runtime"]["locked"] = False
|
||||
return 1
|
||||
elif locked == -1:
|
||||
return self.registry["runtime"]["locked"]
|
||||
return 0
|
||||
|
||||
def revise(self):
|
||||
"""执行最终评分
|
||||
PuzzleWidget 的 handler 除了测试, 严禁直接执行 Electron 的 revisor 函数, 否则造成逻辑混乱
|
||||
"""
|
||||
if self.registry["runtime"]["locked"]:
|
||||
logger.debug(f"允许总评分: {self.registry['runtime']['min_rate']}")
|
||||
self.registry["electron"].revisor(self.registry["runtime"]["min_rate"], is_new_activation=self.registry["runtime"]["newact"])
|
||||
else:
|
||||
logger.debug("禁止总评分")
|
||||
|
||||
def do_eval(self):
|
||||
"""
|
||||
执行并以结果替换当前单元的所有 eval 语句
|
||||
TODO: 带有限制的 eval, 异步/多线程执行避免堵塞
|
||||
"""
|
||||
logger.debug("Atom.do_eval 开始")
|
||||
|
||||
# eval 环境设置
|
||||
def eval_with_env(s: str):
|
||||
# 初始化默认值
|
||||
nucleon = self.registry["nucleon"]
|
||||
default = {}
|
||||
metadata = {}
|
||||
try:
|
||||
default = config_var.get()["puzzles"]
|
||||
metadata = nucleon.metadata
|
||||
except Exception:
|
||||
# 如果无法获取配置或元数据, 使用空字典
|
||||
logger.debug("无法获取配置或元数据, 使用空字典")
|
||||
pass
|
||||
try:
|
||||
eval_value = eval(s)
|
||||
if isinstance(eval_value, (list, dict)):
|
||||
ret = eval_value
|
||||
else:
|
||||
ret = str(eval_value)
|
||||
logger.debug(
|
||||
"eval 执行成功: '%s' -> '%s'",
|
||||
s,
|
||||
str(ret)[:50] + "..." if len(ret) > 50 else ret,
|
||||
)
|
||||
except Exception as e:
|
||||
ret = f"此 eval 实例发生错误: {e}"
|
||||
logger.warning("eval 执行错误: '%s' -> %s", s, e)
|
||||
return ret
|
||||
|
||||
def traverse(data, modifier):
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
data[key] = traverse(value, modifier)
|
||||
return data
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
data[i] = traverse(item, modifier)
|
||||
return data
|
||||
elif isinstance(data, tuple):
|
||||
return tuple(traverse(item, modifier) for item in data)
|
||||
else:
|
||||
if isinstance(data, str):
|
||||
if data.startswith("eval:"):
|
||||
logger.debug("发现 eval 表达式: '%s'", data[5:])
|
||||
return modifier(data[5:])
|
||||
return data
|
||||
|
||||
# 如果 nucleon 存在且有 do_eval 方法, 调用它
|
||||
nucleon = self.registry["nucleon"]
|
||||
if nucleon is not None and hasattr(nucleon, "do_eval"):
|
||||
nucleon.do_eval()
|
||||
logger.debug("已调用 nucleon.do_eval")
|
||||
|
||||
# 如果 electron 存在且其 algodata 包含 eval 字符串, 遍历它
|
||||
electron = self.registry["electron"]
|
||||
if electron is not None and hasattr(electron, "algodata"):
|
||||
traverse(electron.algodata, eval_with_env)
|
||||
logger.debug("已处理 electron algodata eval")
|
||||
|
||||
# 如果 orbital 存在且是字典, 遍历它
|
||||
orbital = self.registry["orbital"]
|
||||
if orbital is not None and isinstance(orbital, dict):
|
||||
traverse(orbital, eval_with_env)
|
||||
logger.debug("orbital eval 完成")
|
||||
|
||||
logger.debug("Atom.do_eval 完成")
|
||||
|
||||
def persist(self, key):
|
||||
logger.debug("Atom.persist: key='%s'", key)
|
||||
path: pathlib.Path | None = self.registry[key + "_path"]
|
||||
if isinstance(path, pathlib.Path):
|
||||
path = typing.cast(pathlib.Path, path)
|
||||
logger.debug("持久化路径: %s, 格式: %s", path, self.registry[key + "_fmt"])
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if self.registry[key + "_fmt"] == "toml":
|
||||
with open(path, "r+") as f:
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
toml.dump(self.registry[key], f)
|
||||
logger.debug("TOML 数据已保存到: %s", path)
|
||||
elif self.registry[key + "_fmt"] == "json":
|
||||
with open(path, "r+") as f:
|
||||
origin = json.load(f)
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
origin[self.ident] = self.registry[key].algodata
|
||||
json.dump(origin, f, indent=2, ensure_ascii=False)
|
||||
logger.debug("JSON 数据已保存到: %s", path)
|
||||
else:
|
||||
logger.error("不受支持的持久化格式: %s", self.registry[key + "_fmt"])
|
||||
raise KeyError("不受支持的持久化格式")
|
||||
else:
|
||||
logger.error("路径未初始化: %s_path", key)
|
||||
raise TypeError("对未初始化的路径对象操作")
|
||||
|
||||
def __getitem__(self, key):
|
||||
logger.debug("Atom.__getitem__: key='%s'", key)
|
||||
if key in self.registry:
|
||||
value = self.registry[key]
|
||||
logger.debug("返回 value type: %s", type(value).__name__)
|
||||
return value
|
||||
logger.error("不支持的键: '%s'", key)
|
||||
raise KeyError(f"不支持的键: {key}")
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
logger.debug(
|
||||
"Atom.__setitem__: key='%s', value type: %s", key, type(value).__name__
|
||||
)
|
||||
if key in self.registry:
|
||||
self.registry[key] = value
|
||||
logger.debug("键 '%s' 已设置", key)
|
||||
else:
|
||||
logger.error("不支持的键: '%s'", key)
|
||||
raise KeyError(f"不支持的键: {key}")
|
||||
|
||||
@staticmethod
|
||||
def placeholder():
|
||||
return (Electron.placeholder(), Nucleon.placeholder(), {})
|
||||
|
||||
|
||||
atom_registry: bidict.bidict[str, Atom] = bidict.bidict()
|
||||
151
src/heurams/kernel/particles/electron.py
Normal file
151
src/heurams/kernel/particles/electron.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import heurams.services.timer as timer
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.algorithms import algorithms
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Electron:
|
||||
"""电子: 记忆分析元数据及算法"""
|
||||
|
||||
def __init__(self, ident: str, algodata: dict = {}, algo_name: str = "supermemo2"):
|
||||
"""初始化电子对象 (记忆数据)
|
||||
|
||||
Args:
|
||||
ident: 算法的唯一标识符, 用于区分不同的算法实例, 使用 algodata[ident] 获取
|
||||
algodata: 算法数据字典, 包含算法的各项参数和设置
|
||||
algo: 使用的算法模块标识
|
||||
"""
|
||||
logger.debug(
|
||||
"创建 Electron 实例, ident: '%s', algo_name: '%s'", ident, algo_name
|
||||
)
|
||||
self.algodata = algodata
|
||||
self.ident = ident
|
||||
self.algo = algorithms[algo_name]
|
||||
logger.debug("使用的算法类: %s", self.algo.__name__)
|
||||
|
||||
if self.algo not in self.algodata.keys():
|
||||
self.algodata[self.algo.algo_name] = {}
|
||||
logger.debug("算法键 '%s' 不存在, 已创建空字典", self.algo)
|
||||
if not self.algodata[self.algo.algo_name]:
|
||||
logger.debug("算法数据为空, 使用默认值初始化")
|
||||
self._default_init(self.algo.defaults)
|
||||
else:
|
||||
logger.debug("算法数据已存在, 跳过默认初始化")
|
||||
logger.debug(
|
||||
"Electron 初始化完成, algodata keys: %s", list(self.algodata.keys())
|
||||
)
|
||||
|
||||
def _default_init(self, defaults: dict):
|
||||
"""默认初始化包装"""
|
||||
logger.debug(
|
||||
"Electron._default_init: 使用默认值, keys: %s", list(defaults.keys())
|
||||
)
|
||||
self.algodata[self.algo.algo_name] = defaults.copy()
|
||||
|
||||
def activate(self):
|
||||
"""激活此电子"""
|
||||
logger.debug("Electron.activate: 激活 ident='%s'", self.ident)
|
||||
self.algodata[self.algo.algo_name]["is_activated"] = 1
|
||||
self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp()
|
||||
logger.debug("电子已激活, is_activated=1")
|
||||
|
||||
def modify(self, var: str, value):
|
||||
"""修改 algodata[algo] 中子字典数据"""
|
||||
logger.debug("Electron.modify: var='%s', value=%s", var, value)
|
||||
if var in self.algodata[self.algo.algo_name]:
|
||||
self.algodata[self.algo.algo_name][var] = value
|
||||
self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp()
|
||||
logger.debug("变量 '%s' 已修改, 更新 last_modify", var)
|
||||
else:
|
||||
logger.warning("'%s' 非已知元数据字段", var)
|
||||
print(f"警告: '{var}' 非已知元数据字段")
|
||||
|
||||
def is_due(self):
|
||||
"""是否应该复习"""
|
||||
logger.debug("Electron.is_due: 检查 ident='%s'", self.ident)
|
||||
result = self.algo.is_due(self.algodata)
|
||||
logger.debug("is_due 结果: %s", result)
|
||||
return result and self.is_activated()
|
||||
|
||||
def is_activated(self):
|
||||
result = self.algodata[self.algo.algo_name]["is_activated"]
|
||||
logger.debug("Electron.is_activated: ident='%s', 结果: %d", self.ident, result)
|
||||
return result
|
||||
|
||||
def get_rate(self):
|
||||
"评价"
|
||||
try:
|
||||
logger.debug("Electron.rate: ident='%s'", self.ident)
|
||||
result = self.algo.rate(self.algodata)
|
||||
logger.debug("rate 结果: %s", result)
|
||||
return result
|
||||
except:
|
||||
return 0
|
||||
|
||||
def nextdate(self) -> int:
|
||||
logger.debug("Electron.nextdate: ident='%s'", self.ident)
|
||||
result = self.algo.nextdate(self.algodata)
|
||||
logger.debug("nextdate 结果: %d", result)
|
||||
return result
|
||||
|
||||
def revisor(self, quality: int = 5, is_new_activation: bool = False):
|
||||
"""算法迭代决策机制实现
|
||||
|
||||
Args:
|
||||
quality (int): 记忆保留率量化参数 (0-5)
|
||||
is_new_activation (bool): 是否为初次激活
|
||||
"""
|
||||
logger.debug(
|
||||
"Electron.revisor: ident='%s', quality=%d, is_new_activation=%s",
|
||||
self.ident,
|
||||
quality,
|
||||
is_new_activation,
|
||||
)
|
||||
self.algo.revisor(self.algodata, quality, is_new_activation)
|
||||
logger.debug(
|
||||
"revisor 完成, 更新后的 algodata: %s", self.algodata.get(self.algo, {})
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"记忆单元预览 \n"
|
||||
f"标识符: '{self.ident}' \n"
|
||||
f"算法: {self.algo} \n"
|
||||
f"易度系数: {self.algodata[self.algo.algo_name]['efactor']:.2f} \n"
|
||||
f"已经重复的次数: {self.algodata[self.algo.algo_name]['rept']} \n"
|
||||
f"下次间隔: {self.algodata[self.algo.algo_name]['interval']} 天 \n"
|
||||
f"下次复习日期时间戳: {self.algodata[self.algo.algo_name]['next_date']}"
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.ident == other.ident:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.ident)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "ident":
|
||||
return self.ident
|
||||
if key in self.algodata[self.algo.algo_name]:
|
||||
return self.algodata[self.algo.algo_name][key]
|
||||
else:
|
||||
raise KeyError(f"键 '{key}' 未在 algodata[self.algo] 中")
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key == "ident":
|
||||
raise AttributeError("ident 应为只读")
|
||||
self.algodata[self.algo.algo_name][key] = value
|
||||
self.algodata[self.algo.algo_name]["last_modify"] = timer.get_timestamp()
|
||||
|
||||
def __len__(self):
|
||||
"""仅返回当前算法的配置数量"""
|
||||
return len(self.algodata[self.algo.algo_name])
|
||||
|
||||
@staticmethod
|
||||
def placeholder():
|
||||
"""生成一个电子占位符"""
|
||||
return Electron("电子对象样例内容", {})
|
||||
71
src/heurams/kernel/particles/loader.py
Normal file
71
src/heurams/kernel/particles/loader.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from .nucleon import Nucleon
|
||||
from .electron import Electron
|
||||
import heurams.services.hasher as hasher
|
||||
import pathlib
|
||||
import toml
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def load_nucleon(path: pathlib.Path, fmt="toml"):
|
||||
logger.debug("load_nucleon: 加载文件 %s, 格式: %s", path, fmt)
|
||||
with open(path, "r") as f:
|
||||
dictdata = dict()
|
||||
dictdata = toml.load(f) # type: ignore
|
||||
logger.debug("TOML 解析成功, keys: %s", list(dictdata.keys()))
|
||||
lst = list()
|
||||
nested_data = dict()
|
||||
# 修正 toml 解析器的不管嵌套行为
|
||||
for key, value in dictdata.items():
|
||||
if "__metadata__" in key: # 以免影响句号
|
||||
if "." in key:
|
||||
parts = key.split(".")
|
||||
current = nested_data
|
||||
for part in parts[:-1]:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
current[parts[-1]] = value
|
||||
logger.debug("处理元数据键: %s", key)
|
||||
else:
|
||||
nested_data[key] = value
|
||||
logger.debug("嵌套数据处理完成, keys: %s", list(nested_data.keys()))
|
||||
# print(nested_data)
|
||||
for item, attr in nested_data.items():
|
||||
if item == "__metadata__":
|
||||
continue
|
||||
logger.debug("处理项目: %s", item)
|
||||
lst.append(
|
||||
(
|
||||
Nucleon(item, attr, deepcopy(nested_data["__metadata__"])),
|
||||
deepcopy(nested_data["__metadata__"]["orbital"]),
|
||||
)
|
||||
)
|
||||
logger.debug("load_nucleon 完成, 加载了 %d 个 Nucleon 对象", len(lst))
|
||||
return lst
|
||||
|
||||
|
||||
def load_electron(path: pathlib.Path, fmt="json") -> dict:
|
||||
"""从文件路径加载电子对象
|
||||
|
||||
Args:
|
||||
path (pathlib.Path): 路径
|
||||
fmt (str): 文件格式(可选, 默认 json)
|
||||
|
||||
Returns:
|
||||
dict: 键名是电子对象名称, 值是电子对象
|
||||
"""
|
||||
logger.debug("load_electron: 加载文件 %s, 格式: %s", path, fmt)
|
||||
with open(path, "r") as f:
|
||||
dictdata = dict()
|
||||
dictdata = json.load(f) # type: ignore
|
||||
logger.debug("JSON 解析成功, keys: %s", list(dictdata.keys()))
|
||||
dic = dict()
|
||||
for item, attr in dictdata.items():
|
||||
logger.debug("处理电子项目: %s", item)
|
||||
dic[item] = Electron(item, attr)
|
||||
logger.debug("load_electron 完成, 加载了 %d 个 Electron 对象", len(dic))
|
||||
return dic
|
||||
104
src/heurams/kernel/particles/nucleon.py
Normal file
104
src/heurams/kernel/particles/nucleon.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Nucleon:
|
||||
"""原子核: 材料元数据"""
|
||||
|
||||
def __init__(self, ident: str, payload: dict, metadata: dict = {}):
|
||||
"""初始化原子核 (记忆内容)
|
||||
|
||||
Args:
|
||||
ident: 唯一标识符
|
||||
payload: 记忆内容信息
|
||||
metadata: 可选元数据信息
|
||||
"""
|
||||
logger.debug(
|
||||
"创建 Nucleon 实例, ident: '%s', payload keys: %s, metadata keys: %s",
|
||||
ident,
|
||||
list(payload.keys()) if payload else [],
|
||||
list(metadata.keys()) if metadata else [],
|
||||
)
|
||||
self.metadata = metadata
|
||||
self.payload = payload
|
||||
self.ident = ident
|
||||
logger.debug("Nucleon 初始化完成")
|
||||
|
||||
def __getitem__(self, key):
|
||||
logger.debug("Nucleon.__getitem__: key='%s'", key)
|
||||
if key == "ident":
|
||||
logger.debug("返回 ident: '%s'", self.ident)
|
||||
return self.ident
|
||||
if key in self.payload:
|
||||
value = self.payload[key]
|
||||
logger.debug(
|
||||
"返回 payload['%s'], value type: %s", key, type(value).__name__
|
||||
)
|
||||
return value
|
||||
else:
|
||||
logger.error("键 '%s' 未在 payload 中找到", key)
|
||||
raise KeyError(f"Key '{key}' not found in payload.")
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.payload.keys()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.payload)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.ident)
|
||||
|
||||
def do_eval(self):
|
||||
"""
|
||||
执行并以结果替换当前单元的所有 eval 语句
|
||||
TODO: 带有限制的 eval, 异步/多线程执行避免堵塞
|
||||
"""
|
||||
logger.debug("Nucleon.do_eval 开始")
|
||||
|
||||
# eval 环境设置
|
||||
def eval_with_env(s: str):
|
||||
try:
|
||||
nucleon = self
|
||||
eval_value = eval(s)
|
||||
if isinstance(eval_value, (int, float)):
|
||||
ret = str(eval_value)
|
||||
else:
|
||||
ret = eval_value
|
||||
logger.debug(
|
||||
"eval 执行成功: '%s' -> '%s'",
|
||||
s,
|
||||
str(ret)[:50] + "..." if len(ret) > 50 else ret,
|
||||
)
|
||||
except Exception as e:
|
||||
ret = f"此 eval 实例发生错误: {e}"
|
||||
logger.warning("eval 执行错误: '%s' -> %s", s, e)
|
||||
return ret
|
||||
|
||||
def traverse(data, modifier):
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
data[key] = traverse(value, modifier)
|
||||
return data
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
data[i] = traverse(item, modifier)
|
||||
return data
|
||||
elif isinstance(data, tuple):
|
||||
return tuple(traverse(item, modifier) for item in data)
|
||||
else:
|
||||
if isinstance(data, str):
|
||||
if data.startswith("eval:"):
|
||||
logger.debug("发现 eval 表达式: '%s'", data[5:])
|
||||
return modifier(data[5:])
|
||||
return data
|
||||
|
||||
traverse(self.payload, eval_with_env)
|
||||
traverse(self.metadata, eval_with_env)
|
||||
logger.debug("Nucleon.do_eval 完成")
|
||||
|
||||
@staticmethod
|
||||
def placeholder():
|
||||
"""生成一个占位原子核"""
|
||||
logger.debug("创建 Nucleon 占位符")
|
||||
return Nucleon("核子对象样例内容", {})
|
||||
29
src/heurams/kernel/particles/orbital.py
Normal file
29
src/heurams/kernel/particles/orbital.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import TypedDict
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger.debug("Orbital 类型定义模块已加载")
|
||||
|
||||
|
||||
class OrbitalSchedule(TypedDict):
|
||||
quick_review: list
|
||||
recognition: list
|
||||
final_review: list
|
||||
|
||||
|
||||
class Orbital(TypedDict):
|
||||
schedule: OrbitalSchedule
|
||||
puzzles: dict
|
||||
|
||||
|
||||
"""一份示例
|
||||
["__metadata__.orbital.puzzles"] # 谜题定义
|
||||
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:nucleon['content']", secondery = ["eval:nucleon['keyword_note']", "eval:nucleon['note']"], top_dim = ["eval:nucleon['translation']"] }
|
||||
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:nucleon['content']", jammer = "eval:nucleon['keyword_note']", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
|
||||
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:nucleon['content']", delimiter = "eval:metadata['formation']['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}
|
||||
|
||||
["__metadata__.orbital.schedule"] # 内置的推荐学习方案
|
||||
quick_review = [["FillBlank", "1.0"], ["SelectMeaning", "0.5"], ["recognition", "1.0"]]
|
||||
recognition = [["recognition", "1.0"]]
|
||||
final_review = [["FillBlank", "0.7"], ["SelectMeaning", "0.7"], ["recognition", "1.0"]]
|
||||
"""
|
||||
61
src/heurams/kernel/particles/probe.py
Normal file
61
src/heurams/kernel/particles/probe.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from heurams.context import config_var
|
||||
import pathlib
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def probe_by_filename(filename):
|
||||
"""探测指定文件 (无扩展名) 的所有信息"""
|
||||
logger.debug("probe_by_filename: 探测文件 '%s'", filename)
|
||||
paths: dict = config_var.get().get("paths")
|
||||
logger.debug("配置路径: %s", paths)
|
||||
formats = ["toml", "json"]
|
||||
result = {}
|
||||
for item, attr in paths.items():
|
||||
for i in formats:
|
||||
attr: pathlib.Path = pathlib.Path(attr) / filename + "." + i
|
||||
if attr.exists():
|
||||
logger.debug("找到文件: %s", attr)
|
||||
result[item.replace("_dir", "")] = str(attr)
|
||||
else:
|
||||
logger.debug("文件不存在: %s", attr)
|
||||
logger.debug("probe_by_filename 结果: %s", result)
|
||||
return result
|
||||
|
||||
|
||||
def probe_all(is_stem=1):
|
||||
"""依据目录探测所有信息
|
||||
|
||||
Args:
|
||||
is_stem (boolean): 是否**删除**文件扩展名
|
||||
|
||||
Returns:
|
||||
dict: 有三项, 每一项的键名都是文件组类型, 值都是文件组列表, 只包含文件名
|
||||
"""
|
||||
logger.debug("probe_all: 开始探测, is_stem=%d", is_stem)
|
||||
paths: dict = config_var.get().get("paths")
|
||||
logger.debug("配置路径: %s", paths)
|
||||
result = {}
|
||||
for item, attr in paths.items():
|
||||
attr: pathlib.Path = pathlib.Path(attr)
|
||||
result[item.replace("_dir", "")] = list()
|
||||
logger.debug("扫描目录: %s", attr)
|
||||
file_count = 0
|
||||
for i in attr.iterdir():
|
||||
if not i.is_dir():
|
||||
file_count += 1
|
||||
if is_stem:
|
||||
result[item.replace("_dir", "")].append(str(i.stem))
|
||||
else:
|
||||
result[item.replace("_dir", "")].append(str(i.name))
|
||||
logger.debug("目录 %s 中找到 %d 个文件", attr, file_count)
|
||||
logger.debug("probe_all 完成, 结果 keys: %s", list(result.keys()))
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
|
||||
print(os.getcwd())
|
||||
print(probe_all())
|
||||
63
src/heurams/kernel/puzzles/__init__.py
Normal file
63
src/heurams/kernel/puzzles/__init__.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Puzzle 模块 - 谜题生成系统
|
||||
|
||||
提供多种类型的谜题生成器, 支持从字符串、字典等数据源导入题目
|
||||
"""
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
from .base import BasePuzzle
|
||||
from .cloze import ClozePuzzle
|
||||
from .mcq import MCQPuzzle
|
||||
from .recognition import RecognitionPuzzle
|
||||
|
||||
__all__ = [
|
||||
"BasePuzzle",
|
||||
"ClozePuzzle",
|
||||
"MCQPuzzle",
|
||||
"RecognitionPuzzle",
|
||||
]
|
||||
|
||||
puzzles = {
|
||||
"mcq": MCQPuzzle,
|
||||
"cloze": ClozePuzzle,
|
||||
"recognition": RecognitionPuzzle,
|
||||
"base": BasePuzzle,
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_by_dict(config_dict: dict) -> BasePuzzle:
|
||||
"""
|
||||
根据配置字典创建谜题
|
||||
|
||||
Args:
|
||||
config_dict: 配置字典, 包含谜题类型和参数
|
||||
|
||||
Returns:
|
||||
BasePuzzle: 谜题实例
|
||||
|
||||
Raises:
|
||||
ValueError: 当配置无效时抛出
|
||||
"""
|
||||
logger.debug(
|
||||
"puzzles.create_by_dict: config_dict keys=%s", list(config_dict.keys())
|
||||
)
|
||||
puzzle_type = config_dict.get("type")
|
||||
|
||||
if puzzle_type == "cloze":
|
||||
return puzzles["cloze"](
|
||||
text=config_dict["text"],
|
||||
min_denominator=config_dict.get("min_denominator", 7),
|
||||
)
|
||||
elif puzzle_type == "mcq":
|
||||
return puzzles["mcq"](
|
||||
mapping=config_dict["mapping"],
|
||||
jammer=config_dict.get("jammer", []),
|
||||
max_riddles_num=config_dict.get("max_riddles_num", 2),
|
||||
prefix=config_dict.get("prefix", ""),
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"未知的谜题类型: {puzzle_type}")
|
||||
16
src/heurams/kernel/puzzles/base.py
Normal file
16
src/heurams/kernel/puzzles/base.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# base.py
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BasePuzzle:
|
||||
"""谜题基类"""
|
||||
|
||||
def refresh(self):
|
||||
logger.debug("BasePuzzle.refresh 被调用(未实现)")
|
||||
raise NotImplementedError("谜题对象未实现 refresh 方法")
|
||||
|
||||
def __str__(self):
|
||||
logger.debug("BasePuzzle.__str__ 被调用")
|
||||
return f"谜题: {type(self).__name__}"
|
||||
55
src/heurams/kernel/puzzles/cloze.py
Normal file
55
src/heurams/kernel/puzzles/cloze.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from .base import BasePuzzle
|
||||
import random
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ClozePuzzle(BasePuzzle):
|
||||
"""填空题谜题生成器
|
||||
|
||||
Args:
|
||||
text: 原始字符串(需要 delimiter 分割句子, 末尾应有 delimiter)
|
||||
min_denominator: 最小概率倒数(如占所有可生成填空数的 1/7 中的 7, 若期望值小于 1, 则取 1)
|
||||
"""
|
||||
|
||||
def __init__(self, text: str, min_denominator: int, delimiter: str = "/"):
|
||||
logger.debug(
|
||||
"ClozePuzzle.__init__: text length=%d, min_denominator=%d, delimiter='%s'",
|
||||
len(text),
|
||||
min_denominator,
|
||||
delimiter,
|
||||
)
|
||||
self.text = text
|
||||
self.min_denominator = min_denominator
|
||||
self.wording = "填空题 - 尚未刷新谜题"
|
||||
self.answer = ["填空题 - 尚未刷新谜题"]
|
||||
self.delimiter = delimiter
|
||||
logger.debug("ClozePuzzle 初始化完成")
|
||||
|
||||
def refresh(self): # 刷新谜题
|
||||
logger.debug("ClozePuzzle.refresh 开始")
|
||||
placeholder = "___SLASH___"
|
||||
tmp_text = self.text.replace(self.delimiter, placeholder)
|
||||
words = tmp_text.split(placeholder)
|
||||
if not words:
|
||||
logger.warning("ClozePuzzle.refresh: 无单词可处理")
|
||||
return
|
||||
words = [word for word in words if word]
|
||||
logger.debug("ClozePuzzle.refresh: 分割出 %d 个单词", len(words))
|
||||
num_blanks = min(max(1, len(words) // self.min_denominator), len(words))
|
||||
logger.debug("ClozePuzzle.refresh: 需要生成 %d 个填空", num_blanks)
|
||||
indices_to_blank = random.sample(range(len(words)), num_blanks)
|
||||
indices_to_blank.sort()
|
||||
blanked_words = list(words)
|
||||
answer = list()
|
||||
for index in indices_to_blank:
|
||||
blanked_words[index] = "__" * len(words[index])
|
||||
answer.append(words[index])
|
||||
self.answer = answer
|
||||
self.wording = "".join(blanked_words)
|
||||
logger.debug("ClozePuzzle.refresh 完成, 生成 %d 个填空", len(answer))
|
||||
|
||||
def __str__(self):
|
||||
logger.debug("ClozePuzzle.__str__ 被调用")
|
||||
return f"{self.wording}\n{str(self.answer)}"
|
||||
242
src/heurams/kernel/puzzles/mcq.py
Normal file
242
src/heurams/kernel/puzzles/mcq.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# mcq.py
|
||||
from .base import BasePuzzle
|
||||
import random
|
||||
from typing import List, Dict, Optional, Union
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MCQPuzzle(BasePuzzle):
|
||||
"""选择题谜题生成器
|
||||
|
||||
该类用于生成和管理选择题谜题, 支持多个题目同时生成,
|
||||
每个题目包含问题, 正确答案和干扰项选项.
|
||||
|
||||
Attributes:
|
||||
prefix (str): 题目前缀文本
|
||||
mapping (Dict[str, str]): 问题和正确答案的映射字典
|
||||
jammer (List[str]): 干扰项列表
|
||||
max_riddles_num (int): 最大题目数量限制
|
||||
wording (Union[str, List[str]]): 题目文本内容
|
||||
answer (Union[str, List[str]]): 正确答案列表
|
||||
options (List[List[str]]): 每个题目的选项列表
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mapping: Dict[str, str],
|
||||
jammer: List[str],
|
||||
max_riddles_num: int = 2,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
"""初始化选择题谜题生成器
|
||||
|
||||
Args:
|
||||
mapping: 问题和正确答案的映射字典, 键为问题, 值为正确答案
|
||||
jammer: 干扰项列表, 用于生成错误选项
|
||||
max_riddles_num: 每次生成的最大题目数量, 范围限制在1-5之间
|
||||
prefix: 题目前缀文本, 会显示在每个题目之前
|
||||
"""
|
||||
logger.debug(
|
||||
"MCQPuzzle.__init__: mapping size=%d, jammer size=%d, max_riddles_num=%d",
|
||||
len(mapping),
|
||||
len(jammer),
|
||||
max_riddles_num,
|
||||
)
|
||||
self.prefix = prefix
|
||||
self.mapping = mapping
|
||||
self.max_riddles_num = max(1, min(max_riddles_num, 5))
|
||||
|
||||
# 初始化干扰项, 确保至少有4个选项
|
||||
self._init_jammer(jammer)
|
||||
|
||||
# 初始化题目状态
|
||||
self._reset_puzzle_state()
|
||||
|
||||
def _init_jammer(self, jammer: List[str]) -> None:
|
||||
"""初始化干扰项列表
|
||||
|
||||
合并传入的干扰项和所有正确答案, 确保去重后至少有4个干扰项.
|
||||
|
||||
Args:
|
||||
jammer: 传入的干扰项列表
|
||||
"""
|
||||
# 合并正确答案和传入的干扰项, 并去重
|
||||
logger.debug(f"答案映射: {self.mapping}, {type(self.mapping)}")
|
||||
logger.debug(f"干扰项: {jammer}, {type(jammer)}")
|
||||
unique_jammers = set(jammer + list(self.mapping.values()))
|
||||
self.jammer = list(unique_jammers)
|
||||
|
||||
# 确保至少有4个干扰项
|
||||
while len(self.jammer) < 4:
|
||||
self.jammer.append(" " * (4 - len(self.jammer)))
|
||||
|
||||
unique_jammers = set(jammer + list(self.mapping.values()))
|
||||
|
||||
def _reset_puzzle_state(self) -> None:
|
||||
"""重置谜题状态为初始值
|
||||
|
||||
将题目文本, 答案和选项重置为默认状态.
|
||||
"""
|
||||
self.wording: Union[str, List[str]] = "选择题 - 尚未刷新谜题"
|
||||
self.answer: Union[str, List[str]] = ["选择题 - 尚未刷新谜题"]
|
||||
self.options: List[List[str]] = []
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""刷新谜题, 生成指定数量的选择题
|
||||
|
||||
从mapping中随机选择指定数量的问题, 为每个问题生成包含正确答案
|
||||
和干扰项的选项列表, 并更新谜题状态.
|
||||
|
||||
Raises:
|
||||
ValueError: 当mapping为空时不会抛出异常, 但会设置空谜题状态
|
||||
"""
|
||||
logger.debug("MCQPuzzle.refresh 开始, mapping size=%d", len(self.mapping))
|
||||
if not self.mapping:
|
||||
self._set_empty_puzzle()
|
||||
return
|
||||
|
||||
num_questions = min(self.max_riddles_num, len(self.mapping))
|
||||
selected_questions = random.sample(list(self.mapping.items()), num_questions)
|
||||
|
||||
puzzles: List[str] = []
|
||||
answers: List[str] = []
|
||||
all_options: List[List[str]] = []
|
||||
|
||||
for question, correct_answer in selected_questions:
|
||||
options = self._generate_options(correct_answer)
|
||||
puzzles.append(question)
|
||||
answers.append(correct_answer)
|
||||
all_options.append(options)
|
||||
|
||||
self.wording = self._format_questions(puzzles)
|
||||
self.answer = answers
|
||||
self.options = all_options
|
||||
|
||||
def _set_empty_puzzle(self) -> None:
|
||||
"""设置为空谜题状态
|
||||
|
||||
当没有可用的题目时, 设置相应的提示信息.
|
||||
"""
|
||||
self.wording = "无可用题目"
|
||||
self.answer = ["无答案"]
|
||||
self.options = []
|
||||
|
||||
def _generate_options(self, correct_answer: str) -> List[str]:
|
||||
"""为单个问题生成选项列表(包含正确答案和干扰项)
|
||||
|
||||
Args:
|
||||
correct_answer: 当前问题的正确答案
|
||||
|
||||
Returns:
|
||||
包含4个选项的列表, 其中一个是正确答案, 三个是干扰项
|
||||
|
||||
Note:
|
||||
如果可用干扰项不足3个, 会使用重复的干扰项填充
|
||||
"""
|
||||
options = [correct_answer]
|
||||
|
||||
# 获取可用的干扰项(排除正确答案)
|
||||
available_jammers = [
|
||||
jammer for jammer in self.jammer if jammer != correct_answer
|
||||
]
|
||||
|
||||
# 选择3个干扰项
|
||||
if len(available_jammers) >= 3:
|
||||
selected_jammers = random.sample(available_jammers, 3)
|
||||
else:
|
||||
selected_jammers = random.choices(available_jammers, k=3)
|
||||
|
||||
options.extend(selected_jammers)
|
||||
random.shuffle(options)
|
||||
|
||||
return options
|
||||
|
||||
def _format_questions(self, puzzles: List[str]) -> List[str]:
|
||||
"""格式化问题列表为可读的文本
|
||||
|
||||
Args:
|
||||
puzzles: 原始问题文本列表
|
||||
|
||||
Returns:
|
||||
格式化后的问题文本列表, 包含编号和前缀
|
||||
|
||||
Example:
|
||||
输入: ["问题1", "问题2"]
|
||||
输出: ["前缀:\\n 1. 问题1", "前缀:\\n 2. 问题2"]
|
||||
"""
|
||||
if not puzzles:
|
||||
return []
|
||||
|
||||
formatted_questions = []
|
||||
for i, puzzle in enumerate(puzzles, 1):
|
||||
question_text = (
|
||||
f"{self.prefix}:\n {i}. {puzzle}" if self.prefix else f"{i}. {puzzle}"
|
||||
)
|
||||
formatted_questions.append(question_text)
|
||||
|
||||
return formatted_questions
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""返回谜题的字符串表示
|
||||
|
||||
Returns:
|
||||
包含所有问题和正确答案的格式化字符串
|
||||
|
||||
Example:
|
||||
选择题 - 尚未刷新谜题
|
||||
正确答案: 选择题 - 尚未刷新谜题
|
||||
"""
|
||||
if isinstance(self.wording, list):
|
||||
question_text = "\n".join(self.wording)
|
||||
else:
|
||||
question_text = self.wording
|
||||
|
||||
if isinstance(self.answer, list):
|
||||
answer_text = ", ".join(self.answer)
|
||||
else:
|
||||
answer_text = str(self.answer)
|
||||
|
||||
return f"{question_text}\n正确答案: {answer_text}"
|
||||
|
||||
def get_question_count(self) -> int:
|
||||
"""获取当前生成的题目数量
|
||||
|
||||
Returns:
|
||||
当前题目的数量, 如果尚未刷新则返回 0
|
||||
"""
|
||||
if isinstance(self.wording, list):
|
||||
return len(self.wording)
|
||||
elif self.wording == "选择题 - 尚未刷新谜题" or self.wording == "无可用题目":
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def get_correct_answer_for_question(self, question_index: int) -> Optional[str]:
|
||||
"""获取指定题目的正确答案
|
||||
|
||||
Args:
|
||||
question_index: 题目索引(从0开始)
|
||||
|
||||
Returns:
|
||||
指定题目的正确答案, 如果索引无效则返回None
|
||||
"""
|
||||
if not isinstance(self.answer, list):
|
||||
return None
|
||||
if 0 <= question_index < len(self.answer):
|
||||
return self.answer[question_index]
|
||||
return None
|
||||
|
||||
def get_options_for_question(self, question_index: int) -> Optional[List[str]]:
|
||||
"""获取指定题目的选项列表
|
||||
|
||||
Args:
|
||||
question_index: 题目索引(从0开始)
|
||||
|
||||
Returns:
|
||||
指定题目的选项列表, 如果索引无效则返回None
|
||||
"""
|
||||
if 0 <= question_index < len(self.options):
|
||||
return self.options[question_index]
|
||||
return None
|
||||
18
src/heurams/kernel/puzzles/recognition.py
Normal file
18
src/heurams/kernel/puzzles/recognition.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# mcq.py
|
||||
from .base import BasePuzzle
|
||||
import random
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RecognitionPuzzle(BasePuzzle):
|
||||
"""识别占位符"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
logger.debug("RecognitionPuzzle.__init__")
|
||||
super().__init__()
|
||||
|
||||
def refresh(self):
|
||||
logger.debug("RecognitionPuzzle.refresh(空实现)")
|
||||
pass
|
||||
11
src/heurams/kernel/reactor/__init__.py
Normal file
11
src/heurams/kernel/reactor/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .states import PhaserState, ProcessionState
|
||||
from .procession import Procession
|
||||
from .fission import Fission
|
||||
from .phaser import Phaser
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = ["PhaserState", "ProcessionState", "Procession", "Fission", "Phaser"]
|
||||
|
||||
logger.debug("反应堆模块已加载")
|
||||
43
src/heurams/kernel/reactor/fission.py
Normal file
43
src/heurams/kernel/reactor/fission.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as puz
|
||||
import random
|
||||
from .states import PhaserState
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
|
||||
class Fission:
|
||||
"""裂变器: 单原子调度展开器"""
|
||||
|
||||
def __init__(self, atom: pt.Atom, phase=PhaserState.RECOGNITION):
|
||||
self.logger = get_logger(__name__)
|
||||
self.atom = atom
|
||||
# print(f"{phase.value}")
|
||||
self.orbital_schedule = atom.registry["orbital"]["schedule"][phase.value] # type: ignore
|
||||
self.orbital_puzzles = atom.registry["orbital"]["puzzles"]
|
||||
# print(self.orbital_schedule)
|
||||
self.puzzles = list()
|
||||
for item, possibility in self.orbital_schedule: # type: ignore
|
||||
print(f"ad:{item}")
|
||||
self.logger.debug(f"开始处理 orbital 项: {item}")
|
||||
if not isinstance(possibility, float):
|
||||
possibility = float(possibility)
|
||||
while possibility > 1:
|
||||
self.puzzles.append(
|
||||
{
|
||||
"puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]],
|
||||
"alia": item,
|
||||
}
|
||||
)
|
||||
possibility -= 1
|
||||
if random.random() <= possibility:
|
||||
self.puzzles.append(
|
||||
{
|
||||
"puzzle": puz.puzzles[self.orbital_puzzles[item]["__origin__"]],
|
||||
"alia": item,
|
||||
}
|
||||
)
|
||||
print(f"ok:{item}")
|
||||
self.logger.debug(f"orbital 项处理完成: {item}")
|
||||
|
||||
def generate(self):
|
||||
yield from self.puzzles
|
||||
50
src/heurams/kernel/reactor/phaser.py
Normal file
50
src/heurams/kernel/reactor/phaser.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# 移相器类定义
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
from .states import PhaserState, ProcessionState
|
||||
from .procession import Procession
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Phaser:
|
||||
"""移相器: 全局调度阶段管理器"""
|
||||
|
||||
def __init__(self, atoms: list[pt.Atom]) -> None:
|
||||
logger.debug("Phaser.__init__: 原子数量=%d", len(atoms))
|
||||
new_atoms = list()
|
||||
old_atoms = list()
|
||||
self.state = PhaserState.UNSURE
|
||||
for i in atoms:
|
||||
if not i.registry["electron"].is_activated():
|
||||
new_atoms.append(i)
|
||||
else:
|
||||
old_atoms.append(i)
|
||||
logger.debug("新原子数量=%d, 旧原子数量=%d", len(new_atoms), len(old_atoms))
|
||||
self.processions = list()
|
||||
if len(old_atoms):
|
||||
self.processions.append(
|
||||
Procession(old_atoms, PhaserState.QUICK_REVIEW, "初始复习")
|
||||
)
|
||||
logger.debug("创建初始复习 Procession")
|
||||
if len(new_atoms):
|
||||
self.processions.append(
|
||||
Procession(new_atoms, PhaserState.RECOGNITION, "新记忆")
|
||||
)
|
||||
logger.debug("创建新记忆 Procession")
|
||||
self.processions.append(Procession(atoms, PhaserState.FINAL_REVIEW, "总体复习"))
|
||||
logger.debug("创建总体复习 Procession")
|
||||
logger.debug("Phaser 初始化完成, processions 数量=%d", len(self.processions))
|
||||
|
||||
def current_procession(self):
|
||||
logger.debug("Phaser.current_procession 被调用")
|
||||
for i in self.processions:
|
||||
i: Procession
|
||||
if not i.state == ProcessionState.FINISHED:
|
||||
self.state = i.phase
|
||||
logger.debug("找到未完成的 Procession: phase=%s", i.phase)
|
||||
return i
|
||||
self.state = PhaserState.FINISHED
|
||||
logger.debug("所有 Procession 已完成, 状态设置为 FINISHED")
|
||||
return 0
|
||||
73
src/heurams/kernel/reactor/procession.py
Normal file
73
src/heurams/kernel/reactor/procession.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import heurams.kernel.particles as pt
|
||||
from .states import PhaserState, ProcessionState
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Procession:
|
||||
"""队列: 标识单次记忆流程"""
|
||||
|
||||
def __init__(self, atoms: list, phase: PhaserState, name: str = ""):
|
||||
logger.debug(
|
||||
"Procession.__init__: 原子数量=%d, phase=%s, name='%s'",
|
||||
len(atoms),
|
||||
phase.value,
|
||||
name,
|
||||
)
|
||||
self.atoms = atoms
|
||||
self.queue = atoms.copy()
|
||||
self.current_atom = atoms[0]
|
||||
self.cursor = 0
|
||||
self.name = name
|
||||
self.phase = phase
|
||||
self.state: ProcessionState = ProcessionState.RUNNING
|
||||
logger.debug("Procession 初始化完成, 队列长度=%d", len(self.queue))
|
||||
|
||||
def forward(self, step=1):
|
||||
logger.debug("Procession.forward: step=%d, 当前 cursor=%d", step, self.cursor)
|
||||
self.cursor += step
|
||||
if self.cursor == len(self.queue):
|
||||
self.state = ProcessionState.FINISHED
|
||||
logger.debug("Procession 已完成")
|
||||
else:
|
||||
self.state = ProcessionState.RUNNING
|
||||
try:
|
||||
logger.debug("cursor 更新为: %d", self.cursor)
|
||||
self.current_atom = self.queue[self.cursor]
|
||||
logger.debug("当前原子更新为: %s", self.current_atom.ident)
|
||||
return 1 # 成功
|
||||
except IndexError as e:
|
||||
logger.debug("IndexError: %s", e)
|
||||
self.state = ProcessionState.FINISHED
|
||||
logger.debug("Procession 因索引错误而完成")
|
||||
return 0
|
||||
|
||||
def append(self, atom=None):
|
||||
if atom == None:
|
||||
atom = self.current_atom
|
||||
logger.debug("Procession.append: atom=%s", atom.ident if atom else "None")
|
||||
if self.queue[len(self.queue) - 1] != atom or len(self) <= 1:
|
||||
self.queue.append(atom)
|
||||
logger.debug("原子已追加到队列, 新队列长度=%d", len(self.queue))
|
||||
else:
|
||||
logger.debug("原子未追加(重复或队列长度<=1)")
|
||||
|
||||
def __len__(self):
|
||||
length = len(self.queue) - self.cursor
|
||||
logger.debug("Procession.__len__: 剩余长度=%d", length)
|
||||
return length
|
||||
|
||||
def process(self):
|
||||
logger.debug("Procession.process: cursor=%d", self.cursor)
|
||||
return self.cursor
|
||||
|
||||
def total_length(self):
|
||||
total = len(self.queue)
|
||||
logger.debug("Procession.total_length: %d", total)
|
||||
return total
|
||||
|
||||
def is_empty(self):
|
||||
empty = len(self.queue)
|
||||
logger.debug("Procession.is_empty: %d", empty)
|
||||
return empty
|
||||
20
src/heurams/kernel/reactor/states.py
Normal file
20
src/heurams/kernel/reactor/states.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from enum import Enum, auto
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PhaserState(Enum):
|
||||
UNSURE = "unsure"
|
||||
QUICK_REVIEW = "quick_review"
|
||||
RECOGNITION = "recognition"
|
||||
FINAL_REVIEW = "final_review"
|
||||
FINISHED = "finished"
|
||||
|
||||
|
||||
class ProcessionState(Enum):
|
||||
RUNNING = auto()
|
||||
FINISHED = auto()
|
||||
|
||||
|
||||
logger.debug("状态枚举定义已加载")
|
||||
2
src/heurams/providers/README.md
Normal file
2
src/heurams/providers/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Provider - 提供者
|
||||
底层相关与第三方 API 接口包装
|
||||
14
src/heurams/providers/audio/__init__.py
Normal file
14
src/heurams/providers/audio/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# 音频播放器, 必须基于文件操作
|
||||
from . import termux_audio
|
||||
from . import playsound_audio
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"termux_audio",
|
||||
"playsound_audio",
|
||||
]
|
||||
|
||||
providers = {"termux": termux_audio, "playsound": playsound_audio}
|
||||
logger.debug("音频 providers 已注册: %s", list(providers.keys()))
|
||||
21
src/heurams/providers/audio/playsound_audio.py
Normal file
21
src/heurams/providers/audio/playsound_audio.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""通用音频适配器
|
||||
基于 playsound 库的音频播放器, 在绝大多数 python 环境上提供音频服务
|
||||
注意: 在未配置 pulseaudio 的 termux 不可用
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import playsound
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def play_by_path(path: pathlib.Path):
|
||||
logger.debug("playsound_audio.play_by_path: 开始播放 %s", path)
|
||||
try:
|
||||
playsound.playsound(str(path))
|
||||
logger.debug("播放完成: %s", path)
|
||||
except Exception as e:
|
||||
logger.error("播放失败: %s, 错误: %s", path, e)
|
||||
raise
|
||||
12
src/heurams/providers/audio/protocol.py
Normal file
12
src/heurams/providers/audio/protocol.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from typing import Protocol
|
||||
import pathlib
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PlayFunctionProtocol(Protocol):
|
||||
def __call__(self, path: pathlib.Path) -> None: ...
|
||||
|
||||
|
||||
logger.debug("音频协议模块已加载")
|
||||
22
src/heurams/providers/audio/termux_audio.py
Normal file
22
src/heurams/providers/audio/termux_audio.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Termux 音频适配
|
||||
适配 Termux 的 play-audio 命令, 以在 android 上提供可用的播放体验
|
||||
无需配置 pulseaudio
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# from .protocol import PlayFunctionProtocol
|
||||
|
||||
|
||||
def play_by_path(path: pathlib.Path):
|
||||
logger.debug("termux_audio.play_by_path: 开始播放 %s", path)
|
||||
try:
|
||||
os.system(f"play-audio {path}")
|
||||
logger.debug("播放命令已执行: %s", path)
|
||||
except Exception as e:
|
||||
logger.error("播放失败: %s, 错误: %s", path, e)
|
||||
raise
|
||||
6
src/heurams/providers/llm/__init__.py
Normal file
6
src/heurams/providers/llm/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# 大语言模型
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
logger.debug("LLM providers 模块已加载")
|
||||
5
src/heurams/providers/llm/base.py
Normal file
5
src/heurams/providers/llm/base.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
logger.debug("LLM 基类模块已加载")
|
||||
5
src/heurams/providers/llm/openai.py
Normal file
5
src/heurams/providers/llm/openai.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
logger.debug("OpenAI provider 模块已加载(未实现)")
|
||||
17
src/heurams/providers/tts/__init__.py
Normal file
17
src/heurams/providers/tts/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from .base import BaseTTS
|
||||
from .edge_tts import EdgeTTS
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"BaseTTS",
|
||||
"EdgeTTS",
|
||||
]
|
||||
|
||||
providers = {
|
||||
"basetts": BaseTTS,
|
||||
"edgetts": EdgeTTS,
|
||||
}
|
||||
|
||||
logger.debug("TTS providers 已注册: %s", list(providers.keys()))
|
||||
15
src/heurams/providers/tts/base.py
Normal file
15
src/heurams/providers/tts/base.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import pathlib
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BaseTTS:
|
||||
name = "BaseTTS"
|
||||
|
||||
@classmethod
|
||||
def convert(cls, text: str, path: pathlib.Path | str = "") -> pathlib.Path:
|
||||
"""path 是可选参数, 不填则自动返回生成文件路径"""
|
||||
logger.debug("BaseTTS.convert: text length=%d, path=%s", len(text), path)
|
||||
logger.warning("BaseTTS.convert 是基类方法, 未实现具体功能")
|
||||
return path # type: ignore
|
||||
26
src/heurams/providers/tts/edge_tts.py
Normal file
26
src/heurams/providers/tts/edge_tts.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from .base import BaseTTS
|
||||
import pathlib
|
||||
import edge_tts
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class EdgeTTS(BaseTTS):
|
||||
name = "EdgeTTS"
|
||||
|
||||
@classmethod
|
||||
def convert(cls, text, path: pathlib.Path | str = "") -> pathlib.Path:
|
||||
logger.debug("EdgeTTS.convert: text length=%d, path=%s", len(text), path)
|
||||
try:
|
||||
communicate = edge_tts.Communicate(
|
||||
text,
|
||||
"zh-CN-YunjianNeural",
|
||||
)
|
||||
logger.debug("EdgeTTS 通信对象创建成功, 正在保存音频")
|
||||
communicate.save_sync(str(path))
|
||||
logger.debug("EdgeTTS 音频已保存到: %s", path)
|
||||
return path # type: ignore
|
||||
except Exception as e:
|
||||
logger.error("EdgeTTS.convert 失败: %s", e)
|
||||
raise
|
||||
2
src/heurams/services/README.md
Normal file
2
src/heurams/services/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Services - 服务
|
||||
基础服务相关代码
|
||||
12
src/heurams/services/audio_service.py
Normal file
12
src/heurams/services/audio_service.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# 音频服务
|
||||
from heurams.context import config_var
|
||||
from heurams.providers.audio import providers as prov
|
||||
from typing import Callable
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
play_by_path: Callable = prov[config_var.get()["services"]["audio"]].play_by_path
|
||||
logger.debug(
|
||||
"音频服务初始化完成, 使用 provider: %s", config_var.get()["services"]["audio"]
|
||||
)
|
||||
55
src/heurams/services/config.py
Normal file
55
src/heurams/services/config.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# 配置文件服务
|
||||
import pathlib
|
||||
import toml
|
||||
import typing
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
|
||||
class ConfigFile:
|
||||
def __init__(self, path: pathlib.Path):
|
||||
self.logger = get_logger(__name__)
|
||||
self.path = path
|
||||
if not self.path.exists():
|
||||
self.path.touch()
|
||||
self.logger.debug("创建配置文件: %s", self.path)
|
||||
self.data = dict()
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""从文件加载配置数据"""
|
||||
with open(self.path, "r") as f:
|
||||
try:
|
||||
self.data = toml.load(f)
|
||||
self.logger.debug("配置文件加载成功: %s", self.path)
|
||||
except toml.TomlDecodeError as e:
|
||||
print(f"{e}")
|
||||
self.logger.error("TOML解析错误: %s", e)
|
||||
self.data = {}
|
||||
|
||||
def modify(self, key: str, value: typing.Any):
|
||||
"""修改配置值并保存"""
|
||||
self.data[key] = value
|
||||
self.logger.debug("修改配置项: %s = %s", key, value)
|
||||
self.save()
|
||||
|
||||
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:
|
||||
toml.dump(self.data, f)
|
||||
self.logger.debug("配置文件已保存: %s", save_path)
|
||||
|
||||
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
|
||||
19
src/heurams/services/hasher.py
Normal file
19
src/heurams/services/hasher.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# 哈希服务
|
||||
import hashlib
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_md5(text):
|
||||
logger.debug(f"计算哈希, 输入`{text}`")
|
||||
result = hashlib.md5(text.encode("utf-8")).hexdigest()
|
||||
logger.debug("哈希结果: %s...", result[:8])
|
||||
return result
|
||||
|
||||
|
||||
def hash(text):
|
||||
logger.debug(f"计算哈希, 输入`{text}`")
|
||||
result = hashlib.md5(text.encode("utf-8")).hexdigest()
|
||||
logger.debug("哈希结果: %s...", result[:8])
|
||||
return result
|
||||
155
src/heurams/services/logger.py
Normal file
155
src/heurams/services/logger.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
HeurAMS 日志服务模块
|
||||
基于Python标准logging库, 提供统一的日志记录功能
|
||||
"""
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Optional, Union
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_LOG_LEVEL = logging.DEBUG
|
||||
DEFAULT_LOG_FILE = pathlib.Path("heurams.log")
|
||||
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
# 全局logger缓存
|
||||
_loggers = {}
|
||||
|
||||
|
||||
def setup_logging(
|
||||
log_file: Union[str, pathlib.Path] = DEFAULT_LOG_FILE,
|
||||
log_level: int = DEFAULT_LOG_LEVEL,
|
||||
log_format: str = DEFAULT_LOG_FORMAT,
|
||||
date_format: str = DEFAULT_DATE_FORMAT,
|
||||
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
||||
backup_count: int = 5,
|
||||
) -> None:
|
||||
"""
|
||||
配置全局日志系统
|
||||
|
||||
Args:
|
||||
log_file: 日志文件路径
|
||||
log_level: 日志级别 (logging.DEBUG, logging.INFO等)
|
||||
log_format: 日志格式字符串
|
||||
date_format: 日期时间格式
|
||||
max_bytes: 单个日志文件最大字节数
|
||||
backup_count: 备份文件数量
|
||||
"""
|
||||
# 确保日志目录存在
|
||||
log_path = pathlib.Path(log_file)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 创建formatter
|
||||
formatter = logging.Formatter(log_format, date_format)
|
||||
|
||||
# 创建文件handler(使用RotatingFileHandler防止日志过大)
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
filename=log_path,
|
||||
maxBytes=max_bytes,
|
||||
backupCount=backup_count,
|
||||
encoding="utf-8",
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
file_handler.setLevel(log_level)
|
||||
|
||||
# 配置root logger - 设置为 WARNING 级别(只记录重要信息)
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.WARNING) # 这里改为 WARNING
|
||||
|
||||
# 移除所有现有handler
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
# 创建自己的应用logger(单独设置DEBUG级别)
|
||||
app_logger = logging.getLogger("heurams")
|
||||
app_logger.setLevel(log_level) # 保持DEBUG级别
|
||||
app_logger.addHandler(file_handler)
|
||||
|
||||
# 禁止传播到root logger, 避免双重记录
|
||||
app_logger.propagate = False
|
||||
|
||||
# 设置第三方库的日志级别为WARNING, 避免调试信息干扰
|
||||
third_party_loggers = [
|
||||
"markdown_it",
|
||||
"markdown_it.rules_block",
|
||||
"markdown_it.rules_core",
|
||||
"markdown_it.rules_inline",
|
||||
"asyncio",
|
||||
]
|
||||
|
||||
for logger_name in third_party_loggers:
|
||||
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
||||
|
||||
# 记录日志系统初始化
|
||||
app_logger.info("日志系统已初始化, 日志文件: %s", log_path)
|
||||
|
||||
|
||||
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
||||
"""
|
||||
获取指定名称的logger
|
||||
|
||||
Args:
|
||||
name: logger名称, 通常使用模块名(__name__)
|
||||
如果为None, 返回root logger
|
||||
|
||||
Returns:
|
||||
logging.Logger实例
|
||||
"""
|
||||
if name is None:
|
||||
return logging.getLogger()
|
||||
|
||||
# 确保使用 heurams 作为前缀, 继承应用logger的配置
|
||||
if not name.startswith("heurams") and name != "":
|
||||
logger_name = f"heurams.{name}"
|
||||
else:
|
||||
logger_name = name
|
||||
|
||||
# 缓存logger以提高性能
|
||||
if logger_name not in _loggers:
|
||||
logger = logging.getLogger(logger_name)
|
||||
_loggers[logger_name] = logger
|
||||
|
||||
return _loggers[logger_name]
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def debug(msg: str, *args, **kwargs) -> None:
|
||||
"""DEBUG级别日志"""
|
||||
get_logger().debug(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def info(msg: str, *args, **kwargs) -> None:
|
||||
"""INFO级别日志"""
|
||||
get_logger().info(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def warning(msg: str, *args, **kwargs) -> None:
|
||||
"""WARNING级别日志"""
|
||||
get_logger().warning(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def error(msg: str, *args, **kwargs) -> None:
|
||||
"""ERROR级别日志"""
|
||||
get_logger().error(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def critical(msg: str, *args, **kwargs) -> None:
|
||||
"""CRITICAL级别日志"""
|
||||
get_logger().critical(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def exception(msg: str, *args, **kwargs) -> None:
|
||||
"""记录异常信息 (ERROR级别)"""
|
||||
get_logger().exception(msg, *args, **kwargs)
|
||||
|
||||
|
||||
# 初始化日志系统(硬编码配置)
|
||||
setup_logging()
|
||||
|
||||
|
||||
# 模块级别的logger实例
|
||||
logger = get_logger(__name__)
|
||||
logger.info("HeurAMS日志服务模块已加载")
|
||||
31
src/heurams/services/timer.py
Normal file
31
src/heurams/services/timer.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# 时间服务
|
||||
from heurams.context import config_var
|
||||
import time
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_daystamp() -> int:
|
||||
"""获取当前日戳(以天为单位的整数时间戳)"""
|
||||
time_override = config_var.get().get("daystamp_override", -1)
|
||||
if time_override != -1:
|
||||
logger.debug("使用覆盖的日戳: %d", time_override)
|
||||
return int(time_override)
|
||||
|
||||
result = int((time.time() + config_var.get().get("timezone_offset")) // (24 * 3600))
|
||||
logger.debug("计算日戳: %d", result)
|
||||
return result
|
||||
|
||||
|
||||
def get_timestamp() -> float:
|
||||
"""获取 UNIX 时间戳"""
|
||||
# 搞这个类的原因是要支持可复现操作
|
||||
time_override = config_var.get().get("timestamp_override", -1)
|
||||
if time_override != -1:
|
||||
logger.debug("使用覆盖的时间戳: %f", time_override)
|
||||
return float(time_override)
|
||||
|
||||
result = time.time()
|
||||
logger.debug("获取当前时间戳: %f", result)
|
||||
return result
|
||||
12
src/heurams/services/tts_service.py
Normal file
12
src/heurams/services/tts_service.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# 文本转语音服务
|
||||
from heurams.context import config_var
|
||||
from heurams.providers.tts import TTSs
|
||||
from typing import Callable
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
convert: Callable = TTSs[config_var.get().get("tts_provider")]
|
||||
logger.debug(
|
||||
"TTS服务初始化完成, 使用 provider: %s", config_var.get().get("tts_provider")
|
||||
)
|
||||
10
src/heurams/services/version.py
Normal file
10
src/heurams/services/version.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# 版本控制集成服务
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
ver = "0.4.0"
|
||||
stage = "prototype"
|
||||
codename = "fledge" # 雏鸟, 0.4.x 版本
|
||||
|
||||
logger.info("HeurAMS 版本: %s (%s), 阶段: %s", ver, codename, stage)
|
||||
2
src/heurams/utils/README.md
Normal file
2
src/heurams/utils/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Utils - 实用工具
|
||||
脚本与部分分离式工具函数
|
||||
Reference in New Issue
Block a user