16 Commits

Author SHA1 Message Date
39459a0f6e 更新版本号 2025-08-21 13:30:59 +08:00
cccf7189e3 自动播放机制 2025-08-21 13:30:30 +08:00
2c51f2cea3 改动 2025-08-21 06:28:22 +08:00
2ad014fcd8 更新文件树 2025-08-16 07:33:42 +08:00
4ad289d02d 改进部署组件 2025-08-14 11:16:43 +08:00
28ccfdd227 删除运行时文件 2025-08-14 11:13:11 +08:00
f83d5c934d 增加若干元数据 2025-08-14 11:12:33 +08:00
4f9eb3b7d1 重命名文件 2025-08-12 19:10:14 +08:00
c44a38f3c8 更新自述文件 2025-08-09 22:44:02 +08:00
f760e7f0fa 预缓存实用程序改动 2025-08-09 08:41:40 +08:00
30eb45e1cb 更新自述文件 2025-08-06 07:52:42 +08:00
2a30f136cb 更新自述文件 2025-08-06 07:52:15 +08:00
051c4847b2 更新自述文件 2025-08-06 07:43:36 +08:00
0873caa5fc 若干善后改进 2025-08-06 07:42:43 +08:00
6d3d2e665c 实装自动评分系统 2025-08-06 06:46:30 +08:00
edf2f0868a 改进 2025-08-06 06:30:41 +08:00
79 changed files with 116 additions and 99 deletions

2
.playsound.py Normal file
View File

@@ -0,0 +1,2 @@
def playsound(p):
print(p)

View File

@@ -0,0 +1,7 @@
# 贡献指南
## 使用 Nuitka 静态编译
运行
```bash
nuitka --clang --jobs=6 --standalone --onefile main.py
```

View File

