190 lines
7.2 KiB
Python
190 lines
7.2 KiB
Python
import json
|
|
from pronouncing import rhymes, syllable_count, phones_for_word, search_stresses, stresses
|
|
from random import choice, sample
|
|
from grammatical_types import (
|
|
Noun,
|
|
TransitiveVerb,
|
|
IntransitiveVerb,
|
|
Adjective,
|
|
Adverb,
|
|
Meter
|
|
)
|
|
|
|
def get_words():
|
|
with open("words.json") as datafile:
|
|
words = json.load(datafile)
|
|
words['nouns'] = set([Noun(w) for w in words['nouns']])
|
|
words['transitive_verbs'] = set([TransitiveVerb(w) for w in words['transitive_verbs']])
|
|
words['intransitive_verbs'] = set([IntransitiveVerb(w) for w in words['intransitive_verbs']])
|
|
words['adjectives'] = set([Adjective(w) for w in words['adjectives']])
|
|
words['adverbs'] = set([Adverb(w) for w in words['adverbs']])
|
|
return words
|
|
|
|
words = get_words()
|
|
|
|
word_types = {
|
|
Noun: "nouns",
|
|
TransitiveVerb: "transitive_verbs",
|
|
IntransitiveVerb: "intransitive_verbs",
|
|
Adjective: "adjectives",
|
|
Adverb: "adverbs"
|
|
}
|
|
|
|
def random_word(word_type, count=1, rhymes_with=None, meter=None, syllables=None):
|
|
"""Returns one or more randomly-chosen words.
|
|
|
|
>>> random_word(Noun)
|
|
'pineapple'
|
|
>>> random_word(Adjective, syllables=4)
|
|
'methodical'
|
|
>>> random_word(Adverb, count=3)
|
|
['flatly', 'gravely', 'gently']
|
|
>>> random_word(Noun, count=4, rhymes_with="sand")
|
|
['grand', 'command', 'band', 'brand']
|
|
|
|
Args:
|
|
word_type (type): `Noun`, `TransitiveVerb`, `IntransitiveVerb`, `Adjective`, or `Adverb`.
|
|
count (int): Optional, default `1`. Number of words to return.
|
|
rhymes_with (str): Optional. When provided, the word(s) must rhyme.
|
|
meter (Meter): Optional. When provided, the word(s) must match the given meter.
|
|
syllables (int): Optional. When provided, the word(s) must have the given number of syllables.
|
|
|
|
Returns:
|
|
word_type: Or a list of `word_type` when count is greater than 1.
|
|
"""
|
|
if word_type not in word_types.keys():
|
|
raise TypeError("random words are not supported for type {}".format(word_type))
|
|
assert isinstance(count, int)
|
|
assert rhymes_with is None or isinstance(rhymes_with, str)
|
|
assert meter is None or isinstance(meter, Meter)
|
|
assert syllables is None or isinstance(syllables, int)
|
|
|
|
conditions = []
|
|
matches = words[word_types[word_type]]
|
|
if rhymes_with:
|
|
conditions.append("rhymes with " + rhymes_with)
|
|
matches = matches.intersection(map(word_type, rhymes(rhymes_with)))
|
|
if meter:
|
|
conditions.append("has meter " + meter)
|
|
matches = matches.intersection(map(word_type, search_stresses('^' + meter + '$')))
|
|
if syllables:
|
|
conditions.append("has {} syllables".format(syllables))
|
|
matches = matches.intersection(word for word in matches if count_syllables(word) == syllables)
|
|
if len(matches) < count:
|
|
target = "a " + word_type.__name__ if count == 1 else str(count) + ' ' + word_type.__name__ + 's'
|
|
raise NoWordError("Couldn't find {} with conditions: {}".format(
|
|
target, " and ".join(conditions)))
|
|
return sample(tuple(matches), count) if count > 1 else choice(tuple(matches))
|
|
|
|
def rhyming_pair(first_word_type, second_word_type, meter=None, syllables=None, attempts=20):
|
|
"""Returns a pair of words which rhyme.
|
|
|
|
>>> rhyming_pair(Noun, Adjective, syllables=2)
|
|
('traffic', 'graphic')
|
|
>>> rhyming_pair(TransitiveVerb, IntransitiveVerb, meter="10")
|
|
('fumble', 'grumble')
|
|
|
|
Args:
|
|
first_word_type (type): `Noun`, `TransitiveVerb`, `IntransitiveVerb`, `Adjective`, or `Adverb`.
|
|
second_word_type (type): `Noun`, `TransitiveVerb`, `IntransitiveVerb`, `Adjective`, or `Adverb`.
|
|
meter (Meter): Optional. A stress pattern for syllables like "01" (ad MIRE) or "100" (SYM phon y)
|
|
syllables (int): Optional. The number of syllables in each word.
|
|
attempts (int): Optional, default is `20`. Number of times to try before giving up.
|
|
This function chooses the first word first, and then looks for a matching second word. If there
|
|
is no match, we need to throw away the first word and try again. Without a limit on the number
|
|
of attempts, this function would potentially search forever, locking up your program.
|
|
The default value of `attempts` should be fine, but you can tune it if you want.
|
|
|
|
Returns:
|
|
(first_word_type, second_word_type)
|
|
"""
|
|
if first_word_type not in word_types.keys():
|
|
raise TypeError("random words are not supported for type {}".format(first_word_type))
|
|
if second_word_type not in word_types.keys():
|
|
raise TypeError("random words are not supported for type {}".format(second_word_type))
|
|
assert meter is None or isinstance(meter, Meter)
|
|
assert syllables is None or isinstance(syllables, int)
|
|
assert isinstance(attempts, int)
|
|
|
|
for i in range(attempts):
|
|
try:
|
|
first = random_word(first_word_type, meter=meter, syllables=syllables)
|
|
second = random_word(second_word_type, rhymes_with=first, meter=meter, syllables=syllables)
|
|
return first, second
|
|
except NoWordError:
|
|
continue
|
|
conditions = []
|
|
if meter:
|
|
conditions.append("has meter " + meter)
|
|
if syllables:
|
|
conditions.append("has {} syllables".format(syllables))
|
|
raise NoWordError("Couldn't find a rhyming {} and {} with {}".format(
|
|
first_word_type.__name__, second_word_type.__name__,
|
|
" and ".join(conditions)))
|
|
|
|
def count_syllables(word):
|
|
"""Counts the number of syllables in a word.
|
|
|
|
>>> count_syllables("bogus")
|
|
2
|
|
|
|
Args:
|
|
word (str)
|
|
|
|
Returns:
|
|
int: The number of syllables in the word.
|
|
"""
|
|
phones = phones_for_word(word)
|
|
if len(phones) > 0:
|
|
return syllable_count(phones[0])
|
|
|
|
def get_meter(word):
|
|
"""Returns the word's meter, or its pattern of stresses when the word is spoken.
|
|
Meter can be used to write poetry with a beat.
|
|
The meter is a string of digits. 1 means primary stress (loudest),
|
|
2 means secondary stress (medium), and 0 means unstressed (quiet).
|
|
For example:
|
|
|
|
>>> get_meter("animal")
|
|
'100'
|
|
>>> get_meter("helicopter")
|
|
'1020'
|
|
>>> get_meter("inexcusable")
|
|
'20100'
|
|
|
|
Args:
|
|
word (str)
|
|
|
|
Returns:
|
|
Meter
|
|
"""
|
|
phones = phones_for_word(word)
|
|
if not phones:
|
|
raise NoWordError("Could not pronounce " + word)
|
|
return Meter(stresses(phones[0]))
|
|
|
|
def starts_with_vowel_sound(text):
|
|
"""Checks whether the text starts with a vowel sound.
|
|
Useful for determining whether to use 'a' or 'an' as an
|
|
indefinite article.
|
|
|
|
>>> starts_with_vowel_sound("horrible hounds")
|
|
False
|
|
>>> starts_with_vowel_sound("awesome owls")
|
|
True
|
|
|
|
Args:
|
|
text (str): One or more words.
|
|
|
|
Returns:
|
|
bool: `True` if the text starts with
|
|
"""
|
|
first_word = text.split(" ")[0]
|
|
vowel_phones = ["AA", "AE", "AH", "AO", "AW", "AY", "EH", "ER", "EY", "IY", "IH", "OW", "OY", "UH", "UW"]
|
|
phones = phones_for_word(first_word)
|
|
return len(phones) > 0 and phones[0].split(' ')[0][:2] in vowel_phones
|
|
|
|
class NoWordError(Exception):
|
|
"This error means no word(s) matched the search."
|
|
|