import random from copy import deepcopy from pathlib import Path import gradio as gr from utils import KanaData, Recognizer class App: def __init__( self, kana_data_path="data/kana-data.json", kana_char_dir="data/images/kana-chars", bg_image_path="data/images/bg.png", model_xml_path="model/model.xml", model_char_path="model/chars.txt", default_kana="あ", font_name="Kiwi Maru", css_path="style.css", favicon_path="favicon.png", brush_color="#111", brush_size=15, storage_key="kana-write-storage-key", storage_secret="kana-write-secret", ): self.brush_color = brush_color self.brush_size = brush_size self.bg_image_path = bg_image_path self.recognizer = Recognizer(model_xml_path, model_char_path) self.kana_data = KanaData.load(kana_data_path) self.kana_set = {kana for kana in self.kana_data.spell if len(kana) == 1} self.hira_set = {kana for v in self.kana_data.hiragana.values() for kana in v} self.kata_set = {kana for v in self.kana_data.katakana.values() for kana in v} self.kana_char_dir = Path(kana_char_dir) self.kana_images = [str(p) for p in self.kana_char_dir.rglob("*")] self.font_name = font_name self.css_path = css_path self.favicon_path = favicon_path self.default_kana = default_kana self.default_kana_image = str(self.kana_char_dir / f"{self.default_kana}.png") roma = self.conv_kana_to_roma(self.default_kana) self.default_roma = f"平假名 {self.default_kana} ({roma})" self.storage_key = storage_key self.storage_secret = storage_secret self.init_app() def is_hiragana(self, kana): return kana in self.hira_set def is_katakana(self, kana): return kana in self.kata_set def init_app(self): with self.init_blocks() as self.app: self.init_states() self.init_layout() self.init_events() self.init_storage() def init_blocks(self): return gr.Blocks(title="假名手寫練習") def init_layout(self): gr.HTML("

✍️ 假名手寫練習

") self.init_practice_tab() with gr.Sidebar(open=False): self.init_setting_tab() def init_states(self): self.curr_kana = gr.State(self.default_kana) self.curr_kana_list = gr.State(list()) self.curr_kana_image = gr.State(self.default_kana_image) def init_practice_tab(self): with gr.Group(): self.sketchpad = gr.Sketchpad( self.default_kana_image, type="numpy", image_mode="RGB", brush=gr.Brush(self.brush_size, self.brush_color), eraser=False, layers=False, label="🖊️ 寫字板", ) self.target_txt = gr.Textbox(self.default_roma, lines=2, label="🎯 練習目標") self.result_txt = gr.TextArea(lines=2, label="💯 辨識結果") with gr.Row(): self.next_btn = gr.Button("👉 下一個字") self.recog_btn = gr.Button("🔎 手寫辨識") def init_setting_tab(self): with gr.Accordion("⚙️ 練習設定"): self.use_hiragana = gr.Checkbox(True, label="練習平假名") self.use_katakana = gr.Checkbox(True, label="練習片假名") self.use_assist_chk = gr.Checkbox(True, label="顯示輔助字") self.use_kana_hint_chk = gr.Checkbox(True, label="練習目標提示假名") def init_events(self): recog_inputs = [self.sketchpad, self.curr_kana] recog_kwargs = gr_kwargs(self.recognize, recog_inputs, self.result_txt, "minimal") clear_kwargs = gr_kwargs(self.clear, self.curr_kana_image, self.sketchpad) next_inputs = [self.use_hiragana, self.use_katakana, self.use_assist_chk] next_inputs += [self.use_kana_hint_chk, self.curr_kana_list] next_outputs = [self.curr_kana, self.sketchpad, self.curr_kana_image] next_outputs += [self.target_txt, self.result_txt, self.curr_kana_list] next_kwargs = gr_kwargs(self.get_rand_kana, next_inputs, next_outputs) update_inputs = [self.curr_kana, self.use_hiragana, self.use_katakana] update_inputs += [self.use_assist_chk, self.use_kana_hint_chk] update_inputs += [self.curr_kana_list] update_outputs = [self.curr_kana, self.sketchpad, self.curr_kana_image] update_outputs += [self.target_txt, self.curr_kana_list] update_kwargs = gr_kwargs(self.update, update_inputs, update_outputs) self.recog_btn.click(**recog_kwargs) self.sketchpad.clear(**clear_kwargs) self.next_btn.click(**next_kwargs) self.use_hiragana.change(**update_kwargs) self.use_katakana.change(**update_kwargs) self.use_assist_chk.change(**update_kwargs) self.use_kana_hint_chk.change(**update_kwargs) def init_storage(self): components = [self.use_assist_chk, self.use_kana_hint_chk] triggers = [component.change for component in components] browser_state = gr.BrowserState( [component.value for component in components], storage_key=self.storage_key, secret=self.storage_secret, ) self.app.load(inputs=browser_state, outputs=components)(lambda data: data) gr.on(triggers, inputs=components, outputs=browser_state)(lambda *data: data) def launch_app(self): font = gr.themes.GoogleFont(self.font_name) text_size = gr.themes.sizes.text_lg theme = gr.themes.Ocean(font=font, text_size=text_size) self.app.launch( theme=theme, css_paths=self.css_path, footer_links=[None], favicon_path=self.favicon_path, ) def conv_kana_to_roma(self, kana): return self.kana_data.spell[kana][0] def init_kana_list(self): curr_kana_list = deepcopy(self.kana_images) random.shuffle(curr_kana_list) return curr_kana_list def get_kana( self, kana: str, use_hira: bool, use_kata: bool, use_assist: bool, use_kana_hint: bool, kana_list: list, ): kana_list = kana_list if kana_list else self.init_kana_list() kana_image = self.kana_char_dir / f"{kana}.png" if kana else kana_list.pop() kana = Path(kana_image).stem if use_hira ^ use_kata: while self.is_hiragana(kana) and not use_hira: kana_list = kana_list if kana_list else self.init_kana_list() kana_image = kana_list.pop() kana = Path(kana_image).stem while self.is_katakana(kana) and not use_kata: kana_list = kana_list if kana_list else self.init_kana_list() kana_image = kana_list.pop() kana = Path(kana_image).stem kana_image = kana_image if use_assist else self.bg_image_path kana_type = "平假名" if self.is_hiragana(kana) else "片假名" roma = self.conv_kana_to_roma(kana) roma = f"{kana_type} {kana} ({roma})" if use_kana_hint else f"{kana_type} {roma}" return kana, str(kana_image), roma, kana_list def get_rand_kana(self, use_hira, use_kata, use_assist, use_hint, kana_list): args = (None, use_hira, use_kata, use_assist, use_hint, kana_list) kana, image, roma, kana_list = self.get_kana(*args) return kana, image, image, roma, None, kana_list def update(self, kana, use_hira, use_kata, use_assist, use_hint, kana_list): args = (kana, use_hira, use_kata, use_assist, use_hint, kana_list) kana, image, roma, kana_list = self.get_kana(*args) return kana, image, image, roma, kana_list def recognize(self, image, curr_kana): image = image["layers"][0] image[image == 0] = 255 image[image != 255] = 0 _, results = self.recognizer.recognize(image) return f"正解:{curr_kana}\n辨識:" + ", ".join( f"{result.char} ({self.conv_kana_to_roma(result.char)}) {result.prob:.2%}" for items in results for result in items if result.prob > 1e-2 and result.char in self.kana_set ) def clear(self, curr_kana_image): return curr_kana_image def gr_kwargs(fn, inputs=None, outputs=None, show_progress="hidden", **kwargs): return dict( fn=fn, inputs=inputs, outputs=outputs, show_progress=show_progress, **kwargs, ) if __name__ == "__main__": App().launch_app()