poefy 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +74 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/LICENSE +13 -0
- data/README.md +522 -0
- data/Rakefile +6 -0
- data/bin/poefy +205 -0
- data/data/emily_dickinson.txt +9942 -0
- data/data/english_as_she_is_spoke.txt +647 -0
- data/data/shakespeare_sonnets.txt +2618 -0
- data/data/spec_test_tiny.txt +12 -0
- data/data/st_therese_of_lisieux.txt +3700 -0
- data/data/whitman_leaves.txt +17815 -0
- data/lib/poefy/conditional_satisfaction.rb +208 -0
- data/lib/poefy/database.rb +252 -0
- data/lib/poefy/generation.rb +268 -0
- data/lib/poefy/handle_error.rb +27 -0
- data/lib/poefy/poefy_gen_base.rb +124 -0
- data/lib/poefy/poetic_forms.rb +330 -0
- data/lib/poefy/self.rb +21 -0
- data/lib/poefy/string_manipulation.rb +81 -0
- data/lib/poefy/version.rb +29 -0
- data/lib/poefy.rb +49 -0
- data/poefy.gemspec +33 -0
- data/spec/poefy_spec.rb +464 -0
- data/spec/spec_helper.rb +9 -0
- metadata +175 -0
@@ -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
|
+
################################################################################
|