Toya0421 commited on
Commit
a47ced4
·
verified ·
1 Parent(s): 6bc83ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -499
app.py CHANGED
@@ -3,9 +3,11 @@ from openai import OpenAI
3
  from datasets import Dataset
4
  from datetime import datetime, timedelta
5
  import pandas as pd
6
- import time, os, random, tempfile, json, glob
7
 
8
- # --- API / HF 設定 ---
 
 
9
  API_KEY = os.getenv("API_KEY")
10
  BASE_URL = "https://openrouter.ai/api/v1"
11
  HF_TOKEN = os.getenv("HF_TOKEN")
@@ -14,23 +16,25 @@ LOG_FILE = "reading_logs.csv"
14
 
15
  client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
16
 
17
- # --- passage_information.xlsx 読み込み (Text# と flesch_score 使用) ---
 
 
18
  passage_info_df = pd.read_excel("passage_information.xlsx")
19
 
20
- # --- 状態変数 ---
 
 
21
  used_passages = set()
22
  current_user_id = None
23
  current_level = None
24
 
 
 
25
 
26
  # ======================================================
27
- # 新しい教材管理:passages フォルダからランダム選択
28
  # ======================================================
29
-
30
  def load_passage_file(text_id):
31
- """
32
- passages/pg{text_id}.txt を読み込み、内容を返す。
33
- """
34
  path = f"passages/pg{text_id}.txt"
35
  if not os.path.exists(path):
36
  return None
@@ -38,124 +42,97 @@ def load_passage_file(text_id):
38
  return f.read()
39
 
40
  def get_new_passage_random():
41
- """
42
- passages フォルダからランダムに教材を選び(pg◯.txt)、
43
- passage_information.xlsx の Text# の flesch_score を original_level として返す。
44
- """
45
  global used_passages
46
-
47
- # --- pg*.txt を取得 ---
48
  files = glob.glob("passages/pg*.txt")
49
- if not files:
50
- return None, None, None
51
-
52
- # --- ファイル名から Text# (整数) を抽出 ---
53
- all_ids = []
54
- for f in files:
55
- name = os.path.basename(f)
56
- num = name.replace("pg", "").replace(".txt", "")
57
- if num.isdigit():
58
- all_ids.append(int(num))
59
-
60
- # --- 未使用の ID を優先 ---
61
  available = [pid for pid in all_ids if pid not in used_passages]
62
  if not available:
63
  used_passages.clear()
64
- available = list(all_ids)
65
 
66
- # --- ランダムに選択 ---
67
- text_id = random.choice(available)
68
- used_passages.add(text_id)
69
 
70
- # --- テキスト読み込み ---
71
- text = load_passage_file(text_id)
72
- if text is None:
73
- return None, None, None
74
 
75
- # --- Excel から original_level (flesch_score) を取得 ---
76
- row = passage_info_df[passage_info_df["Text#"] == text_id]
77
- if len(row) == 0:
78
- orig_level = None
79
- else:
80
- orig_level = row.iloc[0]["flesch_score"]
81
 
82
- return text_id, text, orig_level
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
 
 
 
 
 
 
84
 
85
  # ======================================================
86
- # Rewrite
87
  # ======================================================
 
 
 
 
88
 
89
- def rewrite_level(text, target_level):
90
- level_to_flesch = {
91
- 1: 90,
92
- 2: 70,
93
- 3: 55,
94
- 4: 40,
95
- 5: 25
96
- }
97
 
 
98
  target_flesch = level_to_flesch[int(target_level)]
99
 
100
  prompt = f"""
101
- Rewrite the following passage so it fits about {target_flesch} Flesch Reading Ease Score
102
- - Extract only the portions of the text that should be read as the main body,
103
- excluding the title, author name, source information, chapter number, annotations, and footers.
104
- - When outputting, make sure sections divided by chapters, etc., are clearly distinguishable by leaving a blank line between them.
105
- - Preserve the original meaning faithfully.
106
- - Do not add new information or remove essential information.
107
- - Output only the rewritten passage. Do not include explanations.
108
- {text}
 
 
 
 
 
 
 
 
109
  """
