class Mac::Say
A class wrapper around the MacOS `say` commad Allows to use simple TTS on Mac
right from Ruby scripts
A class wrapper around the MacOS `say` command Allows to use simple TTS on Mac
right from Ruby scripts
Constants
- VERSION
mac-say version
- VOICE_ATTRIBUTES
The list of the voice attributes available
- VOICE_PATTERN
A regex pattern to parse say voices list output
Attributes
Current config @return [Hash] a Hash with current configuration
Current voices list
@return [Array<Hash>] an array of voices Hashes supported by the say command @example Get all the voices
Mac::Say.voices #=> [ { :name => :agnes, :language => :en, :country => :us, :sample => "Isn't it nice to have a computer that will talk to you?", :gender => :female, :joke => false, :quality => :low, :singing => false }, { :name => :albert, :language => :en, :country => :us, :sample => "I have a frog in my throat. No, I mean a real frog!", :gender => :male, :joke => true, :quality => :medium, :singing => false }, ... ]
Public Class Methods
Say
constructor: sets initial configuration for say command to use
@param say_path [String] the full path to the say app binary (default: '/usr/bin/say' or USE_FAKE_SAY environment variable) @param voice [Symbol] voice to be used by the say command (default: :alex) @param rate [Integer] speech rate in words per minute (default: 175) accepts values in (175..720) @param file [String] path to the file to read (default: nil)
@raise [VoiceNotFound] if the given voice doesn't exist or wasn't installed
# File lib/mac/say.rb, line 81 def initialize(voice: :alex, rate: 175, file: nil, say_path: ENV['USE_FAKE_SAY'] || '/usr/bin/say') @config = { say_path: say_path, voice: voice, rate: rate, file: file } @voices = nil load_voices raise VoiceNotFound, "Voice '#{voice}' isn't a valid voice" unless valid_voice? voice end
Read the given string with the given voice
@param string [String] a text to read using say command @param voice [Symbol] voice to be used by the say command (default: :alex)
@return [Array<String, Integer>] an array with the actual say command used
and it's exit code. E.g.: ["/usr/bin/say -v 'alex' -r 175", 0]
@raise [CommandNotFound] if the say command wasn't found @raise [VoiceNotFound] if the given voice doesn't exist or wasn't installed
# File lib/mac/say.rb, line 105 def self.say(string, voice = :alex) mac = new(voice: voice.downcase.to_sym) mac.say(string: string) end
Look for voices by their attributes (e.g. :name, :language, :country, :gender, etc.)
@overload voice(attribute, value)
@param attribute [Symbol] the attribute to search voices by @param value [Symbol, String] the value of the attribute to search voices by
@overload voice(&block)
@yield [voice] Passes the given block to @voices.find_all
@return [Array<Hash>, Hash, nil] an array with all the voices matched by the attribute or
a voice Hash if only one voice corresponds to the attribute, nil if no voices found
@example Find voices by one or more attributes
Mac::Say.new.voice(:joke, false) Mac::Say.new.voice(:gender, :female) Mac::Say.new.voice { |v| v[:joke] == true && v[:gender] == :female } Mac::Say.new.voice { |v| v[:language] == :en && v[:gender] == :male && v[:quality] == :high && v[:joke] == false }
@raise [UnknownVoiceAttribute] if the voice attribute isn't supported def self.voice(attribute = nil, value = nil, &block)
# File lib/mac/say.rb, line 167 def self.voice(attribute = nil, value = nil, &block) mac = new if block_given? mac.voice(&block) else mac.voice(attribute, value) end end
Get all the voices supported by the say command on current machine
@return [Array<Hash>] an array of voices Hashes supported by the say command @example Get all the voices
Mac::Say.voices #=> [ { :name => :agnes, :language => :en, :country => :us, :sample => "Isn't it nice to have a computer that will talk to you?", :gender => :female, :joke => false, :quality => :low, :singing => false }, { :name => :albert, :language => :en, :country => :us, :sample => "I have a frog in my throat. No, I mean a real frog!", :gender => :male, :joke => true, :quality => :medium, :singing => false }, ... ]
# File lib/mac/say.rb, line 239 def self.voices mac = new mac.voices end
Public Instance Methods
Read the given string/file with the given voice and rate
Providing file, voice or rate arguments changes instance state and influence all the subsequent say
calls unless they have their own custom arguments
@param string [String] a text to read using say command (default: nil) @param file [String] path to the file to read (default: nil) @param voice [Symbol] voice to be used by the say command (default: :alex) @param rate [Integer] speech rate in words per minute (default: 175) accepts values in (175..720)
@return [Array<String, Integer>] an array with the actual say command used
and it's exit code. E.g.: ["/usr/bin/say -v 'alex' -r 175", 0]
@raise [CommandNotFound] if the say command wasn't found @raise [VoiceNotFound] if the given voice doesn't exist or wasn't installed @raise [FileNotFound] if the given file wasn't found or isn't readable by the current user
@example Say
something (for more examples check README.md or examples/examples.rb files)
Mac::Say.new.say string: 'Hello world' #=> ["/usr/bin/say -v 'alex' -r 175", 0] Mac::Say.new.say string: 'Hello world', voice: :fiona #=> ["/usr/bin/say -v 'fiona' -r 175", 0] Mac::Say.new.say file: /tmp/text.txt, rate: 300 #=> ["/usr/bin/say -f /tmp/text.txt -v 'alex' -r 300", 0]
# File lib/mac/say.rb, line 131 def say(string: nil, file: nil, voice: nil, rate: nil) if voice raise VoiceNotFound, "Voice '#{voice}' isn't a valid voice" unless valid_voice?(voice) @config[:voice] = voice end if file raise FileNotFound, "File '#{file}' wasn't found or it's not readable by the current user" unless valid_file_path?(file) @config[:file] = file end @config[:rate] = rate if rate execute_command(string) end
Look for voices by their attributes (e.g. :name, :language, :country, :gender, etc.)
@overload voice(attribute, value)
@param attribute [Symbol] the attribute to search voices by @param value [Symbol, String] the value of the attribute to search voices by
@overload voice(&block)
@yield [voice] Passes the given block to @voices.find_all
@return [Array<Hash>, Hash, nil] an array with all the voices matched by the attribute or
a voice Hash if only one voice corresponds to the attribute, nil if no voices found
@example Find voices by one or more attributes
Mac::Say.new.voice(:joke, false) Mac::Say.new.voice(:gender, :female) Mac::Say.new.voice { |v| v[:joke] == true && v[:gender] == :female } Mac::Say.new.voice { |v| v[:language] == :en && v[:gender] == :male && v[:quality] == :high && v[:joke] == false }
@raise [UnknownVoiceAttribute] if the voice attribute isn't supported
# File lib/mac/say.rb, line 197 def voice(attribute = nil, value = nil, &block) return unless (attribute && !value.nil?) || block_given? raise UnknownVoiceAttribute, "Voice has no '#{attribute}' attribute" if attribute && !VOICE_ATTRIBUTES.include?(attribute) if block_given? found_voices = @voices.find_all(&block) else found_voices = @voices.find_all {|voice| voice[attribute] === value } end return if found_voices.empty? found_voices.count == 1 ? found_voices.first : found_voices end
Private Instance Methods
Actual command execution using current config and the string given
@param string [String] a text to read using say command
@return [Array<String, Integer>] an array with the actual say command used
and it's exit code. E.g.: ["/usr/bin/say -v 'alex' -r 175", 0]
@raise [CommandNotFound] if the say command wasn't found @raise [FileNotFound] if the given file wasn't found or isn't readable by the current user
# File lib/mac/say.rb, line 257 def execute_command(string = nil) say_command = generate_command say = IO.popen(say_command, 'w+') say.write(string) if string say.close [say_command, $CHILD_STATUS.exitstatus] end
Command generation using current config
@return [String] a command to be executed with all the arguments
@raise [CommandNotFound] if the say command wasn't found @raise [FileNotFound] if the given file wasn't found or isn't readable by the current user
# File lib/mac/say.rb, line 272 def generate_command say_path = @config[:say_path] file = @config[:file] raise CommandNotFound, "Command `say` couldn't be found by '#{@config[:say_path]}' path" unless valid_command_path? say_path if file && !valid_file_path?(file) raise FileNotFound, "File '#{file}' wasn't found or it's not readable by the current user" end file = file ? " -f #{@config[:file]}" : '' "#{@config[:say_path]}#{file} -v '#{@config[:voice]}' -r #{@config[:rate].to_i}" end
Parsing voices list from the `say` command itself Memoize voices list for the instance
@return [Array<Hash>, nil] an array of voices Hashes supported by the say command or nil
if voices where parsed before and stored in @voices instance variable
@raise [CommandNotFound] if the say command wasn't found
# File lib/mac/say.rb, line 293 def load_voices return if @voices say_path = @config[:say_path] raise CommandNotFound, "Command `say` couldn't be found by '#{say_path}' path" unless valid_command_path? say_path @voices = `#{say_path} -v '?'`.scan(VOICE_PATTERN).map do |voice| lang = voice[1].split(/[_-]/) name = voice[0].strip.downcase.to_sym additional_attributes = ADDITIONAL_VOICE_ATTRIBUTES[name] || ADDITIONAL_VOICE_ATTRIBUTES[:_unknown_voice] { name: name, language: lang[0].downcase.to_sym, country: lang[1].downcase.to_sym, sample: voice[2].strip }.merge(additional_attributes) end end
Checks say command existence by the path
@param path [String] the path of the `say` command to validate
@return [Boolean] if the command exists and if it is executable
# File lib/mac/say.rb, line 332 def valid_command_path?(path) File.exist?(path) && File.executable?(path) end
Checks text file existence by the path
@param path [String] the path of the text file validate
@return [Boolean] if the file exists and if it is readable
# File lib/mac/say.rb, line 341 def valid_file_path?(path) path && File.exist?(path) && File.readable?(path) end
Checks voice existence by the name Loads voices if they weren't loaded before
@param name [String, Symbol] the name of the voice to validate
@return [Boolean] if the voices name in the list of voices
@raise [CommandNotFound] if the say command wasn't found
# File lib/mac/say.rb, line 322 def valid_voice?(name) load_voices unless @voices voice(:name, name) end