readloops 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- readloops-2.3.0/LICENSE +21 -0
- readloops-2.3.0/PKG-INFO +272 -0
- readloops-2.3.0/README.md +232 -0
- readloops-2.3.0/app/__init__.py +0 -0
- readloops-2.3.0/app/api/__init__.py +0 -0
- readloops-2.3.0/app/api/articles.py +97 -0
- readloops-2.3.0/app/api/reading.py +157 -0
- readloops-2.3.0/app/api/stats.py +254 -0
- readloops-2.3.0/app/api/words.py +210 -0
- readloops-2.3.0/app/cli.py +185 -0
- readloops-2.3.0/app/config.py +58 -0
- readloops-2.3.0/app/database.py +204 -0
- readloops-2.3.0/app/main.py +63 -0
- readloops-2.3.0/app/models.py +35 -0
- readloops-2.3.0/app/services/__init__.py +0 -0
- readloops-2.3.0/app/services/ai.py +571 -0
- readloops-2.3.0/app/services/corpus.py +78 -0
- readloops-2.3.0/app/services/dict_import.py +236 -0
- readloops-2.3.0/app/services/similarity.py +209 -0
- readloops-2.3.0/app/services/smart_test.py +345 -0
- readloops-2.3.0/app/services/srs.py +154 -0
- readloops-2.3.0/app/web/css/style.css +2338 -0
- readloops-2.3.0/app/web/icon.svg +14 -0
- readloops-2.3.0/app/web/index.html +179 -0
- readloops-2.3.0/app/web/js/app.js +1923 -0
- readloops-2.3.0/app/web/manifest.json +24 -0
- readloops-2.3.0/pyproject.toml +87 -0
- readloops-2.3.0/readloops.egg-info/PKG-INFO +272 -0
- readloops-2.3.0/readloops.egg-info/SOURCES.txt +38 -0
- readloops-2.3.0/readloops.egg-info/dependency_links.txt +1 -0
- readloops-2.3.0/readloops.egg-info/entry_points.txt +2 -0
- readloops-2.3.0/readloops.egg-info/requires.txt +10 -0
- readloops-2.3.0/readloops.egg-info/top_level.txt +1 -0
- readloops-2.3.0/setup.cfg +4 -0
- readloops-2.3.0/tests/test_api_endpoints.py +61 -0
- readloops-2.3.0/tests/test_cli.py +73 -0
- readloops-2.3.0/tests/test_database.py +56 -0
- readloops-2.3.0/tests/test_similarity.py +39 -0
- readloops-2.3.0/tests/test_smoke.py +28 -0
- readloops-2.3.0/tests/test_srs.py +56 -0
readloops-2.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ReadLoops Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
readloops-2.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: readloops
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: AI 驱动的英语阅读训练器:可理解输入 + FSRS 间隔重复
|
|
5
|
+
Author: Fangyuan2711
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pekoqq/ReadLoops
|
|
8
|
+
Project-URL: Repository, https://github.com/pekoqq/ReadLoops
|
|
9
|
+
Project-URL: Issue Tracker, https://github.com/pekoqq/ReadLoops/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/pekoqq/ReadLoops/blob/main/docs/CHANGELOG.md
|
|
11
|
+
Keywords: english,reading,fsrs,spaced-repetition,cet4,ai
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Environment :: Console
|
|
21
|
+
Classifier: Environment :: Web Environment
|
|
22
|
+
Classifier: Framework :: FastAPI
|
|
23
|
+
Classifier: Intended Audience :: Education
|
|
24
|
+
Classifier: Natural Language :: Chinese (Simplified)
|
|
25
|
+
Classifier: Natural Language :: English
|
|
26
|
+
Classifier: Topic :: Education
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: fastapi>=0.110
|
|
31
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
32
|
+
Requires-Dist: httpx>=0.27
|
|
33
|
+
Requires-Dist: pydantic>=2.0
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
38
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# ReadLoops
|
|
42
|
+
|
|
43
|
+
> AI 驱动的英语阅读训练器。基于「可理解输入(i+1)」+ 阅读驱动的 FSRS 间隔重复,面向大学英语四级(进阶六级)。
|
|
44
|
+
|
|
45
|
+
## 它解决什么问题
|
|
46
|
+
|
|
47
|
+
背单词软件的问题是「脱离语境」。ReadLoops 反过来:**先读文章,在语境里反复遇见生词,再用 FSRS 安排复习**。文章由 AI 生成,用词受控(95–98% 词汇覆盖率),风格对齐四级真题。
|
|
48
|
+
|
|
49
|
+
## 功能
|
|
50
|
+
|
|
51
|
+
- **AI 文章生成** — 受控生词量(每篇 6–15 个),真题风格蒸馏,TF-IDF 相似度去重
|
|
52
|
+
- **划词查词** — 本地词典优先,AI 兜底;查词 1 次记录 → 2 次标记 → 3 次自动进生词本
|
|
53
|
+
- **生词本** — 增删改查、批量操作、词形变化、FSRS 记忆强度
|
|
54
|
+
- **高亮标记** — 划词高亮、点击取消、重渲染后自动恢复
|
|
55
|
+
- **专注模式** — 自动进入、边缘触发工具栏、水波纹引导
|
|
56
|
+
- **智能词汇测试** — 按用户数据权重抽样,多题型
|
|
57
|
+
- **FSRS 间隔重复** — 文章选词驱动,测试结果回写
|
|
58
|
+
- **阅读统计** — 概览卡片、词汇分布、查词热词、GitHub 式热力图
|
|
59
|
+
- **PWA** — 可安装为独立桌面应用
|
|
60
|
+
- **多主题** — 深色 / 明亮 / 纸质护眼 / 纯黑
|
|
61
|
+
|
|
62
|
+
## 技术栈
|
|
63
|
+
|
|
64
|
+
| 层 | 选型 |
|
|
65
|
+
|----|------|
|
|
66
|
+
| 后端 | FastAPI + SQLite(无 ORM,原生 `sqlite3`) |
|
|
67
|
+
| 前端 | 原生 HTML / CSS / JS 单页应用(零构建、零依赖) |
|
|
68
|
+
| AI | DeepSeek(`deepseek-flash`),强制关闭思考模式 |
|
|
69
|
+
| 算法 | 自研 FSRS(`app/services/srs.py`)+ 自研 TF-IDF 余弦相似度 |
|
|
70
|
+
|
|
71
|
+
设计上刻意保持轻量:**后端零 ORM、前端零框架、算法零第三方依赖**。
|
|
72
|
+
|
|
73
|
+
## 快速开始
|
|
74
|
+
|
|
75
|
+
支持 **Windows / macOS / Linux**。需要 Python 3.10 或更高版本。
|
|
76
|
+
|
|
77
|
+
### 方式一:pip 安装(推荐)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install readloops
|
|
81
|
+
|
|
82
|
+
readloops init # 初始化数据库
|
|
83
|
+
readloops # 启动,并自动打开浏览器
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> 若提示 `readloops: command not found`,说明 Python 的脚本目录不在 PATH 中。
|
|
87
|
+
> 改用 `python -m app.cli` 或参考下方「方式二」。
|
|
88
|
+
|
|
89
|
+
### 方式二:从源码安装
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/pekoqq/ReadLoops.git && cd ReadLoops
|
|
93
|
+
|
|
94
|
+
# 创建虚拟环境
|
|
95
|
+
python3 -m venv .venv
|
|
96
|
+
|
|
97
|
+
# 激活(按系统选择)
|
|
98
|
+
source .venv/bin/activate # macOS / Linux
|
|
99
|
+
.venv\Scripts\activate # Windows (PowerShell / CMD)
|
|
100
|
+
|
|
101
|
+
pip install -e . # 以可编辑模式安装
|
|
102
|
+
readloops # 启动
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 方式三:Docker(推荐用于服务器 / 长期运行)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
docker compose up -d
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
或不用 compose:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
docker build -t readloops .
|
|
115
|
+
docker run -d --name readloops \
|
|
116
|
+
-p 127.0.0.1:8000:8000 \
|
|
117
|
+
-v readloops-data:/data \
|
|
118
|
+
--restart unless-stopped \
|
|
119
|
+
readloops
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **三大系统命令完全一致**,不依赖本机 Python 环境
|
|
123
|
+
- 数据库持久化在 `readloops-data` 卷中,容器重建不丢数据
|
|
124
|
+
- 默认只绑定本机(安全)。要让局域网 / 其他设备访问,把 `docker-compose.yml` 的端口改成 `"8000:8000"`
|
|
125
|
+
- 查看日志:`docker logs -f readloops`
|
|
126
|
+
|
|
127
|
+
### 命令行说明
|
|
128
|
+
|
|
129
|
+
| 命令 | 作用 |
|
|
130
|
+
|------|------|
|
|
131
|
+
| `readloops` | 启动服务并打开浏览器(默认命令) |
|
|
132
|
+
| `readloops serve` | 只启动服务 |
|
|
133
|
+
| `readloops serve --port 9000 --no-browser` | 自定义端口、不打开浏览器 |
|
|
134
|
+
| `readloops init` | 初始化数据库 |
|
|
135
|
+
| `readloops doctor` | 检查运行环境是否就绪 |
|
|
136
|
+
|
|
137
|
+
启动后访问 <http://127.0.0.1:8000>,进入「设置」填入 AI API Key,即可生成第一篇文章。
|
|
138
|
+
|
|
139
|
+
> 首次启动会自动创建数据库并建表。但**词库为空时无法生成文章**,见下一节。
|
|
140
|
+
|
|
141
|
+
## 数据准备(重要)
|
|
142
|
+
|
|
143
|
+
仓库**不包含任何数据文件**。原因见「版权说明」。
|
|
144
|
+
|
|
145
|
+
### 1. 词典 / 词表
|
|
146
|
+
|
|
147
|
+
文章生成需要一份词表来挑选目标生词。推荐使用开源词典 [ECDICT](https://github.com/skywind3000/ECDICT):
|
|
148
|
+
|
|
149
|
+
- 下载 `ecdict.csv`(或使用项目提供的 `tools/` 脚本)
|
|
150
|
+
- 词表需导入 `words` 表,字段:`lemma / text / meaning / phonetic / level`
|
|
151
|
+
|
|
152
|
+
`app/services/corpus.py` 提供了 `import_word_list(filepath, level)` 用于导入纯文本词表(格式:`word [音标] 词性.释义`)。
|
|
153
|
+
|
|
154
|
+
### 2. 真题语料(可选)
|
|
155
|
+
|
|
156
|
+
用于「文章相似度匹配」。**受版权保护,需自行准备**:
|
|
157
|
+
|
|
158
|
+
- 项目提供抓取脚本 `tools/style_analysis/crawl_lazynote.py`,请自行确认目标站点的使用条款
|
|
159
|
+
- 语料文件默认路径:`语料库/真题阅读纯文本/all_passages_lazynote.json`
|
|
160
|
+
- 可用环境变量 `READLOOPS_CORPUS_FILE` 指定其他路径
|
|
161
|
+
- **未提供语料时程序照常运行**,仅跳过相似度匹配
|
|
162
|
+
|
|
163
|
+
> 注:生成用的「真题风格参数」已固化在 `app/services/ai.py` 的 prompt 模板中,**不依赖语料文件**。
|
|
164
|
+
> `tools/style_analysis/cet4_style_params.json` 是分析产物,仅在你想重新蒸馏风格时才需要语料。
|
|
165
|
+
|
|
166
|
+
## 配置
|
|
167
|
+
|
|
168
|
+
| 环境变量 | 默认值 | 说明 |
|
|
169
|
+
|----------|--------|------|
|
|
170
|
+
| `AI_BASE_URL` | `https://api.deepseek.com` | AI 接口地址 |
|
|
171
|
+
| `AI_API_KEY` | 空 | API Key(也可在网页「设置」中填写,存于数据库) |
|
|
172
|
+
| `AI_MODEL` | `deepseek-flash` | 模型名 |
|
|
173
|
+
| `READLOOPS_DATA_DIR` | 见下 | 数据目录(数据库存放位置) |
|
|
174
|
+
| `READLOOPS_DB_PATH` | `<数据目录>/yuedu.db` | 数据库文件路径 |
|
|
175
|
+
| `READLOOPS_CORPUS_FILE` | `语料库/真题阅读纯文本/all_passages_lazynote.json` | 真题语料路径 |
|
|
176
|
+
| `READLOOPS_CORPUS_DIR` | `语料库/真题阅读纯文本` | 抓取脚本输出目录 |
|
|
177
|
+
|
|
178
|
+
### 数据目录在哪
|
|
179
|
+
|
|
180
|
+
程序会自动选择合适的位置,无需手动配置:
|
|
181
|
+
|
|
182
|
+
| 运行方式 | 数据目录 |
|
|
183
|
+
|----------|----------|
|
|
184
|
+
| 源码模式(项目根有 `pyproject.toml`) | `<项目根>/data` |
|
|
185
|
+
| pip 安装 · Windows | `%LOCALAPPDATA%\ReadLoops` |
|
|
186
|
+
| pip 安装 · macOS | `~/Library/Application Support/ReadLoops` |
|
|
187
|
+
| pip 安装 · Linux | `~/.local/share/readloops` |
|
|
188
|
+
|
|
189
|
+
用 `readloops doctor` 可随时查看当前实际路径。想自定义就设 `READLOOPS_DATA_DIR`。
|
|
190
|
+
|
|
191
|
+
> 数据库**不会**写入 Python 安装目录(site-packages),升级或卸载包不会影响你的数据。
|
|
192
|
+
|
|
193
|
+
## 在其他设备上访问
|
|
194
|
+
|
|
195
|
+
默认只绑定 `127.0.0.1`——即只有本机能访问。要让手机或别的电脑也能用:
|
|
196
|
+
|
|
197
|
+
### 同一局域网
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
readloops serve --host 0.0.0.0
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Docker 用户把 `docker-compose.yml` 的端口改成 `"8000:8000"`。然后访问 `http://<电脑局域网IP>:8000`。
|
|
204
|
+
|
|
205
|
+
> ⚠️ 局域网内任何人都能访问,且**没有密码保护**。仅在可信网络下这样做。
|
|
206
|
+
|
|
207
|
+
### 通过 Tailscale(可在外网使用)
|
|
208
|
+
|
|
209
|
+
若你的设备都在同一 Tailscale 网络中:
|
|
210
|
+
|
|
211
|
+
1. 在常开的机器上跑服务,监听 `0.0.0.0`
|
|
212
|
+
2. 手机安装 Tailscale 并登录同一账号
|
|
213
|
+
3. 访问 `http://<机器的Tailscale IP>:8000`
|
|
214
|
+
|
|
215
|
+
只有你自己的设备能访问,且不受网络位置限制。
|
|
216
|
+
|
|
217
|
+
> 说明:界面目前未做手机小屏的专项适配,能打开使用但体验一般;响应式优化在路线图中。
|
|
218
|
+
|
|
219
|
+
## 项目结构
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
readloops/
|
|
223
|
+
├── app/
|
|
224
|
+
│ ├── cli.py # 命令行入口(readloops serve / init / doctor)
|
|
225
|
+
│ ├── main.py # FastAPI 入口
|
|
226
|
+
│ ├── config.py # 路径与全局配置(跨平台数据目录)
|
|
227
|
+
│ ├── database.py # SQLite schema(10 表)+ init_db
|
|
228
|
+
│ ├── models.py # 数据模型
|
|
229
|
+
│ ├── api/ # 路由:articles / words / reading / stats
|
|
230
|
+
│ ├── services/
|
|
231
|
+
│ │ ├── ai.py # AI 调用 + 文章生成(含去重循环)
|
|
232
|
+
│ │ ├── srs.py # FSRS 间隔重复(自研)
|
|
233
|
+
│ │ ├── similarity.py # TF-IDF + 余弦相似度(自研)
|
|
234
|
+
│ │ ├── smart_test.py # 智能出题
|
|
235
|
+
│ │ ├── scheduler.py # 生词调度
|
|
236
|
+
│ │ ├── level.py # 难度自适应
|
|
237
|
+
│ │ └── corpus.py # 词表导入
|
|
238
|
+
│ └── web/ # 前端(index.html / css / js / PWA)
|
|
239
|
+
├── tools/ # 数据构建脚本
|
|
240
|
+
├── skills/ # 真题风格蒸馏 Skill
|
|
241
|
+
├── tests/ # 测试
|
|
242
|
+
└── docs/ # 架构与变更文档
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## 开发与质量
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# 安装开发依赖
|
|
249
|
+
pip install -r requirements-dev.txt
|
|
250
|
+
|
|
251
|
+
# 代码检查与格式化
|
|
252
|
+
ruff check .
|
|
253
|
+
ruff format .
|
|
254
|
+
|
|
255
|
+
# 运行测试
|
|
256
|
+
pytest
|
|
257
|
+
|
|
258
|
+
# 安装 git 钩子(提交前自动检查)
|
|
259
|
+
pre-commit install
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
详见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
263
|
+
|
|
264
|
+
## 版权说明
|
|
265
|
+
|
|
266
|
+
- **本仓库不含词典与真题语料**。词表请使用开源词典(如 ECDICT);真题语料版权归原作者/出版方所有,本项目不分发,仅提供自用抓取脚本。
|
|
267
|
+
- 生成的文章内容由 AI 产生,请自行判断其准确性与适用性。
|
|
268
|
+
- 本项目仅供个人学习使用。
|
|
269
|
+
|
|
270
|
+
## 许可证
|
|
271
|
+
|
|
272
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# ReadLoops
|
|
2
|
+
|
|
3
|
+
> AI 驱动的英语阅读训练器。基于「可理解输入(i+1)」+ 阅读驱动的 FSRS 间隔重复,面向大学英语四级(进阶六级)。
|
|
4
|
+
|
|
5
|
+
## 它解决什么问题
|
|
6
|
+
|
|
7
|
+
背单词软件的问题是「脱离语境」。ReadLoops 反过来:**先读文章,在语境里反复遇见生词,再用 FSRS 安排复习**。文章由 AI 生成,用词受控(95–98% 词汇覆盖率),风格对齐四级真题。
|
|
8
|
+
|
|
9
|
+
## 功能
|
|
10
|
+
|
|
11
|
+
- **AI 文章生成** — 受控生词量(每篇 6–15 个),真题风格蒸馏,TF-IDF 相似度去重
|
|
12
|
+
- **划词查词** — 本地词典优先,AI 兜底;查词 1 次记录 → 2 次标记 → 3 次自动进生词本
|
|
13
|
+
- **生词本** — 增删改查、批量操作、词形变化、FSRS 记忆强度
|
|
14
|
+
- **高亮标记** — 划词高亮、点击取消、重渲染后自动恢复
|
|
15
|
+
- **专注模式** — 自动进入、边缘触发工具栏、水波纹引导
|
|
16
|
+
- **智能词汇测试** — 按用户数据权重抽样,多题型
|
|
17
|
+
- **FSRS 间隔重复** — 文章选词驱动,测试结果回写
|
|
18
|
+
- **阅读统计** — 概览卡片、词汇分布、查词热词、GitHub 式热力图
|
|
19
|
+
- **PWA** — 可安装为独立桌面应用
|
|
20
|
+
- **多主题** — 深色 / 明亮 / 纸质护眼 / 纯黑
|
|
21
|
+
|
|
22
|
+
## 技术栈
|
|
23
|
+
|
|
24
|
+
| 层 | 选型 |
|
|
25
|
+
|----|------|
|
|
26
|
+
| 后端 | FastAPI + SQLite(无 ORM,原生 `sqlite3`) |
|
|
27
|
+
| 前端 | 原生 HTML / CSS / JS 单页应用(零构建、零依赖) |
|
|
28
|
+
| AI | DeepSeek(`deepseek-flash`),强制关闭思考模式 |
|
|
29
|
+
| 算法 | 自研 FSRS(`app/services/srs.py`)+ 自研 TF-IDF 余弦相似度 |
|
|
30
|
+
|
|
31
|
+
设计上刻意保持轻量:**后端零 ORM、前端零框架、算法零第三方依赖**。
|
|
32
|
+
|
|
33
|
+
## 快速开始
|
|
34
|
+
|
|
35
|
+
支持 **Windows / macOS / Linux**。需要 Python 3.10 或更高版本。
|
|
36
|
+
|
|
37
|
+
### 方式一:pip 安装(推荐)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install readloops
|
|
41
|
+
|
|
42
|
+
readloops init # 初始化数据库
|
|
43
|
+
readloops # 启动,并自动打开浏览器
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> 若提示 `readloops: command not found`,说明 Python 的脚本目录不在 PATH 中。
|
|
47
|
+
> 改用 `python -m app.cli` 或参考下方「方式二」。
|
|
48
|
+
|
|
49
|
+
### 方式二:从源码安装
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/pekoqq/ReadLoops.git && cd ReadLoops
|
|
53
|
+
|
|
54
|
+
# 创建虚拟环境
|
|
55
|
+
python3 -m venv .venv
|
|
56
|
+
|
|
57
|
+
# 激活(按系统选择)
|
|
58
|
+
source .venv/bin/activate # macOS / Linux
|
|
59
|
+
.venv\Scripts\activate # Windows (PowerShell / CMD)
|
|
60
|
+
|
|
61
|
+
pip install -e . # 以可编辑模式安装
|
|
62
|
+
readloops # 启动
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 方式三:Docker(推荐用于服务器 / 长期运行)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
docker compose up -d
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
或不用 compose:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
docker build -t readloops .
|
|
75
|
+
docker run -d --name readloops \
|
|
76
|
+
-p 127.0.0.1:8000:8000 \
|
|
77
|
+
-v readloops-data:/data \
|
|
78
|
+
--restart unless-stopped \
|
|
79
|
+
readloops
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **三大系统命令完全一致**,不依赖本机 Python 环境
|
|
83
|
+
- 数据库持久化在 `readloops-data` 卷中,容器重建不丢数据
|
|
84
|
+
- 默认只绑定本机(安全)。要让局域网 / 其他设备访问,把 `docker-compose.yml` 的端口改成 `"8000:8000"`
|
|
85
|
+
- 查看日志:`docker logs -f readloops`
|
|
86
|
+
|
|
87
|
+
### 命令行说明
|
|
88
|
+
|
|
89
|
+
| 命令 | 作用 |
|
|
90
|
+
|------|------|
|
|
91
|
+
| `readloops` | 启动服务并打开浏览器(默认命令) |
|
|
92
|
+
| `readloops serve` | 只启动服务 |
|
|
93
|
+
| `readloops serve --port 9000 --no-browser` | 自定义端口、不打开浏览器 |
|
|
94
|
+
| `readloops init` | 初始化数据库 |
|
|
95
|
+
| `readloops doctor` | 检查运行环境是否就绪 |
|
|
96
|
+
|
|
97
|
+
启动后访问 <http://127.0.0.1:8000>,进入「设置」填入 AI API Key,即可生成第一篇文章。
|
|
98
|
+
|
|
99
|
+
> 首次启动会自动创建数据库并建表。但**词库为空时无法生成文章**,见下一节。
|
|
100
|
+
|
|
101
|
+
## 数据准备(重要)
|
|
102
|
+
|
|
103
|
+
仓库**不包含任何数据文件**。原因见「版权说明」。
|
|
104
|
+
|
|
105
|
+
### 1. 词典 / 词表
|
|
106
|
+
|
|
107
|
+
文章生成需要一份词表来挑选目标生词。推荐使用开源词典 [ECDICT](https://github.com/skywind3000/ECDICT):
|
|
108
|
+
|
|
109
|
+
- 下载 `ecdict.csv`(或使用项目提供的 `tools/` 脚本)
|
|
110
|
+
- 词表需导入 `words` 表,字段:`lemma / text / meaning / phonetic / level`
|
|
111
|
+
|
|
112
|
+
`app/services/corpus.py` 提供了 `import_word_list(filepath, level)` 用于导入纯文本词表(格式:`word [音标] 词性.释义`)。
|
|
113
|
+
|
|
114
|
+
### 2. 真题语料(可选)
|
|
115
|
+
|
|
116
|
+
用于「文章相似度匹配」。**受版权保护,需自行准备**:
|
|
117
|
+
|
|
118
|
+
- 项目提供抓取脚本 `tools/style_analysis/crawl_lazynote.py`,请自行确认目标站点的使用条款
|
|
119
|
+
- 语料文件默认路径:`语料库/真题阅读纯文本/all_passages_lazynote.json`
|
|
120
|
+
- 可用环境变量 `READLOOPS_CORPUS_FILE` 指定其他路径
|
|
121
|
+
- **未提供语料时程序照常运行**,仅跳过相似度匹配
|
|
122
|
+
|
|
123
|
+
> 注:生成用的「真题风格参数」已固化在 `app/services/ai.py` 的 prompt 模板中,**不依赖语料文件**。
|
|
124
|
+
> `tools/style_analysis/cet4_style_params.json` 是分析产物,仅在你想重新蒸馏风格时才需要语料。
|
|
125
|
+
|
|
126
|
+
## 配置
|
|
127
|
+
|
|
128
|
+
| 环境变量 | 默认值 | 说明 |
|
|
129
|
+
|----------|--------|------|
|
|
130
|
+
| `AI_BASE_URL` | `https://api.deepseek.com` | AI 接口地址 |
|
|
131
|
+
| `AI_API_KEY` | 空 | API Key(也可在网页「设置」中填写,存于数据库) |
|
|
132
|
+
| `AI_MODEL` | `deepseek-flash` | 模型名 |
|
|
133
|
+
| `READLOOPS_DATA_DIR` | 见下 | 数据目录(数据库存放位置) |
|
|
134
|
+
| `READLOOPS_DB_PATH` | `<数据目录>/yuedu.db` | 数据库文件路径 |
|
|
135
|
+
| `READLOOPS_CORPUS_FILE` | `语料库/真题阅读纯文本/all_passages_lazynote.json` | 真题语料路径 |
|
|
136
|
+
| `READLOOPS_CORPUS_DIR` | `语料库/真题阅读纯文本` | 抓取脚本输出目录 |
|
|
137
|
+
|
|
138
|
+
### 数据目录在哪
|
|
139
|
+
|
|
140
|
+
程序会自动选择合适的位置,无需手动配置:
|
|
141
|
+
|
|
142
|
+
| 运行方式 | 数据目录 |
|
|
143
|
+
|----------|----------|
|
|
144
|
+
| 源码模式(项目根有 `pyproject.toml`) | `<项目根>/data` |
|
|
145
|
+
| pip 安装 · Windows | `%LOCALAPPDATA%\ReadLoops` |
|
|
146
|
+
| pip 安装 · macOS | `~/Library/Application Support/ReadLoops` |
|
|
147
|
+
| pip 安装 · Linux | `~/.local/share/readloops` |
|
|
148
|
+
|
|
149
|
+
用 `readloops doctor` 可随时查看当前实际路径。想自定义就设 `READLOOPS_DATA_DIR`。
|
|
150
|
+
|
|
151
|
+
> 数据库**不会**写入 Python 安装目录(site-packages),升级或卸载包不会影响你的数据。
|
|
152
|
+
|
|
153
|
+
## 在其他设备上访问
|
|
154
|
+
|
|
155
|
+
默认只绑定 `127.0.0.1`——即只有本机能访问。要让手机或别的电脑也能用:
|
|
156
|
+
|
|
157
|
+
### 同一局域网
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
readloops serve --host 0.0.0.0
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Docker 用户把 `docker-compose.yml` 的端口改成 `"8000:8000"`。然后访问 `http://<电脑局域网IP>:8000`。
|
|
164
|
+
|
|
165
|
+
> ⚠️ 局域网内任何人都能访问,且**没有密码保护**。仅在可信网络下这样做。
|
|
166
|
+
|
|
167
|
+
### 通过 Tailscale(可在外网使用)
|
|
168
|
+
|
|
169
|
+
若你的设备都在同一 Tailscale 网络中:
|
|
170
|
+
|
|
171
|
+
1. 在常开的机器上跑服务,监听 `0.0.0.0`
|
|
172
|
+
2. 手机安装 Tailscale 并登录同一账号
|
|
173
|
+
3. 访问 `http://<机器的Tailscale IP>:8000`
|
|
174
|
+
|
|
175
|
+
只有你自己的设备能访问,且不受网络位置限制。
|
|
176
|
+
|
|
177
|
+
> 说明:界面目前未做手机小屏的专项适配,能打开使用但体验一般;响应式优化在路线图中。
|
|
178
|
+
|
|
179
|
+
## 项目结构
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
readloops/
|
|
183
|
+
├── app/
|
|
184
|
+
│ ├── cli.py # 命令行入口(readloops serve / init / doctor)
|
|
185
|
+
│ ├── main.py # FastAPI 入口
|
|
186
|
+
│ ├── config.py # 路径与全局配置(跨平台数据目录)
|
|
187
|
+
│ ├── database.py # SQLite schema(10 表)+ init_db
|
|
188
|
+
│ ├── models.py # 数据模型
|
|
189
|
+
│ ├── api/ # 路由:articles / words / reading / stats
|
|
190
|
+
│ ├── services/
|
|
191
|
+
│ │ ├── ai.py # AI 调用 + 文章生成(含去重循环)
|
|
192
|
+
│ │ ├── srs.py # FSRS 间隔重复(自研)
|
|
193
|
+
│ │ ├── similarity.py # TF-IDF + 余弦相似度(自研)
|
|
194
|
+
│ │ ├── smart_test.py # 智能出题
|
|
195
|
+
│ │ ├── scheduler.py # 生词调度
|
|
196
|
+
│ │ ├── level.py # 难度自适应
|
|
197
|
+
│ │ └── corpus.py # 词表导入
|
|
198
|
+
│ └── web/ # 前端(index.html / css / js / PWA)
|
|
199
|
+
├── tools/ # 数据构建脚本
|
|
200
|
+
├── skills/ # 真题风格蒸馏 Skill
|
|
201
|
+
├── tests/ # 测试
|
|
202
|
+
└── docs/ # 架构与变更文档
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 开发与质量
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# 安装开发依赖
|
|
209
|
+
pip install -r requirements-dev.txt
|
|
210
|
+
|
|
211
|
+
# 代码检查与格式化
|
|
212
|
+
ruff check .
|
|
213
|
+
ruff format .
|
|
214
|
+
|
|
215
|
+
# 运行测试
|
|
216
|
+
pytest
|
|
217
|
+
|
|
218
|
+
# 安装 git 钩子(提交前自动检查)
|
|
219
|
+
pre-commit install
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
详见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
223
|
+
|
|
224
|
+
## 版权说明
|
|
225
|
+
|
|
226
|
+
- **本仓库不含词典与真题语料**。词表请使用开源词典(如 ECDICT);真题语料版权归原作者/出版方所有,本项目不分发,仅提供自用抓取脚本。
|
|
227
|
+
- 生成的文章内容由 AI 产生,请自行判断其准确性与适用性。
|
|
228
|
+
- 本项目仅供个人学习使用。
|
|
229
|
+
|
|
230
|
+
## 许可证
|
|
231
|
+
|
|
232
|
+
[MIT](LICENSE)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""文章路由:生成、列表、详情。"""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException
|
|
5
|
+
|
|
6
|
+
from app.database import get_db
|
|
7
|
+
from app.services.ai import generate_article
|
|
8
|
+
|
|
9
|
+
router = APIRouter(prefix="/api/articles", tags=["articles"])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.post("/generate")
|
|
13
|
+
async def generate():
|
|
14
|
+
"""生成一篇新文章。"""
|
|
15
|
+
article = generate_article()
|
|
16
|
+
if not article:
|
|
17
|
+
raise HTTPException(status_code=500, detail="无法生成文章,请检查 AI 配置")
|
|
18
|
+
return {
|
|
19
|
+
"id": article.id,
|
|
20
|
+
"title": article.title,
|
|
21
|
+
"content": article.content,
|
|
22
|
+
"source": article.source,
|
|
23
|
+
"word_count": article.word_count,
|
|
24
|
+
"new_word_count": article.new_word_count,
|
|
25
|
+
"target_words": json.loads(article.target_words) if article.target_words else [],
|
|
26
|
+
"created_at": article.created_at,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get("/")
|
|
31
|
+
async def list_articles(limit: int = 30):
|
|
32
|
+
"""获取文章列表。"""
|
|
33
|
+
with get_db() as conn:
|
|
34
|
+
rows = conn.execute(
|
|
35
|
+
"SELECT id, title, source, word_count, new_word_count, reading_time_seconds, created_at "
|
|
36
|
+
"FROM articles ORDER BY created_at DESC LIMIT ?",
|
|
37
|
+
(limit,),
|
|
38
|
+
).fetchall()
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"id": r["id"],
|
|
42
|
+
"title": r["title"],
|
|
43
|
+
"source": r["source"],
|
|
44
|
+
"word_count": r["word_count"],
|
|
45
|
+
"new_word_count": r["new_word_count"],
|
|
46
|
+
"reading_time_seconds": r["reading_time_seconds"],
|
|
47
|
+
"created_at": r["created_at"],
|
|
48
|
+
}
|
|
49
|
+
for r in rows
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get("/{article_id}")
|
|
54
|
+
async def get_article(article_id: int):
|
|
55
|
+
"""获取文章详情。"""
|
|
56
|
+
with get_db() as conn:
|
|
57
|
+
row = conn.execute(
|
|
58
|
+
"SELECT * FROM articles WHERE id = ?", (article_id,)
|
|
59
|
+
).fetchone()
|
|
60
|
+
if not row:
|
|
61
|
+
raise HTTPException(status_code=404, detail="文章不存在")
|
|
62
|
+
return {
|
|
63
|
+
"id": row["id"],
|
|
64
|
+
"title": row["title"],
|
|
65
|
+
"content": row["content"],
|
|
66
|
+
"source": row["source"],
|
|
67
|
+
"word_count": row["word_count"],
|
|
68
|
+
"new_word_count": row["new_word_count"],
|
|
69
|
+
"target_words": json.loads(row["target_words"]) if row["target_words"] else [],
|
|
70
|
+
"difficulty_score": row["difficulty_score"],
|
|
71
|
+
"reading_time_seconds": row["reading_time_seconds"],
|
|
72
|
+
"created_at": row["created_at"],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.get("/{article_id}/details")
|
|
77
|
+
async def get_article_details(article_id: int):
|
|
78
|
+
"""获取文章增强详情:高频词、目标生词、真题相似度。"""
|
|
79
|
+
from app.services.similarity import get_article_details
|
|
80
|
+
details = get_article_details(article_id)
|
|
81
|
+
if not details:
|
|
82
|
+
raise HTTPException(status_code=404, detail="文章不存在")
|
|
83
|
+
return details
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.delete("/{article_id}")
|
|
87
|
+
async def delete_article(article_id: int):
|
|
88
|
+
"""删除文章。"""
|
|
89
|
+
with get_db() as conn:
|
|
90
|
+
row = conn.execute("SELECT id FROM articles WHERE id = ?", (article_id,)).fetchone()
|
|
91
|
+
if not row:
|
|
92
|
+
raise HTTPException(status_code=404, detail="文章不存在")
|
|
93
|
+
# 先删除关联数据,再删除文章本身(避免外键约束失败)
|
|
94
|
+
conn.execute("DELETE FROM highlights WHERE article_id = ?", (article_id,))
|
|
95
|
+
conn.execute("DELETE FROM reading_sessions WHERE article_id = ?", (article_id,))
|
|
96
|
+
conn.execute("DELETE FROM articles WHERE id = ?", (article_id,))
|
|
97
|
+
return {"status": "ok", "message": "文章已删除"}
|