refactor: 开始翻新状态机

This commit is contained in:
2026-04-21 12:52:30 +08:00
parent bcdfddce10
commit 9dd6733063
19 changed files with 111 additions and 135 deletions

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,3 +0,0 @@
title = "测试单元: 过秦论"
author = "__heurams__"
desc = "高考古诗文: 过秦论"

View File

@@ -1,11 +0,0 @@
["秦孝公据崤函之固, 拥雍州之地,"]
note = []
content = "秦孝公/据/崤函/之固/, 拥/雍州/之地,/"
translation = "秦孝公占据着崤山和函谷关的险固地势,拥有雍州的土地,"
keyword_note = {"据"="占据", "崤函"="崤山和函谷关", "雍州"="古代九州之一"}
["君臣固守以窥周室,"]
note = []
content = "君臣/固守/以窥/周室,/"
translation = "君臣牢固地守卫着,借以窥视周王室的权力,"
keyword_note = {"窥"="窥视"}

View File

@@ -1,5 +0,0 @@
schedule = ["quick_review", "recognition", "final_review"]
[phases]
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"]]

View File

@@ -1,17 +0,0 @@
[annotation]
note = "笔记"
keyword_note = "关键词翻译"
translation = "语句翻译"
delimiter = "分隔符"
content = "内容"
tts_text = "文本转语音文本"
[common]
delimiter = "/"
tts_text = "eval:payload['content'].replace('/', '')"
[common.puzzles] # 谜题定义
# 我们称 "Recognition" 为 recognition 谜题的 alia
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:payload['content']", secondary = ["eval:payload['keyword_note']", "eval:payload['note']"], top_dim = ["eval:payload['translation']"] }
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:payload['content']", primary = "eval:payload['content']", mapping = "eval:payload['keyword_note']", jammer = "eval:list(payload['keyword_note'].values())", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:payload['content']", delimiter = "eval:nucleon['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}

View File

@@ -0,0 +1,4 @@
#puzzle_container > * {
height: auto;
width: auto;
}

View File

@@ -5,14 +5,14 @@ from pathlib import Path
from typing import Callable
from textual.app import ComposeResult
from textual.containers import Center, ScrollableContainer
from textual.containers import Center, ScrollableContainer, Container
from textual.reactive import reactive
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, Static
import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz
from heurams.context import config_var
from heurams.context import config_var, rootdir
from heurams.kernel.reactor import *
from heurams.services.favorite_service import favorite_manager
from heurams.services.logger import get_logger
@@ -28,8 +28,13 @@ class MemScreen(Screen):
("d", "toggle_dark", ""),
("v", "play_voice", "朗读"),
("*", "toggle_favorite", "收藏"),
("n", "block_prompt"),
("s", "block_prompt"),
("z", "block_prompt"),
]
CSS_PATH = rootdir / 'interface' / 'css' / 'screens' / 'memoqueue.tcss'
if config_var.get()["interface"]["global"]["quick_pass"]:
BINDINGS.append(("k", "quick_pass", "正确应答"))
BINDINGS.append(("f", "quick_fail", "错误应答"))
@@ -38,17 +43,17 @@ class MemScreen(Screen):
def __init__(
self,
phaser: Phaser,
router: Router,
repo=None,
name=None,
id=None,
classes=None,
) -> None:
super().__init__(name, id, classes)
self.phaser = phaser
self.router = router
self.repo = repo
self.update_state()
self.fission: Fission
self.expander: Expander
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
@@ -59,17 +64,17 @@ class MemScreen(Screen):
def update_state(self):
"""更新状态机"""
self.procession: Procession = self.phaser.current_procession() # type: ignore
self.procession: Procession = self.router.current_procession() # type: ignore
self.atom: pt.Atom = self.procession.current_atom # type: ignore
def on_mount(self):
self.fission = self.procession.get_fission()
self.expander = self.procession.get_expander()
self.mount_puzzle()
self.update_display()
def puzzle_widget(self):
try:
puzzle = self.fission.get_current_puzzle_inf()
puzzle = self.expander.get_current_puzzle_inf()
return shim.puzzle2widget[puzzle["puzzle"]]( # type: ignore
atom=self.atom, alia=puzzle["alia"] # type: ignore
)
@@ -93,7 +98,7 @@ class MemScreen(Screen):
def mount_puzzle(self):
"""挂载当前谜题组件"""
if self.procession.phase == PhaserState.FINISHED:
if self.procession.phase == RouterState.FINISHED:
self.mount_finished_widget()
return
container = self.query_one("#puzzle_container")
@@ -139,10 +144,10 @@ class MemScreen(Screen):
if new_rating == -1: # 安全值
return
self.update_state()
if self.procession.phase == PhaserState.FINISHED:
if self.procession.phase == RouterState.FINISHED:
rating = -1
return
self.fission.report(new_rating)
self.expander.report(new_rating)
self.forward(new_rating)
self.rating = -1
@@ -150,9 +155,9 @@ class MemScreen(Screen):
self.update_state()
allow_forward = 1 if rating >= 4 else 0
if allow_forward:
self.fission.forward()
if self.fission.state == "retronly":
self.forward_atom(self.fission.get_quality())
self.expander.forward()
if self.expander.state == "retronly":
self.forward_atom(self.expander.get_quality())
self.update_state()
self.mount_puzzle()
self.update_display()
@@ -177,7 +182,7 @@ class MemScreen(Screen):
self.update_state() # 刷新状态
self.procession.forward(1)
self.update_state() # 刷新状态
self.fission = self.procession.get_fission()
self.expander = self.procession.get_expander()
def action_go_back(self):
self.app.pop_screen()
@@ -224,3 +229,6 @@ class MemScreen(Screen):
self.app.notify(f"已收藏:{ident}", severity="information")
# 更新显示(如果需要)
self.update_display()
def action_block_prompt(self):
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", severity="error")

