poefy 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Handle the procedural generation of poems.
6
+ ################################################################################
7
+
8
+ require 'timeout'
9
+
10
+ ################################################################################
11
+
12
+ module Poefy
13
+
14
+ module Generation
15
+
16
+ # Generate specific poem types.
17
+ def poem poetic_form = @poetic_form
18
+
19
+ if !@db.exists?
20
+ return handle_error 'ERROR: Database does not yet exist', nil
21
+ end
22
+
23
+ # Validate the poetic form hash.
24
+ raise ArgumentError, 'Argument must be a hash' unless
25
+ poetic_form.is_a?(Hash)
26
+ poetic_form = validate_poetic_form poetic_form
27
+
28
+ # Make sure the hash contains ':form' or ':rhyme' keys.
29
+ if !(poetic_form[:form] or poetic_form[:rhyme])
30
+ return handle_error \
31
+ "ERROR: No valid rhyme or form option specified.\n" +
32
+ " Try again using the -f or -r option.\n", nil
33
+ end
34
+
35
+ # Loop until we find a valid poem.
36
+ output = poem_proper_sentence poetic_form
37
+
38
+ # Return nil if poem could not be created.
39
+ return nil if (output.nil? or output == [] or output == [''])
40
+
41
+ # Indent the output using the :indent string.
42
+ output = do_indent(output, get_poetic_form_indent(poetic_form))
43
+
44
+ # Append nil lines to the end if the :rhyme demands it.
45
+ rhyme = tokenise_rhyme get_poetic_form_rhyme poetic_form
46
+ (output.length...rhyme.length).each do |i|
47
+ output[i] = ''
48
+ end
49
+
50
+ output
51
+ end
52
+
53
+ # Get all rhyming lines for the word.
54
+ def rhymes word, key = nil
55
+ return nil if word.nil?
56
+ sproc = @db.db.prepare %Q[
57
+ SELECT rhyme, final_word, syllables, line
58
+ FROM lines
59
+ WHERE rhyme = ?
60
+ ORDER BY rhyme, final_word, syllables, line
61
+ ]
62
+ output = word.to_phrase.rhymes.keys.map do |rhyme|
63
+ sproc.reset!
64
+ sproc.bind_param(1, rhyme)
65
+ sproc.execute.to_a
66
+ end.flatten
67
+ sproc.close
68
+ if !key.nil? and %w[rhyme final_word syllables line].include?(key)
69
+ output.map!{ |i| i[key] }
70
+ end
71
+ output
72
+ end
73
+
74
+
75
+ private
76
+
77
+ # Generate a poem using the database and a poem format.
78
+ # If ':proper' then loop to find a valid poem.
79
+ def poem_proper_sentence poetic_form = @poetic_form
80
+ poetic_form = validate_poetic_form poetic_form
81
+ output, count_down = [], 500
82
+
83
+ # Final line must close with sentence-end punctuation.
84
+ # Don't start poems with these words.
85
+ banned_first_words = poetic_form[:proper] ? %w{and but or nor yet} : []
86
+
87
+ loop do
88
+ output = gen_poem_using_conditions poetic_form
89
+ break if output.nil? or output.empty?
90
+ if !banned_first_words.include?(first_word(output[0]).downcase) and
91
+ ( !poetic_form[:proper] or has_stop_punctuation?(output[-1]) )
92
+ break
93
+ end
94
+
95
+ # Fail after some number of failures.
96
+ if (count_down -= 1) == 0 and poetic_form[:proper]
97
+ return handle_error \
98
+ "ERROR: Proper sentence structure not able to be honoured.\n" +
99
+ " Ensure the input has closing punctuation or\n" +
100
+ " try again using the -p option."
101
+ end
102
+ end
103
+
104
+ # (previous way of dealing with closing full stops)
105
+ # (leaving the code commented out for now)
106
+ # output[-1] = end_the_sentence(output[-1])
107
+ output
108
+ end
109
+
110
+ # Use the constraints in 'poetic_form' to generate a poem.
111
+ def gen_poem_using_conditions poetic_form = @poetic_form
112
+ poetic_form = poetic_form_full poetic_form
113
+ poetic_form = validate_poetic_form poetic_form
114
+
115
+ # Tokenise the rhyme string, and return [] if invalid.
116
+ tokenised_rhyme = tokenise_rhyme poetic_form[:rhyme]
117
+ if tokenised_rhyme == []
118
+ return handle_error 'ERROR: Rhyme string is not valid', []
119
+ end
120
+
121
+ # Add lines number as ':line' in each element's hash.
122
+ by_line = conditions_by_line(tokenised_rhyme, poetic_form)
123
+
124
+ # Now we have ':line', so we can break the array order.
125
+ # Let's get rid of empty lines, and group by the rhyme letter.
126
+ conditions_by_rhyme = by_line.reject do |i|
127
+ i[:rhyme] == ' '
128
+ end.group_by do |i|
129
+ i[:rhyme_letter]
130
+ end
131
+
132
+ # Okay, this is great. But if we're making villanelles we'll need
133
+ # duplicated refrain lines. So we won't need unique rhymes for those.
134
+ # So make a distinct set of lines conditions, still grouped by rhyme.
135
+ # This will be the same as [conditions_by_rhyme], except duplicate lines
136
+ # are removed. (These are lines with capitals and numbers: i.e. A1, B2)
137
+ # It will keep the condition hash of only the first refrain line.
138
+ distinct_line_conds = Hash.new { |h,k| h[k] = [] }
139
+ conditions_by_rhyme.each do |key, values|
140
+ uppers = []
141
+ values.each do |v|
142
+ char_1 = v[:rhyme][0]
143
+ if char_1 == char_1.upcase
144
+ if !uppers.include?(v[:rhyme])
145
+ uppers << v[:rhyme]
146
+ distinct_line_conds[key] << v
147
+ end
148
+ else
149
+ distinct_line_conds[key] << v
150
+ end
151
+ end
152
+ end
153
+
154
+ # Right, let's now loop through each rhyme group and find all from
155
+ # the database where the number of lines can be fulfilled.
156
+
157
+ # First, get the order of rhymes, from most to least.
158
+ distinct_line_conds = distinct_line_conds.sort_by{ |k,v| v.count }.reverse
159
+
160
+ # This will store the rhymes that have already been used in the poem.
161
+ # This is so we do not duplicate rhymes between distinct rhyme letters.
162
+ rhymes_already_used = []
163
+
164
+ # This is the final set of lines.
165
+ all_lines = []
166
+
167
+ # Loop through each rhyme group to find lines that satisfy the conditions.
168
+ distinct_line_conds.each do |rhyme_letter, line_conds|
169
+
170
+ # The conditions that will be passed to '#conditional_selection'.
171
+ # This is an array of procs, one for each line.
172
+ conditions = line_conds.map do |cond|
173
+ proc { |arr, elem| diff_end(arr, elem) and validate_line(elem, cond)}
174
+ end
175
+
176
+ # Get all rhymes from the database with at least as many final
177
+ # words as there are lines to be matched.
178
+ rhymes = nil
179
+
180
+ # If all the lines include a 'syllable' condition,
181
+ # then we can specify to only query for matching lines.
182
+ if line_conds.all?{ |i| i[:syllable] }
183
+ min = line_conds.min do |a, b|
184
+ [*a[:syllable]].min <=> [*b[:syllable]].min
185
+ end[:syllable]
186
+ max = line_conds.max do |a, b|
187
+ [*a[:syllable]].max <=> [*b[:syllable]].max
188
+ end[:syllable]
189
+ min_max = { min: [*min].min, max: [*max].max }
190
+ min_max = nil if min_max[:max] == 0
191
+ rhymes = @db.sproc_rhymes_all!(line_conds.count, min_max)
192
+ else
193
+ rhymes = @db.sproc_rhymes_all!(line_conds.count)
194
+ end
195
+
196
+ # Get just the rhyme part of the hash.
197
+ rhymes = rhymes.map{ |i| i['rhyme'] }
198
+ rhymes = rhymes - rhymes_already_used
199
+
200
+ # For each rhyme, get all lines and try to sastify all conditions.
201
+ out = []
202
+ rhymes.shuffle.each do |rhyme|
203
+ out = try_rhyme(conditions, rhyme)
204
+ break if !out.empty?
205
+ end
206
+ if out.empty?
207
+ return handle_error 'ERROR: Not enough rhyming lines in the input'
208
+ end
209
+ rhymes_already_used << out.first['rhyme']
210
+
211
+ # Add the line number back to the array.
212
+ line_conds.count.times do |i|
213
+ out[i]['line_number'] = line_conds[i][:line]
214
+ end
215
+
216
+ out.each do |i|
217
+ all_lines << i
218
+ end
219
+ end
220
+
221
+ # Transpose lines to their actual location.
222
+ poem_lines = []
223
+ all_lines.each do |line|
224
+ poem_lines[line['line_number'] - 1] = line['line']
225
+ end
226
+
227
+ # Go back to the [by_line] array and find all the refrain line nos.
228
+ refrains = Hash.new { |h,k| h[k] = [] }
229
+ by_line.reject{ |i| i[:rhyme] == ' ' }.each do |line|
230
+ if line[:rhyme][0] == line[:rhyme][0].upcase
231
+ refrains[line[:rhyme]] << line[:line]
232
+ end
233
+ end
234
+ refrains.keys.each do |k|
235
+ refrains[k].sort!
236
+ end
237
+
238
+ # Use the first refrain line and repeat it for the others.
239
+ refrains.each do |key, values|
240
+ values[1..-1].each do |i|
241
+ poem_lines[i-1] = poem_lines[values.first-1]
242
+ end
243
+ end
244
+
245
+ poem_lines
246
+ end
247
+
248
+ # Loop through the rhymes until we find one that works.
249
+ # (In a reasonable time-frame)
250
+ def try_rhyme conditions, rhyme
251
+ output = []
252
+ lines = @db.sproc_lines_all!(rhyme)
253
+ begin
254
+ Timeout::timeout(2) do
255
+ output = conditional_selection(lines.shuffle, conditions)
256
+ break
257
+ end
258
+ rescue
259
+ output = []
260
+ end
261
+ output
262
+ end
263
+
264
+ end
265
+
266
+ end
267
+
268
+ ################################################################################
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Handle error message.
6
+ # Quit the program if called from console.
7
+ ################################################################################
8
+
9
+ module Poefy
10
+
11
+ module HandleError
12
+
13
+ private
14
+
15
+ def handle_error msg, return_value = nil
16
+ if @console
17
+ STDERR.puts msg
18
+ exit 1
19
+ end
20
+ return_value
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ ################################################################################
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Base internals for the PoefyGen class.
6
+ ################################################################################
7
+
8
+ require 'json'
9
+
10
+ ################################################################################
11
+
12
+ module Poefy
13
+
14
+ module PoefyGenBase
15
+
16
+ attr_reader :console, :db, :local, :overwrite
17
+
18
+ def initialize db_name, options = {}
19
+ handle_options options
20
+ @db = Poefy::Database.new get_database_file(db_name), @console
21
+ end
22
+
23
+ # Make a database using the given lines.
24
+ def make_database input, overwrite = @overwrite
25
+ if overwrite
26
+ make_database! input
27
+ else
28
+ @db.make_new validate_lines input
29
+ end
30
+ end
31
+ def make_database! input
32
+ @db.make_new! validate_lines input
33
+ end
34
+
35
+ # Close the database.
36
+ def close
37
+ @db.close
38
+ end
39
+
40
+ # Validate the lines. Arg could be a filename,
41
+ # newline delimited string, or array of lines.
42
+ def validate_lines input
43
+
44
+ # If the input is a file, then read it.
45
+ lines = File.exists?(input) ? File.read(input) : input
46
+
47
+ # If lines is not an array, assume string and split on newlines.
48
+ lines = lines.respond_to?(:each) ? lines : lines.split("\n")
49
+ lines.map(&:strip!)
50
+ lines
51
+ end
52
+
53
+ private
54
+
55
+ # Find the correct database file.
56
+ # If local, just use the value.
57
+ # Else, use the database in /data/ directory.
58
+ def get_database_file database_name
59
+ if @local
60
+ database_name
61
+ else
62
+ path = File.expand_path('../../../data', __FILE__)
63
+ file = File.basename(database_name, '.db')
64
+ path + '/' + file + '.db'
65
+ end
66
+ end
67
+
68
+ # Handle the optional initialize options hash.
69
+ def handle_options options
70
+ @console = options[:console] || false
71
+ @overwrite = options[:overwrite] || false
72
+ @local = options[:local] || false
73
+ @poetic_form = {}
74
+ @poetic_form[:proper] = options[:proper] || true
75
+ @poetic_form = validate_poetic_form options
76
+ end
77
+
78
+ # Make sure the options hash is in order.
79
+ def validate_poetic_form poetic_form
80
+ input, output = poetic_form, {}
81
+ form_string = get_valid_form input[:form]
82
+
83
+ # Handle obvious inputs.
84
+ output[:form] = form_string if form_string
85
+ output[:rhyme] = input[:rhyme] if input[:rhyme]
86
+ output[:indent] = input[:indent] if input[:indent]
87
+ output[:syllable] = input[:syllable] if input[:syllable]
88
+ output[:regex] = input[:regex] if input[:regex]
89
+
90
+ # Tokenise string to arrays and hashes.
91
+ rhyme = get_poetic_form_rhyme(output)
92
+ if output[:rhyme]
93
+ output[:rhyme] = tokenise_rhyme output[:rhyme]
94
+ end
95
+ if output[:syllable]
96
+ output[:syllable] = transform_string_syllable output[:syllable], rhyme
97
+ end
98
+ if output[:regex]
99
+ output[:regex] = transform_string_regex output[:regex], rhyme
100
+ end
101
+
102
+ # Get from instance by default.
103
+ output[:proper] = input[:proper].nil? ?
104
+ @poetic_form[:proper] : input[:proper]
105
+
106
+ # Tiny amendment to solve later errors.
107
+ output[:rhyme] = ' ' if output[:rhyme] == ''
108
+ output
109
+ end
110
+
111
+ # Handle error message. Quit the program if called from console.
112
+ def handle_error msg
113
+ if @console
114
+ STDERR.puts msg
115
+ exit 1
116
+ end
117
+ nil
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ ################################################################################