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.
Files changed (42) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/lib/punchblock.rb +1 -1
  3. data/lib/punchblock/connection.rb +1 -0
  4. data/lib/punchblock/connection/asterisk.rb +0 -1
  5. data/lib/punchblock/connection/freeswitch.rb +49 -0
  6. data/lib/punchblock/event/offer.rb +1 -1
  7. data/lib/punchblock/translator.rb +5 -0
  8. data/lib/punchblock/translator/asterisk.rb +16 -28
  9. data/lib/punchblock/translator/asterisk/call.rb +4 -21
  10. data/lib/punchblock/translator/asterisk/component.rb +0 -5
  11. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +0 -3
  12. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +0 -1
  13. data/lib/punchblock/translator/asterisk/component/input.rb +7 -97
  14. data/lib/punchblock/translator/asterisk/component/output.rb +0 -4
  15. data/lib/punchblock/translator/asterisk/component/record.rb +0 -2
  16. data/lib/punchblock/translator/freeswitch.rb +153 -0
  17. data/lib/punchblock/translator/freeswitch/call.rb +265 -0
  18. data/lib/punchblock/translator/freeswitch/component.rb +92 -0
  19. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +57 -0
  20. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +17 -0
  21. data/lib/punchblock/translator/freeswitch/component/input.rb +29 -0
  22. data/lib/punchblock/translator/freeswitch/component/output.rb +56 -0
  23. data/lib/punchblock/translator/freeswitch/component/record.rb +79 -0
  24. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +26 -0
  25. data/lib/punchblock/translator/input_component.rb +108 -0
  26. data/lib/punchblock/version.rb +1 -1
  27. data/punchblock.gemspec +3 -2
  28. data/spec/punchblock/connection/freeswitch_spec.rb +90 -0
  29. data/spec/punchblock/translator/asterisk/call_spec.rb +23 -2
  30. data/spec/punchblock/translator/asterisk/component/input_spec.rb +3 -3
  31. data/spec/punchblock/translator/asterisk_spec.rb +1 -1
  32. data/spec/punchblock/translator/freeswitch/call_spec.rb +922 -0
  33. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +279 -0
  34. data/spec/punchblock/translator/freeswitch/component/input_spec.rb +312 -0
  35. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +369 -0
  36. data/spec/punchblock/translator/freeswitch/component/record_spec.rb +373 -0
  37. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +285 -0
  38. data/spec/punchblock/translator/freeswitch/component_spec.rb +118 -0
  39. data/spec/punchblock/translator/freeswitch_spec.rb +597 -0
  40. data/spec/punchblock_spec.rb +11 -0
  41. data/spec/spec_helper.rb +1 -0
  42. 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,17 @@
1
+ # encoding: utf-8
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Freeswitch
6
+ module Component
7
+ class FliteOutput < TTSOutput
8
+ private
9
+
10
+ def document
11
+ @component_node.ssml.inner_text.to_s
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ 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