Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -138,8 +138,6 @@ def get_new_passage_random(used_passages_set, target_level):
|
|
| 138 |
if num.isdigit():
|
| 139 |
all_ids.append(int(num))
|
| 140 |
|
| 141 |
-
# ★追加:excelのflesch_scoreでフィルタ(target_fleschより低いものだけ)
|
| 142 |
-
# passage_information.xlsx に該当行がないものは除外(スコア不明だと条件判定できないため)
|
| 143 |
eligible_ids = []
|
| 144 |
for pid in all_ids:
|
| 145 |
row = passage_info_df[passage_info_df["Text#"] == pid]
|
|
@@ -187,16 +185,11 @@ def get_new_passage_random(used_passages_set, target_level):
|
|
| 187 |
# ======================================================
|
| 188 |
|
| 189 |
def extract_main_body(text: str) -> str:
|
| 190 |
-
"""
|
| 191 |
-
書き換えは行わず、「本文っぽい部分」だけを軽量なヒューリスティックで抽出する。
|
| 192 |
-
(タイトル/著者/URL/出典/著作権/Project Gutenbergヘッダ・フッタ/注釈などを落とす)
|
| 193 |
-
"""
|
| 194 |
if not text:
|
| 195 |
return ""
|
| 196 |
|
| 197 |
lines = text.splitlines()
|
| 198 |
|
| 199 |
-
# ありがちなメタ情報/フッタ/ヘッダの除外パターン
|
| 200 |
drop_patterns = [
|
| 201 |
r"^\s*title\s*:\s*.*$",
|
| 202 |
r"^\s*author\s*:\s*.*$",
|
|
@@ -208,21 +201,19 @@ def extract_main_body(text: str) -> str:
|
|
| 208 |
r".*GUTENBERG.*",
|
| 209 |
r"^\s*http[s]?://\S+.*$",
|
| 210 |
r"^\s*www\.\S+.*$",
|
| 211 |
-
r"^\s*\[.*\]\s*$",
|
| 212 |
-
r"^\s*\(\s*.*\s*\)\s*$",
|
| 213 |
r"^\s*end\s+of\s+.*$",
|
| 214 |
r"^\s*\*{3}.*\*{3}\s*$",
|
| 215 |
]
|
| 216 |
drop_re = re.compile("|".join(f"(?:{p})" for p in drop_patterns), re.IGNORECASE)
|
| 217 |
|
| 218 |
-
# まず明らかなメタ行を除外
|
| 219 |
kept = []
|
| 220 |
for ln in lines:
|
| 221 |
if drop_re.match(ln.strip()):
|
| 222 |
continue
|
| 223 |
kept.append(ln)
|
| 224 |
|
| 225 |
-
# 先頭の「短いタイトル行」っぽいものを数行だけ落とす(本文が始まるまで)
|
| 226 |
def is_title_like(s: str) -> bool:
|
| 227 |
t = s.strip()
|
| 228 |
if not t:
|
|
@@ -262,14 +253,7 @@ def extract_main_body(text: str) -> str:
|
|
| 262 |
# ======================================================
|
| 263 |
|
| 264 |
def rewrite_level(text, target_level):
|
| 265 |
-
level_to_flesch = {
|
| 266 |
-
1: 90,
|
| 267 |
-
2: 75,
|
| 268 |
-
3: 65,
|
| 269 |
-
4: 55,
|
| 270 |
-
5: 40
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
target_flesch = level_to_flesch[int(target_level)]
|
| 274 |
|
| 275 |
prompt = f"""
|
|
@@ -277,9 +261,9 @@ Rewrite the following passage so it fits about {target_flesch} Flesch Reading Ea
|
|
| 277 |
- Extract only the portions of the text that should be read as the main body,
|
| 278 |
excluding the title, author name, source information, chapter number, annotations, and footers.
|
| 279 |
- When outputting, make sure sections divided by chapters, etc., are clearly distinguishable by leaving a blank line between them.
|
| 280 |
-
- Preserve the original meaning faithfully.
|
| 281 |
-
- Do not add new information or remove essential information.
|
| 282 |
-
- Output only the rewritten passage. Do not include explanations.
|
| 283 |
{text}
|
| 284 |
"""
|
| 285 |
|
|
@@ -338,24 +322,24 @@ def start_test(student_id, level_input, group_input, session_state):
|
|
| 338 |
save_log(entry)
|
| 339 |
|
| 340 |
return (
|
| 341 |
-
"",
|
| 342 |
-
"",
|
| 343 |
-
"",
|
| 344 |
-
json.dumps([]),
|
| 345 |
-
0,
|
| 346 |
-
0,
|
| 347 |
-
"",
|
| 348 |
-
"",
|
| 349 |
-
None,
|
| 350 |
-
gr.update(interactive=False, visible=False),
|
| 351 |
-
gr.update(interactive=False, visible=True),
|
| 352 |
-
gr.update(interactive=False, visible=False),
|
| 353 |
session_state
|
| 354 |
)
|
| 355 |
|
| 356 |
user_id = str(student_id).strip()
|
| 357 |
level = int(level_input)
|
| 358 |
-
group = int(group_input)
|
| 359 |
|
| 360 |
used_passages_set = set()
|
| 361 |
|
|
@@ -371,19 +355,18 @@ def start_test(student_id, level_input, group_input, session_state):
|
|
| 371 |
}
|
| 372 |
save_log(entry)
|
| 373 |
|
| 374 |
-
# ★変更:target level を渡して「難しい教材のみ」から選ぶ
|
| 375 |
pid, text, orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
|
| 376 |
if text is None:
|
| 377 |
return (
|
| 378 |
-
"",
|
| 379 |
-
"教材が
|
| 380 |
-
"",
|
| 381 |
-
json.dumps([]),
|
| 382 |
-
0,
|
| 383 |
-
0,
|
| 384 |
-
"",
|
| 385 |
-
"",
|
| 386 |
-
None,
|
| 387 |
gr.update(interactive=False, visible=False),
|
| 388 |
gr.update(interactive=False, visible=False),
|
| 389 |
gr.update(interactive=False, visible=False),
|
|
@@ -407,20 +390,17 @@ def start_test(student_id, level_input, group_input, session_state):
|
|
| 407 |
next_upd = gr.update(interactive=True, visible=True)
|
| 408 |
finish_upd = gr.update(interactive=False, visible=False)
|
| 409 |
|
| 410 |
-
page_num = 1
|
| 411 |
now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 412 |
-
|
| 413 |
-
entry2 = {
|
| 414 |
"user_id": user_id,
|
| 415 |
"group": group,
|
| 416 |
"assigned_level": level,
|
| 417 |
"passage_id": pid,
|
| 418 |
"original_level": orig_lev,
|
| 419 |
"action_time": now2,
|
| 420 |
-
"action_type":
|
| 421 |
"page_text": pages[0]
|
| 422 |
-
}
|
| 423 |
-
save_log(entry2)
|
| 424 |
|
| 425 |
session_state = {
|
| 426 |
"user_id": user_id,
|
|
@@ -453,9 +433,9 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
|
|
| 453 |
user_id = session_state.get("user_id")
|
| 454 |
level = session_state.get("level")
|
| 455 |
group = session_state.get("group")
|
| 456 |
-
|
| 457 |
now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 458 |
-
|
| 459 |
"user_id": user_id,
|
| 460 |
"group": group,
|
| 461 |
"assigned_level": level,
|
|
@@ -464,8 +444,7 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
|
|
| 464 |
"action_time": now,
|
| 465 |
"action_type": "next_pushed",
|
| 466 |
"page_text": None
|
| 467 |
-
}
|
| 468 |
-
save_log(entry)
|
| 469 |
|
| 470 |
pages = json.loads(pages_json)
|
| 471 |
if not pages:
|
|
@@ -478,7 +457,7 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
|
|
| 478 |
new_page = min(current_page + 1, total_pages - 1)
|
| 479 |
|
| 480 |
now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 481 |
-
|
| 482 |
"user_id": user_id,
|
| 483 |
"group": group,
|
| 484 |
"assigned_level": level,
|
|
@@ -487,8 +466,7 @@ def next_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
|
|
| 487 |
"action_time": now2,
|
| 488 |
"action_type": f"page_displayed_{new_page+1}",
|
| 489 |
"page_text": pages[new_page]
|
| 490 |
-
}
|
| 491 |
-
save_log(entry2)
|
| 492 |
|
| 493 |
if new_page == total_pages - 1:
|
| 494 |
return (
|
|
@@ -519,7 +497,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
|
|
| 519 |
group = session_state.get("group")
|
| 520 |
|
| 521 |
now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 522 |
-
|
| 523 |
"user_id": user_id,
|
| 524 |
"group": group,
|
| 525 |
"assigned_level": level,
|
|
@@ -528,8 +506,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
|
|
| 528 |
"action_time": now,
|
| 529 |
"action_type": "prev_pushed",
|
| 530 |
"page_text": None
|
| 531 |
-
}
|
| 532 |
-
save_log(entry)
|
| 533 |
|
| 534 |
pages = json.loads(pages_json)
|
| 535 |
if not pages:
|
|
@@ -547,7 +524,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
|
|
| 547 |
finish_upd = gr.update(interactive=(not next_visible), visible=(not next_visible))
|
| 548 |
|
| 549 |
now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 550 |
-
|
| 551 |
"user_id": user_id,
|
| 552 |
"group": group,
|
| 553 |
"assigned_level": level,
|
|
@@ -556,8 +533,7 @@ def prev_page(pages_json, current_page, total_pages, pid, orig_lev, session_stat
|
|
| 556 |
"action_time": now2,
|
| 557 |
"action_type": f"page_displayed_{new_page+1}",
|
| 558 |
"page_text": pages[new_page]
|
| 559 |
-
}
|
| 560 |
-
save_log(entry2)
|
| 561 |
|
| 562 |
return (
|
| 563 |
pages[new_page],
|
|
@@ -579,7 +555,7 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
|
|
| 579 |
pages = json.loads(pages_json)
|
| 580 |
now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 581 |
|
| 582 |
-
|
| 583 |
"user_id": user_id,
|
| 584 |
"group": group,
|
| 585 |
"assigned_level": level,
|
|
@@ -588,10 +564,8 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
|
|
| 588 |
"action_time": now,
|
| 589 |
"action_type": action,
|
| 590 |
"page_text": None
|
| 591 |
-
}
|
| 592 |
-
save_log(entry)
|
| 593 |
|
| 594 |
-
# ★変更:target level を渡して「難しい教材のみ」から選ぶ
|
| 595 |
new_pid, new_text, new_orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
|
| 596 |
if new_text is None:
|
| 597 |
return (
|
|
@@ -621,7 +595,7 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
|
|
| 621 |
finish_upd = gr.update(interactive=False, visible=False)
|
| 622 |
|
| 623 |
now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 624 |
-
|
| 625 |
"user_id": user_id,
|
| 626 |
"group": group,
|
| 627 |
"assigned_level": level,
|
|
@@ -630,8 +604,7 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
|
|
| 630 |
"action_time": now2,
|
| 631 |
"action_type": "page_displayed_1",
|
| 632 |
"page_text": new_pages[0]
|
| 633 |
-
}
|
| 634 |
-
save_log(entry2)
|
| 635 |
|
| 636 |
session_state = {
|
| 637 |
"user_id": user_id,
|
|
@@ -659,87 +632,8 @@ def finish_or_retire(pages_json, current_page, pid, orig_lev, action, session_st
|
|
| 659 |
# ======================================================
|
| 660 |
# UI(タイトル表示を追加。それ以外は変更しない)
|
| 661 |
# ★追加:パスワード付きログCSVダウンロード
|
| 662 |
-
# ★
|
| 663 |
# ======================================================
|
| 664 |
-
|
| 665 |
-
# ★差し替え:Edgeでも確実に効くハイライトJS(style直書き)
|
| 666 |
-
radio_js = r"""
|
| 667 |
-
() => {
|
| 668 |
-
const root = document.getElementById("group_radio");
|
| 669 |
-
if (!root) return;
|
| 670 |
-
|
| 671 |
-
const getScheme = () => {
|
| 672 |
-
try {
|
| 673 |
-
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
| 674 |
-
? "dark" : "light";
|
| 675 |
-
} catch (e) {
|
| 676 |
-
return "light";
|
| 677 |
-
}
|
| 678 |
-
};
|
| 679 |
-
|
| 680 |
-
const clear = (labels) => {
|
| 681 |
-
labels.forEach(l => {
|
| 682 |
-
l.classList.remove("radio-selected");
|
| 683 |
-
l.style.background = "";
|
| 684 |
-
l.style.border = "";
|
| 685 |
-
l.style.boxShadow = "";
|
| 686 |
-
l.style.borderRadius = "";
|
| 687 |
-
l.style.padding = "";
|
| 688 |
-
});
|
| 689 |
-
};
|
| 690 |
-
|
| 691 |
-
const apply = (label) => {
|
| 692 |
-
const scheme = getScheme();
|
| 693 |
-
label.classList.add("radio-selected");
|
| 694 |
-
|
| 695 |
-
if (scheme === "dark") {
|
| 696 |
-
label.style.background = "#1f2937";
|
| 697 |
-
label.style.border = "2px solid #60a5fa";
|
| 698 |
-
label.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.06) inset";
|
| 699 |
-
} else {
|
| 700 |
-
label.style.background = "#eef2ff";
|
| 701 |
-
label.style.border = "2px solid #4f46e5";
|
| 702 |
-
label.style.boxShadow = "0 0 0 1px rgba(0,0,0,0.04) inset";
|
| 703 |
-
}
|
| 704 |
-
label.style.borderRadius = "12px";
|
| 705 |
-
label.style.padding = "10px 12px";
|
| 706 |
-
};
|
| 707 |
-
|
| 708 |
-
const update = () => {
|
| 709 |
-
const labels = root.querySelectorAll("label");
|
| 710 |
-
if (!labels || labels.length === 0) return;
|
| 711 |
-
|
| 712 |
-
clear(labels);
|
| 713 |
-
|
| 714 |
-
const checked = root.querySelector('input[type="radio"]:checked');
|
| 715 |
-
if (!checked) return;
|
| 716 |
-
|
| 717 |
-
const label = checked.closest("label");
|
| 718 |
-
if (!label) return;
|
| 719 |
-
|
| 720 |
-
apply(label);
|
| 721 |
-
};
|
| 722 |
-
|
| 723 |
-
update();
|
| 724 |
-
|
| 725 |
-
root.addEventListener("change", update);
|
| 726 |
-
root.addEventListener("click", update);
|
| 727 |
-
|
| 728 |
-
let n = 0;
|
| 729 |
-
const t = setInterval(() => {
|
| 730 |
-
update();
|
| 731 |
-
n += 1;
|
| 732 |
-
if (n >= 15) clearInterval(t);
|
| 733 |
-
}, 200);
|
| 734 |
-
|
| 735 |
-
try {
|
| 736 |
-
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
| 737 |
-
mq.addEventListener?.("change", update);
|
| 738 |
-
mq.addListener?.(update);
|
| 739 |
-
} catch (e) {}
|
| 740 |
-
};
|
| 741 |
-
"""
|
| 742 |
-
|
| 743 |
custom_css = """
|
| 744 |
/* ===============================
|
| 745 |
共通(両モード)
|
|
@@ -749,22 +643,18 @@ custom_css = """
|
|
| 749 |
line-height: 1.8 !important;
|
| 750 |
font-family: "Noto Sans", sans-serif !important;
|
| 751 |
}
|
| 752 |
-
|
| 753 |
-
/* 教材表示ボックス */
|
| 754 |
.reading-area {
|
| 755 |
padding: 20px !important;
|
| 756 |
border-radius: 12px !important;
|
| 757 |
border: 1px solid #ccc !important;
|
| 758 |
transition: background-color 0.2s ease, color 0.2s ease;
|
| 759 |
}
|
| 760 |
-
|
| 761 |
-
/* ★追加:Radio / Dropdown などフォーム部品の可読性を安定させる(両モード共通) */
|
| 762 |
.gradio-container label, .gradio-container .wrap label {
|
| 763 |
color: inherit !important;
|
| 764 |
}
|
| 765 |
.gradio-container input[type="radio"],
|
| 766 |
.gradio-container input[type="checkbox"] {
|
| 767 |
-
accent-color: #2563eb !important;
|
| 768 |
}
|
| 769 |
.gradio-container select {
|
| 770 |
color: inherit !important;
|
|
@@ -778,38 +668,28 @@ custom_css = """
|
|
| 778 |
background-color: #ffffff !important;
|
| 779 |
color: #222 !important;
|
| 780 |
}
|
| 781 |
-
|
| 782 |
-
/* ★追加:フォーム周り(Radio/Dropdown)の背景と境界を明るい環境で見やすく */
|
| 783 |
.gr-panel, .gr-box, .gr-group {
|
| 784 |
background-color: #ffffff !important;
|
| 785 |
border-color: #ddd !important;
|
| 786 |
}
|
| 787 |
-
|
| 788 |
.reading-area {
|
| 789 |
background-color: #fafafa !important;
|
| 790 |
color: #222 !important;
|
| 791 |
border-color: #ddd !important;
|
| 792 |
}
|
| 793 |
-
|
| 794 |
textarea, input, .gr-textbox textarea {
|
| 795 |
background-color: #ffffff !important;
|
| 796 |
color: #222 !important;
|
| 797 |
border: 1px solid #ccc !important;
|
| 798 |
}
|
| 799 |
-
|
| 800 |
-
/* ★追加:Dropdown(Select) をライトで見やすく */
|
| 801 |
select, .gr-dropdown select {
|
| 802 |
background-color: #ffffff !important;
|
| 803 |
color: #222 !important;
|
| 804 |
border: 1px solid #ccc !important;
|
| 805 |
}
|
| 806 |
-
|
| 807 |
-
/* ★追加:Radioの選択肢テキストが薄くなるケースの対策 */
|
| 808 |
.gr-radio label, .gr-radio .wrap label, .gr-radio span {
|
| 809 |
color: #222 !important;
|
| 810 |
}
|
| 811 |
-
|
| 812 |
-
/* ★追加:ライトモードでボタンが背景に溶ける場合の対策 */
|
| 813 |
button {
|
| 814 |
background-color: #f5f5f5 !important;
|
| 815 |
color: #111 !important;
|
|
@@ -828,26 +708,21 @@ custom_css = """
|
|
| 828 |
background-color: #1e1e1e !important;
|
| 829 |
color: #e6e6e6 !important;
|
| 830 |
}
|
| 831 |
-
|
| 832 |
.reading-area {
|
| 833 |
background-color: #2a2a2a !important;
|
| 834 |
color: #f2f2f2 !important;
|
| 835 |
border-color: #444 !important;
|
| 836 |
}
|
| 837 |
-
|
| 838 |
textarea, input, .gr-textbox textarea {
|
| 839 |
background-color: #2c2c2c !important;
|
| 840 |
color: #f0f0f0 !important;
|
| 841 |
border: 1px solid #555 !important;
|
| 842 |
}
|
| 843 |
-
|
| 844 |
-
/* ★追加:Dropdown(Select) をダークで見やすく */
|
| 845 |
select, .gr-dropdown select {
|
| 846 |
background-color: #2c2c2c !important;
|
| 847 |
color: #f0f0f0 !important;
|
| 848 |
border: 1px solid #555 !important;
|
| 849 |
}
|
| 850 |
-
|
| 851 |
button {
|
| 852 |
background-color: #3a3a3a !important;
|
| 853 |
color: #f0f0f0 !important;
|
|
@@ -856,29 +731,18 @@ custom_css = """
|
|
| 856 |
button:hover {
|
| 857 |
background-color: #4a4a4a !important;
|
| 858 |
}
|
| 859 |
-
|
| 860 |
.gr-panel, .gr-box, .gr-group {
|
| 861 |
background-color: #272727 !important;
|
| 862 |
border-color: #444 !important;
|
| 863 |
}
|
| 864 |
-
|
| 865 |
-
/* ★追加:Radioの選択肢が薄くなるケースの対策 */
|
| 866 |
.gr-radio label, .gr-radio .wrap label, .gr-radio span {
|
| 867 |
color: #e6e6e6 !important;
|
| 868 |
}
|
| 869 |
}
|
| 870 |
|
| 871 |
-
/* ★追加:Radio行が細くてハイライトが目立たない問題の保険(Edge向け) */
|
| 872 |
-
#group_radio label {
|
| 873 |
-
width: 100% !important;
|
| 874 |
-
box-sizing: border-box !important;
|
| 875 |
-
}
|
| 876 |
-
|
| 877 |
/* ===============================
|
| 878 |
-
★
|
| 879 |
=============================== */
|
| 880 |
-
|
| 881 |
-
/* ラジオ自体がOS/ブラウザ依存で薄くなるのを防ぐ */
|
| 882 |
#group_radio input[type="radio"]{
|
| 883 |
appearance: auto !important;
|
| 884 |
-webkit-appearance: radio !important;
|
|
@@ -886,8 +750,6 @@ custom_css = """
|
|
| 886 |
height: 18px !important;
|
| 887 |
accent-color: #2563eb !important;
|
| 888 |
}
|
| 889 |
-
|
| 890 |
-
/* 各選択肢を行として見やすく */
|
| 891 |
#group_radio label{
|
| 892 |
width: 100% !important;
|
| 893 |
box-sizing: border-box !important;
|
|
@@ -899,34 +761,22 @@ custom_css = """
|
|
| 899 |
gap: 10px !important;
|
| 900 |
}
|
| 901 |
|
| 902 |
-
/*
|
| 903 |
-
(DOM差に備えて :has / input+span / input~span を全部用意)
|
| 904 |
-
*/
|
| 905 |
@media (prefers-color-scheme: light){
|
| 906 |
#group_radio label:has(input[type="radio"]:checked){
|
| 907 |
background: #eef2ff !important;
|
| 908 |
border: 2px solid #4f46e5 !important;
|
| 909 |
}
|
| 910 |
-
#group_radio input[type="radio"]:checked + span,
|
| 911 |
-
#group_radio input[type="radio"]:checked ~ span{
|
| 912 |
-
font-weight: 700 !important;
|
| 913 |
-
}
|
| 914 |
}
|
| 915 |
-
|
| 916 |
@media (prefers-color-scheme: dark){
|
| 917 |
#group_radio label:has(input[type="radio"]:checked){
|
| 918 |
background: #1f2937 !important;
|
| 919 |
border: 2px solid #60a5fa !important;
|
| 920 |
}
|
| 921 |
-
#group_radio input[type="radio"]:checked + span,
|
| 922 |
-
#group_radio input[type="radio"]:checked ~ span{
|
| 923 |
-
font-weight: 700 !important;
|
| 924 |
-
}
|
| 925 |
}
|
| 926 |
-
|
| 927 |
"""
|
| 928 |
|
| 929 |
-
with gr.Blocks(css=custom_css
|
| 930 |
gr.Markdown("# 📚 Reading Exercise")
|
| 931 |
|
| 932 |
session_state = gr.State({"user_id": None, "level": None, "group": 2, "used_passages": []})
|
|
@@ -934,10 +784,10 @@ with gr.Blocks(css=custom_css, js=radio_js) as demo:
|
|
| 934 |
student_id_input = gr.Textbox(label="学生番号(必須)")
|
| 935 |
|
| 936 |
group_input = gr.Radio(
|
| 937 |
-
choices=[("Group 1", "1"), ("Group 2", "2")],
|
| 938 |
label="実験グループを選択",
|
| 939 |
value="2",
|
| 940 |
-
elem_id="group_radio"
|
| 941 |
)
|
| 942 |
|
| 943 |
level_input = gr.Dropdown(
|
|
@@ -1048,7 +898,6 @@ with gr.Blocks(css=custom_css, js=radio_js) as demo:
|
|
| 1048 |
]
|
| 1049 |
)
|
| 1050 |
|
| 1051 |
-
# ★追加:ログCSVダウンロード(パスワード必須)
|
| 1052 |
gr.Markdown("## 🔐 管理者用:ログCSVダウンロード(パスワード必須)")
|
| 1053 |
admin_password = gr.Textbox(label="Password", type="password")
|
| 1054 |
download_btn = gr.Button("ログCSVを生成してダウンロード")
|
|
|
|
| 138 |
if num.isdigit():
|
| 139 |
all_ids.append(int(num))
|
| 140 |
|
|
|
|
|
|
|
| 141 |
eligible_ids = []
|
| 142 |
for pid in all_ids:
|
| 143 |
row = passage_info_df[passage_info_df["Text#"] == pid]
|
|
|
|
| 185 |
# ======================================================
|
| 186 |
|
| 187 |
def extract_main_body(text: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
if not text:
|
| 189 |
return ""
|
| 190 |
|
| 191 |
lines = text.splitlines()
|
| 192 |
|
|
|
|
| 193 |
drop_patterns = [
|
| 194 |
r"^\s*title\s*:\s*.*$",
|
| 195 |
r"^\s*author\s*:\s*.*$",
|
|
|
|
| 201 |
r".*GUTENBERG.*",
|
| 202 |
r"^\s*http[s]?://\S+.*$",
|
| 203 |
r"^\s*www\.\S+.*$",
|
| 204 |
+
r"^\s*\[.*\]\s*$",
|
| 205 |
+
r"^\s*\(\s*.*\s*\)\s*$",
|
| 206 |
r"^\s*end\s+of\s+.*$",
|
| 207 |
r"^\s*\*{3}.*\*{3}\s*$",
|
| 208 |
]
|
| 209 |
drop_re = re.compile("|".join(f"(?:{p})" for p in drop_patterns), re.IGNORECASE)
|
| 210 |
|
|
|
|
| 211 |
kept = []
|
| 212 |
for ln in lines:
|
| 213 |
if drop_re.match(ln.strip()):
|
| 214 |
continue
|
| 215 |
kept.append(ln)
|
| 216 |
|
|
|
|
| 217 |
def is_title_like(s: str) -> bool:
|
| 218 |
t = s.strip()
|
| 219 |
if not t:
|
|
|
|
| 253 |
# ======================================================
|
| 254 |
|
| 255 |
def rewrite_level(text, target_level):
|
| 256 |
+
level_to_flesch = {1: 90, 2: 75, 3: 65, 4: 55, 5: 40}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
target_flesch = level_to_flesch[int(target_level)]
|
| 258 |
|
| 259 |
prompt = f"""
|
|
|
|
| 261 |
- Extract only the portions of the text that should be read as the main body,
|
| 262 |
excluding the title, author name, source information, chapter number, annotations, and footers.
|
| 263 |
- When outputting, make sure sections divided by chapters, etc., are clearly distinguishable by leaving a blank line between them.
|
| 264 |
+
- Preserve the original meaning faithfully.
|
| 265 |
+
- Do not add new information or remove essential information.
|
| 266 |
+
- Output only the rewritten passage. Do not include explanations.
|
| 267 |
{text}
|
| 268 |
"""
|
| 269 |
|
|
|
|
| 322 |
save_log(entry)
|
| 323 |
|
| 324 |
return (
|
| 325 |
+
"",
|
| 326 |
+
"",
|
| 327 |
+
"",
|
| 328 |
+
json.dumps([]),
|
| 329 |
+
0,
|
| 330 |
+
0,
|
| 331 |
+
"",
|
| 332 |
+
"",
|
| 333 |
+
None,
|
| 334 |
+
gr.update(interactive=False, visible=False),
|
| 335 |
+
gr.update(interactive=False, visible=True),
|
| 336 |
+
gr.update(interactive=False, visible=False),
|
| 337 |
session_state
|
| 338 |
)
|
| 339 |
|
| 340 |
user_id = str(student_id).strip()
|
| 341 |
level = int(level_input)
|
| 342 |
+
group = int(group_input) # ★group_inputは "1"/"2" でもOK
|
| 343 |
|
| 344 |
used_passages_set = set()
|
| 345 |
|
|
|
|
| 355 |
}
|
| 356 |
save_log(entry)
|
| 357 |
|
|
|
|
| 358 |
pid, text, orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
|
| 359 |
if text is None:
|
| 360 |
return (
|
| 361 |
+
"",
|
| 362 |
+
"教材が見つかりません",
|
| 363 |
+
"",
|
| 364 |
+
json.dumps([]),
|
| 365 |
+
0,
|
| 366 |
+
0,
|
| 367 |
+
"",
|
| 368 |
+
"",
|
| 369 |
+
None,
|
| 370 |
gr.update(interactive=False, visible=False),
|
| 371 |
gr.update(interactive=False, visible=False),
|
| 372 |
gr.update(interactive=False, visible=False),
|
|
|
|
| 390 |
next_upd = gr.update(interactive=True, visible=True)
|
| 391 |
finish_upd = gr.update(interactive=False, visible=False)
|
| 392 |
|
|
|
|
| 393 |
now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 394 |
+
save_log({
|
|
|
|
| 395 |
"user_id": user_id,
|
| 396 |
"group": group,
|
| 397 |
"assigned_level": level,
|
| 398 |
"passage_id": pid,
|
| 399 |
"original_level": orig_lev,
|
| 400 |
"action_time": now2,
|
| 401 |
+
"action_type": "page_displayed_1",
|
| 402 |
"page_text": pages[0]
|
| 403 |
+
})
|
|
|
|
| 404 |
|
| 405 |
session_state = {
|
| 406 |
"user_id": user_id,
|
|
|
|
| 433 |
user_id = session_state.get("user_id")
|
| 434 |
level = session_state.get("level")
|
| 435 |
group = session_state.get("group")
|
| 436 |
+
|
| 437 |
now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 438 |
+
save_log({
|
| 439 |
"user_id": user_id,
|
| 440 |
"group": group,
|
| 441 |
"assigned_level": level,
|
|
|
|
| 444 |
"action_time": now,
|
| 445 |
"action_type": "next_pushed",
|
| 446 |
"page_text": None
|
| 447 |
+
})
|
|
|
|
| 448 |
|
| 449 |
pages = json.loads(pages_json)
|
| 450 |
if not pages:
|
|
|
|
| 457 |
new_page = min(current_page + 1, total_pages - 1)
|
| 458 |
|
| 459 |
now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 460 |
+
save_log({
|
| 461 |
"user_id": user_id,
|
| 462 |
"group": group,
|
| 463 |
"assigned_level": level,
|
|
|
|
| 466 |
"action_time": now2,
|
| 467 |
"action_type": f"page_displayed_{new_page+1}",
|
| 468 |
"page_text": pages[new_page]
|
| 469 |
+
})
|
|
|
|
| 470 |
|
| 471 |
if new_page == total_pages - 1:
|
| 472 |
return (
|
|
|
|
| 497 |
group = session_state.get("group")
|
| 498 |
|
| 499 |
now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 500 |
+
save_log({
|
| 501 |
"user_id": user_id,
|
| 502 |
"group": group,
|
| 503 |
"assigned_level": level,
|
|
|
|
| 506 |
"action_time": now,
|
| 507 |
"action_type": "prev_pushed",
|
| 508 |
"page_text": None
|
| 509 |
+
})
|
|
|
|
| 510 |
|
| 511 |
pages = json.loads(pages_json)
|
| 512 |
if not pages:
|
|
|
|
| 524 |
finish_upd = gr.update(interactive=(not next_visible), visible=(not next_visible))
|
| 525 |
|
| 526 |
now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 527 |
+
save_log({
|
| 528 |
"user_id": user_id,
|
| 529 |
"group": group,
|
| 530 |
"assigned_level": level,
|
|
|
|
| 533 |
"action_time": now2,
|
| 534 |
"action_type": f"page_displayed_{new_page+1}",
|
| 535 |
"page_text": pages[new_page]
|
| 536 |
+
})
|
|
|
|
| 537 |
|
| 538 |
return (
|
| 539 |
pages[new_page],
|
|
|
|
| 555 |
pages = json.loads(pages_json)
|
| 556 |
now = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 557 |
|
| 558 |
+
save_log({
|
| 559 |
"user_id": user_id,
|
| 560 |
"group": group,
|
| 561 |
"assigned_level": level,
|
|
|
|
| 564 |
"action_time": now,
|
| 565 |
"action_type": action,
|
| 566 |
"page_text": None
|
| 567 |
+
})
|
|
|
|
| 568 |
|
|
|
|
| 569 |
new_pid, new_text, new_orig_lev, title, used_passages_set = get_new_passage_random(used_passages_set, level)
|
| 570 |
if new_text is None:
|
| 571 |
return (
|
|
|
|
| 595 |
finish_upd = gr.update(interactive=False, visible=False)
|
| 596 |
|
| 597 |
now2 = (datetime.utcnow() + timedelta(hours=9)).isoformat()
|
| 598 |
+
save_log({
|
| 599 |
"user_id": user_id,
|
| 600 |
"group": group,
|
| 601 |
"assigned_level": level,
|
|
|
|
| 604 |
"action_time": now2,
|
| 605 |
"action_type": "page_displayed_1",
|
| 606 |
"page_text": new_pages[0]
|
| 607 |
+
})
|
|
|
|
| 608 |
|
| 609 |
session_state = {
|
| 610 |
"user_id": user_id,
|
|
|
|
| 632 |
# ======================================================
|
| 633 |
# UI(タイトル表示を追加。それ以外は変更しない)
|
| 634 |
# ★追加:パスワード付きログCSVダウンロード
|
| 635 |
+
# ★修正:JSは使わずCSSのみ(スタートが進まない問題を回避)
|
| 636 |
# ======================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
custom_css = """
|
| 638 |
/* ===============================
|
| 639 |
共通(両モード)
|
|
|
|
| 643 |
line-height: 1.8 !important;
|
| 644 |
font-family: "Noto Sans", sans-serif !important;
|
| 645 |
}
|
|
|
|
|
|
|
| 646 |
.reading-area {
|
| 647 |
padding: 20px !important;
|
| 648 |
border-radius: 12px !important;
|
| 649 |
border: 1px solid #ccc !important;
|
| 650 |
transition: background-color 0.2s ease, color 0.2s ease;
|
| 651 |
}
|
|
|
|
|
|
|
| 652 |
.gradio-container label, .gradio-container .wrap label {
|
| 653 |
color: inherit !important;
|
| 654 |
}
|
| 655 |
.gradio-container input[type="radio"],
|
| 656 |
.gradio-container input[type="checkbox"] {
|
| 657 |
+
accent-color: #2563eb !important;
|
| 658 |
}
|
| 659 |
.gradio-container select {
|
| 660 |
color: inherit !important;
|
|
|
|
| 668 |
background-color: #ffffff !important;
|
| 669 |
color: #222 !important;
|
| 670 |
}
|
|
|
|
|
|
|
| 671 |
.gr-panel, .gr-box, .gr-group {
|
| 672 |
background-color: #ffffff !important;
|
| 673 |
border-color: #ddd !important;
|
| 674 |
}
|
|
|
|
| 675 |
.reading-area {
|
| 676 |
background-color: #fafafa !important;
|
| 677 |
color: #222 !important;
|
| 678 |
border-color: #ddd !important;
|
| 679 |
}
|
|
|
|
| 680 |
textarea, input, .gr-textbox textarea {
|
| 681 |
background-color: #ffffff !important;
|
| 682 |
color: #222 !important;
|
| 683 |
border: 1px solid #ccc !important;
|
| 684 |
}
|
|
|
|
|
|
|
| 685 |
select, .gr-dropdown select {
|
| 686 |
background-color: #ffffff !important;
|
| 687 |
color: #222 !important;
|
| 688 |
border: 1px solid #ccc !important;
|
| 689 |
}
|
|
|
|
|
|
|
| 690 |
.gr-radio label, .gr-radio .wrap label, .gr-radio span {
|
| 691 |
color: #222 !important;
|
| 692 |
}
|
|
|
|
|
|
|
| 693 |
button {
|
| 694 |
background-color: #f5f5f5 !important;
|
| 695 |
color: #111 !important;
|
|
|
|
| 708 |
background-color: #1e1e1e !important;
|
| 709 |
color: #e6e6e6 !important;
|
| 710 |
}
|
|
|
|
| 711 |
.reading-area {
|
| 712 |
background-color: #2a2a2a !important;
|
| 713 |
color: #f2f2f2 !important;
|
| 714 |
border-color: #444 !important;
|
| 715 |
}
|
|
|
|
| 716 |
textarea, input, .gr-textbox textarea {
|
| 717 |
background-color: #2c2c2c !important;
|
| 718 |
color: #f0f0f0 !important;
|
| 719 |
border: 1px solid #555 !important;
|
| 720 |
}
|
|
|
|
|
|
|
| 721 |
select, .gr-dropdown select {
|
| 722 |
background-color: #2c2c2c !important;
|
| 723 |
color: #f0f0f0 !important;
|
| 724 |
border: 1px solid #555 !important;
|
| 725 |
}
|
|
|
|
| 726 |
button {
|
| 727 |
background-color: #3a3a3a !important;
|
| 728 |
color: #f0f0f0 !important;
|
|
|
|
| 731 |
button:hover {
|
| 732 |
background-color: #4a4a4a !important;
|
| 733 |
}
|
|
|
|
| 734 |
.gr-panel, .gr-box, .gr-group {
|
| 735 |
background-color: #272727 !important;
|
| 736 |
border-color: #444 !important;
|
| 737 |
}
|
|
|
|
|
|
|
| 738 |
.gr-radio label, .gr-radio .wrap label, .gr-radio span {
|
| 739 |
color: #e6e6e6 !important;
|
| 740 |
}
|
| 741 |
}
|
| 742 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
/* ===============================
|
| 744 |
+
★Group選択:CSSのみで見やすく(EdgeでもOK)
|
| 745 |
=============================== */
|
|
|
|
|
|
|
| 746 |
#group_radio input[type="radio"]{
|
| 747 |
appearance: auto !important;
|
| 748 |
-webkit-appearance: radio !important;
|
|
|
|
| 750 |
height: 18px !important;
|
| 751 |
accent-color: #2563eb !important;
|
| 752 |
}
|
|
|
|
|
|
|
| 753 |
#group_radio label{
|
| 754 |
width: 100% !important;
|
| 755 |
box-sizing: border-box !important;
|
|
|
|
| 761 |
gap: 10px !important;
|
| 762 |
}
|
| 763 |
|
| 764 |
+
/* :has が効く環境は行全体ハイライト */
|
|
|
|
|
|
|
| 765 |
@media (prefers-color-scheme: light){
|
| 766 |
#group_radio label:has(input[type="radio"]:checked){
|
| 767 |
background: #eef2ff !important;
|
| 768 |
border: 2px solid #4f46e5 !important;
|
| 769 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 770 |
}
|
|
|
|
| 771 |
@media (prefers-color-scheme: dark){
|
| 772 |
#group_radio label:has(input[type="radio"]:checked){
|
| 773 |
background: #1f2937 !important;
|
| 774 |
border: 2px solid #60a5fa !important;
|
| 775 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
}
|
|
|
|
| 777 |
"""
|
| 778 |
|
| 779 |
+
with gr.Blocks(css=custom_css) as demo:
|
| 780 |
gr.Markdown("# 📚 Reading Exercise")
|
| 781 |
|
| 782 |
session_state = gr.State({"user_id": None, "level": None, "group": 2, "used_passages": []})
|
|
|
|
| 784 |
student_id_input = gr.Textbox(label="学生番号(必須)")
|
| 785 |
|
| 786 |
group_input = gr.Radio(
|
| 787 |
+
choices=[("Group 1", "1"), ("Group 2", "2")], # ★表示問題回避のため文字列
|
| 788 |
label="実験グループを選択",
|
| 789 |
value="2",
|
| 790 |
+
elem_id="group_radio"
|
| 791 |
)
|
| 792 |
|
| 793 |
level_input = gr.Dropdown(
|
|
|
|
| 898 |
]
|
| 899 |
)
|
| 900 |
|
|
|
|
| 901 |
gr.Markdown("## 🔐 管理者用:ログCSVダウンロード(パスワード必須)")
|
| 902 |
admin_password = gr.Textbox(label="Password", type="password")
|
| 903 |
download_btn = gr.Button("ログCSVを生成してダウンロード")
|