13 Commits

Author SHA1 Message Date
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
78 changed files with 120 additions and 90 deletions

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

@@ -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

31
main.py
View File

@@ -22,18 +22,18 @@ import auxiliary as aux
import compositions as compo
import builtins
_original_open = builtins.open
# Hook python 的 open() 函数, 使用 utf-8 (兼容 Windows 万年 GBK)
_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.0"
config = aux.ConfigFile("config.toml")
@@ -43,19 +43,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__(
@@ -127,10 +117,6 @@ 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
def action_play_voice(self):
def play():
cache_dir = pathlib.Path(f"./cache/voice/")
@@ -148,6 +134,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 +172,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 +250,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

0
nucleon/书愤.toml Normal file
View File

View File

0
nucleon/六国论.toml Normal file
View File

0
nucleon/劝学.toml Normal file
View File

0
nucleon/声声慢.toml Normal file
View File

0
nucleon/客至.toml Normal file
View File

0
nucleon/将进酒.toml Normal file
View File

View File

View File

0
nucleon/师说.toml Normal file
View File

View File

View File

View File

View File

0
nucleon/扬州慢.toml Normal file
View File

View File

View File

0
nucleon/无衣.toml Normal file
View File

View File

0
nucleon/望海潮.toml Normal file
View File

0
nucleon/朝天子.toml Normal file
View File

View File

0
nucleon/桂枝香.toml Normal file
View File

View File

0
nucleon/永遇乐.toml Normal file
View File

0
nucleon/江城子.toml Normal file
View File

View File

0
nucleon/燕歌行.toml Normal file
View File

0
nucleon/琵琶行.toml Normal file
View File

View File

0
nucleon/登快阁.toml Normal file
View File

View File

0
nucleon/登高.toml Normal file
View File

0
nucleon/短歌行.toml Normal file
View File

View File

22
nucleon/示例.toml Normal file
View File

@@ -0,0 +1,22 @@
# 文件头, 按部就班复制即可
["keydata"]
note = "笔记"
keyword_note = "关键词翻译"
translation = "语句翻译"
["testdata"]
additional_inf = ["translation","keyword_note", "note"]
fill_blank_test = {"from"=["content"], "hint"=["translation"]}
#可重复的单元
draw_card_test = {"from"=["keyword_note"]}
[CONTENT]
note = []
translation = "TRANSLATION"
keyword_note = {"KN_KEY": "KN_VALUE"}
#这是一个示例:(不要求附加在生成文本中)
["臣/密/言: /臣/以/险衅/, 夙/遭/闵凶./"]
note = []
translation = "臣子李密陈言: 我因命运不好, 小时候遭遇到了不幸"
keyword_note = {"险衅"="凶险祸患(这里指命运不好)", "夙"="早时, 这里指年幼的时候", "闵"="通'悯', 指可忧患的事", "凶"="不幸, 指丧父"}

0
nucleon/礼运.toml Normal file
View File

0
nucleon/离骚.toml Normal file
View File

View File

View File

0
nucleon/苏幕遮.toml Normal file
View File

0
nucleon/菩萨蛮.toml Normal file
View File

0
nucleon/虞美人.toml Normal file
View File

0
nucleon/蜀相.toml Normal file
View File

0
nucleon/蜀道难.toml Normal file
View File

0
nucleon/论语.toml Normal file
View File

View File

0
nucleon/贺新郎.toml Normal file
View File

0
nucleon/赤壁赋.toml Normal file
View File

0
nucleon/过秦论.toml Normal file
View File

0
nucleon/锦瑟.toml Normal file
View File

View File

View File

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"]}

0
nucleon/青玉案.toml Normal file
View File

0
nucleon/静女.toml Normal file
View File

View File

0
nucleon/鹊桥仙.toml Normal 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>