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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -226
app.py CHANGED
@@ -1,269 +1,178 @@
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
-
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()
51
 
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}],
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:
75
- return levels[idx + 1]
76
- elif not prev_correct and idx > 0:
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):
94
- df.to_csv(LOG_FILE, mode="a", index=False, header=False)
95
- else:
96
- df.to_csv(LOG_FILE, index=False)
97
-
98
- all_logs = pd.read_csv(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
- "⚠️ 学生番号を入力してからテストを開始してください",
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
- })
143
- return
144
-
145
- # --- 回答送信 ---
146
- def next_step(prev_level, user_answer, question_text, passage_text,
147
- displayed_time, question_number, user_id, passage_id):
148
-
149
- global question_count, used_passages, action_log
150
-
151
- if not user_answer:
152
- return (
153
- "⚠️ Please select an answer!", passage_text, question_text, prev_level,
154
- None, "", True, displayed_time, question_number, user_id, passage_id
155
  )
 
 
156
 
157
- submit_time = datetime.utcnow() + timedelta(hours=9)
158
- question_count += 1
159
-
160
- correct = check_answer_with_ai(passage_text, question_text, user_answer)
161
- new_level = adaptive_test(prev_level, correct)
162
-
163
- # --- ✅ ログ保存 (actionsに選択履歴がすべて入る)
164
- entry = {
165
- "user_id": user_id,
166
- "question_number": question_count,
167
- "lexile_level": prev_level,
168
- "passage_id": passage_id,
169
- "question": question_text,
170
- "user_answer": user_answer,
171
- "correct": correct,
172
- "displayed_time": displayed_time,
173
- "submit_time": submit_time.strftime("%Y-%m-%d %H:%M:%S"),
174
- "actions": json.dumps(action_log, ensure_ascii=False)
175
- }
176
- log_to_csv_and_push(entry)
177
-
178
- # ✅ 最終問題なら結果だけ大きく表示
179
- if question_count >= 5:
180
- return (
181
- f"<h1>🎯 Your Reading level: <strong>{new_level}L</strong></h1>",
182
- "", "", new_level,
183
- None, "", False, "", "", user_id, passage_id
184
- )
185
 
186
- # --- 次の問題へ ---
187
- next_passage_id, next_text = get_passage(new_level)
188
- used_passages.add(next_passage_id)
189
 
190
- next_question = generate_question(next_text)
191
- next_display_time = datetime.utcnow() + timedelta(hours=9)
 
 
192
 
193
- # ✅ 新しい問題ために選択操作ログをリセット
194
- action_log = []
195
 
196
- feedback = "✅ Correct!" if correct else "❌ Incorrect."
 
 
197
 
198
- return (
199
- feedback + "\n➡️ Loading next question…",
200
- next_text, next_question, new_level,
201
- None, "", True, next_display_time.isoformat(), question_count + 1, user_id, next_passage_id
202
- )
203
 
204
- # --- Gradio UI ---
205
- with gr.Blocks() as demo:
206
- gr.Markdown("# 📘 Reading Level Test")
207
 
208
- student_id_input = gr.Textbox(label="Student ID", placeholder="例: B123456")
209
- start_btn = gr.Button("▶️ Start Test")
210
 
211
- text_display = gr.Textbox(label="Reading Passage", lines=8, interactive=False)
212
- question_display = gr.Textbox(label="Question", lines=6, interactive=False)
213
- user_answer = gr.Radio(["A", "B", "C", "D"], label="Your Answer")
214
- submit_btn = gr.Button("Submit Answer")
 
 
 
 
 
 
215
 
216
- feedback_display = gr.Markdown()
217
 
218
- hidden_level = gr.Number(visible=False)
219
- hidden_passage = gr.Textbox(visible=False)
220
- hidden_display_time = gr.Textbox(visible=False)
221
- hidden_question_number = gr.Number(visible=False)
222
- hidden_user_id = gr.Textbox(visible=False)
223
- hidden_passage_id = gr.Textbox(visible=False)
224
- test_visible = gr.State(False)
225
 
226
- start_btn.click(
227
- fn=start_test,
228
- inputs=[student_id_input],
229
- outputs=[
230
- text_display, question_display, hidden_level, hidden_passage_id, user_answer,
231
- feedback_display, test_visible, hidden_display_time,
232
- hidden_question_number, hidden_user_id
233
- ]
234
- )
235
 
236
- # ✅ 選択肢変更ログ
237
- user_answer.change(
238
- fn=log_choice_change,
239
- inputs=[user_answer, hidden_question_number, hidden_user_id],
240
- outputs=[]
241
- )
242
 
243
- # 回答送信
244
- submit_btn.click(
245
- fn=next_step,
246
- inputs=[hidden_level, user_answer, question_display, text_display,
247
- hidden_display_time, hidden_question_number, hidden_user_id, hidden_passage_id],
248
- outputs=[
249
- feedback_display, text_display, question_display, hidden_level,
250
- user_answer, hidden_passage, test_visible, hidden_display_time,
251
- hidden_question_number, hidden_user_id, hidden_passage_id
252
- ]
253
- )
254
 
255
- # 表示のON/OFF制御
256
- def toggle_visibility(show):
257
- v = bool(show)
258
- return (
259
- gr.update(visible=v), gr.update(visible=v),
260
- gr.update(visible=v), gr.update(visible=v)
261
- )
262
 
263
- test_visible.change(
264
- fn=toggle_visibility,
265
- inputs=test_visible,
266
- outputs=[text_display, question_display, user_answer, submit_btn]
267
  )
268
 
269
- demo.launch()
 
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()