Toya0421 commited on
Commit
e93fdee
·
verified ·
1 Parent(s): e26d9a1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -71
app.py CHANGED
@@ -10,42 +10,41 @@ 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/lexile_test_logging"
13
-
14
- # --- ログファイル ---
15
  LOG_FILE = "logs.csv"
16
 
17
- # --- passage本文は外部ファイルから読み込み ---
18
- PASSAGE_FILE = "passage.csv"
19
- passages_df = pd.read_csv(PASSAGE_FILE)
20
 
21
- levels = [300, 600, 850, 1050, 1250]
 
 
22
 
23
- client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
24
 
25
- # --- passage取得 ---
26
- def get_passage(level):
27
- candidates = passages_df[passages_df["lexile_level"] == level]
28
- row = candidates.sample(1).iloc[0]
29
- return row["passage_id"], row["text"]
30
 
31
- # --- 問題生成 ---
32
  def generate_question(text):
33
  prompt = f"""
34
  Read the following passage and create ONE multiple-choice question with 4 options (A–D).
35
- Only output the question and options — DO NOT include which one is correct.
36
- Format strictly as:
37
  Q: <question text>
38
  A. <option>
39
  B. <option>
40
  C. <option>
41
  D. <option>
 
42
  Passage:
43
  {text}
44
  """
45
  response = client.chat.completions.create(
46
  model="google/gemma-3-27b-it:free",
47
  messages=[{"role": "user", "content": prompt}],
48
- max_tokens=300,
49
  temperature=0.7,
50
  )
51
  return response.choices[0].message.content.strip()
@@ -54,20 +53,22 @@ def generate_question(text):
54
  def check_answer_with_ai(text, question, user_answer):
55
  prompt = f"""
56
  Read the passage and question below. Decide if the user's answer is correct.
57
- P: {text}
58
- Q: {question}
59
- User: {user_answer}
60
- Respond only: Correct or Incorrect
 
 
61
  """
62
  response = client.chat.completions.create(
63
  model="google/gemma-3-27b-it:free",
64
  messages=[{"role": "user", "content": prompt}],
65
- max_tokens=15,
66
  temperature=0,
67
  )
68
  return response.choices[0].message.content.strip().lower() == "correct"
69
 
70
- # --- 難易度調整 ---
71
  def adaptive_test(prev_level, prev_correct):
72
  idx = levels.index(prev_level)
73
  if prev_correct and idx < len(levels) - 1:
@@ -76,7 +77,17 @@ def adaptive_test(prev_level, prev_correct):
76
  return levels[idx - 1]
77
  return prev_level
78
 
79
- # --- ログ書き込み(回答送信時のみ) ---
 
 
 
 
 
 
 
 
 
 
80
  def log_to_csv_and_push(entry):
81
  df = pd.DataFrame([entry])
82
  if os.path.exists(LOG_FILE):
@@ -88,49 +99,70 @@ def log_to_csv_and_push(entry):
88
  tmp_dir = tempfile.mkdtemp()
89
  tmp_path = os.path.join(tmp_dir, "data.parquet")
90
  all_logs.to_parquet(tmp_path)
 
91
  dataset = Dataset.from_parquet(tmp_path)
92
  dataset.push_to_hub(DATASET_REPO, token=HF_TOKEN)
93
 
94
- # --- 状態 ---
95
- used_passages = set()
96
- question_count = 0
97
- MAX_QUESTIONS = 5
98
- current_user_id = None
99
- action_log = []
100
-
101
- # --- テスト開始 ---
102
- def start_test(user_input_id):
103
  global used_passages, question_count, current_user_id, action_log
104
  used_passages = set()
105
  question_count = 0
106
  action_log = []
107
 
108
- # ユーザーIDが空 uuid割当
109
- current_user_id = user_input_id.strip() if user_input_id else str(uuid.uuid4())
 
 
 
 
 
 
110
 
111
  level = 850
112
  passage_id, text = get_passage(level)
113
  used_passages.add(passage_id)
114
 
115
  question = generate_question(text)
116
-
117
  displayed_time = datetime.utcnow() + timedelta(hours=9)
118
- action_log.append({"action": "displayed", "time": displayed_time.isoformat(), "question_number": 1})
119
 
120
- return text, question, level, passage_id, "", "", True, displayed_time.isoformat(), current_user_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  # --- 回答送信 ---
123
- def next_step(prev_level, user_answer, question_text, passage_text, displayed_time, user_id, passage_id):
 
 
124
  global question_count, used_passages, action_log
