💬 Ruby wrapper around the macOS `say` command
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
examples
img
lib/mac
test
.document
.gitignore
.rubocop.yml
.travis.yml
.yardopts
ChangeLog.md
Gemfile
LICENSE.txt
README.md
Rakefile
config.reek
mac-say.gemspec

README.md

mac-say

Ruby wrapper around the modern version of the macOS say command. Inspired by the @bratta's mactts

Build Status Coverage Status Code Climate InchCI Status mac-say gem

Features

  • Basic strings reading
  • Basic files reading
  • Multiline strings support
  • Dynamic voices parsing (based on real say output)
  • Voices list generation (including samples and ISO information)
  • Voices search (by name / language / country / etc.)
  • Simple (class-level) and customisable (instance-level) usage
  • Observe reading progress line by line
  • Audio output support

Install

$ gem install mac-say

Examples

require 'pp'
require 'mac/say'

# ===== Class level =====

# Get all the voices
pp Mac::Say.voices

# Collect the separate attributes lists
pp Mac::Say.voices.collect { |v| v[:name] }
pp Mac::Say.voices.collect { |v| v[:language] }
pp Mac::Say.voices.collect { |v| v[:sample] }

# Look for voices by an attribute
pp Mac::Say.voice(:singing, true)
pp Mac::Say.voice(:joke, false)
pp Mac::Say.voice(:gender, :female)

# Look for voices by multiple attributes
pp Mac::Say.voice { |v| v[:joke] == true && v[:gender] == :female }
pp Mac::Say.voice { |v| v[:language] == :en && v[:gender] == :male && v[:quality] == :high && v[:joke] == false }

# Find a voice (returns a Hash)
pp Mac::Say.voice(:name, :alex)
pp Mac::Say.voice(:country, :scotland)

# Find the voices by the feature (returns an Array)
pp Mac::Say.voice(:language, :en)

# Work with the voices collection
indian_english = Mac::Say.voice(:country, :in).select { |v| v[:language] == :en }.first[:name]

# Use multiline text
puts Mac::Say.say <<-DATA, indian_english
  Invokes the given block passing in successive elements from self, deleting elements for which the block returns a false value.
  The array may not be changed instantly every time the block is called.
  If changes were made, it will return self, otherwise it returns nil.
DATA

# ===== Instance level =====

# with constant name in the constructor and custom rate
talker = Mac::Say.new(voice: :alex, rate: 300)
talker.say string: 'Hello world'

# with the voice name from the class method + dynamic sample
talker = Mac::Say.new(voice: Mac::Say.voice(:country, :scotland)[:name])
talker.say string: talker.voice(:country, :scotland)[:sample]

# with the dynamic voice name selected from the multiple voices
talker = Mac::Say.new
voice = talker.voice(:language, :en)&.sample(1)&.first&.fetch :name
talker.say string: 'Hello world!', voice: voice

# changing voice in runtime for an instance of talker (while saying something)
voice = talker.voice(:country, :kr)
talker.say string: voice[:sample], voice: voice[:name]

# or change the voice without saying anything
talker.say voice: :"ting-ting"
talker.say string: '您好,我叫Ting-Ting。我讲中文普通话。'

# Listen to all the languages with the dynamic voices + dynamic samples
polyglot = Mac::Say.new
voices = polyglot.voices

voices.each_with_index do |v, i|
  puts "#{i + 1} :: #{v[:name]} :: '#{v[:sample]}'"
  polyglot.say string: v[:sample], voice: v[:name]
end

# Or perform a roll call
roll_call = Mac::Say.new
voices = roll_call.voices

voices.each_with_index do |v, i|
  puts "#{i + 1} :: #{v[:name]}"
  roll_call.say string: v[:name], voice: v[:name]
end

# ===== Reading files =====

# Read the file (prior to the string)
file_path = File.absolute_path '../test/fixtures/text/en_gb.txt', File.dirname(__FILE__)

# with a voice from the class
voice = Mac::Say.voice(:country, :gb)&.first&.fetch(:name)
reader = Mac::Say.new(file: file_path, voice: voice)
reader.read

# with a dynamic voice from the class
voice = Mac::Say.voice(:country, :scotland)[:name]
reader = Mac::Say.new(file: file_path)
reader.read voice: voice

# with a dynamic voice from the instance
reader = Mac::Say.new(file: file_path)
reader.read(voice: reader.voice(:country, :us)[2][:name])

# with a dynamic voice from the instance
new_file_path = File.absolute_path '../test/fixtures/text/en_us.txt', File.dirname(__FILE__)

# with a dynamic file change
reader = Mac::Say.new(file: file_path)
reader.read voice: :alex
reader.read file: new_file_path

# ===== Exceptions =====

# wrong file
begin
  reader = Mac::Say.new(file: 'wrong')
  reader.read
rescue Mac::Say::FileNotFound => e
  puts e.message
end

# wrong file
begin
  reader = Mac::Say.new
  reader.read file: 'too_wrong'
rescue Mac::Say::FileNotFound => e
  puts e.message
end

# wrong path
begin
  Mac::Say.new(say_path: '/usr/bin/wrong_say')
rescue Mac::Say::CommandNotFound => e
  puts e.message
end

# wrong voice
begin
  talker = Mac::Say.new(voice: :wrong)
  talker.say string: 'OMG! I lost my voice!'
rescue Mac::Say::VoiceNotFound => e
  puts e.message
end

# wrong voice
begin
  talker = Mac::Say.new
  talker.say string: 'OMG! I lost my voice!', voice: :too_wrong
rescue Mac::Say::VoiceNotFound => e
  puts e.message
end

# wrong voice
begin
  Mac::Say.say 'OMG! I lost my voice!', :still_wrong
rescue Mac::Say::VoiceNotFound => e
  puts e.message
end

# wrong feature
begin
  Mac::Say.voice(:tone, :enthusiastic)
rescue Mac::Say::UnknownVoiceAttribute => e
  puts e.message
end

# wrong feature
begin
  Mac::Say.new.voice(:articulation, :nostalgic)
rescue Mac::Say::UnknownVoiceAttribute => e
  puts e.message
end

Installing & Updating MacOS TTS Voices

Open System Preferences using Spotlight / Alfred / Dock and follow text or visual instructions:

System Preferences → Accessibility → Speech → System Voice →
→ Customize… → (select voices) → OK → (Wait for download…)

Installing & Updating MacOS TTS Voices

Dev Notes

# generated with Ore: https://github.com/ruby-ore/ore
$ mine mac-say --git --mit --rubygems-tasks --markdown --minitest --travis --yard

# generate docs (unless this resolved: https://github.com/rrrene/inch/issues/42)
$ yard --markup markdown --markup-provider=redcarpet --title "mac-say Documentation" --protected --asset img:img

# check the docs
$ inch --pedantic

# test with a fake `say`
$ USE_FAKE_SAY='./test/fake/say' bundle exec rake test

# test with rake
$ bundle exec rake test

# test with m
$ bundle exec m

# run one test by LN
$ bundle exec m ./test/test_mac-say.rb:34

Copyright

Copyright (c) 2017 Serge Bedzhyk

See LICENSE.txt for details.