View File

@@ -181,6 +181,6 @@ def launch(repo, app, scheduled_num):
from .memoqueue import MemScreen
pheser = rt.Phaser(atoms_to_provide)
memscreen = MemScreen(pheser, repo=repo)
router = rt.Router(atoms_to_provide)
memscreen = MemScreen(router=router, repo=repo)
app.push_screen(memscreen)

View File

@@ -2,7 +2,7 @@ import copy
import random
from typing import TypedDict
from textual.containers import Container
from textual.containers import Container, ScrollableContainer
from textual.message import Message
from textual.widget import Widget
from textual.widgets import Button, Label, Markdown
@@ -68,7 +68,7 @@ class ClozePuzzle(BasePuzzleWidget):
yield Label(self.puzzle.wording, id="sentence")
yield Markdown(f"> {self.listprint(self.inputlist)}", id="inputpreview")
# 渲染当前问题的选项
with Container(id="btn-container"):
with ScrollableContainer(id="btn-container"):
for i in self.ans:
h = str(hash(i))
self.hashmap[h] = i

View File

@@ -28,7 +28,7 @@ class Finished(Widget):
def compose(self):
yield Label("本次记忆进程结束", id="finished_msg")
yield Label(f"算法数据{'已保存' if self.is_saved else "未能保存"}")
yield Button("返回上一级", id="back-to-menu")
yield Button("返回上一级", flat=True, id="back-to-menu")
def on_button_pressed(self, event):
button_id = event.button.id

View File

@@ -75,7 +75,7 @@ class MCQPuzzle(BasePuzzleWidget):
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
# 渲染当前问题的选项
with Container(id="btn-container"):
with ScrollableContainer(id="btn-container"):
for i in current_options:
self.hashmap[str(hash(i))] = i
btnid = f"sel{str(self.cursor).zfill(3)}-{hash(i)}"

View File