110
 
111
  resp = client.chat.completions.create(
112
  model="google/gemini-2.5-flash",
113
  messages=[{"role": "user", "content": prompt}],
114
  temperature=0.4,
115
- max_tokens=5000
116
  )
117
- return resp.choices[0].message.content.strip()
118
-
119
-
120
- import re
121
-
122
- def split_pages(text, max_words=300):
123
- """
124
- 文単位でページを分割する。
125
- - 文の途中でページを分割しない
126
- - max_words の上限を超えないようにする
127
- """
128
- # 文に分割(. ? ! のあとに改行やスペースが続くパターン)
129
- sentences = re.split(r'(?<=[.!?])\s+', text.strip())
130
- pages = []
131
- current_page = []
132
- current_word_count = 0
133
-
134
- for sentence in sentences:
135
- words = sentence.split()
136
- sentence_len = len(words)
137
-
138
- # 次の文を追加すると max_words を超える場合 → 新しいページを作る
139
- if current_word_count + sentence_len > max_words:
140
- if current_page:
141
- pages.append(" ".join(current_page))
142
- current_page = [sentence]
143
- current_word_count = sentence_len
144
- else:
145
- current_page.append(sentence)
146
- current_word_count += sentence_len
147
-
148
- # 最後のページを追加
149
- if current_page:
150
- pages.append(" ".join(current_page))
151
-
152
- return pages or [text]
153
 
 
 
 
154
 
155
  # ======================================================
156
- # Save Log
157
  # ======================================================
158
-
159
  def save_log(entry):
160
  df = pd.DataFrame([entry])
161
  if os.path.exists(LOG_FILE):
@@ -163,459 +140,133 @@ def save_log(entry):
163
  else:
164
  df.to_csv(LOG_FILE, index=False)
165
 
166
- all_logs = pd.read_csv(LOG_FILE)
167
- tmp_dir = tempfile.mkdtemp()
168
- tmp_path = os.path.join(tmp_dir, "data.parquet")
169
- all_logs.to_parquet(tmp_path)
170
- dataset = Dataset.from_parquet(tmp_path)
171
- dataset.push_to_hub(DATASET_REPO, token=HF_TOKEN)
172
-
173
-
174
  # ======================================================
175
- # Start
176
  # ======================================================
177
-
178
  def start_test(student_id, level_input):
179
  global current_user_id, current_level, used_passages
180
  used_passages = set()
181
 
182
- action = "start_pushed"
183
- now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
184
 
185
- entry = {
186
- "user_id": student_id,
187
- "assigned_level": current_level,
188
- "passage_id": None,
189
- "original_level": None,
190
- "action_time": now,
191
- "action_type": action,
192
- "page_text": None
193
- }
194
- save_log(entry)
195
-
196
- if not student_id or str(student_id).strip() == "":
197
- return (
198
- "", "", json.dumps([]), 0, "",
199
- 0, "", None, None,
200
- gr.update(interactive=False, visible=False),
201
- gr.update(interactive=False, visible=True),
202
- gr.update(interactive=False, visible=False)
203
- )
204
-
205
- current_user_id = str(student_id).strip()
206
  current_level = int(level_input)
207
 
208
  pid, text, orig_lev = get_new_passage_random()
209
- if text is None:
210
- return (
211
- "教材が見つかりません", "", json.dumps([]), 0, "",
212
- 0, "", None, None,
213
- gr.update(interactive=False, visible=False),
214
- gr.update(interactive=False, visible=False),
215
- gr.update(interactive=False, visible=False)
216
- )
217
-
218
- rewritten = rewrite_level(text, current_level)
219
- pages = split_pages(rewritten)
220
- total = len(pages)
221
-
222
- if total == 1:
223
- prev_upd = gr.update(interactive=False, visible=False)
224
- next_upd = gr.update(interactive=False, visible=False)
225
- finish_upd = gr.update(interactive=True, visible=True)
226
- else:
227
- prev_upd = gr.update(interactive=False, visible=False)
228
- next_upd = gr.update(interactive=True, visible=True)
229
- finish_upd = gr.update(interactive=False, visible=False)
230
 
