KanaQuiz / app.py
penut85420's picture
refactor app
9a61c89
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()
@dataclass
class KanaCategory:
seion: list[str]
dakuon: list[str]
handakuon: list[str]
youon: list[str]
@dataclass
class KanaSpell:
category: KanaCategory
hiragana: dict[str, list[str]]
katakana: dict[str, list[str]]
spell: dict[str, list[str]]
@classmethod
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)