mooncake030 commited on
Commit
196a03f
·
1 Parent(s): c6905fb

add hira kata options

Browse files
Files changed (4) hide show
  1. app.py +89 -52
  2. favicon.png +0 -0
  3. style.css +3 -0
  4. utils.py +15 -8
app.py CHANGED
@@ -3,7 +3,6 @@ from copy import deepcopy
3
  from pathlib import Path
4
 
5
  import gradio as gr
6
- import numpy as np
7
 
8
  from utils import KanaData, Recognizer
9
 
@@ -18,8 +17,12 @@ class App:
18
  model_char_path="model/chars.txt",
19
  default_kana="あ",
20
  font_name="Kiwi Maru",
 
 
21
  brush_color="#111",
22
  brush_size=15,
 
 
23
  ):
24
  self.brush_color = brush_color
25
  self.brush_size = brush_size
@@ -28,19 +31,32 @@ class App:
28
 
29
  self.kana_data = KanaData.load(kana_data_path)
30
  self.kana_set = {kana for kana in self.kana_data.spell if len(kana) == 1}
 
 
31
 
32
  self.kana_char_dir = Path(kana_char_dir)
33
  self.kana_images = [str(p) for p in self.kana_char_dir.rglob("*")]
34
 
35
  self.font_name = font_name
 
 
36
 
37
  self.default_kana = default_kana
38
  self.default_kana_image = str(self.kana_char_dir / f"{self.default_kana}.png")
39
  roma = self.conv_kana_to_roma(self.default_kana)
40
- self.default_roma = f"{self.default_kana} ({roma})"
 
 
 
41
 
42
  self.init_app()
43
 
 
 
 
 
 
 
44
  def init_app(self):
45
  with self.init_blocks() as self.app:
46
  self.init_states()
@@ -52,10 +68,11 @@ class App:
52
  return gr.Blocks(title="假名手寫練習")
53
 
54
  def init_layout(self):
55
- with gr.Sidebar("練習設定"):
 
 
 
56
  self.init_setting_tab()
57
- with gr.Tab("寫字練習"):
58
- self.init_practice_tab()
59
 
60
  def init_states(self):
61
  self.curr_kana = gr.State(self.default_kana)
@@ -70,41 +87,48 @@ class App:
70
  brush=gr.Brush(self.brush_size, self.brush_color),
71
  eraser=False,
72
  layers=False,
73
- label="寫字板",
74
  )
75
 
76
  with gr.Row():
77
- self.target_txt = gr.Textbox(self.default_roma, label="練習目標")
78
- self.result_txt = gr.Textbox(label="辨識結果")
79
 
80
  with gr.Row():
81
- self.next_btn = gr.Button("下一個字")
82
- self.recog_btn = gr.Button("手寫辨識")
83
 
84
  def init_setting_tab(self):
85
- with gr.Row():
 
 
86
  self.use_assist_chk = gr.Checkbox(True, label="顯示輔助字")
87
- self.use_kana_hint_chk = gr.Checkbox(True, label="提示假名")
88
 
89
  def init_events(self):
90
- recog_kwargs = gr_kwargs(self.do_recog, self.sketchpad, self.result_txt)
 
 
 
91
 
92
- next_inputs = [self.use_assist_chk, self.use_kana_hint_chk, self.curr_kana_list]
 
93
  next_outputs = [self.curr_kana, self.sketchpad, self.curr_kana_image]
94
  next_outputs += [self.target_txt, self.result_txt, self.curr_kana_list]
95
  next_kwargs = gr_kwargs(self.get_rand_kana, next_inputs, next_outputs)
96
 
97
- clear_kwargs = gr_kwargs(self.clear, self.curr_kana_image, self.sketchpad)
98
-
99
- update_inputs = [self.curr_kana, self.use_assist_chk]
100
- update_inputs += [self.use_kana_hint_chk, self.curr_kana_list]
101
  update_outputs = [self.curr_kana, self.sketchpad, self.curr_kana_image]
102
  update_outputs += [self.target_txt, self.curr_kana_list]