231
- page_num = 1
232
- now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
 
 
233
 
234
- entry = {
235
- "user_id": current_user_id,
236
  "assigned_level": current_level,
237
  "passage_id": pid,
238
  "original_level": orig_lev,
239
- "action_time": now,
240
- "action_type": f"page_displayed_{page_num}",
241
- "page_text": pages[0]
242
- }
243
- save_log(entry)
244
 
245
  return (
246
- pages[0],
247
- f"1 / {total}",
248
- json.dumps(pages, ensure_ascii=False),
 
249
  0,
250
- total,
251
  pid,
252
  orig_lev,
253
- current_level,
254
- prev_upd,
255
- next_upd,
256
- finish_upd
257
  )
258
 
259
-
260
  # ======================================================
261
- # Next / Prev / Finish(以下は元コードのまま)
262
  # ======================================================
263
-
264
- def next_page(pages_json, current_page, total_pages, pid, orig_lev):
265
- now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
266
-
267
- entry = {
268
- "user_id": current_user_id,
269
- "assigned_level": current_level,
270
- "passage_id": pid,
271
- "original_level": orig_lev,
272
- "action_time": now,
273
- "action_type": "next_pushed",
274
- "page_text": None
275
- }
276
- save_log(entry)
277
-
278
  pages = json.loads(pages_json)
279
- if not pages:
280
- return ("", "", json.dumps([]), 0,
281
- gr.update(interactive=False, visible=False),
282
- gr.update(interactive=False, visible=False),
283
- gr.update(interactive=False, visible=False))
284
 
285
- new_page = min(current_page + 1, total_pages - 1)
 
286
 
287
- now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
288
- entry2 = {
289
  "user_id": current_user_id,
290
  "assigned_level": current_level,
291
  "passage_id": pid,
292
  "original_level": orig_lev,
293
- "action_time": now2,
294
- "action_type": f"page_displayed_{new_page+1}",
295
- "page_text": pages[new_page]
296
- }
297
- save_log(entry2)
298
-
299
- if new_page == total_pages - 1:
300
- return (
301
- pages[new_page],
302
- f"{new_page+1} / {total_pages}",
303
- json.dumps(pages),
304
- new_page,
305
- gr.update(interactive=True, visible=True),
306
- gr.update(interactive=False, visible=False),
307
- gr.update(interactive=True, visible=True)
308
- )
309
 
 
310
  return (
311
- pages[new_page],
312
- f"{new_page+1} / {total_pages}",
313
- json.dumps(pages),
314
- new_page,
315
- gr.update(interactive=(new_page > 0), visible=(new_page > 0)),
316
- gr.update(interactive=True, visible=True),
317
- gr.update(interactive=False, visible=False)
318
  )
319
 
320
-
321
- def prev_page(pages_json, current_page, total_pages, pid, orig_lev):
322
- now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
323
-
324
- entry = {
325
- "user_id": current_user_id,
326
- "assigned_level": current_level,
327
- "passage_id": pid,
328
- "original_level": orig_lev,
329
- "action_time": now,
330
- "action_type": "prev_pushed",
331
- "page_text": None
332
- }
333
- save_log(entry)
334
-
335
  pages = json.loads(pages_json)
