Toya0421 commited on
Commit
7652c73
·
verified ·
1 Parent(s): 6e8129e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +94 -114
app.py CHANGED
@@ -12,7 +12,6 @@ API_KEY = os.getenv("API_KEY")
12
  BASE_URL = "https://openrouter.ai/api/v1"
13
  LOG_FILE = os.getenv("LOG_FILE", "logs.csv")
14
 
15
- # ✅ 管理者DL用パスワード(環境変数で設定)
16
  ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "0421")
17
 
18
  client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
@@ -22,11 +21,10 @@ passages_df = pd.read_csv("passage.csv")
22
 
23
  levels = [1, 2, 3, 4, 5]
24
 
25
- # ✅ ログの同時書き込みガード(ログは全ユーザーで共有)
26
  _log_lock = threading.Lock()
27
 
28
  # =========================
29
- # ログ列(指定順)
30
  # =========================
31
  LOG_COLUMNS = [
32
  "user_id",
@@ -40,17 +38,32 @@ LOG_COLUMNS = [
40
  "actions",
41
  ]
42
 
43
- # --- セッション state 初期化 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def _new_session_state():
45
  return {
46
  "session_id": str(uuid.uuid4()),
47
  "used_passages": set(),
48
- "question_count": 0, # ✅ 採点済み問題数
49
  "user_id": None,
50
- "action_log": [], # ✅ 1問中の選択変更履歴(submit時にまとめて保存)
51
  }
52
 
53
- # --- ログ追記(CSVのみ・固定カラムで統一) ---
54
  def log_to_csv(entry: dict):
55
  with _log_lock:
56
  row = {col: entry.get(col, None) for col in LOG_COLUMNS}
@@ -67,9 +80,19 @@ def log_to_csv(entry: dict):
67
  )
68
 
69
  def log_event(state: dict, **kwargs):
70
- # ✅ Excelで#####になりにくいよう「JST」を付けてテキスト化
71
  now = (datetime.utcnow() + timedelta(hours=9)).strftime("%Y-%m-%d %H:%M:%S") + " JST"
72
- entry = {"time": now, "user_id": state.get("user_id"), **kwargs}
 
 
 
 
 
 
 
 
 
 
 
73
  log_to_csv(entry)
74
 
75
  # --- AIで問題生成 ---
@@ -112,14 +135,13 @@ Respond with one word: "Correct" or "Incorrect".
112
  temperature=0,
113
  )
114
  result = response.choices[0].message.content.strip().lower()
115
-
116
  if re.search(r"\bincorrect\b", result):
117
  return False
118
  if re.search(r"\bcorrect\b", result):
119
  return True
120
  return False
121
 
122
- # --- 自動難易度調整 ---
123
  def adaptive_test(prev_level, prev_correct):
124
  idx = levels.index(prev_level)
125
  if prev_correct and idx < len(levels) - 1:
@@ -138,7 +160,7 @@ def get_passage(level, used_passages):
138
  text = subset[subset["passage_id"] == passage_id]["text"].iloc[0]
139
  return passage_id, text
140
 
141
- # --- 開始ボタン動作(✅ start時だけログ) ---
142
  def start_test(student_id, state):
143
  state = _new_session_state()
144
 
@@ -157,42 +179,41 @@ def start_test(student_id, state):
157
  state["used_passages"].add(passage_id)
158
 
159
  question = generate_question(text)
160
- displayed_time = datetime.utcnow() + timedelta(hours=9)
161
 
162
- # ✅ startログ:actions列に "start test pushed" を入れる
163
  log_event(
164
  state,
165
- question_number=None,
166
- reading_level=None,
167
- passage_id=None,
168
- question=None,
169
  choice=None,
170
  correct=None,
171
  actions="start test pushed",
172
  )
173
 
 
 
174
  return (
175
  state,
176
  text, question, level, passage_id, None,
177
  "", True, displayed_time.isoformat(), 1, state["user_id"]
178
  )
179
 