103
  update_kwargs = gr_kwargs(self.update, update_inputs, update_outputs)
104
 
105
  self.recog_btn.click(**recog_kwargs)
106
- self.next_btn.click(**next_kwargs)
107
  self.sketchpad.clear(**clear_kwargs)
 
 
 
108
  self.use_assist_chk.change(**update_kwargs)
109
  self.use_kana_hint_chk.change(**update_kwargs)
110
 
@@ -112,11 +136,10 @@ class App:
112
  components = [self.use_assist_chk, self.use_kana_hint_chk]
113
  triggers = [component.change for component in components]
114
 
115
- default_value = [component.value for component in components]
116
  browser_state = gr.BrowserState(
117
- default_value,
118
- storage_key="storage-key",
119
- secret="secret",
120
  )
121
 
122
  self.app.load(inputs=browser_state, outputs=components)(lambda data: data)
@@ -126,7 +149,12 @@ class App:
126
  font = gr.themes.GoogleFont(self.font_name)
127
  text_size = gr.themes.sizes.text_lg
128
  theme = gr.themes.Ocean(font=font, text_size=text_size)
129
- self.app.launch(theme=theme)
 
 
 
 
 
130
 
131
  def conv_kana_to_roma(self, kana):
132
  return self.kana_data.spell[kana][0]
@@ -136,54 +164,63 @@ class App:
136
  random.shuffle(curr_kana_list)
137
  return curr_kana_list
138
 
139
- def get_kana(self, kana: str, use_assist: bool, use_kana_hint: bool, kana_list: list):
 
 
 
 
 
 
 
 
140
  kana_list = kana_list if kana_list else self.init_kana_list()
141
  kana_image = self.kana_char_dir / f"{kana}.png" if kana else kana_list.pop()
142
  kana = Path(kana_image).stem
143
 
 
 
 
 
 
 
 
 
 
 
144
  kana_image = kana_image if use_assist else self.bg_image_path
145
 
 
146
  roma = self.conv_kana_to_roma(kana)
147
- roma = f"{kana} ({roma})" if use_kana_hint else f"{roma}"
148
 
149
  return kana, str(kana_image), roma, kana_list
150
 
151
- def parse_item(self, item):
152
- prob = item["prob"]
153
- char = item["char"]
154
- return f"{char} ({self.conv_kana_to_roma(char)}): {prob:.2%}"
155
 
156
- def is_valid_item(self, item):
157
- if item["prob"] < 1e-2:
158
- return False
159
- if item["char"] not in self.kana_set:
160
- return False
161
- return True
162
 
163
- def do_recog(self, image: dict[str, np.ndarray]):
164
  image = image["layers"][0]
165
  image[image == 0] = 255
166
  image[image != 255] = 0
167
- _, nbest = self.recognizer(image)
168
- return ", ".join(
169
- self.parse_item(item)
170
- for items in nbest
171
- for item in items
172
- if self.is_valid_item(item)
173
- )
174
 
175
- def get_rand_kana(self, assist, kana, chars):
176
- char, img, roma, chars = self.get_kana(None, assist, kana, chars)
177
- return char, img, img, roma, None, chars
 
 
 
178
 
179
  def clear(self, curr_kana_image):
180
  return curr_kana_image
181
 
182
- def update(self, kana, use_assist, use_hint, kana_list):
183
- kana_info = self.get_kana(kana, use_assist, use_hint, kana_list)
184
- kana, image, roma, kana_list = kana_info
185
- return kana, image, image, roma, kana_list
186
-
187
 
188
  def gr_kwargs(fn, inputs=None, outputs=None, show_progress="hidden", **kwargs):
189
  return dict(
 
3
  from pathlib import Path
4
 
5
  import gradio as gr
 
6
 
7
  from utils import KanaData, Recognizer
8
 
 
17
  model_char_path="model/chars.txt",
18
  default_kana="あ",
19
  font_name="Kiwi Maru",
20
+ css_path="style.css",
21
+ favicon_path="favicon.png",
22
  brush_color="#111",
23
  brush_size=15,
24
+ storage_key="kana-write-storage-key",
25
+ storage_secret="kana-write-secret",
26
  ):