336
- if not pages:
337
- return ("", "", json.dumps([]), 0,
338
- gr.update(interactive=False, visible=False),
339
- gr.update(interactive=False, visible=False),
340
- gr.update(interactive=False, visible=False))
341
-
342
- new_page = max(current_page - 1, 0)
343
-
344
- prev_upd = gr.update(interactive=(new_page > 0), visible=(new_page > 0))
345
- next_visible = (new_page < total_pages - 1)
346
- next_upd = gr.update(interactive=next_visible, visible=next_visible)
347
- finish_upd = gr.update(interactive=(not next_visible), visible=(not next_visible))
348
-
349
- now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
350
- entry2 = {
351
- "user_id": current_user_id,
352
- "assigned_level": current_level,
353
- "passage_id": pid,
354
- "original_level": orig_lev,
355
- "action_time": now2,
356
- "action_type": f"page_displayed_{new_page+1}",
357
- "page_text": pages[new_page]
358
- }
359
- save_log(entry2)
360
-
361
  return (
362
- pages[new_page],
363
- f"{new_page+1} / {total_pages}",
364
- json.dumps(pages),
365
- new_page,
366
- prev_upd,
367
- next_upd,
368
- finish_upd
369
- )
370
-
371
-
372
- def finish_or_retire(pages_json, current_page, pid, orig_lev, action):
373
- pages = json.loads(pages_json)
374
- now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
375
-
376
- entry = {
377
- "user_id": current_user_id,
378
- "assigned_level": current_level,
379
- "passage_id": pid,
380
- "original_level": orig_lev,
381
- "action_time": now,
382
- "action_type": action,
383
- "page_text": None
384
- }
385
- save_log(entry)
386
-
387
- new_pid, new_text, new_orig_lev = get_new_passage_random()
388
- if new_text is None:
389
- return (
390
- "教材がありません", "", json.dumps([]), 0, "",
391
- 0, "", None, None,
392
- gr.update(interactive=False, visible=False),
393
- gr.update(interactive=False, visible=False),
394
- gr.update(interactive=False, visible=False)
395
- )
396
-
397
- rewritten = rewrite_level(new_text, current_level)
398
- new_pages = split_pages(rewritten)
399
- total = len(new_pages)
400
-
401
- if total == 1:
402
- prev_upd = gr.update(interactive=False, visible=False)
403
- next_upd = gr.update(interactive=False, visible=False)
404
- finish_upd = gr.update(interactive=True, visible=True)
405
- else:
406
- prev_upd = gr.update(interactive=False, visible=False)
407
- next_upd = gr.update(interactive=True, visible=True)
408
- finish_upd = gr.update(interactive=False, visible=False)
409
-
410
- now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
411
- entry2 = {
412
- "user_id": current_user_id,
413
- "assigned_level": current_level,
414
- "passage_id": new_pid,
415
- "original_level": new_orig_lev,
416
- "action_time": now2,
417
- "action_type": "page_displayed_1",
418
- "page_text": new_pages[0]
419
- }
420
- save_log(entry2)
421
-
422
- return (
423
- new_pages[0],
424
- f"1 / {total}",
425
- json.dumps(new_pages, ensure_ascii=False),
426
- 0,
427
- total,
428
- new_pid,
429
- new_orig_lev,
430
- current_level,
431
- prev_upd,
432
- next_upd,
433
- finish_upd
434
  )
435
 
436
-
437
  # ======================================================
438
- # UI
439
  # ======================================================
