plint

French poetry validator (local mirror of https://gitlab.com/a3nm/plint)
git clone https://a3nm.net/git/plint/
Log | Files | Refs | README

rhyme.py (9505B)


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