initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.apkg
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Environment-dependent path to Maven home directory
|
||||
/mavenHomeManager.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
9
.idea/mandarinanki.iml
generated
Normal file
9
.idea/mandarinanki.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.13" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/mandarinanki.iml" filepath="$PROJECT_DIR$/.idea/mandarinanki.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
123626
cedict_1_0_ts_utf-8_mdbg.txt
Normal file
123626
cedict_1_0_ts_utf-8_mdbg.txt
Normal file
File diff suppressed because it is too large
Load Diff
186
main.py
Normal file
186
main.py
Normal file
@@ -0,0 +1,186 @@
|
||||
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}}<hr id="answer">{{Answer}}',
|
||||
},
|
||||
])
|
||||
|
||||
def generate_card_from_word(word: Word):
|
||||
return generate_card(word.traditional, f"{convert_pinyin(word.pinyin)}<br>{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()
|
||||
Reference in New Issue
Block a user