diff --git a/auxiliary.py b/auxiliary.py index 9894bd3..015b31f 100644 --- a/auxiliary.py +++ b/auxiliary.py @@ -1,33 +1,46 @@ import time import pathlib import toml +import typing -class ConfigFile(): - def __init__(self, path): +class ConfigFile: + def __init__(self, path: str): self.path = pathlib.Path(path) - if self.path.exists() == 0: + if not self.path.exists(): self.path.touch() self.data = dict() + self._load() + + def _load(self): + """从文件加载配置数据""" with open(self.path, 'r') as f: - self.data = toml.load(f) - def modify(self, key, value): + try: + self.data = toml.load(f) + except toml.TomlDecodeError: + self.data = {} + + def modify(self, key: str, value: typing.Any): + """修改配置值并保存""" self.data[key] = value self.save() - def save(self, path=""): - if path == "": - path = self.path - with open(path, 'w') as f: + + 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) - def get(self, key, default = None): + + def get(self, key: str, default: typing.Any = None) -> typing.Any: + """获取配置值,如果不存在返回默认值""" return self.data.get(key, default) + def get_daystamp() -> int: + """获取当前日戳(以天为单位的整数时间戳)""" config = ConfigFile("config.toml") - time_override = config.get("time_override", -1) - if time_override is not None and time_override != -1: - #print(f"TIME OVERRIDEED TO {time_override}") + if time_override != -1: return int(time_override) return int(time.time() // (24 * 3600)) \ No newline at end of file diff --git a/compositions.py b/compositions.py index 46301da..90884f9 100644 --- a/compositions.py +++ b/compositions.py @@ -1,6 +1,16 @@ from textual.app import App, ComposeResult from textual.events import Event -from textual.widgets import Collapsible, Header, Footer, Markdown, ListView, ListItem, Label, Static, Button +from textual.widgets import ( + Collapsible, + Header, + Footer, + Markdown, + ListView, + ListItem, + Label, + Static, + Button, +) from textual.containers import Container, Horizontal, Center from textual.screen import Screen from textual.widget import Widget @@ -12,56 +22,70 @@ import re import random import copy -class Composition(): - def __init__(self, screen: Screen, reactor, atom: Tuple[pt.Electron, pt.Nucleon, Dict] = pt.Atom.placeholder()): + +class Composition: + def __init__( + self, + screen: Screen, + reactor, + atom: Tuple[pt.Electron, pt.Nucleon, Dict] = pt.Atom.placeholder(), + ): self.screen = screen - self.atom = atom + self.atom = atom from reactor import Reactor + self.reactor: Reactor = reactor self.reg = dict() + def regid(self, id_): self.reg[id_] = id_ + str(uuid.uuid4()) return self.reg[id_] + def getid(self, id_): if id_ not in self.reg.keys(): return "None" return self.reg[id_] + def recid(self, id_): return id_[:-36] + def compose(self): yield Label("示例标签", id="testlabel") yield Button("示例按钮", id="testbtn") + def handler(self, event, type_): return 1 - #if hasattr(event, "button"): - #print(event.button.id) - # self.screen.query_one("#testlabel", Label).update("hi") + class Finished(Composition): def __init__(self, screen: Screen, reactor, atom: Tuple[pt.Electron, pt.Nucleon, Dict]): super().__init__(screen, reactor, atom) + def compose(self): yield Label("本次记忆进程结束", id=self.regid("msg")) - #yield Button("示例按钮", id="testbtn") + class Placeholder(Composition): def __init__(self, screen: Screen): self.screen = screen + def compose(self): yield Label("示例标签", id="testlabel") yield Button("示例按钮", id="testbtn", classes="choice") + def handler(self, event, type_): - #print(event.button.id) self.screen.query_one("#testlabel", Label).update("hi") + class Recognition(Composition): def __init__(self, screen: Screen, reactor, atom: Tuple[pt.Electron, pt.Nucleon, Dict]): super().__init__(screen, reactor, atom) + def compose(self): with Center(): yield Static(f"[dim]{self.atom[1]['translation']}[/]") yield Label(f"") - s = str(self.atom[1]['content']) + s = str(self.atom[1]["content"]) replace_dict = { ", ": ",", ". ": ".", @@ -75,132 +99,153 @@ class Recognition(Composition): } for old, new in replace_dict.items(): s = s.replace(old, new) - result = re.split(r"(?<=[,;:|])", s.replace('/', ' ')) + result = re.split(r"(?<=[,;:|])", s.replace("/", " ")) for i in result: with Center(): - yield Label(f"[b][b]{i.replace("/", " ")}[/][/]", id=self.regid("sentence"+str(hash(i)))) # 致敬传奇去重串 uuid - #with Collapsible(title="附加信息", collapsed=True): + yield Label( + f"[b][b]{i.replace('/', ' ')}[/][/]", + id=self.regid("sentence" + str(hash(i))), + ) for i in self.atom[2]["testdata"]["additional_inf"]: if self.atom[1][i]: - #print(type(self.atom[1][i])) - #print(self.atom[1][i]) if isinstance(self.atom[1][i], list): for j in self.atom[1][i]: yield Markdown(f"### {self.atom[2]['keydata'][i]}: {j}") continue if isinstance(self.atom[1][i], Dict): t = "" - for j, k in self.atom[1][i].items(): + for j, k in self.atom[1][i].items(): # type: ignore + # 弱智的 Pylance 类型推导 t += f"> **{j}**: {k} \n" yield Markdown(t, id=self.regid("tran")) with Center(): yield Button("我已知晓", id=self.regid("ok")) + def handler(self, event, type_): - ##print(event) if type_ == "button": - #print(event.button.id) if event.button.id == self.getid("ok"): - #print(1) - """"""#assessment = self.reactor.report(self.reactor.current_atom, -1) - return 0 + return 0 if type_ == 1: pass return -1 + class BasicEvaluation(Composition): - # 不再使用, 仅作为测试 def __init__(self, screen: Screen, reactor, atom: Tuple[pt.Electron, pt.Nucleon, Dict]): super().__init__(screen, reactor, atom) + def compose(self): yield Label(self.atom[1]["content"], id="sentence") with Container(id="button_container"): btn = {} - btn['5'] = Button("完美回想", variant="success", id=self.regid("feedback5"), classes="choice") - btn['4'] = Button("犹豫后正确", variant="success", id=self.regid("feedback4"), classes="choice") - btn['3'] = Button("困难地正确", variant="warning", id=self.regid("feedback3"), classes="choice") - btn['2'] = Button("错误但熟悉", variant="warning", id=self.regid("feedback2"), classes="choice") - btn['1'] = Button("错误且不熟", variant="error", id=self.regid("feedback1"), classes="choice") - btn['0'] = Button("完全空白", variant="error", id=self.regid("feedback0"), classes="choice") - yield Horizontal(btn['5'], btn['4']) - yield Horizontal(btn['3'], btn['2']) - yield Horizontal(btn['1'], btn['0']) + btn["5"] = Button( + "完美回想", variant="success", id=self.regid("feedback5"), classes="choice" + ) + btn["4"] = Button( + "犹豫后正确", variant="success", id=self.regid("feedback4"), classes="choice" + ) + btn["3"] = Button( + "困难地正确", variant="warning", id=self.regid("feedback3"), classes="choice" + ) + btn["2"] = Button( + "错误但熟悉", variant="warning", id=self.regid("feedback2"), classes="choice" + ) + btn["1"] = Button( + "错误且不熟", variant="error", id=self.regid("feedback1"), classes="choice" + ) + btn["0"] = Button( + "完全空白", variant="error", id=self.regid("feedback0"), classes="choice" + ) + yield Horizontal(btn["5"], btn["4"]) + yield Horizontal(btn["3"], btn["2"]) + yield Horizontal(btn["1"], btn["0"]) + def handler(self, event, type_): if "feedback" in event.button.id: - #print(self.recid(event.button.id)[8:9]) assess = int(self.recid(event.button.id)[8:9]) ret = self.reactor.report(self.atom, assess) return ret + class FillBlank(Composition): def __init__(self, screen: Screen, reactor, atom: Tuple[pt.Electron, pt.Nucleon, Dict]): super().__init__(screen, reactor, atom) self.inputlist = [] self.hashtable = {} self._work() + def _work(self): self.puzzle = pz.BlankPuzzle(self.atom[1]["content"], 4) self.puzzle.refresh() self.ans = copy.copy(self.puzzle.answer) random.shuffle(self.ans) + def compose(self): yield Label(self.puzzle.wording, id=self.regid("sentence")) yield Label(f"当前输入: {self.inputlist}", id=self.regid("inputpreview")) - #yield Label(renderable=f"答案: {self.puzzle.answer}", id=self.regid("ans")) for i in self.ans: self.hashtable[str(hash(i))] = i yield Button(i, id=self.regid(f"select{hash(i)}")) yield Button("退格", id=self.regid(f"delete")) + def handler(self, event, type_): if type_ == "button": - if self.recid(event.button.id) == "delete" and len(self.inputlist) > 0: - self.inputlist.pop() + if self.recid(event.button.id) == "delete": + if len(self.inputlist) > 0: + self.inputlist.pop() + else: + return 1 else: self.inputlist.append(self.hashtable[self.recid(event.button.id)[6:]]) if len(self.inputlist) < len(self.puzzle.answer): return 1 else: if self.inputlist == self.puzzle.answer: - print("ok") return 0 else: self.inputlist = [] return 1 + class DrawCard(Composition): def __init__(self, screen: Screen, reactor, atom: Tuple[pt.Electron, pt.Nucleon, Dict]): super().__init__(screen, reactor, atom) self.inputlist = [] self.hashtable = {} self._work() + def _work(self): - self.puzzle = pz.SelectionPuzzle(self.atom[1]["keyword_note"], [], 2, "选择正确词义: ") + self.puzzle = pz.SelectionPuzzle(self.atom[1]["keyword_note"], [], 2, "选择正确词义: ") # type: ignore self.puzzle.refresh() + def compose(self): - print(len(self.inputlist)) - yield Label(self.atom[1].content, id=self.regid("sentence")) + yield Label(self.atom[1].content.replace("/",""), id=self.regid("sentence")) yield Label(self.puzzle.wording[len(self.inputlist)], id=self.regid("puzzle")) yield Label(f"当前输入: {self.inputlist}", id=self.regid("inputpreview")) - yield Label(renderable=f"答案: {self.puzzle.answer}", id=self.regid("ans")) for i in self.puzzle.options[len(self.inputlist)]: self.hashtable[str(hash(i))] = i yield Button(i, id=self.regid(f"select{hash(i)}")) yield Button("退格", id=self.regid(f"delete")) + def handler(self, event, type_): if type_ == "button": - if self.recid(event.button.id) == "delete" and len(self.inputlist) > 0: - self.inputlist.pop() + if self.recid(event.button.id) == "delete": + if len(self.inputlist) > 0: + self.inputlist.pop() + else: + return 1 else: self.inputlist.append(self.hashtable[self.recid(event.button.id)[6:]]) if len(self.inputlist) < len(self.puzzle.answer): return 1 else: if self.inputlist == self.puzzle.answer: - print("ok") return 0 else: self.inputlist = [] return 1 + registry = { "sample": Composition, "recognition": Recognition, @@ -210,16 +255,16 @@ registry = { } -# TEST - class TestScreen(Screen): def __init__(self): super().__init__(name=None, id=None, classes=None) - self.comp = Recognition(self, None, pt.Atom.advanced_placeholder()) + self.comp = Recognition(self, None, pt.Atom.advanced_placeholder()) + def compose(self) -> ComposeResult: yield Header(show_clock=True) yield from self.comp.compose() yield Footer() + def on_mount(self) -> None: pass @@ -229,9 +274,10 @@ class TestScreen(Screen): def action_quit_app(self) -> None: self.app.exit() + class AppLauncher(App): CSS_PATH = "styles.tcss" - TITLE = '测试布局' + TITLE = "测试布局" BINDINGS = [("escape", "quit", "退出"), ("d", "toggle_dark", "改变色调")] SCREENS = { "testscreen": TestScreen, @@ -241,6 +287,7 @@ class AppLauncher(App): self.action_toggle_dark() self.push_screen("testscreen") + if __name__ == "__main__": app = AppLauncher() - app.run() + app.run() \ No newline at end of file diff --git a/electron/陈情表.toml b/electron/陈情表.toml new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py index e80271d..c59be7e 100644 --- a/main.py +++ b/main.py @@ -1,35 +1,43 @@ from textual.app import App, ComposeResult -from textual.widgets import Header, Footer, ListView, ProgressBar, DirectoryTree, ListItem, Label, Static, Button +from textual.widgets import ( + Header, + Footer, + ListView, + ProgressBar, + DirectoryTree, + ListItem, + Label, + Static, + Button, +) from textual.containers import Container, Horizontal, Center from textual.screen import Screen import pathlib import threading import edge_tts as tts from playsound import playsound - import particles as pt from reactor import Reactor, Apparatus import auxiliary as aux import compositions as compo - import builtins -# 保存原始的 open 函数 _original_open = builtins.open -# 定义新的 open 函数,默认使用 UTF-8 + def _open(*args, **kwargs): - if 'encoding' not in kwargs: - kwargs['encoding'] = 'utf-8' + if "encoding" not in kwargs: + kwargs["encoding"] = "utf-8" return _original_open(*args, **kwargs) -# 替换全局的 open + builtins.open = _open -ver = '0.3.0b' +ver = "0.3.0b" config = aux.ConfigFile("config.toml") + class MemScreen(Screen): BINDINGS = [ ("d", "toggle_dark", "改变色调"), @@ -49,38 +57,30 @@ class MemScreen(Screen): ("/", "press('q0')", None), ] btn = dict() + def __init__( self, nucleon_file: pt.NucleonUnion, electron_file: pt.ElectronUnion, - tasked_num + tasked_num, ): super().__init__(name=None, id=None, classes=None) self.reactor = Reactor(nucleon_file, electron_file, self, tasked_num) self.stage = 1 self.stage += self.reactor.set_round_templated(self.stage) - ##print(self.reactor.procession) self.reactor.forward() - #self.compo:compo.Composition = compo.Placeholder(self) self.compo = next(self.reactor.current_appar) - def compose(self) -> ComposeResult: - #print(self.compo) yield Header(show_clock=True) with Center(): - yield Static(f"{len(self.reactor.procession) - self.reactor.index}/{len(self.reactor.procession)}") - #yield ProgressBar(total=len(self.reactor.procession) - 1, show_percentage=False, show_eta=False, id="progress") + yield Static( + f"{len(self.reactor.procession) - self.reactor.index}/{len(self.reactor.procession)}" + ) yield from self.compo.compose() yield Footer() - """ - def _get_progress_text(self): - return f"{len(self.reactor.procession) - self.reactor.index}/{len(self.reactor.procession)}" - """ - def on_mount(self): - # 首次挂载时调用 pass def on_button_pressed(self, event): @@ -90,13 +90,12 @@ class MemScreen(Screen): def _forward_judge(self, ret): if ret == -1: return - if ret == 0: # 成功 + if ret == 0: try: self.compo = next(self.reactor.current_appar) self.refresh_ui() except StopIteration: nxt = self.reactor.forward(1) - #print(2) try: self.compo = next(self.reactor.current_appar) except: @@ -106,13 +105,13 @@ class MemScreen(Screen): if self.stage == 4: if config.get("save"): self.reactor.save() - self.compo = compo.Finished(self, None, pt.Atom.placeholder()) + self.compo = compo.Finished( + self, None, pt.Atom.placeholder() + ) self.refresh_ui() - #self._show_finished_screen("今日目标已完成") else: self.reactor.set_round_templated(self.stage) self.reactor.forward(1) - #self._update_ui() self.stage += 1 self.compo = next(self.reactor.current_appar) self.refresh_ui() @@ -121,71 +120,47 @@ class MemScreen(Screen): else: self.refresh_ui() return - if ret == 1: # 不允许前进 + if ret == 1: self.refresh_ui() return def refresh_ui(self): self.call_later(self.recompose) - #self.call_later(lambda: self.query_one("#progress", expect_type=ProgressBar).advance(self.reactor.index)) - ##print(area.children) - #for child in list(area.children): - # child.remove() # 致敬传奇组件树 DOM - ##print(1,list(self.compo.compose())) - #area.mount(*list(self.compo.compose())) def report(self, quality): assessment = self.reactor.report(self.reactor.current_atom, quality) return assessment - """if assessment == 1: - # 需要复习 - feedback_label.update(f"评分为 {quality}, 已经加入至复习, 请重复记忆") - else: - ret = self.reactor.forward(1) - if ret == -1: - if self.reactor.round_set == 0: - if self.stage == 4: - # NOTE # - if config.get("save"): - self.reactor.save() - self._show_finished_screen("今日目标已完成") - else: - self.reactor.set_round_templated(self.stage) - self.reactor.forward(1) - self._update_ui() - self.stage += 1 - return - #feedback_label.update("") # 清除反馈消息 - self._update_ui()""" - #def action_press(self, btnid): - # self.on_button_pressed(btnid) - def action_play_voice(self): def play(): cache_dir = pathlib.Path(f"./cache/voice/") - cache_dir.mkdir(parents = True, exist_ok = True) - cache = cache_dir / f"{self.reactor.current_atom[1].content.replace("/","")}.wav" + cache_dir.mkdir(parents=True, exist_ok=True) + cache = cache_dir / f"{self.reactor.current_atom[1].content.replace('/','')}.wav" if not cache.exists(): - communicate = tts.Communicate(self.reactor.current_atom[1].content.replace("/",""), "zh-CN-YunjianNeural") - communicate.save_sync(f"./cache/voice/{self.reactor.current_atom[1].content.replace("/","")}.wav") + communicate = tts.Communicate( + self.reactor.current_atom[1].content.replace("/", ""), + "zh-CN-YunjianNeural", + ) + communicate.save_sync( + f"./cache/voice/{self.reactor.current_atom[1].content.replace('/','')}.wav" + ) playsound(str(cache)) + threading.Thread(target=play).start() def action_toggle_dark(self): self.app.action_toggle_dark() def action_pop_screen(self): - """返回到上一个屏幕""" self.app.pop_screen() -class PreparationScreen(Screen): - BINDINGS = [ - ("q", "go_back", "返回"), - ("escape", "quit_app", "退出") - ] - def __init__(self, nucleon_file: pt.NucleonUnion, electron_file: pt.ElectronUnion) -> None: +class PreparationScreen(Screen): + BINDINGS = [("q", "go_back", "返回"), ("escape", "quit_app", "退出")] + + def __init__( + self, nucleon_file: pt.NucleonUnion, electron_file: pt.ElectronUnion + ) -> None: super().__init__(name=None, id=None, classes=None) self.nucleon_file = nucleon_file self.electron_file = electron_file @@ -194,10 +169,19 @@ class PreparationScreen(Screen): yield Header(show_clock=True) with Container(id="learning_screen_container"): yield Label(f"记忆项目: [b]{self.nucleon_file.name}[/b]\n") - yield Label(f"核子文件对象: ./nucleon/[b]{self.nucleon_file.name}[/b].toml") - yield Label(f"电子文件对象: ./electron/[b]{self.electron_file.name}[/b].toml") + yield Label( + f"核子文件对象: ./nucleon/[b]{self.nucleon_file.name}[/b].toml" + ) + yield Label( + f"电子文件对象: ./electron/[b]{self.electron_file.name}[/b].toml" + ) yield Label(f"核子数量:{len(self.nucleon_file)}") - yield Button("开始记忆", id="start_memorizing_button", variant="primary", classes="start-button") + yield Button( + "开始记忆", + id="start_memorizing_button", + variant="primary", + classes="start-button", + ) yield Static(f"\n全文如下:\n") yield Static(self._get_full_content(), classes="full") yield Footer() @@ -205,7 +189,7 @@ class PreparationScreen(Screen): def _get_full_content(self): content = "" for i in self.nucleon_file.nucleons: - content += i['content'] + content += i["content"] return content def action_go_back(self): @@ -215,76 +199,79 @@ class PreparationScreen(Screen): self.app.exit() def on_button_pressed(self, event: Button.Pressed) -> None: - pass if event.button.id == "start_memorizing_button": - #init_file(Path(self.atom_file).name) - newscr = MemScreen(self.nucleon_file, self.electron_file, config.get("tasked_number", 8)) - self.app.push_screen( - newscr + newscr = MemScreen( + self.nucleon_file, self.electron_file, config.get("tasked_number", 8) ) - #if event.button.id == "edit_metadata_button": - # init_file(Path(self.atom_file).name) - # os.system("reset;nano ./data/" + str(Path(self.atom_file).name.replace(".txt", "_atoms.json"))) + self.app.push_screen(newscr) + class FileSelectorScreen(Screen): global ver + def compose(self) -> ComposeResult: yield Header(show_clock=True) yield Container( Label(f'欢迎使用 "潜进" 辅助记忆软件, 版本 {ver}', classes="title-label"), Label("选择要学习的文件:", classes="title-label"), - ListView(id="file-list", classes="file-list-view") + ListView(id="file-list", classes="file-list-view"), ) yield Footer() def on_mount(self) -> None: file_list_widget = self.query_one("#file-list", ListView) nucleon_path = pathlib.Path("./nucleon") - nucleon_files = sorted([f.name for f in nucleon_path.iterdir() if f.suffix == ".toml"]) + nucleon_files = sorted( + [f.name for f in nucleon_path.iterdir() if f.suffix == ".toml"] + ) if nucleon_files: for filename in nucleon_files: file_list_widget.append(ListItem(Label(filename))) else: - file_list_widget.append(ListItem(Static("在 ./nucleon/ 中未找到任何核子文件. 请放置文件后重启应用."))) + file_list_widget.append( + ListItem(Static("在 ./nucleon/ 中未找到任何核子文件. 请放置文件后重启应用.")) + ) file_list_widget.disabled = True def on_list_view_selected(self, event: ListView.Selected) -> None: if not isinstance(event.item, ListItem): - self.notify("无法选择此项。", severity="error") return selected_label = event.item.query_one(Label) if "未找到任何 .toml 文件" in str(selected_label.renderable): - self.notify("请先在 `./atoms/` 目录中放置 .toml 文件。", severity="warning") return selected_filename = str(selected_label.renderable) - nucleon_file = pt.NucleonUnion(pathlib.Path("./nucleon") / selected_filename) + nucleon_file = pt.NucleonUnion( + pathlib.Path("./nucleon") / selected_filename + ) electron_file_path = pathlib.Path("./electron") / selected_filename if electron_file_path.exists(): pass else: electron_file_path.touch() - electron_file = pt.ElectronUnion(pathlib.Path("./electron") / selected_filename) - # self.notify(f"已选择: {selected_filename}", timeout=2) + electron_file = pt.ElectronUnion( + pathlib.Path("./electron") / selected_filename + ) self.app.push_screen(PreparationScreen(nucleon_file, electron_file)) def action_quit_app(self) -> None: self.app.exit() + class AppLauncher(App): CSS_PATH = "styles.tcss" - TITLE = '潜进 - 辅助记忆程序' + TITLE = "潜进 - 辅助记忆程序" BINDINGS = [("escape", "quit", "退出"), ("d", "toggle_dark", "改变色调")] SCREENS = { "file_selection_screen": FileSelectorScreen, } def on_mount(self) -> None: - #self.action_toggle_dark() self.push_screen("file_selection_screen") + if __name__ == "__main__": app = AppLauncher() - app.run() + app.run() \ No newline at end of file diff --git a/particles.py b/particles.py index 4e7bb50..7c0d1bf 100644 --- a/particles.py +++ b/particles.py @@ -3,37 +3,36 @@ import toml import time import auxiliary as aux -import time -class Electron(): +class Electron: """电子: 记忆分析元数据及算法""" - algorithm = "SM-2" # 暂时使用 SM-2 算法进行记忆拟合, 考虑 SM-15 替代 + algorithm = "SM-2" # 暂时使用 SM-2 算法进行记忆拟合, 考虑 SM-15 替代 def __init__(self, content: str, metadata: dict): self.content = content self.metadata = metadata if metadata == {}: - #print("NULL") + # print("NULL") self._default_init() - + def _default_init(self): defaults = { - 'efactor': 2.5, # 易度系数, 越大越简单, 最大为5 - 'real_rept': 0, # (实际)重复次数 - 'rept': 0, # (有效)重复次数 - 'interval': 0, # 最佳间隔 - 'last_date': 0, # 上一次复习的时间戳 - 'next_date': 0, # 将要复习的时间戳 - 'is_activated': 0, # 激活状态 + 'efactor': 2.5, # 易度系数, 越大越简单, 最大为5 + 'real_rept': 0, # (实际)重复次数 + 'rept': 0, # (有效)重复次数 + 'interval': 0, # 最佳间隔 + 'last_date': 0, # 上一次复习的时间戳 + 'next_date': 0, # 将要复习的时间戳 + 'is_activated': 0, # 激活状态 # *NOTE: 此处"时间戳"是以天为单位的整数, 即 UNIX 时间戳除以一天的秒数取整 - 'last_modify': time.time() # 最后修改时间戳(此处是UNIX时间戳) + 'last_modify': time.time() # 最后修改时间戳(此处是UNIX时间戳) } self.metadata = defaults - + def activate(self): self.metadata['is_activated'] = 1 self.metadata['last_modify'] = time.time() - + def modify(self, var: str, value): if var in self.metadata: self.metadata[var] = value @@ -41,7 +40,7 @@ class Electron(): else: print(f"警告: '{var}' 非已知元数据字段") - def revisor(self, quality: int = 5, is_new_activation: bool = False): + def revisor(self, quality: int = 5, is_new_activation: bool = False): """SM-2 算法迭代决策机制实现 根据 quality(0 ~ 5) 进行参数迭代最佳间隔 quality 由主程序评估 @@ -51,41 +50,47 @@ class Electron(): """ if quality == -1: return -1 - - self.metadata['efactor'] = self.metadata['efactor'] + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)) + + self.metadata['efactor'] = self.metadata['efactor'] + ( + 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02) + ) self.metadata['efactor'] = max(1.3, self.metadata['efactor']) if quality < 3: # 若保留率低于 3,重置重复次数 - self.metadata['rept'] = 0 - self.metadata['interval'] = 0 # 设为0,以便下面重新计算 I(1) + self.metadata['rept'] = 0 + self.metadata['interval'] = 0 # 设为0,以便下面重新计算 I(1) else: self.metadata['rept'] += 1 - + self.metadata['real_rept'] += 1 - if is_new_activation: # 初次激活 + if is_new_activation: # 初次激活 self.metadata['rept'] = 0 self.metadata['efactor'] = 2.5 - if self.metadata['rept'] == 0: # 刚被重置或初次激活后复习 - self.metadata['interval'] = 1 # I(1) + if self.metadata['rept'] == 0: # 刚被重置或初次激活后复习 + self.metadata['interval'] = 1 # I(1) elif self.metadata['rept'] == 1: - self.metadata['interval'] = 6 # I(2) 经验公式 + self.metadata['interval'] = 6 # I(2) 经验公式 else: - self.metadata['interval'] = round(self.metadata['interval'] * self.metadata['efactor']) + self.metadata['interval'] = round( + self.metadata['interval'] * self.metadata['efactor'] + ) self.metadata['last_date'] = aux.get_daystamp() self.metadata['next_date'] = aux.get_daystamp() + self.metadata['interval'] self.metadata['last_modify'] = time.time() def __str__(self): - return (f"记忆单元预览 \n" - f"内容: '{self.content}' \n" - f"易度系数: {self.metadata['efactor']:.2f} \n" - f"已经重复的次数: {self.metadata['rept']} \n" - f"下次间隔: {self.metadata['interval']} 天 \n" - f"下次复习日期时间戳: {self.metadata['next_date']}") + return ( + f"记忆单元预览 \n" + f"内容: '{self.content}' \n" + f"易度系数: {self.metadata['efactor']:.2f} \n" + f"已经重复的次数: {self.metadata['rept']} \n" + f"下次间隔: {self.metadata['interval']} 天 \n" + f"下次复习日期时间戳: {self.metadata['next_date']}" + ) def __eq__(self, other): if self.content == other.content: @@ -94,7 +99,7 @@ class Electron(): def __hash__(self): return hash(self.content) - + def __getitem__(self, key): if key == "content": return self.content @@ -102,14 +107,10 @@ class Electron(): return self.metadata[key] else: raise KeyError(f"Key '{key}' not found in metadata.") - + def __setitem__(self, key, value): if key == "content": raise AttributeError("content 应为只读") - - # 可以在此处添加更复杂的验证逻辑,例如只允许修改预定义的 metadata 键 - # 或者根据键进行类型检查等。 - self.metadata[key] = value self.metadata['last_modify'] = time.time() @@ -118,14 +119,15 @@ class Electron(): def __len__(self): return len(self.metadata) - + @staticmethod def placeholder(): return Electron("电子对象样例内容", {}) + class Nucleon: """核子: 材料元数据""" - + def __init__(self, content: str, data: dict): self.metadata = data self.content = content @@ -151,7 +153,8 @@ class Nucleon: def placeholder(): return Nucleon("核子对象样例内容", {}) -class NucleonUnion(): + +class NucleonUnion: """ 替代原有 NucleonFile 类, 支持复杂逻辑 @@ -166,6 +169,7 @@ class NucleonUnion(): Parameters: path (Path): 包含核子数据的文件路径。 """ + def __init__(self, path): self.path = path self.name = path.name.replace(path.suffix, "") @@ -189,8 +193,10 @@ class NucleonUnion(): tmp = {i.content: i.metadata for i in self.nucleons} toml.dump(tmp, f) -class ElectronUnion(): - "取代原有 ElectronFile 类, 以支持复杂逻辑" + +class ElectronUnion: + """取代原有 ElectronFile 类, 以支持复杂逻辑""" + def __init__(self, path): self.path = path self.name = path.name.replace(path.suffix, "") @@ -201,19 +207,21 @@ class ElectronUnion(): lst.append(Electron(i, all[i])) self.electrons = lst self.electrons_dict = {i.content: i for i in lst} - + def sync(self): """同步 electrons_dict 中新增对到 electrons 中""" self.electrons = self.electrons_dict.values() def save(self): - #print(1) + # print(1) with open(self.path, 'w') as f: tmp = {i.content: i.metadata for i in self.electrons} - #print(tmp) + # print(tmp) toml.dump(tmp, f) -"""class AtomicFile(): + +""" +class AtomicFile(): def __init__(self, path, type_="unknown"): self.path = path self.type_ = type_ @@ -237,30 +245,42 @@ class ElectronUnion(): return "" def get_len(self): return len(self.datalist) - """ -class Atom(): + +class Atom: @staticmethod def placeholder(): return (Electron.placeholder(), Nucleon.placeholder(), {}) - + @staticmethod def advanced_placeholder(): return ( Electron("两只黄鹤鸣翠柳", {}), - Nucleon("两只黄鹤鸣翠柳", {"note": [], - "translation": "臣子李密陈言:我因命运不好,小时候遭遇到了不幸", - "keyword_note": {"险衅":"凶险祸患(这里指命运不好)", "夙":"早时,这里指年幼的时候", "闵":"通'悯',指可忧患的事", "凶":"不幸,指丧父"}}), + Nucleon( + "两只黄鹤鸣翠柳", + { + "note": [], + "translation": "臣子李密陈言:我因命运不好,小时候遭遇到了不幸", + "keyword_note": { + "险衅": "凶险祸患(这里指命运不好)", + "夙": "早时,这里指年幼的时候", + "闵": "通'悯',指可忧患的事", + "凶": "不幸,指丧父" + } + } + ), { - "keydata":{ + "keydata": { "note": "笔记", "keyword_note": "关键词翻译", - "translation": "语句翻译"}, - "testdata":{ + "translation": "语句翻译" + }, + "testdata": { "additional_inf": ["translation", "note", "keyword_note"], "fill_blank_test": ["translation"], "draw_card_test": ["keyword_note"] }, "is_new_activation": 0 - }) \ No newline at end of file + } + ) \ No newline at end of file diff --git a/precaching.py b/precaching.py index 9b94f6d..921da5f 100644 --- a/precaching.py +++ b/precaching.py @@ -4,40 +4,49 @@ import edge_tts as tts from pathlib import Path import shutil -def precache(text): - cache_dir = Path(f"./cache/voice/") - cache_dir.mkdir(parents = True, exist_ok = True) + +def precache(text: str): + """预缓存单个文本的音频""" + cache_dir = Path("./cache/voice/") + cache_dir.mkdir(parents=True, exist_ok=True) cache = cache_dir / f"{text}.wav" if not cache.exists(): communicate = tts.Communicate(text, "zh-CN-YunjianNeural") communicate.save_sync(f"./cache/voice/{text}.wav") -def proc_file(path): + +def proc_file(path: Path): + """处理单个文件""" nu = pt.NucleonUnion(path) c = 0 for i in nu.nucleons: c += 1 - print(f"预缓存 [{nu.name}] ({c}/{len(nu)}): {i["content"]}") - precache(f"{i["content"]}") + print(f"预缓存 [{nu.name}] ({c}/{len(nu)}): {i['content']}") + precache(i['content']) -def walk(path_str): + +def walk(path_str: str): + """遍历目录处理所有文件""" path = Path(path_str) - print(f"正在遍历目录: {path}") + for item in path.iterdir(): - if item.is_file(): - if item.suffix == ".toml": - print(f"正预缓存文件: {item.name}") - proc_file(item) + if item.is_file() and item.suffix == ".toml": + print(f"正预缓存文件: {item.name}") + proc_file(item) elif item.is_dir(): print(f"进入目录: {item.name}") -print("音频预缓存实用程序") -print("需要?") -print("全部缓存: A") -print("清空缓存: C") -choice = input("输入选项 $ ") -if choice == "a" or choice == "A": - walk("./nucleon") -if choice == "c" or choice == "C": - shutil.rmtree("./cache/voice") \ No newline at end of file + +if __name__ == "__main__": + print("音频预缓存实用程序") + print("A: 全部缓存") + print("C: 清空缓存") + + choice = input("输入选项 $ ").upper() + + if choice == "A": + walk("./nucleon") + elif choice == "C": + shutil.rmtree("./cache/voice", ignore_errors=True) + print("缓存已清空") \ No newline at end of file diff --git a/puzzles.py b/puzzles.py index 2d96c90..0cb7b8f 100644 --- a/puzzles.py +++ b/puzzles.py @@ -1,8 +1,10 @@ import random -class Puzzle(): + +class Puzzle: pass + class BlankPuzzle(Puzzle): """填空题谜题生成器 @@ -10,20 +12,21 @@ class BlankPuzzle(Puzzle): text: 原始字符串(需要 "/" 分割句子, 末尾应有 "/") min_denominator: 最小概率倒数(如占所有可生成填空数的 1/7 中的 7, 若期望值小于 1, 则取 1) """ - def __init__(self, text, min_denominator): + + def __init__(self, text: str, min_denominator: int): self.text = text self.min_denominator = min_denominator self.wording = "填空题 - 尚未刷新谜题" self.answer = ["填空题 - 尚未刷新谜题"] - def refresh(self): # 刷新谜题 - placeholder = "___SLASH___" + def refresh(self): # 刷新谜题 + placeholder = "___SLASH___" tmp_text = self.text.replace("/", placeholder) words = tmp_text.split(placeholder) if not words: - return "" - words = [word for word in words if word] - num_blanks = min(max(1, len(words) // self.min_denominator), len(words)) + return + words = [word for word in words if word] + num_blanks = min(max(1, len(words) // self.min_denominator), len(words)) indices_to_blank = random.sample(range(len(words)), num_blanks) indices_to_blank.sort() blanked_words = list(words) @@ -31,15 +34,13 @@ class BlankPuzzle(Puzzle): for index in indices_to_blank: blanked_words[index] = "__" * len(words[index]) answer.append(words[index]) - result = [] - for word in blanked_words: - result.append(word) self.answer = answer - self.wording = "".join(result) - + self.wording = "".join(blanked_words) + def __str__(self): return f"{self.wording}\n{str(self.answer)}" + class SelectionPuzzle(Puzzle): """选择题谜题生成器 @@ -49,12 +50,20 @@ class SelectionPuzzle(Puzzle): max_riddles_num: 最大生成谜题数 (默认2个) prefix: 问题前缀 """ - def __init__(self, mapping, jammer: list, max_riddles_num: int = 2, prefix: str = ""): - jammer += ["1","2","3","4"] + + def __init__( + self, + mapping: dict, + jammer: list, + max_riddles_num: int = 2, + prefix: str = "" + ): self.prefix = prefix self.mapping = mapping - self.jammer = list(set(jammer + list(mapping.values()))) # 合并干扰项和正确答案并去重 - self.max_riddles_num = max(1, min(max_riddles_num, 5)) # 限制1-5个谜题 + self.jammer = list(set(jammer + list(mapping.values()))) + while len(self.jammer) < 4: + self.jammer.append(" ") + self.max_riddles_num = max(1, min(max_riddles_num, 5)) self.wording = "选择题 - 尚未刷新谜题" self.answer = ["选择题 - 尚未刷新谜题"] self.options = [] @@ -66,48 +75,46 @@ class SelectionPuzzle(Puzzle): self.answer = ["无答案"] self.options = [] return - - # 确定实际生成的谜题数量 + num_questions = min(self.max_riddles_num, len(self.mapping)) questions = random.sample(list(self.mapping.items()), num_questions) - - # 生成谜题 puzzles = [] answers = [] all_options = [] for question, correct_answer in questions: - # 生成选项 (正确答案 + 3个干扰项) options = [correct_answer] - available_jammers = [j for j in self.jammer if j != correct_answer] - + available_jammers = [ + j for j in self.jammer if j != correct_answer + ] if len(available_jammers) >= 3: selected_jammers = random.sample(available_jammers, 3) else: selected_jammers = random.choices(available_jammers, k=3) - options.extend(selected_jammers) random.shuffle(options) - puzzles.append(question) answers.append(correct_answer) all_options.append(options) - + question_texts = [] - for i, (puzzle, options) in enumerate(zip(puzzles, all_options)): - #options_text = "\n".join([f" {chr(97+j)}. {opt}" for j, opt in enumerate(options)]) + for i, puzzle in enumerate(puzzles): question_texts.append(f"{self.prefix}:\n {i+1}. {puzzle}") self.wording = question_texts self.answer = answers self.options = all_options - + def __str__(self): return f"{self.wording}\n正确答案: {', '.join(self.answer)}" - -puz = SelectionPuzzle({"1+1":"2", "1+2":"3", "1+3": "4"}, ["2","5","0"], 3, '求值: ') +puz = SelectionPuzzle( + {"1+1": "2", "1+2": "3", "1+3": "4"}, + ["2", "5", "0"], + 3, + '求值: ' +) puz.refresh() print(puz.wording) print(puz.answer)