import re from dataclasses import dataclass import wx import genanki import uuid ### Dictionary stuff line_regex = r"(\S+)\s(\S+)\s\[([^\]]+)\]\s(.+)" @dataclass class Word: simplified: str traditional: str pinyin: str definitions: list[str] def parse_cedict(path: str): words = [] with open(path) as file: for line in file: if line.startswith("#"): continue else: words.append(parse_line(line)) return words def parse_line(line: str): traditional, simplified, pinyin, definitions = re.match(line_regex, line).groups() return Word( simplified, traditional, pinyin, [d for d in definitions.split("/") if d], ) def build_pinyin_index(words: list[Word]): index = {} for word in words: if word.pinyin in index: index[word.pinyin].append(word) else: index[word.pinyin] = [word] return index def build_traditional_index(words: list[Word]): index = {} for word in words: if word.traditional in index: index[word.traditional].append(word) else: index[word.traditional] = [word] return index ## Tone markers pinyinToneMarks = { u'a': u'āáǎà', u'e': u'ēéěè', u'i': u'īíǐì', u'o': u'ōóǒò', u'u': u'ūúǔù', u'ü': u'ǖǘǚǜ', u'A': u'ĀÁǍÀ', u'E': u'ĒÉĚÈ', u'I': u'ĪÍǏÌ', u'O': u'ŌÓǑÒ', u'U': u'ŪÚǓÙ', u'Ü': u'ǕǗǙǛ' } def convert_pinyin_callback(match): tone= int(match.group(3)) vowel = match.group(1) # for multiple vowels, use first one if it is a/e/o, otherwise use second one pos=0 if len(vowel) > 1 and not vowel[0] in 'aeoAEO': pos=1 return vowel[0:pos]+pinyinToneMarks[vowel[pos]][tone-1]+vowel[pos+1:] + match.group(2) def convert_pinyin(s): return re.sub(r'([aeiou]{1,3})(n?g?r?)([12345])', convert_pinyin_callback, s, flags=re.IGNORECASE) ### Anki stuff simple_anki_model = genanki.Model( 1607392319, 'Simple Model', fields=[ {'name': 'Question'}, {'name': 'Answer'}, ], templates=[ { 'name': 'Card 1', 'qfmt': '{{Question}}', 'afmt': '{{FrontSide}}
{{Answer}}', }, ]) def generate_card_from_word(word: Word): return generate_card(word.traditional, f"{convert_pinyin(word.pinyin)}
{word.definitions[0]}") def generate_card(front: str, back: str): return genanki.Note(model=simple_anki_model, fields=[front, back]) def generate_deck(cards: list, name: str): d = genanki.Deck(2059400110, name) for card in cards: d.add_note(card) return d ### UI stuff class MainFrame(wx.Frame): def __init__(self): # set up the dictionary dictionary = parse_cedict("./cedict_1_0_ts_utf-8_mdbg.txt") self.pinyin_idx = build_pinyin_index(dictionary) self.traditional_idx = build_traditional_index(dictionary) # IDs of words selected to be in the deck self.selected_words = [] # IDs of words in results self.result_words = [] super().__init__(parent=None, title='Mandarin Anki', size = (420, 340)) panel = wx.Panel(self) vertical_layout = wx.BoxSizer(wx.VERTICAL) columns_layout = wx.BoxSizer(wx.HORIZONTAL) # search and results self.search = wx.TextCtrl(panel, size = (200, -1)) self.search.Bind(wx.EVT_TEXT, self.OnKeyTyped) self.results = wx.ListBox(panel, size = (200, 200)) self.results.Bind(wx.EVT_LISTBOX, self.OnWordSelected) self.selected = wx.ListBox(panel, size = (200, 200)) self.selected.Bind(wx.EVT_LISTBOX, self.OnWordRemoved) self.deck_name = wx.TextCtrl(panel, size = (200, -1)) self.create_deck = wx.Button(panel, label = "ank!") self.create_deck.Bind(wx.EVT_BUTTON, self.OnAnk) vertical_layout.Add(self.search, 0) vertical_layout.Add(columns_layout) vertical_layout.Add(self.deck_name, 0) vertical_layout.Add(self.create_deck, 0) columns_layout.Add(self.results, 0, wx.EXPAND) columns_layout.Add(self.selected, 0, wx.EXPAND) panel.SetSizer(vertical_layout) self.Show() def OnKeyTyped(self, event): if event.GetString(): if event.GetString() in self.pinyin_idx: matches = self.pinyin_idx[event.GetString()] self.results.Set([f"{w.traditional} {convert_pinyin(w.pinyin)} {w.definitions[0]}" for w in matches]) self.result_words = matches elif event.GetString() in self.traditional_idx: match = self.traditional_idx[event.GetString()] self.result_words = [match] self.results.Set([f"{match.traditional} {convert_pinyin(match.pinyin)} {match.definitions[0]}"]) else: self.results.Set([]) self.result_words = [] def OnWordSelected(self, event): word = self.result_words[event.GetEventObject().GetSelection()] formatted = f"{word.traditional} {convert_pinyin(word.pinyin)} {word.definitions[0]}" if not word in self.selected_words: self.selected.InsertItems([formatted], 0) self.selected_words.insert(0, word) def OnWordRemoved(self, event): idx = event.GetEventObject().GetSelection() self.selected.SetSelection(-1) self.selected.Delete(idx) del self.selected_words[idx] def OnAnk(self, event): cards = [generate_card_from_word(w) for w in self.selected_words] deck = generate_deck(cards, self.deck_name.GetValue()) deck.write_to_file(f"{self.deck_name.GetValue().replace(" ", "-")}.apkg") if __name__ == '__main__': app = wx.App() frame = MainFrame() app.MainLoop()