@@ -3,9 +3,9 @@
Reactor 是 HeurAMS 的记忆流程状态机模块, 和界面 (interface) 的实现是解耦的, 以便后期与其他框架的适配.\
得益于 Pickle, 状态机模块支持快照!
## Phaser - 全局阶段控制器
## Router - 全局阶段控制器
在一次队列记忆流程中, Phaser 代表记忆流程本身.
在一次队列记忆流程中, Router 代表记忆流程本身.
### 属性
@@ -25,7 +25,7 @@ Reactor 是 HeurAMS 的记忆流程状态机模块, 和界面 (interface) 的实
在初始化 Procession 时, 每个 Procession 被赋予一个不重复的状态属性 作为"阶段状态"属性, 以此标识 Procession 的阶段属性, 因为每个 Procession 管理一个阶段下的复习进程.
你可以用 state 属性获取 Phaser 的当前状态.
你可以用 state 属性获取 Router 的当前状态.
#### Procession 属性
@@ -34,26 +34,26 @@ Reactor 是 HeurAMS 的记忆流程状态机模块, 和界面 (interface) 的实
### 初始化
Phaser 接受一个存储 Atom 对象的列表, 作为组织记忆流程的材料\
Router 接受一个存储 Atom 对象的列表, 作为组织记忆流程的材料\
在内部, 根据是否激活将其分为 new_atoms 与 old_atoms.\
因此, 如果你传入的列表中有算法上"无所事事"的 Atom, 流程会对其进行"加强复习"
由此创建 Procession.
### 直接输出呈现形式
Phaser 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
Router 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
其以 ascii 表格形式输出, 格式也符合 markdown 表格规范, 你可以直接复制到 markdown.\
示例:
```text
| Type | State | Processions | Current Procession |
|:-------|:--------|:-----------------------|:---------------------|
| Phaser | unsure | ['新记忆', '总体复习'] | 新记忆 |
| Type | State | Processions | Current Procession |
| :----- | :----- | :--------------------- | :----------------- |
| Router | unsure | ['新记忆', '总体复习'] | 新记忆 |
```
| Type | State | Processions | Current Procession |
|:-------|:--------|:-----------------------|:---------------------|
| Phaser | unsure | ['新记忆', '总体复习'] | 新记忆 |
| Type | State | Processions | Current Procession |
| :----- | :----- | :--------------------- | :----------------- |
| Router | unsure | ['新记忆', '总体复习'] | 新记忆 |
### 方法
@@ -85,29 +85,29 @@ Phaser 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
- cursor: 指针, 是当前原子在 atoms 列表中的索引
- phase: "阶段属性"
> 注意区分 "Phaser" 和 "Phase", 其中 "Phase" 表示 "Phaser State".
> 注意区分 "Router" 和 "Phase", 其中 "Phase" 表示 "Router State".
- name\_: 阶段的命名
- state: 当前状态属性
### 初始化
接受一个 atoms 列表与 phase_state (PhaserState Enum 类型)对象
接受一个 atoms 列表与 phase_state (RouterState Enum 类型)对象
### 直接输出呈现形式
Phaser, 但显示数据有所不同\
Phaser 不同, Procession 显示队列会对过长的 atom.ident 进行缩略(末尾 `>` 符号)
Router, 但显示数据有所不同\
Router 不同, Procession 显示队列会对过长的 atom.ident 进行缩略(末尾 `>` 符号)
```text
| Type | Name | State | Progress | Queue | Current Atom |
|:-----------|:-------|:--------|:-----------|:-----------------------|:------------------------------|
| Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, |
| Type | Name | State | Progress | Queue | Current Atom |
| :--------- | :----- | :----- | :------- | :--------------------- | :---------------------------- |
| Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, |
```
| Type | Name | State | Progress | Queue | Current Atom |
|:-----------|:-------|:--------|:-----------|:-----------------------|:------------------------------|
| Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, |
| Type | Name | State | Progress | Queue | Current Atom |
| :--------- | :----- | :----- | :------- | :--------------------- | :---------------------------- |
| Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, |
### 方法
@@ -142,11 +142,11 @@ Phaser 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
判断是否为空队列(传入原子列表对象是空列表的队列)
#### get_fission(self)
#### get_expander(self)
获取当前原子的 Fission 对象, 用于单原子调度展开
获取当前原子的 Expander 对象, 用于单原子调度展开
## Fission - 单原子调度控制器
## Expander - 单原子调度控制器
### 属性

View File

@@ -1,8 +1,8 @@
from heurams.services.logger import get_logger
from .fission import Fission
from .phaser import Phaser
from .expander import Expander
from .router import Router
from .procession import Procession
from .states import PhaserState, ProcessionState
from .states import RouterState, ProcessionState
__all__ = ["PhaserState", "ProcessionState", "Procession", "Fission", "Phaser"]
__all__ = ["RouterState", "ProcessionState", "Procession", "Expander", "Router"]

View File

