Svirepov

forvo-zip2dsl.py

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