typingpool 0.7.0

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.
Files changed (90) hide show
  1. data/Rakefile +23 -0
  2. data/bin/tp-assign +240 -0
  3. data/bin/tp-collect +50 -0
  4. data/bin/tp-config +114 -0
  5. data/bin/tp-finish +101 -0
  6. data/bin/tp-make +169 -0
  7. data/bin/tp-review +175 -0
  8. data/lib/typingpool/amazon.rb +732 -0
  9. data/lib/typingpool/app.rb +634 -0
  10. data/lib/typingpool/config.rb +344 -0
  11. data/lib/typingpool/error.rb +22 -0
  12. data/lib/typingpool/filer.rb +396 -0
  13. data/lib/typingpool/project.rb +593 -0
  14. data/lib/typingpool/template.rb +175 -0
  15. data/lib/typingpool/templates/assignment/amazon-init.js +38 -0
  16. data/lib/typingpool/templates/assignment/interview/nameless.html.erb +13 -0
  17. data/lib/typingpool/templates/assignment/interview/noisy.html.erb +12 -0
  18. data/lib/typingpool/templates/assignment/interview/partials/voices.html.erb +10 -0
  19. data/lib/typingpool/templates/assignment/interview/phone.html.erb +12 -0
  20. data/lib/typingpool/templates/assignment/interview.html.erb +11 -0
  21. data/lib/typingpool/templates/assignment/main.css +20 -0
  22. data/lib/typingpool/templates/assignment/partials/entry.html.erb +19 -0
  23. data/lib/typingpool/templates/assignment/partials/footer.html.erb +3 -0
  24. data/lib/typingpool/templates/assignment/partials/header.html.erb +11 -0
  25. data/lib/typingpool/templates/assignment/partials/labeling-example.html.erb +4 -0
  26. data/lib/typingpool/templates/assignment/partials/labeling.html.erb +5 -0
  27. data/lib/typingpool/templates/assignment/partials/length-description.html.erb +6 -0
  28. data/lib/typingpool/templates/assignment/partials/voices.html.erb +10 -0
  29. data/lib/typingpool/templates/assignment/speech.html.erb +11 -0
  30. data/lib/typingpool/templates/config.yml +21 -0
  31. data/lib/typingpool/templates/project/audio/chunks/.empty_directory +0 -0
  32. data/lib/typingpool/templates/project/audio/originals/.empty_directory +0 -0
  33. data/lib/typingpool/templates/project/data/.empty_directory +0 -0
  34. data/lib/typingpool/templates/project/etc/ About these files - read me.txt +8 -0
  35. data/lib/typingpool/templates/project/etc/audio-compat.js +25 -0
  36. data/lib/typingpool/templates/project/etc/player/audio-player.js +4 -0
  37. data/lib/typingpool/templates/project/etc/player/license.txt +19 -0
  38. data/lib/typingpool/templates/project/etc/player/player.swf +0 -0
  39. data/lib/typingpool/templates/project/etc/transcript.css +49 -0
  40. data/lib/typingpool/templates/transcript.html.erb +23 -0
  41. data/lib/typingpool/test/fixtures/amazon-question-html.html +95 -0
  42. data/lib/typingpool/test/fixtures/amazon-question-url.txt +1 -0
  43. data/lib/typingpool/test/fixtures/audio/mp3/interview.1.mp3 +0 -0
  44. data/lib/typingpool/test/fixtures/audio/mp3/interview.2.mp3 +0 -0
  45. data/lib/typingpool/test/fixtures/audio/wma/VN620007.WMA +0 -0
  46. data/lib/typingpool/test/fixtures/audio/wma/VN620052.WMA +0 -0
  47. data/lib/typingpool/test/fixtures/config-1 +20 -0
  48. data/lib/typingpool/test/fixtures/config-2 +25 -0
  49. data/lib/typingpool/test/fixtures/not_yaml.txt +4 -0
  50. data/lib/typingpool/test/fixtures/template-2.html.erb +10 -0
  51. data/lib/typingpool/test/fixtures/template-3.html.erb +22 -0
  52. data/lib/typingpool/test/fixtures/template.html.erb +10 -0
  53. data/lib/typingpool/test/fixtures/tp_collect_id.txt +1 -0
  54. data/lib/typingpool/test/fixtures/tp_collect_sandbox-assignment.csv +8 -0
  55. data/lib/typingpool/test/fixtures/tp_review_id.txt +1 -0
  56. data/lib/typingpool/test/fixtures/tp_review_sandbox-assignment.csv +8 -0
  57. data/lib/typingpool/test/fixtures/transcript-chunks.csv +226 -0
  58. data/lib/typingpool/test/fixtures/utf8_transcript.txt +7 -0
  59. data/lib/typingpool/test/fixtures/vcr/tp-collect-1.yml +2712 -0
  60. data/lib/typingpool/test/fixtures/vcr/tp-collect-2.yml +2718 -0
  61. data/lib/typingpool/test/fixtures/vcr/tp-collect-3.yml +2768 -0
  62. data/lib/typingpool/test/fixtures/vcr/tp-review-1.yml +570 -0
  63. data/lib/typingpool/test/fixtures/vcr/tp-review-2.yml +351 -0
  64. data/lib/typingpool/test.rb +418 -0
  65. data/lib/typingpool/transcript.rb +181 -0
  66. data/lib/typingpool/utility.rb +272 -0
  67. data/lib/typingpool.rb +500 -0
  68. data/test/make_amazon_question_fixture.rb +24 -0
  69. data/test/make_tp_collect_fixture_1.rb +26 -0
  70. data/test/make_tp_collect_fixture_2.rb +16 -0
  71. data/test/make_tp_collect_fixture_3.rb +15 -0
  72. data/test/make_tp_collect_fixture_4.rb +17 -0
  73. data/test/make_tp_review_fixture_1.rb +26 -0
  74. data/test/make_tp_review_fixture_2.rb +30 -0
  75. data/test/make_transcript_chunks_fixture.rb +53 -0
  76. data/test/test_integration_script_1_tp_config.rb +108 -0
  77. data/test/test_integration_script_2_tp_make.rb +119 -0
  78. data/test/test_integration_script_3_tp_assign.rb +152 -0
  79. data/test/test_integration_script_4_tp_review.rb +72 -0
  80. data/test/test_integration_script_5_tp_collect.rb +44 -0
  81. data/test/test_integration_script_6_tp_finish.rb +123 -0
  82. data/test/test_unit_amazon.rb +153 -0
  83. data/test/test_unit_config.rb +94 -0
  84. data/test/test_unit_filer.rb +202 -0
  85. data/test/test_unit_project.rb +168 -0
  86. data/test/test_unit_project_local.rb +68 -0
  87. data/test/test_unit_project_remote.rb +157 -0
  88. data/test/test_unit_template.rb +111 -0
  89. data/test/test_unit_transcript.rb +77 -0
  90. metadata +234 -0
