poefy 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
################################################################################
|