Toya0421 commited on
Commit
a61cf9e
·
verified ·
1 Parent(s): 88fcf15

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +331 -160
app.py CHANGED
@@ -1,187 +1,358 @@
1
  import gradio as gr
 
 
 
2
  import pandas as pd
3
- import time
4
- import random
5
- from datasets import load_dataset, Dataset
6
-
7
- # --- Load passages CSV ---
8
- try:
9
- passages_df = pd.read_csv("passage.csv")
10
- except:
11
- passages_df = pd.DataFrame(columns=["genre", "passage_id", "page", "text"])
12
-
13
- # --- Logging dataset ---
14
- log_repo = "Toya0421/reading_exercise_logging"
15
-
16
-
17
- def start_test(student_id, selected_genre):
18
- if not student_id:
19
- return gr.update(value="⚠️ 学籍番号を入力してください", visible=True), gr.update(visible=False)
20
-
21
- # そのジャンルから教材リストを抽出
22
- genre_passages = passages_df[passages_df["genre"] == selected_genre]
23
-
24
- if genre_passages.empty:
25
- return "⚠️ 選択ジャンルに教材がありません", gr.update(visible=False)
26
-
27
- # 最初の教材を選ぶ
28
- passage_id = random.choice(genre_passages["passage_id"].unique())
29
- pages = genre_passages[genre_passages["passage_id"] == passage_id].sort_values("page")
30
-
31
- # 初期状態
32
- state = {
33
- "student_id": student_id,
34
- "genre": selected_genre,
35
- "passage_id": passage_id,
36
- "pages": pages,
37
- "page_index": 0,
38
- "logs": []
39
- }
40
-
41
- return "", state, pages.iloc[0]["text"], enable_buttons(state)
42
-
43
-
44
- def enable_buttons(state):
45
- last_page = len(state["pages"]) - 1
46
- pi = state["page_index"]
47
-
48
- return {
49
- "prev": gr.update(interactive=(pi > 0)),
50
- "next": gr.update(interactive=(pi < last_page)),
51
- "finish": gr.update(interactive=(pi == last_page)), # ✅ 最終ページのみ押せる
52
- "retire": gr.update(interactive=True)
53
- }
54
-
55
-
56
- def next_page(state):
57
- if state is None:
58
- return None, "", enable_buttons(state)
59
-
60
- if state["page_index"] < len(state["pages"]) - 1:
61
- state["page_index"] += 1
62
-
63
- text = state["pages"].iloc[state["page_index"]]["text"]
64
- return state, text, enable_buttons(state)
65
-
66
-
67
- def prev_page(state):
68
- if state is None:
69
- return None, "", enable_buttons(state)
70
-
71
- if state["page_index"] > 0:
72
- state["page_index"] -= 1
73
-
74
- text = state["pages"].iloc[state["page_index"]]["text"]
75
- return state, text, enable_buttons(state)
76
-
77
-
78
- def choose_next_passage(state, finished):
79
- """finished = True → 読み終えた、False → リタイア"""
80
-
81
- # ログ追加
82
- state["logs"].append({
83
- "student_id": state["student_id"],
84
- "genre": state["genre"],
85
- "passage_id": state["passage_id"],
86
- "completed": finished,
87
- "timestamp": time.time()
88
- })
89
-
90
- # ログ保存
91
- save_logs(state["logs"])
92
-
93
- # 次の教材選択
94
- genre_passages = passages_df[passages_df["genre"] == state["genre"]]
95
- remaining = genre_passages[genre_passages["passage_id"] != state["passage_id"]]
96
-
97
- if remaining.empty:
98
- return state, "✅ 同ジャンルの教材は終了しました", disable_all()
99
-
100
- # 次の教材
101
- new_passage_id = random.choice(remaining["passage_id"].unique())
102
- pages = genre_passages[genre_passages["passage_id"] == new_passage_id].sort_values("page")
103
-
104
- state["passage_id"] = new_passage_id
105
- state["pages"] = pages
106
- state["page_index"] = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- return state, pages.iloc[0]["text"], enable_buttons(state)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- def disable_all():
112
- return {
113
- "prev": gr.update(interactive=False),
114
- "next": gr.update(interactive=False),
115
- "finish": gr.update(interactive=False),
116
- "retire": gr.update(interactive=False)
 
 
 
 
 
 
 
 
 
 
117
  }
