Svirepov

forvo-zip2dsl.py

Apr 4th, 2022 (edited)
323
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 12.16 KB | None | 0 0
  1. #!/usr/bin/python
  2.  
  3. import sys
  4. import argparse
  5. import zipfile
  6. import re
  7. from urllib.parse import unquote
  8. from collections import defaultdict
  9.  
  10. '''
  11. Changelog
  12.  2022-05-12
  13.    - minor fix: escape ~<>{}#@^ everywhere, not only in headwords
  14.  2022-04-17
  15.    - contents_lang is now optional: there is very little reason to set it
  16.      to anything other than English anyway;
  17.    - two new command line options have been introduced:
  18.      '-s <sub_file>' loads a (zip-specific) set of regex>>replacement pairs;
  19.         for examples of how this works, see the comment at the bottom;
  20.      '-p' normalizes headwords ending in a period or a comma:
  21.         'some phrase[.,]' gets merged into 'some phrase' *IF* the latter
  22.         already exists;
  23.    - file lists are now accepted as well as actual .zip files,
  24.      mainly so that I don't have to keep around all those huge .ZIPs
  25.  
  26.  2022-04-05
  27.    numerous minor fixes: handle URL-encoded sequences, weed out all kinds of
  28.    control characters and other non-printable garbage, trim whitespace
  29. '''
  30.  
  31.  
  32. class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
  33.     pass
  34. parser = argparse.ArgumentParser(
  35.         description='DSL file generator for Forvo/LinguaLibre datasets',
  36.         formatter_class=CustomFormatter,
  37.         allow_abbrev=False,
  38.         epilog='''usage examples:
  39.  %(prog)s -v -p zh.zip 'Forvo Mandarin' Chinese >ForvoMandarin.dsl
  40.  %(prog)s -v -p -s ForvoDutch.sub nl.zip 'Forvo Dutch' Dutch >ForvoDutch.dsl
  41. ''')
  42.  
  43. parser.add_argument('infile', help='an actual zip file or a text file listing its contents')
  44. parser.add_argument('dict_name', help='dictionary name (#NAME)')
  45. parser.add_argument('index_lang', help='#INDEX_LANGUAGE')
  46. parser.add_argument('contents_lang', nargs='?', default='English',
  47.         help='#CONTENTS_LANGUAGE')
  48. parser.add_argument('-v', '--verbose', action='store_true', help='verbose mode')
  49. parser.add_argument('-s', '--sub-file', metavar='SUB_FILE', action='append',
  50.         help='load pattern>>replacement pairs from SUB_FILE; you can stack together as many .sub files as you want: -s file1.sub -s file2.sub...')
  51. parser.add_argument('-p', '--merge-period', action='store_true',
  52.         help="normalize headwords ending in a period or a comma: 'some phrase[.,]' gets merged into 'some phrase' *IF* the latter already exists; this does not affect single-word abbreviations such as etc./ca./ibid.")
  53.  
  54. args = parser.parse_args()
  55. dict_name, merge_period = args.dict_name, args.merge_period
  56.  
  57.  
  58. class TextSub:
  59.     def __init__(self, *args):
  60.         self.subs = []
  61.         for a in args:
  62.             if isinstance(a, str):
  63.                 self.load_file(a)
  64.             else:
  65.                 pat, repl = a
  66.                 self.add_sub(pat, repl, re.IGNORECASE)
  67.     def add_sub(self, pat, repl, flags=0):
  68.         self.subs.append( (re.compile(pat, flags), repl) )
  69.     def load_file(self, filename):
  70.         with open(filename) as f:
  71.             for line in f:
  72.                 if line[0] == '#' or line.isspace():
  73.                     continue
  74.                 if '>>' not in line:
  75.                     raise Exception('{}: malformed regex>>repl line: {}'.format(filename, line))
  76.                 pat, repl = line.rstrip('\r\n').split('>>', 1)
  77.                 self.add_sub(pat, repl, re.IGNORECASE)
  78.     def __call__(self, s):
  79.         for pat, repl in self.subs:
  80.             s = pat.sub(repl, s)
  81.         return s
  82.  
  83. if args.sub_file:
  84.     normalizer = TextSub(*args.sub_file)
  85. else:
  86.     normalizer = None
  87.  
  88.  
  89. garbage = dict.fromkeys(
  90.     '\u0010\u007f\u0080\u0085\u0086\u0088\u008a\u008d\u008f\u0090\u0092\u0093'
  91.     '\u200b\u200c\u200d\u200e\u200f\u202a\u202b\u202c\u202d\u202e\u206f\ufeff')
  92. garbage['\u00a0'] = ' '
  93. del_garbage = str.maketrans(garbage)
  94. esc_headword = str.maketrans({ c : '\\'+c for c in r'\[](){}<>#@~^' })
  95. # {} and <> need to be escaped too: {{ comment }}, <<ref>>
  96. esc_body = str.maketrans({ c : '\\'+c for c in r'\[]{}<>#@~^' })
  97.  
  98.  
  99. def get_zip_contents(infile):
  100.     if infile.lower().endswith('.zip'):
  101.         with zipfile.ZipFile(infile) as zip_ref:
  102.             yield from zip_ref.namelist()
  103.     else:
  104.         with open(infile) as f:
  105.             for line in f:
  106.                 yield line.rstrip('\r\n')
  107.  
  108. words = defaultdict(list)
  109. period_comma = set()
  110. nclips = 0
  111. for filename in get_zip_contents(args.infile):
  112.     if filename[-1] == '/':
  113.         continue
  114.     ext_idx = filename.rindex('.')
  115.     word_idx = filename.rindex('/', 0, ext_idx)
  116.     # even if there is no slash, this will still work as expected
  117.     username_idx = filename.rfind('/', 0, word_idx)
  118.     username = filename[username_idx+1:word_idx]
  119.     word = unquote(filename[word_idx+1:ext_idx]).translate(del_garbage).strip()
  120.     if normalizer:
  121.         word = normalizer(word)
  122.     if not word:
  123.         print('{}: empty headword: {}'.format(dict_name, filename),
  124.                 file=sys.stderr)
  125.         continue
  126.     words[word].append( (filename, username) )
  127.     nclips += 1
  128.     if merge_period and (word[-1] in ',。,' or word[-1] == '.' and ' ' in word):
  129.         period_comma.add(word)
  130.  
  131. if merge_period:
  132.     nmerged = 0
  133.     # reverse-sort the list to account for weird scenarios such as
  134.     #   blah.. -> blah. -> blah
  135.     for w in sorted(period_comma, reverse=True):
  136.         stripped = w[:-1]
  137.         if stripped in words:
  138.             words[stripped] += words[w]
  139.             del words[w]
  140.             nmerged += 1
  141.  
  142. if args.verbose:
  143.     merged_msg = ' ({:,} merged)'.format(nmerged) if merge_period else ''
  144.     print('{}: {:,} sound clips in {:,} articles{}'.format(
  145.         dict_name, nclips, len(words), merged_msg), file=sys.stderr)
  146.  
  147. print(
  148.     '\ufeff'
  149.     '#NAME "{}"\n'
  150.     '#INDEX_LANGUAGE "{}"\n'
  151.     '#CONTENTS_LANGUAGE "{}"\n'.format(
  152.         dict_name, args.index_lang, args.contents_lang))
  153.  
  154. escaped = { k.translate(esc_headword) : v for k, v in words.items() }
  155. for headword, snds in sorted(escaped.items()):
  156.     print(headword)
  157.     for filename, username in sorted(snds, key=lambda item: item[1].lower()):
  158.         print('\t[m1][s]{}[/s] [c slategray][i]{}[/i][/c][/m]'.format(
  159.             filename.translate(esc_body), username.translate(esc_body)))
  160.  
  161.  
  162. '''
  163. ### .sub file examples ###
  164. # syntax:
  165. pattern1>>replacement1
  166. pattern2>>replacement2
  167. ...
  168. # empty lines and lines beginning with '#' are ignored;
  169. # if you need to put '#' at the very beginning of your pattern,
  170. # just use [#] or \# instead; also, make sure the pattern neither contains two
  171. # '>' characters in a row nor ends with a trailing '>' (use [>] as appropriate);
  172. # the 'replacement' part may contain arbitrary text
  173.  
  174.  
  175. ### normalize-whitespace.sub ###
  176. # collapse all consecutive whitespace into a single space;
  177. # note that the following line ends with a SPACE character:
  178. \s+>>
  179. ### end of normalize-whitespace.sub ###
  180.  
  181.  
  182. ### ForvoDutch.sub ###
  183. \s+>>
  184. (?<!^),$>>
  185. \.$>>.
  186. ,([a-z])>>, \1
  187.  
  188. # 's, 't, zo'n, z'n
  189. [’ʼ´`‘"]([stn])\b>>'\1
  190. # d'r, Lieven D'hulst, rouge de l'ouest
  191. \b([dl])[’ʼ´`‘"]>>\1'
  192. ### end of ForvoDutch.sub ###
  193.  
  194.  
  195. ### ForvoEnglish.sub ###
  196. (?<!^),$>>
  197. \.$>>.
  198. ([a-z]),([a-z])>>\1, \2
  199. fi>>fi
  200. fl>>fl
  201. [’ʼ´`‘"′]([dmst]|ll|re|ve|tis|twas|em)\b>>'\1
  202. # o'clock / O'Shea / y'all / y'know
  203. \b([oy])’(?=[a-z])>>\1'
  204. # ma'am / ma'
  205. \b(ma)’\b>>\1'
  206. # '90s
  207. ’([0-9]0s)\b>>'\1
  208. # bangin'
  209. ([a-z]{2,}in)’(?![a-z])>>\1'
  210. # e'er / ne'er / o'er
  211. \b(e|o|ne)’(er)\b>>\1'\2
  212. # nine days' wonder, farmers' market etc.
  213. # this sometimes produces false positives:
  214. #([a-z]{2,}s)’(?=\s+[a-z])>>\1'
  215. # and this does not allow for more than one match per line:
  216. ^([^‘]*?[a-z]{2,}s)’(?=\s+[a-z])>>\1'
  217.  
  218. #\bhis ‘helpful hints' about\b>>his ‘helpful hints’ about
  219. #\bhumbling of the ‘queen of the sciences' is\b>>humbling of the ‘queen of the sciences’ is
  220. #\bart of ‘righteous' slander\b>>art of ‘righteous’ slander
  221.  
  222. "it is the principle of the thing\.’$>>"it is the principle of the thing."
  223. \bain’tcha\b>>ain'tcha
  224. \banderson \.paak\b>>anderson paak
  225. \bdeandre’ bembry\b>>deandre' bembry
  226. \bde’ath\b>>de'ath
  227. \bd’(amalfi|entrecasteaux|urbervilles)\b>>d'\1
  228. \bentr’acte\b>>entr'acte
  229. \bg’night\b>>g'night
  230. \bi knew you were right all along \. never doubted you for second\.$>>i knew you were right all along. never doubted you for a second.
  231. \bjohn o’ groats\b>>john o' groats
  232. \blupita nyong’o\b>>lupita nyong'o
  233. \bm’culloch\b>>m'culloch
  234. \boshkosh b’gosh\b>>oshkosh b'gosh
  235. \bsweet *['’]n low\b>>sweet'n low
  236. ^‘'tis cypher lies beneath\b>>'tis cypher lies beneath
  237. ### end of ForvoEnglish.sub ###
  238.  
  239.  
  240. ### ForvoFrench.sub ###
  241. \b([cdjlmnstç]|qu|jusqu|lorsqu|puisqu|presqu|quelqu|aujourd)[’ʼ´`‘"]>>\1'
  242. # WTF is i’humidité or i'asie
  243. \bi['’´]([aeiouéèh])>>l'\1
  244. [’`‘]>>'
  245. ‐>>-
  246. , >>,
  247. \. \. \.>>...
  248. \.$>>.
  249. (?<!^),$>>
  250. t-il>>-t-il
  251. # doux d'Espagne
  252. \bd ([ae])>>d'\1
  253. # y a-t-il
  254. \by-a-t>>y a-t
  255. \baujourd hui\b>>aujourd'hui
  256. \bemmenez moi\b>>emmenez-moi
  257. \bl '>>l'
  258. ^l >>il
  259. ^a (l'|la|bientôt|très bientôt|demain|plus|mesure|marée basse|quoi bon|partir|bas|bout|ce soir|consommer|mon|son|point|vous|nous|toi|vos|tes|table|quelle heure|combien|gauche)\b>>à \1
  260. # convert back: a l'air (sympa) / à l'air libre
  261. ^à l'air$>>a l'air
  262. # convert back: a l'intention / à l'intention de (qqn)
  263. ^à l'intention$>>a l'intention
  264. \btu a\b>>tu as
  265. \bca\b(?!\.$)>>ça
  266. \bapr[eé]s\b>>après
  267. \b(baptist|berg|boni|brugui|carr|carri|cimeti|courri|cuisini|elzi|f|ferr|ferri|fremi|fr|fresni|fureti|gibeci|goug|gravi|grenouilli|laudoni|lecl|lotbini|lumi|massi|meissoni|messali|orni|rivi|salonni|truch|vassi|vell)ere\b>>\1ère
  268. \b(bouch|bussi|chevali|coug|deshouli|eygali|favi|goug|houli|humi|joncqui|mazi|palli|perri|peyri|savenni|serreudi)eres\b>>\1ères
  269. \bgrandm[eè]re\b>>grand-mère
  270. vayssìere>>vayssière
  271. \btaillefere\b>>tailleferre
  272. \byvresse legere\b>>ivresse légère
  273. \betre\b>>être
  274. saint germain des pres>>saint-germain-des-prés
  275. quelque-chose>>quelque chose
  276. a-peu-près>>à-peu-près
  277. \ba (paris|cannes|castelreng|bouzy|croire|grignoter|présent)\b>>à \1
  278. \b(parler|demander) a\b>>\1 à
  279. \ba l('école|'arrache|'aise|a cave)\b>>à l\1
  280. \b[aà] [eé]milie\b>>à émilie
  281. \bà beau mentir>>a beau mentir
  282. \btout a fait\b>>tout à fait
  283. \bquelle dommage\b>>quel dommage
  284. \b(gratin|ann|chamois|cuv|gicl|sens)ee\b>>\1ée
  285. \bmusee>>musée
  286. \brandonee\b>>randonnée
  287. \bepee\b>>épée
  288. \babimee\b>>abîmée
  289. \bmarne la vallee\b>>marne-la-vallée
  290. \bnous chargons\b>>nous chargeons
  291. \bje mangais\b>>je mangeais
  292. \bombragaient\b>>ombrageaient
  293. \bcontinuerent\b>>continuèrent
  294. \bangoul[eè]me\b>>angoulême
  295. \b(cr|3|6)eme\b>>\1ème
  296. \b(deux|trois|quatr)iemes\b>>\1ièmes
  297. \bpraticant\b>>pratiquant
  298. \bsamuel sorbiére\b>>samuel sorbière
  299. \ben règle génére\b>>en règle générale
  300. \breglelmantee\b>>réglementée
  301. \bmille tonneres\b>>mille tonnerres
  302. ",original=">>""
  303. ,([a-zéèêàç])>>, \1
  304.  
  305. \bqu'est-ce qu-on fait\b>>qu'est-ce qu'on fait
  306. \bqeulqu'un\b>>quelqu'un
  307. \bje serre ia main\b>>je serre la main
  308. \bsi'l vous plaît\b>>s'il vous plaît
  309. # what on earth is this
  310. \babédamebondiou l'étiant pourtant\b>>abédamebondiou i'étiant pourtant
  311. \bau volant de\b>>au volant de
  312. \btenir de quelq'un\b>>tenir de quelqu'un
  313. \bdirac´h\b>>dirac'h
  314. \btiens, voila justement ma'ame baptieret\b>>tiens, voilà justement madame baptieret
  315. \bvoila\b>>voilà
  316. \bj'agis toujours a mon gré\b>>j'agis toujours à mon gré
  317. \bquelle date sommes-nous aujourd'hui ?>>quelle date sommes-nous aujourd'hui ?
  318. \bil peut y a voir plusieurs\b>>il peut y avoir plusieurs
  319. \bil abattit l'arbre a coups de hache\b>>il abattit l'arbre à coups de hache
  320. \bplein de vie et trés amical avec l'homme\b>>plein de vie et très amical avec l'homme
  321. \bavoir été bercé un peu prés du mur\b>>avoir été bercé un peu près du mur
  322. \bnous avons pris un autobus et nous sommes allés a quelque kilomètres d'alger\b>>nous avons pris un autobus et nous sommes allés à quelques kilomètres d'alger
  323. \bavoir une idée derrière une tête\b>>avoir une idée derrière la tête
  324. \bil n'y a a pas lieu de\b>>il n'y a pas lieu de
  325. \bappellation originale controlée\b>>appellation originale contrôlée
  326. \bune chaîne d'information en continu}} qui\b>>une chaîne d'information en continu qui
  327. ### end of ForvoFrench.sub ###
  328. '''
  329.  
Add Comment
Please, Sign In to add comment