Toya0421 commited on
Commit
f3f0f55
·
verified ·
1 Parent(s): eb7cbcb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -88
app.py CHANGED
@@ -1,46 +1,43 @@
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, uuid, tempfile, json
7
  import re
 
8
 
9
  # --- API設定 ---
10
  API_KEY = os.getenv("API_KEY")
11
  BASE_URL = "https://openrouter.ai/api/v1"
12
- HF_TOKEN = os.getenv("HF_TOKEN")
13
- DATASET_REPO = "Toya0421/lexile_test_logging"
14
- LOG_FILE = "logs.csv"
 
15
 
16
  client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
17
 
18
- # --- 外部CSVとして管理する passsage_id / passage データ ---
19
- # (例)columns: passage_id, lexile_level, text
20
  passages_df = pd.read_csv("passage.csv")
21
 
22
  levels = [1, 2, 3, 4, 5]
23
 
24
- # --- 状態 ---
25
- used_passages = set()
26
- question_count = 0
27
- current_user_id = None
28
- action_log = [] # ✅ 選択肢変更ログ
29
 
30
  # --- AIで問題生成 ---
31
  def generate_question(text):
32
  prompt = f"""
33
- Read the following passage and create ONE multiple-choice question with 4 options (A–D).
34
- Only output the question and options.
35
- Format:
36
- Q: <question text>
37
- A. <option>
38
- B. <option>
39
- C. <option>
40
- D. <option>
41
- Passage:
42
- {text}
43
- """
44
  response = client.chat.completions.create(
45
  model="google/gemma-3-27b-it:free",
46
  messages=[{"role": "user", "content": prompt}],
@@ -52,14 +49,14 @@ def generate_question(text):
52
  # --- 正誤判定 ---
53
  def check_answer_with_ai(text, question, user_answer):
54
  prompt = f"""
55
- Read the passage and question below. Decide if the user's answer is correct.
56
- Passage:
57
- {text}
58
- Question:
59
- {question}
60
- User Answer: {user_answer}
61
- Respond with one word: "Correct" or "Incorrect".
62
- """
63
  response = client.chat.completions.create(
64
  model="google/gemma-3-27b-it:free",
65
  messages=[{"role": "user", "content": prompt}],
@@ -68,14 +65,11 @@ def check_answer_with_ai(text, question, user_answer):
68
  )
69
  result = response.choices[0].message.content.strip().lower()
70
 
71
- # まず "incorrect" を検出("incorrect" の中に "correct" が含まれるため先に判定する必要ある)
72
  if re.search(r"\bincorrect\b", result):
73
  return False
74
- # 次に "correct" を検出
75
  if re.search(r"\bcorrect\b", result):
76
  return True
77
-
78
- # どちらでも判断できない場合は安全側(不正解)
79
  return False
80
 
81
  # --- 自動難易度調整 ---
@@ -87,8 +81,8 @@ def adaptive_test(prev_level, prev_correct):
87
  return levels[idx - 1]
88
  return prev_level
89
 
90
- # --- passage取得 ---
91
- def get_passage(level):
92
  subset = passages_df[passages_df["level"] == level]
93
  available = [pid for pid in subset["passage_id"] if pid not in used_passages]
94
  if not available:
@@ -97,83 +91,88 @@ def get_passage(level):
97
  text = subset[subset["passage_id"] == passage_id]["text"].iloc[0]
98
  return passage_id, text
99
 
100
- # --- ログ追記 & Push ---
101
- def log_to_csv_and_push(entry):
102
- df = pd.DataFrame([entry])
103
- if os.path.exists(LOG_FILE):
104
- df.to_csv(LOG_FILE, mode="a", index=False, header=False)
105
- else:
106
- df.to_csv(LOG_FILE, index=False)
107
-
108
- all_logs = pd.read_csv(LOG_FILE)
109
- tmp_dir = tempfile.mkdtemp()
110
- tmp_path = os.path.join(tmp_dir, "data.parquet")
111
- all_logs.to_parquet(tmp_path)
112
-
113
- dataset = Dataset.from_parquet(tmp_path)
114
- dataset.push_to_hub(DATASET_REPO, token=HF_TOKEN)
 
 
115
 
116
  # --- 開始ボタン動作 ---
117
- def start_test(student_id):
118
- global used_passages, question_count, current_user_id, action_log
119
- used_passages = set()
120
- question_count = 0
121
- action_log = []
122
 
123
  if not student_id or student_id.strip() == "":
124
  return (
 
125
  "", "", 0, "", "",
126
  "⚠️ 学生番号を入力してからテストを開始してください",
127
  False, "", "", ""
128
  )
129
 
130
- current_user_id = student_id.strip()
131
 
132
  level = 3
133
- passage_id, text = get_passage(level)
134
- used_passages.add(passage_id)
135
 
136
  question = generate_question(text)
137
  displayed_time = datetime.utcnow() + timedelta(hours=9)
138
 
139
  return (
 
140
  text, question, level, passage_id, "",
141
- "", True, displayed_time.isoformat(), 1, current_user_id
142
  )
143
 
144
- # --- 選択肢変更イベント(ここで操作履歴を保存) ---
145
- def log_choice_change(choice, question_number, user_id):
146
- global action_log
 
147
  if choice:
148
- action_log.append({
149
  "action": "choice",
150
  "choice": choice,
151
  "time": (datetime.utcnow() + timedelta(hours=9)).isoformat()
152
  })
153
- return
154
 
155
  # --- 回答送信 ---
156
  def next_step(prev_level, user_answer, question_text, passage_text,
157
- displayed_time, question_number, user_id, passage_id):
158
 
159
- global question_count, used_passages, action_log
 
160
 
161
  if not user_answer:
162
  return (
 
163
  "⚠️ Please select an answer!", passage_text, question_text, prev_level,
164
  None, "", True, displayed_time, question_number, user_id, passage_id
165
  )
166
 
167
  submit_time = datetime.utcnow() + timedelta(hours=9)
168
- question_count += 1
169
 
170
  correct = check_answer_with_ai(passage_text, question_text, user_answer)
171
  new_level = adaptive_test(prev_level, correct)
172
 
173
- # --- ✅ ログ保存 (actionsに選択履歴がすべて入る)
174
  entry = {
175
  "user_id": user_id,
176
- "question_number": question_count,
177
  "reading_level": prev_level,
178
  "passage_id": passage_id,
179
  "question": question_text,
@@ -181,41 +180,66 @@ def next_step(prev_level, user_answer, question_text, passage_text,
181
  "correct": correct,
182
  "displayed_time": displayed_time,
183
  "submit_time": submit_time.strftime("%Y-%m-%d %H:%M:%S"),
184
- "actions": json.dumps(action_log, ensure_ascii=False)
185
  }
186
- log_to_csv_and_push(entry)
187
 
188
- # 最終問題なら結果だけ大きく表示
189
- if question_count >= 5:
190
  final_level = prev_level if correct else new_level
191
  return (
 
192
  f"<h1>🎯 Your Reading level: <strong>Level {final_level}</strong></h1>",
193
  "", "", final_level,
194
  None, "", False, "", "", user_id, passage_id
195
  )
196
 
197
- # --- 次の問題へ ---
198
- next_passage_id, next_text = get_passage(new_level)
199
- used_passages.add(next_passage_id)
200
 
201
  next_question = generate_question(next_text)
202
  next_display_time = datetime.utcnow() + timedelta(hours=9)
203
 
204
  # ✅ 新しい問題のために選択操作ログをリセット
205
- action_log = []
206
 
207
  feedback = "✅ Correct!" if correct else "❌ Incorrect."
208
 
209
  return (
 
210
  feedback + "\n➡️ Loading next question…",
211
  next_text, next_question, new_level,
212
- None, "", True, next_display_time.isoformat(), question_count + 1, user_id, next_passage_id
213
  )
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  # --- Gradio UI ---
216
  with gr.Blocks() as demo:
217
  gr.Markdown("# 📘 Reading Level Test")
218
 
 
 
 
219
  student_id_input = gr.Textbox(label="Student ID", placeholder="例: B123456")
220
  start_btn = gr.Button("▶️ Start Test")
221
 
@@ -236,34 +260,36 @@ with gr.Blocks() as demo:
236
 
237
  start_btn.click(
238
  fn=start_test,
239
- inputs=[student_id_input],
240
  outputs=[
 
241
  text_display, question_display, hidden_level, hidden_passage_id, user_answer,
242
  feedback_display, test_visible, hidden_display_time,
243
  hidden_question_number, hidden_user_id
244
  ]
245
  )
246
 
247
- # 選択肢変更ログ
248
  user_answer.change(
249
  fn=log_choice_change,
250
- inputs=[user_answer, hidden_question_number, hidden_user_id],
251
- outputs=[]
252
  )
253
 
254
- # 回答送信
255
  submit_btn.click(
256
  fn=next_step,
257
  inputs=[hidden_level, user_answer, question_display, text_display,
258
- hidden_display_time, hidden_question_number, hidden_user_id, hidden_passage_id],
259
  outputs=[
 
260
  feedback_display, text_display, question_display, hidden_level,
261
  user_answer, hidden_passage, test_visible, hidden_display_time,
262
  hidden_question_number, hidden_user_id, hidden_passage_id
263
  ]
264
  )
265
 
266
- # 表示のON/OFF制御
267
  def toggle_visibility(show):
268
  v = bool(show)
269
  return (
@@ -277,4 +303,18 @@ with gr.Blocks() as demo:
277
  outputs=[text_display, question_display, user_answer, submit_btn]
278
  )
279
 
280
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  from openai import OpenAI
 
3
  from datetime import datetime, timedelta
4
  import pandas as pd
5
+ import os, random, tempfile, json
6
  import re
7
+ import threading
8
 
9
  # --- API設定 ---
10
  API_KEY = os.getenv("API_KEY")
11
  BASE_URL = "https://openrouter.ai/api/v1"
12
+ LOG_FILE = os.getenv("LOG_FILE", "logs.csv")
13
+
14
+ # 管理者DL用パスワード(環境変数で設定)
15
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "")
16
 
17
  client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
18
 
19
+ # --- 外部CSVとして管理する passage データ ---
 
20
  passages_df = pd.read_csv("passage.csv")
21
 
22
  levels = [1, 2, 3, 4, 5]
23
 
24
+ # ログの同時書き込みガード(ログは全ユーザーで共有)
25
+ _log_lock = threading.Lock()
 
 
 
26
 
27
  # --- AIで問題生成 ---
28
  def generate_question(text):
29
  prompt = f"""
30
+ Read the following passage and create ONE multiple-choice question with 4 options (A–D).
31
+ Only output the question and options.
32
+ Format:
33
+ Q: <question text>
34
+ A. <option>
35
+ B. <option>
36
+ C. <option>
37
+ D. <option>
38
+ Passage:
39
+ {text}
40
+ """
41
  response = client.chat.completions.create(
42
  model="google/gemma-3-27b-it:free",
43
  messages=[{"role": "user", "content": prompt}],
 
49
  # --- 正誤判定 ---
50
  def check_answer_with_ai(text, question, user_answer):
51
  prompt = f"""
52
+ Read the passage and question below. Decide if the user's answer is correct.
53
+ Passage:
54
+ {text}
55
+ Question:
56
+ {question}
57
+ User Answer: {user_answer}
58
+ Respond with one word: "Correct" or "Incorrect".
59
+ """
60
  response = client.chat.completions.create(
61
  model="google/gemma-3-27b-it:free",
62
  messages=[{"role": "user", "content": prompt}],
 
65
  )
66
  result = response.choices[0].message.content.strip().lower()
67
 
68
+ # "incorrect" の中に "correct" が含まれるため順序重要
69
  if re.search(r"\bincorrect\b", result):
70
  return False
 
71
  if re.search(r"\bcorrect\b", result):
72
  return True
 
 
73
  return False
74
 
75
  # --- 自動難易度調整 ---
 
81
  return levels[idx - 1]
82
  return prev_level
83
 
84
+ # --- passage取得(✅ used_passages は state から渡す) ---
85
+ def get_passage(level, used_passages):
86
  subset = passages_df[passages_df["level"] == level]
87
  available = [pid for pid in subset["passage_id"] if pid not in used_passages]
88
  if not available:
 
91
  text = subset[subset["passage_id"] == passage_id]["text"].iloc[0]
92
  return passage_id, text
93
 
94
+ # --- ログ追記(✅ CSVのみ。Dataset保存はしない) ---
95
+ def log_to_csv(entry):
96
+ with _log_lock:
97
+ df = pd.DataFrame([entry])
98
+ if os.path.exists(LOG_FILE):
99
+ df.to_csv(LOG_FILE, mode="a", index=False, header=False)
100
+ else:
101
+ df.to_csv(LOG_FILE, index=False)
102
+
103
+ # --- セッション state 初期化 ---
104
+ def _new_session_state():
105
+ return {
106
+ "used_passages": set(),
107
+ "question_count": 0,
108
+ "user_id": None,
109
+ "action_log": [],
110
+ }
111
 
112
  # --- 開始ボタン動作 ---
113
+ def start_test(student_id, state):
114
+ # 新規開始時は常に初期化
115
+ state = _new_session_state()
 
 
116
 
117
  if not student_id or student_id.strip() == "":
118
  return (
119
+ state,
120
  "", "", 0, "", "",
121
  "⚠️ 学生番号を入力してからテストを開始してください",
122
  False, "", "", ""
123
  )
124
 
125
+ state["user_id"] = student_id.strip()
126
 
127
  level = 3
128
+ passage_id, text = get_passage(level, state["used_passages"])
129
+ state["used_passages"].add(passage_id)
130
 
131
  question = generate_question(text)
132
  displayed_time = datetime.utcnow() + timedelta(hours=9)
133
 
134
  return (
135
+ state,
136
  text, question, level, passage_id, "",
137
+ "", True, displayed_time.isoformat(), 1, state["user_id"]
138
  )
139
 
140
+ # --- 選択肢変更イベント(操作履歴を保存) ---
141
+ def log_choice_change(choice, question_number, user_id, state):
142
+ if not isinstance(state, dict):
143
+ state = _new_session_state()
144
  if choice:
145
+ state["action_log"].append({
146
  "action": "choice",
147
  "choice": choice,
148
  "time": (datetime.utcnow() + timedelta(hours=9)).isoformat()
149
  })
150
+ return state
151
 
152
  # --- 回答送信 ---
153
  def next_step(prev_level, user_answer, question_text, passage_text,
154
+ displayed_time, question_number, user_id, passage_id, state):
155
 
156
+ if not isinstance(state, dict):
157
+ state = _new_session_state()
158
 
159
  if not user_answer:
160
  return (
161
+ state,
162
  "⚠️ Please select an answer!", passage_text, question_text, prev_level,
163
  None, "", True, displayed_time, question_number, user_id, passage_id
164
  )
165
 
166
  submit_time = datetime.utcnow() + timedelta(hours=9)
167
+ state["question_count"] += 1
168
 
169
  correct = check_answer_with_ai(passage_text, question_text, user_answer)
170
  new_level = adaptive_test(prev_level, correct)
171
 
172
+ # ✅ ログ保存actionsに選択履歴
173
  entry = {
174
  "user_id": user_id,
175
+ "question_number": state["question_count"],
176
  "reading_level": prev_level,
177
  "passage_id": passage_id,
178
  "question": question_text,
 
180
  "correct": correct,
181
  "displayed_time": displayed_time,
182
  "submit_time": submit_time.strftime("%Y-%m-%d %H:%M:%S"),
183
+ "actions": json.dumps(state["action_log"], ensure_ascii=False)
184
  }
185
+ log_to_csv(entry)
186
 
187
+ # 最終問題なら結果だけ大きく表示
188
+ if state["question_count"] >= 5:
189
  final_level = prev_level if correct else new_level
190
  return (
191
+ state,
192
  f"<h1>🎯 Your Reading level: <strong>Level {final_level}</strong></h1>",
193
  "", "", final_level,
194
  None, "", False, "", "", user_id, passage_id
195
  )
196
 
197
+ # 次の問題へ
198
+ next_passage_id, next_text = get_passage(new_level, state["used_passages"])
199
+ state["used_passages"].add(next_passage_id)
200
 
201
  next_question = generate_question(next_text)
202
  next_display_time = datetime.utcnow() + timedelta(hours=9)
203
 
204
  # ✅ 新しい問題のために選択操作ログをリセット
205
+ state["action_log"] = []
206
 
207
  feedback = "✅ Correct!" if correct else "❌ Incorrect."
208
 
209
  return (
210
+ state,
211
  feedback + "\n➡️ Loading next question…",
212
  next_text, next_question, new_level,
213
+ None, "", True, next_display_time.isoformat(), state["question_count"] + 1, user_id, next_passage_id
214
  )
215
 
216
+ # --- 管理者DL(再起動までの logs.csv を返す) ---
217
+ def download_logs(admin_password):
218
+ if not ADMIN_PASSWORD:
219
+ return None, "⚠️ ADMIN_PASSWORD が設定されていません(環境変数で設定してください)"
220
+
221
+ if (admin_password or "") != ADMIN_PASSWORD:
222
+ return None, "❌ Password incorrect."
223
+
224
+ with _log_lock:
225
+ if not os.path.exists(LOG_FILE):
226
+ return None, "⚠️ logs.csv がまだ存在しません(ログが1件もありません)"
227
+
228
+ # DL安定のため一時コピーして返す
229
+ tmp_dir = tempfile.mkdtemp()
230
+ out_path = os.path.join(tmp_dir, "logs.csv")
231
+ with open(LOG_FILE, "rb") as fsrc, open(out_path, "wb") as fdst:
232
+ fdst.write(fsrc.read())
233
+
234
+ return out_path, "✅ logs.csv is ready."
235
+
236
  # --- Gradio UI ---
237
  with gr.Blocks() as demo:
238
  gr.Markdown("# 📘 Reading Level Test")
239
 
240
+ # ✅ セッションごとの状態
241
+ session_state = gr.State(_new_session_state())
242
+
243
  student_id_input = gr.Textbox(label="Student ID", placeholder="例: B123456")
244
  start_btn = gr.Button("▶️ Start Test")
245
 
 
260
 
261
  start_btn.click(
262
  fn=start_test,
263
+ inputs=[student_id_input, session_state],
264
  outputs=[
265
+ session_state,
266
  text_display, question_display, hidden_level, hidden_passage_id, user_answer,
267
  feedback_display, test_visible, hidden_display_time,
268
  hidden_question_number, hidden_user_id
269
  ]
270
  )
271
 
272
+ # 選択肢変更ログ
273
  user_answer.change(
274
  fn=log_choice_change,
275
+ inputs=[user_answer, hidden_question_number, hidden_user_id, session_state],
276
+ outputs=[session_state]
277
  )
278
 
279
+ # 回答送信
280
  submit_btn.click(
281
  fn=next_step,
282
  inputs=[hidden_level, user_answer, question_display, text_display,
283
+ hidden_display_time, hidden_question_number, hidden_user_id, hidden_passage_id, session_state],
284
  outputs=[
285
+ session_state,
286
  feedback_display, text_display, question_display, hidden_level,
287
  user_answer, hidden_passage, test_visible, hidden_display_time,
288
  hidden_question_number, hidden_user_id, hidden_passage_id
289
  ]
290
  )
291
 
292
+ # 表示のON/OFF制御
293
  def toggle_visibility(show):
294
  v = bool(show)
295
  return (
 
303
  outputs=[text_display, question_display, user_answer, submit_btn]
304
  )
305
 
306
+ gr.Markdown("---")
307
+ gr.Markdown("## 🔒 Admin: Download logs (since last restart)")
308
+
309
+ admin_pw = gr.Textbox(label="Admin Password", type="password", placeholder="Enter admin password")
310
+ dl_btn = gr.Button("⬇️ Download logs.csv")
311
+ dl_file = gr.File(label="logs.csv (download)")
312
+ dl_msg = gr.Markdown()
313
+
314
+ dl_btn.click(
315
+ fn=download_logs,
316
+ inputs=[admin_pw],
317
+ outputs=[dl_file, dl_msg]
318
+ )
319
+
320
+ demo.launch()