This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. .gitattributes +0 -7
  2. .gitignore +0 -92
  3. .vscode/PythonImportHelper-v2-Completion.json +0 -1277
  4. Dockerfile +0 -42
  5. README.md +5 -327
  6. app.py +0 -1068
  7. books.db +0 -3
  8. data/10005_一年级上册/audios/page_002_piece_00.mp3 +0 -3
  9. data/10005_一年级上册/audios/page_003_piece_00.mp3 +0 -3
  10. data/10005_一年级上册/audios/page_004_piece_00.mp3 +0 -3
  11. data/10005_一年级上册/audios/page_004_piece_01.mp3 +0 -3
  12. data/10005_一年级上册/audios/page_004_piece_02.mp3 +0 -3
  13. data/10005_一年级上册/audios/page_004_piece_03.mp3 +0 -3
  14. data/10005_一年级上册/audios/page_004_piece_04.mp3 +0 -3
  15. data/10005_一年级上册/audios/page_004_piece_05.mp3 +0 -3
  16. data/10005_一年级上册/audios/page_004_piece_06.mp3 +0 -3
  17. data/10005_一年级上册/audios/page_004_piece_07.mp3 +0 -3
  18. data/10005_一年级上册/audios/page_004_piece_08.mp3 +0 -3
  19. data/10005_一年级上册/audios/page_004_piece_09.mp3 +0 -3
  20. data/10005_一年级上册/audios/page_005_piece_00.mp3 +0 -3
  21. data/10005_一年级上册/audios/page_005_piece_01.mp3 +0 -3
  22. data/10005_一年级上册/audios/page_005_piece_02.mp3 +0 -3
  23. data/10005_一年级上册/audios/page_005_piece_03.mp3 +0 -3
  24. data/10005_一年级上册/audios/page_005_piece_04.mp3 +0 -3
  25. data/10005_一年级上册/audios/page_006_piece_00.mp3 +0 -3
  26. data/10005_一年级上册/audios/page_006_piece_01.mp3 +0 -3
  27. data/10005_一年级上册/audios/page_006_piece_02.mp3 +0 -3
  28. data/10005_一年级上册/audios/page_006_piece_03.mp3 +0 -3
  29. data/10005_一年级上册/audios/page_006_piece_04.mp3 +0 -3
  30. data/10005_一年级上册/audios/page_006_piece_05.mp3 +0 -3
  31. data/10005_一年级上册/audios/page_006_piece_06.mp3 +0 -3
  32. data/10005_一年级上册/audios/page_007_piece_00.mp3 +0 -3
  33. data/10005_一年级上册/audios/page_007_piece_01.mp3 +0 -3
  34. data/10005_一年级上册/audios/page_007_piece_02.mp3 +0 -3
  35. data/10005_一年级上册/audios/page_009_piece_00.mp3 +0 -3
  36. data/10005_一年级上册/audios/page_009_piece_01.mp3 +0 -3
  37. data/10005_一年级上册/audios/page_009_piece_02.mp3 +0 -3
  38. data/10005_一年级上册/audios/page_009_piece_03.mp3 +0 -3
  39. data/10005_一年级上册/audios/page_009_piece_04.mp3 +0 -3
  40. data/10005_一年级上册/audios/page_009_piece_05.mp3 +0 -3
  41. data/10005_一年级上册/audios/page_010_piece_00.mp3 +0 -3
  42. data/10005_一年级上册/audios/page_010_piece_01.mp3 +0 -3
  43. data/10005_一年级上册/audios/page_011_piece_00.mp3 +0 -3
  44. data/10005_一年级上册/audios/page_011_piece_01.mp3 +0 -3
  45. data/10005_一年级上册/audios/page_011_piece_02.mp3 +0 -3
  46. data/10005_一年级上册/audios/page_011_piece_03.mp3 +0 -3
  47. data/10005_一年级上册/audios/page_011_piece_04.mp3 +0 -3
  48. data/10005_一年级上册/audios/page_011_piece_05.mp3 +0 -3
  49. data/10005_一年级上册/audios/page_011_piece_06.mp3 +0 -3
  50. data/10005_一年级上册/audios/page_011_piece_07.mp3 +0 -3
