drime

French rhyme dictionary with web and CLI interface
git clone https://a3nm.net/git/drime/
Log | Files | Refs | README

rhyme.py (8440B)


      1 #!/usr/bin/env python3
      2 #encoding: utf8
      3 
      4 import copy
      5 import re
      6 import sys
      7 from pprint import pprint
      8 import frhyme
      9 import functools
     10 from options import default_options
     11 from common import is_vowels, normalize
     12 
     13 # number of possible rhymes to consider
     14 NBEST = 5
     15 # phonetic vowels
     16 vowel = list("Eeaio592O#@y%u()$")
     17 
     18 # use for supposed liaison both in phon and eye
     19 liaison = {
     20     'c': 'k',
     21     'd': 't',
     22     'g': 'k',
     23     'k': 'k',
     24     'p': 'p',
     25     'r': 'R',
     26     's': 'z',
     27     't': 't',
     28     'x': 'z',
     29     'z': 'z',
     30     }
     31 
     32 tolerance = {
     33     'ï': 'i',
     34     "ai": "é",
     35     'm': 'n',
     36     'à': 'a',
     37     'û': 'u',
     38     'ù': 'u'
     39     }
     40 
     41 
     42 class Constraint:
     43   def __init__(self, classical=True, phon=1):
     44     self.phon = phon # minimal number of common suffix phones
     45     self.classical = classical # should we impose classical rhyme rules
     46 
     47   def mmax(self, a, b):
     48     """max, with -1 representing infty"""
     49     if a == -1 or b == -1:
     50       return -1
     51     else:
     52       return max(a, b)
     53 
     54   def restrict(self, c):
     55     """take the max between us and constraint object c"""
     56     if not c:
     57       return
     58     self.phon = self.mmax(self.phon, c.phon)
     59     self.classical = self.classical or c.classical
     60 
     61 class Rhyme:
     62   def apply_mergers(self, phon):
     63     return ''.join([(self.mergers[x] if x in self.mergers.keys()
     64         else x) for x in phon])
     65 
     66   def supposed_liaison(self, x):
     67     if x[-1] in liaison.keys() and self.options['eye_supposed_ok']:
     68       return x[:-1] + liaison[x[-1]]
     69     return x
     70 
     71   def __init__(self, line, constraint=None, mergers=None, options=None, phon=None):
     72     if constraint:
     73       self.constraint = constraint
     74     else:
     75       self.constraint = Constraint()
     76     self.mergers = {}
     77     # length of smallest end-of-verse word in syllables
     78     # will be provided later
     79     self.last_count = 42
     80     if options:
     81       self.options = options
     82     else:
     83       self.options = default_options
     84     if mergers:
     85       for phon_set in mergers:
     86         for pho in phon_set[1:]:
     87           self.mergers[pho] = phon_set[0]
     88     if not phon:
     89       phon = self.lookup(line)
     90     self.phon = set([self.apply_mergers(x) for x in phon])
     91     self.eye = self.supposed_liaison(self.consonant_suffix(line))
     92     self.raw_eye = line
     93     self.old_phon = None
     94     self.old_eye = None
     95     self.old_raw_eye = None
     96     self.old_last_count = None
     97     self.new_rhyme = None
     98 
     99     # store if rhyme is a succession of two vowels
    100     self.double_vocalic = False
    101     l2 = normalize(line)
    102     if len(l2) >= 2:
    103       if is_vowels(l2[-2], with_y=False, with_h=False):
    104         self.double_vocalic = True
    105       if l2[-2] == 'h':
    106         if len(l2) >= 3 and is_vowels(l2[-3], with_y=False, with_h=False):
    107           self.double_vocalic = True
    108     self.old_double_vocalic = False
    109 
    110   def rollback(self):
    111     self.phon = self.old_phon
    112     self.eye = self.old_eye
    113     self.raw_eye = self.old_raw_eye
    114     self.last_count = self.old_last_count
    115     self.double_vocalic = self.old_double_vocalic
    116 
    117   def sufficient_phon(self):
    118     # return the shortest accepted rhymes among old_phon
    119     ok = set()
    120     for p in self.phon:
    121       slen = len(p)
    122       for i in range(len(p)):
    123         if p[-(i+1)] in vowel:
    124           slen = i+1
    125           break
    126       slen = max(slen, self.constraint.phon)
    127       ok.add(p[-slen:])
    128     return ok
    129 
    130   def sufficient_eye_length(self, old_phon=None):
    131     if not self.constraint.classical:
    132       return self.eye, 0 # not classical, nothing required
    133     if ((old_phon >= 2 if old_phon else self.satisfied_phon(2))
    134         or not self.options['poor_eye_required']):
    135       return self.eye, 1
    136     if self.last_count == 1:
    137       return self.eye, 1
    138     if self.options['poor_eye_vocalic_ok'] and self.double_vocalic:
    139       return self.eye, 1
    140     if self.options['poor_eye_supposed_ok']:
    141       return self.eye, 2
    142     else:
    143       return self.raw_eye, 2
    144 
    145   def sufficient_eye(self, old_phon=None):
    146     d, val = self.sufficient_eye_length(old_phon)
    147     if val <= len(d):
    148       return d[-val:]
    149     else:
    150       return d
    151 
    152   def match(self, phon, eye, raw_eye):
    153     """limit our phon and eye to those which match phon and eye and which
    154     respect constraints"""
    155     new_phon = set()
    156     for x in self.phon:
    157       for y in phon:
    158         val = phon_rhyme(x, y)
    159         if val >= self.constraint.phon and self.constraint.phon >= 0:
    160           new_phon.add(x[-val:])
    161     self.phon = new_phon
    162     if self.eye:
    163       val = eye_rhyme(self.eye, eye)
    164       if val == 0:
    165         self.eye = ""
    166       else:
    167         self.eye = self.eye[-val:]
    168     if self.raw_eye:
    169       val = eye_rhyme(self.raw_eye, raw_eye)
    170       if val == 0:
    171         self.raw_eye = ""
    172       else:
    173         self.raw_eye = self.raw_eye[-val:]
    174 
    175   def adjustLastCount(self, v):
    176     self.last_count = min(self.last_count, v)
    177 
    178   def restrict(self, r):
    179     """take the intersection between us and rhyme object r"""
    180     if self.satisfied():
    181       self.old_phon = self.phon
    182       self.old_eye = self.eye
    183       self.old_last_count = self.last_count
    184       self.old_double_vocalic = self.double_vocalic
    185       self.old_raw_eye = self.raw_eye
    186     # lastCount will be applied later
    187     self.constraint.restrict(r.constraint)
    188     self.new_rhyme = r
    189     if not r.double_vocalic:
    190       self.double_vocalic = False # rhyme is ok if all rhymes are double vocalic
    191     self.match(set([self.apply_mergers(x) for x in r.phon]),
    192         self.supposed_liaison(self.consonant_suffix(r.eye)), r.raw_eye)
    193 
    194   def consonant_suffix(self, s):
    195     if not self.options['eye_tolerance_ok']:
    196       return s
    197     for k in tolerance.keys():
    198       if s.endswith(k):
    199         return s[:-(len(k))] + tolerance[k]
    200     return s
    201 
    202   def feed(self, line, constraint=None):
    203     """extend us with a line and a constraint"""
    204     # lastCount is not applied yet
    205     return self.restrict(Rhyme(line, constraint, self.mergers, self.options))
    206 
    207   def satisfied_phon(self, val=None):
    208     if not val:
    209       val = self.constraint.phon
    210     for x in self.phon:
    211       if len(x) >= val:
    212         return True
    213     return False
    214 
    215   def satisfied_eye(self):
    216     d, l = self.sufficient_eye_length()
    217     return len(d) >= l
    218 
    219   def satisfied(self):
    220     return self.satisfied_phon() and self.satisfied_eye()
    221 
    222   def pprint(self):
    223     pprint(self.phon)
    224 
    225   def adjust(self, result, s):
    226     """add liason kludges"""
    227     result2 = copy.deepcopy(result)
    228     # adjust for tolerance with classical rhymes
    229     # e.g. "vautours"/"ours", "estomac"/"Sidrac"
    230     if self.options['phon_supposed_ok']:
    231       # the case 'ent' would lead to trouble for gender
    232       if s[-1] in liaison.keys() and not s.endswith('ent'):
    233         for r in result2:
    234           result.add(r + liaison[s[-1]])
    235           if (s[-1] == 's'):
    236             result.add(r + 's')
    237     return result
    238 
    239   def lookup(self, s):
    240     """lookup the pronunciation of s, adding rime normande kludges"""
    241     result = raw_lookup(s)
    242     if self.options['normande_ok'] and (s.endswith('er') or s.endswith('ers')):
    243       result.add("ER")
    244     return self.adjust(result, s)
    245 
    246 def suffix(x, y):
    247   """length of the longest common suffix of x and y"""
    248   bound = min(len(x), len(y))
    249   for i in range(bound):
    250     a = x[-(1+i)]
    251     b = y[-(1+i)]
    252     if a != b:
    253       return i
    254   return bound
    255 
    256 def phon_rhyme(x, y):
    257   """are x and y acceptable phonetic rhymes?"""
    258   assert(isinstance(x, str))
    259   assert(isinstance(y, str))
    260   nphon = suffix(x, y)
    261   for c in x[-nphon:]:
    262     if c in vowel:
    263       return nphon
    264   return 0
    265 
    266 def eye_rhyme(x, y):
    267   """value of x and y as an eye rhyme"""
    268   return suffix(x, y)
    269 
    270 def concat_couples(a, b):
    271   """the set of x+y for x in a, y in b"""
    272   s = set()
    273   for x in a:
    274     for y in b:
    275       s.add(x + y)
    276   return s
    277 
    278 def raw_lookup(s):
    279   # kludge: take the last three words and concatenate them to take short words
    280   # into account
    281   s = s.split(' ')[-3:]
    282   sets = list(map((lambda a: set([x[1] for x in
    283     frhyme.lookup(escape(a), NBEST)])), s))
    284   return functools.reduce(concat_couples, sets, set(['']))
    285 
    286 #workaround for lexique
    287 def escape(t):
    288   return re.sub('œ', 'oe', re.sub('æ', 'ae', t))
    289 
    290 if __name__ == '__main__':
    291   while True:
    292     line = sys.stdin.readline()
    293     if not line:
    294       break
    295     line = line.lower().strip().split(' ')
    296     if len(line) < 1:
    297       continue
    298     constraint = Constraint()
    299     rhyme = Rhyme(line[0], constraint, self.mergers, self.options)
    300     for x in line[1:]:
    301       rhyme.feed(x, 42)
    302       rhyme.pprint()
    303       if not rhyme.satisfied():
    304         print("No.")
    305         break
    306     if rhyme.satisfied():
    307       print ("Yes.")
    308