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() @dataclass 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()