adhearsion 2.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +4 -3
- data/CHANGELOG.md +30 -0
- data/README.markdown +1 -0
- data/adhearsion.gemspec +3 -4
- data/bin/ahn +0 -20
- data/features/cli_create.feature +1 -1
- data/features/cli_restart.feature +25 -1
- data/features/cli_start.feature +0 -2
- data/features/plugin_generator.feature +66 -15
- data/features/support/env.rb +0 -13
- data/lib/adhearsion.rb +26 -6
- data/lib/adhearsion/call.rb +42 -7
- data/lib/adhearsion/call_controller.rb +5 -2
- data/lib/adhearsion/call_controller/dial.rb +92 -50
- data/lib/adhearsion/call_controller/input.rb +19 -6
- data/lib/adhearsion/call_controller/menu_dsl/menu.rb +4 -0
- data/lib/adhearsion/call_controller/output.rb +143 -161
- data/lib/adhearsion/call_controller/output/abstract_player.rb +30 -0
- data/lib/adhearsion/call_controller/output/async_player.rb +26 -0
- data/lib/adhearsion/call_controller/output/formatter.rb +81 -0
- data/lib/adhearsion/call_controller/output/player.rb +25 -0
- data/lib/adhearsion/call_controller/record.rb +19 -2
- data/lib/adhearsion/events.rb +3 -0
- data/lib/adhearsion/foundation.rb +12 -6
- data/lib/adhearsion/foundation/exception_handler.rb +8 -6
- data/lib/adhearsion/generators/app/templates/README.md +13 -0
- data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +7 -1
- data/lib/adhearsion/generators/plugin/plugin_generator.rb +1 -0
- data/lib/adhearsion/generators/plugin/templates/plugin-template.gemspec.tt +3 -7
- data/lib/adhearsion/generators/plugin/templates/spec/spec_helper.rb.tt +0 -1
- data/lib/adhearsion/outbound_call.rb +15 -5
- data/lib/adhearsion/punchblock_plugin.rb +13 -2
- data/lib/adhearsion/punchblock_plugin/initializer.rb +13 -12
- data/lib/adhearsion/router.rb +43 -2
- data/lib/adhearsion/router/evented_route.rb +15 -0
- data/lib/adhearsion/router/openended_route.rb +16 -0
- data/lib/adhearsion/router/route.rb +31 -13
- data/lib/adhearsion/router/unaccepting_route.rb +11 -0
- data/lib/adhearsion/version.rb +1 -1
- data/pre-commit +14 -1
- data/spec/adhearsion/call_controller/dial_spec.rb +105 -10
- data/spec/adhearsion/call_controller/input_spec.rb +19 -21
- data/spec/adhearsion/call_controller/output/async_player_spec.rb +67 -0
- data/spec/adhearsion/call_controller/output/formatter_spec.rb +90 -0
- data/spec/adhearsion/call_controller/output/player_spec.rb +65 -0
- data/spec/adhearsion/call_controller/output_spec.rb +436 -190
- data/spec/adhearsion/call_controller/record_spec.rb +49 -6
- data/spec/adhearsion/call_controller_spec.rb +10 -2
- data/spec/adhearsion/call_spec.rb +138 -0
- data/spec/adhearsion/calls_spec.rb +1 -1
- data/spec/adhearsion/outbound_call_spec.rb +48 -8
- data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +34 -23
- data/spec/adhearsion/router/evented_route_spec.rb +34 -0
- data/spec/adhearsion/router/openended_route_spec.rb +61 -0
- data/spec/adhearsion/router/route_spec.rb +26 -4
- data/spec/adhearsion/router/unaccepting_route_spec.rb +72 -0
- data/spec/adhearsion/router_spec.rb +107 -2
- data/spec/adhearsion_spec.rb +19 -0
- data/spec/capture_warnings.rb +28 -21
- data/spec/spec_helper.rb +2 -3
- data/spec/support/call_controller_test_helpers.rb +31 -30
- metadata +32 -29
@@ -26,11 +26,13 @@ module Adhearsion
|
|
26
26
|
# @param [Hash] options see below
|
27
27
|
#
|
28
28
|
# @option options [String] :from the caller id to be used when the call is placed. It is advised you properly adhere to the
|
29
|
-
# policy of VoIP termination providers with respect to caller id values.
|
29
|
+
# policy of VoIP termination providers with respect to caller id values. Defaults to the caller ID of the dialing call, so for normal bridging scenarios, you do not need to set this.
|
30
30
|
#
|
31
31
|
# @option options [Numeric] :for this option can be thought of best as a timeout.
|
32
32
|
# i.e. timeout after :for if no one answers the call
|
33
33
|
#
|
34
|
+
# @option options [CallController] :confirm the controller to execute on answered outbound calls to give an opportunity to screen the call. The calls will be joined if the outbound call is still active after this controller completes.
|
35
|
+
#
|
34
36
|
# @example Make a call to the PSTN using my SIP provider for VoIP termination
|
35
37
|
# dial "SIP/19095551001@my.sip.voip.terminator.us"
|
36
38
|
#
|
@@ -43,77 +45,112 @@ module Adhearsion
|
|
43
45
|
# @return [DialStatus] the status of the dial operation
|
44
46
|
#
|
45
47
|
def dial(to, options = {}, latch = nil)
|
46
|
-
|
48
|
+
dial = Dial.new to, options, latch, call
|
49
|
+
dial.run
|
50
|
+
dial.await_completion
|
51
|
+
dial.cleanup_calls
|
52
|
+
dial.status
|
53
|
+
end
|
47
54
|
|
48
|
-
|
55
|
+
class Dial
|
56
|
+
attr_accessor :status
|
49
57
|
|
50
|
-
|
58
|
+
def initialize(to, options, latch, call)
|
59
|
+
raise Call::Hangup unless call.alive? && call.active?
|
60
|
+
@options, @latch, @call = options, latch, call
|
61
|
+
@targets = to.respond_to?(:has_key?) ? to : Array(to)
|
62
|
+
set_defaults
|
63
|
+
end
|
51
64
|
|
52
|
-
|
65
|
+
def set_defaults
|
66
|
+
@status = DialStatus.new
|
53
67
|
|
54
|
-
|
55
|
-
options[:timeout] ||= _for if _for
|
68
|
+
@latch ||= CountDownLatch.new @targets.size
|
56
69
|
|
57
|
-
|
70
|
+
@options[:from] ||= @call.from
|
58
71
|
|
59
|
-
|
60
|
-
|
72
|
+
_for = @options.delete :for
|
73
|
+
@options[:timeout] ||= _for if _for
|
61
74
|
|
62
|
-
|
63
|
-
|
64
|
-
begin
|
65
|
-
next if call_to_hangup.id == new_call.id
|
66
|
-
logger.debug "#dial hanging up call #{call_to_hangup.id} because this call has been answered by another channel"
|
67
|
-
call_to_hangup.hangup
|
68
|
-
rescue Celluloid::DeadActorError
|
69
|
-
# This actor may previously have been shut down due to the call ending
|
70
|
-
end
|
71
|
-
end
|
75
|
+
@confirmation_controller = @options.delete :confirm
|
76
|
+
end
|
72
77
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
78
|
+
def run
|
79
|
+
track_originating_call
|
80
|
+
prep_calls
|
81
|
+
place_calls
|
82
|
+
end
|
83
|
+
|
84
|
+
def track_originating_call
|
85
|
+
@call.on_end { |_| @latch.countdown! until @latch.count == 0 }
|
86
|
+
end
|
87
|
+
|
88
|
+
def prep_calls
|
89
|
+
@calls = @targets.map do |target, specific_options|
|
90
|
+
new_call = OutboundCall.new
|
91
|
+
|
92
|
+
new_call.on_end do |event|
|
93
|
+
@latch.countdown! unless new_call["dial_countdown_#{@call.id}"]
|
94
|
+
status.error! if event.reason == :error
|
77
95
|
end
|
78
96
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
97
|
+
new_call.on_answer do |event|
|
98
|
+
@calls.each do |call_to_hangup, _|
|
99
|
+
begin
|
100
|
+
next if call_to_hangup.id == new_call.id
|
101
|
+
logger.debug "#dial hanging up call #{call_to_hangup.id} because this call has been answered by another channel"
|
102
|
+
call_to_hangup.hangup
|
103
|
+
rescue Celluloid::DeadActorError
|
104
|
+
# This actor may previously have been shut down due to the call ending
|
105
|
+
end
|
106
|
+
end
|
83
107
|
|
84
|
-
|
85
|
-
|
108
|
+
new_call.on_unjoined @call do |unjoined|
|
109
|
+
new_call["dial_countdown_#{@call.id}"] = true
|
110
|
+
@latch.countdown!
|
111
|
+
end
|
112
|
+
|
113
|
+
if @confirmation_controller
|
114
|
+
status.unconfirmed!
|
115
|
+
new_call.execute_controller @confirmation_controller.new(new_call), lambda { |call| call.signal :confirmed }
|
116
|
+
new_call.wait :confirmed
|
117
|
+
end
|
86
118
|
|
87
|
-
|
88
|
-
|
89
|
-
|
119
|
+
if new_call.alive? && new_call.active?
|
120
|
+
logger.debug "#dial joining call #{new_call.id} to #{@call.id}"
|
121
|
+
new_call.join @call
|
122
|
+
status.answer!
|
123
|
+
end
|
90
124
|
end
|
125
|
+
|
126
|
+
[new_call, target, specific_options]
|
91
127
|
end
|
92
128
|
|
93
|
-
|
129
|
+
status.calls = @calls
|
94
130
|
end
|
95
131
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
132
|
+
def place_calls
|
133
|
+
@calls.map! do |call, target, specific_options|
|
134
|
+
local_options = @options.dup.deep_merge specific_options if specific_options
|
135
|
+
call.dial target, (local_options || @options)
|
136
|
+
call
|
137
|
+
end
|
100
138
|
end
|
101
139
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
status.timeout! unless no_timeout
|
140
|
+
def await_completion
|
141
|
+
@latch.wait(@options[:timeout]) || status.timeout!
|
142
|
+
end
|
106
143
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
144
|
+
def cleanup_calls
|
145
|
+
logger.debug "#dial finished. Hanging up #{@calls.size} outbound calls: #{@calls.map(&:id).join ", "}."
|
146
|
+
@calls.each do |outbound_call|
|
147
|
+
begin
|
148
|
+
outbound_call.hangup
|
149
|
+
rescue Celluloid::DeadActorError
|
150
|
+
# This actor may previously have been shut down due to the call ending
|
151
|
+
end
|
113
152
|
end
|
114
153
|
end
|
115
|
-
|
116
|
-
status
|
117
154
|
end
|
118
155
|
|
119
156
|
class DialStatus
|
@@ -147,6 +184,11 @@ module Adhearsion
|
|
147
184
|
def error!
|
148
185
|
@result ||= :error
|
149
186
|
end
|
187
|
+
|
188
|
+
# @private
|
189
|
+
def unconfirmed!
|
190
|
+
@result ||= :unconfirmed
|
191
|
+
end
|
150
192
|
end
|
151
193
|
|
152
194
|
end
|
@@ -8,6 +8,10 @@ module Adhearsion
|
|
8
8
|
def to_s
|
9
9
|
response
|
10
10
|
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
"#<Adhearsion::CallController::Input::Result response=#{response.inspect}, status=#{status.inspect}>"
|
14
|
+
end
|
11
15
|
end
|
12
16
|
|
13
17
|
#
|
@@ -46,12 +50,21 @@ module Adhearsion
|
|
46
50
|
menu_instance.validate :basic
|
47
51
|
result_of_menu = nil
|
48
52
|
|
49
|
-
|
50
|
-
|
53
|
+
catch :finish do
|
54
|
+
until MenuDSL::Menu::MenuResultDone === result_of_menu
|
55
|
+
raise unless menu_instance.should_continue?
|
56
|
+
|
57
|
+
result_of_menu = menu_instance.continue
|
51
58
|
|
52
|
-
|
53
|
-
|
54
|
-
|
59
|
+
if result_of_menu.is_a?(MenuDSL::Menu::MenuGetAnotherDigit)
|
60
|
+
next_digit = play_sound_files_for_menu menu_instance, sound_files
|
61
|
+
if next_digit
|
62
|
+
menu_instance << next_digit
|
63
|
+
else
|
64
|
+
menu_instance.timeout!
|
65
|
+
throw :finish
|
66
|
+
end
|
67
|
+
end
|
55
68
|
end
|
56
69
|
end
|
57
70
|
|
@@ -194,7 +207,7 @@ module Adhearsion
|
|
194
207
|
def wait_for_digit(timeout = 1)
|
195
208
|
timeout = nil if timeout == -1
|
196
209
|
timeout *= 1_000 if timeout
|
197
|
-
input_component = execute_component_and_await_completion
|
210
|
+
input_component = execute_component_and_await_completion Punchblock::Component::Input.new :mode => :dtmf,
|
198
211
|
:initial_timeout => timeout,
|
199
212
|
:inter_digit_timeout => timeout,
|
200
213
|
:grammar => {
|
@@ -3,6 +3,13 @@
|
|
3
3
|
module Adhearsion
|
4
4
|
class CallController
|
5
5
|
module Output
|
6
|
+
extend ActiveSupport::Autoload
|
7
|
+
|
8
|
+
autoload :AbstractPlayer
|
9
|
+
autoload :AsyncPlayer
|
10
|
+
autoload :Formatter
|
11
|
+
autoload :Player
|
12
|
+
|
6
13
|
PlaybackError = Class.new Adhearsion::Error # Represents failure to play audio, such as when the sound file cannot be found
|
7
14
|
|
8
15
|
#
|
@@ -11,11 +18,26 @@ module Adhearsion
|
|
11
18
|
# @param [String, #to_s] text The text to be rendered
|
12
19
|
# @param [Hash] options A set of options for output
|
13
20
|
#
|
21
|
+
# @raises [PlaybackError] if the given argument could not be played
|
22
|
+
#
|
14
23
|
def say(text, options = {})
|
15
|
-
play_ssml(text, options) || output(ssml_for_text(text.to_s), options)
|
24
|
+
player.play_ssml(text, options) || player.output(Formatter.ssml_for_text(text.to_s), options)
|
16
25
|
end
|
17
26
|
alias :speak :say
|
18
27
|
|
28
|
+
#
|
29
|
+
# Speak output using text-to-speech (TTS) and return as soon as it begins
|
30
|
+
#
|
31
|
+
# @param [String, #to_s] text The text to be rendered
|
32
|
+
# @param [Hash] options A set of options for output
|
33
|
+
#
|
34
|
+
# @raises [PlaybackError] if the given argument could not be played
|
35
|
+
#
|
36
|
+
def say!(text, options = {})
|
37
|
+
async_player.play_ssml(text, options) || async_player.output(Formatter.ssml_for_text(text.to_s), options)
|
38
|
+
end
|
39
|
+
alias :speak! :say!
|
40
|
+
|
19
41
|
#
|
20
42
|
# Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now),
|
21
43
|
# Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. To specify how the Date/Time objects are said
|
@@ -36,34 +58,70 @@ module Adhearsion
|
|
36
58
|
# @example Play two sound files
|
37
59
|
# play "/path/to/you-sound-cute.mp3", "/path/to/what-are-you-wearing.wav"
|
38
60
|
#
|
39
|
-
# @
|
40
|
-
# some sound file(s) could not be played.
|
41
|
-
#
|
42
|
-
# @see play_time
|
43
|
-
# @see play_numeric
|
44
|
-
# @see play_audio
|
61
|
+
# @raises [PlaybackError] if (one of) the given argument(s) could not be played
|
45
62
|
#
|
46
63
|
def play(*arguments)
|
47
|
-
|
48
|
-
|
49
|
-
when Hash
|
50
|
-
play_ssml_for argument.delete(:value), argument
|
51
|
-
when RubySpeech::SSML::Speak
|
52
|
-
play_ssml argument
|
53
|
-
else
|
54
|
-
play_ssml_for argument
|
55
|
-
end
|
56
|
-
end
|
64
|
+
player.play_ssml Formatter.ssml_for_collection(arguments)
|
65
|
+
true
|
57
66
|
end
|
58
67
|
|
59
68
|
#
|
60
|
-
# Plays the specified
|
61
|
-
#
|
69
|
+
# Plays the specified sound file names and returns as soon as it begins. This method will handle Time/DateTime objects (e.g. Time.now),
|
70
|
+
# Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. To specify how the Date/Time objects are said
|
71
|
+
# pass in as an array with the first parameter as the Date/Time/DateTime object along with a hash with the
|
72
|
+
# additional options. See play_time for more information.
|
62
73
|
#
|
63
|
-
# @
|
74
|
+
# @example Play file hello-world
|
75
|
+
# play 'http://www.example.com/hello-world.mp3'
|
76
|
+
# play '/path/on/disk/hello-world.wav'
|
77
|
+
# @example Speak current time
|
78
|
+
# play Time.now
|
79
|
+
# @example Speak today's date
|
80
|
+
# play Date.today
|
81
|
+
# @example Speak today's date in a specific format
|
82
|
+
# play Date.today, :strftime => "%d/%m/%Y", :format => "dmy"
|
83
|
+
# @example Play sound file, speak number, play two more sound files
|
84
|
+
# play %w"http://www.example.com/a-connect-charge-of.wav 22 /path/to/cents-per-minute.wav /path/to/will-apply.mp3"
|
85
|
+
# @example Play two sound files
|
86
|
+
# play "/path/to/you-sound-cute.mp3", "/path/to/what-are-you-wearing.wav"
|
87
|
+
#
|
88
|
+
# @raises [PlaybackError] if (one of) the given argument(s) could not be played
|
89
|
+
# @returns [Punchblock::Component::Output]
|
64
90
|
#
|
65
91
|
def play!(*arguments)
|
66
|
-
|
92
|
+
async_player.play_ssml Formatter.ssml_for_collection(arguments)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Plays the given audio file.
|
97
|
+
# SSML supports http:// paths and full disk paths.
|
98
|
+
# The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory.
|
99
|
+
#
|
100
|
+
# @param [String] file http:// URL or full disk path to the sound file
|
101
|
+
# @param [Hash] options Additional options to specify how exactly to say time specified.
|
102
|
+
# @option options [String] :fallback The text to play if the file is not available
|
103
|
+
#
|
104
|
+
# @raises [PlaybackError] if (one of) the given argument(s) could not be played
|
105
|
+
#
|
106
|
+
def play_audio(file, options = nil)
|
107
|
+
player.play_ssml Formatter.ssml_for_audio(file, options)
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Plays the given audio file and returns as soon as it begins.
|
113
|
+
# SSML supports http:// paths and full disk paths.
|
114
|
+
# The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory.
|
115
|
+
#
|
116
|
+
# @param [String] file http:// URL or full disk path to the sound file
|
117
|
+
# @param [Hash] options Additional options to specify how exactly to say time specified.
|
118
|
+
# @option options [String] :fallback The text to play if the file is not available
|
119
|
+
#
|
120
|
+
# @raises [PlaybackError] if (one of) the given argument(s) could not be played
|
121
|
+
# @returns [Punchblock::Component::Output]
|
122
|
+
#
|
123
|
+
def play_audio!(file, options = nil)
|
124
|
+
async_player.play_ssml Formatter.ssml_for_audio(file, options)
|
67
125
|
end
|
68
126
|
|
69
127
|
#
|
@@ -79,71 +137,63 @@ module Adhearsion
|
|
79
137
|
# @option options [String] :strftime This format is what defines the string that is sent to the Speech Synthesis Engine.
|
80
138
|
# It uses Time::strftime symbols.
|
81
139
|
#
|
82
|
-
# @
|
140
|
+
# @raises [ArgumentError] if the given argument can not be played
|
83
141
|
#
|
84
142
|
def play_time(time, options = {})
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
play_ssml ssml_for_time(time, options)
|
143
|
+
raise ArgumentError unless [Date, Time, DateTime].include?(time.class) && options.is_a?(Hash)
|
144
|
+
player.play_ssml Formatter.ssml_for_time(time, options)
|
145
|
+
true
|
89
146
|
end
|
90
147
|
|
91
148
|
#
|
92
|
-
# Plays the given
|
93
|
-
#
|
94
|
-
# is pronounced as "one hundred" instead of "one zero zero".
|
149
|
+
# Plays the given Date, Time, or Integer (seconds since epoch)
|
150
|
+
# using the given timezone and format and returns as soon as it begins.
|
95
151
|
#
|
96
|
-
# @param [
|
152
|
+
# @param [Date, Time, DateTime] time Time to be said.
|
153
|
+
# @param [Hash] options Additional options to specify how exactly to say time specified.
|
154
|
+
# @option options [String] :format This format is used only to disambiguate times that could be interpreted in different ways.
|
155
|
+
# For example, 01/06/2011 could mean either the 1st of June or the 6th of January.
|
156
|
+
# Please refer to the SSML specification.
|
157
|
+
# @see http://www.w3.org/TR/ssml-sayas/#S3.1
|
158
|
+
# @option options [String] :strftime This format is what defines the string that is sent to the Speech Synthesis Engine.
|
159
|
+
# It uses Time::strftime symbols.
|
97
160
|
#
|
98
|
-
# @
|
161
|
+
# @raises [ArgumentError] if the given argument can not be played
|
162
|
+
# @returns [Punchblock::Component::Output]
|
99
163
|
#
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
end
|
164
|
+
def play_time!(time, options = {})
|
165
|
+
raise ArgumentError unless [Date, Time, DateTime].include?(time.class) && options.is_a?(Hash)
|
166
|
+
async_player.play_ssml Formatter.ssml_for_time(time, options)
|
104
167
|
end
|
105
168
|
|
106
169
|
#
|
107
|
-
# Plays the given
|
108
|
-
#
|
109
|
-
#
|
170
|
+
# Plays the given Numeric argument or string representing a decimal number.
|
171
|
+
# When playing numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
|
172
|
+
# is pronounced as "one hundred" instead of "one zero zero".
|
110
173
|
#
|
111
|
-
# @param [String]
|
112
|
-
# @param [Hash] options Additional options to specify how exactly to say time specified.
|
113
|
-
# @option options [String] :fallback The text to play if the file is not available
|
174
|
+
# @param [Numeric, String] Numeric or String containing a valid Numeric, like "321".
|
114
175
|
#
|
115
|
-
# @
|
176
|
+
# @raises [ArgumentError] if the given argument can not be played
|
116
177
|
#
|
117
|
-
def
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
# @private
|
122
|
-
def play_ssml(ssml, options = {})
|
123
|
-
if [RubySpeech::SSML::Speak, Nokogiri::XML::Document].include? ssml.class
|
124
|
-
output ssml.to_s, options
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
# @private
|
129
|
-
def output(content, options = {})
|
130
|
-
options.merge! :ssml => content
|
131
|
-
execute_component_and_await_completion ::Punchblock::Component::Output.new(options)
|
178
|
+
def play_numeric(number)
|
179
|
+
raise ArgumentError unless number.kind_of?(Numeric) || number =~ /^\d+$/
|
180
|
+
player.play_ssml Formatter.ssml_for_numeric(number)
|
181
|
+
true
|
132
182
|
end
|
133
183
|
|
134
184
|
#
|
135
|
-
#
|
136
|
-
#
|
185
|
+
# Plays the given Numeric argument or string representing a decimal number and returns as soon as it begins.
|
186
|
+
# When playing numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
|
187
|
+
# is pronounced as "one hundred" instead of "one zero zero".
|
188
|
+
#
|
189
|
+
# @param [Numeric, String] Numeric or String containing a valid Numeric, like "321".
|
137
190
|
#
|
138
|
-
# @
|
191
|
+
# @raises [ArgumentError] if the given argument can not be played
|
192
|
+
# @returns [Punchblock::Component::Output]
|
139
193
|
#
|
140
|
-
def
|
141
|
-
|
142
|
-
|
143
|
-
result = stream_file output
|
144
|
-
break unless result.nil?
|
145
|
-
end
|
146
|
-
result
|
194
|
+
def play_numeric!(number)
|
195
|
+
raise ArgumentError unless number.kind_of?(Numeric) || number =~ /^\d+$/
|
196
|
+
async_player.play_ssml Formatter.ssml_for_numeric(number)
|
147
197
|
end
|
148
198
|
|
149
199
|
#
|
@@ -161,122 +211,54 @@ module Adhearsion
|
|
161
211
|
# @param [Hash] Additional options.
|
162
212
|
#
|
163
213
|
# @return [String, nil] The single DTMF character entered by the user, or nil if nothing was entered
|
214
|
+
# @raises [PlaybackError] if (one of) the given argument(s) could not be played
|
164
215
|
#
|
165
216
|
def interruptible_play(*outputs)
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
result = interruptible_play! output
|
170
|
-
rescue PlaybackError => e
|
171
|
-
# Ignore this exception and play the next output
|
172
|
-
logger.error "Error playing back the prompt: #{e.message}"
|
173
|
-
ensure
|
174
|
-
break if result
|
175
|
-
end
|
176
|
-
end
|
177
|
-
result
|
178
|
-
end
|
179
|
-
|
180
|
-
# @private
|
181
|
-
def detect_type(output)
|
182
|
-
result = nil
|
183
|
-
result = :time if [Date, Time, DateTime].include? output.class
|
184
|
-
result = :numeric if output.kind_of?(Numeric) || output =~ /^\d+$/
|
185
|
-
result = :audio if !result && (/^\//.match(output.to_s) || URI::regexp.match(output.to_s))
|
186
|
-
result ||= :text
|
187
|
-
end
|
188
|
-
|
189
|
-
# @private
|
190
|
-
def play_ssml_for(*args)
|
191
|
-
play_ssml ssml_for(args)
|
192
|
-
end
|
193
|
-
|
194
|
-
#
|
195
|
-
# Generates SSML for the argument and options passed, using automatic detection
|
196
|
-
# Directly returns the argument if it is already an SSML document
|
197
|
-
#
|
198
|
-
# @param [String, Hash, RubySpeech::SSML::Speak] the argument with options as accepted by the play_ methods, or an SSML document
|
199
|
-
# @return [RubySpeech::SSML::Speak] an SSML document
|
200
|
-
#
|
201
|
-
# @private
|
202
|
-
#
|
203
|
-
def ssml_for(*args)
|
204
|
-
return args[0] if args.size == 1 && args[0].is_a?(RubySpeech::SSML::Speak)
|
205
|
-
argument, options = args.flatten
|
206
|
-
options ||= {}
|
207
|
-
type = detect_type argument
|
208
|
-
send "ssml_for_#{type}", argument, options
|
209
|
-
end
|
210
|
-
|
211
|
-
# @private
|
212
|
-
def ssml_for_text(argument, options = {})
|
213
|
-
RubySpeech::SSML.draw { argument }
|
214
|
-
end
|
215
|
-
|
216
|
-
# @private
|
217
|
-
def ssml_for_time(argument, options = {})
|
218
|
-
interpretation = case argument
|
219
|
-
when Date then 'date'
|
220
|
-
when Time then 'time'
|
221
|
-
end
|
222
|
-
|
223
|
-
format = options.delete :format
|
224
|
-
strftime = options.delete :strftime
|
225
|
-
|
226
|
-
time_to_say = strftime ? argument.strftime(strftime) : argument.to_s
|
227
|
-
|
228
|
-
RubySpeech::SSML.draw do
|
229
|
-
say_as(:interpret_as => interpretation, :format => format) { time_to_say }
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
# @private
|
234
|
-
def ssml_for_numeric(argument, options = {})
|
235
|
-
RubySpeech::SSML.draw do
|
236
|
-
say_as(:interpret_as => 'cardinal') { argument.to_s }
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
# @private
|
241
|
-
def ssml_for_audio(argument, options = {})
|
242
|
-
fallback = (options || {}).delete :fallback
|
243
|
-
RubySpeech::SSML.draw do
|
244
|
-
audio(:src => argument) { fallback }
|
217
|
+
outputs.find do |output|
|
218
|
+
digit = stream_file output
|
219
|
+
return digit if digit
|
245
220
|
end
|
246
221
|
end
|
247
222
|
|
248
223
|
#
|
249
224
|
# Plays a single output, not only files, accepting interruption by one of the digits specified
|
250
|
-
# Currently still stops execution, will be fixed soon in Punchblock
|
251
225
|
#
|
252
226
|
# @param [Object] String or Hash specifying output and options
|
253
227
|
# @param [String] String with the digits that are allowed to interrupt output
|
254
228
|
#
|
255
229
|
# @return [String, nil] The pressed digit, or nil if nothing was pressed
|
230
|
+
# @private
|
256
231
|
#
|
257
232
|
def stream_file(argument, digits = '0123456789#*')
|
258
233
|
result = nil
|
259
|
-
|
260
|
-
output_component = ::Punchblock::Component::Output.new :ssml => ssml.to_s
|
261
|
-
input_stopper_component = ::Punchblock::Component::Input.new :mode => :dtmf,
|
234
|
+
stopper = Punchblock::Component::Input.new :mode => :dtmf,
|
262
235
|
:grammar => {
|
263
|
-
:value => grammar_accept(digits)
|
236
|
+
:value => grammar_accept(digits)
|
264
237
|
}
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
rescue ::Punchblock::ProtocolError => e
|
272
|
-
raise PlaybackError, "Output failed for argument #{argument.inspect} due to #{e.inspect}"
|
238
|
+
|
239
|
+
player.output Formatter.ssml_for(argument) do |output_component|
|
240
|
+
stopper.register_event_handler Punchblock::Event::Complete do |event|
|
241
|
+
output_component.stop! unless output_component.complete?
|
242
|
+
end
|
243
|
+
write_and_await_response stopper
|
273
244
|
end
|
274
|
-
|
275
|
-
|
245
|
+
|
246
|
+
stopper.stop! if stopper.executing?
|
247
|
+
reason = stopper.complete_event.reason
|
276
248
|
result = reason.interpretation if reason.respond_to? :interpretation
|
277
249
|
return parse_single_dtmf result unless result.nil?
|
278
250
|
result
|
279
251
|
end
|
252
|
+
|
253
|
+
# @private
|
254
|
+
def player
|
255
|
+
@player ||= Player.new(self)
|
256
|
+
end
|
257
|
+
|
258
|
+
# @private
|
259
|
+
def async_player
|
260
|
+
@async_player ||= AsyncPlayer.new(self)
|
261
|
+
end
|
280
262
|
end # Output
|
281
263
|
end # CallController
|
282
264
|
end # Adhearsion
|