.gitattributes CHANGED
@@ -33,10 +33,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- FZKT_GBK.woff filter=lfs diff=lfs merge=lfs -text
37
- *.mp3 filter=lfs diff=lfs merge=lfs -text
38
- *.jpg filter=lfs diff=lfs merge=lfs -text
39
- *.woff filter=lfs diff=lfs merge=lfs -text
40
- *.ttf filter=lfs diff=lfs merge=lfs -text
41
- *.woff2 filter=lfs diff=lfs merge=lfs -text
42
- *.db filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
.gitignore DELETED
@@ -1,92 +0,0 @@
1
- # Python 编译文件
2
- *.pyc
3
- *.pyo
4
- *.pyd
5
- __pycache__/
6
- *.py[cod]
7
- *$py.class
8
-
9
- # C 扩展
10
- *.so
11
-
12
- # 分发/打包
13
- .Python
14
- build/
15
- develop-eggs/
16
- dist/
17
- downloads/
18
- eggs/
19
- .eggs/
20
- lib/
21
- lib64/
22
- parts/
23
- sdist/
24
- var/
25
- wheels/
26
- *.egg-info/
27
- .installed.cfg
28
- *.egg
29
-
30
- # PyInstaller
31
- *.manifest
32
- *.spec
33
-
34
- # 单元测试 / 覆盖率
35
- htmlcov/
36
- .tox/
37
- .coverage
38
- .coverage.*
39
- .cache
40
- nosetests.xml
41
- coverage.xml
42
- *.cover
43
- .hypothesis/
44
- .pytest_cache/
45
-
46
- # 虚拟环境
47
- venv/
48
- ENV/
49
- env/
50
- .venv
51
-
52
- # Flask
53
- instance/
54
- .webassets-cache
55
- *.db
56
-
57
- # 日志文件
58
- logs/
59
- *.log
60
-
61
- # 环境变量
62
- .env
63
- .env.local
64
-
65
- # IDE
66
- .vscode/
67
- .idea/
68
- *.swp
69
- *.swo
70
- *~
71
- .DS_Store
72
-
73
- # 备份文件
74
- *.backup
75
- *.bak
76
- *.old
77
-
78
- # Jupyter Notebook
79
- .ipynb_checkpoints
80
-
81
- # pyenv
82
- .python-version
83
-
84
- # pipenv
85
- Pipfile.lock
86
-
87
- # poetry
88
- poetry.lock
89
-
90
- # 项目特定
91
- # 如果不想提交某些大文件或临时文件,在这里添加
92
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.vscode/PythonImportHelper-v2-Completion.json DELETED
@@ -1,1277 +0,0 @@
1
- [
2
- {
3
- "label": "os",
4
- "kind": 6,
5
- "isExtraImport": true,
6
- "importPath": "os",
7
- "description": "os",
8
- "detail": "os",
9
- "documentation": {}
10
- },
11
- {
12
- "label": "sys",
13
- "kind": 6,
14
- "isExtraImport": true,
15
- "importPath": "sys",
16
- "description": "sys",
17
- "detail": "sys",
18
- "documentation": {}
19
- },
20
- {
21
- "label": "json",
22
- "kind": 6,
23
- "isExtraImport": true,
24
- "importPath": "json",
25
- "description": "json",
26
- "detail": "json",
27
- "documentation": {}
28
- },
29
- {
30
- "label": "time",
31
- "kind": 6,
32
- "isExtraImport": true,
33
- "importPath": "time",
34
- "description": "time",
35
- "detail": "time",
36
- "documentation": {}
37
- },
38
- {
39
- "label": "datetime",
40
- "importPath": "datetime",
41
- "description": "datetime",
42
- "isExtraImport": true,
43
- "detail": "datetime",
44
- "documentation": {}
45
- },
46
- {
47
- "label": "datetime",
48
- "importPath": "datetime",
49
- "description": "datetime",
50
- "isExtraImport": true,
51
- "detail": "datetime",
52
- "documentation": {}
53
- },
54
- {
55
- "label": "Path",
56
- "importPath": "pathlib",
57
- "description": "pathlib",
58
- "isExtraImport": true,
59
- "detail": "pathlib",
60
- "documentation": {}
61
- },
62
- {
63
- "label": "Path",
64
- "importPath": "pathlib",
65
- "description": "pathlib",
66
- "isExtraImport": true,
67
- "detail": "pathlib",
68
- "documentation": {}
69
- },
70
- {
71
- "label": "Path",
72
- "importPath": "pathlib",
73
- "description": "pathlib",
74
- "isExtraImport": true,
75
- "detail": "pathlib",
76
- "documentation": {}
77
- },
78
- {
79
- "label": "Path",
80
- "importPath": "pathlib",
81
- "description": "pathlib",
82
- "isExtraImport": true,
83
- "detail": "pathlib",
84
- "documentation": {}
85
- },
86
- {
87
- "label": "Path",
88
- "importPath": "pathlib",
89
- "description": "pathlib",
90
- "isExtraImport": true,
91
- "detail": "pathlib",
92
- "documentation": {}
93
- },
94
- {
95
- "label": "Path",
96
- "importPath": "pathlib",
97
- "description": "pathlib",
98
- "isExtraImport": true,
99
- "detail": "pathlib",
100
- "documentation": {}
101
- },
102
- {
103
- "label": "Path",
104
- "importPath": "pathlib",
105
- "description": "pathlib",
106
- "isExtraImport": true,
107
- "detail": "pathlib",
108
- "documentation": {}
109
- },
110
- {
111
- "label": "Flask",
112
- "importPath": "flask",
113
- "description": "flask",
114
- "isExtraImport": true,
115
- "detail": "flask",
116
- "documentation": {}
117
- },
118
- {
119
- "label": "render_template",
120
- "importPath": "flask",
121
- "description": "flask",
122
- "isExtraImport": true,
123
- "detail": "flask",
124
- "documentation": {}
125
- },
126
- {
127
- "label": "send_from_directory",
128
- "importPath": "flask",
129
- "description": "flask",
130
- "isExtraImport": true,
131
- "detail": "flask",
132
- "documentation": {}
133
- },
134
- {
135
- "label": "jsonify",
136
- "importPath": "flask",
137
- "description": "flask",
138
- "isExtraImport": true,
139
- "detail": "flask",
140
- "documentation": {}
141
- },
142
- {
143
- "label": "request",
144
- "importPath": "flask",
145
- "description": "flask",
146
- "isExtraImport": true,
147
- "detail": "flask",
148
- "documentation": {}
149
- },
150
- {
151
- "label": "session",
152
- "importPath": "flask",
153
- "description": "flask",
154
- "isExtraImport": true,
155
- "detail": "flask",
156
- "documentation": {}
157
- },
158
- {
159
- "label": "CORS",
160
- "importPath": "flask_cors",
161
- "description": "flask_cors",
162
- "isExtraImport": true,
163
- "detail": "flask_cors",
164
- "documentation": {}
165
- },
166
- {
167
- "label": "logging",
168
- "kind": 6,
169
- "isExtraImport": true,
170
- "importPath": "logging",
171
- "description": "logging",
172
- "detail": "logging",
173
- "documentation": {}
174
- },
175
- {
176
- "label": "RotatingFileHandler",
177
- "importPath": "logging.handlers",
178
- "description": "logging.handlers",
179
- "isExtraImport": true,
180
- "detail": "logging.handlers",
181
- "documentation": {}
182
- },
183
- {
184
- "label": "get_db_instance",
185
- "importPath": "db_manager",
186
- "description": "db_manager",
187
- "isExtraImport": true,
188
- "detail": "db_manager",
189
- "documentation": {}
190
- },
191
- {
192
- "label": "re",
193
- "kind": 6,
194
- "isExtraImport": true,
195
- "importPath": "re",
196
- "description": "re",
197
- "detail": "re",
198
- "documentation": {}
199
- },
200
- {
201
- "label": "subprocess",
202
- "kind": 6,
203
- "isExtraImport": true,
204
- "importPath": "subprocess",
205
- "description": "subprocess",
206
- "detail": "subprocess",
207
- "documentation": {}
208
- },
209
- {
210
- "label": "urllib.parse",
211
- "kind": 6,
212
- "isExtraImport": true,
213
- "importPath": "urllib.parse",
214
- "description": "urllib.parse",
215
- "detail": "urllib.parse",
216
- "documentation": {}
217
- },
218
- {
219
- "label": "urlparse",
220
- "importPath": "urllib.parse",
221
- "description": "urllib.parse",
222
- "isExtraImport": true,
223
- "detail": "urllib.parse",
224
- "documentation": {}
225
- },
226
- {
227
- "label": "unquote",
228
- "importPath": "urllib.parse",
229
- "description": "urllib.parse",
230
- "isExtraImport": true,
231
- "detail": "urllib.parse",
232
- "documentation": {}
233
- },
234
- {
235
- "label": "List",
236
- "importPath": "typing",
237
- "description": "typing",
238
- "isExtraImport": true,
239
- "detail": "typing",
240
- "documentation": {}
241
- },
242
- {
243
- "label": "Dict",
244
- "importPath": "typing",
245
- "description": "typing",
246
- "isExtraImport": true,
247
- "detail": "typing",
248
- "documentation": {}
249
- },
250
- {
251
- "label": "Set",
252
- "importPath": "typing",
253
- "description": "typing",
254
- "isExtraImport": true,
255
- "detail": "typing",
256
- "documentation": {}
257
- },
258
- {
259
- "label": "Dict",
260
- "importPath": "typing",
261
- "description": "typing",
262
- "isExtraImport": true,
263
- "detail": "typing",
264
- "documentation": {}
265
- },
266
- {
267
- "label": "List",
268
- "importPath": "typing",
269
- "description": "typing",
270
- "isExtraImport": true,
271
- "detail": "typing",
272
- "documentation": {}
273
- },
274
- {
275
- "label": "Optional",
276
- "importPath": "typing",
277
- "description": "typing",
278
- "isExtraImport": true,
279
- "detail": "typing",
280
- "documentation": {}
281
- },
282
- {
283
- "label": "Tuple",
284
- "importPath": "typing",
285
- "description": "typing",
286
- "isExtraImport": true,
287
- "detail": "typing",
288
- "documentation": {}
289
- },
290
- {
291
- "label": "hashlib",
292
- "kind": 6,
293
- "isExtraImport": true,
294
- "importPath": "hashlib",
295
- "description": "hashlib",
296
- "detail": "hashlib",
297
- "documentation": {}
298
- },
299
- {
300
- "label": "sqlite3",
301
- "kind": 6,
302
- "isExtraImport": true,
303
- "importPath": "sqlite3",
304
- "description": "sqlite3",
305
- "detail": "sqlite3",
306
- "documentation": {}
307
- },
308
- {
309
- "label": "requests",
310
- "kind": 6,
311
- "isExtraImport": true,
312
- "importPath": "requests",
313
- "description": "requests",
314
- "detail": "requests",
315
- "documentation": {}
316
- },
317
- {
318
- "label": "multiprocessing",
319
- "kind": 6,
320
- "isExtraImport": true,
321
- "importPath": "multiprocessing",
322
- "description": "multiprocessing",
323
- "detail": "multiprocessing",
324
- "documentation": {}
325
- },
326
- {
327
- "label": "OpenSearch",
328
- "importPath": "opensearchpy",
329
- "description": "opensearchpy",
330
- "isExtraImport": true,
331
- "detail": "opensearchpy",
332
- "documentation": {}
333
- },
334
- {
335
- "label": "RequestsHttpConnection",
336
- "importPath": "opensearchpy",
337
- "description": "opensearchpy",
338
- "isExtraImport": true,
339
- "detail": "opensearchpy",
340
- "documentation": {}
341
- },
342
- {
343
- "label": "OpenSearchException",
344
- "importPath": "opensearchpy.exceptions",
345
- "description": "opensearchpy.exceptions",
346
- "isExtraImport": true,
347
- "detail": "opensearchpy.exceptions",
348
- "documentation": {}
349
- },
350
- {
351
- "label": "setup_logging",
352
- "kind": 2,
353
- "importPath": "app",
354
- "description": "app",
355
- "peekOfCode": "def setup_logging():\n \"\"\"配置日志系统\"\"\"\n if not app.debug:\n # 创建日志目录\n log_dir = Path('logs')\n log_dir.mkdir(exist_ok=True)\n # 文件日志处理器(每个文件最大10MB,保留10个备份)\n file_handler = RotatingFileHandler(\n log_dir / 'app.log',\n maxBytes=10 * 1024 * 1024,",
356
- "detail": "app",
357
- "documentation": {}
358
- },
359
- {
360
- "label": "get_client_ip",
361
- "kind": 2,
362
- "importPath": "app",
363
- "description": "app",
364
- "peekOfCode": "def get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址\n 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部\n \"\"\"\n # 按优先级检查各种代理头部\n headers_to_check = [\n 'X-Forwarded-For',\n 'X-Real-IP',\n 'CF-Connecting-IP', # Cloudflare",
365
- "detail": "app",
366
- "documentation": {}
367
- },
368
- {
369
- "label": "load_book_data",
370
- "kind": 2,
371
- "importPath": "app",
372
- "description": "app",
373
- "peekOfCode": "def load_book_data():\n \"\"\"\n 加载书籍数据(已废弃,保留是为了兼容性)\n 现在使用数据库接口,不再加载JSON文件\n \"\"\"\n global BOOK_DATA\n app.logger.info('ℹ️ load_book_data 已废弃,使用数据库接口')\n # 设置为空字典表示已初始化,但不再使用\n BOOK_DATA = {}\n return True",
374
- "detail": "app",
375
- "documentation": {}
376
- },
377
- {
378
- "label": "init_database",
379
- "kind": 2,
380
- "importPath": "app",
381
- "description": "app",
382
- "peekOfCode": "def init_database():\n \"\"\"初始化数据库连接\"\"\"\n global DB\n try:\n DB = get_db_instance('books.db')\n app.logger.info('✅ 数据库连接初始化成功')\n return True\n except Exception as e:\n app.logger.error(f'❌ 数据库初始化失败: {e}')\n return False",
383
- "detail": "app",
384
- "documentation": {}
385
- },
386
- {
387
- "label": "index",
388
- "kind": 2,
389
- "importPath": "app",
390
- "description": "app",
391
- "peekOfCode": "def index():\n \"\"\"主页 - 书籍目录\"\"\"\n return send_from_directory('.', 'index.html')\n@app.route('/reader')\ndef reader():\n \"\"\"阅读页面\"\"\"\n return send_from_directory('.', 'reader.html')\n@app.route('/<path:filename>')\ndef serve_static(filename):\n \"\"\"提供根目录下的静态文件(如 style.css, script.js)\"\"\"",
392
- "detail": "app",
393
- "documentation": {}
394
- },
395
- {
396
- "label": "reader",
397
- "kind": 2,
398
- "importPath": "app",
399
- "description": "app",
400
- "peekOfCode": "def reader():\n \"\"\"阅读页面\"\"\"\n return send_from_directory('.', 'reader.html')\n@app.route('/<path:filename>')\ndef serve_static(filename):\n \"\"\"提供根目录下的静态文件(如 style.css, script.js)\"\"\"\n # 避免与API路由冲突\n if filename.startswith('api/'):\n return jsonify({'error': '接口不存在'}), 404\n return send_from_directory('.', filename)",
401
- "detail": "app",
402
- "documentation": {}
403
- },
404
- {
405
- "label": "serve_static",
406
- "kind": 2,
407
- "importPath": "app",
408
- "description": "app",
409
- "peekOfCode": "def serve_static(filename):\n \"\"\"提供根目录下的静态文件(如 style.css, script.js)\"\"\"\n # 避免与API路由冲突\n if filename.startswith('api/'):\n return jsonify({'error': '接口不存在'}), 404\n return send_from_directory('.', filename)\n# ============================================================================\n# 路由:API 端点\n# ============================================================================\n@app.route('/api/health')",
410
- "detail": "app",
411
- "documentation": {}
412
- },
413
- {
414
- "label": "health_check",
415
- "kind": 2,
416
- "importPath": "app",
417
- "description": "app",
418
- "peekOfCode": "def health_check():\n \"\"\"健康检查端点\"\"\"\n return jsonify({\n 'status': 'healthy',\n 'timestamp': datetime.now().isoformat(),\n 'version': '2.0.0-flask'\n })\n@app.route('/api/book/info')\ndef get_book_info():\n \"\"\"",
419
- "detail": "app",
420
- "documentation": {}
421
- },
422
- {
423
- "label": "get_book_info",
424
- "kind": 2,
425
- "importPath": "app",
426
- "description": "app",
427
- "peekOfCode": "def get_book_info():\n \"\"\"\n 获取书籍信息(旧接口,保留兼容性)\n 推荐使用: /api/v2/books/<book_id>\n \"\"\"\n try:\n if not DB:\n return jsonify({'error': '数据库未初始化'}), 500\n # 默认获取第一本书的信息(为了兼容旧版本)\n books = DB.get_all_books()",
428
- "detail": "app",
429
- "documentation": {}
430
- },
431
- {
432
- "label": "get_page_content",
433
- "kind": 2,
434
- "importPath": "app",
435
- "description": "app",
436
- "peekOfCode": "def get_page_content(page_num):\n \"\"\"\n 获取指定页面内容(旧接口,保留兼容性)\n 推荐使用: /api/v2/books/<book_id>/pages/<page_num>\n \"\"\"\n try:\n if not DB:\n return jsonify({'error': '数据库未初始化'}), 500\n # 获取最后导入的书籍\n books = DB.get_all_books()",
437
- "detail": "app",
438
- "documentation": {}
439
- },
440
- {
441
- "label": "save_progress",
442
- "kind": 2,
443
- "importPath": "app",
444
- "description": "app",
445
- "peekOfCode": "def save_progress():\n \"\"\"保存学习进度\"\"\"\n try:\n data = request.get_json()\n # 这里可以保存到数据库,现在先存到 session\n if 'progress' not in session:\n session['progress'] = {}\n session['progress'].update({\n 'current_page': data.get('current_page', 0),\n 'bookmarks': data.get('bookmarks', []),",
446
- "detail": "app",
447
- "documentation": {}
448
- },
449
- {
450
- "label": "load_progress",
451
- "kind": 2,
452
- "importPath": "app",
453
- "description": "app",
454
- "peekOfCode": "def load_progress():\n \"\"\"加载学习进度\"\"\"\n progress = session.get('progress', {\n 'current_page': 0,\n 'bookmarks': [],\n 'settings': {}\n })\n return jsonify(progress)\n@app.route('/api/search', methods=['POST'])\ndef search_content():",
455
- "detail": "app",
456
- "documentation": {}
457
- },
458
- {
459
- "label": "search_content",
460
- "kind": 2,
461
- "importPath": "app",
462
- "description": "app",
463
- "peekOfCode": "def search_content():\n \"\"\"搜索内容(未来可集成 OpenSearch)\"\"\"\n try:\n data = request.get_json()\n keyword = data.get('keyword', '').strip()\n if not keyword:\n return jsonify({'error': '搜索关键词不能为空'}), 400\n # 简单搜索实现(在书籍数据中搜索)\n if not BOOK_DATA:\n return jsonify({'error': '书籍数据未加载'}), 500",
464
- "detail": "app",
465
- "documentation": {}
466
- },
467
- {
468
- "label": "opensearch_status",
469
- "kind": 2,
470
- "importPath": "app",
471
- "description": "app",
472
- "peekOfCode": "def opensearch_status():\n \"\"\"检查 OpenSearch 连接状态(示例)\"\"\"\n try:\n # 这里可以实际连接 OpenSearch\n # from opensearchpy import OpenSearch\n # client = OpenSearch([{'host': OPENSEARCH_CONFIG['host'], 'port': OPENSEARCH_CONFIG['port']}])\n # info = client.info()\n return jsonify({\n 'configured': True,\n 'host': OPENSEARCH_CONFIG['host'],",
473
- "detail": "app",
474
- "documentation": {}
475
- },
476
- {
477
- "label": "get_stats",
478
- "kind": 2,
479
- "importPath": "app",
480
- "description": "app",
481
- "peekOfCode": "def get_stats():\n \"\"\"获取学习统计数据\"\"\"\n # 从 session 或数据库获取统计数据\n progress = session.get('progress', {})\n return jsonify({\n 'current_page': progress.get('current_page', 0),\n 'total_bookmarks': len(progress.get('bookmarks', [])),\n 'last_visit': progress.get('last_updated', None),\n 'total_pages': len(BOOK_DATA.get('pages', [])) if BOOK_DATA else 0\n })",
482
- "detail": "app",
483
- "documentation": {}
484
- },
485
- {
486
- "label": "get_books",
487
- "kind": 2,
488
- "importPath": "app",
489
- "description": "app",
490
- "peekOfCode": "def get_books():\n \"\"\"\n 获取所有书籍列表\n Query Parameters:\n grade_id: 年级ID(可选)\n Returns:\n {\n \"success\": true,\n \"count\": 10,\n \"books\": [",
491
- "detail": "app",
492
- "documentation": {}
493
- },
494
- {
495
- "label": "get_book_info_v2",
496
- "kind": 2,
497
- "importPath": "app",
498
- "description": "app",
499
- "peekOfCode": "def get_book_info_v2(book_id):\n \"\"\"\n 获取指定书籍的详细信息\n Args:\n book_id: 书籍ID\n Returns:\n {\n \"success\": true,\n \"book\": {\n \"book_id\": 1,",
500
- "detail": "app",
501
- "documentation": {}
502
- },
503
- {
504
- "label": "get_book_pages",
505
- "kind": 2,
506
- "importPath": "app",
507
- "description": "app",
508
- "peekOfCode": "def get_book_pages(book_id):\n \"\"\"\n 获取书籍的所有页面列表(不含片段内容)\n Args:\n book_id: 书籍ID\n Returns:\n {\n \"success\": true,\n \"book_id\": 168,\n \"book_name\": \"一年级上册\",",
509
- "detail": "app",
510
- "documentation": {}
511
- },
512
- {
513
- "label": "get_book_catalog",
514
- "kind": 2,
515
- "importPath": "app",
516
- "description": "app",
517
- "peekOfCode": "def get_book_catalog(book_id):\n \"\"\"\n 获取书籍目录结构(章节目录)\n Args:\n book_id: 书籍ID\n Returns:\n {\n \"success\": true,\n \"book_id\": 168,\n \"book_name\": \"一年级上册\",",
518
- "detail": "app",
519
- "documentation": {}
520
- },
521
- {
522
- "label": "get_page_content_v2",
523
- "kind": 2,
524
- "importPath": "app",
525
- "description": "app",
526
- "peekOfCode": "def get_page_content_v2(book_id, page_num):\n \"\"\"\n 获取指定页面的完整内容\n Args:\n book_id: 书籍ID\n page_num: 页码\n Returns:\n {\n \"success\": true,\n \"book_id\": 1,",
527
- "detail": "app",
528
- "documentation": {}
529
- },
530
- {
531
- "label": "search_book_content",
532
- "kind": 2,
533
- "importPath": "app",
534
- "description": "app",
535
- "peekOfCode": "def search_book_content(book_id):\n \"\"\"\n 在书籍中搜索内容\n Query Parameters:\n keyword: 搜索关键词\n limit: 返回结果数量(默认20)\n Returns:\n {\n \"success\": true,\n \"book_id\": 168,",
536
- "detail": "app",
537
- "documentation": {}
538
- },
539
- {
540
- "label": "search_all_content",
541
- "kind": 2,
542
- "importPath": "app",
543
- "description": "app",
544
- "peekOfCode": "def search_all_content():\n \"\"\"\n 在所有书籍中搜索内容\n Query Parameters:\n keyword: 搜索关键词\n limit: 返回结果数量(默认50)\n Returns:\n {\n \"success\": true,\n \"keyword\": \"hello\",",
545
- "detail": "app",
546
- "documentation": {}
547
- },
548
- {
549
- "label": "get_book_statistics",
550
- "kind": 2,
551
- "importPath": "app",
552
- "description": "app",
553
- "peekOfCode": "def get_book_statistics(book_id):\n \"\"\"\n 获取书籍统计信息\n Args:\n book_id: 书籍ID\n Returns:\n {\n \"success\": true,\n \"statistics\": {\n \"book_id\": 168,",
554
- "detail": "app",
555
- "documentation": {}
556
- },
557
- {
558
- "label": "get_overall_statistics",
559
- "kind": 2,
560
- "importPath": "app",
561
- "description": "app",
562
- "peekOfCode": "def get_overall_statistics():\n \"\"\"\n 获取整体统计信息\n Returns:\n {\n \"success\": true,\n \"statistics\": {\n \"total_books\": 30,\n \"total_pages\": 2000,\n \"total_pieces\": 50000,",
563
- "detail": "app",
564
- "documentation": {}
565
- },
566
- {
567
- "label": "not_found",
568
- "kind": 2,
569
- "importPath": "app",
570
- "description": "app",
571
- "peekOfCode": "def not_found(error):\n \"\"\"404错误处理\"\"\"\n if request.path.startswith('/api/'):\n return jsonify({'error': '接口不存在'}), 404\n return send_from_directory('.', 'index.html')\n@app.errorhandler(500)\ndef internal_error(error):\n \"\"\"500错误处理\"\"\"\n app.logger.error(f'服务器错误: {error}')\n return jsonify({'error': '服务器内部错误'}), 500",
572
- "detail": "app",
573
- "documentation": {}
574
- },
575
- {
576
- "label": "internal_error",
577
- "kind": 2,
578
- "importPath": "app",
579
- "description": "app",
580
- "peekOfCode": "def internal_error(error):\n \"\"\"500错误处理\"\"\"\n app.logger.error(f'服务器错误: {error}')\n return jsonify({'error': '服务器内部错误'}), 500\n# ============================================================================\n# 请求钩子\n# ============================================================================\n@app.before_request\ndef before_request():\n \"\"\"请求前处理\"\"\"",
581
- "detail": "app",
582
- "documentation": {}
583
- },
584
- {
585
- "label": "before_request",
586
- "kind": 2,
587
- "importPath": "app",
588
- "description": "app",
589
- "peekOfCode": "def before_request():\n \"\"\"请求前处理\"\"\"\n # 获取客户端真实 IP\n client_ip = get_client_ip()\n # 记录所有请求信息(包括静态文件)\n user_agent = request.headers.get('User-Agent', 'Unknown')[:100] # 限制长度\n # API 请求记录详细信息\n if request.path.startswith('/api/'):\n app.logger.info(\n f\"[{client_ip}] {request.method} {request.path} \"",
590
- "detail": "app",
591
- "documentation": {}
592
- },
593
- {
594
- "label": "after_request",
595
- "kind": 2,
596
- "importPath": "app",
597
- "description": "app",
598
- "peekOfCode": "def after_request(response):\n \"\"\"请求后处理 - 添加缓存控制\"\"\"\n # API 端点不缓存\n if request.path.startswith('/api/'):\n response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'\n response.headers['Pragma'] = 'no-cache'\n response.headers['Expires'] = '0'\n else:\n # 静态资源缓存1小时\n response.headers['Cache-Control'] = 'public, max-age=3600'",
599
- "detail": "app",
600
- "documentation": {}
601
- },
602
- {
603
- "label": "auto_generate_database",
604
- "kind": 2,
605
- "importPath": "app",
606
- "description": "app",
607
- "peekOfCode": "def auto_generate_database():\n \"\"\"\n 自动生成数据库\n 如果 books.db 不存在,自动运行导入脚本生成数据库\n \"\"\"\n db_path = 'books.db'\n schema_path = 'db_schema.sql'\n data_dir = 'books_api_data'\n # 检查必要的文件和目录\n if not os.path.exists(schema_path):",
608
- "detail": "app",
609
- "documentation": {}
610
- },
611
- {
612
- "label": "initialize_app",
613
- "kind": 2,
614
- "importPath": "app",
615
- "description": "app",
616
- "peekOfCode": "def initialize_app():\n \"\"\"初始化应用\"\"\"\n print(\"🚀 交互式英语学习应用 - Flask 版本\")\n print(\"=\" * 60)\n # 设置日志\n setup_logging()\n # 检查必要文件(暂时不检查 books.db,因为会自动生成)\n if not os.path.exists('index.html'):\n app.logger.error(f\"❌ 缺少必要文件: index.html\")\n return False",
617
- "detail": "app",
618
- "documentation": {}
619
- },
620
- {
621
- "label": "main",
622
- "kind": 2,
623
- "importPath": "app",
624
- "description": "app",
625
- "peekOfCode": "def main():\n \"\"\"主函数\"\"\"\n # 初始化应用\n if not initialize_app():\n print(\"❌ 应用初始化失败\")\n return 1\n # Hugging Face Spaces 要求监听 7860 端口\n port = int(os.environ.get('PORT', 7860))\n debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'\n print(f\"🌐 监听端口: {port}\")",
626
- "detail": "app",
627
- "documentation": {}
628
- },
629
- {
630
- "label": "app",
631
- "kind": 5,
632
- "importPath": "app",
633
- "description": "app",
634
- "peekOfCode": "app = Flask(__name__, \n static_folder='static',\n static_url_path='/static')\n# 配置\napp.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')\napp.config['JSON_AS_ASCII'] = False # 支持中文JSON\napp.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 最大上传16MB\n# 启用CORS\nCORS(app, resources={\n r\"/*\": {",
635
- "detail": "app",
636
- "documentation": {}
637
- },
638
- {
639
- "label": "app.config['SECRET_KEY']",
640
- "kind": 5,
641
- "importPath": "app",
642
- "description": "app",
643
- "peekOfCode": "app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')\napp.config['JSON_AS_ASCII'] = False # 支持中文JSON\napp.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 最大上传16MB\n# 启用CORS\nCORS(app, resources={\n r\"/*\": {\n \"origins\": \"*\",\n \"methods\": [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"],\n \"allow_headers\": [\"Content-Type\", \"Authorization\"]\n }",
644
- "detail": "app",
645
- "documentation": {}
646
- },
647
- {
648
- "label": "app.config['JSON_AS_ASCII']",
649
- "kind": 5,
650
- "importPath": "app",
651
- "description": "app",
652
- "peekOfCode": "app.config['JSON_AS_ASCII'] = False # 支持中文JSON\napp.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 最大上传16MB\n# 启用CORS\nCORS(app, resources={\n r\"/*\": {\n \"origins\": \"*\",\n \"methods\": [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"],\n \"allow_headers\": [\"Content-Type\", \"Authorization\"]\n }\n})",
653
- "detail": "app",
654
- "documentation": {}
655
- },
656
- {
657
- "label": "app.config['MAX_CONTENT_LENGTH']",
658
- "kind": 5,
659
- "importPath": "app",
660
- "description": "app",
661
- "peekOfCode": "app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 最大上传16MB\n# 启用CORS\nCORS(app, resources={\n r\"/*\": {\n \"origins\": \"*\",\n \"methods\": [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"],\n \"allow_headers\": [\"Content-Type\", \"Authorization\"]\n }\n})\n# 配置日志",
662
- "detail": "app",
663
- "documentation": {}
664
- },
665
- {
666
- "label": "OPENSEARCH_CONFIG",
667
- "kind": 5,
668
- "importPath": "app",
669
- "description": "app",
670
- "peekOfCode": "OPENSEARCH_CONFIG = {\n 'host': os.environ.get('OPENSEARCH_HOST', '192.168.3.33'),\n 'port': int(os.environ.get('OPENSEARCH_PORT', 9200)),\n 'use_ssl': os.environ.get('OPENSEARCH_USE_SSL', 'False').lower() == 'true',\n}\n# 全局变量:存储学习数据\nBOOK_DATA = None\n# 数据库实例\nDB = None\ndef get_client_ip():",
671
- "detail": "app",
672
- "documentation": {}
673
- },
674
- {
675
- "label": "BOOK_DATA",
676
- "kind": 5,
677
- "importPath": "app",
678
- "description": "app",
679
- "peekOfCode": "BOOK_DATA = None\n# 数据库实例\nDB = None\ndef get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址\n 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部\n \"\"\"\n # 按优先级检查各种代理头部\n headers_to_check = [",
680
- "detail": "app",
681
- "documentation": {}
682
- },
683
- {
684
- "label": "DB",
685
- "kind": 5,
686
- "importPath": "app",
687
- "description": "app",
688
- "peekOfCode": "DB = None\ndef get_client_ip():\n \"\"\"\n 获取客户端真实 IP 地址\n 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部\n \"\"\"\n # 按优先级检查各种代理头部\n headers_to_check = [\n 'X-Forwarded-For',\n 'X-Real-IP',",
689
- "detail": "app",
690
- "documentation": {}
691
- },
692
- {
693
- "label": "ResourceManager",
694
- "kind": 6,
695
- "importPath": "check_and_download",
696
- "description": "check_and_download",
697
- "peekOfCode": "class ResourceManager:\n def __init__(self, json_file: str, assets_dir: str):\n self.json_file = json_file\n self.assets_dir = Path(assets_dir)\n self.audio_dir = self.assets_dir / \"audios\"\n self.image_dir = self.assets_dir / \"images\"\n # 确保目录存在\n self.audio_dir.mkdir(parents=True, exist_ok=True)\n self.image_dir.mkdir(parents=True, exist_ok=True)\n def load_json_data(self) -> Dict:",
698
- "detail": "check_and_download",
699
- "documentation": {}
700
- },
701
- {
702
- "label": "main",
703
- "kind": 2,
704
- "importPath": "check_and_download",
705
- "description": "check_and_download",
706
- "peekOfCode": "def main():\n rm = ResourceManager(\"book_10242.json\", \"assets\")\n print(\"=== 资源完整性检查和下载脚本 ===\")\n # 检查并找出缺失的文件\n missing_urls = rm.check_and_find_missing()\n if not missing_urls[\"audio\"] and not missing_urls[\"image\"]:\n print(\"\\n所有文件都已正确下载,无需额外操作!\")\n return\n # 生成下载脚本\n script_content = rm.generate_download_script(missing_urls)",
707
- "detail": "check_and_download",
708
- "documentation": {}
709
- },
710
- {
711
- "label": "Config",
712
- "kind": 6,
713
- "importPath": "config",
714
- "description": "config",
715
- "peekOfCode": "class Config:\n \"\"\"基础配置\"\"\"\n # Flask 基础配置\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-please-change-in-production'\n JSON_AS_ASCII = False # 支持中文JSON\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 最大上传16MB\n # 会话配置\n SESSION_TYPE = 'filesystem'\n SESSION_PERMANENT = False\n SESSION_USE_SIGNER = True",
716
- "detail": "config",
717
- "documentation": {}
718
- },
719
- {
720
- "label": "DevelopmentConfig",
721
- "kind": 6,
722
- "importPath": "config",
723
- "description": "config",
724
- "peekOfCode": "class DevelopmentConfig(Config):\n \"\"\"开发环境配置\"\"\"\n DEBUG = True\n TESTING = False\nclass ProductionConfig(Config):\n \"\"\"生产环境配置\"\"\"\n DEBUG = False\n TESTING = False\n # 生产环境应该从环境变量读取敏感信息\n # SECRET_KEY 验证在应用启动时进行,而不是在导入时",
725
- "detail": "config",
726
- "documentation": {}
727
- },
728
- {
729
- "label": "ProductionConfig",
730
- "kind": 6,
731
- "importPath": "config",
732
- "description": "config",
733
- "peekOfCode": "class ProductionConfig(Config):\n \"\"\"生产环境配置\"\"\"\n DEBUG = False\n TESTING = False\n # 生产环境应该从环境变量读取敏感信息\n # SECRET_KEY 验证在应用启动时进行,而不是在导入时\n SECRET_KEY = os.environ.get('SECRET_KEY', Config.SECRET_KEY)\nclass TestingConfig(Config):\n \"\"\"测试环境配置\"\"\"\n TESTING = True",
734
- "detail": "config",
735
- "documentation": {}
736
- },
737
- {
738
- "label": "TestingConfig",
739
- "kind": 6,
740
- "importPath": "config",
741
- "description": "config",
742
- "peekOfCode": "class TestingConfig(Config):\n \"\"\"测试环境配置\"\"\"\n TESTING = True\n DEBUG = True\n # 使用内存数据库进行测试\n # SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'\n# 配置字典\nconfig = {\n 'development': DevelopmentConfig,\n 'production': ProductionConfig,",
743
- "detail": "config",
744
- "documentation": {}
745
- },
746
- {
747
- "label": "config",
748
- "kind": 5,
749
- "importPath": "config",
750
- "description": "config",
751
- "peekOfCode": "config = {\n 'development': DevelopmentConfig,\n 'production': ProductionConfig,\n 'testing': TestingConfig,\n 'default': DevelopmentConfig\n}",
752
- "detail": "config",
753
- "documentation": {}
754
- },
755
- {
756
- "label": "DatabaseManager",
757
- "kind": 6,
758
- "importPath": "db_manager",
759
- "description": "db_manager",
760
- "peekOfCode": "class DatabaseManager:\n \"\"\"数据库管理器类\"\"\"\n def __init__(self, db_path: str = 'books.db'):\n \"\"\"\n 初始化数据库管理器\n Args:\n db_path: 数据库文件路径\n \"\"\"\n self.db_path = db_path\n self.conn = None",
761
- "detail": "db_manager",
762
- "documentation": {}
763
- },
764
- {
765
- "label": "get_db_instance",
766
- "kind": 2,
767
- "importPath": "db_manager",
768
- "description": "db_manager",
769
- "peekOfCode": "def get_db_instance(db_path: str = 'books.db') -> DatabaseManager:\n \"\"\"\n 获取数据库管理器单例\n Args:\n db_path: 数据库文件路径\n Returns:\n DatabaseManager实例\n \"\"\"\n global _db_instance\n if _db_instance is None:",
770
- "detail": "db_manager",
771
- "documentation": {}
772
- },
773
- {
774
- "label": "close_db",
775
- "kind": 2,
776
- "importPath": "db_manager",
777
- "description": "db_manager",
778
- "peekOfCode": "def close_db():\n \"\"\"关闭全局数据库连接\"\"\"\n global _db_instance\n if _db_instance:\n _db_instance.close()\n _db_instance = None",
779
- "detail": "db_manager",
780
- "documentation": {}
781
- },
782
- {
783
- "label": "logger",
784
- "kind": 5,
785
- "importPath": "db_manager",
786
- "description": "db_manager",
787
- "peekOfCode": "logger = logging.getLogger(__name__)\nclass DatabaseManager:\n \"\"\"数据库管理器类\"\"\"\n def __init__(self, db_path: str = 'books.db'):\n \"\"\"\n 初始化数据库管理器\n Args:\n db_path: 数据库文件路径\n \"\"\"\n self.db_path = db_path",
788
- "detail": "db_manager",
789
- "documentation": {}
790
- },
791
- {
792
- "label": "_db_instance",
793
- "kind": 5,
794
- "importPath": "db_manager",
795
- "description": "db_manager",
796
- "peekOfCode": "_db_instance = None\ndef get_db_instance(db_path: str = 'books.db') -> DatabaseManager:\n \"\"\"\n 获取数据库管理器单例\n Args:\n db_path: 数据库文件路径\n Returns:\n DatabaseManager实例\n \"\"\"\n global _db_instance",
797
- "detail": "db_manager",
798
- "documentation": {}
799
- },
800
- {
801
- "label": "main",
802
- "kind": 2,
803
- "importPath": "debug_pages",
804
- "description": "debug_pages",
805
- "peekOfCode": "def main():\n \"\"\"检查第4页和第5页的数据\"\"\"\n try:\n with open('book_10242.json', 'r', encoding='utf-8') as f:\n data = json.load(f)\n # 解析Data字段中的JSON字符串\n pages_data = json.loads(data['Data'])\n print(f\"总页数: {len(pages_data)}\")\n print(\"=\" * 80)\n # 检查第4页和第5页(数组索引为3和4)",
806
- "detail": "debug_pages",
807
- "documentation": {}
808
- },
809
- {
810
- "label": "extract_filename_from_url",
811
- "kind": 2,
812
- "importPath": "delete_encrypted_files",
813
- "description": "delete_encrypted_files",
814
- "peekOfCode": "def extract_filename_from_url(url):\n \"\"\"从URL中提取文件名\"\"\"\n if not url:\n return None\n # 解析URL,去掉查询参数\n parsed = urlparse(url)\n path = parsed.path\n # URL解码\n path = unquote(path)\n # 提取文件名",
815
- "detail": "delete_encrypted_files",
816
- "documentation": {}
817
- },
818
- {
819
- "label": "main",
820
- "kind": 2,
821
- "importPath": "delete_encrypted_files",
822
- "description": "delete_encrypted_files",
823
- "peekOfCode": "def main():\n json_file = '/data/zhangl/code/hf/point/book_10242.json'\n audio_dir = '/data/zhangl/code/hf/point/assets/audios'\n image_dir = '/data/zhangl/code/hf/point/assets/images'\n print(\"正在解析 JSON 文件...\")\n try:\n with open(json_file, 'r', encoding='utf-8') as f:\n data = json.load(f)\n # 解析Data字段中的JSON字符串\n pages_data = json.loads(data['Data'])",
824
- "detail": "delete_encrypted_files",
825
- "documentation": {}
826
- },
827
- {
828
- "label": "create_directories",
829
- "kind": 2,
830
- "importPath": "download_resources",
831
- "description": "download_resources",
832
- "peekOfCode": "def create_directories():\n \"\"\"创建assets目录结构\"\"\"\n assets_dir = Path(\"assets\")\n images_dir = assets_dir / \"images\"\n audios_dir = assets_dir / \"audios\"\n images_dir.mkdir(parents=True, exist_ok=True)\n audios_dir.mkdir(parents=True, exist_ok=True)\n print(f\"✅ 创建目录: {images_dir}\")\n print(f\"✅ 创建目录: {audios_dir}\")\n return images_dir, audios_dir",
833
- "detail": "download_resources",
834
- "documentation": {}
835
- },
836
- {
837
- "label": "get_filename_from_url",
838
- "kind": 2,
839
- "importPath": "download_resources",
840
- "description": "download_resources",
841
- "peekOfCode": "def get_filename_from_url(url):\n \"\"\"从URL中提取文件名\"\"\"\n # 解析URL并获取路径部分\n parsed_url = urllib.parse.urlparse(url)\n path = parsed_url.path\n # 获取文件名\n filename = os.path.basename(path)\n # 如果没有文件名,使用URL的hash作为文件名\n if not filename or '.' not in filename:\n url_hash = hashlib.md5(url.encode()).hexdigest()[:8]",
842
- "detail": "download_resources",
843
- "documentation": {}
844
- },
845
- {
846
- "label": "download_file",
847
- "kind": 2,
848
- "importPath": "download_resources",
849
- "description": "download_resources",
850
- "peekOfCode": "def download_file(url, save_path, max_retries=3):\n \"\"\"下载文件\"\"\"\n headers = {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'\n }\n for attempt in range(max_retries):\n try:\n print(f\"📥 下载中: {os.path.basename(save_path)}\")\n response = requests.get(url, headers=headers, timeout=30)\n response.raise_for_status()",
851
- "detail": "download_resources",
852
- "documentation": {}
853
- },
854
- {
855
- "label": "extract_urls_from_json",
856
- "kind": 2,
857
- "importPath": "download_resources",
858
- "description": "download_resources",
859
- "peekOfCode": "def extract_urls_from_json(json_file):\n \"\"\"从JSON文件中提取所有图片和音频URL\"\"\"\n with open(json_file, 'r', encoding='utf-8') as f:\n data = json.load(f)\n urls = {\n 'images': set(),\n 'audios': set()\n }\n # 解析Data字段中的JSON字符串\n if 'Data' in data:",
860
- "detail": "download_resources",
861
- "documentation": {}
862
- },
863
- {
864
- "label": "main",
865
- "kind": 2,
866
- "importPath": "download_resources",
867
- "description": "download_resources",
868
- "peekOfCode": "def main():\n \"\"\"主函数\"\"\"\n print(\"🚀 开始下载资源...\")\n # 创建目录\n images_dir, audios_dir = create_directories()\n # 提取URL\n print(\"📋 解析JSON文件...\")\n urls = extract_urls_from_json('book_10242.json')\n print(f\"📊 找到 {len(urls['images'])} 个图片URL\")\n print(f\"📊 找到 {len(urls['audios'])} 个音频URL\")",
869
- "detail": "download_resources",
870
- "documentation": {}
871
- },
872
- {
873
- "label": "extract_data_field",
874
- "kind": 2,
875
- "importPath": "extract_data",
876
- "description": "extract_data",
877
- "peekOfCode": "def extract_data_field(input_file, output_file):\n \"\"\"\n 从输入文件中提取Data字段并保存到输出文件\n Args:\n input_file: 输入JSON文件路径\n output_file: 输出JSON文件路径\n \"\"\"\n try:\n # 读取输入文件\n print(f\"正在读取文件: {input_file}\")",
878
- "detail": "extract_data",
879
- "documentation": {}
880
- },
881
- {
882
- "label": "on_starting",
883
- "kind": 2,
884
- "importPath": "gunicorn_config",
885
- "description": "gunicorn_config",
886
- "peekOfCode": "def on_starting(server):\n \"\"\"服务器启动时执行\"\"\"\n print(\"=\" * 60)\n print(\"🚀 Gunicorn 服务器启动\")\n print(f\"📍 绑定地址: {bind}\")\n print(f\"👷 工作进程数: {workers}\")\n print(f\"🧵 每进程线程数: {threads}\")\n print(f\"⏱️ 超时时间: {timeout}s\")\n print(\"=\" * 60)\ndef worker_int(worker):",
887
- "detail": "gunicorn_config",
888
- "documentation": {}
889
- },
890
- {
891
- "label": "worker_int",
892
- "kind": 2,
893
- "importPath": "gunicorn_config",
894
- "description": "gunicorn_config",
895
- "peekOfCode": "def worker_int(worker):\n \"\"\"工作进程被中断时执行\"\"\"\n print(f\"⚠️ 工作进程 {worker.pid} 被中断\")\ndef worker_abort(worker):\n \"\"\"工作进程异常退出时执行\"\"\"\n print(f\"❌ 工作进程 {worker.pid} 异常退出\")\ndef post_worker_init(worker):\n \"\"\"工作进程初始化后执行\"\"\"\n print(f\"✅ 工作进程 {worker.pid} 初始化完成\")",
896
- "detail": "gunicorn_config",
897
- "documentation": {}
898
- },
899
- {
900
- "label": "worker_abort",
901
- "kind": 2,
902
- "importPath": "gunicorn_config",
903
- "description": "gunicorn_config",
904
- "peekOfCode": "def worker_abort(worker):\n \"\"\"工作进程异常退出时执行\"\"\"\n print(f\"❌ 工作进程 {worker.pid} 异常退出\")\ndef post_worker_init(worker):\n \"\"\"工作进程初始化后执行\"\"\"\n print(f\"✅ 工作进程 {worker.pid} 初始化完成\")",
905
- "detail": "gunicorn_config",
906
- "documentation": {}
907
- },
908
- {
909
- "label": "post_worker_init",
910
- "kind": 2,
911
- "importPath": "gunicorn_config",
912
- "description": "gunicorn_config",
913
- "peekOfCode": "def post_worker_init(worker):\n \"\"\"工作进程初始化后执行\"\"\"\n print(f\"✅ 工作进程 {worker.pid} 初始化完成\")",
914
- "detail": "gunicorn_config",
915
- "documentation": {}
916
- },
917
- {
918
- "label": "bind",
919
- "kind": 5,
920
- "importPath": "gunicorn_config",
921
- "description": "gunicorn_config",
922
- "peekOfCode": "bind = f\"0.0.0.0:{os.environ.get('PORT', 7860)}\"\n# 工作进程数(根据 CPU 核心数自动调整)\nworkers = int(os.environ.get('GUNICORN_WORKERS', multiprocessing.cpu_count() * 2 + 1))\n# 如果资源受限,可以手动设置较小的值\nworkers = min(workers, 4)\n# 工作进程类型\nworker_class = 'sync'\n# 每个工作进程的线程数\nthreads = 2\n# 超时时间(秒)",
923
- "detail": "gunicorn_config",
924
- "documentation": {}
925
- },
926
- {
927
- "label": "workers",
928
- "kind": 5,
929
- "importPath": "gunicorn_config",
930
- "description": "gunicorn_config",
931
- "peekOfCode": "workers = int(os.environ.get('GUNICORN_WORKERS', multiprocessing.cpu_count() * 2 + 1))\n# 如果资源受限,可以手动设置较小的值\nworkers = min(workers, 4)\n# 工作进程类型\nworker_class = 'sync'\n# 每个工作进程的线程数\nthreads = 2\n# 超时时间(秒)\ntimeout = 120\n# 保持活动连接的时间",
932
- "detail": "gunicorn_config",
933
- "documentation": {}
934
- },
935
- {
936
- "label": "workers",
937
- "kind": 5,
938
- "importPath": "gunicorn_config",
939
- "description": "gunicorn_config",
940
- "peekOfCode": "workers = min(workers, 4)\n# 工作进程类型\nworker_class = 'sync'\n# 每个工作进程的线程数\nthreads = 2\n# 超时时间(秒)\ntimeout = 120\n# 保持活动连接的时间\nkeepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)",
941
- "detail": "gunicorn_config",
942
- "documentation": {}
943
- },
944
- {
945
- "label": "worker_class",
946
- "kind": 5,
947
- "importPath": "gunicorn_config",
948
- "description": "gunicorn_config",
949
- "peekOfCode": "worker_class = 'sync'\n# 每个工作进程的线程数\nthreads = 2\n# 超时时间(秒)\ntimeout = 120\n# 保持活动连接的时间\nkeepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)\nmax_requests = 1000\nmax_requests_jitter = 50",
950
- "detail": "gunicorn_config",
951
- "documentation": {}
952
- },
953
- {
954
- "label": "threads",
955
- "kind": 5,
956
- "importPath": "gunicorn_config",
957
- "description": "gunicorn_config",
958
- "peekOfCode": "threads = 2\n# 超时时间(秒)\ntimeout = 120\n# 保持活动连接的时间\nkeepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)\nmax_requests = 1000\nmax_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout",
959
- "detail": "gunicorn_config",
960
- "documentation": {}
961
- },
962
- {
963
- "label": "timeout",
964
- "kind": 5,
965
- "importPath": "gunicorn_config",
966
- "description": "gunicorn_config",
967
- "peekOfCode": "timeout = 120\n# 保持活动连接的时间\nkeepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)\nmax_requests = 1000\nmax_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'",
968
- "detail": "gunicorn_config",
969
- "documentation": {}
970
- },
971
- {
972
- "label": "keepalive",
973
- "kind": 5,
974
- "importPath": "gunicorn_config",
975
- "description": "gunicorn_config",
976
- "peekOfCode": "keepalive = 5\n# 最大请求数(处理后重启进程,防止内存泄漏)\nmax_requests = 1000\nmax_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址",
977
- "detail": "gunicorn_config",
978
- "documentation": {}
979
- },
980
- {
981
- "label": "max_requests",
982
- "kind": 5,
983
- "importPath": "gunicorn_config",
984
- "description": "gunicorn_config",
985
- "peekOfCode": "max_requests = 1000\nmax_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间",
986
- "detail": "gunicorn_config",
987
- "documentation": {}
988
- },
989
- {
990
- "label": "max_requests_jitter",
991
- "kind": 5,
992
- "importPath": "gunicorn_config",
993
- "description": "gunicorn_config",
994
- "peekOfCode": "max_requests_jitter = 50\n# 日志配置\naccesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间\n# %(m)s - 请求方法",
995
- "detail": "gunicorn_config",
996
- "documentation": {}
997
- },
998
- {
999
- "label": "accesslog",
1000
- "kind": 5,
1001
- "importPath": "gunicorn_config",
1002
- "description": "gunicorn_config",
1003
- "peekOfCode": "accesslog = '-' # 输出到 stdout\nerrorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间\n# %(m)s - 请求方法\n# %(U)s - URL 路径\n# %(q)s - 查询字符串",
1004
- "detail": "gunicorn_config",
1005
- "documentation": {}
1006
- },
1007
- {
1008
- "label": "errorlog",
1009
- "kind": 5,
1010
- "importPath": "gunicorn_config",
1011
- "description": "gunicorn_config",
1012
- "peekOfCode": "errorlog = '-' # 输出到 stderr\nloglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间\n# %(m)s - 请求方法\n# %(U)s - URL 路径\n# %(q)s - 查询字符串\n# %(s)s - 状态码",
1013
- "detail": "gunicorn_config",
1014
- "documentation": {}
1015
- },
1016
- {
1017
- "label": "loglevel",
1018
- "kind": 5,
1019
- "importPath": "gunicorn_config",
1020
- "description": "gunicorn_config",
1021
- "peekOfCode": "loglevel = 'info'\n# 自定义访问日志格式 - 包含真实客户端 IP\n# %(h)s - 远程地址\n# %({X-Forwarded-For}i)s - X-Forwarded-For 头部(代理后面的真实 IP)\n# %(t)s - 时间\n# %(m)s - 请求方法\n# %(U)s - URL 路径\n# %(q)s - 查询字符串\n# %(s)s - 状态码\n# %(b)s - 响应大小",
1022
- "detail": "gunicorn_config",
1023
- "documentation": {}
1024
- },
1025
- {
1026
- "label": "access_log_format",
1027
- "kind": 5,
1028
- "importPath": "gunicorn_config",
1029
- "description": "gunicorn_config",
1030
- "peekOfCode": "access_log_format = (\n '[%(t)s] '\n 'Client: %({X-Forwarded-For}i)s (Remote: %(h)s) '\n '%(m)s %(U)s%(q)s '\n 'Status: %(s)s '\n 'Size: %(b)s bytes '\n 'Time: %(D)s μs '\n 'UA: \"%({User-Agent}i)s\"'\n)\n# 进程名称前缀",
1031
- "detail": "gunicorn_config",
1032
- "documentation": {}
1033
- },
1034
- {
1035
- "label": "proc_name",
1036
- "kind": 5,
1037
- "importPath": "gunicorn_config",
1038
- "description": "gunicorn_config",
1039
- "peekOfCode": "proc_name = 'english_learning_app'\n# 守护进程(通常设为 False,让容器管理进程)\ndaemon = False\n# 预加载应用(减少内存占用)\npreload_app = True\n# 在请求处理前后执行的钩子\ndef on_starting(server):\n \"\"\"服务器启动时执行\"\"\"\n print(\"=\" * 60)\n print(\"🚀 Gunicorn 服务器启动\")",
1040
- "detail": "gunicorn_config",
1041
- "documentation": {}
1042
- },
1043
- {
1044
- "label": "daemon",
1045
- "kind": 5,
1046
- "importPath": "gunicorn_config",
1047
- "description": "gunicorn_config",
1048
- "peekOfCode": "daemon = False\n# 预加载应用(减少内存占用)\npreload_app = True\n# 在请求处理前后执行的钩子\ndef on_starting(server):\n \"\"\"服务器启动时执行\"\"\"\n print(\"=\" * 60)\n print(\"🚀 Gunicorn 服务器启动\")\n print(f\"📍 绑定地址: {bind}\")\n print(f\"👷 工作进程数: {workers}\")",
1049
- "detail": "gunicorn_config",
1050
- "documentation": {}
1051
- },
1052
- {
1053
- "label": "preload_app",
1054
- "kind": 5,
1055
- "importPath": "gunicorn_config",
1056
- "description": "gunicorn_config",
1057
- "peekOfCode": "preload_app = True\n# 在请求处理前后执行的钩子\ndef on_starting(server):\n \"\"\"服务器启动时执行\"\"\"\n print(\"=\" * 60)\n print(\"🚀 Gunicorn 服务器启动\")\n print(f\"📍 绑定地址: {bind}\")\n print(f\"👷 工作进程数: {workers}\")\n print(f\"🧵 每进程线程数: {threads}\")\n print(f\"⏱️ 超时时间: {timeout}s\")",
1058
- "detail": "gunicorn_config",
1059
- "documentation": {}
1060
- },
1061
- {
1062
- "label": "create_database",
1063
- "kind": 2,
1064
- "importPath": "import_book_data",
1065
- "description": "import_book_data",
1066
- "peekOfCode": "def create_database(db_path='books.db', schema_path='db_schema.sql'):\n \"\"\"\n 创建数据库和表结构\n Args:\n db_path: 数据库文件路径\n schema_path: Schema SQL文件路径\n Returns:\n sqlite3.Connection: 数据库连接对象\n \"\"\"\n logger.info(f\"📦 创建数据库: {db_path}\")",
1067
- "detail": "import_book_data",
1068
- "documentation": {}
1069
- },
1070
- {
1071
- "label": "import_book_info",
1072
- "kind": 2,
1073
- "importPath": "import_book_data",
1074
- "description": "import_book_data",
1075
- "peekOfCode": "def import_book_info(conn, book_dir: Path) -> int:\n \"\"\"\n 导入书籍基本信息\n Args:\n conn: 数据库连接\n book_dir: 书籍目录\n Returns:\n int: 书籍ID\n \"\"\"\n book_info_file = book_dir / \"book_info.json\"",
1076
- "detail": "import_book_data",
1077
- "documentation": {}
1078
- },
1079
- {
1080
- "label": "import_pages",
1081
- "kind": 2,
1082
- "importPath": "import_book_data",
1083
- "description": "import_book_data",
1084
- "peekOfCode": "def import_pages(conn, book_dir: Path, book_id: int):\n \"\"\"\n 导入页面数据\n Args:\n conn: 数据库连接\n book_dir: 书籍目录\n book_id: 书籍ID\n \"\"\"\n pages_file = book_dir / \"pages.json\"\n if not pages_file.exists():",
1085
- "detail": "import_book_data",
1086
- "documentation": {}
1087
- },
1088
- {
1089
- "label": "generate_catalog_from_pages",
1090
- "kind": 2,
1091
- "importPath": "import_book_data",
1092
- "description": "import_book_data",
1093
- "peekOfCode": "def generate_catalog_from_pages(book_dir: Path, book_id: int, pages_data: list) -> list:\n \"\"\"\n 从pages.json自动生成目录结构\n 为每一页生成一个目录项,使用该页第一个piece的文本作为标题\n Args:\n book_dir: 书籍目录\n book_id: 书籍ID\n pages_data: 页面数据列表\n Returns:\n 生成的目录列表",
1094
- "detail": "import_book_data",
1095
- "documentation": {}
1096
- },
1097
- {
1098
- "label": "import_catalog",
1099
- "kind": 2,
1100
- "importPath": "import_book_data",
1101
- "description": "import_book_data",
1102
- "peekOfCode": "def import_catalog(conn, book_dir: Path, book_id: int, pages_data: list = None):\n \"\"\"\n 导入目录数据(强制从pages.json生成)\n Args:\n conn: 数据库连接\n book_dir: 书籍目录\n book_id: 书籍ID\n pages_data: 页面数据(用于自动生成目录)\n \"\"\"\n catalogs = []",
1103
- "detail": "import_book_data",
1104
- "documentation": {}
1105
- },
1106
- {
1107
- "label": "import_book",
1108
- "kind": 2,
1109
- "importPath": "import_book_data",
1110
- "description": "import_book_data",
1111
- "peekOfCode": "def import_book(conn, book_dir: Path):\n \"\"\"\n 导入单本书籍的完整数据\n Args:\n conn: 数据库连接\n book_dir: 书籍目录\n \"\"\"\n book_name = book_dir.name\n logger.info(f\"\\n{'=' * 60}\")\n logger.info(f\"开始导入书籍: {book_name}\")",
1112
- "detail": "import_book_data",
1113
- "documentation": {}
1114
- },
1115
- {
1116
- "label": "import_all_books",
1117
- "kind": 2,
1118
- "importPath": "import_book_data",
1119
- "description": "import_book_data",
1120
- "peekOfCode": "def import_all_books(conn, data_dir: str):\n \"\"\"\n 导入所有书籍\n Args:\n conn: 数据库连接\n data_dir: books_api_data 目录路径\n \"\"\"\n logger.info(\"=\" * 60)\n logger.info(\"开始批量导入书籍数据\")\n logger.info(f\"数据目录: {data_dir}\")",
1121
- "detail": "import_book_data",
1122
- "documentation": {}
1123
- },
1124
- {
1125
- "label": "verify_data",
1126
- "kind": 2,
1127
- "importPath": "import_book_data",
1128
- "description": "import_book_data",
1129
- "peekOfCode": "def verify_data(conn):\n \"\"\"\n 验证导入的数据\n Args:\n conn: 数据库连接\n \"\"\"\n logger.info(\"\\n🔍 验证数据...\")\n cursor = conn.cursor()\n # 统计数据\n cursor.execute('SELECT COUNT(*) FROM books')",
1130
- "detail": "import_book_data",
1131
- "documentation": {}
1132
- },
1133
- {
1134
- "label": "main",
1135
- "kind": 2,
1136
- "importPath": "import_book_data",
1137
- "description": "import_book_data",
1138
- "peekOfCode": "def main():\n \"\"\"主函数\"\"\"\n import argparse\n print(\"=\" * 80)\n print(\"📚 Books API Data 导入工具\")\n print(\"=\" * 80)\n # 命令行参数解析\n parser = argparse.ArgumentParser(description='导入 books_api_data 数据到 SQLite 数据库')\n parser.add_argument('data_dir', nargs='?', default='books_api_data',\n help='books_api_data 数据目录路径 (默认: books_api_data)')",
1139
- "detail": "import_book_data",
1140
- "documentation": {}
1141
- },
1142
- {
1143
- "label": "logger",
1144
- "kind": 5,
1145
- "importPath": "import_book_data",
1146
- "description": "import_book_data",
1147
- "peekOfCode": "logger = logging.getLogger(__name__)\ndef create_database(db_path='books.db', schema_path='db_schema.sql'):\n \"\"\"\n 创建数据库和表结构\n Args:\n db_path: 数据库文件路径\n schema_path: Schema SQL文件路径\n Returns:\n sqlite3.Connection: 数据库连接对象\n \"\"\"",
1148
- "detail": "import_book_data",
1149
- "documentation": {}
1150
- },
1151
- {
1152
- "label": "OpenSearchClient",
1153
- "kind": 6,
1154
- "importPath": "opensearch_client",
1155
- "description": "opensearch_client",
1156
- "peekOfCode": "class OpenSearchClient:\n \"\"\"OpenSearch 客户端封装类\"\"\"\n def __init__(self, host='192.168.3.33', port=9200, use_ssl=False, verify_certs=False):\n \"\"\"\n 初始化 OpenSearch 客户端\n Args:\n host: OpenSearch 主机地址\n port: OpenSearch 端口\n use_ssl: 是否使用 SSL\n verify_certs: 是否验证证书",
1157
- "detail": "opensearch_client",
1158
- "documentation": {}
1159
- },
1160
- {
1161
- "label": "initialize_learning_index",
1162
- "kind": 2,
1163
- "importPath": "opensearch_client",
1164
- "description": "opensearch_client",
1165
- "peekOfCode": "def initialize_learning_index(client, index_name='english_learning_content'):\n \"\"\"\n 初始化英语学习内容索引\n 索引包含:单词、例句、页面内容等\n \"\"\"\n # 定义索引映射\n mappings = {\n 'properties': {\n 'type': {'type': 'keyword'}, # 内容类型:word, sentence, page\n 'page_num': {'type': 'integer'}, # 页码",
1166
- "detail": "opensearch_client",
1167
- "documentation": {}
1168
- },
1169
- {
1170
- "label": "search_english_content",
1171
- "kind": 2,
1172
- "importPath": "opensearch_client",
1173
- "description": "opensearch_client",
1174
- "peekOfCode": "def search_english_content(client, keyword, index_name='english_learning_content'):\n \"\"\"\n 搜索英语学习内容\n Args:\n client: OpenSearchClient 实例\n keyword: 搜索关键词\n index_name: 索引名��\n Returns:\n 搜索结果列表\n \"\"\"",
1175
- "detail": "opensearch_client",
1176
- "documentation": {}
1177
- },
1178
- {
1179
- "label": "logger",
1180
- "kind": 5,
1181
- "importPath": "opensearch_client",
1182
- "description": "opensearch_client",
1183
- "peekOfCode": "logger = logging.getLogger(__name__)\nclass OpenSearchClient:\n \"\"\"OpenSearch 客户端封装类\"\"\"\n def __init__(self, host='192.168.3.33', port=9200, use_ssl=False, verify_certs=False):\n \"\"\"\n 初始化 OpenSearch 客户端\n Args:\n host: OpenSearch 主机地址\n port: OpenSearch 端口\n use_ssl: 是否使用 SSL",
1184
- "detail": "opensearch_client",
1185
- "documentation": {}
1186
- },
1187
- {
1188
- "label": "main",
1189
- "kind": 2,
1190
- "importPath": "quick_check",
1191
- "description": "quick_check",
1192
- "peekOfCode": "def main():\n with open('book_10242.json', 'r', encoding='utf-8') as f:\n data = json.load(f)\n pages_data = json.loads(data['Data'])\n # 检查第4页(索引3)和第5页(索引4)\n for i in [3, 4]:\n if i < len(pages_data):\n page = pages_data[i]\n print(f\"=== 第{page['pageNumber']}页 ===\")\n print(f\"pageId: {page['pageId']}\")",
1193
- "detail": "quick_check",
1194
- "documentation": {}
1195
- },
1196
- {
1197
- "label": "test_ip_logging",
1198
- "kind": 2,
1199
- "importPath": "test_ip_logging",
1200
- "description": "test_ip_logging",
1201
- "peekOfCode": "def test_ip_logging():\n \"\"\"测试 IP 地址记录\"\"\"\n base_url = \"http://localhost:7860\"\n print(\"=\" * 60)\n print(\"🧪 测试客户端 IP 记录功能\")\n print(\"=\" * 60)\n # 测试场景\n test_cases = [\n {\n \"name\": \"正常请求(无代理)\",",
1202
- "detail": "test_ip_logging",
1203
- "documentation": {}
1204
- },
1205
- {
1206
- "label": "show_example_logs",
1207
- "kind": 2,
1208
- "importPath": "test_ip_logging",
1209
- "description": "test_ip_logging",
1210
- "peekOfCode": "def show_example_logs():\n \"\"\"显示日志示例\"\"\"\n print(\"\\n\" + \"=\" * 60)\n print(\"📝 日志输出示例\")\n print(\"=\" * 60)\n examples = [\n \"[2025-10-16 18:58:23] INFO in app: [203.0.113.1] GET /api/health | UA: Mozilla/5.0 (Windows NT 10.0)\",\n \"[2025-10-16 18:58:24] INFO in app: [198.51.100.1] POST /api/search | UA: Mozilla/5.0 (iPhone; CPU iPhone OS)\",\n \"[2025-10-16 18:58:25] INFO in app: [192.0.2.1] GET /api/book/info | UA: curl/7.68.0\",\n ]",
1211
- "detail": "test_ip_logging",
1212
- "documentation": {}
1213
- },
1214
- {
1215
- "label": "print_status",
1216
- "kind": 2,
1217
- "importPath": "test_setup",
1218
- "description": "test_setup",
1219
- "peekOfCode": "def print_status(message, status):\n \"\"\"打印状态信息\"\"\"\n if status:\n print(f\"✅ {message}\")\n else:\n print(f\"❌ {message}\")\n return status\ndef check_python_version():\n \"\"\"检查Python版本\"\"\"\n version = sys.version_info",
1220
- "detail": "test_setup",
1221
- "documentation": {}
1222
- },
1223
- {
1224
- "label": "check_python_version",
1225
- "kind": 2,
1226
- "importPath": "test_setup",
1227
- "description": "test_setup",
1228
- "peekOfCode": "def check_python_version():\n \"\"\"检查Python版本\"\"\"\n version = sys.version_info\n is_ok = version.major == 3 and version.minor >= 12\n print_status(\n f\"Python版本: {version.major}.{version.minor}.{version.micro} {'(需要 3.12+)' if not is_ok else ''}\",\n is_ok\n )\n return is_ok\ndef check_dependencies():",
1229
- "detail": "test_setup",
1230
- "documentation": {}
1231
- },
1232
- {
1233
- "label": "check_dependencies",
1234
- "kind": 2,
1235
- "importPath": "test_setup",
1236
- "description": "test_setup",
1237
- "peekOfCode": "def check_dependencies():\n \"\"\"检查Python依赖\"\"\"\n dependencies = ['flask', 'flask_cors']\n all_ok = True\n for dep in dependencies:\n try:\n __import__(dep)\n print_status(f\"Python包 '{dep}' 已安装\", True)\n except ImportError:\n print_status(f\"Python包 '{dep}' 未安装\", False)",
1238
- "detail": "test_setup",
1239
- "documentation": {}
1240
- },
1241
- {
1242
- "label": "check_files",
1243
- "kind": 2,
1244
- "importPath": "test_setup",
1245
- "description": "test_setup",
1246
- "peekOfCode": "def check_files():\n \"\"\"检查必要文件\"\"\"\n files = [\n 'app.py',\n 'db_manager.py',\n 'import_book_data.py',\n 'db_schema.sql',\n 'index.html',\n 'reader.html',\n 'static/js/catalog.js',",
1247
- "detail": "test_setup",
1248
- "documentation": {}
1249
- },
1250
- {
1251
- "label": "check_data_directory",
1252
- "kind": 2,
1253
- "importPath": "test_setup",
1254
- "description": "test_setup",
1255
- "peekOfCode": "def check_data_directory():\n \"\"\"检查数据目录\"\"\"\n data_dir = Path('books_api_data')\n if not data_dir.exists():\n print_status(\"数据目录 'books_api_data' 不存在\", False)\n print(\" 提示: 请将 books_api_data 目录放置在项目根目录下\")\n return False\n # 检查是否有书籍数据\n book_dirs = [d for d in data_dir.iterdir() if d.is_dir()]\n if len(book_dirs) == 0:",
1256
- "detail": "test_setup",
1257
- "documentation": {}
1258
- },
1259
- {
1260
- "label": "check_database",
1261
- "kind": 2,
1262
- "importPath": "test_setup",
1263
- "description": "test_setup",
1264
- "peekOfCode": "def check_database():\n \"\"\"检查数据库\"\"\"\n db_file = Path('books.db')\n if not db_file.exists():\n print_status(\"数据库 'books.db' 不存在\", False)\n print(\" 提示: 运行 'python3 import_book_data.py books_api_data' 来导入数据\")\n return False\n print_status(\"数据库 'books.db' 存在\", True)\n # 检查数据库内容\n try:",
1265
- "detail": "test_setup",
1266
- "documentation": {}
1267
- },
1268
- {
1269
- "label": "main",
1270
- "kind": 2,
1271
- "importPath": "test_setup",
1272
- "description": "test_setup",
1273
- "peekOfCode": "def main():\n \"\"\"主函数\"\"\"\n print(\"=\" * 70)\n print(\"🔍 交互式英语学习平台 - 系统设置检查\")\n print(\"=\" * 70)\n print()\n checks = [\n (\"Python版本\", check_python_version),\n (\"Python依赖\", check_dependencies),\n (\"必要文件\", check_files),",
1274
- "detail": "test_setup",
1275
- "documentation": {}
1276
- }
1277
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile DELETED
@@ -1,42 +0,0 @@
1
- FROM python:3.12-slim
2
-
3
- # 设置工作目录
4
- WORKDIR /app
5
-
6
- # 设置环境变量
7
- ENV PYTHONUNBUFFERED=1 \
8
- PYTHONDONTWRITEBYTECODE=1 \
9
- PORT=7860 \
10
- FLASK_ENV=production
11
-
12
- # 安装系统依赖(如需要)
13
- RUN apt-get update && apt-get install -y --no-install-recommends \
14
- curl \
15
- && rm -rf /var/lib/apt/lists/*
16
-
17
- # 复制requirements文件
18
- COPY requirements.txt .
19
-
20
- # 安装Python依赖
21
- RUN pip install --no-cache-dir --upgrade pip && \
22
- pip install --no-cache-dir -r requirements.txt
23
-
24
- # 复制应用文件
25
- COPY . .
26
-
27
- # 创建日志目录并设置权限
28
- RUN mkdir -p logs && chmod 777 logs
29
-
30
- # 暴露7860端口(Hugging Face Spaces要求)
31
- EXPOSE 7860
32
-
33
- # 健康检查
34
- HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
35
- CMD curl -f http://localhost:7860/api/health || exit 1
36
-
37
- # 使用 Gunicorn 启动 Flask 应用(生产环境推荐)
38
- # 使用配置文件以显示客户端真实 IP
39
- CMD ["gunicorn", "-c", "gunicorn_config.py", "app:app"]
40
-
41
- # 如果想使用 Flask 内置服务器(开发/测试用),使用下面这行:
42
- # CMD ["python3.12", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,332 +1,10 @@
1
  ---
2
- title: 交互式学习平台
3
- emoji: 📚
4
- colorFrom: blue
5
- colorTo: purple
6
  sdk: docker
7
- app_port: 7860
8
  pinned: false
9
  ---
10
 
11
- # 交互式英语学习平台
12
-
13
- 基于 data 数据结构的交互式英语学习应用,支持点读、音频播放、翻译显示等功能。
14
-
15
- ## 📋 功能特性
16
-
17
- ### 书籍目录页面
18
- - 📚 显示所有可用教材
19
- - 🔍 搜索教材名称和分类
20
- - 📊 显示统计信息(教材数、页面数、内容片段数等)
21
- - 🎨 精美的卡片式布局
22
-
23
- ### 阅读页面
24
- - 📖 交互式点读功能
25
- - 🔊 音频播放控制
26
- - 🌐 中英文翻译切换
27
- - 📑 章节目录导航
28
- - 🔖 书签功能
29
- - 🔍 全文搜索
30
- - ⚙️ 个性化设置(播放速度、自动翻译等)
31
- - ⌨️ 键盘快捷键支持
32
-
33
- ## 🚀 快速开始
34
-
35
- ### 1. 环境要求
36
-
37
- - Python 3.12
38
- - SQLite 3
39
- - 现代浏览器(Chrome、Firefox、Safari、Edge)
40
-
41
- ### 2. 安装依赖
42
-
43
- ```bash
44
- pip install flask flask-cors
45
- ```
46
-
47
- ### 3. 准备数据
48
-
49
- 将 `data` 目录放置在项目根目录下:
50
-
51
- ```
52
- /data/zhangl/code/hf/point/
53
- ├── data/ # 教材数据目录
54
- │ ├── 168_一年级上册/
55
- │ │ ├── book_info.json
56
- │ │ ├── pages.json
57
- │ │ ├── catalog.json
58
- │ │ ├── images/
59
- │ │ └── audios/
60
- │ ├── 169_二年级上册/
61
- │ └── ...
62
- ├── app.py
63
- ├── import_book_data.py
64
- └── ...
65
- ```
66
-
67
- ### 4. 导入数据到数据库
68
-
69
- ```bash
70
- # 首次导入(会自动创建数据库)
71
- python3 import_book_data.py data
72
-
73
- # 重新导入所有数据(删除现有数据库)
74
- python3 import_book_data.py data --recreate
75
-
76
- # 仅验证数据
77
- python3 import_book_data.py --verify-only
78
- ```
79
-
80
- 导入选项:
81
- - `data_dir`: 数据目录路径(默认:data)
82
- - `--db`: 数据库文件路径(默认:books.db)
83
- - `--schema`: Schema 文件路径(默认:db_schema.sql)
84
- - `--recreate`: 删除现有数据库并重新创建
85
- - `--verify-only`: 仅验证数据,不导入
86
-
87
- ### 5. 启动应用
88
-
89
- ```bash
90
- python3 app.py
91
- ```
92
-
93
- 应用将在 `http://0.0.0.0:7860` 启动。
94
-
95
- 访问地址:
96
- - 书籍目录:`http://localhost:7860/`
97
- - 阅读页面:`http://localhost:7860/reader?book_id=168`
98
-
99
- ## 📁 项目结构
100
-
101
- ```
102
- /data/zhangl/code/hf/point/
103
- ├── data/ # 教材数据目录
104
- ├── static/
105
- │ ├── css/
106
- │ │ ├── style.css # 主样式文件
107
- │ │ ├── inter.css # Inter 字体
108
- │ │ └── all.min.css # Font Awesome 图标
109
- │ └── js/
110
- │ ├── catalog.js # 书籍目录页面逻辑
111
- │ └── reader.js # 阅读页面逻辑
112
- ├── app.py # Flask 应用主文件
113
- ├── db_manager.py # 数据库管理器
114
- ├── import_book_data.py # 数据导入脚本
115
- ├── db_schema.sql # 数据库结构定义
116
- ├── index.html # 书籍目录页面
117
- ├── reader.html # 阅读页面
118
- ├── books.db # SQLite 数据库(自动生成)
119
- └── README.md
120
- ```
121
-
122
- ## 🗄️ 数据库结构
123
-
124
- ### books 表
125
- 存储书籍的基本信息:
126
- - `market_book_id`: 书籍ID(主键)
127
- - `market_book_name`: 书籍名称
128
- - `market_book_cover`: 封面图片路径
129
- - `max_page`: 最大页码
130
- - `grade_id`: 年级ID
131
- - `reel_id`: 学期ID(1=上册,2=下册)
132
- - 等等...
133
-
134
- ### pages 表
135
- 存储页面信息:
136
- - `page_id`: 页面ID(主键)
137
- - `book_id`: 所属书籍ID
138
- - `page_number`: 页码
139
- - `origin_img_url`: 原始图片路径
140
- - `encrypt_img_url`: 加密图片路径
141
-
142
- ### pieces 表
143
- 存储页面内容片段:
144
- - `piece_id`: 片段ID(主键)
145
- - `page_id`: 所属页面ID
146
- - `original`: 原文(英文)
147
- - `translation`: 译文(中文)
148
- - `origin_sound_url`: 音频路径
149
- - `duration`: 音频时长
150
- - `coordinate_x/y/width/height`: 坐标信息
151
- - 等等...
152
-
153
- ### catalogs 表
154
- 存储书籍目录结构:
155
- - `catalog_id`: 目录项ID(主键)
156
- - `book_id`: 所属书籍ID
157
- - `catalog_name`: 目录名称(英文)
158
- - `catalog_name_cn`: 目录名称(中文)
159
- - `start_page/end_page`: 起止页码
160
- - `parent_id`: 父级目录ID(支持树形结构)
161
-
162
- ### pieces_fts 表
163
- 全文搜索索引(FTS5):
164
- - 支持在 `original` 和 `translation` 字段中快速搜索
165
-
166
- ## 🔌 API 接口
167
-
168
- ### 书籍相关
169
-
170
- #### 获取所有书籍列表
171
- ```
172
- GET /api/v2/books?grade_id=40
173
- ```
174
-
175
- #### 获取书籍详情
176
- ```
177
- GET /api/v2/books/{book_id}
178
- ```
179
-
180
- #### 获取书籍页面列表
181
- ```
182
- GET /api/v2/books/{book_id}/pages
183
- ```
184
-
185
- #### 获取书籍目录结构
186
- ```
187
- GET /api/v2/books/{book_id}/catalog
188
- ```
189
-
190
- #### 获取页面内容
191
- ```
192
- GET /api/v2/books/{book_id}/pages/{page_number}
193
- ```
194
-
195
- ### 搜索相关
196
-
197
- #### 在书籍中搜索
198
- ```
199
- GET /api/v2/books/{book_id}/search?keyword=hello&limit=20
200
- ```
201
-
202
- #### 全局搜索
203
- ```
204
- GET /api/v2/search?keyword=hello&limit=50
205
- ```
206
-
207
- ### 统计相关
208
-
209
- #### 获取书籍统计信息
210
- ```
211
- GET /api/v2/books/{book_id}/statistics
212
- ```
213
-
214
- #### 获取整体统计信息
215
- ```
216
- GET /api/v2/statistics
217
- ```
218
-
219
- ## ⌨️ 键盘快捷键
220
-
221
- 在阅读页面中:
222
- - `←` / `→`: 上一页 / 下一页
223
- - `Space`: 播放/暂停音频
224
- - `T`: 切换翻译显示
225
- - `I`: 切换交互区域显示
226
- - `Esc`: 关闭弹出面板
227
-
228
- ## 🎨 功能亮点
229
-
230
- 1. **按需加载**: 页面内容按需加载,提高性能
231
- 2. **全文搜索**: 使用 SQLite FTS5 实现高效全文搜索
232
- 3. **响应式设计**: 自适应不同屏幕尺寸
233
- 4. **交互式点读**: 点击文本片段播放对应音频
234
- 5. **坐标系统**: 使用相对坐标,适配不同分辨率
235
- 6. **目录导航**: 支持章节目录快速跳转
236
- 7. **本地资源**: 支持从本地 data 目录加载资源
237
-
238
- ## 🔧 开发调试
239
-
240
- ### 启用调试模式
241
-
242
- ```bash
243
- export FLASK_DEBUG=True
244
- python3 app.py
245
- ```
246
-
247
- ### 查看日志
248
-
249
- 应用日志存储在 `logs/app.log`
250
-
251
- ### 数据库操作
252
-
253
- 使用 `db_manager.py` 提供的数据库管理器:
254
-
255
- ```python
256
- from db_manager import get_db_instance
257
-
258
- db = get_db_instance('books.db')
259
-
260
- # 获取所有书籍
261
- books = db.get_all_books()
262
-
263
- # 搜索内容
264
- results = db.search_content(book_id=168, keyword='hello', limit=20)
265
-
266
- # 获取统计信息
267
- stats = db.get_overall_statistics()
268
- ```
269
-
270
- ## 📝 数据格式说明
271
-
272
- ### 坐标系统
273
- 文本片段的坐标使用相对坐标(0-1范围):
274
- ```json
275
- {
276
- "coordinate": {
277
- "x": 0.0482, // 左上角X坐标(相对于图片宽度)
278
- "y": 0.0357, // 左上角Y坐标(相对于图片高度)
279
- "width": 0.3195, // 宽度(相对于图片宽度)
280
- "height": 0.0982 // 高度(相对于图片高度)
281
- }
282
- }
283
- ```
284
-
285
- ### 资源路径
286
- 资源路径采用相对路径格式:
287
- ```
288
- 168_一年级上册/images/page_001.jpg
289
- 168_一年级上册/audios/page_002_piece_00.mp3
290
- ```
291
-
292
- 实际访问时会自动拼接为:
293
- ```
294
- data/168_一年级上册/images/page_001.jpg
295
- data/168_一年级上册/audios/page_002_piece_00.mp3
296
- ```
297
-
298
- ## 🐛 故障排除
299
-
300
- ### 数据库导入失败
301
- - 检查 data 目录是否存在
302
- - 确保每个书籍目录包含 book_info.json、pages.json
303
- - 检查 db_schema.sql 文件是否存在
304
-
305
- ### 页面加载失败
306
- - 检查数据库是否正确导入
307
- - 查看浏览器控制台是否有错误
308
- - 检查 data 目录下的资源文件是否完整
309
-
310
- ### 音频播放失败
311
- - 检查音频文件路径是否正确
312
- - 确保浏览器支持 MP3 格式
313
- - 查看浏览器控制台错误信息
314
-
315
- ## 📄 许可证
316
-
317
- 本项目仅供学习和研究使用。
318
-
319
- ## 👥 贡献
320
-
321
- 欢迎提交 Issue 和 Pull Request。
322
-
323
- ## 📞 联系方式
324
-
325
- 如有问题或建议,请联系开发团队。
326
-
327
- ---
328
-
329
- **版本**: v2.0.0
330
- **更新日期**: 2025-10-17
331
- **Python版本**: 3.12
332
- **数据库**: SQLite 3
 
1
  ---
2
+ title: Point
3
+ emoji: 🐢
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,1068 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- 交互式英语学习应用 - Flask 重构版本
5
- 在7860端口提供HTTP服务,支持静态文件和RESTful API
6
- """
7
-
8
- import os
9
- import sys
10
- import json
11
- import time
12
- from datetime import datetime
13
- from pathlib import Path
14
-
15
- from flask import Flask, render_template, send_from_directory, jsonify, request, session
16
- from flask_cors import CORS
17
- import logging
18
- from logging.handlers import RotatingFileHandler
19
- from database.db_manager import get_db_instance
20
-
21
- # 创建 Flask 应用
22
- app = Flask(__name__,
23
- static_folder='static',
24
- static_url_path='/static')
25
-
26
- # 配置
27
- app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
28
- app.config['JSON_AS_ASCII'] = False # 支持中文JSON
29
- app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 最大上传16MB
30
-
31
- # 启用CORS
32
- CORS(app, resources={
33
- r"/*": {
34
- "origins": "*",
35
- "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
36
- "allow_headers": ["Content-Type", "Authorization"]
37
- }
38
- })
39
-
40
- # 配置日志
41
- def setup_logging():
42
- """配置日志系统"""
43
- if not app.debug:
44
- try:
45
- # 尝试在当前目录创建日志目录
46
- log_dir = Path('logs')
47
- log_dir.mkdir(exist_ok=True)
48
- log_file = log_dir / 'app.log'
49
- except (PermissionError, OSError):
50
- # 如果当前目录无权限,使用 /tmp 目录
51
- try:
52
- log_dir = Path('/tmp/logs')
53
- log_dir.mkdir(exist_ok=True)
54
- log_file = log_dir / 'app.log'
55
- print(f"⚠️ 使用临时日志目录: {log_file}")
56
- except (PermissionError, OSError):
57
- # 如果都失败,只使用控制台日志
58
- print("⚠️ 无法创建日志文件,仅输出到控制台")
59
- log_file = None
60
-
61
- # 如果有有效的日志文件路径,添加文件处理器
62
- if log_file:
63
- try:
64
- file_handler = RotatingFileHandler(
65
- log_file,
66
- maxBytes=10 * 1024 * 1024,
67
- backupCount=10,
68
- encoding='utf-8'
69
- )
70
- file_handler.setFormatter(logging.Formatter(
71
- '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
72
- ))
73
- file_handler.setLevel(logging.INFO)
74
- app.logger.addHandler(file_handler)
75
- except (PermissionError, OSError) as e:
76
- print(f"⚠️ 无法创建日志处理器: {e}")
77
-
78
- # 确保至少有控制台输出
79
- if not app.logger.handlers:
80
- console_handler = logging.StreamHandler()
81
- console_handler.setFormatter(logging.Formatter(
82
- '[%(asctime)s] %(levelname)s: %(message)s'
83
- ))
84
- console_handler.setLevel(logging.INFO)
85
- app.logger.addHandler(console_handler)
86
-
87
- app.logger.setLevel(logging.INFO)
88
- app.logger.info('🚀 Flask 应用启动')
89
-
90
- # OpenSearch 配置(根据项目需求)
91
- OPENSEARCH_CONFIG = {
92
- 'host': os.environ.get('OPENSEARCH_HOST', '192.168.3.33'),
93
- 'port': int(os.environ.get('OPENSEARCH_PORT', 9200)),
94
- 'use_ssl': os.environ.get('OPENSEARCH_USE_SSL', 'False').lower() == 'true',
95
- }
96
-
97
- # 全局变量:存储学习数据
98
- BOOK_DATA = None
99
-
100
- # 数据库实例
101
- DB = None
102
-
103
- def get_client_ip():
104
- """
105
- 获取客户端真实 IP 地址
106
- 在代理服务器(如 Hugging Face Spaces)后面时,需要检查代理头部
107
- """
108
- # 按优先级检查各种代理头部
109
- headers_to_check = [
110
- 'X-Forwarded-For',
111
- 'X-Real-IP',
112
- 'CF-Connecting-IP', # Cloudflare
113
- 'True-Client-IP',
114
- 'X-Client-IP',
115
- ]
116
-
117
- for header in headers_to_check:
118
- ip = request.headers.get(header)
119
- if ip:
120
- # X-Forwarded-For 可能包含多个 IP,取第一个
121
- return ip.split(',')[0].strip()
122
-
123
- # 如果没有代理头部,使用 remote_addr
124
- return request.remote_addr or 'Unknown'
125
-
126
- def load_book_data():
127
- """
128
- 加载书籍数据(已废弃,保留是为了兼容性)
129
- 现在使用数据库接口,不再加载JSON文件
130
- """
131
- global BOOK_DATA
132
- app.logger.info('ℹ️ load_book_data 已废弃,使用数据库接口')
133
- # 设置为空字典表示已初始化,但不再使用
134
- BOOK_DATA = {}
135
- return True
136
-
137
- def init_database():
138
- """初始化数据库连接"""
139
- global DB
140
- try:
141
- DB = get_db_instance('books.db')
142
- app.logger.info('✅ 数据库连接初始化成功')
143
- return True
144
- except Exception as e:
145
- app.logger.error(f'❌ 数据库初始化失败: {e}')
146
- return False
147
-
148
-
149
- # ============================================================================
150
- # 路由:静态文件服务
151
- # ============================================================================
152
-
153
- @app.route('/')
154
- def index():
155
- """主页 - 书籍目录"""
156
- return send_from_directory('.', 'index.html')
157
-
158
- @app.route('/reader')
159
- def reader():
160
- """阅读页面"""
161
- return send_from_directory('.', 'reader.html')
162
-
163
- @app.route('/<path:filename>')
164
- def serve_static(filename):
165
- """提供根目录下的静态文件(如 style.css, script.js)"""
166
- # 避免与API路由冲突
167
- if filename.startswith('api/'):
168
- return jsonify({'error': '接口不存在'}), 404
169
- return send_from_directory('.', filename)
170
-
171
- # ============================================================================
172
- # 路由:API 端点
173
- # ============================================================================
174
-
175
- @app.route('/api/health')
176
- def health_check():
177
- """健康检查端点"""
178
- return jsonify({
179
- 'status': 'healthy',
180
- 'timestamp': datetime.now().isoformat(),
181
- 'version': '2.0.0-flask'
182
- })
183
-
184
- @app.route('/api/book/info')
185
- def get_book_info():
186
- """
187
- 获取书籍信息(旧接口,保留兼容性)
188
- 推荐使用: /api/v2/books/<book_id>
189
- """
190
- try:
191
- if not DB:
192
- return jsonify({'error': '数据库未初始化'}), 500
193
-
194
- # 默认获取第一本书的信息(为了兼容旧版本)
195
- books = DB.get_all_books()
196
- if not books:
197
- return jsonify({'error': '没有找到书籍数据'}), 404
198
-
199
- # 使用最后导入的书籍
200
- book = books[-1]
201
-
202
- # 返回书籍元信息
203
- info = {
204
- 'book_id': book['market_book_id'],
205
- 'total_pages': book['max_page'],
206
- 'title': book['market_book_name'],
207
- 'loaded': True
208
- }
209
- return jsonify(info)
210
- except Exception as e:
211
- app.logger.error(f"获取书籍信息失败: {e}")
212
- return jsonify({'error': str(e)}), 500
213
-
214
- @app.route('/api/book/page/<int:page_num>')
215
- def get_page_content(page_num):
216
- """
217
- 获取指定页面内容(旧接口,保留兼容性)
218
- 推荐使用: /api/v2/books/<book_id>/pages/<page_num>
219
- """
220
- try:
221
- if not DB:
222
- return jsonify({'error': '数据库未初始化'}), 500
223
-
224
- # 获取最后导入的书籍
225
- books = DB.get_all_books()
226
- if not books:
227
- return jsonify({'error': '没有找到书籍数据'}), 404
228
-
229
- book = books[-1]
230
- book_id = book['market_book_id']
231
-
232
- # 获取页面内容
233
- page = DB.get_page_content(book_id, page_num)
234
- if not page:
235
- return jsonify({'error': '页码超出范围'}), 404
236
-
237
- return jsonify({
238
- 'page_num': page_num,
239
- 'content': page,
240
- 'total_pages': book['max_page']
241
- })
242
- except Exception as e:
243
- app.logger.error(f"获取页面内容失败: {e}")
244
- return jsonify({'error': str(e)}), 500
245
-
246
- @app.route('/api/progress/save', methods=['POST'])
247
- def save_progress():
248
- """保存学习进度"""
249
- try:
250
- data = request.get_json()
251
-
252
- # 这里可以保存到数据库,现在先存到 session
253
- if 'progress' not in session:
254
- session['progress'] = {}
255
-
256
- session['progress'].update({
257
- 'current_page': data.get('current_page', 0),
258
- 'bookmarks': data.get('bookmarks', []),
259
- 'settings': data.get('settings', {}),
260
- 'last_updated': datetime.now().isoformat()
261
- })
262
-
263
- app.logger.info(f"保存学习进度: 第 {data.get('current_page')} 页")
264
-
265
- return jsonify({
266
- 'success': True,
267
- 'message': '学习进度已保存'
268
- })
269
- except Exception as e:
270
- app.logger.error(f"保存进度失败: {e}")
271
- return jsonify({
272
- 'success': False,
273
- 'error': str(e)
274
- }), 500
275
-
276
- @app.route('/api/progress/load', methods=['GET'])
277
- def load_progress():
278
- """加载学习进度"""
279
- progress = session.get('progress', {
280
- 'current_page': 0,
281
- 'bookmarks': [],
282
- 'settings': {}
283
- })
284
- return jsonify(progress)
285
-
286
- @app.route('/api/search', methods=['POST'])
287
- def search_content():
288
- """搜索内容(未来可集成 OpenSearch)"""
289
- try:
290
- data = request.get_json()
291
- keyword = data.get('keyword', '').strip()
292
-
293
- if not keyword:
294
- return jsonify({'error': '搜索关键词不能为空'}), 400
295
-
296
- # 简单搜索实现(在书籍数据中搜索)
297
- if not BOOK_DATA:
298
- return jsonify({'error': '书籍数据未加载'}), 500
299
-
300
- results = []
301
- pages = BOOK_DATA.get('pages', [])
302
-
303
- for page_num, page_content in enumerate(pages):
304
- # 在页面内容中搜索关键词
305
- page_str = json.dumps(page_content, ensure_ascii=False)
306
- if keyword.lower() in page_str.lower():
307
- results.append({
308
- 'page_num': page_num,
309
- 'preview': str(page_content)[:200] + '...'
310
- })
311
-
312
- app.logger.info(f"搜索 '{keyword}': 找到 {len(results)} 个结果")
313
-
314
- return jsonify({
315
- 'keyword': keyword,
316
- 'total': len(results),
317
- 'results': results[:20] # 限制返回前20个结果
318
- })
319
- except Exception as e:
320
- app.logger.error(f"搜索失败: {e}")
321
- return jsonify({
322
- 'success': False,
323
- 'error': str(e)
324
- }), 500
325
-
326
- @app.route('/api/opensearch/status')
327
- def opensearch_status():
328
- """检查 OpenSearch 连接状态(示例)"""
329
- try:
330
- # 这里可以实际连接 OpenSearch
331
- # from opensearchpy import OpenSearch
332
- # client = OpenSearch([{'host': OPENSEARCH_CONFIG['host'], 'port': OPENSEARCH_CONFIG['port']}])
333
- # info = client.info()
334
-
335
- return jsonify({
336
- 'configured': True,
337
- 'host': OPENSEARCH_CONFIG['host'],
338
- 'port': OPENSEARCH_CONFIG['port'],
339
- 'status': 'not_implemented',
340
- 'message': 'OpenSearch 集成待实现'
341
- })
342
- except Exception as e:
343
- return jsonify({
344
- 'configured': False,
345
- 'error': str(e)
346
- }), 500
347
-
348
- @app.route('/api/stats')
349
- def get_stats():
350
- """获取学习统计数据"""
351
- # 从 session 或数据库获取统计数据
352
- progress = session.get('progress', {})
353
-
354
- return jsonify({
355
- 'current_page': progress.get('current_page', 0),
356
- 'total_bookmarks': len(progress.get('bookmarks', [])),
357
- 'last_visit': progress.get('last_updated', None),
358
- 'total_pages': len(BOOK_DATA.get('pages', [])) if BOOK_DATA else 0
359
- })
360
-
361
- # ============================================================================
362
- # 路由:新版API端点(基于SQLite数据库)
363
- # ============================================================================
364
-
365
- @app.route('/api/v2/books')
366
- def get_books():
367
- """
368
- 获取所有书籍列表
369
-
370
- Query Parameters:
371
- grade_id: 年级ID(可选)
372
-
373
- Returns:
374
- {
375
- "success": true,
376
- "count": 10,
377
- "books": [
378
- {
379
- "market_book_id": 168,
380
- "market_book_name": "一年级上册",
381
- "market_book_cover": "168_一年级上册/images/page_001.jpg",
382
- "max_page": 73,
383
- "market_classify_name": "沪教版(深圳)",
384
- "grade_id": 40,
385
- "reel_id": 1
386
- }
387
- ]
388
- }
389
- """
390
- try:
391
- if not DB:
392
- return jsonify({
393
- 'success': False,
394
- 'error': '数据库未初始化'
395
- }), 500
396
-
397
- # 检查是否有年级筛选
398
- grade_id = request.args.get('grade_id', type=int)
399
-
400
- if grade_id:
401
- books = DB.get_books_by_grade(grade_id)
402
- else:
403
- books = DB.get_all_books()
404
-
405
- app.logger.info(f"获取书籍列表: {len(books)} 本书")
406
-
407
- return jsonify({
408
- 'success': True,
409
- 'count': len(books),
410
- 'books': books
411
- })
412
- except Exception as e:
413
- app.logger.error(f"获取书籍列表失败: {e}")
414
- return jsonify({
415
- 'success': False,
416
- 'error': str(e)
417
- }), 500
418
-
419
- @app.route('/api/v2/books/<int:book_id>')
420
- def get_book_info_v2(book_id):
421
- """
422
- 获取指定书籍的详细信息
423
-
424
- Args:
425
- book_id: 书籍ID
426
-
427
- Returns:
428
- {
429
- "success": true,
430
- "book": {
431
- "book_id": 1,
432
- "book_name": "书籍名称",
433
- "total_pages": 100,
434
- ...
435
- }
436
- }
437
- """
438
- try:
439
- if not DB:
440
- return jsonify({
441
- 'success': False,
442
- 'error': '数据库未初始化'
443
- }), 500
444
-
445
- book = DB.get_book_by_id(book_id)
446
- if not book:
447
- return jsonify({
448
- 'success': False,
449
- 'error': '书籍不存在'
450
- }), 404
451
-
452
- app.logger.info(f"获取书籍信息: ID={book_id}")
453
-
454
- return jsonify({
455
- 'success': True,
456
- 'book': book
457
- })
458
- except Exception as e:
459
- app.logger.error(f"获取书籍信息失败: {e}")
460
- return jsonify({
461
- 'success': False,
462
- 'error': str(e)
463
- }), 500
464
-
465
- @app.route('/api/v2/books/<int:book_id>/pages')
466
- def get_book_pages(book_id):
467
- """
468
- 获取书籍的所有页面列表(不含片段内容)
469
-
470
- Args:
471
- book_id: 书籍ID
472
-
473
- Returns:
474
- {
475
- "success": true,
476
- "book_id": 168,
477
- "book_name": "一年级上册",
478
- "total_pages": 73,
479
- "pages": [
480
- {
481
- "page_id": 2111,
482
- "page_number": 1,
483
- "origin_img_url": "...",
484
- "encrypt_img_url": "..."
485
- }
486
- ]
487
- }
488
- """
489
- try:
490
- if not DB:
491
- return jsonify({
492
- 'success': False,
493
- 'error': '数据库未初始化'
494
- }), 500
495
-
496
- # 检查书籍是否存在
497
- book = DB.get_book_by_id(book_id)
498
- if not book:
499
- return jsonify({
500
- 'success': False,
501
- 'error': '书籍不存在'
502
- }), 404
503
-
504
- # 获取页面列表
505
- pages = DB.get_book_pages(book_id)
506
-
507
- app.logger.info(f"获取书籍页面列表: 书籍ID={book_id}, 页数={len(pages)}")
508
-
509
- return jsonify({
510
- 'success': True,
511
- 'book_id': book_id,
512
- 'book_name': book['market_book_name'],
513
- 'total_pages': len(pages),
514
- 'pages': pages
515
- })
516
- except Exception as e:
517
- app.logger.error(f"获取书籍页面列表失败: {e}")
518
- return jsonify({
519
- 'success': False,
520
- 'error': str(e)
521
- }), 500
522
-
523
- @app.route('/api/v2/books/<int:book_id>/catalog')
524
- def get_book_catalog(book_id):
525
- """
526
- 获取书籍目录结构(章节目录)
527
-
528
- Args:
529
- book_id: 书籍ID
530
-
531
- Returns:
532
- {
533
- "success": true,
534
- "book_id": 168,
535
- "book_name": "一年级上册",
536
- "catalog": [
537
- {
538
- "catalog_id": 1,
539
- "catalog_name": "Unit 1 Hello",
540
- "catalog_name_cn": "第一单元 你好",
541
- "start_page": 2,
542
- "end_page": 10,
543
- "thumbnail": "...",
544
- "children": []
545
- }
546
- ]
547
- }
548
- """
549
- try:
550
- if not DB:
551
- return jsonify({
552
- 'success': False,
553
- 'error': '数据库未初始化'
554
- }), 500
555
-
556
- # 检查书籍是否存在
557
- book = DB.get_book_by_id(book_id)
558
- if not book:
559
- return jsonify({
560
- 'success': False,
561
- 'error': '书籍不存在'
562
- }), 404
563
-
564
- # 获取目录
565
- catalog = DB.get_book_catalog(book_id)
566
-
567
- app.logger.info(f"获取书籍目录: 书籍ID={book_id}, 目录项={len(catalog)}")
568
-
569
- return jsonify({
570
- 'success': True,
571
- 'book_id': book_id,
572
- 'book_name': book['market_book_name'],
573
- 'catalog': catalog
574
- })
575
- except Exception as e:
576
- app.logger.error(f"获取书籍目录失败: {e}")
577
- return jsonify({
578
- 'success': False,
579
- 'error': str(e)
580
- }), 500
581
-
582
- @app.route('/api/v2/books/<int:book_id>/pages/<int:page_num>')
583
- def get_page_content_v2(book_id, page_num):
584
- """
585
- 获取指定页面的完整内容
586
-
587
- Args:
588
- book_id: 书籍ID
589
- page_num: 页码
590
-
591
- Returns:
592
- {
593
- "success": true,
594
- "book_id": 1,
595
- "page": {
596
- "page_id": 1001,
597
- "page_number": 1,
598
- "origin_img_url": "https://...",
599
- "pieces": [
600
- {
601
- "piece_id": 10001,
602
- "original": "Hello",
603
- "translation": "你好",
604
- "origin_sound_url": "https://...",
605
- ...
606
- }
607
- ],
608
- "piece_count": 10
609
- }
610
- }
611
- """
612
- try:
613
- if not DB:
614
- return jsonify({
615
- 'success': False,
616
- 'error': '数据库未初始化'
617
- }), 500
618
-
619
- # 检查书籍是否存在
620
- book = DB.get_book_by_id(book_id)
621
- if not book:
622
- return jsonify({
623
- 'success': False,
624
- 'error': '书籍不存在'
625
- }), 404
626
-
627
- # 获取页面内容
628
- page = DB.get_page_content(book_id, page_num)
629
- if not page:
630
- return jsonify({
631
- 'success': False,
632
- 'error': f'页码 {page_num} 不存在'
633
- }), 404
634
-
635
- app.logger.info(f"获取页面内容: 书籍ID={book_id}, 页码={page_num}")
636
-
637
- return jsonify({
638
- 'success': True,
639
- 'book_id': book_id,
640
- 'book_name': book['market_book_name'],
641
- 'page': page
642
- })
643
- except Exception as e:
644
- app.logger.error(f"获取页面内容失败: {e}")
645
- return jsonify({
646
- 'success': False,
647
- 'error': str(e)
648
- }), 500
649
-
650
- @app.route('/api/v2/books/<int:book_id>/search')
651
- def search_book_content(book_id):
652
- """
653
- 在书籍中搜索内容
654
-
655
- Query Parameters:
656
- keyword: 搜索关键词
657
- limit: ��回结果数量(默认20)
658
-
659
- Returns:
660
- {
661
- "success": true,
662
- "book_id": 168,
663
- "keyword": "hello",
664
- "count": 5,
665
- "results": [
666
- {
667
- "page_number": 2,
668
- "piece_id": 26342,
669
- "original": "Hello",
670
- "translation": "你好",
671
- "origin_sound_url": "..."
672
- }
673
- ]
674
- }
675
- """
676
- try:
677
- if not DB:
678
- return jsonify({
679
- 'success': False,
680
- 'error': '数据库未初始化'
681
- }), 500
682
-
683
- keyword = request.args.get('keyword', '').strip()
684
- if not keyword:
685
- return jsonify({
686
- 'success': False,
687
- 'error': '请提供搜索关键词'
688
- }), 400
689
-
690
- limit = request.args.get('limit', 20, type=int)
691
-
692
- # 检查书籍是否存在
693
- book = DB.get_book_by_id(book_id)
694
- if not book:
695
- return jsonify({
696
- 'success': False,
697
- 'error': '书籍不存在'
698
- }), 404
699
-
700
- # 搜索
701
- results = DB.search_content(book_id, keyword, limit)
702
-
703
- app.logger.info(f"搜索内容: 书籍ID={book_id}, 关键词={keyword}, 结果数={len(results)}")
704
-
705
- return jsonify({
706
- 'success': True,
707
- 'book_id': book_id,
708
- 'book_name': book['market_book_name'],
709
- 'keyword': keyword,
710
- 'count': len(results),
711
- 'results': results
712
- })
713
- except Exception as e:
714
- app.logger.error(f"搜索失败: {e}")
715
- return jsonify({
716
- 'success': False,
717
- 'error': str(e)
718
- }), 500
719
-
720
- @app.route('/api/v2/search')
721
- def search_all_content():
722
- """
723
- 在所有书籍中搜索内容
724
-
725
- Query Parameters:
726
- keyword: 搜索关键词
727
- limit: 返回结果数量(默认50)
728
-
729
- Returns:
730
- {
731
- "success": true,
732
- "keyword": "hello",
733
- "count": 15,
734
- "results": [
735
- {
736
- "market_book_id": 168,
737
- "market_book_name": "一年级上册",
738
- "page_number": 2,
739
- "piece_id": 26342,
740
- "original": "Hello",
741
- "translation": "你好",
742
- "origin_sound_url": "..."
743
- }
744
- ]
745
- }
746
- """
747
- try:
748
- if not DB:
749
- return jsonify({
750
- 'success': False,
751
- 'error': '数据库未初始化'
752
- }), 500
753
-
754
- keyword = request.args.get('keyword', '').strip()
755
- if not keyword:
756
- return jsonify({
757
- 'success': False,
758
- 'error': '请提供搜索关键词'
759
- }), 400
760
-
761
- limit = request.args.get('limit', 50, type=int)
762
-
763
- # 搜索所有书籍
764
- results = DB.search_all_books(keyword, limit)
765
-
766
- app.logger.info(f"全局搜索: 关键词={keyword}, 结果数={len(results)}")
767
-
768
- return jsonify({
769
- 'success': True,
770
- 'keyword': keyword,
771
- 'count': len(results),
772
- 'results': results
773
- })
774
- except Exception as e:
775
- app.logger.error(f"搜索失败: {e}")
776
- return jsonify({
777
- 'success': False,
778
- 'error': str(e)
779
- }), 500
780
-
781
- @app.route('/api/v2/books/<int:book_id>/statistics')
782
- def get_book_statistics(book_id):
783
- """
784
- 获取书籍统计信息
785
-
786
- Args:
787
- book_id: 书籍ID
788
-
789
- Returns:
790
- {
791
- "success": true,
792
- "statistics": {
793
- "book_id": 168,
794
- "total_pages": 73,
795
- "total_pieces": 500,
796
- "total_audio": 450,
797
- "total_catalogs": 12
798
- }
799
- }
800
- """
801
- try:
802
- if not DB:
803
- return jsonify({
804
- 'success': False,
805
- 'error': '数据库未初始化'
806
- }), 500
807
-
808
- # 检查书籍是否存在
809
- book = DB.get_book_by_id(book_id)
810
- if not book:
811
- return jsonify({
812
- 'success': False,
813
- 'error': '书籍不存在'
814
- }), 404
815
-
816
- # 获取统计信息
817
- stats = DB.get_book_statistics(book_id)
818
-
819
- app.logger.info(f"获取统计信息: 书籍ID={book_id}")
820
-
821
- return jsonify({
822
- 'success': True,
823
- 'statistics': stats
824
- })
825
- except Exception as e:
826
- app.logger.error(f"获取统计信息失败: {e}")
827
- return jsonify({
828
- 'success': False,
829
- 'error': str(e)
830
- }), 500
831
-
832
- @app.route('/api/v2/statistics')
833
- def get_overall_statistics():
834
- """
835
- 获取整体统计信息
836
-
837
- Returns:
838
- {
839
- "success": true,
840
- "statistics": {
841
- "total_books": 30,
842
- "total_pages": 2000,
843
- "total_pieces": 50000,
844
- "total_catalogs": 360
845
- }
846
- }
847
- """
848
- try:
849
- if not DB:
850
- return jsonify({
851
- 'success': False,
852
- 'error': '数据库未初始化'
853
- }), 500
854
-
855
- stats = DB.get_overall_statistics()
856
-
857
- app.logger.info("获取整体统计信息")
858
-
859
- return jsonify({
860
- 'success': True,
861
- 'statistics': stats
862
- })
863
- except Exception as e:
864
- app.logger.error(f"获取统计信息失败: {e}")
865
- return jsonify({
866
- 'success': False,
867
- 'error': str(e)
868
- }), 500
869
-
870
- # ============================================================================
871
- # 错误处理
872
- # ============================================================================
873
-
874
- @app.errorhandler(404)
875
- def not_found(error):
876
- """404错误处理"""
877
- if request.path.startswith('/api/'):
878
- return jsonify({'error': '接口不存在'}), 404
879
- return send_from_directory('.', 'index.html')
880
-
881
- @app.errorhandler(500)
882
- def internal_error(error):
883
- """500错误处理"""
884
- app.logger.error(f'服务器错误: {error}')
885
- return jsonify({'error': '服务器内部错误'}), 500
886
-
887
- # ============================================================================
888
- # 请求钩子
889
- # ============================================================================
890
-
891
- @app.before_request
892
- def before_request():
893
- """请求前处理"""
894
- # 获取客户端真实 IP
895
- client_ip = get_client_ip()
896
-
897
- # 记录所有请求信息(包括静态文件)
898
- user_agent = request.headers.get('User-Agent', 'Unknown')[:100] # 限制长度
899
-
900
- # API 请求记录详细信息
901
- if request.path.startswith('/api/'):
902
- app.logger.info(
903
- f"[{client_ip}] {request.method} {request.path} "
904
- f"| UA: {user_agent}"
905
- )
906
- # 静态资源只记录简要信息
907
- elif app.debug:
908
- app.logger.debug(f"[{client_ip}] {request.method} {request.path}")
909
-
910
- # 将 IP 存储到 g 对象,方便在其他地方使用
911
- from flask import g
912
- g.client_ip = client_ip
913
-
914
- @app.after_request
915
- def after_request(response):
916
- """请求后处理 - 添加缓存控制"""
917
- # API 端点不缓存
918
- if request.path.startswith('/api/'):
919
- response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
920
- response.headers['Pragma'] = 'no-cache'
921
- response.headers['Expires'] = '0'
922
- else:
923
- # 静态资源缓存1小时
924
- response.headers['Cache-Control'] = 'public, max-age=3600'
925
-
926
- return response
927
-
928
- # ============================================================================
929
- # 应用初始化和启动
930
- # ============================================================================
931
-
932
- def auto_generate_database():
933
- """
934
- 自动生成数据库
935
- 如果 books.db 不存在,自动运行导入脚本生成数据库
936
- """
937
- db_path = 'books.db'
938
- schema_path = 'database/db_schema.sql'
939
- data_dir = 'data'
940
-
941
- # 检查必要的文件和目录
942
- if not os.path.exists(schema_path):
943
- print(f"❌ 错误: Schema文件 {schema_path} 不存在")
944
- return False
945
-
946
- if not os.path.exists(data_dir):
947
- print(f"❌ 错误: 数据目录 {data_dir} 不存在")
948
- print(f" 无法自动生成数据库,请先准备数据")
949
- return False
950
-
951
- print(f"📦 未找到数据库文件,正在自动生成...")
952
- print(f" 数据目录: {data_dir}")
953
- print(f" 这可能需要几分钟时间,请稍候...")
954
- print("=" * 60)
955
-
956
- try:
957
- # 导入必要的函数
958
- from database.import_book_data import create_database, import_all_books, verify_data
959
-
960
- # 创建数据库
961
- conn = create_database(db_path, schema_path)
962
-
963
- # 导入所有书籍数据
964
- import_all_books(conn, data_dir)
965
-
966
- # 验证数据
967
- verify_data(conn)
968
-
969
- # 关闭连接
970
- conn.close()
971
-
972
- print("=" * 60)
973
- print("✅ 数据库自动生成完成!")
974
- print("=" * 60)
975
- return True
976
-
977
- except Exception as e:
978
- print(f"❌ 自动生成数据库失败: {e}")
979
- app.logger.error(f"自动生成数据库失败: {e}", exc_info=True)
980
- return False
981
-
982
- def initialize_app():
983
- """初始化应用"""
984
- print("🚀 交互式英语学习应用 - Flask 版本")
985
- print("=" * 60)
986
-
987
- # 设置日志
988
- setup_logging()
989
-
990
- # 检查必要文件(暂时不检查 books.db,因为会自动生成)
991
- if not os.path.exists('index.html'):
992
- app.logger.error(f"❌ 缺少必要文件: index.html")
993
- return False
994
-
995
- # 加载书籍数据
996
- if not load_book_data():
997
- return False
998
-
999
- # 检查数据库文件
1000
- if not os.path.exists('books.db'):
1001
- print("ℹ️ 未找到数据库文件 books.db")
1002
- print("🔄 正在自动生成数据库...")
1003
-
1004
- # 自动生成数据库
1005
- if not auto_generate_database():
1006
- print("❌ 数据库自动生成失败")
1007
- print(" 请手动运行: python3 database/import_book_data.py")
1008
- return False
1009
-
1010
- # 初始化数据库连接
1011
- print("📚 正在初始化数据库连接...")
1012
- if not init_database():
1013
- print("❌ 数据库初始化失败")
1014
- return False
1015
-
1016
- print("✅ 应用初始化完成")
1017
- return True
1018
-
1019
- def main():
1020
- """主函数"""
1021
- # 初始化应用(如果还未初始化)
1022
- if not DB:
1023
- if not initialize_app():
1024
- print("❌ 应用初始化失败")
1025
- return 1
1026
-
1027
- # Hugging Face Spaces 要求监听 7860 端口
1028
- port = int(os.environ.get('PORT', 7860))
1029
- debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
1030
-
1031
- print(f"🌐 监听端口: {port}")
1032
- print(f"📁 工作目录: {os.getcwd()}")
1033
- print(f"🔧 调试模式: {'开启' if debug else '关闭'}")
1034
- print("=" * 60)
1035
- print("🎉 应用已准备就绪!")
1036
- print("=" * 60)
1037
-
1038
- try:
1039
- # 启动 Flask 应用
1040
- app.run(
1041
- host='0.0.0.0',
1042
- port=port,
1043
- debug=debug,
1044
- threaded=True, # 多线程处理请求
1045
- use_reloader=debug # 开发模式下启用热重载
1046
- )
1047
- except KeyboardInterrupt:
1048
- print("\n\n🛑 服务器已停止")
1049
- return 0
1050
- except Exception as e:
1051
- print(f"❌ 服务器启动失败: {e}")
1052
- app.logger.error(f"服务器启动失败: {e}")
1053
- return 1
1054
-
1055
- # ============================================================================
1056
- # 应用初始化(支持 Gunicorn 和直接运行)
1057
- # ============================================================================
1058
-
1059
- # 在模块加载时初始化应用(无论是 gunicorn 还是直接运行都会执行)
1060
- # 这确保了在 HuggingFace Spaces 上使用 gunicorn 时也能正确初始化
1061
- if not DB: # 避免重复初始化
1062
- print("🔧 开始初始化应用...")
1063
- if not initialize_app():
1064
- print("❌ 应用初始化失败")
1065
- sys.exit(1)
1066
-
1067
- if __name__ == "__main__":
1068
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
books.db DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b95bb63da9498f42a3e27b60303a62bd8071958b4d1cb8e4475312ac68838d8f
3
- size 516096
 
 
 
 
data/10005_一年级上册/audios/page_002_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:a5f33e7250177e0b9919b0c2b2f1e1eb58f059efd17146821b49aeb6cac19bf3
3
- size 51826
 
 
 
 
data/10005_一年级上册/audios/page_003_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f3db8d98e71f8fadcb57b1eff3fd1652dfd0b12f3fb3021881df15ca7a884921
3
- size 45139
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:0b43ed35c99df647d6205526e780b0492982f42e8e7bc86276674b82305a3d44
3
- size 42631
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_01.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f976fad7083db695b0210e81c7c37fdc1a3751be9d0cf52c6452454158b05827
3
- size 36362
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_02.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:db1910ca633d8ca10e97f09dd3edc210ed66772510013b4525fd56bdc1ce0cb7
3
- size 59768
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_03.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:5823b138fa96edbff4e42c46d3de1eed62e76c27040a623388865450ab207901
3
- size 52245
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_04.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:1c2ecddb71a94cea2fedffecee8babd2e20d949aa63e0c45073e9565c7ebaca0
3
- size 71053
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_05.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d143cccfe5d0c48dd7f2f039fb244bacc3c346741b9179866e7b6258e6df6f3d
3
- size 82338
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_06.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7d1a9b7680628a28f861105a30f66d9b32b1e2f5d228282c84e3b743e5747aa8
3
- size 50155
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_07.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:dd5afef021a9e411b217b81fa90d8d6760e014aab881ead835b6d205a153d316
3
- size 49737
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_08.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:892987c43bbc5b99242fd554d8ae99015ccb0849ed98000f2f8b4aa1af52bb73
3
- size 58514
 
 
 
 
data/10005_一年级上册/audios/page_004_piece_09.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:873cdbbb4978f991636e332a5dc5dad869c9d754ad1a1872ffb2136a16342079
3
- size 90379
 
 
 
 
data/10005_一年级上册/audios/page_005_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:2ef96ddf79d08895258099e26255c97cb6cff1dd493dc83970f25759788b120c
3
- size 45975
 
 
 
 
data/10005_一年级上册/audios/page_005_piece_01.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c9ef26e060ca1db607b9362413332a2f3abd24992348565abc785a299a513266
3
- size 32182
 
 
 
 
data/10005_一年级上册/audios/page_005_piece_02.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:85864c7d35a0cb0e57f796a760c653d2a44fb2b6e17812a7ab36332fcd835bc2
3
- size 28839
 
 
 
 
data/10005_一年级上册/audios/page_005_piece_03.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:6eb99945c52d876ed1c4efbe6ecf4dc68ed199fc4bbbfd6c654806c6fc30adca
3
- size 33436
 
 
 
 
data/10005_一年级上册/audios/page_005_piece_04.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:21c220fd9848e6d77f2fb977bc79c44e7df817acfc6a1e934b989a85e99b6f59
3
- size 32600
 
 
 
 
data/10005_一年级上册/audios/page_006_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:1fb27d35047bb161899fcf97d84bc906ae17dcc5328230bc78e62b9c91316bbb
3
- size 46811
 
 
 
 
data/10005_一年级上册/audios/page_006_piece_01.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:975e3dc752f7477e7a514f57cc0a78f8bd8aaeefd991d7155d763fe27faf4974
3
- size 25913
 
 
 
 
data/10005_一年级上册/audios/page_006_piece_02.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:e5683b45230fb533fd2793b8bc1dd28f00005b228fa02fc0ef74adae24e6044a
3
- size 23823
 
 
 
 
data/10005_一年级上册/audios/page_006_piece_03.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:0d244ffdd82258c463048e5c7739d629f0d0d9e11f5fae8a39beb5121c25ee89
3
- size 25077
 
 
 
 
data/10005_一年级上册/audios/page_006_piece_04.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:4d01f72dc5ad26b83a1af2e69788fa51a164101a08b51fb0028cadd66c17b4d6
3
- size 25495
 
 
 
 
data/10005_一年级上册/audios/page_006_piece_05.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:3881dd3a52388c87a166100276b975c909821ea2ed5df9fde9ea0d317ff0ed61
3
- size 24659
 
 
 
 
data/10005_一年级上册/audios/page_006_piece_06.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b8836ad3b13c0f167b3440948212a5ea33215989074275dd59542e07d3f37dd4
3
- size 21733
 
 
 
 
data/10005_一年级上册/audios/page_007_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:384454cc204eec379e8c53aba0eddcff8a4a727bd91cc9891077d0c886418698
3
- size 53080
 
 
 
 
data/10005_一年级上册/audios/page_007_piece_01.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:faa75a6c25c33bc530d574534debc1cda01e8780276a8bda3df93f8ddfaa38bd
3
- size 129567
 
 
 
 
data/10005_一年级上册/audios/page_007_piece_02.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:9531cb7e9b1cc2a5758ee920ab7182d7a5be60e845e965c4821b34376e2a9288
3
- size 133746
 
 
 
 
data/10005_一年级上册/audios/page_009_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:8a5b4467fbdb87518d42a31bad1ed7b4a3d747cdafaf2b79cbadd5d9312f1a10
3
- size 47647
 
 
 
 
data/10005_一年级上册/audios/page_009_piece_01.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b0deea352ddba234530ed7ba0cbb9058fc800ca0fb40ded8d3999bcb44170255
3
- size 22569
 
 
 
 
data/10005_一年级上册/audios/page_009_piece_02.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:97d098d3294bcceda1ef9425afa47945d7d38dbe1bfc5d0680fdd3addd8033db
3
- size 20897
 
 
 
 
data/10005_一年级上册/audios/page_009_piece_03.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:4c12fd9818d9f1923828e2a3dc839fb297b1f2120497f0d6f00900054384fdd9
3
- size 25495
 
 
 
 
data/10005_一年级上册/audios/page_009_piece_04.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ccbb125b0451f64214a84f9e79d4b7cfc70d243c5822ecb57d447eba3fa7083e
3
- size 27585
 
 
 
 
data/10005_一年级上册/audios/page_009_piece_05.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:a755929366404882216e8f397a126e40cf3c9ca490afd65ac65ae907dd885528
3
- size 28003
 
 
 
 
data/10005_一年级上册/audios/page_010_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f7d47bf1be6e08099577644d3123d508064fc076b057c37f659db2c8d2e6b68d
3
- size 83173
 
 
 
 
data/10005_一年级上册/audios/page_010_piece_01.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:5b6519fab7096620a24834db703a5e8ecf43d3b857ced4e779c00952b77ec922
3
- size 87353
 
 
 
 
data/10005_一年级上册/audios/page_011_piece_00.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:555ec8a313d6de63854fdb07c4e4d0a89e6ee6e96b99a483ef2fa9eafede9209
3
- size 53498
 
 
 
 
data/10005_一年级上册/audios/page_011_piece_01.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:be324c440873e425aef730222704af1e3f681d9a025396df748e24f98e1aee4a
3
- size 23405
 
 
 
 
data/10005_一年级上册/audios/page_011_piece_02.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ba6abf1c452b6dfba05484e2dde2422274cbfdac03d83dd016dd9ed6ec1f4d4a
3
- size 23823
 
 
 
 
data/10005_一年级上册/audios/page_011_piece_03.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:4538306b11fb20ea1b5e279fd4f71314051480e04d14ee608dd636c63d9bb3b3
3
- size 32600
 
 
 
 
data/10005_一年级上册/audios/page_011_piece_04.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:1813f326124a81b5a8a6dac0967fffac8f21b316597fd59a0ee9c577f5a967e4
3
- size 29257
 
 
 
 
data/10005_一年级上册/audios/page_011_piece_05.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d40d3aabfb6296af236f622b39bcbbcb2d095d948c69f56882acec9780b97642
3
- size 29257
 
 
 
 
data/10005_一年级上册/audios/page_011_piece_06.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:0fb841e233e063d5fbfb36a27ff2270bc3d970da883fb3d5459f29f8ec4bce58
3
- size 30093
 
 
 
 
data/10005_一年级上册/audios/page_011_piece_07.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:1786b7c7835750b76eb8e707ba40a4483ebcc8c21a14e9a1b34ae1a32097ac7c
3
- size 28839