mac-say 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/img/logo.png ADDED
Binary file
Binary file
data/lib/mac/say.rb ADDED
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'say/version'
3
+ require 'English'
4
+
5
+ # Wrapper namespace module for a Say class
6
+ module Mac
7
+ # A class wrapper around the MacOS `say` commad
8
+ # Allows to use simple TTS on Mac right from Ruby scripts
9
+ class Say
10
+ # A regex pattern to parse say voices list output
11
+ VOICES_PATTERN = /(^[\w-]+)\s+([\w-]+)\s+#\s([\p{Graph}\p{Zs}]+$)/i
12
+
13
+ # An error raised when `say` command couldn't be found
14
+ class CommandNotFound < StandardError; end
15
+
16
+ # An error raised when a text file couldn't be found
17
+ class FileNotFound < StandardError; end
18
+
19
+ # An error raised when the given voice isn't valid
20
+ class VoiceNotFound < StandardError; end
21
+
22
+ # An error raised when there is no a feature of voice to match
23
+ class UnknownVoiceFeature < StandardError; end
24
+
25
+ # Current voices list
26
+ #
27
+ # @return [Array<Hash>] an array of voices Hashes supported by the say command
28
+ # @example Get all the voices
29
+ # Mac::Say.voices #=>
30
+ # [
31
+ # {
32
+ # :name => :agnes,
33
+ # :iso_code => {
34
+ # :language => :en,
35
+ # :country => :us
36
+ # },
37
+ # :sample => "Isn't it nice to have a computer that will talk to you?"
38
+ # },
39
+ # {
40
+ # :name => :albert,
41
+ # :iso_code => {
42
+ # :language => :en,
43
+ # :country => :us
44
+ # },
45
+ # :sample => " I have a frog in my throat. No, I mean a real frog!"
46
+ # },
47
+ # ...
48
+ # ]
49
+ attr_reader :voices
50
+
51
+ # Current config
52
+ # @return [Hash] a Hash with current configuration
53
+ attr_reader :config
54
+
55
+ # Say constructor: sets initial configuration for say command to use
56
+ #
57
+ # @param say_path [String] the full path to the say app binary (default: '/usr/bin/say' or USE_FAKE_SAY environment variable)
58
+ # @param voice [Symbol] voice to be used by the say command (default: :alex)
59
+ # @param rate [Integer] speech rate in words per minute (default: 175) accepts values in (175..720)
60
+ # @param file [String] path to the file to read (default: nil)
61
+ #
62
+ # @raise [VoiceNotFound] if the given voice doesn't exist or wasn't installed
63
+ def initialize(voice: :alex, rate: 175, file: nil, say_path: ENV['USE_FAKE_SAY'] || '/usr/bin/say')
64
+ @config = {
65
+ say_path: say_path,
66
+ voice: voice,
67
+ rate: rate,
68
+ file: file
69
+ }
70
+
71
+ @voices = nil
72
+ load_voices
73
+
74
+ raise VoiceNotFound, "Voice '#{voice}' isn't a valid voice" unless valid_voice? voice
75
+ end
76
+
77
+ # Read the given string with the given voice
78
+ #
79
+ # @param string [String] a text to read using say command
80
+ # @param voice [Symbol] voice to be used by the say command (default: :alex)
81
+ #
82
+ # @return [Array<String, Integer>] an array with the actual say command used
83
+ # and it's exit code. E.g.: ["/usr/bin/say -v 'alex' -r 175", 0]
84
+ #
85
+ # @raise [CommandNotFound] if the say command wasn't found
86
+ # @raise [VoiceNotFound] if the given voice doesn't exist or wasn't installed
87
+ def self.say(string, voice = :alex)
88
+ mac = new(voice: voice.downcase.to_sym)
89
+ mac.say(string: string)
90
+ end
91
+
92
+ # Read the given string/file with the given voice and rate
93
+ #
94
+ # Providing file, voice or rate arguments changes instance state and influence
95
+ # all the subsequent #say calls unless they have their own custom arguments
96
+ #
97
+ # @param string [String] a text to read using say command
98
+ # @param file [String] path to the file to read (default: nil)
99
+ # @param voice [Symbol] voice to be used by the say command (default: :alex)
100
+ # @param rate [Integer] speech rate in words per minute (default: 175) accepts values in (175..720)
101
+ #
102
+ # @return [Array<String, Integer>] an array with the actual say command used
103
+ # and it's exit code. E.g.: ["/usr/bin/say -v 'alex' -r 175", 0]
104
+ #
105
+ # @raise [CommandNotFound] if the say command wasn't found
106
+ # @raise [VoiceNotFound] if the given voice doesn't exist or wasn't installed
107
+ # @raise [FileNotFound] if the given file wasn't found or isn't readable by the current user
108
+ #
109
+ # @example Say something (for more examples check README.md or examples/examples.rb files)
110
+ # Mac::Say.new.say string: 'Hello world' #=> ["/usr/bin/say -v 'alex' -r 175", 0]
111
+ # Mac::Say.new.say string: 'Hello world', voice: :fiona #=> ["/usr/bin/say -v 'fiona' -r 175", 0]
112
+ # Mac::Say.new.say file: /tmp/text.txt, rate: 300 #=> ["/usr/bin/say -f /tmp/text.txt -v 'alex' -r 300", 0]
113
+ def say(string: nil, file: nil, voice: nil, rate: nil)
114
+ if voice
115
+ raise VoiceNotFound, "Voice '#{voice}' isn't a valid voice" unless valid_voice?(voice)
116
+ @config[:voice] = voice
117
+ end
118
+
119
+ if file
120
+ raise FileNotFound, "File '#{file}' wasn't found or it's not readable by the current user" unless valid_file_path?(file)
121
+ @config[:file] = file
122
+ end
123
+
124
+ @config[:rate] = rate if rate
125
+
126
+ execute_command(string)
127
+ end
128
+
129
+ # Find a voice by one of its features (e.g. :name, :language, :country)
130
+ #
131
+ # @return [Array<Hash>, Hash] an array with all the voices matched by the feature or
132
+ # a voice Hash if only one voice corresponds to the feature
133
+ #
134
+ # @raise [UnknownVoiceFeature] if the voice feature isn't supported
135
+ def self.voice(feature, name)
136
+ mac = new
137
+ mac.voice(feature, name)
138
+ end
139
+
140
+ # Find a voice by one of its features (e.g. :name, :language, :country)
141
+ #
142
+ # @return [Array<Hash>, Hash] an array with all the voices matched by the feature or
143
+ # a voice Hash if only one voice corresponds to the feature
144
+ #
145
+ # @raise [UnknownVoiceFeature] if the voice feature isn't supported
146
+ def voice(feature, value)
147
+ raise UnknownVoiceFeature, "Voice has no '#{feature}' feature" unless [:name, :language, :country].include?(feature)
148
+
149
+ condition = feature == :name ? ->(v) { v[feature] == value } : ->(v) { v[:iso_code][feature] == value }
150
+ found_voices = @voices.find_all(&condition)
151
+
152
+ return if found_voices.empty?
153
+
154
+ found_voices.count == 1 ? found_voices.first : found_voices
155
+ end
156
+
157
+ # Get all the voices supported by the say command on current machine
158
+ #
159
+ # @return [Array<Hash>] an array of voices Hashes supported by the say command
160
+ # @example Get all the voices
161
+ # Mac::Say.voices #=>
162
+ # [
163
+ # {
164
+ # :name => :agnes,
165
+ # :iso_code => {
166
+ # :language => :en,
167
+ # :country => :us
168
+ # },
169
+ # :sample => "Isn't it nice to have a computer that will talk to you?"
170
+ # },
171
+ # {
172
+ # :name => :albert,
173
+ # :iso_code => {
174
+ # :language => :en,
175
+ # :country => :us
176
+ # },
177
+ # :sample => " I have a frog in my throat. No, I mean a real frog!"
178
+ # },
179
+ # ...
180
+ # ]
181
+ def self.voices
182
+ mac = new
183
+ mac.voices
184
+ end
185
+
186
+ alias read say
187
+
188
+ private
189
+
190
+ # Actual command execution using current config and the string given
191
+ #
192
+ # @param string [String] a text to read using say command
193
+ #
194
+ # @return [Array<String, Integer>] an array with the actual say command used
195
+ # and it's exit code. E.g.: ["/usr/bin/say -v 'alex' -r 175", 0]
196
+ #
197
+ # @raise [CommandNotFound] if the say command wasn't found
198
+ # @raise [FileNotFound] if the given file wasn't found or isn't readable by the current user
199
+ def execute_command(string = nil)
200
+ say_command = generate_command
201
+ say = IO.popen(say_command, 'w+')
202
+ say.write(string) if string
203
+ say.close
204
+
205
+ [say_command, $CHILD_STATUS.exitstatus]
206
+ end
207
+
208
+ # Command generation using current config
209
+ #
210
+ # @return [String] a command to be executed with all the arguments
211
+ #
212
+ # @raise [CommandNotFound] if the say command wasn't found
213
+ # @raise [FileNotFound] if the given file wasn't found or isn't readable by the current user
214
+ def generate_command
215
+ say_path = @config[:say_path]
216
+ file = @config[:file]
217
+
218
+ raise CommandNotFound, "Command `say` couldn't be found by '#{@config[:say_path]}' path" unless valid_command_path? say_path
219
+
220
+ if file && !valid_file_path?(file)
221
+ raise FileNotFound, "File '#{file}' wasn't found or it's not readable by the current user"
222
+ end
223
+
224
+ file = file ? " -f #{@config[:file]}" : ''
225
+ "#{@config[:say_path]}#{file} -v '#{@config[:voice]}' -r #{@config[:rate].to_i}"
226
+ end
227
+
228
+ # Parsing voices list from the `say` command itself
229
+ # Memoize voices list for the instance
230
+ #
231
+ # @return [Array<Hash>, nil] an array of voices Hashes supported by the say command or nil
232
+ # if voices where parsed before and stored in @voices instance variable
233
+ #
234
+ # @raise [CommandNotFound] if the say command wasn't found
235
+ def load_voices
236
+ return if @voices
237
+ say_path = @config[:say_path]
238
+ raise CommandNotFound, "Command `say` couldn't be found by '#{say_path}' path" unless valid_command_path? say_path
239
+
240
+ @voices = `#{say_path} -v '?'`.scan(VOICES_PATTERN).map do |voice|
241
+ lang = voice[1].split(/[_-]/)
242
+ {
243
+ name: voice[0].downcase.to_sym,
244
+ iso_code: { language: lang[0].downcase.to_sym, country: lang[1].downcase.to_sym },
245
+ sample: voice[2]
246
+ }
247
+ end
248
+ end
249
+
250
+ # Checks voice existence by the name
251
+ # Loads voices if they weren't loaded before
252
+ #
253
+ # @return [Boolean] if the voices name in the list of voices
254
+ #
255
+ # @raise [CommandNotFound] if the say command wasn't found
256
+ def valid_voice?(name)
257
+ load_voices unless @voices
258
+ voice(:name, name)
259
+ end
260
+
261
+ # Checks say command existence by the path
262
+ #
263
+ # @return [Boolean] if the command exists and if it is executable
264
+ def valid_command_path?(path)
265
+ File.exist?(path) && File.executable?(path)
266
+ end
267
+
268
+ # Checks text file existence by the path
269
+ #
270
+ # @return [Boolean] if the file exists and if it is readable
271
+ def valid_file_path?(path)
272
+ path && File.exist?(path) && File.readable?(path)
273
+ end
274
+ end
275
+ end
276
+
277
+ p Mac::Say.voice(:name, 'fuck')
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Wrapper namespace module for a Say class
4
+ module Mac
5
+ # A class wrapper around the MacOS `say` commad
6
+ # Allows to use simple TTS on Mac right from Ruby scripts
7
+ class Say
8
+ # mac-say version
9
+ VERSION = '0.1.0'
10
+ end
11
+ end
data/mac-say.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path('../lib', __FILE__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'mac/say/version'
7
+
8
+ Gem::Specification.new do |gem|
9
+ gem.name = "mac-say"
10
+ gem.version = Mac::Say::VERSION
11
+ gem.summary = %q{Ruby wrapper around the macOS `say` command}
12
+ gem.description = %q{Ruby wrapper around the modern version of the macOS `say` command. Inspired by the @bratta's mactts}
13
+ gem.license = "MIT"
14
+ gem.authors = ["Serge Bedzhyk"]
15
+ gem.email = "smileart21@gmail.com"
16
+ gem.homepage = "https://rubygems.org/gems/mac-say"
17
+
18
+ gem.files = `git ls-files`.split($/)
19
+
20
+ `git submodule --quiet foreach --recursive pwd`.split($/).each do |submodule|
21
+ submodule.sub!("#{Dir.pwd}/",'')
22
+
23
+ Dir.chdir(submodule) do
24
+ `git ls-files`.split($/).map do |subpath|
25
+ gem.files << File.join(submodule,subpath)
26
+ end
27
+ end
28
+ end
29
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
30
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
31
+ gem.require_paths = ['lib']
32
+
33
+ gem.add_development_dependency 'bundler', '~> 1.10'
34
+ gem.add_development_dependency 'minitest', '~> 5.0'
35
+ gem.add_development_dependency 'minitest-reporters', '~> 1.1'
36
+ gem.add_development_dependency 'rake', '~> 12.0'
37
+ gem.add_development_dependency 'simplecov', '~> 0.12'
38
+ gem.add_development_dependency 'rubygems-tasks', '~> 0.2'
39
+ gem.add_development_dependency 'yard', '~> 0.8.7.5'
40
+ gem.add_development_dependency 'inch', '~> 0.7.1'
41
+ gem.add_development_dependency 'redcarpet', '~> 3.4'
42
+ gem.add_development_dependency 'github-markup', '~> 1.4'
43
+ gem.add_development_dependency 'm', '~> 1.5'
44
+ gem.add_development_dependency 'coveralls', '~> 0.8'
45
+ end
data/test/fake/say ADDED
@@ -0,0 +1,60 @@
1
+ #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ require 'optparse'
4
+
5
+ VOICES = <<-VOICES
6
+ Alex en_US # Most people recognize me by my voice.
7
+ Daniel en_GB # Hello, my name is Daniel. I am a British-English voice.
8
+ Fiona en-scotland # Hello, my name is Fiona. I am a Scottish-English voice.
9
+ Serena en_GB # Hello, my name is Serena. I am a British-English voice.
10
+ Ting-Ting zh_CN # 您好,我叫Ting-Ting。我讲中文普通话。
11
+ Veena en_IN # Hello, my name is Veena. I am an Indian-English voice.
12
+ VOICES
13
+
14
+ VOICES_NAMES = [
15
+ :alex,
16
+ :daniel,
17
+ :fiona,
18
+ :'ting-ting',
19
+ :veena
20
+ ]
21
+
22
+ options = {
23
+ voice: :alex
24
+ }
25
+
26
+ OptionParser.new do |opts|
27
+ opts.banner = 'Usage: say [options]'
28
+
29
+ opts.on('-v VOICE') do |v|
30
+ options[:voice] = v.to_sym
31
+ end
32
+
33
+ opts.on('-r RATE') do |r|
34
+ options[:rate] = r.to_i
35
+ end
36
+
37
+ opts.on('-f FILE') do |f|
38
+ options[:file] = f
39
+ end
40
+ end.parse!
41
+
42
+ input = STDIN.read if !STDIN.tty? && !STDIN.closed?
43
+
44
+ if options[:voice]
45
+ if options[:voice] == :'?'
46
+ print VOICES
47
+ exit 0
48
+ end
49
+ exit 1 unless VOICES_NAMES.include?(options[:voice])
50
+ exit 0 if input && !input.empty?
51
+ end
52
+
53
+ if options[:file]
54
+ file_path = options[:file]
55
+ file_path = File.absolute_path file_path, File.dirname(__FILE__)
56
+
57
+ File.exist?(file_path) ? exit(0) : exit(127)
58
+ end
59
+
60
+ exit 1
@@ -0,0 +1,4 @@
1
+ Alice had sat on the bank by her sister till she was tired.
2
+ Once or twice she had looked at the book her sister held in her hand, but there were no pictures in it, "and what is the use of a book," thought Alice, "without pictures?"
3
+ She asked herself as well as she could, for the hot day made her feel quite dull, if it would be worth while to get up and pick some daisies to make a chain.
4
+ Just then a white rabbit with pink eyes ran close by her.
@@ -0,0 +1 @@
1
+ British English
@@ -0,0 +1,7 @@
1
+ Atticus said to Jem one day, "I’d rather you shot at tin cans in the backyard, but I know you’ll go after birds.
2
+ Shoot all the blue jays you want, if you can hit ‘em, but remember it’s a sin to kill a mockingbird."
3
+
4
+ That was the only time I ever heard Atticus say it was a sin to do something, and I asked Miss Maudie about it.
5
+ "Your father’s right," she said. "Mockingbirds don’t do one thing except make music for us to enjoy.
6
+ They don’t eat up people’s gardens, don’t nest in corn cribs, they don’t do one thing but sing their hearts out
7
+ for us. That’s why it’s a sin to kill a mockingbird.
@@ -0,0 +1 @@
1
+ American English
data/test/helper.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require 'rubygems'
3
+
4
+ require 'coveralls'
5
+ Coveralls.wear!
6
+
7
+ begin
8
+ require 'bundler/setup'
9
+ rescue LoadError => error
10
+ abort error.message
11
+ end
12
+
13
+ require 'simplecov'
14
+ SimpleCov.start
15
+
16
+ require 'minitest/autorun'
17
+ require 'minitest/reporters'
18
+
19
+ Minitest::Reporters.use! [
20
+ Minitest::Reporters::SpecReporter.new
21
+ ]
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+ require 'helper'
3
+ require 'mac/say'
4
+
5
+ describe 'Mac::Say as a macOS `say` wrapper' do
6
+ describe 'On a class level' do
7
+ before do
8
+ @say_path = ENV['USE_FAKE_SAY'] ? ENV['USE_FAKE_SAY'] : '/usr/bin/say'
9
+ end
10
+
11
+ it 'must have a VERSION constant' do
12
+ Mac::Say.const_get('VERSION').wont_be_empty
13
+ end
14
+
15
+ it 'must return available voices as an Array of Hashes' do
16
+ Mac::Say.voices.wont_be_empty
17
+ Mac::Say.voices.must_be_kind_of Array
18
+ end
19
+
20
+ it 'must return specific Hash structure for a voice' do
21
+ voice = Mac::Say.voice(:name, :alex)
22
+ voice.wont_be_empty
23
+ voice.must_be_kind_of Hash
24
+ voice.keys.must_equal [:name, :iso_code, :sample]
25
+ end
26
+
27
+ it 'must return specific Hash structure for an iso_code' do
28
+ voice = Mac::Say.voice(:name, :alex)
29
+ voice.wont_be_empty
30
+ voice[:iso_code].must_be_kind_of Hash
31
+ voice[:iso_code].keys.must_equal [:language, :country]
32
+ end
33
+
34
+ it '.voice must search for a voice by name' do
35
+ voice = Mac::Say.voice(:name, :alex)
36
+ voice[:name].must_equal :alex
37
+ end
38
+
39
+ it '.voice must search for a voice by country' do
40
+ voice = Mac::Say.voice(:country, :scotland)
41
+ voice[:name].must_equal :fiona
42
+ end
43
+
44
+ it '.voice must search for a voice by language' do
45
+ voices = Mac::Say.voice(:language, :en)
46
+ voices.count.must_be :>, 2
47
+ end
48
+
49
+ it '.voice must return one voice as a Hash' do
50
+ voice = Mac::Say.voice(:name, :alex)
51
+ voice.must_be_kind_of Hash
52
+ end
53
+
54
+ it '.voice must return an Array of voices if > 1' do
55
+ voices = Mac::Say.voice(:country, :gb)
56
+ voices.must_be_kind_of Array
57
+ end
58
+
59
+ it ".voice must return nil if voice wasn't found" do
60
+ voices = Mac::Say.voice(:name, :xxx)
61
+ voices.must_be_nil
62
+ end
63
+
64
+ it '.say must return 0 in successive speech' do
65
+ expectation = ["#{@say_path} -v 'alex' -r 175", 0]
66
+ Mac::Say.say('42').must_equal expectation
67
+ end
68
+
69
+ it '.say must use custom voice' do
70
+ expectation = ["#{@say_path} -v 'alex' -r 175", 0]
71
+ Mac::Say.say('42', :alex).must_equal expectation
72
+ end
73
+
74
+ it '.say must work with multiple lines' do
75
+ expectation = ["#{@say_path} -v 'alex' -r 175", 0]
76
+ Mac::Say.say(<<-TEXT, :alex).must_equal expectation
77
+ 1
78
+ 2
79
+ 3
80
+ TEXT
81
+ end
82
+
83
+ it '.say must fail on wrong voice' do
84
+ -> {
85
+ Mac::Say.say 'OMG! I lost my voice!', :wrong
86
+ }.must_raise Mac::Say::VoiceNotFound
87
+ end
88
+
89
+ it '.voice must fail on wrong voice feature' do
90
+ -> {
91
+ Mac::Say.voice(:tone, :enthusiastic)
92
+ }.must_raise Mac::Say::UnknownVoiceFeature
93
+ end
94
+ end
95
+
96
+ describe "On an instance level" do
97
+ before do
98
+ @reader = Mac::Say.new
99
+ @say_path = @reader.config[:say_path]
100
+ end
101
+
102
+ it 'must instantiate Mac::Say' do
103
+ @reader.must_be_instance_of Mac::Say
104
+ end
105
+
106
+ it '#say must return 0 on successive speech' do
107
+ expectation = ["#{@say_path} -v 'alex' -r 175", 0]
108
+ @reader.say(string: '42').must_equal expectation
109
+ end
110
+
111
+ it '#read must be a synonym of #say' do
112
+ expectation = ["#{@say_path} -v 'alex' -r 175", 0]
113
+ @reader.read(string: '42').must_equal expectation
114
+ end
115
+
116
+ it '#say must support :file' do
117
+ absolute_path = File.absolute_path './fixtures/text/en_gb_test.txt', File.dirname(__FILE__)
118
+ expectation = ["#{@say_path} -f #{absolute_path} -v 'alex' -r 175", 0]
119
+
120
+ @reader.say(file: absolute_path).must_equal expectation
121
+ end
122
+
123
+ it '#say must read :file from initial config' do
124
+ absolute_path = File.absolute_path './fixtures/text/en_gb_test.txt', File.dirname(__FILE__)
125
+ expectation = ["#{@say_path} -f #{absolute_path} -v 'alex' -r 175", 0]
126
+
127
+ @reader = Mac::Say.new(file: absolute_path)
128
+ @reader.say.must_equal expectation
129
+ end
130
+
131
+ it '#say must change :file from initial config' do
132
+ gb_absolute_path = File.absolute_path './fixtures/text/en_gb_test.txt', File.dirname(__FILE__)
133
+ us_absolute_path = File.absolute_path './fixtures/text/en_us_test.txt', File.dirname(__FILE__)
134
+
135
+ expectation = ["#{@say_path} -f #{us_absolute_path} -v 'alex' -r 175", 0]
136
+
137
+ # init
138
+ @reader = Mac::Say.new(file: gb_absolute_path)
139
+ @reader.config[:file].must_equal gb_absolute_path
140
+
141
+ # change
142
+ @reader.say(file: us_absolute_path).must_equal expectation
143
+ @reader.config[:file].must_equal us_absolute_path
144
+ end
145
+
146
+ it '#say must prioritise :file over :string' do
147
+ absolute_path = File.absolute_path './fixtures/text/en_gb_test.txt', File.dirname(__FILE__)
148
+ expectation = ["#{@say_path} -f #{absolute_path} -v 'alex' -r 175", 0]
149
+
150
+ @reader.say(string: 'test', file: absolute_path).must_equal expectation
151
+ end
152
+
153
+ it '#say must support custom :rate' do
154
+ expectation = ["#{@say_path} -v 'alex' -r 250", 0]
155
+ @reader.say(string: '42', rate: 250).must_equal expectation
156
+ end
157
+
158
+ it '#say must support custom :voice' do
159
+ expectation = ["#{@say_path} -v 'fiona' -r 175", 0]
160
+ @reader.say(string: '42', voice: :fiona).must_equal expectation
161
+ end
162
+
163
+ it '#say must change the :voice' do
164
+ expectation = ["#{@say_path} -v 'fiona' -r 175", 0]
165
+ @reader.config[:voice].must_equal :alex
166
+
167
+ @reader.say(string: '42', voice: :fiona).must_equal expectation
168
+ @reader.config[:voice].must_equal :fiona
169
+
170
+ @reader.say(string: '13').must_equal expectation
171
+ end
172
+
173
+ it '#say must change the :rate' do
174
+ expectation = ["#{@say_path} -v 'alex' -r 300", 0]
175
+ @reader.config[:rate].must_equal 175
176
+
177
+ @reader.say(string: '42', rate: 300).must_equal expectation
178
+ @reader.config[:rate].must_equal 300
179
+
180
+ @reader.say(string: '13').must_equal expectation
181
+ end
182
+
183
+ it '#say must fail on wrong initial voice' do
184
+ -> {
185
+ talker = Mac::Say.new(voice: :wrong)
186
+ talker.say string: 'OMG! I lost my voice!'
187
+ }.must_raise Mac::Say::VoiceNotFound
188
+ end
189
+
190
+ it '#say must fail on wrong dynamic voice' do
191
+ -> {
192
+ talker = Mac::Say.new
193
+ talker.say string: 'OMG! I lost my voice!', voice: :wrong
194
+ }.must_raise Mac::Say::VoiceNotFound
195
+ end
196
+
197
+ it '#voice must fail on wrong say path' do
198
+ -> {
199
+ Mac::Say.new(say_path: '/wrong/wrong/path').voice(:name, :alex)
200
+ }.must_raise Mac::Say::CommandNotFound
201
+ end
202
+
203
+ it '#say must fail on wrong say path' do
204
+ -> {
205
+ Mac::Say.new(say_path: '/wrong/wrong/path').say 'test'
206
+ }.must_raise Mac::Say::CommandNotFound
207
+ end
208
+
209
+ it '#say must fail on wrong file path' do
210
+ -> {
211
+ Mac::Say.new.say(file: '/wrong/wrong/path')
212
+ }.must_raise Mac::Say::FileNotFound
213
+ end
214
+
215
+ it '#voice must fail on wrong feature' do
216
+ -> {
217
+ Mac::Say.new.voice(:articulation, :nostalgic)
218
+ }.must_raise Mac::Say::UnknownVoiceFeature
219
+ end
220
+
221
+ it '#say must fail on initial wrong file path' do
222
+ -> {
223
+ Mac::Say.new(file: '/wrong/wrong/path').say
224
+ }.must_raise Mac::Say::FileNotFound
225
+ end
226
+ end
227
+ end