118
-
119
-
120
- def save_logs(logs):
121
- if not logs:
122
- return
123
- df = pd.DataFrame(logs)
124
- dataset = Dataset.from_pandas(df)
125
- dataset.push_to_hub(log_repo, private=False)
126
-
127
-
128
- # ==== UI ====
129
-
130
- with gr.Blocks() as demo:
131
- gr.Markdown("### 📘 リーディングアプリ")
132
-
133
- student_id = gr.Textbox(label="学籍番号を入力", placeholder="例: A12345")
134
-
135
- genre_select = gr.Dropdown(
136
- choices=[
137
- "Literature", "Science&Technology", "History",
138
- "Social Science&Society", "Arts&Culture",
139
- "Religion&Philosophy", "Lifestyle&Hobbies",
140
- "Health&Medicine", "Education&Reference"
141
- ],
142
- label="ジャンルを選択"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  )
144
 
145
- start_btn = gr.Button("スタート")
146
- warn = gr.Textbox(label="警告", visible=False)
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- passage_state = gr.State()
149
- passage_text = gr.Textbox(label="教材", lines=15, interactive=False)
 
150
 
151
- prev_btn = gr.Button("◀ 前へ")
152
- next_btn = gr.Button("次へ ▶")
153
- finish_btn = gr.Button("読み終えた")
154
- retire_btn = gr.Button("リタイア")
155
 
156
- # --- Event binding ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  start_btn.click(
158
- start_test,
159
- [student_id, genre_select],
160
- [warn, passage_state, passage_text, prev_btn, next_btn, finish_btn, retire_btn]
 
 
 
 
 
 
161
  )
162
 
 
163
  next_btn.click(
164
- next_page,
165
- passage_state,
166
- [passage_state, passage_text, prev_btn, next_btn, finish_btn, retire_btn]
 
 
 
 
167
  )
168
 
169
  prev_btn.click(
170
- prev_page,
171
- passage_state,
172
- [passage_state, passage_text, prev_btn, next_btn, finish_btn, retire_btn]
 
 
 
 
173
  )
174
 
 
175
  finish_btn.click(
176
- lambda s: choose_next_passage(s, True),
177
- passage_state,
178
- [passage_state, passage_text, prev_btn, next_btn, finish_btn, retire_btn]
 
 
 
 
 
 
179
  )
180
 
181
  retire_btn.click(
182
- lambda s: choose_next_passage(s, False),
183
- passage_state,
184
- [passage_state, passage_text, prev_btn, next_btn, finish_btn, retire_btn]
 
 
 
 
 
 
185
  )
186
 
187
  demo.launch()
 
1
  import gradio as gr