180
- # --- 選択変更(CSVには書かない。actionsにだけ溜める) ---
181
  def log_choice_change(choice, question_number, user_id, state):
182
- if not isinstance(state, dict):
183
- state = _new_session_state()
184
  if choice:
185
  t_iso = (datetime.utcnow() + timedelta(hours=9)).isoformat()
186
- state["action_log"].append({"action": "choice", "choice": choice, "time": t_iso})
 
 
 
 
187
  return state
188
 
189
- # --- 回答送信(✅ submit時だけログ) ---
190
  def next_step(prev_level, user_answer, question_text, passage_text,
191
  displayed_time, question_number, user_id, passage_id, state):
192
 
193
- if not isinstance(state, dict):
194
- state = _new_session_state()
195
-
196
  if not user_answer:
197
  return (
198
  state,
@@ -200,13 +221,9 @@ def next_step(prev_level, user_answer, question_text, passage_text,
200
  None, "", True, displayed_time, question_number, user_id, passage_id
201
  )
202
 
203
- submit_time = datetime.utcnow() + timedelta(hours=9)
204
  state["question_count"] += 1
205
-
206
  correct = check_answer_with_ai(passage_text, question_text, user_answer)
207
- new_level = adaptive_test(prev_level, correct)
208
 
209
- # ✅ submitログ(ここだけ記録)
210
  log_event(
211
  state,
212
  question_number=state["question_count"],
@@ -218,133 +235,96 @@ def next_step(prev_level, user_answer, question_text, passage_text,
218
  actions=json.dumps(state["action_log"], ensure_ascii=False),
219
  )
220
 
221
- # 最終問題なら結果だけ大きく表示
222
  if state["question_count"] >= 5:
223
- final_level = prev_level if correct else new_level
224
  return (
225
  state,
226
- f"<h1>🎯 Your Reading level: <strong>Level {final_level}</strong></h1>",
227
- "", "", final_level,
228
  None, "", False, "", "", user_id, passage_id
229
  )
230
 
231
- # 次の問題へ
232
- next_passage_id, next_text = get_passage(new_level, state["used_passages"])
233
  state["used_passages"].add(next_passage_id)
234
 
235
  next_question = generate_question(next_text)
236
- next_display_time = datetime.utcnow() + timedelta(hours=9)
237
-
238
- # ✅ 次の問題のために選択変更履歴をリセット
239
  state["action_log"] = []
240
 
241
- feedback = "✅ Correct!" if correct else "❌ Incorrect."
242
-
243
  return (
244
  state,
245
- feedback + "\n➡️ Loading next question…",
246
- next_text, next_question, new_level,
247
- None, "", True, next_display_time.isoformat(), state["question_count"] + 1, user_id, next_passage_id
 
 
 
 
248
  )
249
 
250
- # --- 管理者DL ---
251
  def download_logs(admin_password):
252
- if not ADMIN_PASSWORD:
253
- return None, "⚠️ ADMIN_PASSWORD が設定されていません(環境変数で設定してください)"
254
-
255
- if (admin_password or "") != ADMIN_PASSWORD:
256
  return None, "❌ Password incorrect."
257
 
258
- with _log_lock:
259
- if not os.path.exists(LOG_FILE):
260
- return None, "⚠️ logs.csv がまだ存在しません(ログが1件もありません)"
261
 
262
- tmp_dir = tempfile.mkdtemp()
263
- out_path = os.path.join(tmp_dir, "logs.csv")
264
- with open(LOG_FILE, "rb") as fsrc, open(out_path, "wb") as fdst:
265
- fdst.write(fsrc.read())
266
 
267
  return out_path, "✅ logs.csv is ready."
268
 
269
- # --- Gradio UI ---
270
  with gr.Blocks() as demo:
271
  gr.Markdown("# 📘 Reading Level Test")
272
 
273
  session_state = gr.State(_new_session_state())
274
 
275
- student_id_input = gr.Textbox(label="Student ID", placeholder="例: B123456")
276
  start_btn = gr.Button("▶️ Start Test")
277
 
278
- text_display = gr.Textbox(label="Reading Passage", lines=15, interactive=False)
279
- question_display = gr.Textbox(label="Question", lines=6, interactive=False)
280
- user_answer = gr.Radio(["A", "B", "C", "D"], label="Your Answer")
281
  submit_btn = gr.Button("Submit Answer")
282
 
283
- feedback_display = gr.Markdown()
284
 
285
  hidden_level = gr.Number(visible=False)
286
- hidden_passage = gr.Textbox(visible=False) # 互換用(未使用)
287
- hidden_display_time = gr.Textbox(visible=False)
288
- hidden_question_number = gr.Number(visible=False)
289
- hidden_user_id = gr.Textbox(visible=False)
290
- hidden_passage_id = gr.Textbox(visible=False)
291
- test_visible = gr.State(False)
292
 
293
  start_btn.click(
294
- fn=start_test,
295
- inputs=[student_id_input, session_state],
296
- outputs=[
297
- session_state,
298
- text_display, question_display, hidden_level, hidden_passage_id, user_answer,
299
- feedback_display, test_visible, hidden_display_time,
300
- hidden_question_number, hidden_user_id
301
- ]
302
  )
303
 
304
- # ✅ 選択肢変更はCSVに書かず、actionsにだけ溜める
305
  user_answer.change(
306
- fn=log_choice_change,
307
- inputs=[user_answer, hidden_question_number, hidden_user_id, session_state],
308
- outputs=[session_state]
309
  )
310
 
311
  submit_btn.click(
312
- fn=next_step,
313
- inputs=[hidden_level, user_answer, question_display, text_display,
314
- hidden_display_time, hidden_question_number, hidden_user_id, hidden_passage_id, session_state],
315
- outputs=[
316
- session_state,
317
- feedback_display, text_display, question_display, hidden_level,
318
- user_answer, hidden_passage, test_visible, hidden_display_time,
319
- hidden_question_number, hidden_user_id, hidden_passage_id
320
- ]
321
  )
322
 
323
- def toggle_visibility(show):
324
- v = bool(show)
325
- return (
326
- gr.update(visible=v), gr.update(visible=v),
327
- gr.update(visible=v), gr.update(visible=v)
328
- )
329
-
330
- test_visible.change(
331
- fn=toggle_visibility,
332
- inputs=test_visible,
333
- outputs=[text_display, question_display, user_answer, submit_btn]
334
- )
335
 
336
- gr.Markdown("---")
337
- gr.Markdown("## 🔒 Admin: Download logs (since last restart)")
338
-
339
- admin_pw = gr.Textbox(label="Admin Password", type="password", placeholder="Enter admin password")
340
- dl_btn = gr.Button("⬇️ Download logs.csv")
341
- dl_file = gr.File(label="logs.csv (download)")
342
- dl_msg = gr.Markdown()
343
-
344
- dl_btn.click(
345
- fn=download_logs,
346
- inputs=[admin_pw],
347
- outputs=[dl_file, dl_msg]
348
- )
349
 
350
  demo.launch()
 
12
  BASE_URL = "https://openrouter.ai/api/v1"
13
  LOG_FILE = os.getenv("LOG_FILE", "logs.csv")
14
 
 
15
  ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "0421")
16
 
17
  client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
 
21
 
22
  levels = [1, 2, 3, 4, 5]
23
 
 
24
  _log_lock = threading.Lock()
25
 
26
  # =========================
27
+ # ログ列(固定・指定順)
28
  # =========================
29
  LOG_COLUMNS = [
30
  "user_id",
 
38
  "actions",
39
  ]
40
 
41
+ # =========================
42
+ # 文字化け防止(ASCII正規化)
43
+ # =========================
44
+ def normalize_text_for_csv(text: str) -> str:
45
+ if not text:
46
+ return text
47
+ return (
48
+ text.replace("—", "-")
49
+ .replace("–", "-")
50
+ .replace("’", "'")
51
+ .replace("‘", "'")
52
+ .replace("“", '"')
53
+ .replace("”", '"')
54
+ )
55
+
56
+ # --- セッション state ---
57
  def _new_session_state():
58
  return {
59
  "session_id": str(uuid.uuid4()),
60
  "used_passages": set(),
61
+ "question_count": 0,
62
  "user_id": None,
63
+ "action_log": [],
64
  }
65
 
66
+ # --- CSV追記(固定) ---
67
  def log_to_csv(entry: dict):
68
  with _log_lock:
69
  row = {col: entry.get(col, None) for col in LOG_COLUMNS}
 
80
  )