@@ -0,0 +1,396 @@
1
+ module Typingpool
2
+
3
+ #Convenience wrapper for basic file operations. Base class for
4
+ #wrappers for specialized file types (audio, CSV) and for file
5
+ #collections.
6
+ class Filer
7
+ require 'fileutils'
8
+ include Utility::Castable
9
+ include Comparable
10
+
11
+ #Fully-expanded path to file
12
+ attr_reader :path
13
+
14
+ #Constructor.
15
+ # ==== Params
16
+ #[path] Fully expanded path to file.
17
+ #[encoding] Optional. Encoding for all text operations on the
18
+ # file. Should be compatiable with :encoding arg to
19
+ # IO.read. Default is 'UTF-8'.
20
+ def initialize(path, encoding='UTF-8')
21
+ @path = path
22
+ @encoding = encoding
23
+ end
24
+
25
+ def <=>(other)
26
+ path <=> other.path
27
+ end
28
+
29
+ #Returns contents of file or nil if the file does not exist.
30
+ def read
31
+ if File.exists? @path
32
+ IO.read(@path, :encoding => @encoding)
33
+ end
34
+ end
35
+
36
+ #Write data to the file.
37
+ def write(data, mode='w')
38
+ File.open(@path, mode, :encoding => @encoding) do |out|
39
+ out << data
40
+ end
41
+ end
42
+
43
+ #Moves the underlying file AND updates the @path of the Filer instance.
44
+ def mv!(to)
45
+ FileUtils.mv(@path, to)
46
+ if File.directory? to
47
+ to = File.join(to, File.basename(path))
48
+ end
49
+ @path = to
50
+ end
51
+
52
+ #Returns the underlying file as an IO stream. Convenient for
53
+ #Project::Remote#put.
54
+ def to_stream(mode='r')
55
+ File.new(@path, mode, :encoding => @encoding)
56
+ end
57
+
58
+ #Filer objects always stringify to their path. We might change
59
+ #this later such that to_str gives the path but to_s gives the
60
+ #content of the file as text.
61
+ def to_s
62
+ @path
63
+ end
64
+ alias :to_str :to_s
65
+
66
+ #Returns the parent dir of the underlying file as a Filer::Dir
67
+ #instance.
68
+ def dir
69
+ Filer::Dir.new(File.dirname(@path))
70
+ end
71
+
72
+ #Cast this file into a new Filer subtype, e.g. Filer::Audio.
73
+ # ==== Params
74
+ # [sym] Symbol corresponding to Filer subclass to cast into. For
75
+ # example, passing :audio will cast into a Filer::Audio.
76
+ # ==== Returns
77
+ # Instance of new Filer subclass
78
+ def as(sym)
79
+ #super calls into Utility::Castable mixin
80
+ super(sym, @path)
81
+ end
82
+
83
+
84
+ #Convenience wrapper for CSV files. Makes them Enumerable, so you
85
+ #can iterate through rows with each, map, select, etc. You can
86
+ #also modify in place with each!. See Filer base class for other
87
+ #methods.
88
+ class CSV < Filer
89
+ include Enumerable
90
+ require 'csv'
91
+
92
+ #Reads into an array of hashes, with hash keys determined by the
93
+ #first row of the CSV file. Parsing rules are the default for
94
+ #CSV.parse.
95
+ def read
96
+ raw = super or return []
97
+ rows = ::CSV.parse(raw.to_s)
98
+ headers = rows.shift or raise Error::File, "No CSV at #{@path}"
99
+ rows.map{|row| Utility.array_to_hash(row, headers) }
100
+ end
101
+
102
+ #Takes array of hashes followed by optional list of keys (by
103
+ #default keys are determined by looking at all the
104
+ #hashes). Lines are written per the defaults of
105
+ #CSV.generate_line.
106
+ def write(hashes, headers=hashes.map{|h| h.keys}.flatten.uniq)
107
+ super(
108
+ ::CSV.generate_line(headers, :encoding => @encoding) +
109
+ hashes.map do |hash|
110
+ ::CSV.generate_line(headers.map{|header| hash[header] }, :encoding => @encoding)
111
+ end.join
112
+ )
113
+ end
114
+
115
+ #Takes an array of arrays, corresponding to the rows, and a list
116
+ #of headers/keys to write at the top.
117
+ def write_arrays(arrays, headers)
118
+ write(arrays.map{|array| Utility.array_to_hash(array, headers) }, headers)
119
+ end
120
+
121
+ #Enumerate through the rows, with each row represented by a
122
+ #hash.
123
+ def each
124
+ read.each do |row|
125
+ yield row
126
+ end
127
+ end
128
+
129
+ #Same as each, but any changes to the rows will be written back
130
+ #out to the underlying CSV file.
131
+ def each!
132
+ #each_with_index doesn't return the array, so we have to use each
133
+ write(each{|hash| yield(hash) })
134
+ end
135
+ end #CSV
136
+
137
+ #Convenience wrapper for audio files.You can convert to mp3s,
138
+ #split into multiple files, and dynamically read the bitrate.
139
+ class Audio < Filer
140
+ require 'open3'
141
+
142
+ #Does the file have a '.mp3' extension?
143
+ def mp3?
144
+ File.extname(@path).downcase.eql?('.mp3')
145
+ end
146
+
147
+ #Convert to mp3 via ffmpeg.
148
+ # ==== Params
149
+ # [dest] Filer object corresponding to the path the mp3 version
150
+ # should end up at.
151
+ # [bitrate] If passed, bitrate should be an integer
152
+ # corresponding to kb/s. If not, we use the bitrate
153
+ # from the current file or, if that can't be read,
154
+ # default to 192kbps. Does not check if the file is
155
+ # already an mp3. Returns a new Filer::Audio
156
+ # representing the new mp3 file.
157
+ # ==== Returns
158
+ # Filer::Audio containing the new mp3.
159
+ def to_mp3(dest=self.dir.file("#{File.basename(@path, '.*') }.mp3"), bitrate=nil)
160
+ bitrate ||= self.bitrate || 192
161
+ Utility.system_quietly('ffmpeg', '-i', @path, '-acodec', 'libmp3lame', '-ab', "#{bitrate}k", '-ac', '2', dest)
162
+ File.exists?(dest) or raise Error::Shell, "Could not found output from `ffmpeg` on #{path}"
163
+ self.class.new(dest.path)
164
+ end
165
+
166
+ #Reads the bitrate of the audio file via ffmpeg. Returns an
167
+ #integer corresponding to kb/s, or nil if the bitrate could not
168
+ #be determined.
169
+ def bitrate
170
+ out, err, status = Open3.capture3('ffmpeg', '-i', @path)
171
+ bitrate = err.match(/(\d+) kb\/s/)
172
+ return bitrate ? bitrate[1].to_i : nil
173
+ end
174
+
175
+ #Splits an mp3 into smaller files.
176
+ # ==== Params
177
+ # [interval_in_min_dot_seconds] Split the file into chunks this
178
+ # large. The interval should be of the format
179
+ # minute.seconds, for example 2 minutes 15 seconds
180
+ # would be written as "2.15". For further details on
181
+ # interval format, consult the documentation for
182
+ # mp3split, a command-line unix utility.
183
+ # [basename] Name the new chunks using this base. Default is the
184
+ # basename of the original file.
185
+ # [dest] Destination directory for the new chunks as a
186
+ # Filer::Dir. Default is the same directory as the
187
+ # original file.
188
+ # ==== Returns
189
+ # Filer::Files containing the new files.
190
+ def split(interval_in_min_dot_seconds, basename=File.basename(path, '.*'), dest=dir)
191
+ #We have to cd into the wrapfile directory and do everything
192
+ #there because old/packaged versions of mp3splt were
193
+ #retarded at handling absolute directory paths
194
+ ::Dir.chdir(dir.path) do
195
+ Utility.system_quietly('mp3splt', '-t', interval_in_min_dot_seconds, '-o', "#{basename}.@m.@s", File.basename(path))
196
+ end
197
+ files = Filer::Files::Audio.new(dir.select{|file| File.basename(file.path).match(/^#{Regexp.escape(basename) }\.\d+\.\d+\.mp3$/) })
198
+ if files.to_a.empty?
199
+ raise Error::Shell, "Could not find output from `mp3splt` on #{path}"
200
+ end
201
+ if dest.path != dir.path
202
+ files.mv!(dest)
203
+ end
204
+ files.sort
205
+ end
206
+
207
+ #Extracts from the filename the offset time of the chunk
208
+ #relative to the original from which it was split. Format is
209
+ #minute.seconds. Suitable for use on files created by 'split'
210
+ #method.
211
+ def offset
212
+ match = File.basename(@path).match(/\d+\.\d\d\b/)
213
+ return match[0] if match
214
+ end
215
+ end #Audio
216
+
217
+ #Handler for collection of Filer instances. Makes them enumerable,
218
+ #Allows easy re-casting to Filer::Files subclasses,
219
+ #and provides various other convenience methods.
220
+ class Files
221
+ include Enumerable
222
+ include Utility::Castable
223
+ require 'fileutils'
224
+
225
+ #Array of Filer instances included in the collection
226
+ attr_reader :files
227
+
228
+ #Constructor. Takes array of Filer instances.
229
+ def initialize(files)
230
+ @files = files
231
+ end
232
+
233
+ #Enumerate through Filer instances.
234
+ def each
235
+ files.each do |file|
236
+ yield file
237
+ end
238
+ end
239
+
240
+ #Cast this collection into a new Filer::Files subtype,
241
+ #e.g. Filer::Files::Audio.
242
+ # ==== Params
243
+ # [sym] Symbol corresponding to Filer::Files subclass to cast
244
+ # into. For example, passing :audio will cast into a
245
+ # Filer::Files::Audio.
246
+ # ==== Returns
247
+ # Instance of new Filer::Files subclass
248
+ def as(sym)
249
+ #super calls into Utility::Castable mixin
250
+ super(sym, files)
251
+ end
252
+
253
+ #Returns array of IO streams created by calling to_stream on
254
+ #each Filer instance in the collection.
255
+ def to_streams
256
+ self.map{|file| file.to_stream }
257
+ end
258
+
259
+ #Calls mv! on each Filer instance in the collection. See
260
+ #documentation for Filer#mv! for definition of "to" param and
261
+ #for return value.
262
+ def mv!(to)
263
+ files.map{|file| file.mv! to }
264
+ end
265
+
266
+ #Handler for collection of Filer::Audio instances. Does
267
+ #everything Filer::Files does, plus can batch convert to mp3 an
268
+ #can merge the Filer::Audio instances into a single audio file,
269
+ #provided they are in mp3 format.
270
+ class Audio < Files
271
+
272
+ #Constructor. Takes an array of Filer or Filer subclass instances.
273
+ def initialize(files)
274
+ @files = files.map{|file| self.file(file.path) }
275
+ end
276
+
277
+ def file(path)
278
+ Filer::Audio.new(path)
279
+ end
280
+
281
+ #Batch convert Filer::Audio instances to mp3 format.
282
+ # ==== Params
283
+ # [dest_dir] Filer::Dir instance corresponding to directory
284
+ # into which mp3 file versions will be created.
285
+ # [bitrate] See documentation for Filer::Audio#bitrate.
286
+ # ==== Returns
287
+ # Filer::Files::Audio instance corresponding to new mp3
288
+ # versions of the original files or, in the case where the
289
+ # original file was already in mp3 format, corresponding to
290
+ # the original files themselves.
291
+ def to_mp3(dest_dir, bitrate=nil)
292
+ mp3s = self.map do |file|
293
+ if file.mp3?
294
+ file
295
+ else
296
+ yield(file) if block_given?
297
+ file.to_mp3(dest_dir.file("#{File.basename(file.path, '.*') }.mp3"), bitrate)
298
+ end
299
+ end
300
+ self.class.new(mp3s)
301
+ end
302
+
303
+ #Merge Filer::Audio instances into a single new file, provided
304
+ #they are all in mp3 format.
305
+ # ==== Params
306
+ #[into_file] Filer or Filer subclass instance corresponding to
307
+ #the location of the new, merged file that should be created.
308
+ # ==== Returns
309
+ # Filer::Audio instance corresponding to the new, merged file.
310
+ def merge(into_file)
311
+ raise Error::Argument, "No files to merge" if self.to_a.empty?
312
+ if self.count > 1
313
+ Utility.system_quietly('mp3wrap', into_file, *self.to_a)
314
+ written = File.join(into_file.dir, "#{File.basename(into_file.path, '.*') }_MP3WRAP.mp3")
315
+ FileUtils.mv(written, into_file)
316
+ else
317
+ FileUtils.cp(self.first, into_file)
318
+ end
319
+ self.file(into_file.path)
320
+ end
321
+ end #Audio
322
+ end #Files
323
+
324
+ #Convenience wrapper for basic directory operations and for
325
+ #casting files to specific filer types (CSV, Audio).
326
+ class Dir < Files
327
+
328
+ #Full expanded path to the dir
329
+ attr_reader :path
330
+
331
+ #Constructor. Takes full expanded path to the dir. Does NOT
332
+ #create dir in the filesystem.
333
+ def initialize(path)
334
+ @path = path
335
+ end
336
+
337
+ class << self
338
+
339
+ #Constructor. Takes full expanded path to the dir and creates
340
+ #the dir in the filesystem. Returns new Filer::Dir.
341
+ def create(path)
342
+ FileUtils.mkdir(path)
343
+ new(path)
344
+ end
345
+
346
+ #Constructor. Takes directory name and full expanded path of
347
+ #the parent directory. If the so-named directory exists within
348
+ #the parent directory, returns it. If not, returns nil.
349
+ def named(name, in_dir)
350
+ path = File.join(in_dir, name)
351
+ if File.exists?(path) && File.directory?(path)
352
+ new(path)
353
+ end
354
+ end
355
+ end #class << self
356
+
357
+ #Filer::Dir isntances stringify to their path.
358
+ def to_s
359
+ @path
360
+ end
361
+ alias :to_str :to_s
362
+
363
+ #Takes an aribtrary number of path elements relative to the
364
+ #Filer::Dir instance. So a file in the subdir path/to/file.txt
365
+ #would be referenced via file('path', 'to', 'file.txt'). Returns
366
+ #a new Filer instance wrapping the referenced file. Does not
367
+ #guarantee that the referenced file exists.
368
+ def file(*relative_path)
369
+ Filer.new(file_path(*relative_path))
370
+ end
371
+
372
+ #Returns the files in the Filer::Dir directory as Filer
373
+ #instances. Excludes files whose names start with a dot.
374
+ def files
375
+ ::Dir.entries(@path).select{|entry| File.file? file_path(entry) }.reject{|entry| entry.match(/^\./) }.map{|entry| self.file(entry) }
376
+ end
377
+
378
+ #Takes relative path elements as params just like the file
379
+ #method. Returns a new Filer::Dir instance wrapping the
380
+ #referenced subdir.
381
+ def subdir(*relative_path)
382
+ Dir.new(file_path(*relative_path))
383
+ end
384
+
385
+ #OS X specific. Opens the dir in the Finder via the 'open' command.
386
+ def finder_open
387
+ system('open', @path)
388
+ end
389
+
390
+ def file_path(*relative_path)
391
+ File.join(@path, *relative_path)
392
+ end
393
+
394
+ end #Dir
395
+ end #Filer
396
+ end #Typingpool