440
- custom_css = """
441
- /* ===============================
442
- 共通(両モード)
443
- =============================== */
444
- .big-text {
445
- font-size: 22px !important;
446
- line-height: 1.8 !important;
447
- font-family: "Noto Sans", sans-serif !important;
448
- }
449
-
450
- /* 教材表示ボックス */
451
- .reading-area {
452
- padding: 20px !important;
453
- border-radius: 12px !important;
454
- border: 1px solid #ccc !important;
455
- transition: background-color 0.2s ease, color 0.2s ease;
456
- }
457
-
458
- /* ===============================
459
- ライトモード
460
- =============================== */
461
- @media (prefers-color-scheme: light) {
462
- body, .gradio-container {
463
- background-color: #ffffff !important;
464
- color: #222 !important;
465
- }
466
-
467
- .reading-area {
468
- background-color: #fafafa !important;
469
- color: #222 !important;
470
- border-color: #ddd !important;
471
- }
472
-
473
- textarea, input, .gr-textbox textarea {
474
- background-color: #ffffff !important;
475
- color: #222 !important;
476
- border: 1px solid #ccc !important;
477
- }
478
- }
479
-
480
- /* ===============================
481
- ダークモード
482
- =============================== */
483
- @media (prefers-color-scheme: dark) {
484
- body, .gradio-container {
485
- background-color: #1e1e1e !important;
486
- color: #e6e6e6 !important;
487
- }
488
-
489
- /* 教材の背景は黒すぎると読みにくいのでやや明るめのチャコール */
490
- .reading-area {
491
- background-color: #2a2a2a !important;
492
- color: #f2f2f2 !important;
493
- border-color: #444 !important;
494
- }
495
-
496
- textarea, input, .gr-textbox textarea {
497
- background-color: #2c2c2c !important;
498
- color: #f0f0f0 !important;
499
- border: 1px solid #555 !important;
500
- }
501
-
502
- /* ボタン��見やすく */
503
- button {
504
- background-color: #3a3a3a !important;
505
- color: #f0f0f0 !important;
506
- border: 1px solid #555 !important;
507
- }
508
- button:hover {
509
- background-color: #4a4a4a !important;
510
- }
511
-
512
- .gr-panel, .gr-box, .gr-group {
513
- background-color: #272727 !important;
514
- border-color: #444 !important;
515
- }
516
- }
517
- """
518
-
519
-
520
- with gr.Blocks(css=custom_css) as demo:
521
  gr.Markdown("# 📚 Reading Exercise")
522
 
523
- student_id_input = gr.Textbox(label="学生番号(必須)")
524
- level_input = gr.Dropdown(
525
- choices=[1,2,3,4,5],
526
- label="あなたの Reading Level(Level Testの結果を選択)",
527
- value=3
528
- )
529
-
530
- start_btn = gr.Button("スタート")
531
 
532
- text_display = gr.Textbox(
533
- label="教材",
534
- lines=18,
535
- interactive=False,
536
- elem_classes=["big-text", "reading-area"]
537
- )
538
- page_display = gr.Textbox(label="進行状況", lines=1, interactive=False)
539
 
540
  hidden_pages = gr.Textbox(visible=False)
541
- hidden_page_index = gr.Number(visible=False)
542
- hidden_total_pages = gr.Number(visible=False)
543
- hidden_passage_id = gr.Textbox(visible=False)
544
  hidden_orig_lev = gr.Textbox(visible=False)
545
- hidden_assigned_lev = gr.Textbox(visible=False)
546
 
547
  with gr.Row():
548
- prev_btn = gr.Button("◀ 前へ", interactive=False, visible=False)
549
- next_btn = gr.Button("次へ ▶", interactive=False, visible=False)
550
- finish_btn = gr.Button("読み終えた", interactive=False, visible=False)
551
-
552
- retire_btn = gr.Button("リタイア")
553
-
554
- start_btn.click(
555
- fn=start_test,
556
- inputs=[student_id_input, level_input],
557
- outputs=[
558
- text_display, page_display,
559
- hidden_pages, hidden_page_index,
560
- hidden_total_pages, hidden_passage_id,
561
- hidden_orig_lev, hidden_assigned_lev,
562
- prev_btn, next_btn, finish_btn
563
- ]
564
  )
565
 
