-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrhyme.py
More file actions
114 lines (88 loc) · 3.4 KB
/
Copy pathrhyme.py
File metadata and controls
114 lines (88 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# rhyme.py
from __future__ import annotations
"""Utilities for rhyme extraction and classification."""
from dataclasses import dataclass
from typing import List, Optional, Set, Tuple
from syllables import CYRILLIC_VOWELS, normalize_text
_PUNCT_STRIP = " \t\r\n.,!?;:'\"()[]{}<>«»—–-…/\\|@#$%^&*_+=~`"
@dataclass(frozen=True)
class RhymeMark:
"""Classification result for a rhyme comparison between two lines."""
kind: str # "good" / "mid" / "bad"
def extract_last_word(line: str) -> str:
"""Return the last word in a line, stripped of punctuation."""
text = (line or "").strip()
if not text:
return ""
text = text.rstrip(_PUNCT_STRIP)
if not text:
return ""
parts = text.split()
if not parts:
return ""
return parts[-1].strip(_PUNCT_STRIP)
def normalize_word_for_rhyme(word: str) -> str:
"""Normalize a word for rhyme comparison."""
normalized = normalize_text(word)
while normalized.endswith(("ь", "ъ")):
normalized = normalized[:-1]
return normalized
def rhyme_tail(word: str, vowels: Optional[Set[str]] = None) -> str:
"""Return the tail of a word starting from its last vowel."""
normalized = normalize_word_for_rhyme(word)
if not normalized:
return ""
vowel_set = vowels or CYRILLIC_VOWELS
last_vowel_index = -1
for idx in range(len(normalized) - 1, -1, -1):
if normalized[idx] in vowel_set:
last_vowel_index = idx
break
if last_vowel_index < 0:
return ""
return normalized[last_vowel_index:]
def last_vowel(word: str, vowels: Optional[Set[str]] = None) -> str:
"""Return the last vowel found in the word."""
normalized = normalize_word_for_rhyme(word)
vowel_set = vowels or CYRILLIC_VOWELS
for idx in range(len(normalized) - 1, -1, -1):
if normalized[idx] in vowel_set:
return normalized[idx]
return ""
def rhyme_marks_for_block(lines: List[str]) -> List[RhymeMark]:
"""Classify rhymes for a block of lyric lines."""
normalized_lines = [line.replace("|", "") for line in lines]
if not normalized_lines:
return []
vowel_set = CYRILLIC_VOWELS
marks: List[RhymeMark] = [RhymeMark("bad") for _ in normalized_lines]
def compare(index_i: int, index_j: int) -> Tuple[RhymeMark, RhymeMark]:
word_i = extract_last_word(normalized_lines[index_i])
word_j = extract_last_word(normalized_lines[index_j])
if not word_i or not word_j:
return (RhymeMark("bad"), RhymeMark("bad"))
tail_i = rhyme_tail(word_i, vowel_set)
tail_j = rhyme_tail(word_j, vowel_set)
if tail_i and tail_j and tail_i == tail_j:
return (RhymeMark("good"), RhymeMark("good"))
vowel_i = last_vowel(word_i, vowel_set)
vowel_j = last_vowel(word_j, vowel_set)
if vowel_i and vowel_j and vowel_i == vowel_j:
return (RhymeMark("mid"), RhymeMark("mid"))
return (RhymeMark("bad"), RhymeMark("bad"))
for idx in range(0, len(normalized_lines), 2):
if idx + 1 < len(normalized_lines):
mark_i, mark_j = compare(idx, idx + 1)
marks[idx] = mark_i
marks[idx + 1] = mark_j
else:
marks[idx] = RhymeMark("bad")
return marks
__all__ = [
"RhymeMark",
"extract_last_word",
"normalize_word_for_rhyme",
"rhyme_tail",
"last_vowel",
"rhyme_marks_for_block",
]