Toya0421 commited on
Commit
3db36f1
·
verified ·
1 Parent(s): 441a163

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +446 -170
app.py CHANGED
@@ -3,11 +3,9 @@ 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, 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,25 +14,23 @@ LOG_FILE = "reading_logs.csv"
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,97 +38,124 @@ def load_passage_file(text_id):
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,88 +163,167 @@ def save_log(entry):
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, original_pages_json, current_page, total_pages, pid, orig_lev, session_state):
 
220
  now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
221
 
222
  entry = {
223
- "user_id": session_state.get("user_id"),
224
- "assigned_level": session_state.get("level"),
225
  "passage_id": pid,
226
  "original_level": orig_lev,
227
  "action_time": now,
@@ -233,18 +335,12 @@ def prev_page(pages_json, original_pages_json, current_page, total_pages, pid, o
233
  pages = json.loads(pages_json)
234
  if not pages:
235
  return ("", "", json.dumps([]), 0,
236
- session_state,
237
  gr.update(interactive=False, visible=False),
238
  gr.update(interactive=False, visible=False),
239
  gr.update(interactive=False, visible=False))
240
 
241
- original_pages = json.loads(original_pages_json)
242
  new_page = max(current_page - 1, 0)
243
 
244
- # ★追加:戻る先が未生成なら生成する(nextと同じ)
245
- if pages[new_page] is None:
246
- pages[new_page] = rewrite_page_with_context(pid, new_page, original_pages, session_state.get("level"))
247
-
248
  prev_upd = gr.update(interactive=(new_page > 0), visible=(new_page > 0))
249
  next_visible = (new_page < total_pages - 1)
250
  next_upd = gr.update(interactive=next_visible, visible=next_visible)
@@ -252,8 +348,8 @@ def prev_page(pages_json, original_pages_json, current_page, total_pages, pid, o
252
 
253
  now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
254
  entry2 = {
255
- "user_id": session_state.get("user_id"),
256
- "assigned_level": session_state.get("level"),
257
  "passage_id": pid,
258
  "original_level": orig_lev,
259
  "action_time": now2,
@@ -265,9 +361,73 @@ def prev_page(pages_json, original_pages_json, current_page, total_pages, pid, o
265
  return (
266
  pages[new_page],
267
  f"{new_page+1} / {total_pages}",
268
- json.dumps(pages, ensure_ascii=False),
269
  new_page,
270
- session_state,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  prev_upd,
272
  next_upd,
273
  finish_upd
@@ -275,62 +435,178 @@ def prev_page(pages_json, original_pages_json, current_page, total_pages, pid, o
275
 
276
 
277
  # ======================================================
278
- # UI
279
  # ======================================================
280
- with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  gr.Markdown("# 📚 Reading Exercise")
282
 
283
- student_id = gr.Textbox(label="学生番号")
284
- level = gr.Dropdown([1,2,3,4,5], value=3, label="Reading Level")
285
- start = gr.Button("スタート")
 
 
 
286
 
287
- text = gr.Textbox(lines=18, interactive=False)
288
- page = gr.Textbox(interactive=False)
 
 
 
 
 
 
 
289
 
290
  hidden_pages = gr.Textbox(visible=False)
291
- hidden_original = gr.Textbox(visible=False)
292
- hidden_idx = gr.Number(visible=False)
293
- hidden_pid = gr.Textbox(visible=False)
294
  hidden_orig_lev = gr.Textbox(visible=False)
 
295
 
296
  with gr.Row():
297
- prev_btn = gr.Button("◀ 前へ", visible=False)
298
- next_btn = gr.Button("次へ ▶", visible=False)
299
- finish_btn = gr.Button("読み終えた", visible=False)
300
-
301
- start.click(
302
- start_test,
303
- [student_id, level],
304
- [text, page, hidden_pages, hidden_original,
305
- hidden_idx, hidden_pid, hidden_orig_lev,
306
- prev_btn, next_btn, finish_btn]
 
 
 
 
 
 
307
  )
308
 
309
  next_btn.click(
310
- next_page,
311
- [hidden_pages, hidden_original, hidden_idx,
312
- hidden_pid, hidden_orig_lev],
313
- [text, page, hidden_pages, hidden_idx,
314
- prev_btn, next_btn, finish_btn]
 
 
 
 
 
 
315
  )
316
 
317
  prev_btn.click(
318
- fn=prev_page,
319
- inputs=[
320
- hidden_pages,
321
- hidden_original, # ★文脈付きrewriteのために必要
322
- hidden_page_index,
323
- hidden_total_pages,
324
- hidden_passage_id,
325
- hidden_orig_lev,
326
- session_state
327
- ],
328
- outputs=[
329
- text_display, page_display,
330
- hidden_pages, hidden_page_index,
331
- session_state,
332
- prev_btn, next_btn, finish_btn]
 
 
 
 
 
 
 
 
333
  )
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
- 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
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
 
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
  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
  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,
 
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)
 
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,
 
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
 
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
+ .reading-area {
451
+ padding: 20px !important;
452
+ border-radius: 12px !important;
453
+ border: 1px solid #ccc !important;
454
+ transition: background-color 0.2s ease, color 0.2s ease;
455
+ }
456
+ /* ===============================
457
+ ライトモード
458
+ =============================== */
459
+ @media (prefers-color-scheme: light) {
460
+ body, .gradio-container {
461
+ background-color: #ffffff !important;
462
+ color: #222 !important;
463
+ }
464
+ .reading-area {
465
+ background-color: #fafafa !important;
466
+ color: #222 !important;
467
+ border-color: #ddd !important;
468
+ }
469
+ textarea, input, .gr-textbox textarea {
470
+ background-color: #ffffff !important;
471
+ color: #222 !important;
472
+ border: 1px solid #ccc !important;
473
+ }
474
+ }
475
+ /* ===============================
476
+ ダークモード
477
+ =============================== */
478
+ @media (prefers-color-scheme: dark) {
479
+ body, .gradio-container {
480
+ background-color: #1e1e1e !important;
481
+ color: #e6e6e6 !important;
482
+ }
483
+ /* 教材の背景は黒すぎると読みにくいのでやや明るめのチャコール */
484
+ .reading-area {
485
+ background-color: #2a2a2a !important;
486
+ color: #f2f2f2 !important;
487
+ border-color: #444 !important;
488
+ }
489
+ textarea, input, .gr-textbox textarea {
490
+ background-color: #2c2c2c !important;
491
+ color: #f0f0f0 !important;
492
+ border: 1px solid #555 !important;
493
+ }
494
+ /* ボタンを見やすく */
495
+ button {
496
+ background-color: #3a3a3a !important;
497
+ color: #f0f0f0 !important;
498
+ border: 1px solid #555 !important;
499
+ }
500
+ button:hover {
501
+ background-color: #4a4a4a !important;
502
+ }
503
+ .gr-panel, .gr-box, .gr-group {
504
+ background-color: #272727 !important;
505
+ border-color: #444 !important;
506
+ }
507
+ }
508
+ """
509
+
510
+
511
+ with gr.Blocks(css=custom_css) as demo:
512
  gr.Markdown("# 📚 Reading Exercise")
513
 
514
+ student_id_input = gr.Textbox(label="学生番号(必須)")
515
+ level_input = gr.Dropdown(
516
+ choices=[1,2,3,4,5],
517
+ label="あなたの Reading Level(Level Testの結果を選択)",
518
+ value=3
519
+ )
520
 
521
+ start_btn = gr.Button("スタート")
522
+
523
+ text_display = gr.Textbox(
524
+ label="教材",
525
+ lines=18,
526
+ interactive=False,
527
+ elem_classes=["big-text", "reading-area"]
528
+ )
529
+ page_display = gr.Textbox(label="進行状況", lines=1, interactive=False)
530
 
531
  hidden_pages = gr.Textbox(visible=False)
532
+ hidden_page_index = gr.Number(visible=False)
533
+ hidden_total_pages = gr.Number(visible=False)
534
+ hidden_passage_id = gr.Textbox(visible=False)
535
  hidden_orig_lev = gr.Textbox(visible=False)
536
+ hidden_assigned_lev = gr.Textbox(visible=False)
537
 
538
  with gr.Row():
539
+ prev_btn = gr.Button("◀ 前へ", interactive=False, visible=False)
540
+ next_btn = gr.Button("次へ ▶", interactive=False, visible=False)
541
+ finish_btn = gr.Button("読み終えた", interactive=False, visible=False)
542
+
543
+ retire_btn = gr.Button("リタイア")
544
+
545
+ start_btn.click(
546
+ fn=start_test,
547
+ inputs=[student_id_input, level_input],
548
+ outputs=[
549
+ text_display, page_display,
550
+ hidden_pages, hidden_page_index,
551
+ hidden_total_pages, hidden_passage_id,
552
+ hidden_orig_lev, hidden_assigned_lev,
553
+ prev_btn, next_btn, finish_btn
554
+ ]
555
  )
556
 
557
  next_btn.click(
558
+ fn=next_page,
559
+ inputs=[
560
+ hidden_pages, hidden_page_index,
561
+ hidden_total_pages, hidden_passage_id,
562
+ hidden_orig_lev
563
+ ],
564
+ outputs=[
565
+ text_display, page_display,
566
+ hidden_pages, hidden_page_index,
567
+ prev_btn, next_btn, finish_btn
568
+ ]
569
  )
570
 
571
  prev_btn.click(
572
+ fn=prev_page,
573
+ inputs=[
574
+ hidden_pages, hidden_page_index,
575
+ hidden_total_pages, hidden_passage_id,
576
+ hidden_orig_lev
577
+ ],
578
+ outputs=[
579
+ text_display, page_display,
580
+ hidden_pages, hidden_page_index,
581
+ prev_btn, next_btn, finish_btn
582
+ ]
583
+ )
584
+
585
+ finish_btn.click(
586
+ fn=lambda p, i, pid, o: finish_or_retire(p, i, pid, o, "finished"),
587
+ inputs=[hidden_pages, hidden_page_index, hidden_passage_id, hidden_orig_lev],
588
+ outputs=[
589
+ text_display, page_display,
590
+ hidden_pages, hidden_page_index,
591
+ hidden_total_pages, hidden_passage_id,
592
+ hidden_orig_lev, hidden_assigned_lev,
593
+ prev_btn, next_btn, finish_btn
594
+ ]
595
  )
596
 
597
+ retire_btn.click(
598
+ fn=lambda p, i, pid, o: finish_or_retire(p, i, pid, o, "retire"),
599
+ inputs=[
600
+ hidden_pages, hidden_page_index,
601
+ hidden_passage_id, hidden_orig_lev
602
+ ],
603
+ outputs=[
604
+ text_display, page_display,
605
+ hidden_pages, hidden_page_index,
606
+ hidden_total_pages, hidden_passage_id,
607
+ hidden_orig_lev, hidden_assigned_lev,
608
+ prev_btn, next_btn, finish_btn
609
+ ]
610
+ )
611
 
612
+ demo.launch()