mac-say 0.1.1 → 0.2.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 +4 -4
- data/ChangeLog.md +19 -2
- data/README.md +15 -7
- data/examples/examples.rb +14 -6
- data/lib/mac/say.rb +115 -51
- data/lib/mac/say/version.rb +2 -2
- data/lib/mac/say/voices_attributes.rb +510 -0
- data/test/fake/say +3 -1
- data/test/helper.rb +7 -3
- data/test/test_mac-say.rb +30 -24
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 691366a15525f635d6c95a206f467a75a3692f3c
|
4
|
+
data.tar.gz: 19b525a7de0607f5a08759a5212c90622d066e7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 299481a3cf645e1c24759a9e254e3a902c90bce1e5e3a9fe4eb3c4303b178716942de5cf5906885e64d537bcb05ac068c6d489eeb4652dfbae9a16876c340c8c
|
7
|
+
data.tar.gz: 6582d02d8da76153bc33352b62fab27a3d23820a056decdafa18a29a572e2b8ac8793a97d758166fbd1cbd83180254b8ac750c1d252feb5d617dc216efaeaf0d
|
data/ChangeLog.md
CHANGED
@@ -1,4 +1,21 @@
|
|
1
|
-
|
1
|
+
v0.2.0 / 2017-02-16
|
2
|
+
===================
|
2
3
|
|
3
|
-
*
|
4
|
+
* Added useful voices meta-information: gender, quality, "is it a joke?"-flag (WARNING: all the attributes values provided are highly subjective!)
|
5
|
+
* Implemented searching for voices by more than one attribute (i.e. @voices.find_all &block delegation)
|
6
|
+
* Improved/Fixed documentation
|
4
7
|
|
8
|
+
v.0.1.1 / 2017-01-30
|
9
|
+
====================
|
10
|
+
|
11
|
+
* Fixed leaked `nil` output from the main lib file
|
12
|
+
* Test code with colourful language deleted from the main lib file 🤦🏻♂️
|
13
|
+
|
14
|
+
v0.1.0 / 2017-01-30
|
15
|
+
===================
|
16
|
+
|
17
|
+
* First version
|
18
|
+
* Basic TTS (Strings/Multiline strings/Files)
|
19
|
+
* Dynamic voices list parsing (based on the real `say` output)
|
20
|
+
* Voices search (by one of the attributes: name / language / country)
|
21
|
+
* Full test/docs coverage + CI configuration + fake `say` command for CI
|
data/README.md
CHANGED
@@ -19,7 +19,7 @@
|
|
19
19
|
* [x] Multiline strings support
|
20
20
|
* [x] Dynamic voices parsing (based on real `say` output)
|
21
21
|
* [x] Voices list generation (including samples and ISO information)
|
22
|
-
* [x] Voices search (by name / language / country)
|
22
|
+
* [x] Voices search (by name / language / country / etc.)
|
23
23
|
* [x] Simple (class-level) and customisable (instance-level) usage
|
24
24
|
* [ ] Observe reading progress line by line❓
|
25
25
|
* [ ] Audio output support❓
|
@@ -41,11 +41,19 @@ require 'mac/say'
|
|
41
41
|
# Get all the voices
|
42
42
|
pp Mac::Say.voices
|
43
43
|
|
44
|
-
# Collect the separate
|
44
|
+
# Collect the separate attributes lists
|
45
45
|
pp Mac::Say.voices.collect { |v| v[:name] }
|
46
|
-
pp Mac::Say.voices.collect { |v| v[:
|
46
|
+
pp Mac::Say.voices.collect { |v| v[:language] }
|
47
47
|
pp Mac::Say.voices.collect { |v| v[:sample] }
|
48
48
|
|
49
|
+
# Look for voices by an attribute
|
50
|
+
pp Mac::Say.voice(:joke, false)
|
51
|
+
pp Mac::Say.voice(:gender, :female)
|
52
|
+
|
53
|
+
# Look for voices by a buch of attributes
|
54
|
+
pp Mac::Say.voice { |v| v[:joke] == true && v[:gender] == :female }
|
55
|
+
pp Mac::Say.voice { |v| v[:language] == :en && v[:gender] == :male && v[:quality] == :high && v[:joke] == false }
|
56
|
+
|
49
57
|
# Find a voice (returns a Hash)
|
50
58
|
pp Mac::Say.voice(:name, :alex)
|
51
59
|
pp Mac::Say.voice(:country, :scotland)
|
@@ -54,7 +62,7 @@ pp Mac::Say.voice(:country, :scotland)
|
|
54
62
|
pp Mac::Say.voice(:language, :en)
|
55
63
|
|
56
64
|
# Work with the voices collection
|
57
|
-
indian_english = Mac::Say.voice(:country, :in).select { |v| v[:
|
65
|
+
indian_english = Mac::Say.voice(:country, :in).select { |v| v[:language] == :en }.first[:name]
|
58
66
|
|
59
67
|
# Use multiline text
|
60
68
|
puts Mac::Say.say <<-DATA, indian_english
|
@@ -74,7 +82,7 @@ talker = Mac::Say.new(voice: Mac::Say.voice(:country, :scotland)[:name])
|
|
74
82
|
talker.say string: talker.voice(:country, :scotland)[:sample]
|
75
83
|
|
76
84
|
# with the dynamic voice name selected from the multiple voices
|
77
|
-
talker = Mac::Say.
|
85
|
+
talker = Mac::Say.new
|
78
86
|
voice = talker.voice(:language, :en)&.sample(1)&.first&.fetch :name
|
79
87
|
talker.say string: 'Hello world!', voice: voice
|
80
88
|
|
@@ -182,14 +190,14 @@ end
|
|
182
190
|
# wrong feature
|
183
191
|
begin
|
184
192
|
Mac::Say.voice(:tone, :enthusiastic)
|
185
|
-
rescue Mac::Say::
|
193
|
+
rescue Mac::Say::UnknownVoiceAttribute => e
|
186
194
|
puts e.message
|
187
195
|
end
|
188
196
|
|
189
197
|
# wrong feature
|
190
198
|
begin
|
191
199
|
Mac::Say.new.voice(:articulation, :nostalgic)
|
192
|
-
rescue Mac::Say::
|
200
|
+
rescue Mac::Say::UnknownVoiceAttribute => e
|
193
201
|
puts e.message
|
194
202
|
end
|
195
203
|
```
|
data/examples/examples.rb
CHANGED
@@ -7,11 +7,19 @@ require_relative '../lib/mac/say'
|
|
7
7
|
# Get all the voices
|
8
8
|
pp Mac::Say.voices
|
9
9
|
|
10
|
-
# Collect the separate
|
10
|
+
# Collect the separate attributes lists
|
11
11
|
pp Mac::Say.voices.collect { |v| v[:name] }
|
12
|
-
pp Mac::Say.voices.collect { |v| v[:
|
12
|
+
pp Mac::Say.voices.collect { |v| v[:language] }
|
13
13
|
pp Mac::Say.voices.collect { |v| v[:sample] }
|
14
14
|
|
15
|
+
# Look for voices by an attribute
|
16
|
+
pp Mac::Say.voice(:joke, false)
|
17
|
+
pp Mac::Say.voice(:gender, :female)
|
18
|
+
|
19
|
+
# Look for voices by multiple attributes
|
20
|
+
pp Mac::Say.voice { |v| v[:joke] == true && v[:gender] == :female }
|
21
|
+
pp Mac::Say.voice { |v| v[:language] == :en && v[:gender] == :male && v[:quality] == :high && v[:joke] == false }
|
22
|
+
|
15
23
|
# Find a voice (returns a Hash)
|
16
24
|
pp Mac::Say.voice(:name, :alex)
|
17
25
|
pp Mac::Say.voice(:country, :scotland)
|
@@ -20,7 +28,7 @@ pp Mac::Say.voice(:country, :scotland)
|
|
20
28
|
pp Mac::Say.voice(:language, :en)
|
21
29
|
|
22
30
|
# Work with the voices collection
|
23
|
-
indian_english = Mac::Say.voice(:country, :in).select { |v| v[:
|
31
|
+
indian_english = Mac::Say.voice(:country, :in).select { |v| v[:language] == :en }.first[:name]
|
24
32
|
|
25
33
|
# Use multiline text
|
26
34
|
puts Mac::Say.say <<-DATA, indian_english
|
@@ -40,7 +48,7 @@ talker = Mac::Say.new(voice: Mac::Say.voice(:country, :scotland)[:name])
|
|
40
48
|
talker.say string: talker.voice(:country, :scotland)[:sample]
|
41
49
|
|
42
50
|
# with the dynamic voice name selected from the multiple voices
|
43
|
-
talker = Mac::Say.
|
51
|
+
talker = Mac::Say.new
|
44
52
|
voice = talker.voice(:language, :en)&.sample(1)&.first&.fetch :name
|
45
53
|
talker.say string: 'Hello world!', voice: voice
|
46
54
|
|
@@ -148,13 +156,13 @@ end
|
|
148
156
|
# wrong feature
|
149
157
|
begin
|
150
158
|
Mac::Say.voice(:tone, :enthusiastic)
|
151
|
-
rescue Mac::Say::
|
159
|
+
rescue Mac::Say::UnknownVoiceAttribute => e
|
152
160
|
puts e.message
|
153
161
|
end
|
154
162
|
|
155
163
|
# wrong feature
|
156
164
|
begin
|
157
165
|
Mac::Say.new.voice(:articulation, :nostalgic)
|
158
|
-
rescue Mac::Say::
|
166
|
+
rescue Mac::Say::UnknownVoiceAttribute => e
|
159
167
|
puts e.message
|
160
168
|
end
|
data/lib/mac/say.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require_relative 'say/version'
|
3
|
+
require_relative 'say/voices_attributes'
|
4
|
+
|
3
5
|
require 'English'
|
4
6
|
|
5
7
|
# Wrapper namespace module for a Say class
|
@@ -8,7 +10,7 @@ module Mac
|
|
8
10
|
# Allows to use simple TTS on Mac right from Ruby scripts
|
9
11
|
class Say
|
10
12
|
# A regex pattern to parse say voices list output
|
11
|
-
|
13
|
+
VOICE_PATTERN = /(^[\w-]+)\s+([\w-]+)\s+#\s([\p{Graph}\p{Zs}]+$)/i
|
12
14
|
|
13
15
|
# An error raised when `say` command couldn't be found
|
14
16
|
class CommandNotFound < StandardError; end
|
@@ -19,8 +21,19 @@ module Mac
|
|
19
21
|
# An error raised when the given voice isn't valid
|
20
22
|
class VoiceNotFound < StandardError; end
|
21
23
|
|
22
|
-
# An error raised when there is no a
|
23
|
-
class
|
24
|
+
# An error raised when there is no a attribute of voice to match
|
25
|
+
class UnknownVoiceAttribute < StandardError; end
|
26
|
+
|
27
|
+
# The list of the voice attributes available
|
28
|
+
VOICE_ATTRIBUTES = [
|
29
|
+
:name,
|
30
|
+
:language,
|
31
|
+
:country,
|
32
|
+
:sample,
|
33
|
+
:gender,
|
34
|
+
:joke,
|
35
|
+
:quality
|
36
|
+
]
|
24
37
|
|
25
38
|
# Current voices list
|
26
39
|
#
|
@@ -29,20 +42,22 @@ module Mac
|
|
29
42
|
# Mac::Say.voices #=>
|
30
43
|
# [
|
31
44
|
# {
|
32
|
-
# :name
|
33
|
-
# :
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
# :
|
45
|
+
# :name => :agnes,
|
46
|
+
# :language => :en,
|
47
|
+
# :country => :us,
|
48
|
+
# :sample => "Isn't it nice to have a computer that will talk to you?",
|
49
|
+
# :gender => :female,
|
50
|
+
# :joke => false,
|
51
|
+
# :quality => :low
|
38
52
|
# },
|
39
53
|
# {
|
40
|
-
# :name
|
41
|
-
# :
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# :
|
54
|
+
# :name => :albert,
|
55
|
+
# :language => :en,
|
56
|
+
# :country => :us,
|
57
|
+
# :sample => "I have a frog in my throat. No, I mean a real frog!",
|
58
|
+
# :gender => :male,
|
59
|
+
# :joke => true,
|
60
|
+
# :quality => :medium
|
46
61
|
# },
|
47
62
|
# ...
|
48
63
|
# ]
|
@@ -94,7 +109,7 @@ module Mac
|
|
94
109
|
# Providing file, voice or rate arguments changes instance state and influence
|
95
110
|
# all the subsequent #say calls unless they have their own custom arguments
|
96
111
|
#
|
97
|
-
# @param string [String] a text to read using say command
|
112
|
+
# @param string [String] a text to read using say command (default: nil)
|
98
113
|
# @param file [String] path to the file to read (default: nil)
|
99
114
|
# @param voice [Symbol] voice to be used by the say command (default: :alex)
|
100
115
|
# @param rate [Integer] speech rate in words per minute (default: 175) accepts values in (175..720)
|
@@ -126,32 +141,67 @@ module Mac
|
|
126
141
|
execute_command(string)
|
127
142
|
end
|
128
143
|
|
129
|
-
#
|
144
|
+
# Look for voices by their attributes (e.g. :name, :language, :country, :gender, etc.)
|
145
|
+
#
|
146
|
+
# @overload voice(attribute, value)
|
147
|
+
# @param attribute [Symbol] the attribute to search voices by
|
148
|
+
# @param value [Symbol, String] the value of the attribute to search voices by
|
149
|
+
# @overload voice(&block)
|
150
|
+
# @yield [voice] Passes the given block to @voices.find_all
|
151
|
+
#
|
152
|
+
# @return [Array<Hash>, Hash, nil] an array with all the voices matched by the attribute or
|
153
|
+
# a voice Hash if only one voice corresponds to the attribute, nil if no voices found
|
154
|
+
#
|
155
|
+
# @example Find voices by one or more attributes
|
130
156
|
#
|
131
|
-
#
|
132
|
-
#
|
157
|
+
# Mac::Say.new.voice(:joke, false)
|
158
|
+
# Mac::Say.new.voice(:gender, :female)
|
133
159
|
#
|
134
|
-
#
|
135
|
-
|
160
|
+
# Mac::Say.new.voice { |v| v[:joke] == true && v[:gender] == :female }
|
161
|
+
# Mac::Say.new.voice { |v| v[:language] == :en && v[:gender] == :male && v[:quality] == :high && v[:joke] == false }
|
162
|
+
#
|
163
|
+
# @raise [UnknownVoiceAttribute] if the voice attribute isn't supported def self.voice(attribute = nil, value = nil, &block)
|
164
|
+
def self.voice(attribute = nil, value = nil, &block)
|
136
165
|
mac = new
|
137
|
-
|
166
|
+
|
167
|
+
if block_given?
|
168
|
+
mac.voice(&block)
|
169
|
+
else
|
170
|
+
mac.voice(attribute, value)
|
171
|
+
end
|
138
172
|
end
|
139
173
|
|
140
|
-
#
|
174
|
+
# Look for voices by their attributes (e.g. :name, :language, :country, :gender, etc.)
|
141
175
|
#
|
142
|
-
# @
|
143
|
-
#
|
176
|
+
# @overload voice(attribute, value)
|
177
|
+
# @param attribute [Symbol] the attribute to search voices by
|
178
|
+
# @param value [Symbol, String] the value of the attribute to search voices by
|
179
|
+
# @overload voice(&block)
|
180
|
+
# @yield [voice] Passes the given block to @voices.find_all
|
144
181
|
#
|
145
|
-
# @
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
182
|
+
# @return [Array<Hash>, Hash, nil] an array with all the voices matched by the attribute or
|
183
|
+
# a voice Hash if only one voice corresponds to the attribute, nil if no voices found
|
184
|
+
#
|
185
|
+
# @example Find voices by one or more attributes
|
186
|
+
#
|
187
|
+
# Mac::Say.new.voice(:joke, false)
|
188
|
+
# Mac::Say.new.voice(:gender, :female)
|
189
|
+
#
|
190
|
+
# Mac::Say.new.voice { |v| v[:joke] == true && v[:gender] == :female }
|
191
|
+
# Mac::Say.new.voice { |v| v[:language] == :en && v[:gender] == :male && v[:quality] == :high && v[:joke] == false }
|
192
|
+
#
|
193
|
+
# @raise [UnknownVoiceAttribute] if the voice attribute isn't supported
|
194
|
+
def voice(attribute = nil, value = nil, &block)
|
195
|
+
return unless (attribute && !value.nil?) || block_given?
|
196
|
+
raise UnknownVoiceAttribute, "Voice has no '#{attribute}' attribute" if attribute && !VOICE_ATTRIBUTES.include?(attribute)
|
197
|
+
|
198
|
+
if block_given?
|
199
|
+
found_voices = @voices.find_all(&block)
|
200
|
+
else
|
201
|
+
found_voices = @voices.find_all {|voice| voice[attribute] === value }
|
202
|
+
end
|
152
203
|
|
153
204
|
return if found_voices.empty?
|
154
|
-
|
155
205
|
found_voices.count == 1 ? found_voices.first : found_voices
|
156
206
|
end
|
157
207
|
|
@@ -162,20 +212,22 @@ module Mac
|
|
162
212
|
# Mac::Say.voices #=>
|
163
213
|
# [
|
164
214
|
# {
|
165
|
-
# :name
|
166
|
-
# :
|
167
|
-
#
|
168
|
-
#
|
169
|
-
#
|
170
|
-
# :
|
215
|
+
# :name => :agnes,
|
216
|
+
# :language => :en,
|
217
|
+
# :country => :us,
|
218
|
+
# :sample => "Isn't it nice to have a computer that will talk to you?",
|
219
|
+
# :gender => :female,
|
220
|
+
# :joke => false,
|
221
|
+
# :quality => :low
|
171
222
|
# },
|
172
223
|
# {
|
173
|
-
# :name
|
174
|
-
# :
|
175
|
-
#
|
176
|
-
#
|
177
|
-
#
|
178
|
-
# :
|
224
|
+
# :name => :albert,
|
225
|
+
# :language => :en,
|
226
|
+
# :country => :us,
|
227
|
+
# :sample => "I have a frog in my throat. No, I mean a real frog!",
|
228
|
+
# :gender => :male,
|
229
|
+
# :joke => true,
|
230
|
+
# :quality => :medium
|
179
231
|
# },
|
180
232
|
# ...
|
181
233
|
# ]
|
@@ -214,7 +266,7 @@ module Mac
|
|
214
266
|
# @raise [FileNotFound] if the given file wasn't found or isn't readable by the current user
|
215
267
|
def generate_command
|
216
268
|
say_path = @config[:say_path]
|
217
|
-
file
|
269
|
+
file = @config[:file]
|
218
270
|
|
219
271
|
raise CommandNotFound, "Command `say` couldn't be found by '#{@config[:say_path]}' path" unless valid_command_path? say_path
|
220
272
|
|
@@ -235,22 +287,30 @@ module Mac
|
|
235
287
|
# @raise [CommandNotFound] if the say command wasn't found
|
236
288
|
def load_voices
|
237
289
|
return if @voices
|
290
|
+
|
238
291
|
say_path = @config[:say_path]
|
239
292
|
raise CommandNotFound, "Command `say` couldn't be found by '#{say_path}' path" unless valid_command_path? say_path
|
240
293
|
|
241
|
-
@voices = `#{say_path} -v '?'`.scan(
|
294
|
+
@voices = `#{say_path} -v '?'`.scan(VOICE_PATTERN).map do |voice|
|
242
295
|
lang = voice[1].split(/[_-]/)
|
296
|
+
name = voice[0].downcase.to_sym
|
297
|
+
|
298
|
+
additional_attributes = ADDITIONAL_VOICE_ATTRIBUTES[name] || ADDITIONAL_VOICE_ATTRIBUTES[:_unknown_voice]
|
299
|
+
|
243
300
|
{
|
244
|
-
name:
|
245
|
-
|
246
|
-
|
247
|
-
|
301
|
+
name: name,
|
302
|
+
language: lang[0].downcase.to_sym,
|
303
|
+
country: lang[1].downcase.to_sym,
|
304
|
+
sample: voice[2].strip
|
305
|
+
}.merge(additional_attributes)
|
248
306
|
end
|
249
307
|
end
|
250
308
|
|
251
309
|
# Checks voice existence by the name
|
252
310
|
# Loads voices if they weren't loaded before
|
253
311
|
#
|
312
|
+
# @param name [String, Symbol] the name of the voice to validate
|
313
|
+
#
|
254
314
|
# @return [Boolean] if the voices name in the list of voices
|
255
315
|
#
|
256
316
|
# @raise [CommandNotFound] if the say command wasn't found
|
@@ -261,6 +321,8 @@ module Mac
|
|
261
321
|
|
262
322
|
# Checks say command existence by the path
|
263
323
|
#
|
324
|
+
# @param path [String] the path of the `say` command to validate
|
325
|
+
#
|
264
326
|
# @return [Boolean] if the command exists and if it is executable
|
265
327
|
def valid_command_path?(path)
|
266
328
|
File.exist?(path) && File.executable?(path)
|
@@ -268,6 +330,8 @@ module Mac
|
|
268
330
|
|
269
331
|
# Checks text file existence by the path
|
270
332
|
#
|
333
|
+
# @param path [String] the path of the text file validate
|
334
|
+
#
|
271
335
|
# @return [Boolean] if the file exists and if it is readable
|
272
336
|
def valid_file_path?(path)
|
273
337
|
path && File.exist?(path) && File.readable?(path)
|
data/lib/mac/say/version.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
# Wrapper namespace module for a Say class
|
4
4
|
module Mac
|
5
|
-
# A class wrapper around the MacOS `say`
|
5
|
+
# A class wrapper around the MacOS `say` command
|
6
6
|
# Allows to use simple TTS on Mac right from Ruby scripts
|
7
7
|
class Say
|
8
8
|
# mac-say version
|
9
|
-
VERSION = '0.
|
9
|
+
VERSION = '0.2.0'
|
10
10
|
end
|
11
11
|
end
|
@@ -0,0 +1,510 @@
|
|
1
|
+
# Additional voice attributes mixed to the original Hash of
|
2
|
+
# parsed voices (to provide additional context when looking for a voice)
|
3
|
+
#
|
4
|
+
# @note Unfortunately information in system *.plist files
|
5
|
+
# e.g. /System/Library/Speech/Voices/<VoiceName>.SpeechVoice/Contents/Info.plist"
|
6
|
+
# is inconsistent, fragmentary and sometimes incorrect, but:
|
7
|
+
#
|
8
|
+
# WARNING: all the attributes values provided here are highly subjective!!!
|
9
|
+
ADDITIONAL_VOICE_ATTRIBUTES = {
|
10
|
+
_unknown_voice: {
|
11
|
+
gender: nil,
|
12
|
+
joke: nil,
|
13
|
+
quality: nil
|
14
|
+
},
|
15
|
+
agnes: {
|
16
|
+
gender: :female,
|
17
|
+
joke: false,
|
18
|
+
quality: :low
|
19
|
+
},
|
20
|
+
albert: {
|
21
|
+
gender: :male,
|
22
|
+
joke: true,
|
23
|
+
quality: :medium
|
24
|
+
},
|
25
|
+
alex: {
|
26
|
+
gender: :male,
|
27
|
+
joke: false,
|
28
|
+
quality: :medium
|
29
|
+
},
|
30
|
+
alice: {
|
31
|
+
gender: :female,
|
32
|
+
joke: false,
|
33
|
+
quality: :high
|
34
|
+
},
|
35
|
+
allison: {
|
36
|
+
gender: :female,
|
37
|
+
joke: false,
|
38
|
+
quality: :high
|
39
|
+
},
|
40
|
+
alva: {
|
41
|
+
gender: :female,
|
42
|
+
joke: false,
|
43
|
+
quality: :low
|
44
|
+
},
|
45
|
+
amelie: {
|
46
|
+
gender: :female,
|
47
|
+
joke: false,
|
48
|
+
quality: :medium
|
49
|
+
},
|
50
|
+
angelica: {
|
51
|
+
gender: :female,
|
52
|
+
joke: false,
|
53
|
+
quality: :low
|
54
|
+
},
|
55
|
+
anna: {
|
56
|
+
gender: :female,
|
57
|
+
joke: false,
|
58
|
+
quality: :medium
|
59
|
+
},
|
60
|
+
audrey: {
|
61
|
+
gender: :female,
|
62
|
+
joke: false,
|
63
|
+
quality: :medium
|
64
|
+
},
|
65
|
+
aurelie: {
|
66
|
+
gender: :female,
|
67
|
+
joke: false,
|
68
|
+
quality: :medium
|
69
|
+
},
|
70
|
+
ava: {
|
71
|
+
gender: :female,
|
72
|
+
joke: false,
|
73
|
+
quality: :high
|
74
|
+
},
|
75
|
+
bahh: {
|
76
|
+
gender: :male,
|
77
|
+
joke: true,
|
78
|
+
quality: :low
|
79
|
+
},
|
80
|
+
bells: {
|
81
|
+
gender: :neutral,
|
82
|
+
joke: true,
|
83
|
+
quality: :medium
|
84
|
+
},
|
85
|
+
boing: {
|
86
|
+
gender: :neutral,
|
87
|
+
joke: true,
|
88
|
+
quality: :medium
|
89
|
+
},
|
90
|
+
bruce: {
|
91
|
+
gender: :male,
|
92
|
+
joke: false,
|
93
|
+
quality: :low
|
94
|
+
},
|
95
|
+
bubbles: {
|
96
|
+
gender: :male,
|
97
|
+
joke: true,
|
98
|
+
quality: :medium
|
99
|
+
},
|
100
|
+
carlos: {
|
101
|
+
gender: :male,
|
102
|
+
joke: false,
|
103
|
+
quality: :high
|
104
|
+
},
|
105
|
+
carmit: {
|
106
|
+
gender: :female,
|
107
|
+
joke: false,
|
108
|
+
quality: :medium
|
109
|
+
},
|
110
|
+
catarina: {
|
111
|
+
gender: :female,
|
112
|
+
joke: false,
|
113
|
+
quality: :low
|
114
|
+
},
|
115
|
+
cellos: {
|
116
|
+
gender: :male,
|
117
|
+
joke: true,
|
118
|
+
quality: :medium
|
119
|
+
},
|
120
|
+
cem: {
|
121
|
+
gender: :male,
|
122
|
+
joke: false,
|
123
|
+
quality: :medium
|
124
|
+
},
|
125
|
+
chantal: {
|
126
|
+
gender: :female,
|
127
|
+
joke: false,
|
128
|
+
quality: :medium
|
129
|
+
},
|
130
|
+
claire: {
|
131
|
+
gender: :female,
|
132
|
+
joke: false,
|
133
|
+
quality: :medium
|
134
|
+
},
|
135
|
+
damayanti: {
|
136
|
+
gender: :female,
|
137
|
+
joke: false,
|
138
|
+
quality: :medium
|
139
|
+
},
|
140
|
+
daniel: {
|
141
|
+
gender: :male,
|
142
|
+
joke: false,
|
143
|
+
quality: :high
|
144
|
+
},
|
145
|
+
deranged: {
|
146
|
+
gender: :neutral,
|
147
|
+
joke: true,
|
148
|
+
quality: :medium
|
149
|
+
},
|
150
|
+
diego: {
|
151
|
+
gender: :male,
|
152
|
+
joke: false,
|
153
|
+
quality: :low
|
154
|
+
},
|
155
|
+
ellen: {
|
156
|
+
gender: :female,
|
157
|
+
joke: false,
|
158
|
+
quality: :low
|
159
|
+
},
|
160
|
+
ewa: {
|
161
|
+
gender: :female,
|
162
|
+
joke: false,
|
163
|
+
quality: :medium
|
164
|
+
},
|
165
|
+
federica: {
|
166
|
+
gender: :female,
|
167
|
+
joke: false,
|
168
|
+
quality: :medium
|
169
|
+
},
|
170
|
+
felipe: {
|
171
|
+
gender: :male,
|
172
|
+
joke: false,
|
173
|
+
quality: :medium
|
174
|
+
},
|
175
|
+
fiona: {
|
176
|
+
gender: :female,
|
177
|
+
joke: false,
|
178
|
+
quality: :high
|
179
|
+
},
|
180
|
+
fred: {
|
181
|
+
gender: :male,
|
182
|
+
joke: true,
|
183
|
+
quality: :low
|
184
|
+
},
|
185
|
+
henrik: {
|
186
|
+
gender: :male,
|
187
|
+
joke: false,
|
188
|
+
quality: :high
|
189
|
+
},
|
190
|
+
hysterical: {
|
191
|
+
gender: :male,
|
192
|
+
joke: true,
|
193
|
+
quality: :medium
|
194
|
+
},
|
195
|
+
ioana: {
|
196
|
+
gender: :female,
|
197
|
+
joke: false,
|
198
|
+
quality: :low
|
199
|
+
},
|
200
|
+
iveta: {
|
201
|
+
gender: :female,
|
202
|
+
joke: false,
|
203
|
+
quality: :low
|
204
|
+
},
|
205
|
+
joana: {
|
206
|
+
gender: :female,
|
207
|
+
joke: false,
|
208
|
+
quality: :low
|
209
|
+
},
|
210
|
+
jorge: {
|
211
|
+
gender: :male,
|
212
|
+
joke: false,
|
213
|
+
quality: :low
|
214
|
+
},
|
215
|
+
juan: {
|
216
|
+
gender: :male,
|
217
|
+
joke: false,
|
218
|
+
quality: :medium
|
219
|
+
},
|
220
|
+
junior: {
|
221
|
+
gender: :male,
|
222
|
+
joke: true,
|
223
|
+
quality: :low
|
224
|
+
},
|
225
|
+
kanya: {
|
226
|
+
gender: :female,
|
227
|
+
joke: false,
|
228
|
+
quality: :medium
|
229
|
+
},
|
230
|
+
karen: {
|
231
|
+
gender: :female,
|
232
|
+
joke: false,
|
233
|
+
quality: :high
|
234
|
+
},
|
235
|
+
kate: {
|
236
|
+
gender: :female,
|
237
|
+
joke: false,
|
238
|
+
quality: :high
|
239
|
+
},
|
240
|
+
kathy: {
|
241
|
+
gender: :female,
|
242
|
+
joke: true,
|
243
|
+
quality: :low
|
244
|
+
},
|
245
|
+
katya: {
|
246
|
+
gender: :female,
|
247
|
+
joke: false,
|
248
|
+
quality: :medium
|
249
|
+
},
|
250
|
+
klara: {
|
251
|
+
gender: :female,
|
252
|
+
joke: false,
|
253
|
+
quality: :low
|
254
|
+
},
|
255
|
+
kyoko: {
|
256
|
+
gender: :female,
|
257
|
+
joke: false,
|
258
|
+
quality: :low
|
259
|
+
},
|
260
|
+
laura: {
|
261
|
+
gender: :female,
|
262
|
+
joke: false,
|
263
|
+
quality: :low
|
264
|
+
},
|
265
|
+
lee: {
|
266
|
+
gender: :male,
|
267
|
+
joke: false,
|
268
|
+
quality: :high
|
269
|
+
},
|
270
|
+
lekha: {
|
271
|
+
gender: :female,
|
272
|
+
joke: false,
|
273
|
+
quality: :low
|
274
|
+
},
|
275
|
+
luca: {
|
276
|
+
gender: :male,
|
277
|
+
joke: false,
|
278
|
+
quality: :high
|
279
|
+
},
|
280
|
+
luciana: {
|
281
|
+
gender: :female,
|
282
|
+
joke: false,
|
283
|
+
quality: :low
|
284
|
+
},
|
285
|
+
maged: {
|
286
|
+
gender: :male,
|
287
|
+
joke: false,
|
288
|
+
quality: :low
|
289
|
+
},
|
290
|
+
magnus: {
|
291
|
+
gender: :male,
|
292
|
+
joke: false,
|
293
|
+
quality: :medium
|
294
|
+
},
|
295
|
+
mariska: {
|
296
|
+
gender: :female,
|
297
|
+
joke: false,
|
298
|
+
quality: :medium
|
299
|
+
},
|
300
|
+
markus: {
|
301
|
+
gender: :male,
|
302
|
+
joke: false,
|
303
|
+
quality: :low
|
304
|
+
},
|
305
|
+
:'mei-jia' => {
|
306
|
+
gender: :female,
|
307
|
+
joke: false,
|
308
|
+
quality: :medium
|
309
|
+
},
|
310
|
+
melina: {
|
311
|
+
gender: :female,
|
312
|
+
joke: false,
|
313
|
+
quality: :low
|
314
|
+
},
|
315
|
+
milena: {
|
316
|
+
gender: :female,
|
317
|
+
joke: false,
|
318
|
+
quality: :medium
|
319
|
+
},
|
320
|
+
moira: {
|
321
|
+
gender: :female,
|
322
|
+
joke: false,
|
323
|
+
quality: :medium
|
324
|
+
},
|
325
|
+
monica: {
|
326
|
+
gender: :female,
|
327
|
+
joke: false,
|
328
|
+
quality: :medium
|
329
|
+
},
|
330
|
+
nicolas: {
|
331
|
+
gender: :male,
|
332
|
+
joke: false,
|
333
|
+
quality: :high
|
334
|
+
},
|
335
|
+
nikos: {
|
336
|
+
gender: :male,
|
337
|
+
joke: false,
|
338
|
+
quality: :medium
|
339
|
+
},
|
340
|
+
nora: {
|
341
|
+
gender: :female,
|
342
|
+
joke: false,
|
343
|
+
quality: :low
|
344
|
+
},
|
345
|
+
oliver: {
|
346
|
+
gender: :male,
|
347
|
+
joke: false,
|
348
|
+
quality: :low
|
349
|
+
},
|
350
|
+
oskar: {
|
351
|
+
gender: :male,
|
352
|
+
joke: false,
|
353
|
+
quality: :medium
|
354
|
+
},
|
355
|
+
otoya: {
|
356
|
+
gender: :male,
|
357
|
+
joke: false,
|
358
|
+
quality: :medium
|
359
|
+
},
|
360
|
+
paola: {
|
361
|
+
gender: :female,
|
362
|
+
joke: false,
|
363
|
+
quality: :high
|
364
|
+
},
|
365
|
+
paulina: {
|
366
|
+
gender: :female,
|
367
|
+
joke: false,
|
368
|
+
quality: :low
|
369
|
+
},
|
370
|
+
petra: {
|
371
|
+
gender: :female,
|
372
|
+
joke: false,
|
373
|
+
quality: :medium
|
374
|
+
},
|
375
|
+
princess: {
|
376
|
+
gender: :female,
|
377
|
+
joke: true,
|
378
|
+
quality: :low
|
379
|
+
},
|
380
|
+
ralph: {
|
381
|
+
gender: :male,
|
382
|
+
joke: true,
|
383
|
+
quality: :low
|
384
|
+
},
|
385
|
+
samantha: {
|
386
|
+
gender: :female,
|
387
|
+
joke: false,
|
388
|
+
quality: :high
|
389
|
+
},
|
390
|
+
sara: {
|
391
|
+
gender: :female,
|
392
|
+
joke: false,
|
393
|
+
quality: :medium
|
394
|
+
},
|
395
|
+
satu: {
|
396
|
+
gender: :female,
|
397
|
+
joke: false,
|
398
|
+
quality: :medium
|
399
|
+
},
|
400
|
+
serena: {
|
401
|
+
gender: :female,
|
402
|
+
joke: false,
|
403
|
+
quality: :medium
|
404
|
+
},
|
405
|
+
:'sin-ji' => {
|
406
|
+
gender: :female,
|
407
|
+
joke: false,
|
408
|
+
quality: :medium
|
409
|
+
},
|
410
|
+
soledad: {
|
411
|
+
gender: :female,
|
412
|
+
joke: false,
|
413
|
+
quality: :medium
|
414
|
+
},
|
415
|
+
susan: {
|
416
|
+
gender: :female,
|
417
|
+
joke: false,
|
418
|
+
quality: :medium
|
419
|
+
},
|
420
|
+
tarik: {
|
421
|
+
gender: :male,
|
422
|
+
joke: false,
|
423
|
+
quality: :low
|
424
|
+
},
|
425
|
+
tessa: {
|
426
|
+
gender: :female,
|
427
|
+
joke: false,
|
428
|
+
quality: :high
|
429
|
+
},
|
430
|
+
thomas: {
|
431
|
+
gender: :male,
|
432
|
+
joke: false,
|
433
|
+
quality: :high
|
434
|
+
},
|
435
|
+
:'ting-ting' => {
|
436
|
+
gender: :female,
|
437
|
+
joke: false,
|
438
|
+
quality: :high
|
439
|
+
},
|
440
|
+
tom: {
|
441
|
+
gender: :male,
|
442
|
+
joke: false,
|
443
|
+
quality: :high
|
444
|
+
},
|
445
|
+
trinoids: {
|
446
|
+
gender: :neutral,
|
447
|
+
joke: true,
|
448
|
+
quality: :medium
|
449
|
+
},
|
450
|
+
veena: {
|
451
|
+
gender: :female,
|
452
|
+
joke: false,
|
453
|
+
quality: :high
|
454
|
+
},
|
455
|
+
vicki: {
|
456
|
+
gender: :female,
|
457
|
+
joke: false,
|
458
|
+
quality: :low
|
459
|
+
},
|
460
|
+
victoria: {
|
461
|
+
gender: :female,
|
462
|
+
joke: false,
|
463
|
+
quality: :low
|
464
|
+
},
|
465
|
+
whisper: {
|
466
|
+
gender: :male,
|
467
|
+
joke: true,
|
468
|
+
quality: :medium
|
469
|
+
},
|
470
|
+
xander: {
|
471
|
+
gender: :male,
|
472
|
+
joke: false,
|
473
|
+
quality: :high
|
474
|
+
},
|
475
|
+
yannick: {
|
476
|
+
gender: :male,
|
477
|
+
joke: false,
|
478
|
+
quality: :medium
|
479
|
+
},
|
480
|
+
yelda: {
|
481
|
+
gender: :female,
|
482
|
+
joke: false,
|
483
|
+
quality: :low
|
484
|
+
},
|
485
|
+
yuna: {
|
486
|
+
gender: :female,
|
487
|
+
joke: false,
|
488
|
+
quality: :medium
|
489
|
+
},
|
490
|
+
yuri: {
|
491
|
+
gender: :male,
|
492
|
+
joke: false,
|
493
|
+
quality: :low
|
494
|
+
},
|
495
|
+
zarvox: {
|
496
|
+
gender: :neutral,
|
497
|
+
joke: true,
|
498
|
+
quality: :medium
|
499
|
+
},
|
500
|
+
zosia: {
|
501
|
+
gender: :female,
|
502
|
+
joke: false,
|
503
|
+
quality: :low
|
504
|
+
},
|
505
|
+
zuzana: {
|
506
|
+
gender: :female,
|
507
|
+
joke: false,
|
508
|
+
quality: :high
|
509
|
+
}
|
510
|
+
}.freeze
|
data/test/fake/say
CHANGED
@@ -9,6 +9,7 @@ Fiona en-scotland # Hello, my name is Fiona. I am a Scottish-Engli
|
|
9
9
|
Serena en_GB # Hello, my name is Serena. I am a British-English voice.
|
10
10
|
Ting-Ting zh_CN # 您好,我叫Ting-Ting。我讲中文普通话。
|
11
11
|
Veena en_IN # Hello, my name is Veena. I am an Indian-English voice.
|
12
|
+
Test en_GB # Well I'm the most mysterious voice round here, believe me.
|
12
13
|
VOICES
|
13
14
|
|
14
15
|
VOICES_NAMES = [
|
@@ -16,7 +17,8 @@ VOICES_NAMES = [
|
|
16
17
|
:daniel,
|
17
18
|
:fiona,
|
18
19
|
:'ting-ting',
|
19
|
-
:veena
|
20
|
+
:veena,
|
21
|
+
:test
|
20
22
|
]
|
21
23
|
|
22
24
|
options = {
|
data/test/helper.rb
CHANGED
@@ -4,15 +4,19 @@ require 'rubygems'
|
|
4
4
|
require 'coveralls'
|
5
5
|
Coveralls.wear!
|
6
6
|
|
7
|
+
require 'simplecov'
|
8
|
+
SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new [
|
9
|
+
SimpleCov::Formatter::HTMLFormatter,
|
10
|
+
Coveralls::SimpleCov::Formatter
|
11
|
+
]
|
12
|
+
SimpleCov.start
|
13
|
+
|
7
14
|
begin
|
8
15
|
require 'bundler/setup'
|
9
16
|
rescue LoadError => error
|
10
17
|
abort error.message
|
11
18
|
end
|
12
19
|
|
13
|
-
require 'simplecov'
|
14
|
-
SimpleCov.start
|
15
|
-
|
16
20
|
require 'minitest/autorun'
|
17
21
|
require 'minitest/reporters'
|
18
22
|
|
data/test/test_mac-say.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'helper'
|
3
3
|
require 'mac/say'
|
4
|
+
require 'mac/say/voices_attributes'
|
4
5
|
|
5
6
|
describe 'Mac::Say as a macOS `say` wrapper' do
|
6
7
|
describe 'On a class level' do
|
@@ -21,34 +22,27 @@ describe 'Mac::Say as a macOS `say` wrapper' do
|
|
21
22
|
voice = Mac::Say.voice(:name, :alex)
|
22
23
|
voice.wont_be_empty
|
23
24
|
voice.must_be_kind_of Hash
|
24
|
-
voice.keys.must_equal
|
25
|
+
voice.keys.must_equal Mac::Say::VOICE_ATTRIBUTES
|
25
26
|
end
|
26
27
|
|
27
|
-
it 'must return
|
28
|
-
|
29
|
-
voice.
|
30
|
-
|
31
|
-
voice[:iso_code].keys.must_equal [:language, :country]
|
32
|
-
end
|
28
|
+
it 'must return additional attributes for known voices' do
|
29
|
+
voice_name = :alex
|
30
|
+
voice = Mac::Say.voice(:name, voice_name)
|
31
|
+
additional_voice_attributes = ADDITIONAL_VOICE_ATTRIBUTES[voice_name]
|
33
32
|
|
34
|
-
|
35
|
-
voice
|
36
|
-
voice[:
|
33
|
+
voice[:gender].must_equal additional_voice_attributes[:gender]
|
34
|
+
voice[:joke].must_equal additional_voice_attributes[:joke]
|
35
|
+
voice[:quality].must_equal additional_voice_attributes[:quality]
|
37
36
|
end
|
38
37
|
|
39
|
-
it '.voice must
|
40
|
-
voice = Mac::Say.voice(:name,
|
38
|
+
it '.voice must search for a voice using single attribute' do
|
39
|
+
voice = Mac::Say.voice(:name, :alex)
|
41
40
|
voice[:name].must_equal :alex
|
42
41
|
end
|
43
42
|
|
44
|
-
it '.voice must search for a voice
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
it '.voice must search for a voice by language' do
|
50
|
-
voices = Mac::Say.voice(:language, :en)
|
51
|
-
voices.count.must_be :>, 2
|
43
|
+
it '.voice must search for a voice using block given' do
|
44
|
+
voices = Mac::Say.voice {|voice| voice[:language] == :en && voice[:joke] == false }
|
45
|
+
voices.must_be_kind_of Array
|
52
46
|
end
|
53
47
|
|
54
48
|
it '.voice must return one voice as a Hash' do
|
@@ -66,6 +60,7 @@ describe 'Mac::Say as a macOS `say` wrapper' do
|
|
66
60
|
voices.must_be_nil
|
67
61
|
end
|
68
62
|
|
63
|
+
|
69
64
|
it '.say must return 0 in successive speech' do
|
70
65
|
expectation = ["#{@say_path} -v 'alex' -r 175", 0]
|
71
66
|
Mac::Say.say('42').must_equal expectation
|
@@ -91,10 +86,10 @@ describe 'Mac::Say as a macOS `say` wrapper' do
|
|
91
86
|
}.must_raise Mac::Say::VoiceNotFound
|
92
87
|
end
|
93
88
|
|
94
|
-
it '.voice must fail on wrong voice
|
89
|
+
it '.voice must fail on wrong voice attribute' do
|
95
90
|
-> {
|
96
91
|
Mac::Say.voice(:tone, :enthusiastic)
|
97
|
-
}.must_raise Mac::Say::
|
92
|
+
}.must_raise Mac::Say::UnknownVoiceAttribute
|
98
93
|
end
|
99
94
|
end
|
100
95
|
|
@@ -133,6 +128,17 @@ describe 'Mac::Say as a macOS `say` wrapper' do
|
|
133
128
|
@reader.say.must_equal expectation
|
134
129
|
end
|
135
130
|
|
131
|
+
it 'must return nil additional attrs for unknown voices' do
|
132
|
+
if ENV['USE_FAKE_SAY']
|
133
|
+
voice = @reader.voice(:name, :test)
|
134
|
+
additional_voice_attributes = ADDITIONAL_VOICE_ATTRIBUTES[:test]
|
135
|
+
|
136
|
+
voice[:gender].must_be_nil
|
137
|
+
voice[:joke].must_be_nil
|
138
|
+
voice[:quality].must_be_nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
136
142
|
it '#say must change :file from initial config' do
|
137
143
|
gb_absolute_path = File.absolute_path './fixtures/text/en_gb_test.txt', File.dirname(__FILE__)
|
138
144
|
us_absolute_path = File.absolute_path './fixtures/text/en_us_test.txt', File.dirname(__FILE__)
|
@@ -217,10 +223,10 @@ describe 'Mac::Say as a macOS `say` wrapper' do
|
|
217
223
|
}.must_raise Mac::Say::FileNotFound
|
218
224
|
end
|
219
225
|
|
220
|
-
it '#voice must fail on wrong
|
226
|
+
it '#voice must fail on wrong attribute' do
|
221
227
|
-> {
|
222
228
|
Mac::Say.new.voice(:articulation, :nostalgic)
|
223
|
-
}.must_raise Mac::Say::
|
229
|
+
}.must_raise Mac::Say::UnknownVoiceAttribute
|
224
230
|
end
|
225
231
|
|
226
232
|
it '#say must fail on initial wrong file path' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mac-say
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Serge Bedzhyk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -201,6 +201,7 @@ files:
|
|
201
201
|
- img/voices_manual.png
|
202
202
|
- lib/mac/say.rb
|
203
203
|
- lib/mac/say/version.rb
|
204
|
+
- lib/mac/say/voices_attributes.rb
|
204
205
|
- mac-say.gemspec
|
205
206
|
- test/fake/say
|
206
207
|
- test/fixtures/text/en_gb.txt
|