word-phone / tool.py
patruff's picture
Upload tool
fc192c6 verified
from smolagents.tools import Tool
import string
import json
import pronouncing
class WordPhoneTool(Tool):
name = "word_phonetic_analyzer"
description = """Analyzes word pronunciation using CMU dictionary and custom pronunciations to get phonemes, syllables, and stress patterns.
Can also compare two words for phonetic similarity and rhyming."""
inputs = {'word': {'type': 'string', 'description': 'Primary word to analyze for pronunciation patterns'}, 'compare_to': {'type': 'string', 'description': 'Optional word to compare against for similarity scoring', 'nullable': True}, 'custom_phones': {'type': 'object', 'description': 'Optional dictionary of custom word pronunciations', 'nullable': True}}
output_type = "string"
VOWEL_REF = "AH,UH,AX|AE,EH|IY,IH|AO,AA|UW,UH|AY,EY|OW,AO|AW,AO|OY,OW|ER,AXR"
def _get_vowel_groups(self):
groups = []
group_strs = self.VOWEL_REF.split("|")
for group_str in group_strs:
groups.append(group_str.split(","))
return groups
def _get_word_phones(self, word, custom_phones=None):
"""Get phones for a word, checking custom dictionary first."""
if custom_phones and word in custom_phones:
return custom_phones[word]["primary_phones"]
import pronouncing
phones = pronouncing.phones_for_word(word)
return phones[0] if phones else None
def _get_last_syllable(self, phones):
last_vowel_idx = -1
last_vowel = None
vowel_groups = self._get_vowel_groups()
for i in range(len(phones)):
phone = phones[i]
base_phone = ""
for j in range(len(phone)):
char = phone[j]
if char not in "012":
base_phone += char
for group in vowel_groups:
if base_phone in group:
last_vowel_idx = i
last_vowel = base_phone
break
if last_vowel_idx == -1:
return None, []
remaining = []
for i in range(last_vowel_idx + 1, len(phones)):
remaining.append(phones[i])
return last_vowel, remaining
def _strip_stress(self, phones):
result = []
for phone in phones:
stripped = ""
for char in phone:
if char not in "012":
stripped += char
result.append(stripped)
return result
def _vowels_match(self, v1, v2):
v1_stripped = ""
v2_stripped = ""
for char in v1:
if char not in "012":
v1_stripped += char
for char in v2:
if char not in "012":
v2_stripped += char
if v1_stripped == v2_stripped:
return True
vowel_groups = self._get_vowel_groups()
for group in vowel_groups:
if v1_stripped in group and v2_stripped in group:
return True
return False
def _calculate_similarity(self, word1, phones1, word2, phones2):
import pronouncing
# Initialize variables before use
last_vowel1 = None
last_vowel2 = None
word1_end = []
word2_end = []
matched = 0
common_length = 0
end1_clean = []
end2_clean = []
i = 0 # Initialize i for loop variable
phone_list1 = phones1.split()
phone_list2 = phones2.split()
# Get last syllable components
result1 = self._get_last_syllable(phone_list1)
result2 = self._get_last_syllable(phone_list2)
last_vowel1, word1_end = result1
last_vowel2, word2_end = result2
# Calculate length similarity score first
phone_diff = abs(len(phone_list1) - len(phone_list2))
max_phones = max(len(phone_list1), len(phone_list2))
length_score = 1.0 if phone_diff == 0 else 1.0 - (phone_diff / max_phones)
# Calculate rhyme score (most important)
rhyme_score = 0.0
if last_vowel1 and last_vowel2:
if self._vowels_match(last_vowel1, last_vowel2):
end1_clean = self._strip_stress(word1_end)
end2_clean = self._strip_stress(word2_end)
if end1_clean == end2_clean:
rhyme_score = 1.0 # Perfect rhyme, capped at 1.0
else:
# Partial rhyme based on ending similarity
common_length = min(len(end1_clean), len(end2_clean))
matched = 0
for i in range(common_length):
if end1_clean[i] == end2_clean[i]:
matched += 1
rhyme_score = 0.6 * (matched / max(len(end1_clean), len(end2_clean)))
# Calculate stress pattern similarity
stress1 = pronouncing.stresses(phones1)
stress2 = pronouncing.stresses(phones2)
stress_score = 1.0 if stress1 == stress2 else 0.5
# Weighted combination prioritizing rhyming and length
total_similarity = (
(rhyme_score * 0.6) + # Rhyming most important (60%)
(length_score * 0.3) + # Length similarity next (30%)
(stress_score * 0.1) # Stress pattern least important (10%)
)
# Ensure total similarity is capped at 1.0
total_similarity = min(1.0, total_similarity)
return {
"similarity": round(total_similarity, 3),
"rhyme_score": round(rhyme_score, 3),
"length_score": round(length_score, 3),
"stress_score": round(stress_score, 3),
"phone_length_difference": phone_diff
}
def forward(self, word, compare_to=None, custom_phones=None):
import json
import string
import pronouncing
# Initialize variables before use
word_last_vowel = None
compare_last_vowel = None
word_end = []
compare_end = []
is_rhyme = False
word_clean = word.lower()
word_clean = word_clean.strip(string.punctuation)
primary_phones = self._get_word_phones(word_clean, custom_phones)
if not primary_phones:
result = {
'word': word_clean,
'found': False,
'error': 'Word not found in dictionary or custom phones'
}
return json.dumps(result, indent=2)
result = {
'word': word_clean,
'found': True,
'analysis': {
'syllable_count': pronouncing.syllable_count(primary_phones),
'phones': primary_phones.split(),
'stresses': pronouncing.stresses(primary_phones),
'phone_count': len(primary_phones.split())
}
}
if compare_to:
compare_clean = compare_to.lower()
compare_clean = compare_clean.strip(string.punctuation)
compare_phones = self._get_word_phones(compare_clean, custom_phones)
if not compare_phones:
result['comparison'] = {
'error': f'Comparison word "{compare_clean}" not found in dictionary or custom phones'
}
else:
# Get rhyme components
word_result = self._get_last_syllable(primary_phones.split())
compare_result = self._get_last_syllable(compare_phones.split())
word_last_vowel, word_end = word_result
compare_last_vowel, compare_end = compare_result
# Calculate if words rhyme
if word_last_vowel and compare_last_vowel:
if self._vowels_match(word_last_vowel, compare_last_vowel):
word_end_clean = self._strip_stress(word_end)
compare_end_clean = self._strip_stress(compare_end)
if word_end_clean == compare_end_clean:
is_rhyme = True
# Calculate detailed comparison stats
word_syl_count = pronouncing.syllable_count(primary_phones)
compare_syl_count = pronouncing.syllable_count(compare_phones)
result['comparison'] = {
'word': compare_clean,
'analysis': {
'syllable_count': compare_syl_count,
'phones': compare_phones.split(),
'stresses': pronouncing.stresses(compare_phones),
'phone_count': len(compare_phones.split())
},
'comparison_stats': {
'is_rhyme': is_rhyme,
'same_syllable_count': word_syl_count == compare_syl_count,
'same_stress_pattern': pronouncing.stresses(primary_phones) == pronouncing.stresses(compare_phones),
'syllable_difference': abs(word_syl_count - compare_syl_count),
'phone_difference': abs(len(primary_phones.split()) - len(compare_phones.split()))
}
}
# Calculate detailed similarity scores
similarity_result = self._calculate_similarity(
word_clean, primary_phones,
compare_clean, compare_phones
)
result['similarity'] = similarity_result
return json.dumps(result, indent=2)
def __init__(self, *args, **kwargs):
self.is_initialized = False