Toya0421 commited on
Commit
dad0129
·
verified ·
1 Parent(s): 8ccda88

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +161 -26
app.py CHANGED
@@ -8,6 +8,9 @@ import re
8
  import threading
9
  import csv # ★追加(csv 1行追記用)
10
 
 
 
 
11
  # --- API / HF 設定 ---
12
  API_KEY = os.getenv("API_KEY")
13
  BASE_URL = "https://openrouter.ai/api/v1"
@@ -41,6 +44,7 @@ LOG_COLUMNS = [
41
  "assigned_level",
42
  "passage_id",
43
  "original_level",
 
44
  "action_time",
45
  "action_type",
46
  "page_text",
@@ -90,7 +94,8 @@ def download_log_csv(password: str) -> str:
90
  # ======================================================
91
  # 新しい教材管理:passages フォルダからランダム選択
92
  # ※ used_passages は session_state に保持(グローバル禁止)
93
- # ★変更:target level よりスコアが低い教材から選ぶ(excelのflesch_score)
 
94
  # ======================================================
95
 
96
  def load_passage_file(text_id):
@@ -117,12 +122,10 @@ def get_title_from_excel(text_id):
117
 
118
  def get_new_passage_random(used_passages_set, target_level):
119
  """
 
120
  passages フォルダからランダムに教材を選び(pg◯.txt)、
121
  passage_information.xlsx の Text# の flesch_score を original_level として返す。
122
-
123
- ★変更点:
124
  - ユーザーの target_level に対応する目標FREよりも低い(=難しい)教材のみから選ぶ
125
- ※ flesch_score は passage_information.xlsx から取得
126
  """
127
  level_to_flesch = {1: 90, 2: 80, 3: 70, 4: 60, 5: 50}
128
  target_flesch = float(level_to_flesch[int(target_level)])
@@ -171,7 +174,54 @@ def get_new_passage_random(used_passages_set, target_level):
171
  orig_level = None
172
  title = None
173
  else:
174
- orig_level = row.iloc[0]["flesch_score"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  title = row.iloc[0].get("Title", None)
176
  if pd.isna(title):
177
  title = None
@@ -180,6 +230,7 @@ def get_new_passage_random(used_passages_set, target_level):
180
 
181
  return text_id, text, orig_level, title, used_passages_set
182
 
 
183
  # ======================================================
184
  # Group1: 本文のみ抽出(書き換えなし)
185
  # ======================================================
@@ -250,22 +301,68 @@ def extract_main_body(text: str) -> str:
250
 
251
  # ======================================================
252
  # Rewrite(同時実行制限付き) Group2で使用
 
253
  # ======================================================
254
 
255
- def rewrite_level(text, target_level):
256
  level_to_flesch = {1: 90, 2: 80, 3: 70, 4: 60, 5: 50}
257
  target_flesch = level_to_flesch[int(target_level)]
258
 
 
 
 
 
 
259
  prompt = f"""
260
- Rewrite the following passage so it fits about {target_flesch} Flesch Reading Ease Score
261
- - Extract only the portions of the text that should be read as the main body,
262
- excluding the title, author name, source information, chapter number, annotations, and footers.
263
- - When outputting, make sure sections divided by chapters, etc., are clearly distinguishable by leaving a blank line between them.
264
- - Preserve the original meaning faithfully.
265
- - Do not add new information or remove essential information.
266
- - Output only the rewritten passage. Do not include explanations.
 
 
267
  {text}
268
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
  with _rewrite_sem:
271
  resp = client.chat.completions.create(
@@ -302,12 +399,19 @@ def split_pages(text, max_words=300):
302
 
303
  # ======================================================
304
  # Start(session_stateでユーザー状態管理)
 
305
  # ======================================================
306
 
307
  def start_test(student_id, level_input, group_input, session_state):
308
  action = "start_pushed"
309
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
310
 
 
 
 
 
 
 
311
  if not student_id or str(student_id).strip() == "":
312
  entry = {
313
  "user_id": None,
@@ -315,6 +419,7 @@ def start_test(student_id, level_input, group_input, session_state):
315
  "assigned_level": None,
316
  "passage_id": None,
317
  "original_level": None,
 
318
  "action_time": now,
319
  "action_type": action,
320
  "page_text": None
@@ -334,7 +439,8 @@ def start_test(student_id, level_input, group_input, session_state):
334
  gr.update(interactive=False, visible=False),
335
  gr.update(interactive=False, visible=True),
336
  gr.update(interactive=False, visible=False),
337
- session_state
 
338
  )
339
 
340
  user_id = str(student_id).strip()
@@ -349,13 +455,19 @@ def start_test(student_id, level_input, group_input, session_state):
349
  "assigned_level": level,
350
  "passage_id": None,
351
  "original_level": None,
 
352
  "action_time": now,
353
  "action_type": action,
354
  "page_text": None
355
  }
356
  save_log(entry)
357
 
358
- pid, text, orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
 
 
 
 
 
359
  if text is None:
360
  return (
361
  "",
@@ -370,13 +482,20 @@ def start_test(student_id, level_input, group_input, session_state):
370
  gr.update(interactive=False, visible=False),
371
  gr.update(interactive=False, visible=False),
372
  gr.update(interactive=False, visible=False),
373
- session_state
 
374
  )
375
 
376
  if group == 1:
377
  processed = extract_main_body(text)
 
378
  else:
379
- processed = rewrite_level(text, level)
 
 
 
 
 
380
 
381
  pages = split_pages(processed)
382
  total = len(pages)
@@ -397,6 +516,7 @@ def start_test(student_id, level_input, group_input, session_state):
397
  "assigned_level": level,
398
  "passage_id": pid,
399
  "original_level": orig_lev,
 
400
  "action_time": now2,
401
  "action_type": "page_displayed_1",
402
  "page_text": pages[0]
@@ -422,7 +542,8 @@ def start_test(student_id, level_input, group_input, session_state):
422
  prev_upd,
423
  next_upd,
424
  finish_upd,
425
- session_state
 
426
  )
427
 
428
  # ======================================================
@@ -441,6 +562,7 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
441
  "assigned_level": level,
442
  "passage_id": pid,
443
  "original_level": orig_lev,
 
444
  "action_time": now,
445
  "action_type": "next_pushed",
446
  "page_text": None
@@ -463,6 +585,7 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
463
  "assigned_level": level,
464
  "passage_id": pid,
465
  "original_level": orig_lev,
 
466
  "action_time": now2,
467
  "action_type": f"page_displayed_{new_page+1}",
468
  "page_text": pages[new_page]
@@ -503,6 +626,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
503
  "assigned_level": level,
504
  "passage_id": pid,
505
  "original_level": orig_lev,
 
506
  "action_time": now,
507
  "action_type": "prev_pushed",
508
  "page_text": None
@@ -530,6 +654,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
530
  "assigned_level": level,
531
  "passage_id": pid,
532
  "original_level": orig_lev,
 
533
  "action_time": now2,
534
  "action_type": f"page_displayed_{new_page+1}",
535
  "page_text": pages[new_page]
@@ -561,12 +686,18 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
561
  "assigned_level": level,
562
  "passage_id": pid,
563
  "original_level": orig_lev,
 
564
  "action_time": now,
565
  "action_type": action,
566
  "page_text": None
567
  })
568
 
569
- new_pid, new_text, new_orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
 
 
 
 
 
570
  if new_text is None:
571
  return (
572
  "", "教材がありません", "", json.dumps([]), 0, "",
@@ -579,8 +710,13 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
579
 
580
  if group == 1:
581
  processed = extract_main_body(new_text)
 
582
  else:
583
- processed = rewrite_level(new_text, level)
 
 
 
 
584
 
585
  new_pages = split_pages(processed)
586
  total = len(new_pages)
@@ -601,6 +737,7 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
601
  "assigned_level": level,
602
  "passage_id": new_pid,
603
  "original_level": new_orig_lev,
 
604
  "action_time": now2,
605
  "action_type": "page_displayed_1",
606
  "page_text": new_pages[0]
@@ -659,7 +796,6 @@ custom_css = """
659
  .gradio-container select {
660
  color: inherit !important;
661
  }
662
-
663
  /* ===============================
664
  ライトモード
665
  =============================== */
@@ -699,7 +835,6 @@ custom_css = """
699
  background-color: #eaeaea !important;
700
  }
701
  }
702
-
703
  /* ===============================
704
  ダークモード
705
  =============================== */
@@ -739,7 +874,6 @@ custom_css = """
739
  color: #e6e6e6 !important;
740
  }
741
  }
