Toya0421 commited on
Commit
d5de4d3
·
verified ·
1 Parent(s): 8022600

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +225 -135
app.py CHANGED
@@ -1,178 +1,268 @@
1
  import gradio as gr
2
  from openai import OpenAI
3
- from datasets import load_dataset, Dataset
4
- from datetime import datetime
5
  import pandas as pd
6
- import time, os, uuid, tempfile
7
 
 
8
  API_KEY = os.getenv("API_KEY")
9
  BASE_URL = "https://openrouter.ai/api/v1"
10
  HF_TOKEN = os.getenv("HF_TOKEN")
 
 
11
 
12
- DATASET_REPO = "Toya0421/reading_exercise_logging"
13
 
14
- client = OpenAI(
15
- base_url=BASE_URL,
16
- api_key=API_KEY
17
- )
18
 
19
- # CSV読み込み(lexile_level level)
20
- def load_passages():
21
- df = pd.read_csv("passage.csv")
22
- df["id"] = df["id"].astype(str)
23
- df["level"] = df["level"].astype(int) # ★修正
24
- df["passage"] = df["passage"].astype(str)
25
- return df
26
 
27
- passages_df = load_passages()
 
 
 
 
28
 
29
-
30
- # ---------------------------------
31
- # ★ levelを決定するロジック(学生番号から自動で決定)
32
- # ---------------------------------
33
- def decide_level(student_id: str):
34
- """
35
- 学生番号の末尾を使って 1〜5 に自動マッピング
36
- """
37
- try:
38
- last_digit = int(student_id[-1])
39
- except:
40
- return 1 # 不正な値のときはレベル1
41
-
42
- return last_digit // 2 + 1 # 0-1→1, 2-3→2, ..., 8-9→5
43
-
44
-
45
- # Passage 抽出
46
- def get_random_passage(level):
47
- target_df = passages_df[passages_df["level"] == level]
48
- if len(target_df) == 0:
49
- return None
50
- row = target_df.sample(1).iloc[0]
51
- return row["id"], row["passage"]
52
-
53
-
54
- def generate_questions(passage_text):
55
  prompt = f"""
56
- You are an English reading comprehension question generator.
57
- Create exactly 3 questions for the following passage.
58
-
59
- Return JSON in this format:
60
- {{
61
- "q1": "...",
62
- "q2": "...",
63
- "q3": "..."
64
- }}
65
-
66
- Passage:
67
- {passage_text}
68
- """
69
  response = client.chat.completions.create(
70
- model="gpt-4o-mini",
71
- messages=[{"role": "user", "content": prompt}]
 
 
72
  )
73
- return response.choices[0].message["content"]
74
 
75
-
76
- def generate_answers(passage_text, questions_json):
77
  prompt = f"""
78
- You are an AI that answers questions based on the passage.
79
-
80
- Return JSON in this format:
81
- {{
82
- "a1": "...",
83
- "a2": "...",
84
- "a3": "..."
85
- }}
86
-
87
- Passage:
88
- {passage_text}
89
-
90
- Questions:
91
- {questions_json}
92
- """
93
  response = client.chat.completions.create(
94
- model="gpt-4o-mini",
95
- messages=[{"role": "user", "content": prompt}]
 
 
96
  )