2
+ 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
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")
12
+ DATASET_REPO = "Toya0421/reading_exercise_logging"
13
+ LOG_FILE = "reading_logs.csv"
14
+
15
+ client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
16
+
17
+ # --- passage.csv 読み込み(columns: passage_id, genre, text, original_lexile_score)---
18
+ passages_df = pd.read_csv("passage.csv")
19
+
20
+ genres = [
21
+ "Literature","Science&Technology","History","Social Science&Society",
22
+ "Arts&Culture","Religion&Philosophy","Lifestyle&Hobbies",
23
+ "Health&Medicine","Education&Reference"
24
+ ]
25
+
26
+ # --- 状態変数 ---
27
+ used_passages = set()
28
+ current_user_id = None
29
+ current_genre = None
30
+ current_lexile = None
31
+ action_log = [] # ページ操作ログ
32
+
33
+ # --- ヘルパー関数 ---
34
+ def rewrite_to_lexile(text, target_lexile):
35
+ prompt = f"""
36
+ Rewrite the following passage so it fits about {target_lexile} Lexile.
37
+ - Keep original meaning and length
38
+ - Avoid figurative language
39
+ - Use simple syntax
40
+ - Output only the rewritten passage
41
+
42
+ {text}
43
+ """
44
+ resp = client.chat.completions.create(
45
+ model="google/gemma-3-27b-it:free",
46
+ messages=[{"role": "user", "content": prompt}],
47
+ temperature=0.4,
48
+ max_tokens=1000
49
+ )
50
+ return resp.choices[0].message.content.strip()
51
+
52
+ def split_pages(text, words=120):
53
+ w = text.split()
54
+ pages = [" ".join(w[i:i+words]) for i in range(0, len(w), words)]
55
+ return pages if pages else [text]
56
+
57
+ def get_new_passage(same_genre=True):
58
+ """同ジャンル or 別ジャンルでランダムに passage を返す (passage_id, text, original_lexile)"""
59
+ global used_passages, current_genre
60
+ if same_genre:
61
+ df = passages_df[passages_df["genre"] == current_genre]
62
+ else:
63
+ df = passages_df[passages_df["genre"] != current_genre]
64
+
65
+ if df.empty:
66
+ return None, None, None
67
+
68
+ available = [pid for pid in df["passage_id"].unique() if pid not in used_passages]
69
+ if not available:
70
+ # 使い切ったら既読クリアして再利用
71
+ used_passages.clear()
72
+ available = list(df["passage_id"].unique())
73
+
74
+ pid = random.choice(available)
75
+ row = df[df["passage_id"] == pid].iloc[0]
76
+ used_passages.add(pid)
77
+ return pid, row["text"], row.get("original_lexile_score", None)
78
+
79
+ def save_log(entry):
80
+ df = pd.DataFrame([entry])
81
+ if os.path.exists(LOG_FILE):
82
+ df.to_csv(LOG_FILE, mode="a", index=False, header=False)
83
+ else:
84
+ df.to_csv(LOG_FILE, index=False)
85
+ # push to HF
86
+ all_logs = pd.read_csv(LOG_FILE)
87
+ tmp_dir = tempfile.mkdtemp()
88
+ tmp_path = os.path.join(tmp_dir, "data.parquet")
89
+ all_logs.to_parquet(tmp_path)
90
+ dataset = Dataset.from_parquet(tmp_path)
91
+ dataset.push_to_hub(DATASET_REPO, token=HF_TOKEN)
92
+
93
+ # ============================
94
+ # Gradio コールバック関数
95
+ # ============================
96
+
97
+ def start_test(student_id, genre, lexile_input):
98
+ """開始:学生番号・ジャンル・Lexile入力は必須(仕様どおり)"""
99
+ global current_user_id, current_genre, current_lexile, used_passages, action_log
100
+ used_passages = set()
101
+ action_log = []
102
+
103
+ if not student_id or str(student_id).strip() == "":
104
+ # テキストとページ表示だけ空で返し、ボタンはすべて disabled
105
+ return (
106
+ "", # text_display
107
+ "", # page_display
108
+ json.dumps([]), # hidden_pages
109
+ 0, # hidden_page_index
110
+ "", # start_time
111
+ 0, # total_pages
112
+ "", None, None, # pid, orig_lex, assigned_lex
113
+ gr.update(interactive=False), # prev_btn update
114
+ gr.update(interactive=False), # next_btn update
115
+ gr.update(interactive=False) # finish_btn update
116
+ )
117
+
118
+ current_user_id = str(student_id).strip()
119
+ current_genre = genre
120
+ current_lexile = int(lexile_input)
121
+
122
+ pid, text, orig_lex = get_new_passage(same_genre=True)
123
+ if text is None:
124
+ return (
125
+ "教材が見つかりません", "", json.dumps([]), 0, "", 0, "", None, None,
126
+ gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False)
127
+ )
128
+
129
+ rewritten = rewrite_to_lexile(text, current_lexile)
130
+ pages = split_pages(rewritten)
131
+ start_time = (datetime.utcnow() + timedelta(hours=9)).isoformat()
132
+ total = len(pages)
133
+
134
+ # ボタン状態(最初は prev 無効、 next 有効 if pages>1、 finish 有効 only if single page)
135
+ prev_upd = gr.update(interactive=False)
136
+ next_upd = gr.update(interactive=(total > 1))
137
+ finish_upd = gr.update(interactive=(0 == total - 1)) # True if only 1 page
138
+
139
+ return (
140
+ pages[0], # text_display
141
+ f"1 / {total}", # page_display
142
+ json.dumps(pages, ensure_ascii=False), # hidden_pages
143
+ 0, # hidden_page_index
144
+ start_time, # hidden_start_time
145
+ total, # hidden_total_pages
146
+ pid, # hidden_passage_id
147
+ orig_lex, # hidden_orig_lex
148
+ current_lexile, # hidden_assigned_lex
149
+ prev_upd,
150
+ next_upd,
151
+ finish_upd
152
+ )
153
 