81
 
82
  def log_event(state: dict, **kwargs):
 
83
  now = (datetime.utcnow() + timedelta(hours=9)).strftime("%Y-%m-%d %H:%M:%S") + " JST"
84
+
85
+ # ✅ 正規化(ここが追加ポイント)
86
+ if "question" in kwargs:
87
+ kwargs["question"] = normalize_text_for_csv(kwargs["question"])
88
+ if "actions" in kwargs and isinstance(kwargs["actions"], str):
89
+ kwargs["actions"] = normalize_text_for_csv(kwargs["actions"])
90
+
91
+ entry = {
92
+ "time": now,
93
+ "user_id": state.get("user_id"),
94
+ **kwargs
95
+ }
96
  log_to_csv(entry)
97
 
98
  # --- AIで問題生成 ---
 
135
  temperature=0,
136
  )
137
  result = response.choices[0].message.content.strip().lower()
 
138
  if re.search(r"\bincorrect\b", result):
139
  return False
140
  if re.search(r"\bcorrect\b", result):
141
  return True
142
  return False
143
 
144
+ # --- 難易度調整 ---
145
  def adaptive_test(prev_level, prev_correct):
146
  idx = levels.index(prev_level)
147
  if prev_correct and idx < len(levels) - 1:
 
160
  text = subset[subset["passage_id"] == passage_id]["text"].iloc[0]