566
  next_btn.click(
567
- fn=next_page,
568
- inputs=[
569
- hidden_pages, hidden_page_index,
570
- hidden_total_pages, hidden_passage_id,
571
- hidden_orig_lev
572
- ],
573
- outputs=[
574
- text_display, page_display,
575
- hidden_pages, hidden_page_index,
576
- prev_btn, next_btn, finish_btn
577
- ]
578
- )
579
-
580
- prev_btn.click(
581
- fn=prev_page,
582
- inputs=[
583
- hidden_pages, hidden_page_index,
584
- hidden_total_pages, hidden_passage_id,
585
- hidden_orig_lev
586
- ],
587
- outputs=[
588
- text_display, page_display,
589
- hidden_pages, hidden_page_index,
590
- prev_btn, next_btn, finish_btn
591
- ]
592
- )
593
-
594
- finish_btn.click(
595
- fn=lambda p, i, pid, o: finish_or_retire(p, i, pid, o, "finished"),
596
- inputs=[hidden_pages, hidden_page_index, hidden_passage_id, hidden_orig_lev],
597
- outputs=[
598
- text_display, page_display,
599
- hidden_pages, hidden_page_index,
600
- hidden_total_pages, hidden_passage_id,
601
- hidden_orig_lev, hidden_assigned_lev,
602
- prev_btn, next_btn, finish_btn
603
- ]
604
- )
605
-
606
- retire_btn.click(
607
- fn=lambda p, i, pid, o: finish_or_retire(p, i, pid, o, "retire"),
608
- inputs=[
609
- hidden_pages, hidden_page_index,
610
- hidden_passage_id, hidden_orig_lev
611
- ],
612
- outputs=[
613
- text_display, page_display,
614
- hidden_pages, hidden_page_index,
615
- hidden_total_pages, hidden_passage_id,
616
- hidden_orig_lev, hidden_assigned_lev,
617
- prev_btn, next_btn, finish_btn
618
- ]
619
  )
620
 
621
  demo.launch()
 
3
  from datasets import Dataset
4
  from datetime import datetime, timedelta
5
  import pandas as pd
6
+ import time, os, random, tempfile, json, glob, re
7
 
8
+ # ======================================================
9
+ # API / HF 設定
10
+ # ======================================================
11
  API_KEY = os.getenv("API_KEY")
12
  BASE_URL = "https://openrouter.ai/api/v1"
13
  HF_TOKEN = os.getenv("HF_TOKEN")
 
16
 
17
  client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
18
 
19
+ # ======================================================
20
+ # passage_information.xlsx 読み込み
21
+ # ======================================================
22
  passage_info_df = pd.read_excel("passage_information.xlsx")
23
 
24
+ # ======================================================
25
+ # グローバル状態(※最小限)
26
+ # ======================================================
27
  used_passages = set()
28
  current_user_id = None
29
  current_level = None
30
 
31
+ # rewrite キャッシュ
32
+ rewrite_cache = {}
33
 
34
  # ======================================================
35
+ # 教材ロード
36
  # ======================================================
 
37
  def load_passage_file(text_id):
 
 
 
38
  path = f"passages/pg{text_id}.txt"
39
  if not os.path.exists(path):
40
  return None
 
42
  return f.read()
43
 
44
  def get_new_passage_random():
 
 
 
 
45
  global used_passages
 
 
46
  files = glob.glob("passages/pg*.txt")
47
+ all_ids = [int(os.path.basename(f).replace("pg", "").replace(".txt", ""))
48
+ for f in files]
49
+
 
 
 
 
 
 
 
 
 
50
  available = [pid for pid in all_ids if pid not in used_passages]
51
  if not available:
52
  used_passages.clear()
53
+ available = all_ids
54
 
55
+ pid = random.choice(available)
56
+ used_passages.add(pid)
 
57
 
58
+ text = load_passage_file(pid)
59
+ row = passage_info_df[passage_info_df["Text#"] == pid]
60
+ orig_level = row.iloc[0]["flesch_score"] if len(row) else None
 
61
 
62
+ return pid, text, orig_level
 
 
 
 
 
63
 
