Spaces:
Running
Running
import json | |
import os | |
import random | |
from dataclasses import asdict, dataclass | |
import gradio as gr | |
import pandas as pd | |
def launch_app(): | |
app = App() | |
app.launch() | |
class Vocab: | |
kana: str | |
kanji: str | |
meaning: str | |
def info(self): | |
info = (self.kanji, self.kana, self.meaning) | |
return " - ".join(i for i in info if i) | |
class App: | |
def __init__(self): | |
vocab = [[Vocab(**vv) for vv in v] for v in load_json("data/vocab.json")] | |
ch = [f"Ch. {i:02d}" for i, _ in enumerate(vocab, 1)] | |
self.vocab: dict[str, list[Vocab]] = dict(zip(ch, vocab)) | |
self.__init_app() | |
def __init_app(self): | |
with self.__init_blocks() as self.app: | |
self.__init_states() | |
self.__init_layout() | |
self.__init_events() | |
def __init_blocks(self): | |
font = gr.themes.GoogleFont("Noto Sans Mono") | |
text_size = gr.themes.sizes.text_md | |
theme = gr.themes.Soft(font=font, text_size=text_size) | |
return gr.Blocks(title="日文單字小測驗", theme=theme) | |
def __init_states(self): | |
self.question_list = gr.State(None) | |
self.curr_item = gr.State(None) | |
self.correct = gr.State(0) | |
self.total = gr.State(0) | |
self.last_item = gr.State(None) | |
def __init_layout(self): | |
with gr.Tabs() as self.tabs: | |
with gr.Tab("設定", id=0): | |
self.__init_chapters() | |
with gr.Tab("測驗", id=1): | |
self.__init_quiz() | |
with gr.Tab("紀錄", id=2): | |
self.__init_record() | |
with gr.Tab("單字"): | |
self.__init_vocab() | |
def __init_chapters(self): | |
ch = sorted(list(self.vocab.keys())) | |
self.chapters = gr.CheckboxGroup(ch, label="章節") | |
with gr.Row(): | |
self.select_all_btn = gr.Button("全選") | |
self.deselect_all_btn = gr.Button("全不選") | |
with gr.Row(): | |
self.select_last_btn = gr.Button("選擇倒數章節") | |
self.select_rand_btn = gr.Button("選擇隨機章節") | |
self.select_n_ch = gr.Slider(1, len(ch), 6, step=1, label="章節數量") | |
self.start_btn = gr.Button("開始測驗") | |
def __init_quiz(self): | |
desc = "完成設定後按下「開始測驗」" | |
with gr.Row(): | |
self.question = gr.Textbox(placeholder=desc, label="題目", interactive=False) | |
self.score = gr.Textbox("0/0", label="分數", submit_btn=True) | |
with gr.Row(): | |
self.answer = gr.Textbox(label="作答", submit_btn=True) | |
self.audio = gr.Audio( | |
type="filepath", | |
label="發音", | |
autoplay=True, | |
show_download_button=False, | |
show_share_button=False, | |
editable=False, | |
) | |
def __init_record(self): | |
with gr.Row(): | |
self.record = gr.TextArea(show_label=False, lines=15) | |
with gr.Row(): | |
self.again_btn = gr.Button("再次測驗") | |
self.back_to_setting_btn = gr.Button("回到設定") | |
def __init_vocab(self): | |
for ch in self.vocab: | |
with gr.Tab(ch): | |
df = pd.DataFrame([asdict(v) for v in self.vocab[ch]]) | |
df = df[["meaning", "kana", "kanji"]] | |
df = df.rename(columns=dict(meaning="意思", kana="假名", kanji="漢字")) | |
gr.Dataframe(df) | |
def __init_events(self): | |
init_inns = [self.chapters] | |
init_outs = [self.question_list, self.tabs] | |
init_args = gr_args(self.init_questions, init_inns, init_outs) | |
reset_outs = [self.score, self.correct, self.total, self.record, self.audio] | |
reset_args = gr_args(self.reset_score, None, reset_outs) | |
next_inns = [self.question_list] | |
next_outs = [self.curr_item, self.question, self.question_list, self.answer] | |
next_args = gr_args(self.next_question, next_inns, next_outs) | |
check_inns = [self.answer, self.curr_item, self.correct] | |
check_inns += [self.total, self.record, self.question_list] | |
check_outs = [self.score, self.correct, self.total, self.record] | |
check_outs += [self.tabs, self.last_item, self.audio] | |
check_args = gr_args(self.check, check_inns, check_outs) | |
back_args = gr_args(self.back_to_setting, None, self.tabs) | |
read_args = gr_args(self.read_vocab, self.last_item, self.audio) | |
select_all_args = gr_args(self.select_all, None, self.chapters) | |
deselect_all_args = gr_args(self.deselect_all, None, self.chapters) | |
select_last_six_args = gr_args(self.select_last_six, self.select_n_ch, self.chapters) | |
select_rand_six_args = gr_args(self.select_rand_six, self.select_n_ch, self.chapters) | |
self.start_btn.click(**init_args).then(**reset_args).then(**next_args) | |
self.again_btn.click(**init_args).then(**reset_args).then(**next_args) | |
self.answer.submit(**check_args).then(**next_args) | |
self.back_to_setting_btn.click(**back_args) | |
self.score.submit(**read_args) | |
self.select_all_btn.click(**select_all_args) | |
self.deselect_all_btn.click(**deselect_all_args) | |
self.select_last_btn.click(**select_last_six_args) | |
self.select_rand_btn.click(**select_rand_six_args) | |
def init_questions(self, chapters): | |
question_list = [v for ch in chapters for v in self.vocab[ch]] | |
random.shuffle(question_list) | |
selected_tab = gr.Tabs(selected=1) if question_list else gr.Tabs(selected=0) | |
return question_list, selected_tab | |
def next_question(self, question_list: list[Vocab]): | |
if question_list: | |
item = question_list.pop() | |
return item, item.meaning, question_list, None | |
return None, None, None, None | |
def check(self, answer: str, item: Vocab, correct, total, record, question_list): | |
answer = answer.strip() | |
total += 1 | |
if answer == item.kana: | |
correct += 1 | |
info = f"{correct}/{total} - 正確" | |
elif item.kanji is not None and answer == item.kanji: | |
correct += 1 | |
info = f"{correct}/{total} - 正確" | |
else: | |
info = f"錯誤 {answer} => {item.info()}" | |
record = f"{record}{info}\n" | |
info = f"{correct}/{total} - {info}" | |
if not question_list: | |
record = f"{record}此輪得分 - {correct}/{total}" | |
tab_idx = 1 if question_list else 2 | |
tab_idx = gr.Tabs(selected=tab_idx) | |
return info, correct, total, record, tab_idx, item.kana, self.read_vocab(item.kana) | |
def reset_score(self): | |
return "0/0", 0, 0, None, None | |
def back_to_setting(self): | |
return gr.Tabs(selected=0) | |
def read_vocab(self, text): | |
return os.path.join("data", "tts", f"{text}.mp3") | |
def select_all(self): | |
return list(self.vocab.keys()) | |
def deselect_all(self): | |
return list() | |
def select_last_six(self, n): | |
return list(self.vocab.keys())[-n:] | |
def select_rand_six(self, n): | |
return random.sample(list(self.vocab.keys()), n) | |
def launch(self): | |
self.app.launch() | |
def gr_args(fn, inputs=None, outputs=None, show_progress="hidden"): | |
return dict(fn=fn, inputs=inputs, outputs=outputs, show_progress=show_progress) | |
def load_json(path): | |
with open(path, "rt", encoding="UTF-8") as fp: | |
return json.load(fp) | |
if __name__ == "__main__": | |
launch_app() | |