@@ -1,10 +1,9 @@
# 潜进 (HeurAMS) - 实验型辅助记忆程序
# 潜进 (HeurAMS) - 启发式辅助记忆程序
> 形人而我无形,**则我专而敌分**
## 概述
"潜进" (HeurAMS, 中文含义: 启发式辅助记忆软件) 是为习题册, 古诗词, 及其他问答/记忆/理解型题目设计的记忆辅助软件, 提供优化记忆方案
"潜进" (HeurAMS) 是为习题册, 古诗词, 及其他问答/记忆/理解型知识设计的辅助记忆软件, 提供动态规划的优化记忆方案
## 技术集成与特性
@@ -13,10 +12,10 @@
- 采用经实证的 SM-2 间隔迭代算法, 此算法亦用作 Anki 闪卡记忆软件的默认闪卡调度器
> 计划: 将添加 FSRS 算法 (Anki 的新可选闪卡调度器) 与一种 SM-15 变体算法作为后续替代
> 参考 https://github.com/slaypni/SM-15
> 为什么使用 SM-15 的变体?
> SM-2 后续算法仅有论文, 无具体方程, 故使用一种基于 SM-15 描述实现的变体算法
- 动态优化每首诗词的记忆间隔时间表
- 实时跟踪记忆曲线,优化长期记忆保留率与稳定性
> 使用 SM-15 的变体:
> SM-2 后续算法并非完全开放, 故使用一种基于 SM-15 描述实现的变体算法
- 动态规划每个记忆单元的记忆间隔时间表
- 动态跟踪记忆反馈数据,优化长期记忆保留率与稳定性
### 学习进程优化
- 逐字解析:支持逐字详细释义解析
@@ -30,12 +29,19 @@
- 简洁直观的复习流程设计
## 屏幕截图
![scrshot2](readme_src/image_2.png)
![scrshot1](readme_src/image_1.png)
> 单击图片以放大
<img src="./readme_src/img1.png" alt="img1" style="zoom: 33%;" />
<img src="./readme_src/img2.png" alt="img2" style="zoom:33%;" />
<img src="./readme_src/img3.png" alt="img3" style="zoom:33%;" />
<img src="./readme_src/img4.png" alt="img4" style="zoom:33%;" />
## 技术架构
> 有关技术与实现的细节, 请参阅 CONTRIBUTING.md
> 提交拉取请求以参与到此开放源代码项目
``` mermaid
graph TD
subgraph 后端
@@ -63,9 +69,3 @@ graph TD
- 平台支持Windows / macOS / Linux / Android (需要 Termux 或 Linux) (终端或浏览器)
- 网络连接:可预缓存语音文件, 需联网使用大模型服务功能
## 使用 Nuitka 静态编译
运行
```bash
nuitka --clang --jobs=6 --standalone --onefile main.py
```

View File

@@ -2,6 +2,9 @@ import time
import pathlib
import toml
import typing
import playsound
import threading
import edge_tts as tts
class ConfigFile:
def __init__(self, path: str):
@@ -34,6 +37,17 @@ class ConfigFile:
"""获取配置值,如果不存在返回默认值"""
return self.data.get(key, default)
def action_play_voice(content):
def play():
communicate = tts.Communicate(
content,
"zh-CN-YunjianNeural",
)
communicate.save_sync(
f"./cache/voice/{content}"
)
playsound()
threading.Thread(target=play).start()
def get_daystamp() -> int:
"""获取当前日戳(以天为单位的整数时间戳)"""

View File

@@ -124,9 +124,8 @@ class Recognition(Composition):
def handler(self, event, type_):
if type_ == "button":
if event.button.id == self.getid("ok"):
self.reactor.report(self.atom, 5)
return 0
if type_ == 1:
pass
return -1
@@ -189,6 +188,7 @@ class FillBlank(Composition):
yield Button("退格", id=self.regid(f"delete"))
def handler(self, event, type_):
# TODO: 改动:在线错误纠正
if type_ == "button":
if self.recid(event.button.id) == "delete":
if len(self.inputlist) > 0:
@@ -201,9 +201,11 @@ class FillBlank(Composition):
return 1
else:
if self.inputlist == self.puzzle.answer:
self.reactor.report(self.atom, 4)
return 0
else:
self.inputlist = []
self.reactor.report(self.atom, 2)
return 1
@@ -240,9 +242,11 @@ class DrawCard(Composition):
return 1
else:
if self.inputlist == self.puzzle.answer:
self.reactor.report(self.atom, 4)
return 0
else:
self.inputlist = []
self.reactor.report(self.atom, 2)
return 1
@@ -276,7 +280,7 @@ class TestScreen(Screen):
class AppLauncher(App):
CSS_PATH = "styles.tcss"
CSS_PATH = "styles.css"
TITLE = "测试布局"
BINDINGS = [("escape", "quit", "退出"), ("d", "toggle_dark", "改变色调")]
SCREENS = {
@@ -290,4 +294,4 @@ class AppLauncher(App):
if __name__ == "__main__":
app = AppLauncher()
app.run()
app.run()

View File

@@ -1,8 +1,14 @@
# [调试] 将更改保存到文件
save = 1
# [调试] 覆写时间
time_override = 10
time_override = -1
# [调试] 一键通过
quick_pass = 0
# 对于每个项目的新记忆核子数量
tasked_number = 8
# 竖屏适配
# 竖屏适配 (未完成)
mobile_mode = 1

42
main.py
View File

@@ -22,18 +22,7 @@ import auxiliary as aux
import compositions as compo
import builtins
_original_open = builtins.open
def _open(*args, **kwargs):
if "encoding" not in kwargs:
kwargs["encoding"] = "utf-8"
return _original_open(*args, **kwargs)
builtins.open = _open
ver = "0.3.0b"
ver = "0.3.1"
config = aux.ConfigFile("config.toml")
@@ -43,19 +32,9 @@ class MemScreen(Screen):
("d", "toggle_dark", "改变色调"),
("q", "pop_screen", "返回主菜单"),
("v", "play_voice", "朗读"),
("0", "press('q0')", None),
("1", "press('q1')", None),
("2", "press('q2')", None),
("3", "press('q3')", None),
("4", "press('q4')", None),
("5", "press('q5')", None),
("[", "press('q5')", None),
("]", "press('q4')", None),
(";", "press('q3')", None),
("'", "press('q2')", None),
(".", "press('q1')", None),
("/", "press('q0')", None),
]
if config.get("quick_pass"):
BINDINGS.append(("k", "quick_pass", "快速通过[调试]"))
btn = dict()
def __init__(
@@ -72,6 +51,8 @@ class MemScreen(Screen):
self.compo = next(self.reactor.current_appar)
def compose(self) -> ComposeResult:
if type(self.compo).__name__ == "Recognition":
self.action_play_voice()
yield Header(show_clock=True)
with Center():
yield Static(
@@ -126,12 +107,10 @@ class MemScreen(Screen):
def refresh_ui(self):
self.call_later(self.recompose)
def report(self, quality):
assessment = self.reactor.report(self.reactor.current_atom, quality)
return assessment
print(type(self.compo).__name__)
def action_play_voice(self):
print("VOICE")
def play():
cache_dir = pathlib.Path(f"./cache/voice/")
cache_dir.mkdir(parents=True, exist_ok=True)
@@ -148,6 +127,9 @@ class MemScreen(Screen):
threading.Thread(target=play).start()
def action_quick_pass(self):
self.reactor.report(self.reactor.current_atom, 5)
self._forward_judge(0)
def action_toggle_dark(self):
self.app.action_toggle_dark()
@@ -183,7 +165,7 @@ class PreparationScreen(Screen):
classes="start-button",
)
yield Static(f"\n全文如下:\n")
yield Static(self._get_full_content(), classes="full")
yield Static(self._get_full_content().replace("/", ""), classes="full")
yield Footer()
def _get_full_content(self):
@@ -261,7 +243,7 @@ class FileSelectorScreen(Screen):
class AppLauncher(App):
CSS_PATH = "styles.tcss"
CSS_PATH = "styles.css"
TITLE = "潜进 - 辅助记忆程序"
BINDINGS = [("escape", "quit", "退出"), ("d", "toggle_dark", "改变色调")]
SCREENS = {

View File

@@ -8,7 +8,7 @@ translation = "语句翻译"
["testdata"]
# 记忆时显示的额外信息
additional_inf = ["translation","keyword_note", "note"]
# 填空测试, content指代键名
# 填空测试, content 指代键名
fill_blank_test = {"from"=["content"], "hint"=["translation"]}
# 选择题测试
draw_card_test = {"from"=["keyword_note"]}

View File

0
nucleon_todo/书愤.toml Normal file
View File

View File

View File

0
nucleon_todo/劝学.toml Normal file
View File

View File

0
nucleon_todo/客至.toml Normal file
View File

View File

View File

View File

0
nucleon_todo/师说.toml Normal file
View File

View File

View File

View File

View File

View File

View File

View File

0
nucleon_todo/无衣.toml Normal file
View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

0
nucleon_todo/登高.toml Normal file
View File

View File

View File

0
nucleon_todo/礼运.toml Normal file
View File

0
nucleon_todo/离骚.toml Normal file
View File

View File

View File

View File

View File

View File

0
nucleon_todo/蜀相.toml Normal file
View File

View File

0
nucleon_todo/论语.toml Normal file
View File

View File

View File

View File

View File

0
nucleon_todo/锦瑟.toml Normal file
View File

View File

View File

View File

0
nucleon_todo/静女.toml Normal file
View File

View File

View File

View File

@@ -48,6 +48,7 @@ class Electron:
Args:
quality (int): 记忆保留率量化参数
"""
print(f"REVISOR: {quality}, {is_new_activation}")
if quality == -1:
return -1

View File

@@ -21,8 +21,8 @@ def proc_file(path: Path):
c = 0
for i in nu.nucleons:
c += 1
print(f"预缓存 [{nu.name}] ({c}/{len(nu)}): {i['content']}")
precache(i['content'])
print(f"预缓存 [{nu.name}] ({c}/{len(nu)}): {i['content'].replace('/', '')}")
precache(i['content'].replace('/', ''))
def walk(path_str: str):
@@ -49,4 +49,4 @@ if __name__ == "__main__":
walk("./nucleon")
elif choice == "C":
shutil.rmtree("./cache/voice", ignore_errors=True)
print("缓存已清空")
print("缓存已清空")

View File

@@ -108,14 +108,14 @@ class SelectionPuzzle(Puzzle):
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.refresh()
print(puz.wording)
print(puz.answer)
print(puz.options)
if __name__ == "__main__":
puz = SelectionPuzzle(
{"1+1": "2", "1+2": "3", "1+3": "4"},
["2", "5", "0"],
3,
'求值: '
)
puz.refresh()
print(puz.wording)
print(puz.answer)
print(puz.options)

View File

@@ -32,7 +32,6 @@ class Reactor():
"""反应堆对象, 处理和分配一次文件记忆流程的资源与策略"""
def __init__(self, nucleon_file: pt.NucleonUnion, electron_file: pt.ElectronUnion, screen, tasked_num):
# 导入原子对象
self.reported = set()
self.nucleon_file = nucleon_file
self.electron_file = electron_file
self.tasked_num = tasked_num
@@ -41,6 +40,7 @@ class Reactor():
counter = self.tasked_num
self.screen = screen
self.electron_dict = electron_file.electrons_dict
self.quality_dict = {}
def electron_dict_get_fallback(key) -> pt.Electron:
value = self.electron_dict.get(key)
# 如果值不存在,则设置默认值
@@ -71,7 +71,6 @@ class Reactor():
self.procession: list
self.failed: list
self.round_title: str
self.reported: set
self.current_atom: typing.Tuple[pt.Electron, pt.Nucleon, dict]
self.round_set = 0
self.current_atom = pt.Atom.placeholder()
@@ -127,23 +126,25 @@ class Reactor():
return 0
def save(self):
self._deploy_report()
print("Progress saved")
# self.nucleon_file.save()
self.electron_file.save()
def _deploy_report(self):
"部署所有 _report"
for e, q in self.quality_dict.items():
if q == -1:
e.revisor(5, True)
continue
e.revisor(q)
def report(self, atom, quality):
"""
0: 初次激活/通过
1: 不通过
"""
"向反应器和最低质量记录汇报"
if atom in self.atoms_new:
atom[0].revisor(quality, True)
return 0
if atom[0] not in self.reported:
atom[0].revisor(quality)
self.reported.add(atom[0])
self.quality_dict[atom[0]] = -1
print(self.quality_dict)
return
self.quality_dict[atom[0]] = min(quality, self.quality_dict.get(atom[0], 5))
if quality <= 3:
self.failed.append(atom)
return 1
else:
return 0
print(self.quality_dict)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

BIN
readme_src/img1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
readme_src/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

BIN
readme_src/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
readme_src/img4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -0,0 +1,17 @@
function getStartUrl() {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("delay");
return url.pathname + "?" + params.toString();
}
async function refresh() {
const ping_url = document.body.dataset.pingurl;
if (ping_url) {
await fetch(ping_url, {
method: "GET",
mode: "no-cors",
});
}
window.location.href = getStartUrl();
}

View File

@@ -4,6 +4,7 @@
<link rel="stylesheet" href="{{ config.static.url }}css/xterm.css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto%20Mono"/>
<script src="{{ config.static.url }}js/textual.js"></script>
<script src="{{ config.static.url }}js/script.js"></script>
<style>
body {
background: #000000;
@@ -99,24 +100,6 @@
z-index: 5;
}
</style>
<script>
function getStartUrl() {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("delay");
return url.pathname + "?" + params.toString();
}
async function refresh() {
const ping_url = document.body.dataset.pingurl;
if (ping_url) {
await fetch(ping_url, {
method: "GET",
mode: "no-cors",
});
}
window.location.href = getStartUrl();
}
</script>
</head>
<body data-pingurl="{{ ping_url }}">
<div class="dialog-container intro-dialog">
@@ -145,4 +128,4 @@
data-font-size="{{ font_size }}"
></div>
</body>
</html>
</html>