742
-
743
  /* ===============================
744
  ★Group選択:CSSのみで見やすく(EdgeでもOK)
745
  =============================== */
@@ -760,7 +894,6 @@ custom_css = """
760
  align-items: center !important;
761
  gap: 10px !important;
762
  }
763
-
764
  /* :has が効く環境は行全体ハイライト */
765
  @media (prefers-color-scheme: light){
766
  #group_radio label:has(input[type="radio"]:checked){
@@ -822,6 +955,7 @@ with gr.Blocks(css=custom_css) as demo:
822
 
823
  retire_btn = gr.Button("リタイア")
824
 
 
825
  start_btn.click(
826
  fn=start_test,
827
  inputs=[student_id_input, level_input, group_input, session_state],
@@ -832,7 +966,8 @@ with gr.Blocks(css=custom_css) as demo:
832
  hidden_total_pages, hidden_passage_id,
833
  hidden_orig_lev, hidden_assigned_lev,
834
  prev_btn, next_btn, finish_btn,
835
- session_state
 
836
  ]
837
  )
838
 
 
8
  import threading
9
  import csv # ★追加(csv 1行追記用)
10
 
11
+ # ★追加:Flesch計測
12
+ import textstat
13
+
14
  # --- API / HF 設定 ---
15
  API_KEY = os.getenv("API_KEY")
16
  BASE_URL = "https://openrouter.ai/api/v1"
 
44
  "assigned_level",
45
  "passage_id",
46
  "original_level",
47
+ "flesch_score", # ★追加:Group1=orig_lev, Group2=rewritten fre
48
  "action_time",
49
  "action_type",
50
  "page_text",
 
94
  # ======================================================
95
  # 新しい教材管理:passages フォルダからランダム選択
96
  # ※ used_passages は session_state に保持(グローバル禁止)
97
+ # ★Group2:target level よりスコアが低い教材から選ぶ(excelのflesch_score)
98
+ # ★Group1:全教材からランダム選択
99
  # ======================================================
100
 
101
  def load_passage_file(text_id):
 
122
 
123
  def get_new_passage_random(used_passages_set, target_level):
124
  """
