My tailor-made suite for studying Japanese
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

239 lines
12 KiB

  1. # -*- coding:utf-8 -*-
  2. import argparse
  3. import json
  4. import sys
  5. import urllib.request
  6. # import UnicodeHelpers
  7. argParser = argparse.ArgumentParser(
  8. description="""Japanese For Me""")
  9. argParser.add_argument('DeckName', type=str,
  10. help='The name of the deck to modify. Add "quotes" if the name has spaces, e.g. "My Deck"')
  11. argParser.add_argument('RomajiFieldName', type=str,
  12. help='The name of the field which has Romaji you want to convert, e.g. "Front"')
  13. argParser.add_argument('--written-field-name', type=str, dest='WrittenFieldName',
  14. help='The name of the field which has what would actually be written in '
  15. 'realistic text (for example, kanji).')
  16. argParser.add_argument('--soft-edit', action='store_const', const=True, default=False, dest='debugSoftEdit',
  17. help='Do not make changes to the deck. Output all changes that would be made. '
  18. 'I recommend running the script with this option first, then look over the results '
  19. 'and confirm whether they are satisfactory.')
  20. argParser.add_argument('--verbose', action='store_const', const=True, default=False, dest='debugVerbose',
  21. help='Output more details about the conversion')
  22. argParser.add_argument('--only-warnings', action='store_const', const=True, default=False, dest='debugOnlyWarnings',
  23. help='Only output warnings and errors. It is recommended to do this in conjuction '
  24. 'with --soft-edit to spot problems before running the script')
  25. # See AnkiConnect docs for available options:
  26. # https://foosoft.net/projects/anki-connect/
  27. def formatAnkiConnectRequest(action, **params):
  28. return {'action': action, 'params': params, 'version': 6}
  29. def invokeAnkiConnect(action, **params):
  30. requestJson = json.dumps(formatAnkiConnectRequest(action, **params)).encode('utf-8')
  31. response = json.load(urllib.request.urlopen(urllib.request.Request('http://localhost:8765', requestJson)))
  32. if len(response) != 2:
  33. raise Exception('response has an unexpected number of fields')
  34. if 'error' not in response:
  35. raise Exception('response is missing required error field')
  36. if 'result' not in response:
  37. raise Exception('response is missing required result field')
  38. if response['error'] is not None:
  39. raise Exception(response['error'])
  40. return response['result']
  41. def getNotes(deckName):
  42. cardsInDeck = invokeAnkiConnect('findCards', query='"deck:{}"'.format(deckName))
  43. if not cardsInDeck:
  44. print("No cards in deck '{}'".format(deckName))
  45. return []
  46. print("{} cards in deck '{}'".format(len(cardsInDeck), deckName))
  47. return invokeAnkiConnect('cardsToNotes', cards = cardsInDeck)
  48. def sanitizeTextForConversion(fieldValue):
  49. # These confuse romkan, and aren't usually a part of the language anyhow
  50. return fieldValue.replace('-', '').replace('’', ' ')
  51. def getDueNotes(deckName):
  52. cardsDue = invokeAnkiConnect('findCards', query='"deck:{}" is:due'.format(deckName))
  53. if not cardsDue:
  54. print("No cards due in deck '{}'".format(deckName))
  55. return []
  56. print("{} cards due in deck '{}'".format(len(cardsDue), deckName))
  57. return invokeAnkiConnect('cardsToNotes', cards = cardsDue)
  58. def testConnection(deckName, fieldToConvert, conversionHintField=None,
  59. shouldEdit=True):
  60. notes = getDueNotes(deckName)
  61. notesInfo = invokeAnkiConnect('notesInfo', notes = notes)
  62. for note in notesInfo:
  63. print(note['fields'])
  64. # def convertNotes(deckName, fieldToConvert, conversionHintField=None,
  65. # shouldEdit=True):
  66. # notes = getNotes(deckName)
  67. # notesInfo = invokeAnkiConnect('notesInfo', notes = notes)
  68. # for currentNote in notesInfo:
  69. # hasWarnings = False
  70. # textToConvert = sanitizeTextForConversion(currentNote['fields'][fieldToConvert]['value'])
  71. # if not textToConvert:
  72. # hasWarnings = True
  73. # print("Error: Empty '{}' found in the following note, which may be malformed:".format(fieldToConvert))
  74. # print(currentNote)
  75. # print("You need to hand-edit this note in order for it to be converted properly.\n"
  76. # "Look over those fields carefully to see if something looks wrong.\n"
  77. # "If you find the problem, resolve it using Anki's Browse feature.")
  78. # print("------------------------------")
  79. # # There's nothing we can do if we don't have a value to convert
  80. # continue
  81. # hint = (sanitizeTextForConversion(currentNote['fields'][conversionHintField]['value'])
  82. # if conversionHintField else None)
  83. # convertedText = None
  84. # if hint:
  85. # hintContainsCharacterTypes = set()
  86. # for char in hint:
  87. # if not char.isalnum():
  88. # # Ignore any punctuation
  89. # continue
  90. # if UnicodeHelpers.is_katakana(char):
  91. # hintContainsCharacterTypes.add("katakana")
  92. # if UnicodeHelpers.is_hiragana(char):
  93. # hintContainsCharacterTypes.add("hiragana")
  94. # if UnicodeHelpers.is_kanji(char):
  95. # hintContainsCharacterTypes.add("kanji")
  96. # if UnicodeHelpers.is_latin(char):
  97. # hintContainsCharacterTypes.add("latin")
  98. # if args.debugVerbose:
  99. # print("{} -> {}".format(hint, hintContainsCharacterTypes))
  100. # if hintContainsCharacterTypes.issubset({"latin"}):
  101. # # There are no Japanese characters; it's probably an initialism or acronym, e.g. 'WWW'
  102. # # Convert to katakana
  103. # convertedText = romkan.to_katakana(textToConvert)
  104. # elif "kanji" not in hintContainsCharacterTypes:
  105. # # The hint is already readable; just use it. This fixes problems where romkan won't
  106. # # convert continuations properly: from input "booringu" the converter outputs
  107. # # ボオリング instead of ボーリング. This also fixes some problems where romaji is
  108. # # erroneous. It doesn't fix cases where there are Kanji and erroneous romaji
  109. # convertedText = hint
  110. # # It's not katakana, or we don't have a hint. Use hiragana
  111. # if not convertedText:
  112. # convertedText = romkan.to_hiragana(textToConvert)
  113. # # No conversion
  114. # if not convertedText:
  115. # hasWarnings = True
  116. # print("ERROR: No conversion for text '{}'".format(textToConvert))
  117. # continue
  118. # suspiciousConversion = False
  119. # for char in convertedText:
  120. # if UnicodeHelpers.is_latin(char):
  121. # print("Warning: conversion did not result in purely Japanese output:\n\t{}\nThere may be "
  122. # "a typo in the romaji, or the romaji format is not understood.".format(convertedText))
  123. # suspiciousConversion = True
  124. # break
  125. # if suspiciousConversion:
  126. # if not hint:
  127. # hasWarnings = True
  128. # print("Could not use edict to find reading because written field not provided")
  129. # else:
  130. # print("Falling back to edict for {}".format(hint))
  131. # hintSanitizedForDictLookup = hint.strip()
  132. # # Remove suru if it's a verbified noun so we can find it in the dictionary
  133. # suruRemoved = False
  134. # # Some wacky character stuff here
  135. # if "(する)" in hint:
  136. # hintSanitizedForDictLookup = hint[:hint.find("(する)")].strip()
  137. # suruRemoved = True
  138. # elif "(する)" in hint:
  139. # hintSanitizedForDictLookup = hint[:hint.find("(する)")].strip()
  140. # suruRemoved = True
  141. # if suruRemoved:
  142. # print("Note: removing '(する)' from hint for dictionary lookup. Now {}"
  143. # .format(hintSanitizedForDictLookup))
  144. # entries = EDictTools.findEntries(hintSanitizedForDictLookup)
  145. # if args.debugVerbose:
  146. # for entry in entries:
  147. # print(entry)
  148. # if entries:
  149. # if len(entries) > 1:
  150. # hasWarnings = True
  151. # print("Warning: multiple entries found:")
  152. # for entry in entries:
  153. # print("\t{}".format(entry))
  154. # print("You may want to edit this note by hand afterwards to select the proper reading.")
  155. # # Pick the first one for now
  156. # convertedText = entries[0].reading
  157. # suspiciousConversion = True
  158. # else:
  159. # print("Using Edict reading: {}".format(entries[0].reading))
  160. # convertedText = entries[0].reading
  161. # suspiciousConversion = False
  162. # if suruRemoved:
  163. # # Add it back
  164. # convertedText += '(する)'
  165. # else:
  166. # hasWarnings = True
  167. # print("No readings found for {}".format(hint))
  168. # if hasWarnings:
  169. # print("Conversion had warnings or errors. Adding romaji to field in case of bad conversion")
  170. # convertedText = convertedText + ' ' + currentNote['fields'][fieldToConvert]['value']
  171. # # When not editing, output results to let the user decide if they are ready for the edit
  172. # if args.debugVerbose or not shouldEdit or hasWarnings:
  173. # if args.debugOnlyWarnings and not hasWarnings:
  174. # pass
  175. # else:
  176. # if hint:
  177. # print("{} -> {} (hint '{}')"
  178. # .format(currentNote['fields'][fieldToConvert]['value'].ljust(15), convertedText.ljust(15), hint))
  179. # else:
  180. # print("'{}' -> '{}'"
  181. # .format(currentNote['fields'][fieldToConvert]['value'], convertedText))
  182. # if hasWarnings:
  183. # print("------------------------------")
  184. # if shouldEdit:
  185. # # Already converted
  186. # if textToConvert == convertedText:
  187. # continue
  188. # # Commit the conversion
  189. # noteFieldUpdate = {"id": currentNote['noteId'], "fields": {fieldToConvert: convertedText}}
  190. # invokeAnkiConnect("updateNoteFields", note = noteFieldUpdate)
  191. if __name__ == '__main__':
  192. print('Japanese For Me: Macoy\'s custom Japanese study tool')
  193. if len(sys.argv) == 1:
  194. argParser.print_help()
  195. exit()
  196. args = argParser.parse_args()
  197. shouldEdit = not args.debugSoftEdit
  198. if shouldEdit:
  199. answer = input("\nWARNING: This script will modify your Anki deck.\n"
  200. "This script's creator is not liable for loss of data!\n"
  201. "If you want to preview changes, run with --soft-edit.\n"
  202. "\nHave you created a backup of your decks? (yes or no) ")
  203. shouldEdit = answer.lower() in ['yes', 'y']
  204. if not shouldEdit and not args.debugSoftEdit:
  205. print("Please back up your data via Anki->File->Export->Anki Collection Package")
  206. else:
  207. testConnection(args.DeckName, args.RomajiFieldName, args.WrittenFieldName,
  208. shouldEdit=shouldEdit and not args.debugSoftEdit)