154
+ def next_page(pages_json, current_page, total_pages):
155
+ pages = json.loads(pages_json)
156
+ if not pages:
157
+ return "", "", json.dumps([]), 0, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False)
158
+ # advance safely
159
+ new_page = min(current_page + 1, total_pages - 1)
160
+ if new_page != current_page:
161
+ action_log.append({"action": "next", "time": (datetime.utcnow() + timedelta(hours=9)).isoformat()})
162
+ finish_enabled = (new_page == total_pages - 1)
163
+ prev_enabled = (new_page > 0)
164
+ next_enabled = (new_page < total_pages - 1)
165
+ return (
166
+ pages[new_page],
167
+ f"{new_page+1} / {total_pages}",
168
+ json.dumps(pages, ensure_ascii=False),
169
+ new_page,
170
+ gr.update(interactive=prev_enabled),
171
+ gr.update(interactive=next_enabled),
172
+ gr.update(interactive=finish_enabled)
173
+ )
174
 
175
+ def prev_page(pages_json, current_page, total_pages):
176
+ pages = json.loads(pages_json)
177
+ if not pages:
178
+ return "", "", json.dumps([]), 0, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False)
179
+ new_page = max(current_page - 1, 0)
180
+ if new_page != current_page:
181
+ action_log.append({"action": "prev", "time": (datetime.utcnow() + timedelta(hours=9)).isoformat()})
182
+ finish_enabled = (new_page == total_pages - 1)
183
+ prev_enabled = (new_page > 0)
184
+ next_enabled = (new_page < total_pages - 1)
185
+ return (
186
+ pages[new_page],
187
+ f"{new_page+1} / {total_pages}",
188
+ json.dumps(pages, ensure_ascii=False),
189
+ new_page,
190
+ gr.update(interactive=prev_enabled),
191
+ gr.update(interactive=next_enabled),
192
+ gr.update(interactive=finish_enabled)
193
+ )
194
 
195
+ def finish_or_retire(pages_json, current_page, pid, orig_lex, start_time, action):
196
+ """action == 'finished' or 'retire'"""
197
+ pages = json.loads(pages_json)
198
+ now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
199
+
200
+ # ログ
201
+ entry = {
202
+ "user_id": current_user_id,
203
+ "genre": current_genre,
204
+ "assigned_lexile": current_lexile,
205
+ "passage_id": pid,
206
+ "original_lexile": orig_lex,
207
+ "first_page_displayed": start_time,
208
+ "finished_time": now,
209
+ "actions": json.dumps(action_log, ensure_ascii=False),
210
+ "result": action
211
  }
212
+ save_log(entry=entry) if 'save_log' in globals() else save_log_local(entry)
213
+
214
+ # 次教材を取得
215
+ if action == "finished":
216
+ new_pid, new_text, new_orig_lex = get_new_passage(same_genre=True)
217
+ else:
218
+ new_pid, new_text, new_orig_lex = get_new_passage(same_genre=False)
219
+
220
+ if new_text is None:
221
+ # 全部終わった or 該当なし
222
+ return (
223
+ "教材がありません", # text_display
224
+ "", # page_display
225
+ json.dumps([]), # hidden_pages
226
+ 0, # page_index
227
+ "", # start_time
228
+ 0, # total_pages
229
+ "", None, None, # pid, orig_lex, assigned_lex
230
+ gr.update(interactive=False),
231
+ gr.update(interactive=False),
232
+ gr.update(interactive=False)
233
+ )
234
+
235
+ rewritten = rewrite_to_lexile(new_text, current_lexile)
236
+ new_pages = split_pages(rewritten)
237
+ new_start = (datetime.utcnow() + timedelta(hours=9)).isoformat()
238
+ total = len(new_pages)
239
+
240
+ # reset action log for new passage
241
+ action_log.clear()
242
+
243
+ prev_upd = gr.update(interactive=False)
244
+ next_upd = gr.update(interactive=(total > 1))
245
+ finish_upd = gr.update(interactive=(0 == total - 1))
246
+
247
+ return (
248
+ new_pages[0],
249
+ f"1 / {total}",
250
+ json.dumps(new_pages, ensure_ascii=False),
251
+ 0,
252
+ new_start,
253
+ total,
254
+ new_pid,
255
+ new_orig_lex,
256
+ current_lexile,
257
+ prev_upd, next_upd, finish_upd
258
  )
259
 