161
  return passage_id, text
162
 
163
+ # --- Start Test ---
164
  def start_test(student_id, state):
165
  state = _new_session_state()
166
 
 
179
  state["used_passages"].add(passage_id)
180
 
181
  question = generate_question(text)
 
182
 
 
183
  log_event(
184
  state,
185
+ question_number=1,
186
+ reading_level=level,
187
+ passage_id=passage_id,
188
+ question=question,
189
  choice=None,
190
  correct=None,
191
  actions="start test pushed",
192
  )
193
 
194
+ displayed_time = datetime.utcnow() + timedelta(hours=9)
195
+
196
  return (
197
  state,
198
  text, question, level, passage_id, None,
199
  "", True, displayed_time.isoformat(), 1, state["user_id"]
200
  )
201
 
202
+ # --- 選択変更(CSVには書かない) ---
203
  def log_choice_change(choice, question_number, user_id, state):
 
 
204
  if choice:
205
  t_iso = (datetime.utcnow() + timedelta(hours=9)).isoformat()
206
+ state["action_log"].append({
207
+ "action": "choice",
208
+ "choice": choice,
209
+ "time": t_iso
210
+ })
211
  return state
212
 
213
+ # --- Submit ---
214
  def next_step(prev_level, user_answer, question_text, passage_text,
215
  displayed_time, question_number, user_id, passage_id, state):
216
 
 
 
 
217
  if not user_answer:
218
  return (
219
  state,
 
221
  None, "", True, displayed_time, question_number, user_id, passage_id
222
  )
223
 
 
224
  state["question_count"] += 1
 
225
  correct = check_answer_with_ai(passage_text, question_text, user_answer)
 
226
 
 
227
  log_event(
228
  state,
229
  question_number=state["question_count"],
 
235
  actions=json.dumps(state["action_log"], ensure_ascii=False),
236
  )
