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.")