125
+ ★Group2用:
126
  passages フォルダからランダムに教材を選び(pg◯.txt)、
127
  passage_information.xlsx の Text# の flesch_score を original_level として返す。
 
 
128
  - ユーザーの target_level に対応する目標FREよりも低い(=難しい)教材のみから選ぶ
 
129
  """
130
  level_to_flesch = {1: 90, 2: 80, 3: 70, 4: 60, 5: 50}
131
  target_flesch = float(level_to_flesch[int(target_level)])
 
174
  orig_level = None
175
  title = None
176
  else:
177
+ orig_level = row.iloc[0].get("flesch_score", None)
178
+ title = row.iloc[0].get("Title", None)
179
+ if pd.isna(title):
180
+ title = None
181
+ else:
182
+ title = str(title)
183
+
184
+ return text_id, text, orig_level, title, used_passages_set
185
+
186
+
187
+ def get_new_passage_random_any(used_passages_set):
188
+ """
189
+ ★Group1用:target_level による難易度フィルタなし
190
+ passages フォルダ内の全教材からランダムに選ぶ。
191
+ original_level (=flesch_score) は passage_information.xlsx から取得して返す。
192
+ """
193
+ files = glob.glob("passages/pg*.txt")
194
+ if not files:
195
+ return None, None, None, None, used_passages_set
196
+
197
+ all_ids = []
198
+ for f in files:
199
+ name = os.path.basename(f)
200
+ num = name.replace("pg", "").replace(".txt", "")
201
+ if num.isdigit():
202
+ all_ids.append(int(num))
203
+
204
+ if not all_ids:
205
+ return None, None, None, None, used_passages_set
206
+
207
+ available = [pid for pid in all_ids if pid not in used_passages_set]
208
+ if not available:
209
+ used_passages_set = set()
210
+ available = list(all_ids)
211
+
212
+ text_id = random.choice(available)
213
+ used_passages_set.add(text_id)
214
+
215
+ text = load_passage_file(text_id)
216
+ if text is None:
217
+ return None, None, None, None, used_passages_set
218
+
219
+ row = passage_info_df[passage_info_df["Text#"] == text_id]
220
+ if len(row) == 0:
221
+ orig_level = None
222
+ title = None
223
+ else:
224
+ orig_level = row.iloc[0].get("flesch_score", None)
225
  title = row.iloc[0].get("Title", None)
226
  if pd.isna(title):
227
  title = None
 
230
 
231
  return text_id, text, orig_level, title, used_passages_set
232
 
233
+
234
  # ======================================================
235
  # Group1: 本文のみ抽出(書き換えなし)
236
  # ======================================================
 
301
 
302
  # ======================================================
303
  # Rewrite(同時実行制限付き) Group2で使用
304
+ # ★プロンプトを「改善後プロンプト」に置換
305
  # ======================================================
306
 
307
+ def rewrite_level(text, target_level, original_fre):
308
  level_to_flesch = {1: 90, 2: 80, 3: 70, 4: 60, 5: 50}
309
  target_flesch = level_to_flesch[int(target_level)]
310
 
311
+ try:
312
+ original_fre_val = float(original_fre)
313
+ except Exception:
314
+ original_fre_val = float("nan")
315
+
316
  prompt = f"""