237
 
 
238
  if state["question_count"] >= 5:
 
239
  return (
240
  state,
241
+ f"<h1>🎯 Your Reading level: <strong>Level {prev_level}</strong></h1>",
242
+ "", "", prev_level,
243
  None, "", False, "", "", user_id, passage_id
244
  )
245
 
246
+ next_level = adaptive_test(prev_level, correct)
247
+ next_passage_id, next_text = get_passage(next_level, state["used_passages"])
248
  state["used_passages"].add(next_passage_id)
249
 
250
  next_question = generate_question(next_text)
 
 
 
251
  state["action_log"] = []
252
 
 
 
253
  return (
254
  state,
255
+ "➡️ Loading next question…",
256
+ next_text, next_question, next_level,
257
+ None, "", True,
258
+ (datetime.utcnow() + timedelta(hours=9)).isoformat(),
259
+ state["question_count"] + 1,
260
+ user_id,
261
+ next_passage_id
262
  )
263
 
264
+ # --- Admin download ---
265
  def download_logs(admin_password):
266
+ if admin_password != ADMIN_PASSWORD:
 
 
 
267
  return None, "❌ Password incorrect."
268
 
269
+ if not os.path.exists(LOG_FILE):
270
+ return None, "⚠️ logs.csv が存在しません"
 
271
 
272
+ tmp_dir = tempfile.mkdtemp()
273
+ out_path = os.path.join(tmp_dir, "logs.csv")
274
+ with open(LOG_FILE, "rb") as fsrc, open(out_path, "wb") as fdst:
275
+ fdst.write(fsrc.read())
276
 
277
  return out_path, "✅ logs.csv is ready."
278
 
279
+ # --- UI ---
280
  with gr.Blocks() as demo:
281
  gr.Markdown("# 📘 Reading Level Test")
282
 
283
  session_state = gr.State(_new_session_state())
284
 
285
+ student_id_input = gr.Textbox(label="Student ID")
286
  start_btn = gr.Button("▶️ Start Test")
287
 
288
+ text_display = gr.Textbox(label="Reading Passage", lines=15)
289
+ question_display = gr.Textbox(label="Question", lines=6)
290
+ user_answer = gr.Radio(["A", "B", "C", "D"])
291
  submit_btn = gr.Button("Submit Answer")
292
 
293
+ feedback = gr.Markdown()
294
 
295
  hidden_level = gr.Number(visible=False)
296
+ hidden_time = gr.Textbox(visible=False)
297
+ hidden_qnum = gr.Number(visible=False)
298
+ hidden_uid = gr.Textbox(visible=False)
299
+ hidden_pid = gr.Textbox(visible=False)
 
 
300
 
301
  start_btn.click(
302
+ start_test,
303
+ [student_id_input, session_state],
304
+ [session_state, text_display, question_display, hidden_level, hidden_pid,
305
+ user_answer, feedback, hidden_time, hidden_qnum, hidden_uid]
 
 
 
 
306
  )
307
 
 
308
  user_answer.change(
309
+ log_choice_change,
310
+ [user_answer, hidden_qnum, hidden_uid, session_state],
311
+ [session_state]
312
  )
313
 
314
  submit_btn.click(
315
+ next_step,
316
+ [hidden_level, user_answer, question_display, text_display,
317
+ hidden_time, hidden_qnum, hidden_uid, hidden_pid, session_state],
318
+ [session_state, feedback, text_display, question_display, hidden_level,
319
+ user_answer, hidden_time, hidden_qnum, hidden_uid, hidden_pid]
 
 
 
 
320
  )
321
 
322
+ gr.Markdown("## 🔒 Admin")
323
+ pw = gr.Textbox(type="password")
324
+ dl = gr.Button("Download logs.csv")
325
+ out = gr.File()
326
+ msg = gr.Markdown()
 
 
 
 
 
 
 
327
 
328
+ dl.click(download_logs, [pw], [out, msg])
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
  demo.launch()