punchblock 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +5 -0
- data/lib/punchblock.rb +1 -1
- data/lib/punchblock/connection.rb +1 -0
- data/lib/punchblock/connection/asterisk.rb +0 -1
- data/lib/punchblock/connection/freeswitch.rb +49 -0
- data/lib/punchblock/event/offer.rb +1 -1
- data/lib/punchblock/translator.rb +5 -0
- data/lib/punchblock/translator/asterisk.rb +16 -28
- data/lib/punchblock/translator/asterisk/call.rb +4 -21
- data/lib/punchblock/translator/asterisk/component.rb +0 -5
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +0 -3
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +0 -1
- data/lib/punchblock/translator/asterisk/component/input.rb +7 -97
- data/lib/punchblock/translator/asterisk/component/output.rb +0 -4
- data/lib/punchblock/translator/asterisk/component/record.rb +0 -2
- data/lib/punchblock/translator/freeswitch.rb +153 -0
- data/lib/punchblock/translator/freeswitch/call.rb +265 -0
- data/lib/punchblock/translator/freeswitch/component.rb +92 -0
- data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +57 -0
- data/lib/punchblock/translator/freeswitch/component/flite_output.rb +17 -0
- data/lib/punchblock/translator/freeswitch/component/input.rb +29 -0
- data/lib/punchblock/translator/freeswitch/component/output.rb +56 -0
- data/lib/punchblock/translator/freeswitch/component/record.rb +79 -0
- data/lib/punchblock/translator/freeswitch/component/tts_output.rb +26 -0
- data/lib/punchblock/translator/input_component.rb +108 -0
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +3 -2
- data/spec/punchblock/connection/freeswitch_spec.rb +90 -0
- data/spec/punchblock/translator/asterisk/call_spec.rb +23 -2
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +3 -3
- data/spec/punchblock/translator/asterisk_spec.rb +1 -1
- data/spec/punchblock/translator/freeswitch/call_spec.rb +922 -0
- data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +279 -0
- data/spec/punchblock/translator/freeswitch/component/input_spec.rb +312 -0
- data/spec/punchblock/translator/freeswitch/component/output_spec.rb +369 -0
- data/spec/punchblock/translator/freeswitch/component/record_spec.rb +373 -0
- data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +285 -0
- data/spec/punchblock/translator/freeswitch/component_spec.rb +118 -0
- data/spec/punchblock/translator/freeswitch_spec.rb +597 -0
- data/spec/punchblock_spec.rb +11 -0
- data/spec/spec_helper.rb +1 -0
- metadata +52 -7
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
class Freeswitch
|
6
|
+
module Component
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
autoload :AbstractOutput
|
10
|
+
autoload :FliteOutput
|
11
|
+
autoload :Input
|
12
|
+
autoload :Output
|
13
|
+
autoload :Record
|
14
|
+
autoload :TTSOutput
|
15
|
+
|
16
|
+
class Component
|
17
|
+
include Celluloid
|
18
|
+
include DeadActorSafety
|
19
|
+
include HasGuardedHandlers
|
20
|
+
|
21
|
+
attr_reader :id, :call, :call_id
|
22
|
+
|
23
|
+
def initialize(component_node, call = nil)
|
24
|
+
@component_node, @call = component_node, call
|
25
|
+
@call_id = safe_from_dead_actors { call.id } if call
|
26
|
+
@id = Punchblock.new_uuid
|
27
|
+
@complete = false
|
28
|
+
setup
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup
|
32
|
+
end
|
33
|
+
|
34
|
+
def execute_command(command)
|
35
|
+
command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command for component #{id}", call_id, id
|
36
|
+
end
|
37
|
+
|
38
|
+
def handle_es_event(event)
|
39
|
+
trigger_handler :es, event
|
40
|
+
end
|
41
|
+
|
42
|
+
def send_complete_event(reason, recording = nil)
|
43
|
+
return if @complete
|
44
|
+
@complete = true
|
45
|
+
event = Punchblock::Event::Complete.new.tap do |c|
|
46
|
+
c.reason = reason
|
47
|
+
c << recording if recording
|
48
|
+
end
|
49
|
+
send_event event
|
50
|
+
current_actor.terminate!
|
51
|
+
end
|
52
|
+
|
53
|
+
def send_event(event)
|
54
|
+
event.component_id = id
|
55
|
+
event.target_call_id = call_id
|
56
|
+
safe_from_dead_actors { translator.handle_pb_event event }
|
57
|
+
end
|
58
|
+
|
59
|
+
def logger_id
|
60
|
+
"#{self.class}: #{call_id ? "Call ID: #{call_id}, Component ID: #{id}" : id}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def call_ended
|
64
|
+
send_complete_event Punchblock::Event::Complete::Hangup.new
|
65
|
+
end
|
66
|
+
|
67
|
+
def application(appname, options = nil)
|
68
|
+
call.application appname, "%[punchblock_component_id=#{id}]#{options}"
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def translator
|
74
|
+
call.translator
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_node_response(value)
|
78
|
+
@component_node.response = value
|
79
|
+
end
|
80
|
+
|
81
|
+
def send_ref
|
82
|
+
set_node_response Ref.new :id => id
|
83
|
+
end
|
84
|
+
|
85
|
+
def with_error(name, text)
|
86
|
+
set_node_response ProtocolError.new.setup(name, text)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
class Freeswitch
|
6
|
+
module Component
|
7
|
+
class AbstractOutput < Component
|
8
|
+
UnrenderableDocError = Class.new OptionError
|
9
|
+
|
10
|
+
def execute(*args)
|
11
|
+
validate
|
12
|
+
send_ref
|
13
|
+
do_output(*args)
|
14
|
+
rescue UnrenderableDocError => e
|
15
|
+
with_error 'unrenderable document error', e.message
|
16
|
+
rescue OptionError => e
|
17
|
+
with_error 'option error', e.message
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute_command(command)
|
21
|
+
case command
|
22
|
+
when Punchblock::Component::Stop
|
23
|
+
command.response = true
|
24
|
+
application 'break'
|
25
|
+
send_complete_event Punchblock::Event::Complete::Stop.new
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def do_output
|
34
|
+
raise 'Not Implemented'
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate
|
38
|
+
raise OptionError, 'An SSML document is required.' unless @component_node.ssml
|
39
|
+
|
40
|
+
[:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
|
41
|
+
raise OptionError, "A #{opt} value is unsupported." if @component_node.send opt
|
42
|
+
end
|
43
|
+
|
44
|
+
case @component_node.interrupt_on
|
45
|
+
when :speech, :dtmf, :any
|
46
|
+
raise OptionError, "An interrupt-on value of #{@component_node.interrupt_on} is unsupported."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def success_reason
|
51
|
+
Punchblock::Component::Output::Complete::Success.new
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
class Freeswitch
|
6
|
+
module Component
|
7
|
+
class Input < Component
|
8
|
+
|
9
|
+
include InputComponent
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def register_dtmf_event_handler
|
14
|
+
component = current_actor
|
15
|
+
call.register_handler :es, :event_name => 'DTMF' do |event|
|
16
|
+
safe_from_dead_actors do
|
17
|
+
component.process_dtmf! event[:dtmf_digit]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def unregister_dtmf_event_handler
|
23
|
+
call.unregister_handler :es, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
class Freeswitch
|
6
|
+
module Component
|
7
|
+
class Output < AbstractOutput
|
8
|
+
private
|
9
|
+
|
10
|
+
def validate
|
11
|
+
super
|
12
|
+
raise OptionError, "A voice value is unsupported." if @component_node.voice
|
13
|
+
filenames
|
14
|
+
end
|
15
|
+
|
16
|
+
def do_output
|
17
|
+
playback "file_string://#{filenames.join('!')}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def filenames
|
21
|
+
@filenames ||= @component_node.ssml.children.map do |node|
|
22
|
+
case node
|
23
|
+
when RubySpeech::SSML::Audio
|
24
|
+
node.src
|
25
|
+
when String
|
26
|
+
raise if node.include?(' ')
|
27
|
+
node
|
28
|
+
else
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
end.compact
|
32
|
+
rescue
|
33
|
+
raise UnrenderableDocError, 'The provided document could not be rendered.'
|
34
|
+
end
|
35
|
+
|
36
|
+
def playback(path)
|
37
|
+
op = current_actor
|
38
|
+
register_handler :es, :event_name => 'CHANNEL_EXECUTE_COMPLETE' do |event|
|
39
|
+
op.send_complete_event! complete_reason_for_event(event)
|
40
|
+
end
|
41
|
+
application 'playback', path
|
42
|
+
end
|
43
|
+
|
44
|
+
def complete_reason_for_event(event)
|
45
|
+
case event[:application_response]
|
46
|
+
when 'FILE PLAYED'
|
47
|
+
success_reason
|
48
|
+
else
|
49
|
+
Punchblock::Event::Complete::Error.new(:details => "Engine error: #{event[:application_response]}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
class Freeswitch
|
6
|
+
module Component
|
7
|
+
class Record < Component
|
8
|
+
RECORDING_BASE_PATH = '/var/punchblock/record'
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@complete_reason = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute
|
15
|
+
max_duration = @component_node.max_duration || -1
|
16
|
+
|
17
|
+
raise OptionError, 'A start-beep value of true is unsupported.' if @component_node.start_beep
|
18
|
+
raise OptionError, 'A start-paused value of true is unsupported.' if @component_node.start_paused
|
19
|
+
raise OptionError, 'An initial-timeout value is unsupported.' if @component_node.initial_timeout && @component_node.initial_timeout != -1
|
20
|
+
raise OptionError, 'A final-timeout value is unsupported.' if @component_node.final_timeout && @component_node.final_timeout != -1
|
21
|
+
raise OptionError, 'A max-duration value that is negative (and not -1) is invalid.' unless max_duration >= -1
|
22
|
+
|
23
|
+
@format = @component_node.format || 'wav'
|
24
|
+
|
25
|
+
component = current_actor
|
26
|
+
call.register_handler :es, :event_name => 'RECORD_STOP', [:[], :record_file_path] => filename do |event|
|
27
|
+
component.finished
|
28
|
+
end
|
29
|
+
|
30
|
+
record_args = ['start', filename]
|
31
|
+
record_args << max_duration/1000 unless max_duration == -1
|
32
|
+
call.uuid_foo :record, record_args.join(' ')
|
33
|
+
|
34
|
+
send_ref
|
35
|
+
rescue OptionError => e
|
36
|
+
with_error 'option error', e.message
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute_command(command)
|
40
|
+
case command
|
41
|
+
when Punchblock::Component::Stop
|
42
|
+
call.uuid_foo :record, "stop #{filename}"
|
43
|
+
@complete_reason = stop_reason
|
44
|
+
command.response = true
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def finished
|
51
|
+
send_complete_event(@complete_reason || success_reason)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def filename
|
57
|
+
File.join RECORDING_BASE_PATH, [id, @format].join('.')
|
58
|
+
end
|
59
|
+
|
60
|
+
def recording
|
61
|
+
Punchblock::Component::Record::Recording.new :uri => "file://#{filename}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop_reason
|
65
|
+
Punchblock::Event::Complete::Stop.new
|
66
|
+
end
|
67
|
+
|
68
|
+
def success_reason
|
69
|
+
Punchblock::Component::Record::Complete::Success.new
|
70
|
+
end
|
71
|
+
|
72
|
+
def send_complete_event(reason)
|
73
|
+
super reason, recording
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
class Freeswitch
|
6
|
+
module Component
|
7
|
+
class TTSOutput < AbstractOutput
|
8
|
+
private
|
9
|
+
|
10
|
+
def do_output(engine, default_voice = nil)
|
11
|
+
op = current_actor
|
12
|
+
register_handler :es, :event_name => 'CHANNEL_EXECUTE_COMPLETE' do |event|
|
13
|
+
op.send_complete_event! success_reason
|
14
|
+
end
|
15
|
+
voice = @component_node.voice || default_voice || 'kal'
|
16
|
+
application :speak, [engine, voice, document].join('|')
|
17
|
+
end
|
18
|
+
|
19
|
+
def document
|
20
|
+
@component_node.ssml.to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Punchblock
|
4
|
+
module Translator
|
5
|
+
module InputComponent
|
6
|
+
def setup
|
7
|
+
@buffer = ""
|
8
|
+
@initial_timeout = @component_node.initial_timeout || -1
|
9
|
+
@inter_digit_timeout = @component_node.inter_digit_timeout || -1
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute
|
13
|
+
validate
|
14
|
+
send_ref
|
15
|
+
|
16
|
+
@grammar = prepare_grammar
|
17
|
+
|
18
|
+
begin_initial_timer @initial_timeout/1000 unless @initial_timeout == -1
|
19
|
+
|
20
|
+
@dtmf_handler_id = register_dtmf_event_handler
|
21
|
+
rescue OptionError => e
|
22
|
+
with_error 'option error', e.message
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_dtmf(digit)
|
26
|
+
@buffer << digit
|
27
|
+
cancel_initial_timer
|
28
|
+
case (match = @grammar.match @buffer.dup)
|
29
|
+
when RubySpeech::GRXML::Match
|
30
|
+
complete success_reason(match)
|
31
|
+
when RubySpeech::GRXML::NoMatch
|
32
|
+
complete Punchblock::Component::Input::Complete::NoMatch.new
|
33
|
+
when RubySpeech::GRXML::PotentialMatch
|
34
|
+
reset_inter_digit_timer
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute_command(command)
|
39
|
+
case command
|
40
|
+
when Punchblock::Component::Stop
|
41
|
+
command.response = true
|
42
|
+
complete Punchblock::Event::Complete::Stop.new
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def validate
|
51
|
+
raise OptionError, 'A grammar document is required.' unless @component_node.grammar
|
52
|
+
raise OptionError, 'A mode value other than DTMF is unsupported.' unless @component_node.mode == :dtmf
|
53
|
+
raise OptionError, 'An initial timeout value that is negative (and not -1) is invalid.' unless @initial_timeout >= -1
|
54
|
+
raise OptionError, 'An inter-digit timeout value that is negative (and not -1) is invalid.' unless @inter_digit_timeout >= -1
|
55
|
+
end
|
56
|
+
|
57
|
+
def prepare_grammar
|
58
|
+
@component_node.grammar.value.clone.tap do |grammar|
|
59
|
+
grammar.inline!
|
60
|
+
grammar.tokenize!
|
61
|
+
grammar.normalize_whitespace
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def begin_initial_timer(timeout)
|
66
|
+
@initial_timer = after timeout do
|
67
|
+
complete Punchblock::Component::Input::Complete::NoInput.new
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def cancel_initial_timer
|
72
|
+
return unless instance_variable_defined?(:@initial_timer) && @initial_timer
|
73
|
+
@initial_timer.cancel
|
74
|
+
@initial_timer = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def reset_inter_digit_timer
|
78
|
+
return if @inter_digit_timeout == -1
|
79
|
+
@inter_digit_timer ||= begin
|
80
|
+
after @inter_digit_timeout/1000 do
|
81
|
+
complete Punchblock::Component::Input::Complete::NoMatch.new
|
82
|
+
end
|
83
|
+
end
|
84
|
+
@inter_digit_timer.reset
|
85
|
+
end
|
86
|
+
|
87
|
+
def cancel_inter_digit_timer
|
88
|
+
return unless instance_variable_defined?(:@inter_digit_timer) && @inter_digit_timer
|
89
|
+
@inter_digit_timer.cancel
|
90
|
+
@inter_digit_timer = nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def success_reason(match)
|
94
|
+
Punchblock::Component::Input::Complete::Success.new :mode => match.mode,
|
95
|
+
:confidence => match.confidence,
|
96
|
+
:utterance => match.utterance,
|
97
|
+
:interpretation => match.interpretation
|
98
|
+
end
|
99
|
+
|
100
|
+
def complete(reason)
|
101
|
+
unregister_dtmf_event_handler
|
102
|
+
cancel_initial_timer
|
103
|
+
cancel_inter_digit_timer
|
104
|
+
send_complete_event reason
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|