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