typingpool 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +23 -0
- data/bin/tp-assign +240 -0
- data/bin/tp-collect +50 -0
- data/bin/tp-config +114 -0
- data/bin/tp-finish +101 -0
- data/bin/tp-make +169 -0
- data/bin/tp-review +175 -0
- data/lib/typingpool/amazon.rb +732 -0
- data/lib/typingpool/app.rb +634 -0
- data/lib/typingpool/config.rb +344 -0
- data/lib/typingpool/error.rb +22 -0
- data/lib/typingpool/filer.rb +396 -0
- data/lib/typingpool/project.rb +593 -0
- data/lib/typingpool/template.rb +175 -0
- data/lib/typingpool/templates/assignment/amazon-init.js +38 -0
- data/lib/typingpool/templates/assignment/interview/nameless.html.erb +13 -0
- data/lib/typingpool/templates/assignment/interview/noisy.html.erb +12 -0
- data/lib/typingpool/templates/assignment/interview/partials/voices.html.erb +10 -0
- data/lib/typingpool/templates/assignment/interview/phone.html.erb +12 -0
- data/lib/typingpool/templates/assignment/interview.html.erb +11 -0
- data/lib/typingpool/templates/assignment/main.css +20 -0
- data/lib/typingpool/templates/assignment/partials/entry.html.erb +19 -0
- data/lib/typingpool/templates/assignment/partials/footer.html.erb +3 -0
- data/lib/typingpool/templates/assignment/partials/header.html.erb +11 -0
- data/lib/typingpool/templates/assignment/partials/labeling-example.html.erb +4 -0
- data/lib/typingpool/templates/assignment/partials/labeling.html.erb +5 -0
- data/lib/typingpool/templates/assignment/partials/length-description.html.erb +6 -0
- data/lib/typingpool/templates/assignment/partials/voices.html.erb +10 -0
- data/lib/typingpool/templates/assignment/speech.html.erb +11 -0
- data/lib/typingpool/templates/config.yml +21 -0
- data/lib/typingpool/templates/project/audio/chunks/.empty_directory +0 -0
- data/lib/typingpool/templates/project/audio/originals/.empty_directory +0 -0
- data/lib/typingpool/templates/project/data/.empty_directory +0 -0
- data/lib/typingpool/templates/project/etc/ About these files - read me.txt +8 -0
- data/lib/typingpool/templates/project/etc/audio-compat.js +25 -0
- data/lib/typingpool/templates/project/etc/player/audio-player.js +4 -0
- data/lib/typingpool/templates/project/etc/player/license.txt +19 -0
- data/lib/typingpool/templates/project/etc/player/player.swf +0 -0
- data/lib/typingpool/templates/project/etc/transcript.css +49 -0
- data/lib/typingpool/templates/transcript.html.erb +23 -0
- data/lib/typingpool/test/fixtures/amazon-question-html.html +95 -0
- data/lib/typingpool/test/fixtures/amazon-question-url.txt +1 -0
- data/lib/typingpool/test/fixtures/audio/mp3/interview.1.mp3 +0 -0
- data/lib/typingpool/test/fixtures/audio/mp3/interview.2.mp3 +0 -0
- data/lib/typingpool/test/fixtures/audio/wma/VN620007.WMA +0 -0
- data/lib/typingpool/test/fixtures/audio/wma/VN620052.WMA +0 -0
- data/lib/typingpool/test/fixtures/config-1 +20 -0
- data/lib/typingpool/test/fixtures/config-2 +25 -0
- data/lib/typingpool/test/fixtures/not_yaml.txt +4 -0
- data/lib/typingpool/test/fixtures/template-2.html.erb +10 -0
- data/lib/typingpool/test/fixtures/template-3.html.erb +22 -0
- data/lib/typingpool/test/fixtures/template.html.erb +10 -0
- data/lib/typingpool/test/fixtures/tp_collect_id.txt +1 -0
- data/lib/typingpool/test/fixtures/tp_collect_sandbox-assignment.csv +8 -0
- data/lib/typingpool/test/fixtures/tp_review_id.txt +1 -0
- data/lib/typingpool/test/fixtures/tp_review_sandbox-assignment.csv +8 -0
- data/lib/typingpool/test/fixtures/transcript-chunks.csv +226 -0
- data/lib/typingpool/test/fixtures/utf8_transcript.txt +7 -0
- data/lib/typingpool/test/fixtures/vcr/tp-collect-1.yml +2712 -0
- data/lib/typingpool/test/fixtures/vcr/tp-collect-2.yml +2718 -0
- data/lib/typingpool/test/fixtures/vcr/tp-collect-3.yml +2768 -0
- data/lib/typingpool/test/fixtures/vcr/tp-review-1.yml +570 -0
- data/lib/typingpool/test/fixtures/vcr/tp-review-2.yml +351 -0
- data/lib/typingpool/test.rb +418 -0
- data/lib/typingpool/transcript.rb +181 -0
- data/lib/typingpool/utility.rb +272 -0
- data/lib/typingpool.rb +500 -0
- data/test/make_amazon_question_fixture.rb +24 -0
- data/test/make_tp_collect_fixture_1.rb +26 -0
- data/test/make_tp_collect_fixture_2.rb +16 -0
- data/test/make_tp_collect_fixture_3.rb +15 -0
- data/test/make_tp_collect_fixture_4.rb +17 -0
- data/test/make_tp_review_fixture_1.rb +26 -0
- data/test/make_tp_review_fixture_2.rb +30 -0
- data/test/make_transcript_chunks_fixture.rb +53 -0
- data/test/test_integration_script_1_tp_config.rb +108 -0
- data/test/test_integration_script_2_tp_make.rb +119 -0
- data/test/test_integration_script_3_tp_assign.rb +152 -0
- data/test/test_integration_script_4_tp_review.rb +72 -0
- data/test/test_integration_script_5_tp_collect.rb +44 -0
- data/test/test_integration_script_6_tp_finish.rb +123 -0
- data/test/test_unit_amazon.rb +153 -0
- data/test/test_unit_config.rb +94 -0
- data/test/test_unit_filer.rb +202 -0
- data/test/test_unit_project.rb +168 -0
- data/test/test_unit_project_local.rb +68 -0
- data/test/test_unit_project_remote.rb +157 -0
- data/test/test_unit_template.rb +111 -0
- data/test/test_unit_transcript.rb +77 -0
- metadata +234 -0
data/bin/tp-make
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'typingpool'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'tempfile'
|
7
|
+
require 'tmpdir'
|
8
|
+
include Typingpool::App::FriendlyExceptions
|
9
|
+
|
10
|
+
options = {
|
11
|
+
:files => [],
|
12
|
+
:voices => [],
|
13
|
+
:unusual => [],
|
14
|
+
:chunk => '1:00',
|
15
|
+
}
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
options[:banner] = "USAGE: #{File.basename($PROGRAM_NAME)} PROJECTNAME FILE [FILE [FILE...]]\n"
|
18
|
+
options[:banner] += " [--chunks 1:00] [--subtitle 'Hack Day interview']\n"
|
19
|
+
options[:banner] += " [--voice 'John' --voice 'Pat Foo, British female'...]\n"
|
20
|
+
options[:banner] += " [--unusual 'Hack Day' --unusual 'Sunnyvale, Chad Dickerson'...]\n"
|
21
|
+
options[:banner] += " [--bitrate 256] [--config PATH]\n"
|
22
|
+
options[:banner] += " [--title PROJECTNAME] [--file foo.mp3 [--file bar.mp3...]]\n"
|
23
|
+
opts.banner = options[:banner]
|
24
|
+
|
25
|
+
opts.on('--title TITLE',
|
26
|
+
'Required. For file names and transcript.',
|
27
|
+
'Also accepted as first argument') do |title|
|
28
|
+
options[:title] = title
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('--file FILE',
|
32
|
+
'Required. Audio for transcribing.',
|
33
|
+
'Repeatable (sorting is by name).',
|
34
|
+
'Also accepted as second and later arguments') do |file|
|
35
|
+
options[:files].push(file)
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on('--subtitle SUBTITLE',
|
39
|
+
'For transcript') do |subtitle|
|
40
|
+
options[:subtitle] = subtitle
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('--chunks MM:SS',
|
44
|
+
'Default: 1:00. Audio divided thusly for',
|
45
|
+
'transcribing. Try also HH:MM:SS and SSS') do |chunk|
|
46
|
+
options[:chunk] = chunk
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on('--voice "NAME[, DESCR]"',
|
50
|
+
'Name, optional description of recorded',
|
51
|
+
'person, to aid transcriber. Repeatable') do |voice|
|
52
|
+
options[:voices].push(voice)
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on('--unusual WORD[,WORD,]',
|
56
|
+
'Unusual word within recording, to aid',
|
57
|
+
'transcriber. Commas for multiple.',
|
58
|
+
'Repeatable') do |word|
|
59
|
+
options[:unusual].push(word)
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on('--config PATH',
|
63
|
+
'Default: ~/.typingpool. A config file') do |path|
|
64
|
+
options[:config] = path
|
65
|
+
end
|
66
|
+
|
67
|
+
opts.on('--bitrate KBPS',
|
68
|
+
'Default: Mirror input. Output bitrate in',
|
69
|
+
'kb/s. Only applies if/when converting to',
|
70
|
+
'MP3') do |kbps|
|
71
|
+
options[:bitrate] = kbps
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on('--help',
|
75
|
+
'Display this screen.') do
|
76
|
+
puts opts
|
77
|
+
exit
|
78
|
+
end
|
79
|
+
end.parse!
|
80
|
+
|
81
|
+
Typingpool::App.if_missing_dependencies do |missing|
|
82
|
+
missing.map!{|cmd| "`#{cmd}`" }
|
83
|
+
them = missing.count > 1 ? 'them' : 'it'
|
84
|
+
abort "It looks like you're missing #{Typingpool::Utility.join_in_english(missing)}. You'll need to install #{them} before Typingpool can run."
|
85
|
+
end
|
86
|
+
|
87
|
+
options[:title] ||= ARGV.shift if ARGV.count > 0
|
88
|
+
options[:files].push(ARGV).flatten! if ARGV.count > 0
|
89
|
+
options[:banner] += "\n#{Typingpool::App::CLI.help_arg_explanation}\n"
|
90
|
+
abort "No files specified\n\n#{options[:banner]}" if options[:files].empty?
|
91
|
+
abort "No title specified\n\n#{options[:banner]}" if options[:title].to_s.empty?
|
92
|
+
options[:files].sort!
|
93
|
+
options[:files].each do |file|
|
94
|
+
File.extname(file) or abort "You need a file extension on the file '#{file}'"
|
95
|
+
File.exists?(file) or abort "There is no file '#{file}'"
|
96
|
+
File.file?(file) or abort "Not a file: '#{file}'"
|
97
|
+
end
|
98
|
+
options[:unusual].map!{|unusual| unusual.split(/\s*,\s*/)}.flatten!
|
99
|
+
options[:voices].map! do |voice|
|
100
|
+
name, description = voice.split(/\s*,\s*/)
|
101
|
+
{
|
102
|
+
:name => name,
|
103
|
+
:description => (description || '')
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
config = Typingpool::App::CLI.config_from_arg(options[:config]) or abort "No config file at '#{options[:config]}'"
|
108
|
+
|
109
|
+
project = with_friendly_exceptions('project title', options[:title]) do
|
110
|
+
Typingpool::Project.new(options[:title], config)
|
111
|
+
end
|
112
|
+
|
113
|
+
with_friendly_exceptions('--chunk argument', options[:chunk]) do
|
114
|
+
project.interval = options[:chunk] if options[:chunk]
|
115
|
+
end
|
116
|
+
|
117
|
+
with_friendly_exceptions('--bitrate argument', options[:bitrate]) do
|
118
|
+
project.bitrate = options[:bitrate] if options[:bitrate]
|
119
|
+
end
|
120
|
+
|
121
|
+
if project.local
|
122
|
+
if (File.exists?(project.local.file('data', 'assignment.csv')) &&
|
123
|
+
project.local.file('data', 'assignment.csv').as(:csv).read.select{|assignment| assignment['audio_uploaded'] == 'maybe' }.count > 0)
|
124
|
+
#project where the upload died partway through
|
125
|
+
STDERR.puts "Fixing incomplete project"
|
126
|
+
STDERR.puts "Determining which mp3s need uploading"
|
127
|
+
else
|
128
|
+
abort "The title '#{options[:title]}' is taken"
|
129
|
+
end #if(File.exists(project.local.file('data', 'assignment.csv') &&...
|
130
|
+
else
|
131
|
+
project.create_local
|
132
|
+
project.local.subtitle = options[:subtitle] if options[:subtitle]
|
133
|
+
options[:files].each{|path| FileUtils.cp(path, project.local.subdir('audio', 'originals')) }
|
134
|
+
|
135
|
+
temp_conversion_dir = Typingpool::Filer::Dir.new(Dir.mktmpdir)
|
136
|
+
converted_files = project.local.subdir('audio', 'originals').as(:audio).to_mp3(temp_conversion_dir, project.bitrate) do |file|
|
137
|
+
STDERR.puts "Converting #{File.basename(file) } to mp3"
|
138
|
+
end
|
139
|
+
|
140
|
+
STDERR.puts "Merging audio" if converted_files.count > 1
|
141
|
+
temp_merge_file = Tempfile.new(["#{project.name}.all.", ".mp3"])
|
142
|
+
temp_merge_filer = Typingpool::Filer::Audio.new(temp_merge_file.path)
|
143
|
+
converted_files.merge(temp_merge_filer)
|
144
|
+
|
145
|
+
STDERR.puts "Splitting audio into uniform bits"
|
146
|
+
chunks = temp_merge_filer.split(project.interval_as_min_dot_sec, project.name, project.local.subdir('audio','chunks'))
|
147
|
+
urls = project.create_remote_names(chunks).map{|file| project.remote.file_to_url(file) }
|
148
|
+
|
149
|
+
#Write mp3 URLs before uploading, so we can rollback if uploading
|
150
|
+
#dies halfway through
|
151
|
+
assignment_path = project.create_assignment_csv(:path => ['data', 'assignment.csv'], :urls => urls, :unusual => options[:unusual], :voices => options[:voices])
|
152
|
+
|
153
|
+
temp_merge_file.close
|
154
|
+
temp_merge_file.unlink
|
155
|
+
FileUtils.remove_entry_secure(temp_conversion_dir)
|
156
|
+
end #if project.local
|
157
|
+
|
158
|
+
Typingpool::App.upload_audio_for_project(project) do |file, as|
|
159
|
+
STDERR.puts "Uploading #{File.basename(file)} to #{project.remote.host}/#{project.remote.path} as #{as}"
|
160
|
+
end
|
161
|
+
|
162
|
+
if STDOUT.tty? && Typingpool::Utility.os_x?
|
163
|
+
STDERR.puts "Opening project folder #{project.local.path}"
|
164
|
+
project.local.finder_open
|
165
|
+
end
|
166
|
+
|
167
|
+
STDERR.puts "Done. Project at:"
|
168
|
+
puts project.local.path
|
169
|
+
|
data/bin/tp-review
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'typingpool'
|
4
|
+
require 'highline/import'
|
5
|
+
require 'optparse'
|
6
|
+
include Typingpool::App::CLI::Formatter
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
OptionParser.new do |commands|
|
10
|
+
options[:banner] = "USAGE: #{File.basename($PROGRAM_NAME) } [PROJECT]\n"
|
11
|
+
options[:banner] += " [--config PATH] [--sandbox]\n"
|
12
|
+
commands.banner = options[:banner]
|
13
|
+
|
14
|
+
commands.on('--project=PROJECT',
|
15
|
+
'Path or name within dir $config_file:transcripts.',
|
16
|
+
'Also accepted as first argument to script.',
|
17
|
+
'If not specified, reviews pending results',
|
18
|
+
'for ALL projects') do |project|
|
19
|
+
options[:project] = project
|
20
|
+
end
|
21
|
+
commands.on('--config=PATH',
|
22
|
+
'Default: ~/.typingpool') do |path|
|
23
|
+
options[:config] = path
|
24
|
+
end
|
25
|
+
commands.on('--sandbox',
|
26
|
+
'Test in Mechanical Turk\'s sandbox') do |sandbox|
|
27
|
+
options[:sandbox] = true
|
28
|
+
end
|
29
|
+
commands.on('--fixture=PATH',
|
30
|
+
"Optional. For testing purposes only.",
|
31
|
+
"A VCR fixture for running with mock data") do |fixture|
|
32
|
+
options[:fixture] = fixture
|
33
|
+
end
|
34
|
+
commands.on('--help',
|
35
|
+
"Display this screen") do
|
36
|
+
STDERR.puts commands
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
end.parse!
|
40
|
+
|
41
|
+
config = Typingpool::App::CLI.config_from_arg(options[:config]) or abort "No config file at '#{options[:config]}'"
|
42
|
+
options[:banner] += "\n#{Typingpool::App::CLI.help_arg_explanation}\n"
|
43
|
+
|
44
|
+
if ARGV.count > 0
|
45
|
+
if options[:project]
|
46
|
+
abort "Duplicate project values ('#{ARGV[0]}' and '#{options[:project]}')"
|
47
|
+
end
|
48
|
+
options[:project] = ARGV.shift
|
49
|
+
end
|
50
|
+
abort "Unexpected argument(s): #{ARGV.join(';')}" if ARGV.count > 0
|
51
|
+
|
52
|
+
if options[:project]
|
53
|
+
options[:project] = Typingpool::App::CLI.project_from_arg_and_config(options[:project], config)
|
54
|
+
end
|
55
|
+
|
56
|
+
if options[:fixture]
|
57
|
+
Typingpool::App.vcr_record(options[:fixture], config)
|
58
|
+
end
|
59
|
+
|
60
|
+
Typingpool::Amazon.setup(:sandbox => options[:sandbox], :config => config)
|
61
|
+
|
62
|
+
assignments = Typingpool::App.assignments_file_for_sandbox_status(options[:sandbox], options[:project]) if options[:project]
|
63
|
+
STDERR.puts "Gathering submissions from Amazon"
|
64
|
+
hits = if options[:project] && ((hit_ids = assignments.map{|assignment| assignment['hit_id'] }.select{|hit_id| hit_id }).count > 0)
|
65
|
+
Typingpool::Amazon::HIT.with_ids(hit_ids).select{|hit| hit.submitted? }
|
66
|
+
else
|
67
|
+
hits = Typingpool::Amazon::HIT.all_reviewable{|hit| hit.submitted? && hit.ours? }
|
68
|
+
if options[:project]
|
69
|
+
#code path for projects assigned through RUI
|
70
|
+
hits.select!{|hit| hit.project_id == options[:project].local.id }
|
71
|
+
end
|
72
|
+
hits
|
73
|
+
end
|
74
|
+
|
75
|
+
STDERR.puts "Matching submissions with local projects"
|
76
|
+
choices = %w(approve reject quit skip)
|
77
|
+
catch :quitting do
|
78
|
+
project_hits = Typingpool::App.find_projects_waiting_for_hits(hits, config)
|
79
|
+
all_hits = project_hits.map{|hash| hash[:hits] }.flatten
|
80
|
+
i = 0
|
81
|
+
project_hits.each do |hash|
|
82
|
+
hits = hash[:hits]
|
83
|
+
project = hash[:project]
|
84
|
+
assignments = Typingpool::App.assignments_file_for_sandbox_status(options[:sandbox], project)
|
85
|
+
hits.sort!{|a, b| a.url <=> b.url }
|
86
|
+
hits.each do |hit|
|
87
|
+
i += 1
|
88
|
+
say(cli_bold("\nTranscript for: ") + hit.url)
|
89
|
+
project_info = cli_bold("Project: ") + project.name
|
90
|
+
if project.local.subtitle
|
91
|
+
project_info += ": #{project.local.subtitle}"
|
92
|
+
end
|
93
|
+
say(project_info)
|
94
|
+
say(cli_bold("Submitted at: ") +
|
95
|
+
hit.assignment.submitted_at.localtime.to_s +
|
96
|
+
cli_bold(" by ") +
|
97
|
+
hit.assignment.worker_id)
|
98
|
+
say(hit.transcript.body_as_text(4, 68))
|
99
|
+
prompt = choices.map do |c|
|
100
|
+
cli_reverse('(') +
|
101
|
+
cli_reverse(cli_bold(c.slice(0).upcase)) +
|
102
|
+
cli_reverse(")#{c.slice(1, c.size)}")
|
103
|
+
end
|
104
|
+
prompt << cli_reverse('[') + prompt.pop + cli_reverse(']')
|
105
|
+
prompt = prompt.join(cli_reverse(', '))
|
106
|
+
prompt += cli_reverse('? ')
|
107
|
+
prompt += cli_reverse("(#{i}/#{all_hits.count}) ")
|
108
|
+
|
109
|
+
choice=nil
|
110
|
+
until choice
|
111
|
+
input = ask(prompt)
|
112
|
+
if input.to_s.match(/^\s*$/)
|
113
|
+
choice = choices.last
|
114
|
+
elsif not(choice = choices.detect{|possible| possible[0] == input.downcase[0] })
|
115
|
+
say("Invalid selection '#{input}'.")
|
116
|
+
end
|
117
|
+
end #until choice
|
118
|
+
|
119
|
+
case choice
|
120
|
+
when 'approve'
|
121
|
+
begin
|
122
|
+
hit.assignment.at_amazon.approve!
|
123
|
+
rescue RTurk::InvalidRequest => exception
|
124
|
+
#assignment may have passed its approval deadline since
|
125
|
+
#the session started and has been auto approved. In that
|
126
|
+
#case, add to the transcript; no need to tell the user.
|
127
|
+
unless exception.message.match(/AWS.MechanicalTurk.InvalidAssignmentState\b.+\bstatus of:\s*Submitted/i)
|
128
|
+
#different issue than auto approval; raise exception
|
129
|
+
raise exception
|
130
|
+
end
|
131
|
+
end #begin
|
132
|
+
Typingpool::App.record_approved_hits_in_assignments_file(assignments, [hit])
|
133
|
+
Typingpool::App.create_transcript(project, assignments)
|
134
|
+
say(cli_bold("Approved. ") + "#{project.name} transcript updated.\n")
|
135
|
+
when 'reject'
|
136
|
+
reason=nil
|
137
|
+
until reason.to_s.match(/\S/)
|
138
|
+
reason = ask("Rejection reason, for worker: ")
|
139
|
+
end
|
140
|
+
begin
|
141
|
+
hit.assignment.at_amazon.reject!(reason)
|
142
|
+
hit.at_amazon.dispose!
|
143
|
+
rescue RTurk::InvalidRequest => exception
|
144
|
+
#see comment under 'approve' case above
|
145
|
+
if exception.message.match(/AWS.MechanicalTurk.InvalidAssignmentState\b.+\bstatus of:\s*Submitted/i)
|
146
|
+
say(cli_bold("Not rejected:") + " This assignment passed its approval deadline since this session began and the assignment has been auto-approved. It will be deleted, but you've been charged for this assignment and your rejection message will not be delivered.")
|
147
|
+
hit.remove_from_amazon
|
148
|
+
else
|
149
|
+
raise exception
|
150
|
+
end
|
151
|
+
else
|
152
|
+
say(cli_bold("Rejected\n"))
|
153
|
+
end #begin
|
154
|
+
Typingpool::App.unrecord_hits_in_assignments_file(assignments, [hit])
|
155
|
+
when 'quit'
|
156
|
+
say(cli_bold("Quitting"))
|
157
|
+
throw :quitting
|
158
|
+
when 'skip'
|
159
|
+
say(cli_bold("Skipping\n"))
|
160
|
+
next
|
161
|
+
end #case choice
|
162
|
+
end #hits.each_with_index
|
163
|
+
|
164
|
+
end #find_projects_waiting_for(...) do
|
165
|
+
end #catch :quitting do
|
166
|
+
|
167
|
+
#Put project path on STDOUT so script can be pipelined, e.g.
|
168
|
+
#`tp-review Foo | tp-finish`
|
169
|
+
if options[:project]
|
170
|
+
puts options[:project].local.path
|
171
|
+
end
|
172
|
+
|
173
|
+
if options[:fixture]
|
174
|
+
Typingpool::App.vcr_stop
|
175
|
+
end
|