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

config[R]

Current config @return [Hash] a Hash with current configuration

voices[R]

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

new(voice: :alex, rate: 175, file: nil, say_path: ENV['USE_FAKE_SAY'] || '/usr/bin/say') click to toggle source

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
say(string, voice = :alex) click to toggle source

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
voice(attribute = nil, value = nil, &block) click to toggle source

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
voices() click to toggle source

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(string: nil, file: nil, voice: nil, rate: nil)
Alias for: say
say(string: nil, file: nil, voice: nil, rate: nil) click to toggle source

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
Also aliased as: read
voice(attribute = nil, value = nil, &block) click to toggle source

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

execute_command(string = nil) click to toggle source

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
generate_command() click to toggle source

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
load_voices() click to toggle source

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
valid_command_path?(path) click to toggle source

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
valid_file_path?(path) click to toggle source

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
valid_voice?(name) click to toggle source

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