lab_autopoetry/vocabulary.py

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