64
+ # ======================================================
65
+ # ページ分割
66
+ # ======================================================
67
+ def split_pages(text, max_words=300):
68
+ sentences = re.split(r'(?<=[.!?])\s+', text.strip())
69
+ pages, buf, wc = [], [], 0
70
+ for s in sentences:
71
+ w = len(s.split())
72
+ if wc + w > max_words:
73
+ pages.append(" ".join(buf))
74
+ buf, wc = [s], w
75
+ else:
76
+ buf.append(s)
77
+ wc += w
78
+ if buf:
79
+ pages.append(" ".join(buf))
80
+ return pages
81
 
82
+ # ======================================================
83
+ # 文脈要約(軽量)
84
+ # ======================================================
85
+ def summarize_for_context(text, max_sentences=2):
86
+ sents = re.split(r'(?<=[.!?])\s+', text.strip())
87
+ return " ".join(sents[:max_sentences])
88
 
89
  # ======================================================
90
+ # 文脈付きページ rewrite(キャッシュあり)
91
  # ======================================================
92
+ def rewrite_page_with_context(pid, page_index, original_pages, target_level):
93
+ key = f"{pid}_{target_level}_{page_index}"
94
+ if key in rewrite_cache:
95
+ return rewrite_cache[key]
96
 
97
+ prev_summary = summarize_for_context(original_pages[page_index - 1]) if page_index > 0 else ""
98
+ next_summary = summarize_for_context(original_pages[page_index + 1]) if page_index < len(original_pages) - 1 else ""
 
 
 
 
 
 
99
 
100
+ level_to_flesch = {1:90, 2:70, 3:55, 4:40, 5:25}
101
  target_flesch = level_to_flesch[int(target_level)]
102
 
103
  prompt = f"""
104
+ You are rewriting ONE page of a passage.
105
+
106
+ Context (for coherence only):
107
+ - Previous summary:
108
+ {prev_summary}
109
+
110
+ - Next summary:
111
+ {next_summary}
112
+
113
+ Rewrite ONLY the following page so that it fits about {target_flesch} Flesch Reading Ease.
114
+ - Preserve meaning and references.
115
+ - Do not add or remove information.
116
+ - Output only the rewritten page.
117
+
118
+ [Page]
119
+ {original_pages[page_index]}
120
  """
121
 
