lib/adhearsion/call_controller/output.rb in adhearsion-2.0.1 vs lib/adhearsion/call_controller/output.rb in adhearsion-2.1.0

- old
+ new

@@ -1,24 +1,46 @@ # encoding: utf-8 module Adhearsion class CallController module Output + extend ActiveSupport::Autoload + + autoload :AbstractPlayer + autoload :AsyncPlayer + autoload :Formatter + autoload :Player + PlaybackError = Class.new Adhearsion::Error # Represents failure to play audio, such as when the sound file cannot be found # # Speak output using text-to-speech (TTS) # # @param [String, #to_s] text The text to be rendered # @param [Hash] options A set of options for output # + # @raises [PlaybackError] if the given argument could not be played + # def say(text, options = {}) - play_ssml(text, options) || output(ssml_for_text(text.to_s), options) + player.play_ssml(text, options) || player.output(Formatter.ssml_for_text(text.to_s), options) end alias :speak :say # + # Speak output using text-to-speech (TTS) and return as soon as it begins + # + # @param [String, #to_s] text The text to be rendered + # @param [Hash] options A set of options for output + # + # @raises [PlaybackError] if the given argument could not be played + # + def say!(text, options = {}) + async_player.play_ssml(text, options) || async_player.output(Formatter.ssml_for_text(text.to_s), options) + end + alias :speak! :say! + + # # Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now), # Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. To specify how the Date/Time objects are said # pass in as an array with the first parameter as the Date/Time/DateTime object along with a hash with the # additional options. See play_time for more information. # @@ -34,41 +56,77 @@ # @example Play sound file, speak number, play two more sound files # play %w"http://www.example.com/a-connect-charge-of.wav 22 /path/to/cents-per-minute.wav /path/to/will-apply.mp3" # @example Play two sound files # play "/path/to/you-sound-cute.mp3", "/path/to/what-are-you-wearing.wav" # - # @return [Boolean] true is returned if everything was successful. Otherwise, false indicates that - # some sound file(s) could not be played. + # @raises [PlaybackError] if (one of) the given argument(s) could not be played # - # @see play_time - # @see play_numeric - # @see play_audio - # def play(*arguments) - arguments.inject(true) do |value, argument| - value = case argument - when Hash - play_ssml_for argument.delete(:value), argument - when RubySpeech::SSML::Speak - play_ssml argument - else - play_ssml_for argument - end - end + player.play_ssml Formatter.ssml_for_collection(arguments) + true end # - # Plays the specified input arguments, raising an exception if any can't be played. - # @see play + # Plays the specified sound file names and returns as soon as it begins. This method will handle Time/DateTime objects (e.g. Time.now), + # Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. To specify how the Date/Time objects are said + # pass in as an array with the first parameter as the Date/Time/DateTime object along with a hash with the + # additional options. See play_time for more information. # - # @private + # @example Play file hello-world + # play 'http://www.example.com/hello-world.mp3' + # play '/path/on/disk/hello-world.wav' + # @example Speak current time + # play Time.now + # @example Speak today's date + # play Date.today + # @example Speak today's date in a specific format + # play Date.today, :strftime => "%d/%m/%Y", :format => "dmy" + # @example Play sound file, speak number, play two more sound files + # play %w"http://www.example.com/a-connect-charge-of.wav 22 /path/to/cents-per-minute.wav /path/to/will-apply.mp3" + # @example Play two sound files + # play "/path/to/you-sound-cute.mp3", "/path/to/what-are-you-wearing.wav" # + # @raises [PlaybackError] if (one of) the given argument(s) could not be played + # @returns [Punchblock::Component::Output] + # def play!(*arguments) - play(*arguments) or raise PlaybackError, "One of the passed outputs is invalid" + async_player.play_ssml Formatter.ssml_for_collection(arguments) end # + # Plays the given audio file. + # SSML supports http:// paths and full disk paths. + # The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory. + # + # @param [String] file http:// URL or full disk path to the sound file + # @param [Hash] options Additional options to specify how exactly to say time specified. + # @option options [String] :fallback The text to play if the file is not available + # + # @raises [PlaybackError] if (one of) the given argument(s) could not be played + # + def play_audio(file, options = nil) + player.play_ssml Formatter.ssml_for_audio(file, options) + true + end + + # + # Plays the given audio file and returns as soon as it begins. + # SSML supports http:// paths and full disk paths. + # The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory. + # + # @param [String] file http:// URL or full disk path to the sound file + # @param [Hash] options Additional options to specify how exactly to say time specified. + # @option options [String] :fallback The text to play if the file is not available + # + # @raises [PlaybackError] if (one of) the given argument(s) could not be played + # @returns [Punchblock::Component::Output] + # + def play_audio!(file, options = nil) + async_player.play_ssml Formatter.ssml_for_audio(file, options) + end + + # # Plays the given Date, Time, or Integer (seconds since epoch) # using the given timezone and format. # # @param [Date, Time, DateTime] time Time to be said. # @param [Hash] options Additional options to specify how exactly to say time specified. @@ -77,78 +135,70 @@ # Please refer to the SSML specification. # @see http://www.w3.org/TR/ssml-sayas/#S3.1 # @option options [String] :strftime This format is what defines the string that is sent to the Speech Synthesis Engine. # It uses Time::strftime symbols. # - # @return [Boolean] true if successful, false if the given argument could not be played. + # @raises [ArgumentError] if the given argument can not be played # def play_time(time, options = {}) - return false unless [Date, Time, DateTime].include? time.class + raise ArgumentError unless [Date, Time, DateTime].include?(time.class) && options.is_a?(Hash) + player.play_ssml Formatter.ssml_for_time(time, options) + true + end - return false unless options.is_a? Hash - play_ssml ssml_for_time(time, options) + # + # Plays the given Date, Time, or Integer (seconds since epoch) + # using the given timezone and format and returns as soon as it begins. + # + # @param [Date, Time, DateTime] time Time to be said. + # @param [Hash] options Additional options to specify how exactly to say time specified. + # @option options [String] :format This format is used only to disambiguate times that could be interpreted in different ways. + # For example, 01/06/2011 could mean either the 1st of June or the 6th of January. + # Please refer to the SSML specification. + # @see http://www.w3.org/TR/ssml-sayas/#S3.1 + # @option options [String] :strftime This format is what defines the string that is sent to the Speech Synthesis Engine. + # It uses Time::strftime symbols. + # + # @raises [ArgumentError] if the given argument can not be played + # @returns [Punchblock::Component::Output] + # + def play_time!(time, options = {}) + raise ArgumentError unless [Date, Time, DateTime].include?(time.class) && options.is_a?(Hash) + async_player.play_ssml Formatter.ssml_for_time(time, options) end # # Plays the given Numeric argument or string representing a decimal number. # When playing numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100") # is pronounced as "one hundred" instead of "one zero zero". # # @param [Numeric, String] Numeric or String containing a valid Numeric, like "321". # - # @return [Boolean] true if successful, false if the given argument could not be played. + # @raises [ArgumentError] if the given argument can not be played # - def play_numeric(number, options = nil) - if number.kind_of?(Numeric) || number =~ /^\d+$/ - play_ssml ssml_for_numeric(number, options) - end + def play_numeric(number) + raise ArgumentError unless number.kind_of?(Numeric) || number =~ /^\d+$/ + player.play_ssml Formatter.ssml_for_numeric(number) + true end # - # Plays the given audio file. - # SSML supports http:// paths and full disk paths. - # The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory. + # Plays the given Numeric argument or string representing a decimal number and returns as soon as it begins. + # When playing numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100") + # is pronounced as "one hundred" instead of "one zero zero". # - # @param [String] file http:// URL or full disk path to the sound file - # @param [Hash] options Additional options to specify how exactly to say time specified. - # @option options [String] :fallback The text to play if the file is not available + # @param [Numeric, String] Numeric or String containing a valid Numeric, like "321". # - # @return [Boolean] true on correct play of the file, false on file missing or not playable + # @raises [ArgumentError] if the given argument can not be played + # @returns [Punchblock::Component::Output] # - def play_audio(file, options = nil) - play_ssml ssml_for_audio(file, options) + def play_numeric!(number) + raise ArgumentError unless number.kind_of?(Numeric) || number =~ /^\d+$/ + async_player.play_ssml Formatter.ssml_for_numeric(number) end - # @private - def play_ssml(ssml, options = {}) - if [RubySpeech::SSML::Speak, Nokogiri::XML::Document].include? ssml.class - output ssml.to_s, options - end - end - - # @private - def output(content, options = {}) - options.merge! :ssml => content - execute_component_and_await_completion ::Punchblock::Component::Output.new(options) - end - # - # Same as interruptible_play, but throws an error if unable to play the output - # @see interruptible_play - # - # @private - # - def interruptible_play!(*outputs) - result = nil - outputs.each do |output| - result = stream_file output - break unless result.nil? - end - result - end - - # # Plays the given output, allowing for DTMF input of a single digit from the user # At the end of the played file it returns nil # # @example Ask the user for a number, then play it back # ssml = RubySpeech::SSML.draw do @@ -159,124 +209,56 @@ # # @param [String, Numeric, Date, Time, RubySpeech::SSML::Speak, Array, Hash] The argument to play to the user, or an array of arguments. # @param [Hash] Additional options. # # @return [String, nil] The single DTMF character entered by the user, or nil if nothing was entered + # @raises [PlaybackError] if (one of) the given argument(s) could not be played # def interruptible_play(*outputs) - result = nil - outputs.each do |output| - begin - result = interruptible_play! output - rescue PlaybackError => e - # Ignore this exception and play the next output - logger.error "Error playing back the prompt: #{e.message}" - ensure - break if result - end + outputs.find do |output| + digit = stream_file output + return digit if digit end - result end - # @private - def detect_type(output) - result = nil - result = :time if [Date, Time, DateTime].include? output.class - result = :numeric if output.kind_of?(Numeric) || output =~ /^\d+$/ - result = :audio if !result && (/^\//.match(output.to_s) || URI::regexp.match(output.to_s)) - result ||= :text - end - - # @private - def play_ssml_for(*args) - play_ssml ssml_for(args) - end - # - # Generates SSML for the argument and options passed, using automatic detection - # Directly returns the argument if it is already an SSML document - # - # @param [String, Hash, RubySpeech::SSML::Speak] the argument with options as accepted by the play_ methods, or an SSML document - # @return [RubySpeech::SSML::Speak] an SSML document - # - # @private - # - def ssml_for(*args) - return args[0] if args.size == 1 && args[0].is_a?(RubySpeech::SSML::Speak) - argument, options = args.flatten - options ||= {} - type = detect_type argument - send "ssml_for_#{type}", argument, options - end - - # @private - def ssml_for_text(argument, options = {}) - RubySpeech::SSML.draw { argument } - end - - # @private - def ssml_for_time(argument, options = {}) - interpretation = case argument - when Date then 'date' - when Time then 'time' - end - - format = options.delete :format - strftime = options.delete :strftime - - time_to_say = strftime ? argument.strftime(strftime) : argument.to_s - - RubySpeech::SSML.draw do - say_as(:interpret_as => interpretation, :format => format) { time_to_say } - end - end - - # @private - def ssml_for_numeric(argument, options = {}) - RubySpeech::SSML.draw do - say_as(:interpret_as => 'cardinal') { argument.to_s } - end - end - - # @private - def ssml_for_audio(argument, options = {}) - fallback = (options || {}).delete :fallback - RubySpeech::SSML.draw do - audio(:src => argument) { fallback } - end - end - - # # Plays a single output, not only files, accepting interruption by one of the digits specified - # Currently still stops execution, will be fixed soon in Punchblock # # @param [Object] String or Hash specifying output and options # @param [String] String with the digits that are allowed to interrupt output # # @return [String, nil] The pressed digit, or nil if nothing was pressed + # @private # def stream_file(argument, digits = '0123456789#*') result = nil - ssml = ssml_for argument - output_component = ::Punchblock::Component::Output.new :ssml => ssml.to_s - input_stopper_component = ::Punchblock::Component::Input.new :mode => :dtmf, + stopper = Punchblock::Component::Input.new :mode => :dtmf, :grammar => { - :value => grammar_accept(digits).to_s + :value => grammar_accept(digits) } - input_stopper_component.register_event_handler ::Punchblock::Event::Complete do |event| - output_component.stop! unless output_component.complete? + + player.output Formatter.ssml_for(argument) do |output_component| + stopper.register_event_handler Punchblock::Event::Complete do |event| + output_component.stop! unless output_component.complete? + end + write_and_await_response stopper end - write_and_await_response input_stopper_component - begin - execute_component_and_await_completion output_component - rescue ::Punchblock::ProtocolError => e - raise PlaybackError, "Output failed for argument #{argument.inspect} due to #{e.inspect}" - end - input_stopper_component.stop! if input_stopper_component.executing? - reason = input_stopper_component.complete_event.reason + + stopper.stop! if stopper.executing? + reason = stopper.complete_event.reason result = reason.interpretation if reason.respond_to? :interpretation return parse_single_dtmf result unless result.nil? result + end + + # @private + def player + @player ||= Player.new(self) + end + + # @private + def async_player + @async_player ||= AsyncPlayer.new(self) end end # Output end # CallController end # Adhearsion