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.
- checksums.yaml +7 -0
- data/.document +4 -0
- data/.gitignore +40 -0
- data/.rubocop.yml +15 -0
- data/.travis.yml +12 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +20 -0
- data/README.md +237 -0
- data/Rakefile +27 -0
- data/config.reek +4 -0
- data/examples/examples.rb +160 -0
- data/img/logo.png +0 -0
- data/img/voices_manual.png +0 -0
- data/lib/mac/say.rb +277 -0
- data/lib/mac/say/version.rb +11 -0
- data/mac-say.gemspec +45 -0
- data/test/fake/say +60 -0
- data/test/fixtures/text/en_gb.txt +4 -0
- data/test/fixtures/text/en_gb_test.txt +1 -0
- data/test/fixtures/text/en_us.txt +7 -0
- data/test/fixtures/text/en_us_test.txt +1 -0
- data/test/helper.rb +21 -0
- data/test/test_mac-say.rb +227 -0
- metadata +243 -0
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
|