260
+ # fallback local saver if earlier definitions differ
261
+ def save_log_local(entry):
262
+ df = pd.DataFrame([entry])
263
+ if os.path.exists(LOG_FILE):
264
+ df.to_csv(LOG_FILE, mode="a", index=False, header=False)
265
+ else:
266
+ df.to_csv(LOG_FILE, index=False)
267
+
268
+ # ============================
269
+ # UI 部分(Lexile 入力は復活)
270
+ # ============================
271
+ with gr.Blocks() as demo:
272
+ gr.Markdown("# 📚 Reading Exercise")
273
 
274
+ student_id_input = gr.Textbox(label="学生番号(必須)")
275
+ lexile_input = gr.Number(label="あなたの Lexile スコア(例: 900)", precision=0)
276
+ genre_input = gr.Dropdown(choices=genres, label="ジャンル(1つ選択)")
277
 
278
+ start_btn = gr.Button("スタート")
 
 
 
279
 
280
+ text_display = gr.Textbox(label="教材", lines=18, interactive=False)
281
+ page_display = gr.Textbox(label="進行状況", lines=1, interactive=False)
282
+
283
+ # hidden state
284
+ hidden_pages = gr.Textbox(visible=False)
285
+ hidden_page_index = gr.Number(visible=False)
286
+ hidden_start_time = gr.Textbox(visible=False)
287
+ hidden_total_pages = gr.Number(visible=False)
288
+ hidden_passage_id = gr.Textbox(visible=False)
289
+ hidden_orig_lex = gr.Textbox(visible=False)
290
+ hidden_assigned_lex = gr.Textbox(visible=False)
291
+
292
+ # buttons row (always visible)
293
+ with gr.Row():
294
+ prev_btn = gr.Button("◀ 前へ", interactive=False)
295
+ next_btn = gr.Button("次へ ▶", interactive=False)
296
+ finish_btn = gr.Button("読み終えた", interactive=False)
297
+ retire_btn = gr.Button("リタイア")
298
+
299
+ # Start click
300
  start_btn.click(
301
+ fn=start_test,
302
+ inputs=[student_id_input, genre_input, lexile_input],
303
+ outputs=[
304
+ text_display, page_display,
305
+ hidden_pages, hidden_page_index,
306
+ hidden_start_time, hidden_total_pages, hidden_passage_id,
307
+ hidden_orig_lex, hidden_assigned_lex,
308
+ prev_btn, next_btn, finish_btn
309
+ ]
310
  )
311
 
312
+ # Next / Prev click
313
  next_btn.click(
314
+ fn=next_page,
315
+ inputs=[hidden_pages, hidden_page_index, hidden_total_pages],
316
+ outputs=[
317
+ text_display, page_display,
318
+ hidden_pages, hidden_page_index,
319
+ prev_btn, next_btn, finish_btn
320
+ ]
321
  )
322
 
323
  prev_btn.click(
324
+ fn=prev_page,
325
+ inputs=[hidden_pages, hidden_page_index, hidden_total_pages],
326
+ outputs=[
327
+ text_display, page_display,
328
+ hidden_pages, hidden_page_index,
329
+ prev_btn, next_btn, finish_btn
330
+ ]
331
  )
332
 
333
+ # Finish / Retire click
334
  finish_btn.click(
335
+ fn=lambda p, i, pid, o, st: finish_or_retire(p, i, pid, o, st, "finished"),
336
+ inputs=[hidden_pages, hidden_page_index, hidden_passage_id, hidden_orig_lex, hidden_start_time],
337
+ outputs=[
338
+ text_display, page_display,
339
+ hidden_pages, hidden_page_index,
340
+ hidden_start_time, hidden_total_pages, hidden_passage_id,
341
+ hidden_orig_lex, hidden_assigned_lex,
342
+ prev_btn, next_btn, finish_btn
343
+ ]
344
  )
345
 
346
  retire_btn.click(
347
+ fn=lambda p, i, pid, o, st: finish_or_retire(p, i, pid, o, st, "retire"),
348
+ inputs=[hidden_pages, hidden_page_index, hidden_passage_id, hidden_orig_lex, hidden_start_time],
349
+ outputs=[
350
+ text_display, page_display,
351
+ hidden_pages, hidden_page_index,
352
+ hidden_start_time, hidden_total_pages, hidden_passage_id,
353
+ hidden_orig_lex, hidden_assigned_lex,
354
+ prev_btn, next_btn, finish_btn
355
+ ]
356
  )
357
 
358
  demo.launch()