317
+ The Flesch Reading Ease score is a numeric measure of text readability,
318
+ where higher scores indicate easier readability and lower scores indicate more difficult text.
319
+
320
+ In this task, we are trying to rewrite a given text into the target Flesch Reading Ease score
321
+ and preserving the original meaning and information.
322
+
323
+ Given the original draft (Flesch Reading Ease = {original_fre_val}):
324
+
325
+ [TEXT START]
326
  {text}
327
+ [TEXT END]
328
+
329
+ Rewrite the above text to the difficulty level of:
330
+ Flesch Reading Ease = {target_flesch}
331
+
332
+ Follow the instructions below carefully.
333
+
334
+ Content preservation:
335
+ - Maintain the original meaning faithfully.
336
+ - Do not add new information.
337
+ - Do not remove important information.
338
+ - Do not introduce interpretations or opinions that are not present in the original text.
339
+
340
+ Scope of rewriting:
341
+ - Rewrite ONLY the main body text.
342
+ - Completely EXCLUDE titles, headings, chapter labels, author names, source information, footnotes, annotations, and introductions.
343
+ - Do NOT include any text other than the rewritten main body under any circumstances.
344
+
345
+ Readability control guidelines:
346
+ - Make sentences shorter.
347
+ - Prefer familiar, high-frequency vocabulary.
348
+ - Use simple and direct sentence structures.
349
+ - Avoid jargon; if technical terms are necessary, explain them clearly in simple language.
350
+ - Minimize figurative language, idioms, and expressions whose meanings are not directly inferable.
351
+
352
+ Language modernization:
353
+ - Rewrite the text in clear, modern English.
354
+ - Remove archaic expressions and unnatural or outdated syntax typical of older texts at all levels.
355
+
356
+ Structure and formatting:
357
+ - Preserve the original paragraph structure of the main text.
358
+ - Insert exactly ONE blank line between paragraphs.
359
+ - Do NOT create new section breaks, chapter divisions, or headings.
360
+
361
+ Output constraints:
362
+ - Output only the rewritten text.
363
+ - Do not include explanations, comments, or metadata.
364
+ - Do not include [TEXT START] and [TEXT END] in the output.
365
+ """.strip()
366
 
367
  with _rewrite_sem:
368
  resp = client.chat.completions.create(
 
399
 
400
  # ======================================================
401
  # Start(session_stateでユーザー状態管理)
402
+ # ★Start後:入力はリロードまで固定(1回だけ)
403
  # ======================================================
404
 
405
  def start_test(student_id, level_input, group_input, session_state):
406
  action = "start_pushed"
407
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
408
 
409
+ # ★Start押下時点で入力を固定(成功/失敗問わず、リロードまで不可)
410
+ lock_student = gr.update(interactive=False)
411
+ lock_group = gr.update(interactive=False)
412
+ lock_level = gr.update(interactive=False)
413
+ lock_start = gr.update(interactive=False)
414
+
415
  if not student_id or str(student_id).strip() == "":
416
  entry = {
417
  "user_id": None,
 
419
  "assigned_level": None,
420
  "passage_id": None,
421
  "original_level": None,
422
+ "flesch_score": None,
423
  "action_time": now,
424
  "action_type": action,
425
  "page_text": None
 
439
  gr.update(interactive=False, visible=False),
440
  gr.update(interactive=False, visible=True),
441
  gr.update(interactive=False, visible=False),
442
+ session_state,
443
+ lock_student, lock_group, lock_level, lock_start
444
  )
445
 
446
  user_id = str(student_id).strip()
 
455
  "assigned_level": level,
456
  "passage_id": None,
457
  "original_level": None,
458
+ "flesch_score": None,
459
  "action_time": now,
460
  "action_type": action,
461
  "page_text": None
462
  }
463
  save_log(entry)
464
 
465
+ # ★変更:Group1は全教材、Group2はレベル制限あり
466
+ if group == 1:
467
+ pid, text, orig_lev, title, used_passages_set = get_new_passage_random_any(used_passages_set)
468
+ else:
469
+ pid, text, orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
470
+
471
  if text is None:
472
  return (
473
  "",
 
482
  gr.update(interactive=False, visible=False),
483
  gr.update(interactive=False, visible=False),
484
  gr.update(interactive=False, visible=False),
485
+ session_state,
486
+ lock_student, lock_group, lock_level, lock_start
487
  )
488
 
489
  if group == 1:
490
  processed = extract_main_body(text)
491
+ measured_fre = orig_lev # ★要件:Group1はpassage_informationのflesch_scoreを記録
492
  else:
493
+ processed = rewrite_level(text, level, orig_lev)
494
+ # ★要件:Group2は書き換え後をtextstatで計測して記録
495
+ try:
496
+ measured_fre = float(textstat.flesch_reading_ease(processed))
497
+ except Exception:
498
+ measured_fre = None
499
 
500
  pages = split_pages(processed)
501
  total = len(pages)
 
516
  "assigned_level": level,
517
  "passage_id": pid,
518
  "original_level": orig_lev,
519
+ "flesch_score": measured_fre, # ★追加
520
  "action_time": now2,
521
  "action_type": "page_displayed_1",
522
  "page_text": pages[0]
 
542
  prev_upd,
543
  next_upd,
544
  finish_upd,
545
+ session_state,
546
+ lock_student, lock_group, lock_level, lock_start
547
  )
548
 
549
  # ======================================================
 
562
  "assigned_level": level,
563
  "passage_id": pid,
564
  "original_level": orig_lev,
565
+ "flesch_score": "", # ★列維持
566
  "action_time": now,
567
  "action_type": "next_pushed",
568
  "page_text": None
 
585
  "assigned_level": level,
586
  "passage_id": pid,
587
  "original_level": orig_lev,
588
+ "flesch_score": "", # ★列維持
589
  "action_time": now2,
590
  "action_type": f"page_displayed_{new_page+1}",
591
  "page_text": pages[new_page]
 
626
  "assigned_level": level,
627
  "passage_id": pid,
628
  "original_level": orig_lev,
629
+ "flesch_score": "", # ★列維持
630
  "action_time": now,
631
  "action_type": "prev_pushed",
632
  "page_text": None
 
654
  "assigned_level": level,
655
  "passage_id": pid,
656
  "original_level": orig_lev,
657
+ "flesch_score": "", # ★列維持
658
  "action_time": now2,
659
  "action_type": f"page_displayed_{new_page+1}",
660
  "page_text": pages[new_page]
 
686
  "assigned_level": level,
687
  "passage_id": pid,
688
  "original_level": orig_lev,
689
+ "flesch_score": "", # ★列維持
690
  "action_time": now,
691
  "action_type": action,
692
  "page_text": None
693
  })
694
 
695
+ # ★変更:Group1は全教材、Group2はレベル制限あり
696
+ if group == 1:
697
+ new_pid, new_text, new_orig_lev, title, used_passages_set = get_new_passage_random_any(used_passages_set)
698
+ else:
699
+ new_pid, new_text, new_orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
700
+
701
  if new_text is None:
702
  return (
703
  "", "教材がありません", "", json.dumps([]), 0, "",
 
710
 
711
  if group == 1:
712
  processed = extract_main_body(new_text)
713
+ measured_fre = new_orig_lev
714
  else:
715
+ processed = rewrite_level(new_text, level, new_orig_lev)
716
+ try:
717
+ measured_fre = float(textstat.flesch_reading_ease(processed))
718
+ except Exception:
719
+ measured_fre = None
720
 
721
  new_pages = split_pages(processed)
722
  total = len(new_pages)
 
737
  "assigned_level": level,
738
  "passage_id": new_pid,
739
  "original_level": new_orig_lev,
740
+ "flesch_score": measured_fre, # ★追加
741
  "action_time": now2,
742
  "action_type": "page_displayed_1",
743
  "page_text": new_pages[0]
 
796
  .gradio-container select {
797
  color: inherit !important;
798
  }
 
799
  /* ===============================
800
  ライトモード
801
  =============================== */
 
835
  background-color: #eaeaea !important;
836
  }
837
  }
 
838
  /* ===============================
839
  ダークモード
840
  =============================== */
 
874
  color: #e6e6e6 !important;
875
  }
876
  }
 
877
  /* ===============================
878
  ★Group選択:CSSのみで見やすく(EdgeでもOK)
879
  =============================== */
 
894
  align-items: center !important;
895
  gap: 10px !important;
896
  }
 
897
  /* :has が効く環境は行全体ハイライト */
898
  @media (prefers-color-scheme: light){
899
  #group_radio label:has(input[type="radio"]:checked){
 
955
 
956
  retire_btn = gr.Button("リタイア")
957
 
958
+ # ★変更:Start後に入力をロックするため、入力コンポーネントもoutputsに追加
959
  start_btn.click(
960
  fn=start_test,
961
  inputs=[student_id_input, level_input, group_input, session_state],
 
966
  hidden_total_pages, hidden_passage_id,
967
  hidden_orig_lev, hidden_assigned_lev,
968
  prev_btn, next_btn, finish_btn,
969
+ session_state,
970
+ student_id_input, group_input, level_input, start_btn
971
  ]
972
  )
973