27
  self.brush_color = brush_color
28
  self.brush_size = brush_size
 
31
 
32
  self.kana_data = KanaData.load(kana_data_path)
33
  self.kana_set = {kana for kana in self.kana_data.spell if len(kana) == 1}
34
+ self.hira_set = {kana for v in self.kana_data.hiragana.values() for kana in v}
35
+ self.kata_set = {kana for v in self.kana_data.katakana.values() for kana in v}
36
 
37
  self.kana_char_dir = Path(kana_char_dir)
38
  self.kana_images = [str(p) for p in self.kana_char_dir.rglob("*")]
39
 
40
  self.font_name = font_name
41
+ self.css_path = css_path
42
+ self.favicon_path = favicon_path
43
 
44
  self.default_kana = default_kana
45
  self.default_kana_image = str(self.kana_char_dir / f"{self.default_kana}.png")
46
  roma = self.conv_kana_to_roma(self.default_kana)
47
+ self.default_roma = f"平假名 {self.default_kana} ({roma})"
48
+
49
+ self.storage_key = storage_key
50
+ self.storage_secret = storage_secret
51
 
52
  self.init_app()
53
 
54
+ def is_hiragana(self, kana):
55
+ return kana in self.hira_set
56
+
57
+ def is_katakana(self, kana):
58
+ return kana in self.kata_set
59
+
60
  def init_app(self):
61
  with self.init_blocks() as self.app:
62
  self.init_states()
 
68
  return gr.Blocks(title="假名手寫練習")
69
 
70
  def init_layout(self):
71
+ gr.Markdown("# ✍️ 假名手寫練習")
72
+ self.init_practice_tab()
73
+
74
+ with gr.Sidebar():
75
  self.init_setting_tab()
 
 
76
 
77
  def init_states(self):
78
  self.curr_kana = gr.State(self.default_kana)
 
87
  brush=gr.Brush(self.brush_size, self.brush_color),
88
  eraser=False,
89
  layers=False,
90
+ label="🖊️ 寫字板",
91
  )
92
 
93
  with gr.Row():
94
+ self.target_txt = gr.Textbox(self.default_roma, label="🎯 練習目標")
95
+ self.result_txt = gr.Textbox(label="💯 辨識結果")
96
 
97
  with gr.Row():
98
+ self.next_btn = gr.Button("👉 下一個字")
99
+ self.recog_btn = gr.Button("🔎 手寫辨識")
100
 
101
  def init_setting_tab(self):
102
+ with gr.Accordion("⚙️ 練習設定"):
103
+ self.use_hiragana = gr.Checkbox(True, label="練習平假名")
104
+ self.use_katakana = gr.Checkbox(True, label="練習片假名")
105
  self.use_assist_chk = gr.Checkbox(True, label="顯示輔助字")
106
+ self.use_kana_hint_chk = gr.Checkbox(True, label="練習目標提示假名")
107
 
108
  def init_events(self):
109
+ recog_kwargs = gr_kwargs(
110
+ self.recognize, [self.sketchpad, self.curr_kana], self.result_txt
111
+ )
112
+ clear_kwargs = gr_kwargs(self.clear, self.curr_kana_image, self.sketchpad)
113
 
114
+ next_inputs = [self.use_hiragana, self.use_katakana, self.use_assist_chk]
115
+ next_inputs += [self.use_kana_hint_chk, self.curr_kana_list]
116
  next_outputs = [self.curr_kana, self.sketchpad, self.curr_kana_image]
117
  next_outputs += [self.target_txt, self.result_txt, self.curr_kana_list]
118
  next_kwargs = gr_kwargs(self.get_rand_kana, next_inputs, next_outputs)
119
 
120
+ update_inputs = [self.curr_kana, self.use_hiragana, self.use_katakana]
121
+ update_inputs += [self.use_assist_chk, self.use_kana_hint_chk]
122
+ update_inputs += [self.curr_kana_list]
 
123
  update_outputs = [self.curr_kana, self.sketchpad, self.curr_kana_image]
