typingpool 0.7.0

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