punchblock 1.3.0 → 1.4.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.
- 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
|