adhearsion 2.6.4 → 3.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (242) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -9
  3. data/CHANGELOG.md +22 -7
  4. data/Gemfile +2 -0
  5. data/README.markdown +4 -5
  6. data/Rakefile +1 -2
  7. data/adhearsion.gemspec +19 -8
  8. data/features/cli_create.feature +19 -3
  9. data/features/step_definitions/cli_steps.rb +0 -11
  10. data/features/support/env.rb +3 -4
  11. data/lib/adhearsion.rb +48 -27
  12. data/lib/adhearsion/call.rb +34 -50
  13. data/lib/adhearsion/call_controller.rb +6 -12
  14. data/lib/adhearsion/call_controller/dial.rb +15 -53
  15. data/lib/adhearsion/call_controller/input.rb +39 -162
  16. data/lib/adhearsion/call_controller/input/ask_grammar_builder.rb +44 -0
  17. data/lib/adhearsion/call_controller/input/menu_builder.rb +136 -0
  18. data/lib/adhearsion/call_controller/input/prompt_builder.rb +78 -0
  19. data/lib/adhearsion/call_controller/input/result.rb +46 -0
  20. data/lib/adhearsion/call_controller/output.rb +48 -67
  21. data/lib/adhearsion/call_controller/output/abstract_player.rb +3 -3
  22. data/lib/adhearsion/call_controller/output/async_player.rb +3 -3
  23. data/lib/adhearsion/call_controller/output/player.rb +1 -1
  24. data/lib/adhearsion/call_controller/record.rb +23 -8
  25. data/lib/adhearsion/calls.rb +1 -1
  26. data/lib/adhearsion/cli_commands/ahn_command.rb +2 -65
  27. data/lib/adhearsion/cli_commands/thor_errors.rb +0 -6
  28. data/lib/adhearsion/configuration.rb +91 -39
  29. data/lib/adhearsion/core_ext/blather/stanza.rb +41 -0
  30. data/lib/adhearsion/core_ext/blather/stanza/presence.rb +13 -0
  31. data/lib/adhearsion/error.rb +5 -0
  32. data/lib/adhearsion/event.rb +21 -0
  33. data/lib/adhearsion/event/active_speaker.rb +11 -0
  34. data/lib/adhearsion/event/answered.rb +11 -0
  35. data/lib/adhearsion/event/asterisk.rb +10 -0
  36. data/lib/adhearsion/event/asterisk/ami.rb +34 -0
  37. data/lib/adhearsion/event/complete.rb +75 -0
  38. data/lib/adhearsion/event/dtmf.rb +11 -0
  39. data/lib/adhearsion/event/end.rb +22 -0
  40. data/lib/adhearsion/event/input_timers_started.rb +9 -0
  41. data/lib/adhearsion/event/joined.rb +17 -0
  42. data/lib/adhearsion/event/offer.rb +14 -0
  43. data/lib/adhearsion/event/ringing.rb +11 -0
  44. data/lib/adhearsion/event/started_speaking.rb +13 -0
  45. data/lib/adhearsion/event/stopped_speaking.rb +13 -0
  46. data/lib/adhearsion/event/unjoined.rb +17 -0
  47. data/lib/adhearsion/events.rb +47 -66
  48. data/lib/adhearsion/foundation.rb +0 -1
  49. data/lib/adhearsion/foundation/object.rb +0 -5
  50. data/lib/adhearsion/generators/app/app_generator.rb +4 -1
  51. data/lib/adhearsion/generators/app/templates/Gemfile.erb +2 -10
  52. data/lib/adhearsion/generators/app/templates/adhearsion.erb +9 -9
  53. data/lib/adhearsion/generators/app/templates/config.ru +7 -0
  54. data/lib/adhearsion/generators/app/templates/en.yml +4 -0
  55. data/lib/adhearsion/generators/app/templates/events.erb +2 -2
  56. data/lib/adhearsion/generators/app/templates/hello_world.wav +0 -0
  57. data/lib/adhearsion/generators/app/templates/simon_game.rb +2 -1
  58. data/lib/adhearsion/generators/app/templates/simon_game_spec.rb +2 -2
  59. data/lib/adhearsion/has_headers.rb +34 -0
  60. data/lib/adhearsion/http_server.rb +37 -0
  61. data/lib/adhearsion/initializer.rb +19 -153
  62. data/lib/adhearsion/logging.rb +6 -25
  63. data/lib/adhearsion/outbound_call.rb +5 -5
  64. data/lib/adhearsion/plugin.rb +1 -0
  65. data/lib/adhearsion/protocol_error.rb +26 -0
  66. data/lib/adhearsion/rayo.rb +30 -0
  67. data/lib/adhearsion/rayo/client.rb +62 -0
  68. data/lib/adhearsion/rayo/client/component_registry.rb +33 -0
  69. data/lib/adhearsion/rayo/command.rb +21 -0
  70. data/lib/adhearsion/rayo/command/accept.rb +16 -0
  71. data/lib/adhearsion/rayo/command/answer.rb +16 -0
  72. data/lib/adhearsion/rayo/command/dial.rb +57 -0
  73. data/lib/adhearsion/rayo/command/hangup.rb +16 -0
  74. data/lib/adhearsion/rayo/command/join.rb +43 -0
  75. data/lib/adhearsion/rayo/command/mute.rb +13 -0
  76. data/lib/adhearsion/rayo/command/redirect.rb +23 -0
  77. data/lib/adhearsion/rayo/command/reject.rb +40 -0
  78. data/lib/adhearsion/rayo/command/unjoin.rb +24 -0
  79. data/lib/adhearsion/rayo/command/unmute.rb +13 -0
  80. data/lib/adhearsion/rayo/command_node.rb +47 -0
  81. data/lib/adhearsion/rayo/component.rb +21 -0
  82. data/lib/adhearsion/rayo/component/asterisk.rb +13 -0
  83. data/lib/adhearsion/rayo/component/asterisk/agi.rb +14 -0
  84. data/lib/adhearsion/rayo/component/asterisk/agi/command.rb +46 -0
  85. data/lib/adhearsion/rayo/component/asterisk/ami.rb +14 -0
  86. data/lib/adhearsion/rayo/component/asterisk/ami/action.rb +61 -0
  87. data/lib/adhearsion/rayo/component/component_node.rb +90 -0
  88. data/lib/adhearsion/rayo/component/input.rb +186 -0
  89. data/lib/adhearsion/rayo/component/output.rb +471 -0
  90. data/lib/adhearsion/rayo/component/prompt.rb +53 -0
  91. data/lib/adhearsion/rayo/component/receive_fax.rb +26 -0
  92. data/lib/adhearsion/rayo/component/record.rb +165 -0
  93. data/lib/adhearsion/rayo/component/send_fax.rb +64 -0
  94. data/lib/adhearsion/rayo/component/stop.rb +11 -0
  95. data/lib/adhearsion/rayo/connection.rb +12 -0
  96. data/lib/adhearsion/rayo/connection/asterisk.rb +74 -0
  97. data/lib/adhearsion/rayo/connection/connected.rb +22 -0
  98. data/lib/adhearsion/rayo/connection/generic_connection.rb +22 -0
  99. data/lib/adhearsion/rayo/connection/xmpp.rb +198 -0
  100. data/lib/adhearsion/rayo/disconnected_error.rb +22 -0
  101. data/lib/adhearsion/{punchblock_plugin → rayo}/initializer.rb +19 -19
  102. data/lib/adhearsion/rayo/rayo_node.rb +127 -0
  103. data/lib/adhearsion/rayo/ref.rb +57 -0
  104. data/lib/adhearsion/statistics.rb +1 -1
  105. data/lib/adhearsion/tasks.rb +1 -2
  106. data/lib/adhearsion/tasks/configuration.rb +1 -1
  107. data/lib/adhearsion/tasks/environment.rb +0 -2
  108. data/lib/adhearsion/tasks/i18n.rb +49 -0
  109. data/lib/adhearsion/translator.rb +11 -0
  110. data/lib/adhearsion/translator/asterisk.rb +234 -0
  111. data/lib/adhearsion/translator/asterisk/agi_app.rb +17 -0
  112. data/lib/adhearsion/translator/asterisk/agi_command.rb +45 -0
  113. data/lib/adhearsion/translator/asterisk/ami_error_converter.rb +20 -0
  114. data/lib/adhearsion/translator/asterisk/call.rb +416 -0
  115. data/lib/adhearsion/translator/asterisk/channel.rb +43 -0
  116. data/lib/adhearsion/translator/asterisk/component.rb +88 -0
  117. data/lib/adhearsion/translator/asterisk/component/asterisk.rb +15 -0
  118. data/lib/adhearsion/translator/asterisk/component/asterisk/agi_command.rb +42 -0
  119. data/lib/adhearsion/translator/asterisk/component/asterisk/ami_action.rb +68 -0
  120. data/lib/adhearsion/translator/asterisk/component/composed_prompt.rb +76 -0
  121. data/lib/adhearsion/translator/asterisk/component/dtmf_recognizer.rb +137 -0
  122. data/lib/adhearsion/translator/asterisk/component/input.rb +34 -0
  123. data/lib/adhearsion/translator/asterisk/component/input_component.rb +90 -0
  124. data/lib/adhearsion/translator/asterisk/component/mrcp_native_prompt.rb +71 -0
  125. data/lib/adhearsion/translator/asterisk/component/mrcp_prompt.rb +55 -0
  126. data/lib/adhearsion/translator/asterisk/component/mrcp_recog_prompt.rb +165 -0
  127. data/lib/adhearsion/translator/asterisk/component/output.rb +233 -0
  128. data/lib/adhearsion/translator/asterisk/component/record.rb +101 -0
  129. data/lib/adhearsion/translator/asterisk/component/stop_by_redirect.rb +30 -0
  130. data/lib/adhearsion/translator/asterisk/unimrcp_app.rb +28 -0
  131. data/lib/adhearsion/uri_list.rb +21 -0
  132. data/lib/adhearsion/version.rb +1 -1
  133. data/spec/adhearsion/call_controller/dial_spec.rb +79 -1420
  134. data/spec/adhearsion/call_controller/input_spec.rb +1141 -237
  135. data/spec/adhearsion/call_controller/output/async_player_spec.rb +10 -10
  136. data/spec/adhearsion/call_controller/output/player_spec.rb +8 -8
  137. data/spec/adhearsion/call_controller/output_spec.rb +162 -215
  138. data/spec/adhearsion/call_controller/record_spec.rb +15 -16
  139. data/spec/adhearsion/call_controller_spec.rb +23 -40
  140. data/spec/adhearsion/call_spec.rb +123 -129
  141. data/spec/adhearsion/calls_spec.rb +3 -3
  142. data/spec/adhearsion/configuration_spec.rb +94 -108
  143. data/spec/adhearsion/event/answered_spec.rb +50 -0
  144. data/spec/adhearsion/event/asterisk/Find Results +402 -0
  145. data/spec/adhearsion/event/asterisk/ami_spec.rb +81 -0
  146. data/spec/adhearsion/event/complete_spec.rb +176 -0
  147. data/spec/adhearsion/event/dtmf_spec.rb +35 -0
  148. data/spec/adhearsion/event/end_spec.rb +85 -0
  149. data/spec/adhearsion/event/input_timers_started_spec.rb +19 -0
  150. data/spec/adhearsion/event/joined_spec.rb +53 -0
  151. data/spec/adhearsion/event/offer_spec.rb +106 -0
  152. data/spec/adhearsion/event/ringing_spec.rb +50 -0
  153. data/spec/adhearsion/event/started_speaking_spec.rb +37 -0
  154. data/spec/adhearsion/event/stopped_speaking_spec.rb +37 -0
  155. data/spec/adhearsion/event/unjoined_spec.rb +48 -0
  156. data/spec/adhearsion/event/untitled +0 -0
  157. data/spec/adhearsion/events_spec.rb +19 -45
  158. data/spec/adhearsion/initializer_spec.rb +12 -184
  159. data/spec/adhearsion/logging_spec.rb +5 -20
  160. data/spec/adhearsion/outbound_call_spec.rb +13 -13
  161. data/spec/adhearsion/plugin_spec.rb +3 -4
  162. data/spec/adhearsion/protocol_error_spec.rb +91 -0
  163. data/spec/adhearsion/rayo/client/component_registry_spec.rb +26 -0
  164. data/spec/adhearsion/rayo/client_spec.rb +134 -0
  165. data/spec/adhearsion/rayo/command/accept_spec.rb +63 -0
  166. data/spec/adhearsion/rayo/command/answer_spec.rb +73 -0
  167. data/spec/adhearsion/rayo/command/dial_spec.rb +156 -0
  168. data/spec/adhearsion/rayo/command/hangup_spec.rb +63 -0
  169. data/spec/adhearsion/rayo/command/join_spec.rb +158 -0
  170. data/spec/adhearsion/rayo/command/mute_spec.rb +32 -0
  171. data/spec/adhearsion/rayo/command/redirect_spec.rb +89 -0
  172. data/spec/adhearsion/rayo/command/reject_spec.rb +117 -0
  173. data/spec/adhearsion/rayo/command/unjoin_spec.rb +82 -0
  174. data/spec/adhearsion/rayo/command/unmute_spec.rb +32 -0
  175. data/spec/adhearsion/rayo/command_node_spec.rb +101 -0
  176. data/spec/adhearsion/rayo/component/asterisk/agi/command_spec.rb +111 -0
  177. data/spec/adhearsion/rayo/component/asterisk/ami/action_spec.rb +173 -0
  178. data/spec/adhearsion/rayo/component/component_node_spec.rb +110 -0
  179. data/spec/adhearsion/rayo/component/input_spec.rb +715 -0
  180. data/spec/adhearsion/rayo/component/output_spec.rb +1030 -0
  181. data/spec/adhearsion/rayo/component/prompt_spec.rb +171 -0
  182. data/spec/adhearsion/rayo/component/receive_fax_spec.rb +136 -0
  183. data/spec/adhearsion/rayo/component/record_spec.rb +497 -0
  184. data/spec/adhearsion/rayo/component/send_fax_spec.rb +144 -0
  185. data/spec/adhearsion/rayo/connection/asterisk_spec.rb +118 -0
  186. data/spec/adhearsion/rayo/connection/xmpp_spec.rb +449 -0
  187. data/spec/adhearsion/rayo/initializer_spec.rb +353 -0
  188. data/spec/adhearsion/rayo/ref_spec.rb +168 -0
  189. data/spec/adhearsion/rayo_spec.rb +7 -0
  190. data/spec/adhearsion/router/route_spec.rb +1 -1
  191. data/spec/adhearsion/statistics_spec.rb +2 -5
  192. data/spec/adhearsion/translator/asterisk/call_spec.rb +2047 -0
  193. data/spec/adhearsion/translator/asterisk/component/asterisk/agi_command_spec.rb +256 -0
  194. data/spec/adhearsion/translator/asterisk/component/asterisk/ami_action_spec.rb +151 -0
  195. data/spec/adhearsion/translator/asterisk/component/composed_prompt_spec.rb +257 -0
  196. data/spec/adhearsion/translator/asterisk/component/input_spec.rb +571 -0
  197. data/spec/adhearsion/translator/asterisk/component/mrcp_native_prompt_spec.rb +774 -0
  198. data/spec/adhearsion/translator/asterisk/component/mrcp_prompt_spec.rb +1244 -0
  199. data/spec/adhearsion/translator/asterisk/component/output_spec.rb +1850 -0
  200. data/spec/adhearsion/translator/asterisk/component/record_spec.rb +426 -0
  201. data/spec/adhearsion/translator/asterisk/component/stop_by_redirect_spec.rb +62 -0
  202. data/spec/adhearsion/translator/asterisk/component_spec.rb +83 -0
  203. data/spec/adhearsion/translator/asterisk_spec.rb +685 -0
  204. data/spec/adhearsion/uri_list_spec.rb +88 -0
  205. data/spec/adhearsion_spec.rb +89 -14
  206. data/spec/fixtures/locale/en.yml +13 -0
  207. data/spec/fixtures/locale/it.yml +13 -0
  208. data/spec/spec_helper.rb +18 -5
  209. data/spec/support/call_controller_test_helpers.rb +3 -2
  210. data/spec/support/initializer_stubs.rb +3 -1
  211. data/spec/support/punchblock_examples.rb +65 -0
  212. data/spec/support/punchblock_mocks.rb +12 -0
  213. metadata +412 -70
  214. data/features/cli_daemon.feature +0 -20
  215. data/features/cli_restart.feature +0 -52
  216. data/features/cli_stop.feature +0 -50
  217. data/lib/adhearsion/call_controller/menu_dsl.rb +0 -21
  218. data/lib/adhearsion/call_controller/menu_dsl/array_match_calculator.rb +0 -26
  219. data/lib/adhearsion/call_controller/menu_dsl/calculated_match.rb +0 -43
  220. data/lib/adhearsion/call_controller/menu_dsl/calculated_match_collection.rb +0 -45
  221. data/lib/adhearsion/call_controller/menu_dsl/fixnum_match_calculator.rb +0 -11
  222. data/lib/adhearsion/call_controller/menu_dsl/match_calculator.rb +0 -40
  223. data/lib/adhearsion/call_controller/menu_dsl/menu.rb +0 -207
  224. data/lib/adhearsion/call_controller/menu_dsl/menu_builder.rb +0 -92
  225. data/lib/adhearsion/call_controller/menu_dsl/range_match_calculator.rb +0 -60
  226. data/lib/adhearsion/call_controller/menu_dsl/string_match_calculator.rb +0 -25
  227. data/lib/adhearsion/call_controller/utility.rb +0 -77
  228. data/lib/adhearsion/foundation/custom_daemonizer.rb +0 -52
  229. data/lib/adhearsion/punchblock_plugin.rb +0 -63
  230. data/scripts/cloc-1.64.pl +0 -10483
  231. data/spec/adhearsion/call_controller/menu_dsl/array_match_calculator_spec.rb +0 -76
  232. data/spec/adhearsion/call_controller/menu_dsl/calculated_match_collection_spec.rb +0 -60
  233. data/spec/adhearsion/call_controller/menu_dsl/calculated_match_spec.rb +0 -61
  234. data/spec/adhearsion/call_controller/menu_dsl/fixnum_match_calculator_spec.rb +0 -39
  235. data/spec/adhearsion/call_controller/menu_dsl/match_calculator_spec.rb +0 -17
  236. data/spec/adhearsion/call_controller/menu_dsl/menu_builder_spec.rb +0 -165
  237. data/spec/adhearsion/call_controller/menu_dsl/menu_spec.rb +0 -420
  238. data/spec/adhearsion/call_controller/menu_dsl/range_match_calculator_spec.rb +0 -32
  239. data/spec/adhearsion/call_controller/menu_dsl/string_match_calculator_spec.rb +0 -40
  240. data/spec/adhearsion/call_controller/utility_spec.rb +0 -90
  241. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +0 -356
  242. data/spec/adhearsion/punchblock_plugin_spec.rb +0 -59
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ruby_speech'
4
+ require 'adhearsion/translator/asterisk/component/dtmf_recognizer'
5
+
6
+ module Adhearsion
7
+ module Translator
8
+ class Asterisk
9
+ module Component
10
+ module InputComponent
11
+ def execute
12
+ validate
13
+ setup_dtmf_recognizer
14
+ send_ref
15
+ start_timers
16
+ rescue OptionError, ArgumentError => e
17
+ with_error 'option error', e.message
18
+ end
19
+
20
+ def process_dtmf(digit)
21
+ @recognizer << digit
22
+ rescue Celluloid::DeadActorError
23
+ logger.warn 'DTMF digit received into a dead recognizer. Dropping digit.'
24
+ end
25
+
26
+ def execute_command(command)
27
+ case command
28
+ when Adhearsion::Rayo::Component::Stop
29
+ command.response = true
30
+ complete Adhearsion::Event::Complete::Stop.new
31
+ else
32
+ super
33
+ end
34
+ end
35
+
36
+ def match(match)
37
+ complete success_reason(match)
38
+ end
39
+
40
+ def nomatch
41
+ complete Adhearsion::Rayo::Component::Input::Complete::NoMatch.new
42
+ end
43
+
44
+ def noinput
45
+ complete Adhearsion::Rayo::Component::Input::Complete::NoInput.new
46
+ end
47
+
48
+ private
49
+
50
+ def input_node
51
+ @component_node
52
+ end
53
+
54
+ def validate
55
+ raise OptionError, 'A grammar document is required.' unless input_node.grammars.first
56
+ raise OptionError, 'Only a single grammar is supported.' unless input_node.grammars.size == 1
57
+ raise OptionError, 'A mode value other than DTMF is unsupported.' unless input_node.mode == :dtmf
58
+ end
59
+
60
+ def setup_dtmf_recognizer
61
+ @recognizer = DTMFRecognizer.new self,
62
+ input_node.grammars.first,
63
+ (input_node.initial_timeout || -1),
64
+ (input_node.inter_digit_timeout || -1),
65
+ input_node.terminator
66
+ end
67
+
68
+ def start_timers
69
+ @recognizer.start_timers
70
+ end
71
+
72
+ def success_reason(match)
73
+ nlsml = RubySpeech::NLSML.draw do
74
+ interpretation confidence: match.confidence do
75
+ instance match.interpretation
76
+ input match.utterance, mode: match.mode
77
+ end
78
+ end
79
+ Adhearsion::Rayo::Component::Input::Complete::Match.new :nlsml => nlsml
80
+ end
81
+
82
+ def complete(reason)
83
+ unregister_dtmf_event_handler
84
+ send_complete_event reason
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8
2
+
3
+ require 'adhearsion/translator/asterisk/component/stop_by_redirect'
4
+ require 'adhearsion/translator/asterisk/component/mrcp_recog_prompt'
5
+
6
+ module Adhearsion
7
+ module Translator
8
+ class Asterisk
9
+ module Component
10
+ class MRCPNativePrompt < Component
11
+ include StopByRedirect
12
+ include MRCPRecogPrompt
13
+
14
+ private
15
+
16
+ def validate
17
+ raise OptionError, "The renderer #{renderer} is unsupported." unless renderer == 'asterisk'
18
+ raise OptionError, "The recognizer #{recognizer} is unsupported." unless recognizer == 'unimrcp'
19
+
20
+ raise OptionError, 'A document is required.' unless output_node.render_documents.count > 0
21
+ raise OptionError, 'Only one document is allowed.' if output_node.render_documents.count > 1
22
+ raise OptionError, 'Only inline documents are allowed.' if first_doc.url
23
+ raise OptionError, 'Only one audio file is allowed.' if first_doc.size > 1
24
+
25
+ raise OptionError, 'A grammar is required.' unless input_node.grammars.count > 0
26
+
27
+ super
28
+ end
29
+
30
+ def renderer
31
+ (output_node.renderer || :asterisk).to_s
32
+ end
33
+
34
+ def recognizer
35
+ (input_node.recognizer || :unimrcp).to_s
36
+ end
37
+
38
+ def execute_unimrcp_app
39
+ execute_app 'MRCPRecog', grammars
40
+ end
41
+
42
+ def first_doc
43
+ output_node.render_documents.first
44
+ end
45
+
46
+ def audio_filename
47
+ path = if first_doc.ssml?
48
+ first_doc.value.children.first.src
49
+ else
50
+ first_doc.value.first
51
+ end.sub('file://', '')
52
+
53
+ dir = File.dirname(path)
54
+ basename = File.basename(path, '.*')
55
+ if dir == '.'
56
+ basename
57
+ else
58
+ File.join(dir, basename)
59
+ end
60
+ end
61
+
62
+ def unimrcp_app_options
63
+ super do |opts|
64
+ opts[:f] = audio_filename
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ require 'adhearsion/translator/asterisk/component/mrcp_recog_prompt'
4
+
5
+ module Adhearsion
6
+ module Translator
7
+ class Asterisk
8
+ module Component
9
+ class MRCPPrompt < Component
10
+ include StopByRedirect
11
+ include MRCPRecogPrompt
12
+
13
+ private
14
+
15
+ def validate
16
+ raise OptionError, "The renderer #{renderer} is unsupported." unless renderer == 'unimrcp'
17
+ raise OptionError, "The recognizer #{recognizer} is unsupported." unless recognizer == 'unimrcp'
18
+ raise OptionError, 'An SSML document is required.' unless output_node.render_documents.count > 0
19
+ raise OptionError, 'Only one document is allowed.' if output_node.render_documents.count > 1
20
+ raise OptionError, 'A grammar is required.' unless input_node.grammars.count > 0
21
+
22
+ super
23
+ end
24
+
25
+ def renderer
26
+ (output_node.renderer || :unimrcp).to_s
27
+ end
28
+
29
+ def recognizer
30
+ (input_node.recognizer || :unimrcp).to_s
31
+ end
32
+
33
+ def execute_unimrcp_app
34
+ execute_app 'SynthAndRecog', render_doc, grammars
35
+ end
36
+
37
+ def render_doc
38
+ d = output_node.render_documents.first
39
+ if d.content_type
40
+ d.value.to_doc.to_s
41
+ else
42
+ d.url
43
+ end
44
+ end
45
+
46
+ def unimrcp_app_options
47
+ super do |opts|
48
+ opts[:vn] = output_node.voice if output_node.voice
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,165 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ruby_speech'
4
+ require 'adhearsion/translator/asterisk/unimrcp_app'
5
+
6
+ module Adhearsion
7
+ module Translator
8
+ class Asterisk
9
+ module Component
10
+ module MRCPRecogPrompt
11
+ UniMRCPError = Class.new Error
12
+
13
+ MRCP_ERRORS = {
14
+ '004' => 'RECOGNIZE failed due to grammar load failure.',
15
+ '005' => 'RECOGNIZE failed due to grammar compilation failure.',
16
+ '006' => 'RECOGNIZE request terminated prematurely due to a recognizer error.',
17
+ '007' => 'RECOGNIZE request terminated because speech was too early.',
18
+ '009' => 'Failure accessing a URI.',
19
+ '010' => 'Language not supported.',
20
+ '016' => 'Any DEFINE-GRAMMAR error other than grammar-load-failure and grammar-compilation-failure.',
21
+ }
22
+
23
+ def execute
24
+ setup_defaults
25
+ validate
26
+ send_ref
27
+ execute_unimrcp_app
28
+ complete
29
+ rescue ChannelGoneError
30
+ call_ended
31
+ rescue UniMRCPError => e
32
+ complete_with_error e.message
33
+ rescue RubyAMI::Error => e
34
+ complete_with_error "Terminated due to AMI error '#{e.message}'"
35
+ rescue OptionError => e
36
+ with_error 'option error', e.message
37
+ end
38
+
39
+ private
40
+
41
+ def validate
42
+ [:interrupt_on, :start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
43
+ raise OptionError, "A #{opt} value is unsupported on Asterisk." if output_node.send opt
44
+ end
45
+
46
+ raise OptionError, "An initial-timeout value must be -1 or a positive integer." if @initial_timeout < -1
47
+ raise OptionError, "An inter-digit-timeout value must be -1 or a positive integer." if @inter_digit_timeout < -1
48
+ raise OptionError, "A recognition-timeout value must be -1, 0, or a positive integer." if @recognition_timeout < -1
49
+ raise OptionError, "A max-silence value must be -1, 0, or a positive integer." if @max_silence < -1
50
+ raise OptionError, "A speech-complete-timeout value must be -1, 0, or a positive integer." if @speech_complete_timeout < -1
51
+ raise OptionError, "A hotword-max-duration value must be -1, 0, or a positive integer." if @hotword_max_duration < -1
52
+ raise OptionError, "A hotword-min-duration value must be -1, 0, or a positive integer." if @hotword_min_duration < -1
53
+ raise OptionError, "A dtmf-terminate-timeout value must be -1, 0, or a positive integer." if @dtmf_terminate_timeout < -1
54
+ raise OptionError, "An n-best-list-length value must be a positive integer." if @n_best_list_length && @n_best_list_length < 1
55
+ raise OptionError, "A speed-vs-accuracy value must be a positive integer." if @speed_vs_accuracy && @speed_vs_accuracy < 0
56
+ end
57
+
58
+ def execute_app(app, *args)
59
+ UniMRCPApp.new(app, *args, unimrcp_app_options).execute @call
60
+ end
61
+
62
+ def unimrcp_app_options
63
+ {uer: 1, b: (@component_node.barge_in == false ? 0 : 1)}.tap do |opts|
64
+ opts[:nit] = @initial_timeout if @initial_timeout > -1
65
+ opts[:dit] = @inter_digit_timeout if @inter_digit_timeout > -1
66
+ opts[:dttc] = input_node.terminator if input_node.terminator
67
+ opts[:spl] = input_node.language if input_node.language
68
+ opts[:ct] = input_node.min_confidence if input_node.min_confidence
69
+ opts[:sl] = input_node.sensitivity if input_node.sensitivity
70
+ opts[:t] = input_node.recognition_timeout if @recognition_timeout > -1
71
+ opts[:sint] = input_node.max_silence if @max_silence > -1
72
+ opts[:sct] = @speech_complete_timeout if @speech_complete_timeout > -1
73
+
74
+ opts[:sva] = @speed_vs_accuracy if @speed_vs_accuracy
75
+ opts[:nb] = @n_best_list_length if @n_best_list_length
76
+ opts[:sit] = @start_input_timers unless @start_input_timers.nil?
77
+ opts[:dtt] = @dtmf_terminate_timeout if @dtmf_terminate_timeout > -1
78
+ opts[:sw] = @save_waveform unless @save_waveform.nil?
79
+ opts[:nac] = @new_audio_channel unless @new_audio_channel.nil?
80
+ opts[:rm] = @recognition_mode if @recognition_mode
81
+ opts[:hmaxd] = @hotword_max_duration if @hotword_max_duration > -1
82
+ opts[:hmind] = @hotword_min_duration if @hotword_min_duration > -1
83
+ opts[:cdb] = @clear_dtmf_buffer unless @clear_dtmf_buffer.nil?
84
+ opts[:enm] = @early_no_match unless @early_no_match.nil?
85
+ opts[:iwu] = @input_waveform_uri if @input_waveform_uri
86
+ opts[:mt] = @media_type if @media_type
87
+
88
+ yield opts
89
+ end
90
+ end
91
+
92
+ def setup_defaults
93
+ @initial_timeout = input_node.initial_timeout || -1
94
+ @inter_digit_timeout = input_node.inter_digit_timeout || -1
95
+ @recognition_timeout = input_node.recognition_timeout || -1
96
+ @max_silence = input_node.max_silence || -1
97
+ @speech_complete_timeout = input_node.headers['Speech-Complete-Timeout'] || -1
98
+ @speed_vs_accuracy = input_node.headers['Speed-Vs-Accuracy']
99
+ @n_best_list_length = input_node.headers['N-Best-List-Length']
100
+ @start_input_timers = input_node.headers['Start-Input-Timers']
101
+ @dtmf_terminate_timeout = input_node.headers['DTMF-Terminate-Timeout'] || -1
102
+ @save_waveform = input_node.headers['Save-Waveform']
103
+ @new_audio_channel = input_node.headers['New-Audio-Channel']
104
+ @recognition_mode = input_node.headers['Recognition-Mode']
105
+ @hotword_max_duration = input_node.headers['Hotword-Max-Duration'] || -1
106
+ @hotword_min_duration = input_node.headers['Hotword-Min-Duration'] || -1
107
+ @clear_dtmf_buffer = input_node.headers['Clear-DTMF-Buffer']
108
+ @early_no_match = input_node.headers['Early-No-Match']
109
+ @input_waveform_uri = input_node.headers['Input-Waveform-URI']
110
+ @media_type = input_node.headers['Media-Type']
111
+ end
112
+
113
+ def grammars
114
+ input_node.grammars.map do |d|
115
+ if d.content_type
116
+ d.value.to_doc.to_s
117
+ else
118
+ d.url
119
+ end
120
+ end.join ','
121
+ end
122
+
123
+ def first_doc
124
+ output_node.render_documents.first
125
+ end
126
+
127
+ def audio_filename
128
+ first_doc.value.first
129
+ end
130
+
131
+ def output_node
132
+ @component_node.output
133
+ end
134
+
135
+ def input_node
136
+ @component_node.input
137
+ end
138
+
139
+ def complete
140
+ case @call.channel_var('RECOG_STATUS')
141
+ when 'INTERRUPTED'
142
+ send_complete_event Adhearsion::Rayo::Component::Input::Complete::NoMatch.new
143
+ when 'ERROR'
144
+ raise UniMRCPError
145
+ else
146
+ send_complete_event case cause = @call.channel_var('RECOG_COMPLETION_CAUSE')
147
+ when '000', '008', '012'
148
+ nlsml = RubySpeech.parse URI.decode(@call.channel_var('RECOG_RESULT'))
149
+ Adhearsion::Rayo::Component::Input::Complete::Match.new nlsml: nlsml
150
+ when '001', '003', '013', '014', '015'
151
+ Adhearsion::Rayo::Component::Input::Complete::NoMatch.new
152
+ when '002', '011'
153
+ Adhearsion::Rayo::Component::Input::Complete::NoInput.new
154
+ when *MRCP_ERRORS.keys
155
+ raise UniMRCPError, MRCP_ERRORS[cause]
156
+ else
157
+ raise UniMRCPError
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,233 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ruby_speech'
4
+ require 'active_support/core_ext/string/filters'
5
+ require 'adhearsion/translator/asterisk/unimrcp_app'
6
+
7
+ module Adhearsion
8
+ module Translator
9
+ class Asterisk
10
+ module Component
11
+ class Output < Component
12
+ include StopByRedirect
13
+
14
+ UnrenderableDocError = Class.new OptionError
15
+ UniMRCPError = Class.new Error
16
+ PlaybackError = Class.new Error
17
+
18
+ def execute
19
+ raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value
20
+ raise OptionError, 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :voice
21
+
22
+ [:start_offset, :start_paused, :repeat_interval, :max_time].each do |opt|
23
+ raise OptionError, "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
24
+ end
25
+
26
+ @early = !@call.answered?
27
+
28
+ rendering_engine = @component_node.renderer || :asterisk
29
+
30
+ repeat_times = @component_node.repeat_times || 1
31
+ repeat_times = 1000 if repeat_times.zero?
32
+
33
+ case rendering_engine.to_sym
34
+ when :asterisk
35
+ validate_audio_only
36
+ setup_for_native
37
+
38
+ repeat_times.times do
39
+ render_docs.each do |doc|
40
+ playback(filenames(doc).values) || raise(PlaybackError)
41
+ end
42
+ end
43
+ when :native_or_unimrcp
44
+ setup_for_native
45
+
46
+ repeat_times.times do
47
+ render_docs.each do |doc|
48
+ doc.value.children.each do |node|
49
+ case node
50
+ when RubySpeech::SSML::Audio
51
+ playback([path_for_audio_node(node)]) || render_with_unimrcp(fallback_doc(doc, node))
52
+ when String
53
+ if node.include?(' ')
54
+ render_with_unimrcp(copied_doc(doc, node))
55
+ else
56
+ playback([node]) || render_with_unimrcp(copied_doc(doc, node))
57
+ end
58
+ else
59
+ render_with_unimrcp(copied_doc(doc, node.node))
60
+ end
61
+ end
62
+ end
63
+ end
64
+ when :unimrcp
65
+ send_progress_if_necessary
66
+ send_ref
67
+ repeat_times.times do
68
+ render_with_unimrcp(*render_docs)
69
+ end
70
+ when :swift
71
+ send_progress_if_necessary
72
+ send_ref
73
+ @call.execute_agi_command 'EXEC Swift', swift_doc
74
+ else
75
+ raise OptionError, "The renderer #{rendering_engine} is unsupported."
76
+ end
77
+ send_finish
78
+ rescue ChannelGoneError
79
+ call_ended
80
+ rescue PlaybackError
81
+ complete_with_error 'Terminated due to playback error'
82
+ rescue UniMRCPError
83
+ complete_with_error 'Terminated due to UniMRCP error'
84
+ rescue RubyAMI::Error => e
85
+ complete_with_error "Terminated due to AMI error '#{e.message}'"
86
+ rescue UnrenderableDocError => e
87
+ with_error 'unrenderable document error', e.message
88
+ rescue OptionError => e
89
+ with_error 'option error', e.message
90
+ end
91
+
92
+ def stop_by_redirect(*args)
93
+ @stopped = true
94
+ super
95
+ end
96
+
97
+ private
98
+
99
+ def setup_for_native
100
+ raise OptionError, "A voice value is unsupported on Asterisk." if @component_node.voice
101
+ raise OptionError, 'Interrupt digits are not allowed with early media.' if @early && @component_node.interrupt_on
102
+
103
+ case @component_node.interrupt_on
104
+ when :any, :dtmf
105
+ interrupt = true
106
+ end
107
+
108
+ send_progress_if_necessary
109
+
110
+ if interrupt
111
+ call.register_handler :ami, [{:name => 'DTMF', [:[], 'End'] => 'Yes'}, {:name => 'DTMFEnd'}] do |event|
112
+ stop_by_redirect finish_reason
113
+ end
114
+ end
115
+
116
+ send_ref
117
+ end
118
+
119
+ def send_progress_if_necessary
120
+ @call.send_progress if @early
121
+ end
122
+
123
+ def validate_audio_only
124
+ render_docs.each do |doc|
125
+ filenames doc, -> { raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.' }
126
+ end
127
+ end
128
+
129
+ def path_for_audio_node(node)
130
+ path = node.src.sub('file://', '')
131
+ dir = File.dirname(path)
132
+ basename = File.basename(path, '.*')
133
+ if dir == '.'
134
+ basename
135
+ else
136
+ File.join(dir, basename)
137
+ end
138
+ end
139
+
140
+ def filenames(doc, check_audio_only_policy = -> {})
141
+ names = {}
142
+ doc.value.children.each do |node|
143
+ case node
144
+ when RubySpeech::SSML::Audio
145
+ names[node] = path_for_audio_node node
146
+ when String
147
+ check_audio_only_policy.call if node.include?(' ')
148
+ names[nil] = node
149
+ else
150
+ check_audio_only_policy.call
151
+ end
152
+ end
153
+ names
154
+ end
155
+
156
+ def playback_options(paths)
157
+ opts = paths.join '&'
158
+ opts << ",noanswer" if @early
159
+ opts
160
+ end
161
+
162
+ def playback(paths)
163
+ return true if @stopped
164
+ @call.execute_agi_command 'EXEC Playback', playback_options(paths)
165
+ @call.channel_var('PLAYBACKSTATUS') != 'FAILED'
166
+ end
167
+
168
+ def fallback_doc(original, failed_audio_node)
169
+ children = failed_audio_node.nokogiri_children
170
+ copied_doc original, children
171
+ end
172
+
173
+ def copied_doc(original, elements)
174
+ doc = RubySpeech::SSML.draw do
175
+ if Nokogiri.jruby?
176
+ self.write_attr 'version', original.value['version']
177
+ self.write_attr 'xml:lang', original.value['xml:lang']
178
+ else
179
+ original.value.attributes.each do |name, value|
180
+ attr_name = value.namespace && value.namespace.prefix ? [value.namespace.prefix, name].join(':') : name
181
+ self.write_attr attr_name, value
182
+ end
183
+ end
184
+
185
+ add_child Nokogiri.jruby? ? elements : elements.to_xml
186
+ end
187
+ Adhearsion::Rayo::Component::Output::Document.new(value: doc)
188
+ end
189
+
190
+ def render_with_unimrcp(*docs)
191
+ docs.each do |doc|
192
+ return if @stopped
193
+ UniMRCPApp.new('MRCPSynth', doc.value.to_s, mrcpsynth_options).execute @call
194
+ raise UniMRCPError if @call.channel_var('SYNTHSTATUS') == 'ERROR'
195
+ end
196
+ end
197
+
198
+ def render_docs
199
+ @component_node.render_documents
200
+ end
201
+
202
+ def concatenated_render_doc
203
+ render_docs.inject RubySpeech::SSML.draw do |doc, argument|
204
+ doc + argument.value
205
+ end
206
+ end
207
+
208
+ def mrcpsynth_options
209
+ {}.tap do |opts|
210
+ opts[:i] = 'any' if [:any, :dtmf].include? @component_node.interrupt_on
211
+ opts[:v] = @component_node.voice if @component_node.voice
212
+ end
213
+ end
214
+
215
+ def swift_doc
216
+ doc = concatenated_render_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
217
+ doc << "|1|1" if [:any, :dtmf].include? @component_node.interrupt_on
218
+ doc.insert 0, "#{@component_node.voice}^" if @component_node.voice
219
+ doc
220
+ end
221
+
222
+ def send_finish
223
+ send_complete_event finish_reason
224
+ end
225
+
226
+ def finish_reason
227
+ Adhearsion::Rayo::Component::Output::Complete::Finish.new
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end