module Poefy::Generation
Public Instance Methods
poem(poetic_form = @poetic_form)
click to toggle source
Generate specific poem types.
# File lib/poefy/generation.rb, line 17 def poem poetic_form = @poetic_form # Can't do much if the database doesn't exist. raise Poefy::MissingDatabase unless @corpus.exists? # Validate the poetic form hash. raise ArgumentError, 'Argument must be a hash' unless poetic_form.is_a?(Hash) poetic_form = validate_poetic_form poetic_form poetic_form = @poetic_form.merge poetic_form # Make sure the hash contains ':form' or ':rhyme' keys. # Make sure the rhyme token is not empty. rhyme = poetic_form[:rhyme] if !(rhyme || poetic_form[:form]) raise Poefy::MissingFormOrRhyme elsif (rhyme && rhyme.count == 1 && rhyme.first[:token] == ' ') raise Poefy::MissingFormOrRhyme end # Loop until we find a valid poem. # There are cases where valid permutations are not able to be # genned on the first try, so keep trying a few more times. output, count_down = nil, 10 loop do output = gen_poem_using_conditions poetic_form break if !output.nil? || count_down == 0 count_down -= 1 end # Return nil if poem could not be created. return nil if (output.nil? or output == [] or output == ['']) # Indent the output using the :indent string. output = do_indent(output, get_poetic_form_indent(poetic_form)) # Append blank lines to the end if the :rhyme demands it. rhyme = tokenise_rhyme get_poetic_form_rhyme poetic_form (output.length...rhyme.length).each do |i| output[i] = '' end output end
poem!(poetic_form = @poetic_form)
click to toggle source
Same as the above method, but swallow any errors.
# File lib/poefy/generation.rb, line 63 def poem! poetic_form = @poetic_form begin poem poetic_form rescue Poefy::Error nil end end
Private Instance Methods
gen_poem_using_conditions(poetic_form = @poetic_form)
click to toggle source
Use the constraints in 'poetic_form' to generate a poem.
# File lib/poefy/generation.rb, line 74 def gen_poem_using_conditions poetic_form = @poetic_form poetic_form = poetic_form_full poetic_form poetic_form = validate_poetic_form poetic_form # Tokenise the rhyme string, and return [] if invalid. tokenised_rhyme = tokenise_rhyme poetic_form[:rhyme] if tokenised_rhyme == [] raise Poefy::RhymeError end # Expand poetic_form[:transform], if there's just one element. if poetic_form[:transform] and !poetic_form[:transform].respond_to?(:each) poetic_form[:transform] = fill_hash poetic_form[:transform], 1..tokenised_rhyme.count end # Add acrostic to the regex, if necessary. if poetic_form[:acrostic_x] acrostic_opts = acrostic_x(poetic_form[:acrostic_x]) poetic_form[:regex] = merge_hashes poetic_form[:regex], acrostic_opts[:regex] poetic_form[:transform] = merge_hashes acrostic_opts[:transform], poetic_form[:transform] elsif poetic_form[:acrostic] poetic_form[:regex] = merge_hashes poetic_form[:regex], acrostic(poetic_form[:acrostic]) end # Add line number as ':line' in each element's hash. by_line = conditions_by_line(tokenised_rhyme, poetic_form) # Remove any regexes that are empty arrays []. by_line.each do |i| i.delete(:regex) if i[:regex] == [] end # If the poetic_form[:proper] option is true, we're going to # need to add additional regex conditions to the first and # last lines. # This is pretty easy for non-repeating lines, but for refrains # we need to apply the regex for all occurrences. if poetic_form[:proper] # Turn the regex into an array, if it isn't already. # Then add the banned starting words. line_conds = [*by_line[0][:regex]] line_conds += [/^((?!and).)/i] line_conds += [/^((?!but).)/i] line_conds += [/^((?!or).)/i] line_conds += [/^((?!nor).)/i] line_conds += [/^((?!yet).)/i] by_line[0][:regex] = line_conds # Same for the last line. line_conds = [*by_line[tokenised_rhyme.count-1][:regex]] line_conds += [/[\.?!]$/] by_line[tokenised_rhyme.count-1][:regex] = line_conds # Get all refrains and group them. refrains = by_line.select do |i| i[:refrain] end.group_by do |i| i[:refrain] end # Now make each refrain :regex be an array of all. refrain_regex = Hash.new { |h,k| h[k] = [] } refrains.each do |key, value| refrain_regex[key] = value.map do |i| i[:regex] end.flatten.compact end # Go through [by_line] and update each :regex. by_line.each do |i| if not refrain_regex[i[:rhyme]].empty? i[:regex] = refrain_regex[i[:rhyme]] end end end # Now we have ':line', so we can break the array order. # Let's get rid of empty lines, and group by the rhyme letter. conditions_by_rhyme = by_line.reject do |i| i[:rhyme] == ' ' end.group_by do |i| i[:rhyme_letter] end # Okay, this is great. But if we're making villanelles we'll need to # duplicate refrain lines. So we won't need unique rhymes for those. # So make a distinct set of lines conditions, still grouped by rhyme. # This will be the same as [conditions_by_rhyme], except duplicate # lines are removed. (In string input, these are lines with # capitals and numbers: i.e. A1, B2) # It will keep the condition hash of only the first refrain line. distinct_line_conds = Hash.new { |h,k| h[k] = [] } conditions_by_rhyme.each do |key, values| refrains = [] values.each do |v| if !v[:refrain] distinct_line_conds[key] << v elsif !refrains.include?(v[:refrain]) refrains << v[:refrain] distinct_line_conds[key] << v end end end # Right, let's now loop through each rhyme group and find all from # the database where the number of lines can be fulfilled. # First, get the order of rhymes, from most to least. distinct_line_conds = distinct_line_conds.sort_by{ |k,v| v.count }.reverse # This will store the rhymes that have already been used in the poem. # This is so we do not duplicate rhymes between distinct rhyme letters. rhymes_already_used = [] # This is the final set of lines. all_lines = [] # Loop through each rhyme group to find lines that satisfy the conditions. distinct_line_conds.each do |rhyme_letter, line_conds| # The conditions that will be passed to '#conditional_sample'. # This is an array of procs, one for each line. conditions = line_conds.map do |cond| proc { |arr, elem| diff_end(arr, elem) and validate_line(elem, cond)} end # Get all rhymes from the database with at least as many final # words as there are lines to be matched. rhymes = nil # If all the lines include a 'syllable' condition, # then we can specify to only query for matching lines. begin min_max = syllable_min_max line_conds rescue raise Poefy::SyllableError end rhymes = @corpus.rhymes_by_count(line_conds.count, min_max) # Get just the rhyme part of the hash. rhymes = rhymes.map{ |i| i['rhyme'] } rhymes = rhymes - rhymes_already_used # For each rhyme, get all lines and try to sastify all conditions. out = [] rhymes.shuffle.each do |rhyme| out = try_rhyme(conditions, rhyme, min_max, line_conds) break if !out.empty? end if out.empty? msg = 'ERROR: Not enough rhyming lines in the input.' if poetic_form[:proper] msg += "\n Perhaps try again using the -p option." end raise Poefy::NotEnoughData.new(msg) end rhymes_already_used << out.first['rhyme'] # Add the line number back to the array. line_conds.count.times do |i| out[i]['line_number'] = line_conds[i][:line] end out.each do |i| all_lines << i end end # Transpose lines to their actual location. poem_lines = [] all_lines.each do |line| poem_lines[line['line_number'] - 1] = line['line'].dup end # Go back to the [by_line] array and find all the refrain line nos. refrains = Hash.new { |h,k| h[k] = [] } by_line.reject{ |i| i[:rhyme] == ' ' }.each do |line| if line[:refrain] refrains[line[:refrain]] << line[:line] end end refrains.keys.each do |k| refrains[k].sort! end # Use the first refrain line and repeat it for the others. refrains.each do |key, values| values[1..-1].each do |i| poem_lines[i-1] = poem_lines[values.first-1] end end # Do the same for [:exact] lines. poetic_form[:rhyme].each.with_index do |line, index| poem_lines[index] = line[:exact] if line[:exact] end # Carry out transformations, if necessary. the_poem = poem_lines if poetic_form[:transform] # Due to the 'merge_hashes' above, each 'poetic_form[:transform]' # value may contain an array of procs. poetic_form[:transform].each do |key, procs| begin # This is to ensure that e.g. '-2' will access from the end. i = (key > 0) ? key - 1 : key [*procs].each do |proc| the_poem[i] = proc.call(the_poem[i], i + 1, poem_lines).to_s end rescue end end end the_poem end
syllable_min_max(line_conds)
click to toggle source
Find min and max syllable count from the conditions.
# File lib/poefy/generation.rb, line 330 def syllable_min_max line_conds min_max = nil if line_conds.all?{ |i| i[:syllable] } min = line_conds.min do |a, b| [*a[:syllable]].min <=> [*b[:syllable]].min end[:syllable] max = line_conds.max do |a, b| [*a[:syllable]].max <=> [*b[:syllable]].max end[:syllable] min_max = { min: [*min].min, max: [*max].max } min_max = nil if min_max[:max] == 0 end min_max end
try_rhyme(conditions, rhyme, syllable_min_max = nil, line_conds = nil)
click to toggle source
Loop through the rhymes until we find one that works. (In a reasonable time-frame)
# File lib/poefy/generation.rb, line 299 def try_rhyme conditions, rhyme, syllable_min_max = nil, line_conds = nil output = [] lines = @corpus.lines_by_rhyme(rhyme, syllable_min_max) # To reduce the number of permutations, reject lines # that do not match any of the lines regex. regex_conds = line_conds.map { |i| i[:regex] } if !regex_conds.include?(nil) and !regex_conds.include?(//) new_lines = [] [*regex_conds].each do |regex_group| possible_lines = lines.dup [*regex_group].each do |regex| possible_lines.reject! { |i| !(i['line'].match(regex)) } end new_lines += possible_lines end lines = new_lines end # Get a sample from the lines that works for all the conditions. begin Timeout::timeout(2) do output = lines.shuffle.conditional_sample(conditions) end rescue output = [] end output end