124
  update_outputs += [self.target_txt, self.curr_kana_list]
125
  update_kwargs = gr_kwargs(self.update, update_inputs, update_outputs)
126
 
127
  self.recog_btn.click(**recog_kwargs)
 
128
  self.sketchpad.clear(**clear_kwargs)
129
+ self.next_btn.click(**next_kwargs)
130
+ self.use_hiragana.change(**update_kwargs)
131
+ self.use_katakana.change(**update_kwargs)
132
  self.use_assist_chk.change(**update_kwargs)
133
  self.use_kana_hint_chk.change(**update_kwargs)
134
 
 
136
  components = [self.use_assist_chk, self.use_kana_hint_chk]
137
  triggers = [component.change for component in components]
138
 
 
139
  browser_state = gr.BrowserState(
140
+ [component.value for component in components],
141
+ storage_key=self.storage_key,
142
+ secret=self.storage_secret,
143
  )
144
 
145
  self.app.load(inputs=browser_state, outputs=components)(lambda data: data)
 
149
  font = gr.themes.GoogleFont(self.font_name)
150
  text_size = gr.themes.sizes.text_lg
151
  theme = gr.themes.Ocean(font=font, text_size=text_size)
152
+ self.app.launch(
153
+ theme=theme,
154
+ css_paths=self.css_path,
155
+ footer_links=[None],
156
+ favicon_path=self.favicon_path,
157
+ )
158
 
159
  def conv_kana_to_roma(self, kana):
160
  return self.kana_data.spell[kana][0]
 
164
  random.shuffle(curr_kana_list)
165
  return curr_kana_list
166
 
167
+ def get_kana(
168
+ self,
169
+ kana: str,
170
+ use_hira: bool,
171
+ use_kata: bool,
172
+ use_assist: bool,
173
+ use_kana_hint: bool,
174
+ kana_list: list,
175
+ ):
176
  kana_list = kana_list if kana_list else self.init_kana_list()
177
  kana_image = self.kana_char_dir / f"{kana}.png" if kana else kana_list.pop()
178
  kana = Path(kana_image).stem
179
 
180
+ if use_hira ^ use_kata:
181
+ while self.is_hiragana(kana) and not use_hira:
182
+ kana_list = kana_list if kana_list else self.init_kana_list()
183
+ kana_image = kana_list.pop()
184
+ kana = Path(kana_image).stem
185
+ while self.is_katakana(kana) and not use_kata:
186
+ kana_list = kana_list if kana_list else self.init_kana_list()
187
+ kana_image = kana_list.pop()
188
+ kana = Path(kana_image).stem
189
+
190
  kana_image = kana_image if use_assist else self.bg_image_path
191
 
192
+ kana_type = "平假名" if self.is_hiragana(kana) else "片假名"
193
  roma = self.conv_kana_to_roma(kana)
194
+ roma = f"{kana_type} {kana} ({roma})" if use_kana_hint else f"{kana_type} {roma}"
195
 
196
  return kana, str(kana_image), roma, kana_list
197
 
198
+ def get_rand_kana(self, use_hira, use_kata, use_assist, use_hint, kana_list):
199
+ args = (None, use_hira, use_kata, use_assist, use_hint, kana_list)
200
+ kana, image, roma, kana_list = self.get_kana(*args)
201
+ return kana, image, image, roma, None, kana_list
202
 
203
+ def update(self, kana, use_hira, use_kata, use_assist, use_hint, kana_list):
204
+ args = (kana, use_hira, use_kata, use_assist, use_hint, kana_list)
205
+ kana, image, roma, kana_list = self.get_kana(*args)
206
+ return kana, image, image, roma, kana_list
 
 
207
 
208
+ def recognize(self, image, curr_kana):
209
  image = image["layers"][0]
210
  image[image == 0] = 255
211
  image[image != 255] = 0
212
+ _, results = self.recognizer.recognize(image)
 
 
 
 
 
 
213
 
