punchblock 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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