@@ -8,39 +8,39 @@ import heurams.kernel.particles as pt
import heurams.kernel.puzzles as puz
from heurams.services.logger import get_logger
from .states import FissionState, PhaserState
from .states import ExpanderState, RouterState
logger = get_logger(__name__)
class Fission(Machine):
class Expander(Machine):
"""单原子调度展开器"""
def __init__(self, atom: pt.Atom, phase=PhaserState.RECOGNITION):
def __init__(self, atom: pt.Atom, phase=RouterState.RECOGNITION):
self.phase = phase
self.cursor = 0
self.atom = atom
self.current_puzzle_inf: dict
# phase 为 PhaserState 枚举实例, 需要获取其value
# phase 为 RouterState 枚举实例, 需要获取其value
phase_value = phase.value
states = [
{"name": FissionState.EXAMMODE.value},
{"name": FissionState.RETRONLY.value},
{"name": ExpanderState.EXAMMODE.value},
{"name": ExpanderState.RETRONLY.value},
]
transitions = [
{
"trigger": "finish",
"source": FissionState.EXAMMODE.value,
"dest": FissionState.RETRONLY.value,
"source": ExpanderState.EXAMMODE.value,
"dest": ExpanderState.RETRONLY.value,
},
]
if phase == PhaserState.FINISHED:
if phase == RouterState.FINISHED:
Machine.__init__(
self,
states=states,
transitions=transitions,
initial=FissionState.EXAMMODE.value,
initial=ExpanderState.EXAMMODE.value,
)
return
orbital_schedule = atom.registry["orbital"]["phases"][phase_value] # type: ignore
@@ -81,7 +81,7 @@ class Fission(Machine):
self,
states=states,
transitions=transitions,
initial=FissionState.EXAMMODE.value,
initial=ExpanderState.EXAMMODE.value,
)
def get_puzzles_inf(self):
@@ -118,7 +118,7 @@ class Fission(Machine):
dic = [
{
"Type": "Fission",
"Type": "Expander",
"Atom": truncate(self.atom.ident),
"State": self.state,
"Progress": f"{self.cursor + 1} / {len(self.puzzles_inf)}",

View File

@@ -4,8 +4,8 @@ from transitions import Machine
import heurams.kernel.particles as pt
from heurams.services.logger import get_logger
from .fission import Fission
from .states import PhaserState, ProcessionState
from .expander import Expander
from .states import RouterState, ProcessionState
logger = get_logger(__name__)
@@ -13,7 +13,7 @@ logger = get_logger(__name__)
class Procession(Machine):
"""队列: 标识单次记忆流程"""
def __init__(self, atoms: list, phase_state: PhaserState, name_: str = ""):
def __init__(self, atoms: list, phase_state: RouterState, name_: str = ""):
logger.debug(
"Procession.__init__: 原子数量=%d, phase=%s, name='%s'",
len(atoms),
@@ -113,8 +113,8 @@ class Procession(Machine):
logger.debug("Procession.is_empty: %s", empty)
return empty
def get_fission(self):
return Fission(atom=self.current_atom, phase=self.phase) # type: ignore
def get_expander(self):
return Expander(atom=self.current_atom, phase=self.phase) # type: ignore
def __repr__(self, style="pipe", ends="\n"):
from heurams.services.textproc import truncate

View File

@@ -5,16 +5,16 @@ from heurams.kernel.particles.placeholders import AtomPlaceholder
from heurams.services.logger import get_logger
from .procession import Procession
from .states import PhaserState, ProcessionState
from .states import RouterState, ProcessionState
logger = get_logger(__name__)
class Phaser(Machine):
class Router(Machine):
"""全局调度阶段管理器"""
def __init__(self, atoms: list[pt.Atom]) -> None:
logger.debug("Phaser.__init__: 原子数量=%d", len(atoms))
logger.debug("Router.__init__: 原子数量=%d", len(atoms))
self.atoms = atoms
new_atoms = list()
@@ -32,50 +32,50 @@ class Phaser(Machine):
# TODO: 改进为基于配置文件的可选复习阶段
if len(old_atoms):
self.processions.append(
Procession(old_atoms, PhaserState.QUICK_REVIEW, "初始复习")
Procession(old_atoms, RouterState.QUICK_REVIEW, "初始复习")
)
logger.debug("创建初始复习 Procession")
if len(new_atoms):
self.processions.append(
Procession(new_atoms, PhaserState.RECOGNITION, "新记忆")
Procession(new_atoms, RouterState.RECOGNITION, "新记忆")
)
logger.debug("创建新记忆 Procession")
self.processions.append(Procession(atoms, PhaserState.FINAL_REVIEW, "总体复习"))
self.processions.append(Procession(atoms, RouterState.FINAL_REVIEW, "总体复习"))
logger.debug("创建总体复习 Procession")
logger.debug("Phaser 初始化完成, processions 数量=%d", len(self.processions))
logger.debug("Router 初始化完成, processions 数量=%d", len(self.processions))
# 设置transitions状态机
states = [
{"name": PhaserState.UNSURE.value, "on_enter": "on_unsure"},
{"name": PhaserState.QUICK_REVIEW.value, "on_enter": "on_quick_review"},
{"name": PhaserState.RECOGNITION.value, "on_enter": "on_recognition"},
{"name": PhaserState.FINAL_REVIEW.value, "on_enter": "on_final_review"},
{"name": PhaserState.FINISHED.value, "on_enter": "on_finished"},
{"name": RouterState.UNSURE.value, "on_enter": "on_unsure"},
{"name": RouterState.QUICK_REVIEW.value, "on_enter": "on_quick_review"},
{"name": RouterState.RECOGNITION.value, "on_enter": "on_recognition"},
{"name": RouterState.FINAL_REVIEW.value, "on_enter": "on_final_review"},
{"name": RouterState.FINISHED.value, "on_enter": "on_finished"},
]
transitions = [
{"trigger": "to_unsure", "source": "*", "dest": PhaserState.UNSURE.value},
{"trigger": "to_unsure", "source": "*", "dest": RouterState.UNSURE.value},
{
"trigger": "to_quick_review",
"source": "*",
"dest": PhaserState.QUICK_REVIEW.value,
"dest": RouterState.QUICK_REVIEW.value,
},
{
"trigger": "to_recognition",
"source": "*",
"dest": PhaserState.RECOGNITION.value,
"dest": RouterState.RECOGNITION.value,
},
{
"trigger": "to_final_review",
"source": "*",
"dest": PhaserState.FINAL_REVIEW.value,
"dest": RouterState.FINAL_REVIEW.value,
},
{
"trigger": "to_finished",
"source": "*",
"dest": PhaserState.FINISHED.value,
"dest": RouterState.FINISHED.value,
},
]
@@ -83,45 +83,45 @@ class Phaser(Machine):
self,
states=states,
transitions=transitions,
initial=PhaserState.UNSURE.value,
initial=RouterState.UNSURE.value,
)
self.to_unsure()
def on_unsure(self):
"""进入UNSURE状态时的回调"""
logger.debug("Phaser 进入 UNSURE 状态")
logger.debug("Router 进入 UNSURE 状态")
def on_quick_review(self):
"""进入QUICK_REVIEW状态时的回调"""
logger.debug("Phaser 进入 QUICK_REVIEW 状态")
logger.debug("Router 进入 QUICK_REVIEW 状态")
def on_recognition(self):
"""进入RECOGNITION状态时的回调"""
logger.debug("Phaser 进入 RECOGNITION 状态")
logger.debug("Router 进入 RECOGNITION 状态")
def on_final_review(self):
"""进入FINAL_REVIEW状态时的回调"""
logger.debug("Phaser 进入 FINAL_REVIEW 状态")
logger.debug("Router 进入 FINAL_REVIEW 状态")
def on_finished(self):
"""进入FINISHED状态时的回调"""
for i in self.atoms:
i.lock(1)
i.revise()
logger.debug("Phaser 进入 FINISHED 状态")
logger.debug("Router 进入 FINISHED 状态")
def current_procession(self):
logger.debug("Phaser.current_procession 被调用")
logger.debug("Router.current_procession 被调用")
for i in self.processions:
i: Procession
if i.state != ProcessionState.FINISHED.value:
# if i.phase == PhaserState.UNSURE: 此判断是不必要的 因为没有这种 Procession
if i.phase == PhaserState.QUICK_REVIEW:
# if i.phase == RouterState.UNSURE: 此判断是不必要的 因为没有这种 Procession
if i.phase == RouterState.QUICK_REVIEW:
self.to_quick_review()
elif i.phase == PhaserState.RECOGNITION:
elif i.phase == RouterState.RECOGNITION:
self.to_recognition()
elif i.phase == PhaserState.FINAL_REVIEW:
elif i.phase == RouterState.FINAL_REVIEW:
self.to_final_review()
logger.debug("找到未完成的 Procession: phase=%s", i.phase)
@@ -130,7 +130,7 @@ class Phaser(Machine):
# 所有Procession都已完成
self.to_finished()
logger.debug("所有 Procession 已完成, 状态设置为 FINISHED")
return Procession([AtomPlaceholder()], PhaserState.FINISHED)
return Procession([AtomPlaceholder()], RouterState.FINISHED)
def __repr__(self, style="pipe", ends="\n"):
from tabulate import tabulate as tabu
@@ -139,7 +139,7 @@ class Phaser(Machine):
lst = [
{
"Type": "Phaser",
"Type": "Router",
"State": self.state,
"Processions": list(map(lambda f: (f.name_), self.processions)),
"Current Procession": "None" if not self.current_procession() else self.current_procession().name_, # type: ignore

View File

@@ -5,7 +5,7 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__)
class PhaserState(Enum):
class RouterState(Enum):
UNSURE = "unsure"
QUICK_REVIEW = "quick_review"
RECOGNITION = "recognition"
@@ -18,7 +18,7 @@ class ProcessionState(Enum):
FINISHED = "finished"
class FissionState(Enum):
class ExpanderState(Enum):
EXAMMODE = "exammode"
RETRONLY = "retronly"

View File

@@ -0,0 +1 @@
"""会话记录模块"""