Spaces:
Running
Running
import json | |
import random | |
from dataclasses import dataclass | |
import fire | |
import gradio as gr | |
class KanaQuizApp: | |
def __init__(self, data_path="data/kana-spell.json", font="Noto Sans JP"): | |
self.data = KanaSpell.load(data_path) | |
self.font = font | |
self.init_app() | |
self.launch() | |
def init_app(self): | |
font = gr.themes.GoogleFont(self.font) | |
text_size = gr.themes.sizes.text_lg | |
theme = gr.themes.Ocean(font=font, text_size=text_size) | |
with gr.Blocks(theme=theme) as self.app: | |
self.init_state() | |
self.init_layout() | |
self.register_events() | |
def init_state(self): | |
self.st_queue = gr.State(None) | |
def init_layout(self): | |
with gr.Tabs(selected=0) as self.tabs: | |
with gr.Tab(label="設定 ⚙️", id=0): | |
self.init_setting_tab() | |
with gr.Tab(label="測驗 📝", id=1): | |
self.init_quiz_tab() | |
with gr.Tab(label="紀錄 📜", id=2): | |
self.init_record_tab() | |
with gr.Tab(label="對照表 💫", id=3): | |
self.init_table() | |
self.txt_debug = gr.TextArea(label="Debug", visible=False) | |
def init_setting_tab(self): | |
with gr.Group(): | |
self.chk_kana = gr.CheckboxGroup(["平假名", "片假名"], value=["平假名"], label="假名") | |
self.chk_seion = gr.CheckboxGroup(self.data.category.seion, value=["a"], label="清音") | |
with gr.Row(): | |
self.chk_dakuon = gr.CheckboxGroup(self.data.category.dakuon, label="濁音") | |
self.chk_handakuon = gr.CheckboxGroup(self.data.category.handakuon, label="半濁音") | |
self.chk_youon = gr.CheckboxGroup(self.data.category.youon, label="拗音") | |
with gr.Row(): | |
self.btn_select_all = gr.Button("全選") | |
self.btn_select_none = gr.Button("全不選") | |
self.btn_start = gr.Button("開始測驗 🚀") | |
def init_quiz_tab(self): | |
with gr.Group(): | |
with gr.Row(): | |
self.txt_test = gr.Textbox(label="題目 👀", interactive=False) | |
self.txt_info = gr.Textbox(label="狀態 📊", interactive=False) | |
with gr.Row(): | |
with gr.Column(): | |
self.txt_input = gr.Textbox(label="作答 ✍️", submit_btn=True) | |
with gr.Column(): | |
with gr.Row(): | |
self.n_correct = gr.Number(label="答對題數 ✅", value=0, interactive=False) | |
self.n_total = gr.Number(label="總答題數 🧮", value=0, interactive=False) | |
def init_record_tab(self): | |
self.txt_record = gr.TextArea(show_label=False, interactive=False) | |
with gr.Row(): | |
self.btn_back_to_settings = gr.Button("回到設定 ⚙️") | |
self.btn_again = gr.Button("再次測驗 🔄") | |
def init_table(self): | |
with gr.Tab("平假名"): | |
with gr.Tab("基本"): | |
gr.Markdown(read_text("data/hiragana-gojuon.md")) | |
with gr.Tab("濁音、半濁音"): | |
gr.Markdown(read_text("data/hiragana-dakuten.md")) | |
with gr.Tab("拗音"): | |
gr.Markdown(read_text("data/hiragana-yoon.md")) | |
with gr.Tab("片假名"): | |
with gr.Tab("基本"): | |
gr.Markdown(read_text("data/katakana-gojuon.md")) | |
with gr.Tab("濁音、半濁音"): | |
gr.Markdown(read_text("data/katakana-dakuten.md")) | |
with gr.Tab("拗音"): | |
gr.Markdown(read_text("data/katakana-yoon.md")) | |
def register_events(self): | |
start_test_inputs = [self.chk_kana, self.chk_seion, self.chk_dakuon] | |
start_test_inputs += [self.chk_handakuon, self.chk_youon] | |
start_test_outputs = [self.txt_test, self.st_queue, self.n_correct, self.n_total] | |
start_test_outputs += [self.txt_record, self.tabs] | |
start_test_args = gr_args(self.start_test, start_test_inputs, start_test_outputs) | |
check_answer_inputs = [self.txt_test, self.txt_input] | |
check_answer_inputs += [self.n_correct, self.n_total, self.txt_record] | |
check_answer_outputs = [self.txt_input, self.n_correct, self.n_total] | |
check_answer_outputs += [self.txt_info, self.txt_record] | |
check_answer_args = gr_args(self.check_answer, check_answer_inputs, check_answer_outputs) | |
next_char_inputs = [self.st_queue, self.n_correct, self.n_total, self.txt_record] | |
next_char_outputs = [self.txt_test, self.st_queue, self.txt_record, self.tabs] | |
next_char_args = gr_args(self.next_char, next_char_inputs, next_char_outputs) | |
select_outputs = [self.chk_kana, self.chk_seion, self.chk_dakuon] | |
select_outputs += [self.chk_handakuon, self.chk_youon] | |
select_all_args = gr_args(self.select_all, outputs=select_outputs) | |
select_none_args = gr_args(self.select_none, outputs=select_outputs) | |
back_to_settings_args = gr_args(self.back_to_settings, outputs=[self.tabs]) | |
self.btn_start.click(**start_test_args) | |
self.txt_input.submit(**check_answer_args).then(**next_char_args) | |
self.btn_select_all.click(**select_all_args) | |
self.btn_select_none.click(**select_none_args) | |
self.btn_again.click(**start_test_args) | |
self.btn_back_to_settings.click(**back_to_settings_args) | |
def start_test(self, kana, seion, dakuon, handakuon, yoon): | |
category = [*seion, *dakuon, *handakuon, *yoon] | |
use_hiragana = "平假名" in kana | |
use_katakana = "片假名" in kana | |
char_list = list() | |
char_list += [ch for k in category for ch in self.data.hiragana[k]] if use_hiragana else [] | |
char_list += [ch for k in category for ch in self.data.katakana[k]] if use_katakana else [] | |
if not char_list: | |
raise gr.Error("請至少選擇一個類別") | |
random.shuffle(char_list) | |
char = char_list.pop(0) | |
return char, char_list, 0, 0, None, gr.Tabs(selected=1) | |
def check_answer(self, txt_test, txt_input, n_correct, n_total, txt_record): | |
txt_input = str.lower(txt_input).strip() | |
if txt_input in self.data.spell[txt_test]: | |
n_correct += 1 | |
message = "正確" | |
else: | |
answer = " / ".join(self.data.spell[txt_test]) | |
message = f"錯誤,答案為 {answer}" | |
txt_record += f"題目:{txt_test}、正解:{answer}、輸入:{txt_input}\n" | |
n_total += 1 | |
return None, n_correct, n_total, message, txt_record | |
def next_char(self, st_queue, n_correct, n_total, txt_record): | |
if not st_queue: | |
gr.Info("測驗結束!") | |
accuracy = n_correct / n_total | |
txt_record += f"正確率 {accuracy:.2%} ({n_correct}/{n_total})" | |
return None, None, txt_record, gr.Tabs(selected=2) | |
char = list.pop(st_queue, 0) | |
return char, st_queue, txt_record, gr.Tabs(selected=1) | |
def select_all(self): | |
return ( | |
["平假名", "片假名"], | |
self.data.category.seion, | |
self.data.category.dakuon, | |
self.data.category.handakuon, | |
self.data.category.youon, | |
) | |
def select_none(self): | |
return [], [], [], [], [] | |
def back_to_settings(self): | |
return gr.Tabs(selected=0) | |
def launch(self): | |
self.app.launch() | |
class KanaCategory: | |
seion: list[str] | |
dakuon: list[str] | |
handakuon: list[str] | |
youon: list[str] | |
class KanaSpell: | |
category: KanaCategory | |
hiragana: dict[str, list[str]] | |
katakana: dict[str, list[str]] | |
spell: dict[str, list[str]] | |
def load(cls, path: str) -> "KanaSpell": | |
with open(path, "rt", encoding="UTF-8") as fp: | |
data = json.load(fp) | |
data["category"] = KanaCategory(**data["category"]) | |
return cls(**data) | |
def gr_args(fn, inputs=None, outputs=None, show_progress="hidden", **kwargs): | |
return dict(fn=fn, inputs=inputs, outputs=outputs, show_progress=show_progress, **kwargs) | |
def read_text(path): | |
with open(path, "rt", encoding="UTF-8") as fp: | |
return fp.read() | |
if __name__ == "__main__": | |
fire.Fire(KanaQuizApp) | |