97
- return response.choices[0].message["content"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
 
99
 
100
- def log_to_huggingface(data):
101
- tmp_path = f"/tmp/{uuid.uuid4()}.json"
102
- ds = Dataset.from_pandas(pd.DataFrame([data]))
103
- ds.to_json(tmp_path)
104
 
105
- commit_message = f"Add log {datetime.utcnow().isoformat()}"
 
106
 
107
- try:
108
- from huggingface_hub import upload_file
 
 
109
 
110
- upload_file(
111
- path_or_fileobj=tmp_path,
112
- path_in_repo=f"logs/{data['log_id']}.json",
113
- repo_id=DATASET_REPO,
114
- repo_type="dataset",
115
- token=HF_TOKEN,
116
- commit_message=commit_message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  )
118
- except Exception as e:
119
- return f"Error uploading: {str(e)}"
120
-
121
- return "Log saved."
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
- # ---------------------------------
125
- # student_id だけ受け取り、内部で level を決定
126
- # ---------------------------------
127
- def run_reading_quiz(student_id):
128
 
129
- # ★ここが最大の変更点
130
- level = decide_level(student_id)
131
 
132
- result = get_random_passage(level)
133
- if result is None:
134
- return "指定されたレベルの文章がありません", "", ""
135
 
136
- pid, passage = result
137
 
138
- questions = generate_questions(passage)
 
 
 
 
139
 
140
- answers = generate_answers(passage, questions)
 
 
141
 
142
- log_data = {
143
- "log_id": str(uuid.uuid4()),
144
- "timestamp": datetime.utcnow().isoformat(),
145
- "student_id": student_id,
146
- "level": level, # ★levelをログに保存
147
- "passage_id": pid,
148
- "passage": passage,
149
- "questions": questions,
150
- "answers": answers,
151
- }
152
 
153
- status = log_to_huggingface(log_data)
 
 
 
154
 
155
- return passage, questions, answers
156
 
 
 
 
 
 
 
 
157
 
158
- # ---------------------------------
159
- # ★ UI(level は削除して student_id のみ)
160
- # ---------------------------------
161
- with gr.Blocks() as app:
162
- gr.Markdown("## 英語リーディング練習")
 
 
 
 
163
 
164
- student_id_input = gr.Textbox(label="学生番号を入力")
 
 
 
 
 
165
 
166
- btn = gr.Button("問題を生成")
 
 
 
 
 
 
 
 
 
 
167
 
168
- passage_out = gr.Textbox(label="Passage", lines=6)
169
- question_out = gr.Textbox(label="Questions (JSON)")
170
- answer_out = gr.Textbox(label="Answers (JSON)")
 
 
 
 
171
 
172
- btn.click(
173
- run_reading_quiz,
174
- inputs=[student_id_input],
175
- outputs=[passage_out, question_out, answer_out]
176
  )
177
 
178
- app.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, uuid, tempfile, json
7
 
8
+ # --- API設定 ---
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/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
+ Passage:
41
+ {text}
42
+ """
 
 
43
  response = client.chat.completions.create(
44
+ model="google/gemma-3-27b-it:free",
45
+ messages=[{"role": "user", "content": prompt}],
46
+ max_tokens=250,
47
+ temperature=0.7,
48
  )
49
+ return response.choices[0].message.content.strip()
50
 
51
+ # --- 正誤判定 ---
52
+ def check_answer_with_ai(text, question, user_answer):
53
  prompt = f"""
54
+ Read the passage and question below. Decide if the user's answer is correct.
55
+ Passage:
56
+ {text}
57
+ Question:
58
+ {question}
59
+ User Answer: {user_answer}
60
+ Respond with one word: "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=10,
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:
74
+ return levels[idx + 1]
75
+ elif not prev_correct and idx > 0:
76
+ return levels[idx - 1]
77
+ return prev_level
78
+
79
+ # --- passage取得 ---
80
+ def get_passage(level):
81
+ subset = passages_df[passages_df["lexile_level"] == level]
82
+ available = [pid for pid in subset["passage_id"] if pid not in used_passages]
83
+ if not available:
84
+ available = list(subset["passage_id"])
85
+ passage_id = random.choice(available)
86
+ text = subset[subset["passage_id"] == passage_id]["text"].iloc[0]
87
+ return passage_id, text
88
+
89
+ # --- ログ追記 & Push ---
90
+ def log_to_csv_and_push(entry):
91
+ df = pd.DataFrame([entry])
92
+ if os.path.exists(LOG_FILE):
93
+ df.to_csv(LOG_FILE, mode="a", index=False, header=False)
94
+ else:
95
+ df.to_csv(LOG_FILE, index=False)
96
+
97
+ all_logs = pd.read_csv(LOG_FILE)
98
+ tmp_dir = tempfile.mkdtemp()
99
+ tmp_path = os.path.join(tmp_dir, "data.parquet")
100
+ all_logs.to_parquet(tmp_path)
101
+
102
+ dataset = Dataset.from_parquet(tmp_path)
103
+ dataset.push_to_hub(DATASET_REPO, token=HF_TOKEN)
104
+
105
+ # --- 開始ボタン動作 ---
106
+ def start_test(student_id):
107
+ global used_passages, question_count, current_user_id, action_log
108
+ used_passages = set()
109
+ question_count = 0
110
+ action_log = []
111
+
112
+ if not student_id or student_id.strip() == "":
113
+ return (
114
+ "", "", 0, "", "",
115
+ "⚠️ 学生番号を入力してからテストを開始してください",
116
+ False, "", "", ""
117
+ )
118
 
119
+ current_user_id = student_id.strip()
120
 
121
+ level = 850
122
+ passage_id, text = get_passage(level)
123
+ used_passages.add(passage_id)
 
124
 
125
+ question = generate_question(text)
126
+ displayed_time = datetime.utcnow() + timedelta(hours=9)
127
 
128
+ return (
129
+ text, question, level, passage_id, "",
130
+ "", True, displayed_time.isoformat(), 1, current_user_id
131
+ )
132
 
133
+ # --- 選択肢変更イベント(ここで操作履歴を保存) ---
134
+ def log_choice_change(choice, question_number, user_id):
135
+ global action_log
136
+ if choice:
137
+ action_log.append({
138
+ "action": "choice",
139
+ "choice": choice,
140
+ "time": (datetime.utcnow() + timedelta(hours=9)).isoformat()
141
+ })
142
+ return
143
+
144
+ # --- 回答送信 ---
145
+ def next_step(prev_level, user_answer, question_text, passage_text,
146
+ displayed_time, question_number, user_id, passage_id):
147
+
148
+ global question_count, used_passages, action_log
149
+
150
+ if not user_answer:
151
+ return (
152
+ "⚠️ Please select an answer!", passage_text, question_text, prev_level,
153
+ None, "", True, displayed_time, question_number, user_id, passage_id
154
  )
 
 
 
 
155
 
156
+ submit_time = datetime.utcnow() + timedelta(hours=9)
157
+ question_count += 1
158
+
159
+ correct = check_answer_with_ai(passage_text, question_text, user_answer)
160
+ new_level = adaptive_test(prev_level, correct)
161
+
162
+ # --- ✅ ログ保存 (actionsに選択履歴がすべて入る)
163
+ entry = {
164
+ "user_id": user_id,
165
+ "question_number": question_count,
166
+ "lexile_level": prev_level,
167
+ "passage_id": passage_id,
168
+ "question": question_text,
169
+ "user_answer": user_answer,
170
+ "correct": correct,
171
+ "displayed_time": displayed_time,
172
+ "submit_time": submit_time.strftime("%Y-%m-%d %H:%M:%S"),
173
+ "actions": json.dumps(action_log, ensure_ascii=False)
174
+ }
175
+ log_to_csv_and_push(entry)
176
+
177
+ # ✅ 最終問題なら結果だけ大きく表示
178
+ if question_count >= 5:
179
+ return (
180
+ f"<h1>🎯 Your Reading level: <strong>{new_level}L</strong></h1>",
181
+ "", "", new_level,
182
+ None, "", False, "", "", user_id, passage_id
183
+ )
184
 
185
+ # --- 次の問題へ ---
186
+ next_passage_id, next_text = get_passage(new_level)
187
+ used_passages.add(next_passage_id)
 
188
 
189
+ next_question = generate_question(next_text)
190
+ next_display_time = datetime.utcnow() + timedelta(hours=9)
191
 
192
+ # 新しい問題のために選択操作ログをリセット
193
+ action_log = []
 
194
 
195
+ feedback = "✅ Correct!" if correct else "❌ Incorrect."
196
 
197
+ return (
198
+ feedback + "\n➡️ Loading next question…",
199
+ next_text, next_question, new_level,
200
+ None, "", True, next_display_time.isoformat(), question_count + 1, user_id, next_passage_id
201
+ )
202
 
203
+ # --- Gradio UI ---
204
+ with gr.Blocks() as demo:
205
+ gr.Markdown("# 📘 Reading Level Test")
206
 
207
+ student_id_input = gr.Textbox(label="Student ID", placeholder="例: B123456")
208
+ start_btn = gr.Button("▶️ Start Test")
 
 
 
 
 
 
 
 
209
 
210
+ text_display = gr.Textbox(label="Reading Passage", lines=8, interactive=False)
211
+ question_display = gr.Textbox(label="Question", lines=6, interactive=False)
212
+ user_answer = gr.Radio(["A", "B", "C", "D"], label="Your Answer")
213
+ submit_btn = gr.Button("Submit Answer")
214
 
215
+ feedback_display = gr.Markdown()
216
 
217
+ hidden_level = gr.Number(visible=False)
218
+ hidden_passage = gr.Textbox(visible=False)
219
+ hidden_display_time = gr.Textbox(visible=False)
220
+ hidden_question_number = gr.Number(visible=False)
221
+ hidden_user_id = gr.Textbox(visible=False)
222
+ hidden_passage_id = gr.Textbox(visible=False)
223
+ test_visible = gr.State(False)
224
 
225
+ start_btn.click(
226
+ fn=start_test,
227
+ inputs=[student_id_input],
228
+ outputs=[
229
+ text_display, question_display, hidden_level, hidden_passage_id, user_answer,
230
+ feedback_display, test_visible, hidden_display_time,
231
+ hidden_question_number, hidden_user_id
232
+ ]
233
+ )
234
 
235
+ # 選択肢変更ログ
236
+ user_answer.change(
237
+ fn=log_choice_change,
238
+ inputs=[user_answer, hidden_question_number, hidden_user_id],
239
+ outputs=[]
240
+ )
241
 
242
+ # 回答送信
243
+ submit_btn.click(
244
+ fn=next_step,
245
+ inputs=[hidden_level, user_answer, question_display, text_display,
246
+ hidden_display_time, hidden_question_number, hidden_user_id, hidden_passage_id],
247
+ outputs=[
248
+ feedback_display, text_display, question_display, hidden_level,
249
+ user_answer, hidden_passage, test_visible, hidden_display_time,
250
+ hidden_question_number, hidden_user_id, hidden_passage_id
251
+ ]
252
+ )
253
 
254
+ # 表示のON/OFF制御
255
+ def toggle_visibility(show):
256
+ v = bool(show)
257
+ return (
258
+ gr.update(visible=v), gr.update(visible=v),
259
+ gr.update(visible=v), gr.update(visible=v)
260
+ )
261
 
262
+ test_visible.change(
263
+ fn=toggle_visibility,
264
+ inputs=test_visible,
265
+ outputs=[text_display, question_display, user_answer, submit_btn]
266
  )
267
 
268
+ demo.launch()