122
  resp = client.chat.completions.create(
123
  model="google/gemini-2.5-flash",
124
  messages=[{"role": "user", "content": prompt}],
125
  temperature=0.4,
126
+ max_tokens=1200
127
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ rewritten = resp.choices[0].message.content.strip()
130
+ rewrite_cache[key] = rewritten
131
+ return rewritten
132
 
133
  # ======================================================
134
+ # ログ保存
135
  # ======================================================
 
136
  def save_log(entry):
137
  df = pd.DataFrame([entry])
138
  if os.path.exists(LOG_FILE):
 
140
  else:
141
  df.to_csv(LOG_FILE, index=False)
142
 
 
 
 
 
 
 
 
 
143
  # ======================================================
144
+ # Start
145
  # ======================================================
 
146
  def start_test(student_id, level_input):
147
  global current_user_id, current_level, used_passages
148
  used_passages = set()
149
 
150
+ if not student_id:
151
+ return "", "", "", "", 0, "", "", None, None, None
152
 
153
+ current_user_id = student_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  current_level = int(level_input)
155
 
156
  pid, text, orig_lev = get_new_passage_random()
157
+ original_pages = split_pages(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ rewritten_pages = [None] * len(original_pages)
160
+ rewritten_pages[0] = rewrite_page_with_context(
161
+ pid, 0, original_pages, current_level
162
+ )
163
 
164
+ save_log({
165
+ "user_id": student_id,
166
  "assigned_level": current_level,
167
  "passage_id": pid,
168
  "original_level": orig_lev,
169
+ "action_time": datetime.now().isoformat(),
170
+ "action_type": "page_displayed_1",
171
+ "page_text": rewritten_pages[0]
172
+ })
 
173
 
174
  return (
175
+ rewritten_pages[0],
176
+ f"1 / {len(original_pages)}",
177
+ json.dumps(rewritten_pages, ensure_ascii=False),
178
+ json.dumps(original_pages, ensure_ascii=False),
179
  0,
 
180
  pid,
181
  orig_lev,
182
+ gr.update(interactive=False, visible=False),
183
+ gr.update(interactive=len(original_pages)>1, visible=len(original_pages)>1),
184
+ gr.update(interactive=len(original_pages)==1, visible=len(original_pages)==1)
 
185
  )
186
 
 
187
  # ======================================================
188
+ # Next / Prev
189
  # ======================================================
190
+ def next_page(pages_json, original_json, page_idx, pid, orig_lev):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  pages = json.loads(pages_json)
192
+ original = json.loads(original_json)
193
+ idx = page_idx + 1
 
 
 
194
 
195
+ if pages[idx] is None:
196
+ pages[idx] = rewrite_page_with_context(pid, idx, original, current_level)
197
 
198
+ save_log({
 
199
  "user_id": current_user_id,
200
  "assigned_level": current_level,
201
  "passage_id": pid,
202
  "original_level": orig_lev,
203
+ "action_time": datetime.now().isoformat(),
204
+ "action_type": f"page_displayed_{idx+1}",
205
+ "page_text": pages[idx]
206
+ })
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ total = len(original)
209
  return (
210
+ pages[idx],
211
+ f"{idx+1} / {total}",
212
+ json.dumps(pages, ensure_ascii=False),
213
+ idx,
214
+ gr.update(interactive=idx>0, visible=idx>0),
215
+ gr.update(interactive=idx<total-1, visible=idx<total-1),
216
+ gr.update(interactive=idx==total-1, visible=idx==total-1)
217
  )
218
 
219
+ def prev_page(pages_json, page_idx):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  pages = json.loads(pages_json)
221
+ idx = page_idx - 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  return (
223
+ pages[idx],
224
+ "",
225
+ pages_json,
226
+ idx,
227
+ gr.update(interactive=idx>0, visible=idx>0),
228
+ gr.update(interactive=True, visible=True),
229
+ gr.update(interactive=False, visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  )
231
 
 
232
  # ======================================================
233
+ # UI
234
  # ======================================================
235
+ with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  gr.Markdown("# 📚 Reading Exercise")
237
 
238
+ student_id = gr.Textbox(label="学生番号")
239
+ level = gr.Dropdown([1,2,3,4,5], value=3, label="Reading Level")
240
+ start = gr.Button("スタート")
 
 
 
 
 
241
 
242
+ text = gr.Textbox(lines=18, interactive=False)
243
+ page = gr.Textbox(interactive=False)
 
 
 
 
 
244
 
245
  hidden_pages = gr.Textbox(visible=False)
246
+ hidden_original = gr.Textbox(visible=False)
247
+ hidden_idx = gr.Number(visible=False)
248
+ hidden_pid = gr.Textbox(visible=False)
249
  hidden_orig_lev = gr.Textbox(visible=False)
 
250
 
251
  with gr.Row():
252
+ prev_btn = gr.Button("◀ 前へ", visible=False)
253
+ next_btn = gr.Button("次へ ▶", visible=False)
254
+ finish_btn = gr.Button("読み終えた", visible=False)
255
+
256
+ start.click(
257
+ start_test,
258
+ [student_id, level],
259
+ [text, page, hidden_pages, hidden_original,
260
+ hidden_idx, hidden_pid, hidden_orig_lev,
261
+ prev_btn, next_btn, finish_btn]
 
 
 
 
 
 
262
  )
263
 
264
  next_btn.click(
265
+ next_page,
266
+ [hidden_pages, hidden_original, hidden_idx,
267
+ hidden_pid, hidden_orig_lev],
268
+ [text, page, hidden_pages, hidden_idx,
269
+ prev_btn, next_btn, finish_btn]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  )
271
 
272
  demo.launch()