214
+ return f"正解:{curr_kana} - 辨識:" + ", ".join(
215
+ f"{result.char} ({self.conv_kana_to_roma(result.char)}): {result.prob:.2%}"
216
+ for items in results
217
+ for result in items
218
+ if result.prob > 1e-2 and result.char in self.kana_set
219
+ )
220
 
221
  def clear(self, curr_kana_image):
222
  return curr_kana_image
223
 
 
 
 
 
 
224
 
225
  def gr_kwargs(fn, inputs=None, outputs=None, show_progress="hidden", **kwargs):
226
  return dict(
favicon.png ADDED
style.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .divider {
2
+ display: none;
3
+ }
utils.py CHANGED
@@ -24,6 +24,10 @@ class KanaData(BaseModel):
24
 
25
 
26
  class Recognizer:
 
 
 
 
27
  def __init__(self, model_path, char_list_path, device="CPU", blank="[blank]"):
28
  core = Core()
29
  self.model = core.read_model(model_path)
@@ -38,7 +42,7 @@ class Recognizer:
38
  with open(char_list_path, "rt", encoding="UTF-8") as fp:
39
  self.chars = [blank] + fp.read().split("\n")
40
 
41
- def __call__(self, image, top_k=10):
42
  image = self.preprocess(image, self.input_height, self.input_width)[None, :, :, :]
43
 
44
  for _ in range(2):
@@ -61,7 +65,7 @@ class Recognizer:
61
  # right edge padding
62
  return np.pad(img, ((0, 0), (0, height - h), (0, width - w)), mode="edge")
63
 
64
- def ctc_decode(self, preds, top_k):
65
  index, texts, nbest = 0, list(), list()
66
 
67
  preds_index: np.ndarray = np.argmax(preds, 2)
@@ -88,10 +92,13 @@ class Recognizer:
88
 
89
  # process n-best
90
  probs = self.softmax(preds[i][0])
91
- k_idx = np.argsort(-probs)[:top_k]
92
- k_probs = probs[k_idx]
93
- k_res = [dict(prob=p, char=self.chars[j]) for j, p in zip(k_idx, k_probs)]
94
- nbest.append(k_res)
 
 
 
95
 
96
  text = "".join(char_list)
97
  texts.append(text)
@@ -101,5 +108,5 @@ class Recognizer:
101
  return texts, nbest
102
 
103
  def softmax(self, x):
104
- e_x = np.exp(x - np.max(x))
105
- return e_x / np.sum(e_x, axis=0)
 
24
 
25
 
26
  class Recognizer:
27
+ class Result(BaseModel):
28
+ char: str
29
+ prob: float
30
+
31
  def __init__(self, model_path, char_list_path, device="CPU", blank="[blank]"):
32
  core = Core()
33
  self.model = core.read_model(model_path)
 
42
  with open(char_list_path, "rt", encoding="UTF-8") as fp:
43
  self.chars = [blank] + fp.read().split("\n")
44
 
45
+ def recognize(self, image, top_k=10):
46
  image = self.preprocess(image, self.input_height, self.input_width)[None, :, :, :]
47
 
48
  for _ in range(2):
 
65
  # right edge padding
66
  return np.pad(img, ((0, 0), (0, height - h), (0, width - w)), mode="edge")
67
 
68
+ def ctc_decode(self, preds, top_k) -> tuple[list, list[list[Result]]]:
69
  index, texts, nbest = 0, list(), list()
70
 
71
  preds_index: np.ndarray = np.argmax(preds, 2)
 
92
 
93
  # process n-best
94
  probs = self.softmax(preds[i][0])
95
+ k_indices = np.argsort(-probs)[:top_k]
96
+ k_probs = probs[k_indices]
97
+ k_results = [
98
+ Recognizer.Result(char=self.chars[j], prob=prob)
99
+ for j, prob in zip(k_indices, k_probs)
100
+ ]
101
+ nbest.append(k_results)
102
 
103
  text = "".join(char_list)
104
  texts.append(text)
 
108
  return texts, nbest
109
 
110
  def softmax(self, x):
111
+ exp_x = np.exp(x - np.max(x))
112
+ return exp_x / np.sum(exp_x, axis=0)