125
 
126
  if not user_answer:
127
- return "⚠️ Please select an answer!", passage_text, question_text, prev_level, None, "", True, displayed_time, user_id, passage_id
 
 
 
128
 
129
- question_count += 1
130
  submit_time = datetime.utcnow() + timedelta(hours=9)
 
 
131
  correct = check_answer_with_ai(passage_text, question_text, user_answer)
 
132
 
133
- # ログ書き込み(回答送信時のみ)
134
  entry = {
135
  "user_id": user_id,
136
  "question_number": question_count,
@@ -141,65 +173,99 @@ def next_step(prev_level, user_answer, question_text, passage_text, displayed_ti
141
  "correct": correct,
142
  "displayed_time": displayed_time,
143
  "submit_time": submit_time.strftime("%Y-%m-%d %H:%M:%S"),
144
- "actions": json.dumps(action_log, ensure_ascii=False),
145
  }
146
  log_to_csv_and_push(entry)
147
 
148
- new_level = adaptive_test(prev_level, correct)
149
- feedback = "✅ Correct!" if correct else "❌ Incorrect."
150
-
151
- # 終了
152
- if question_count >= MAX_QUESTIONS:
153
- return f"🎯 Test finished!\nYour reading level: **{new_level}L**", "", "", new_level, None, "", False, "", user_id, passage_id
154
-
155
- # 次の問題へ
156
- candidates = passages_df[passages_df["lexile_level"] == new_level]
157
- available = [pid for pid in candidates["passage_id"] if pid not in used_passages]
158
- if not available:
159
- available = list(candidates["passage_id"])
160
 
161
- next_passage_id = random.choice(available)
162
- next_text = passages_df[passages_df["passage_id"] == next_passage_id]["text"].iloc[0]
163
  used_passages.add(next_passage_id)
164
 
165
  next_question = generate_question(next_text)
166
  next_display_time = datetime.utcnow() + timedelta(hours=9)
167
 
168
- action_log = [{"action": "displayed", "time": next_display_time.isoformat(), "question_number": question_count+1}]
 
 
 
169
 
170
- return feedback + "\n➡️ Loading next question…", next_text, next_question, new_level, None, "", True, next_display_time.isoformat(), user_id, next_passage_id
 
 
 
 
171
 
172
  # --- Gradio UI ---
173
  with gr.Blocks() as demo:
174
- gr.Markdown("# 📘 Lexile Adaptive Reading Test")
175
 
176
- user_input_id = gr.Textbox(label="Enter your Student ID", placeholder="e.g. 2025012")
177
  start_btn = gr.Button("▶️ Start Test")
178
 
179
- text_display = gr.Textbox(label="Reading Passage", lines=6, interactive=False)
180
- question_display = gr.Textbox(label="Question", lines=7, interactive=False)
181
- answer_box = gr.Radio(choices=["A", "B", "C", "D"], label="Select Answer")
182
  submit_btn = gr.Button("Submit Answer")
183
 
184
  feedback_display = gr.Markdown()
 
185
  hidden_level = gr.Number(visible=False)
186
- hidden_passage_id = gr.Textbox(visible=False)
187
- test_visible = gr.State(True)
188
  hidden_display_time = gr.Textbox(visible=False)
 
189
  hidden_user_id = gr.Textbox(visible=False)
 
 
190
 
191
  start_btn.click(
192
  fn=start_test,
193
- inputs=[user_input_id],
194
- outputs=[text_display, question_display, hidden_level, hidden_passage_id, answer_box,
195
- feedback_display, test_visible, hidden_display_time, hidden_user_id]
 
 
 
 
 
 
 
 
 
 
196
  )
197
 
 
198
  submit_btn.click(
199
  fn=next_step,
200
- inputs=[hidden_level, answer_box, question_display, text_display, hidden_display_time, hidden_user_id, hidden_passage_id],
201
- outputs=[feedback_display, text_display, question_display, hidden_level, answer_box,
202
- hidden_passage_id, test_visible, hidden_display_time, hidden_user_id, hidden_passage_id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  )
204
 
205
  demo.launch()
 
10
  BASE_URL = "https://openrouter.ai/api/v1"
11
  HF_TOKEN = os.getenv("HF_TOKEN")
12
  DATASET_REPO = "Toya0421/lexile_test_logging"
 
 
13
  LOG_FILE = "logs.csv"
14
 
15
+ client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
 
 
16
 
17
+ # --- 外部CSVとして管理する passsage_id / passage データ ---
18
+ # (例)columns: passage_id, lexile_level, text
19
+ passages_df = pd.read_csv("passage.csv")
20
 
21
+ levels = [300, 600, 850, 1050, 1250]
22
 
23
+ # --- 状態 ---
24
+ used_passages = set()
25
+ question_count = 0
26
+ current_user_id = None
27
+ action_log = [] # ✅ 選択肢変更ログ
28
 
29
+ # --- AIで問題生成 ---
30
  def generate_question(text):
31
  prompt = f"""
32
  Read the following passage and create ONE multiple-choice question with 4 options (A–D).
33
+ Only output the question and options.
34
+ Format:
35
  Q: <question text>
36
  A. <option>
37
  B. <option>
38
  C. <option>
39
  D. <option>
40
+
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}],
47
+ max_tokens=250,
48
  temperature=0.7,
49
  )
50
  return response.choices[0].message.content.strip()
 
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}],
66
+ max_tokens=10,
67
  temperature=0,
68
  )
69
  return response.choices[0].message.content.strip().lower() == "correct"
70
 
71
+ # --- 自動難易度調整 ---
72
  def adaptive_test(prev_level, prev_correct):
73
  idx = levels.index(prev_level)
74
  if prev_correct and idx < len(levels) - 1:
 
77
  return levels[idx - 1]
78
  return prev_level
79
 
80
+ # --- passage取得 ---
81
+ def get_passage(level):
82
+ subset = passages_df[passages_df["lexile_level"] == level]
83
+ available = [pid for pid in subset["passage_id"] if pid not in used_passages]
84
+ if not available:
85
+ available = list(subset["passage_id"])
86
+ passage_id = random.choice(available)
87
+ text = subset[subset["passage_id"] == passage_id]["text"].iloc[0]
88
+ return passage_id, text
89
+
90
+ # --- ログ追記 & Push ---
91
  def log_to_csv_and_push(entry):
92
  df = pd.DataFrame([entry])
93
  if os.path.exists(LOG_FILE):
 
99
  tmp_dir = tempfile.mkdtemp()
100
  tmp_path = os.path.join(tmp_dir, "data.parquet")
101
  all_logs.to_parquet(tmp_path)
102
+
103
  dataset = Dataset.from_parquet(tmp_path)
104
  dataset.push_to_hub(DATASET_REPO, token=HF_TOKEN)
105
 
106
+ # --- 開始ボタン動作 ---
107
+ def start_test(student_id):
 
 
 
 
 
 
 
108
  global used_passages, question_count, current_user_id, action_log
109
  used_passages = set()
110
  question_count = 0
111
  action_log = []
112
 
113
+ if not student_id or student_id.strip() == "":
114
+ return (
115
+ "", "", 0, "", "",
116
+ "⚠️ Student ID must be entered before starting.",
117
+ False, "", "", ""
118
+ )
119
+
120
+ current_user_id = student_id.strip()
121
 
122
  level = 850
123
  passage_id, text = get_passage(level)
124
  used_passages.add(passage_id)
125
 
126
  question = generate_question(text)
 
127
  displayed_time = datetime.utcnow() + timedelta(hours=9)
 
128
 
129
+ return (
130
+ text, question, level, passage_id, "",
131
+ "", True, displayed_time.isoformat(), 1, current_user_id
132
+ )
133
+
134
+ # --- 選択肢変更イベント(ここで操作履歴を保存) ---
135
+ def log_choice_change(choice, question_number, user_id):
136
+ global action_log
137
+ if choice:
138
+ action_log.append({
139
+ "action": "choice",
140
+ "choice": choice,
141
+ "time": (datetime.utcnow() + timedelta(hours=9)).isoformat(),
142
+ "question_number": question_number,
143
+ "user_id": user_id
144
+ })
145
+ return
146
 
147
  # --- 回答送信 ---
148
+ def next_step(prev_level, user_answer, question_text, passage_text,
149
+ displayed_time, question_number, user_id, passage_id):
150
+
151
  global question_count, used_passages, action_log
152
 
153
  if not user_answer:
154
+ return (
155
+ "⚠️ Please select an answer!", passage_text, question_text, prev_level,
156
+ None, "", True, displayed_time, question_number, user_id, passage_id
157
+ )
158
 
 
159
  submit_time = datetime.utcnow() + timedelta(hours=9)
160
+ question_count += 1
161
+
162
  correct = check_answer_with_ai(passage_text, question_text, user_answer)
163
+ new_level = adaptive_test(prev_level, correct)
164
 
165
+ # --- ✅ ログ保存 (actionsに選択履歴がすべて入る)
166
  entry = {
167
  "user_id": user_id,
168
  "question_number": question_count,
 
173
  "correct": correct,
174
  "displayed_time": displayed_time,
175
  "submit_time": submit_time.strftime("%Y-%m-%d %H:%M:%S"),
176
+ "actions": json.dumps(action_log, ensure_ascii=False)
177
  }
178
  log_to_csv_and_push(entry)
179
 
180
+ # 最終問題なら結果だけ大きく表示
181
+ if question_count >= 5:
182
+ return (
183
+ f"<h1>🎯 Your Lexile Score: <strong>{new_level}L</strong></h1>",
184
+ "", "", new_level,
185
+ None, "", False, "", "", user_id, passage_id
186
+ )
 
 
 
 
 
187
 
188
+ # --- 次の問題へ ---
189
+ next_passage_id, next_text = get_passage(new_level)
190
  used_passages.add(next_passage_id)
191
 
192
  next_question = generate_question(next_text)
193
  next_display_time = datetime.utcnow() + timedelta(hours=9)
194
 
195
+ # 新しい問題のために選択操作ログをリセット
196
+ action_log = []
197
+
198
+ feedback = "✅ Correct!" if correct else "❌ Incorrect."
199
 
200
+ return (
201
+ feedback + "\n➡️ Loading next question…",
202
+ next_text, next_question, new_level,
203
+ None, "", True, next_display_time.isoformat(), question_count + 1, user_id, next_passage_id
204
+ )
205
 
206
  # --- Gradio UI ---
207
  with gr.Blocks() as demo:
208
+ gr.Markdown("# 📘 Lexile Level Test")
209
 
210
+ student_id_input = gr.Textbox(label="Student ID", placeholder="e.g. S123456")
211
  start_btn = gr.Button("▶️ Start Test")
212
 
213
+ text_display = gr.Textbox(label="Reading Passage", lines=8, interactive=False)
214
+ question_display = gr.Textbox(label="Question", lines=6, interactive=False)
215
+ user_answer = gr.Radio(["A", "B", "C", "D"], label="Your Answer")
216
  submit_btn = gr.Button("Submit Answer")
217
 
218
  feedback_display = gr.Markdown()
219
+
220
  hidden_level = gr.Number(visible=False)
221
+ hidden_passage = gr.Textbox(visible=False)
 
222
  hidden_display_time = gr.Textbox(visible=False)
223
+ hidden_question_number = gr.Number(visible=False)
224
  hidden_user_id = gr.Textbox(visible=False)
225
+ hidden_passage_id = gr.Textbox(visible=False)
226
+ test_visible = gr.State(False)
227
 
228
  start_btn.click(
229
  fn=start_test,
230
+ inputs=[student_id_input],
231
+ outputs=[
232
+ text_display, question_display, hidden_level, hidden_passage_id, user_answer,
233
+ feedback_display, test_visible, hidden_display_time,
234
+ hidden_question_number, hidden_user_id
235
+ ]
236
+ )
237
+
238
+ # ✅ 選択肢変更ログ
239
+ user_answer.change(
240
+ fn=log_choice_change,
241
+ inputs=[user_answer, hidden_question_number, hidden_user_id],
242
+ outputs=[]
243
  )
244
 
245
+ # ✅ 回答送信
246
  submit_btn.click(
247
  fn=next_step,
248
+ inputs=[hidden_level, user_answer, question_display, text_display,
249
+ hidden_display_time, hidden_question_number, hidden_user_id, hidden_passage_id],
250
+ outputs=[
251
+ feedback_display, text_display, question_display, hidden_level,
252
+ user_answer, hidden_passage, test_visible, hidden_display_time,
253
+ hidden_question_number, hidden_user_id, hidden_passage_id
254
+ ]
255
+ )
256
+
257
+ # ✅ 表示のON/OFF制御
258
+ def toggle_visibility(show):
259
+ v = bool(show)
260
+ return (
261
+ gr.update(visible=v), gr.update(visible=v),
262
+ gr.update(visible=v), gr.update(visible=v)
263
+ )
264
+
265
+ test_visible.change(
266
+ fn=toggle_visibility,
267
+ inputs=test_visible,
268
+ outputs=[text_display, question_display, user_answer, submit_btn]
269
  )
270
 
271
  demo.launch()