Created
September 8, 2017 21:31
-
-
Save drusepth/0cd4798a34ee3303c7944042064411ec to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # <`lm`> if a supernatural chicken was haunting you, would that be a poultrygeist? | |
| # <dru> `lm`: I feel like I could set up a pun generator to find words that are a maximum levenshtein distance away | |
| # from a compound word stem (e.g. poultry --> [polter]geist) | |
| # <dru> I have no idea how to set up the rest of the joke other than manually though | |
| # <dru> "what do you call a [adjective related to poltergeist] [synonym for chicken | chicken] that [verb related | |
| # to poltergeist]?" | |
| # <dru> thanks for the inspiration bbl | |
| # Intended joke outcome: | |
| # Q. What do you call a supernatural chicken haunting you? | |
| # A. A poultrygeist! | |
| # Process: | |
| # 1. Generate core pun word: "chicken" # core_word: "chicken" | |
| # 2. Look up all synonyms/typeOfs for core word # synonyms: ["fowl", "poultry"] | |
| # 3. Find another noun a low L-distance away from one # related_word: "poultergeist" | |
| # - This will probably be the bulk of process time | |
| # 4. Look up adjective that can describe related_word # related_adjective: "supernatural" | |
| # - This step can be ommitted without ruining the joke. | |
| # 5. Look up a verb that is often used to describe related_word # related_verb: "haunt" | |
| # 6. Choose a random joke template using these tokens # template: "Q. What do you call a _ _ _ you? / A. A _!" | |
| # - This will probably be moved to a higher step once multiple templates require different tokens. | |
| # 7. Prepare the punchline / generate our pun! | |
| # 8. Token replacement for the best joke in the world | |
| require 'unirest' | |
| require 'levenshtein-ffi' # using a native C binding here so comparisons are faster | |
| require 'pry' | |
| MASHAPE_API_KEY = ENV['MASHAPE_API_KEY'] | |
| ALLOW_API_CALLS = true | |
| class Log | |
| INFO_ENABLED = true | |
| DEBUG_ENABLED = false | |
| def self.info message; puts "[INFO ] #{message}" if INFO_ENABLED; end | |
| def self.debug message; puts "[DEBUG] #{message}" if DEBUG_ENABLED; end | |
| end | |
| class Dictionary | |
| # TODO: almost all of this -- probably want to use a data structure that makes sorting by Levenshtein distance easier | |
| attr_accessor :words | |
| def initialize(filepath) | |
| # TODO: load real wordlist from file, organize, etc | |
| self.words = %w( | |
| apple bottom carrot duck elephant fauna god hillary ice jackal koala llama monkey neutral | |
| orange poultergeist quiet rest six teem unary wide xiv yes zoo | |
| ) | |
| end | |
| end | |
| class Word | |
| attr_accessor :word, :data | |
| def initialize(word) | |
| self.word = word | |
| populate_data if ALLOW_API_CALLS | |
| end | |
| def common_adjectives | |
| response = Unirest.get( | |
| "https://api.datamuse.com/words?rel_jjb=#{self.word}", | |
| headers: { "Accept" => "application/json" } | |
| ).body | |
| # TODO: need a new datasource with more adjectives -- this one doesn't have ANY FOR POULTERGEIST! | |
| response = mock_poultergeist_response if self.word == 'poultergeist' | |
| # Pull out just the words; we don't care about frequency scores | |
| response.map { |word_object| word_object['word'] } | |
| end | |
| def common_verbs | |
| # TODO: all of this -- probably need to build out some custom endpoints on Retort to fetch data for: | |
| # "give me all verbs we've seen a given noun doing". | |
| ['haunt', 'spook', 'chill', 'follow'] | |
| end | |
| private | |
| def populate_data | |
| Log.info("Making API request for #{self.word} word data") | |
| self.data = fetch_word_data | |
| Log.debug("Response: #{self.data}") | |
| mock_api_response if self.data == "Too many requests. Please try again." | |
| end | |
| def fetch_word_data | |
| res = Unirest.get "https://wordsapiv1.p.mashape.com/words/#{self.word}", | |
| headers: { | |
| "X-Mashape-Key" => MASHAPE_API_KEY, | |
| "Accept" => "application/json" | |
| } | |
| res.body | |
| end | |
| def mock_api_response | |
| Log.debug "API is overloaded; falling back on mocks for #{self.word}." | |
| case self.word | |
| when 'chicken'; self.data = mock_chicken_response | |
| else; Log.debug "No mock defined for #{self.word}." | |
| end | |
| end | |
| def mock_chicken_response | |
| { | |
| "word": "chicken", | |
| "results": [ | |
| { | |
| "definition": "a domestic fowl bred for flesh or eggs; believed to have been developed from the red jungle fowl", | |
| "partOfSpeech": "noun", | |
| "synonyms": [ | |
| "gallus gallus" | |
| ], | |
| "typeOf": [ | |
| "fowl", | |
| "poultry", | |
| "domestic fowl" | |
| ], | |
| "hasTypes": [ | |
| "hen", | |
| "capon", | |
| "chick", | |
| "dominick", | |
| "biddy", | |
| "spring chicken", | |
| "cock", | |
| "rhode island red", | |
| "rooster", | |
| "dominique", | |
| "orpington" | |
| ], | |
| "hasParts": [ | |
| "poulet", | |
| "volaille" | |
| ] | |
| }, | |
| { | |
| "definition": "easily frightened", | |
| "partOfSpeech": "adjective", | |
| "synonyms": [ | |
| "chickenhearted", | |
| "lily-livered", | |
| "white-livered", | |
| "yellow", | |
| "yellow-bellied" | |
| ], | |
| "usageOf": [ | |
| "colloquialism" | |
| ], | |
| "similarTo": [ | |
| "fearful", | |
| "cowardly" | |
| ] | |
| }, | |
| { | |
| "definition": "a person who lacks confidence, is irresolute and wishy-washy", | |
| "partOfSpeech": "noun", | |
| "synonyms": [ | |
| "crybaby", | |
| "wimp" | |
| ], | |
| "typeOf": [ | |
| "doormat", | |
| "weakling", | |
| "wuss" | |
| ] | |
| }, | |
| { | |
| "definition": "the flesh of a chicken used for food", | |
| "partOfSpeech": "noun", | |
| "synonyms": [ | |
| "poulet", | |
| "volaille" | |
| ], | |
| "typeOf": [ | |
| "poultry" | |
| ], | |
| "hasTypes": [ | |
| "pullet", | |
| "fryer", | |
| "roaster", | |
| "frier", | |
| "spatchcock", | |
| "hen", | |
| "capon", | |
| "broiler" | |
| ], | |
| "hasParts": [ | |
| "chicken wing", | |
| "white meat", | |
| "breast" | |
| ], | |
| "partOf": [ | |
| "gallus gallus" | |
| ] | |
| }, | |
| { | |
| "definition": "a foolhardy competition; a dangerous activity that is continued until one competitor becomes afraid and stops", | |
| "partOfSpeech": "noun", | |
| "typeOf": [ | |
| "contest", | |
| "competition" | |
| ] | |
| } | |
| ], | |
| "syllables": { | |
| "count": 2, | |
| "list": [ | |
| "chick", | |
| "en" | |
| ] | |
| }, | |
| "pronunciation": { | |
| "all": "'ʧɪkən" | |
| }, | |
| "frequency": 4.8 | |
| } | |
| end | |
| def mock_poultergeist_response | |
| [ | |
| { 'word' => 'friendly', 'score' => 1.0 }, | |
| { 'word' => 'spooky', 'score' => 0.8 }, | |
| { 'word' => 'supernatural', 'score' => 0.5 }, | |
| { 'word' => 'blazing', 'score' => 420 } | |
| ] | |
| end | |
| end | |
| class Verb < Word | |
| attr_accessor :word | |
| def initialize(word) | |
| self.word = word | |
| end | |
| def present_participle_form | |
| self.word + 'ing' #lol | |
| end | |
| end | |
| # 1. Generate core pun word: "chicken" # core_word: "chicken" | |
| core_word = Word.new("chicken") # or Word.new(random_noun) | |
| # 2. Look up all synonyms/typeOfs for core word # synonyms: ["fowl", "poultry"] | |
| # synonyms = core_word.data['results'] | |
| # .select { |word| word['partOfSpeech'] == 'noun' } | |
| # .flat_map { |word| word['typeOf'] } | |
| selected_synonym = "poultry" # todo loop over each synonym so we can fall back / continue | |
| # 3. Find another noun a low L-distance away from one # related_word: "poultergeist" | |
| dictionary = Dictionary.new("one_day_ill_use_a_real_wordlist.txt") | |
| related_word_raw_string = dictionary.words | |
| .sort_by do |word| | |
| minimum_distance = selected_synonym.length | |
| iteratable_chunk_size = [selected_synonym.length, word.length].min | |
| word.chars.each_cons(iteratable_chunk_size) do |character_sequence| | |
| levenshtein_distance = Levenshtein.distance(character_sequence.join, selected_synonym) | |
| Log.debug "\t#{character_sequence.join} => #{levenshtein_distance} (minimum #{minimum_distance} for #{word})" | |
| if levenshtein_distance < minimum_distance | |
| minimum_distance = levenshtein_distance | |
| end | |
| end | |
| minimum_distance | |
| end | |
| .first | |
| related_word = Word.new(related_word_raw_string) | |
| # 4. Look up adjective that can describe related_word # related_adjective: "supernatural" | |
| related_adjective = related_word.common_adjectives.sample | |
| # 5. Look up a verb that is often used to describe related_word # related_verb: "haunt" | |
| related_verb = Verb.new(related_word.common_verbs.sample).present_participle_form | |
| # 6. Choose a random joke template | |
| joke_template = [ | |
| "Q. What do you call a <related_adjective> <core_word> <related_verb> you?", | |
| "A. A <pun>!" | |
| ].join("\n") | |
| # 7. Prepare the PUNchline # punchline: "poultrygeist" | |
| # 7a. Find the related_word subsequence most similar to our selected_synonym | |
| iteratable_chunk_size = [selected_synonym.length, related_word.word.length].min | |
| most_similar_subsequence = related_word.word[0, iteratable_chunk_size] | |
| best_subsequence_score = selected_synonym.length | |
| related_word.word.chars.each_cons(iteratable_chunk_size) do |character_sequence| | |
| levenshtein_distance = Levenshtein.distance(character_sequence.join, selected_synonym) | |
| if levenshtein_distance < best_subsequence_score | |
| best_subsequence_score = levenshtein_distance | |
| most_similar_subsequence = character_sequence.join | |
| end | |
| end | |
| # 7b. After finding the sequence to swap our pun into, replace it inline with our selected_synonym | |
| pun = related_word.word.dup | |
| pun[most_similar_subsequence] = selected_synonym | |
| # 8. Token replacement for the best joke in the world | |
| joke = joke_template | |
| .gsub('<related_adjective>', related_adjective) | |
| .gsub('<core_word>', core_word.word) | |
| .gsub('<related_verb>', related_verb) | |
| .gsub('<pun>', pun) | |
| puts "-"*20, joke |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment