voicemaker 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84b3811f8d8ef6084933654f41b1764c2e1c3eb1d59ac8a6e599d1c18a38bd7a
4
- data.tar.gz: 91491d408b0884424c20771d4a4b9d230a7cd29dc05eb7d4f55caeb2ca8e5440
3
+ metadata.gz: ef73c119c97b71f1114e095357f7bfc04d20e73081752fa5bae226c390911622
4
+ data.tar.gz: 116b47aaa08211a962702f2f2cf1db1134992468a7cb4d296a454f8e23b0f264
5
5
  SHA512:
6
- metadata.gz: 98939ba36fb37cc71987060376422da98e064fab61074f08d8f853d5ce35a1f648db32d10ea7da5c25b9af67c908b4b901fff3b35244dfb96ee9b5d3b795e717
7
- data.tar.gz: 1d4a1894a2f1eddbd30b93bb5c1226664079daaaba8c5f72510a6a5912efb65ad49e1b270a1ee628ce6b4e197285af59f5bcb65da3e70b7b2f2c3c0a4b7b5b84
6
+ metadata.gz: ba494699349745471825e4e9af8c92282f5c075b533ae1a9392f6058a9885f617f61e7d3faab48c1a3b3de4f7ea51b29760bf4985d510a3c50d82017d24be93b
7
+ data.tar.gz: 912165db465f663a2c062206c712bfd35fdc27b5b558fa5a34ee3084618d7565f35bab41cd7e2e02d4c97d9f92808ea2e6def4a923bd3721fc98e6d38944d310
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/voicemaker.svg)](https://badge.fury.io/rb/voicemaker)
4
4
  [![Build Status](https://github.com/DannyBen/voicemaker/workflows/Test/badge.svg)](https://github.com/DannyBen/voicemaker/actions?query=workflow%3ATest)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/b1977ee0d60affeba3d4/maintainability)](https://codeclimate.com/github/DannyBen/voicemaker/maintainability)
5
6
 
6
7
  ---
7
8
 
@@ -35,8 +36,7 @@ First, require and initialize with your Voicemaker API key:
35
36
 
36
37
  ```ruby
37
38
  require 'voicemaker'
38
- api_key = '...'
39
- api = Voicemaker::API.new api_key
39
+ Voicemaker::API.key = 'your api key'
40
40
  ```
41
41
 
42
42
  ### Voices list
@@ -44,13 +44,14 @@ api = Voicemaker::API.new api_key
44
44
  Get the full voices list:
45
45
 
46
46
  ```ruby
47
- result = api.voices
47
+ voices = Voicemaker::Voices.new
48
+ result = voices.all
48
49
  ```
49
50
 
50
51
  Search the voices list for one or more strings (AND search):
51
52
 
52
53
  ```ruby
53
- result = api.voices ["en_US", "female"]
54
+ result = voices.search "en-us", "female"
54
55
  ```
55
56
 
56
57
  ### Audio generation and download
@@ -58,90 +59,221 @@ result = api.voices ["en_US", "female"]
58
59
  Make an API call and get the URL to the audio file in return:
59
60
 
60
61
  ```ruby
61
- params = {
62
- "Engine" => "neural",
63
- "VoiceId" => "ai3-Jony",
64
- "LanguageCode" => "en-US",
65
- "OutputFormat" => "mp3",
66
- "SampleRate" => "48000",
67
- "Effect" => "default",
68
- "MasterSpeed" => "0",
69
- "MasterVolume" => "0",
70
- "MasterPitch" => "0",
71
- "Text" => "Hello world",
72
- }
73
-
74
- result = api.generate params
62
+ tts = Voicemaker::TTS.new voice: 'Jony', text: 'Hello world'
63
+ url = tts.url
64
+ tts.save "output.mp3"
75
65
  ```
76
66
 
77
- For convenience, you can call `#download` instead, to mak ethe API call and
78
- download the file:
67
+ or with the full list of available parameters:
79
68
 
80
69
  ```ruby
81
- result = api.download "out.mp3', params
70
+ params = {
71
+ voice: "Jony",
72
+ output_format: "mp3",
73
+ sample_rate: 48000,
74
+ effect: "default",
75
+ master_speed: 0,
76
+ master_volume: 0,
77
+ master_pitch: 0,
78
+ text: "Hello world"
79
+ }
80
+
81
+ tts = Voicemaker::TTS.new params
82
+ url = tts.url
82
83
  ```
83
84
 
85
+ As the `voice` parameter, you may use either the voice ID (`ai3-Jony`) or the
86
+ voice web name (`Jony`). Just note that some voices have the same webname (for
87
+ example, Emily), so in these cases it is best to use the full voice ID.
88
+
84
89
  ## Command line interface
85
90
 
86
91
  <!-- USAGE -->
92
+ ### `$ voicemaker `
93
+
94
+
87
95
  ```
88
- $ voicemaker --help
96
+ Voicemaker Text-to-Speech
97
+
98
+ Commands:
99
+ voices Get a list of available voices
100
+ tts Generate audio files from text
101
+ new Create a new config file or a project directory
102
+ project Create multiple audio files
103
+
104
+ API Documentation: https://developer.voicemaker.in/apidocs
89
105
 
90
- Voicemaker API
106
+ ```
91
107
 
92
- API Documentation:
93
- https://developer.voicemaker.in/apidocs
108
+ ### `$ voicemaker voices --help`
109
+
110
+
111
+ ```
112
+ Get a list of available voices
94
113
 
95
114
  Usage:
96
- voicemaker voices [--save PATH] [SEARCH...]
97
- voicemaker new CONFIG
98
- voicemaker generate CONFIG [OUTPUT]
99
- voicemaker batch INDIR OUTDIR
100
- voicemaker (-h|--help|--version)
115
+ voicemaker voices [--save PATH --count --compact] [SEARCH...]
116
+ voicemaker voices (-h|--help)
101
117
 
102
- Commands:
103
- voices
104
- Get list of voices, optionally in a given language
118
+ Options:
119
+ -s --save PATH
120
+ Save output to output YAML file
121
+
122
+ -t --compact
123
+ Show each voice in a single line
105
124
 
106
- new
107
- Generate a sample config file
125
+ -c --count
126
+ Add number of voices to the result
108
127
 
109
- generate
110
- Generate audio file. The output filename will be the same as the config
111
- filename, with the proper mp3 or wav extension
128
+ -h --help
129
+ Show this help
112
130
 
113
- batch
114
- Generate multiple audio files from multiple config files
131
+ Parameters:
132
+ SEARCH
133
+ Provide one or more text strings to search for (case insensitive AND search)
134
+
135
+ Environment Variables:
136
+ VOICEMAKER_API_KEY
137
+ Your Voicemaker API key [required]
138
+
139
+ VOICEMAKER_API_HOST
140
+ Override the API host URL
141
+
142
+ VOICEMAKER_CACHE_DIR
143
+ API cache diredctory [default: cache]
144
+
145
+ VOICEMAKER_CACHE_LIFE
146
+ API cache life. These formats are supported:
147
+ off - No cache
148
+ 20s - 20 seconds
149
+ 10m - 10 minutes
150
+ 10h - 10 hours
151
+ 10d - 10 days
152
+
153
+ Examples:
154
+ voicemaker voices en-us
155
+ voicemaker voices --save out.yml en-us
156
+ voicemaker voices en-us female
157
+ voicemaker voices en-us --compact
158
+
159
+ ```
160
+
161
+ ### `$ voicemaker tts --help`
162
+
163
+
164
+ ```
165
+ Generate audio files from text
166
+
167
+ Usage:
168
+ voicemaker tts [options]
169
+ voicemaker tts (-h|--help)
115
170
 
116
171
  Options:
117
- -l --language LANG
118
- Limit results to a specific language
172
+ -v --voice NAME
173
+ Voice ID or Webname
174
+
175
+ -t --text TEXT
176
+ Text to say
177
+
178
+ -f --file PATH
179
+ Load text from file
180
+
181
+ -c --config PATH
182
+ Use a YAML configuration file
119
183
 
120
184
  -s --save PATH
121
- Save output to output YAML file
185
+ Save audio file.
186
+ If not provided, a URL to the audio file will be printed instead.
187
+ When used with the --config option, omit the file extension, as it will be
188
+ determined based on the config file.
189
+
190
+ -d --debug
191
+ Show API parameters
122
192
 
123
193
  -h --help
124
194
  Show this help
125
195
 
126
- --version
127
- Show version number
196
+ Environment Variables:
197
+ VOICEMAKER_API_KEY
198
+ Your Voicemaker API key [required]
199
+
200
+ VOICEMAKER_API_HOST
201
+ Override the API host URL
202
+
203
+ VOICEMAKER_CACHE_DIR
204
+ API cache diredctory [default: cache]
205
+
206
+ VOICEMAKER_CACHE_LIFE
207
+ API cache life. These formats are supported:
208
+ off - No cache
209
+ 20s - 20 seconds
210
+ 10m - 10 minutes
211
+ 10h - 10 hours
212
+ 10d - 10 days
213
+
214
+ Examples:
215
+ voicemaker tts --voice ai3-Jony --text "Hello world" --save out.mp3
216
+ voicemaker tts -v ai3-Jony --file hello.txt --save out.mp3
217
+ voicemaker tts --config english-female.yml -f text.txt -s outfile
218
+
219
+ ```
220
+
221
+ ### `$ voicemaker new --help`
222
+
223
+
224
+ ```
225
+ Create a new config file or a project directory
226
+
227
+ Usage:
228
+ voicemaker new config PATH
229
+ voicemaker new project DIR
230
+ voicemaker new (-h|--help)
231
+
232
+ Commands:
233
+ config
234
+ Create a config file to be used with the 'voicemaker tts' command
235
+
236
+ project
237
+ Generate a project directory to be used with the 'voicemaker project'
238
+ command
239
+
240
+ Options:
241
+ -h --help
242
+ Show this help
128
243
 
129
244
  Parameters:
130
- SEARCH
131
- Provide one or more text strings to search for (case insensitive)
245
+ PATH
246
+ Path to use for creating the config file
247
+
248
+ DIR
249
+ Directory name for creating the project structure
250
+
251
+ Examples:
252
+ voicemaker new config test.yml
253
+ voicemaker new project sample-project
254
+
255
+ ```
256
+
257
+ ### `$ voicemaker project --help`
258
+
259
+
260
+ ```
261
+ Create multiple audio files
132
262
 
133
- CONFIG
134
- Path to config file
263
+ Usage:
264
+ voicemaker project PATH [--debug]
265
+ voicemaker project (-h|--help)
135
266
 
136
- OUTPUT
137
- Path to output mp3/wav file. If not provided, the filename will be the same
138
- as the config file, with wav/mp3 extension
267
+ Options:
268
+ --debug
269
+ Show API parameters
139
270
 
140
- INDIR
141
- Path to input directory, containing config YAML files
271
+ -h --help
272
+ Show this help
142
273
 
143
- OURDIR
144
- Path to output directory, where mp3/wav files will be saved
274
+ Parameters:
275
+ PATH
276
+ Path to the project directory
145
277
 
146
278
  Environment Variables:
147
279
  VOICEMAKER_API_KEY
@@ -150,15 +282,22 @@ Environment Variables:
150
282
  VOICEMAKER_API_HOST
151
283
  Override the API host URL
152
284
 
285
+ VOICEMAKER_CACHE_DIR
286
+ API cache diredctory [default: cache]
287
+
288
+ VOICEMAKER_CACHE_LIFE
289
+ API cache life. These formats are supported:
290
+ off - No cache
291
+ 20s - 20 seconds
292
+ 10m - 10 minutes
293
+ 10h - 10 hours
294
+ 10d - 10 days
295
+
153
296
  Examples:
154
- voicemaker voices en-us
155
- voicemaker voices --save out.yml en-us
156
- voicemaker voices en-us female
157
- voicemaker new test.yml
158
- voicemaker generate test.yml out.mp3
159
- voicemaker batch configs out
297
+ voicemaker project sample-project
160
298
 
161
299
  ```
300
+
162
301
  <!-- USAGE -->
163
302
 
164
303
 
data/lib/sample.yml CHANGED
@@ -1,11 +1,9 @@
1
- Engine: neural
2
- VoiceId: ai3-Jony
3
- LanguageCode: en-US
4
- OutputFormat: mp3
5
- SampleRate: 48000
6
- Effect: default
7
- MasterSpeed: "0"
8
- MasterVolume: "0"
9
- MasterPitch: "0"
10
- Text: |
11
- Hello world
1
+ voice: Jony
2
+
3
+ # All the below are optional
4
+ output_format: mp3
5
+ sample_rate: 48000
6
+ effect: default
7
+ master_speed: 0
8
+ master_volume: 0
9
+ master_pitch: 0
@@ -1,66 +1,81 @@
1
+ require 'lightly'
1
2
  require 'http'
2
- require 'down'
3
3
 
4
4
  module Voicemaker
5
5
  class API
6
- attr_reader :api_key
6
+ ROOT = 'https://developer.voicemaker.in/voice'
7
7
 
8
8
  class << self
9
- attr_writer :base_uri
9
+ attr_writer :root, :key, :cache_life, :cache_dir
10
10
 
11
- def base_uri
12
- @base_uri ||= ENV['VOICEMAKER_API_HOST'] || 'https://developer.voicemaker.in/voice'
11
+ def root
12
+ @root ||= ENV['VOICEMAKER_API_ROOT'] || ROOT
13
13
  end
14
- end
15
14
 
16
- def initialize(api_key)
17
- @api_key = api_key
18
- end
15
+ def key
16
+ @key ||= ENV['VOICEMAKER_API_KEY'] || raise(MissingAuth, "Please set the 'VOICEMAKER_API_KEY' environment variable")
17
+ end
18
+
19
+ def cache_life
20
+ @cache_life ||= ENV['VOICEMAKER_CACHE_LIFE'] || '4h'
21
+ end
19
22
 
20
- # Returns a list of voices, with optional search criteria (array)
21
- def voices(search = nil)
22
- search = nil if search&.empty?
23
- response = HTTP.auth(auth_header).get "#{base_uri}/list"
24
- raise BadResponse, "#{response.status}\n#{response.body}" unless response.status.success?
25
- voices = response.parse.dig 'data', 'voices_list'
26
- raise BadResponse, "Unexpected response: #{response}" unless voices
23
+ def cache_dir
24
+ @cache_dir ||= ENV['VOICEMAKER_CACHE_DIR'] || 'cache'
25
+ end
27
26
 
28
- if search
29
- voices.select do |voice|
30
- search_string = voice.values.join(' ').downcase
31
- search.any? { |query| search_string.include? query.downcase }
27
+ # Performs HTTP GET and cache it
28
+ # Returns a parsed body on success
29
+ # Raises BadResponse on error
30
+ def get(endpoint, params = {})
31
+ cache.get "#{root}/#{endpoint}+#{params}" do
32
+ response = get! endpoint, params
33
+ response.parse
32
34
  end
33
- else
34
- voices
35
35
  end
36
- end
37
36
 
38
- # Returns the URL for the generated file
39
- def generate(params = {})
40
- response = HTTP.auth(auth_header).post "#{base_uri}/api", json: params
41
- if response.status.success?
42
- response.parse['path']
43
- else
44
- raise BadResponse, "#{response.status}\n#{response.body}"
37
+ # Performs HTTP POST and cache it
38
+ # Returns a parsed body on success
39
+ # Raises BadResponse on error
40
+ def post(endpoint, params = {})
41
+ cache.get "#{root}/#{endpoint}+#{params}" do
42
+ response = post! endpoint, params
43
+ response.parse
44
+ end
45
45
  end
46
- end
47
46
 
48
- # Calls the API and saves the file
49
- def download(outpath, params = {})
50
- path = generate params
51
- yield path if block_given?
52
- Down.download path, destination: outpath
53
- end
47
+ def cache
48
+ @cache ||= begin
49
+ lightly = Lightly.new life: cache_life, dir: cache_dir
50
+ lightly.disable if cache_life == 'off'
51
+ lightly
52
+ end
53
+ end
54
54
 
55
- protected
55
+ protected
56
56
 
57
- def base_uri
58
- self.class.base_uri
59
- end
57
+ # Performs HTTP GET
58
+ # Returns a parsed body on success
59
+ # Raises BadResponse on error
60
+ def get!(endpoint, params = {})
61
+ response = HTTP.auth(auth_header).get "#{root}/#{endpoint}", json: params
62
+ raise BadResponse, "#{response.status}\n#{response.body}" unless response.status.success?
63
+ response
64
+ end
60
65
 
61
- def auth_header
62
- "Bearer #{api_key}"
63
- end
66
+ # Performs HTTP POST
67
+ # Returns a parsed body on success
68
+ # Raises BadResponse on error
69
+ def post!(endpoint, params = {})
70
+ response = HTTP.auth(auth_header).post "#{root}/#{endpoint}", json: params
71
+ raise BadResponse, "#{response.status}\n#{response.body}" unless response.status.success?
72
+ response
73
+ end
74
+
75
+ def auth_header
76
+ "Bearer #{key}"
77
+ end
64
78
 
79
+ end
65
80
  end
66
- end
81
+ end
@@ -1,10 +1,25 @@
1
+ require 'lp'
1
2
  require 'mister_bin'
2
- require 'voicemaker/command'
3
+ require 'voicemaker/commands/base'
4
+ require 'voicemaker/commands/voices_command'
5
+ require 'voicemaker/commands/tts_command'
6
+ require 'voicemaker/commands/new_command'
7
+ require 'voicemaker/commands/project_command'
3
8
 
4
9
  module Voicemaker
5
10
  class CLI
6
11
  def self.runner
7
- MisterBin::Runner.new handler: Command
12
+ router = MisterBin::Runner.new version: VERSION,
13
+ header: "Voicemaker Text-to-Speech",
14
+ footer: "API Documentation: https://developer.voicemaker.in/apidocs"
15
+
16
+ router.route "voices", to: Commands::VoicesCommand
17
+ router.route "tts", to: Commands::TTSCommand
18
+ router.route "new", to: Commands::NewCommand
19
+ router.route "project", to: Commands::ProjectCommand
20
+
21
+ router
22
+
8
23
  end
9
24
  end
10
25
  end
@@ -0,0 +1,23 @@
1
+ module Voicemaker
2
+ module Commands
3
+ class Base < MisterBin::Command
4
+
5
+ class << self
6
+ def api_environment
7
+ environment "VOICEMAKER_API_KEY", "Your Voicemaker API key [required]"
8
+ environment "VOICEMAKER_API_HOST", "Override the API host URL"
9
+ environment "VOICEMAKER_CACHE_DIR", "API cache diredctory [default: cache]"
10
+ environment "VOICEMAKER_CACHE_LIFE", <<~EOF
11
+ API cache life. These formats are supported:
12
+ off - No cache
13
+ 20s - 20 seconds
14
+ 10m - 10 minutes
15
+ 10h - 10 hours
16
+ 10d - 10 days
17
+ EOF
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ module Voicemaker
2
+ module Commands
3
+ class NewCommand < Base
4
+ help "Create a new config file or a project directory"
5
+
6
+ usage "voicemaker new config PATH"
7
+ usage "voicemaker new project DIR"
8
+ usage "voicemaker new (-h|--help)"
9
+
10
+ command "config", "Create a config file to be used with the 'voicemaker tts' command"
11
+ command "project", "Generate a project directory to be used with the 'voicemaker project' command"
12
+
13
+ param "PATH", "Path to use for creating the config file"
14
+ param "DIR", "Directory name for creating the project structure"
15
+
16
+ example "voicemaker new config test.yml"
17
+ example "voicemaker new project sample-project"
18
+
19
+ def config_command
20
+ copy_config_template args['PATH']
21
+ end
22
+
23
+ def project_command
24
+ base = args['DIR']
25
+ copy_config_template "#{base}/config.yml"
26
+ Dir.mkdir "#{base}/in" unless Dir.exist? "#{base}/in"
27
+ Dir.mkdir "#{base}/out" unless Dir.exist? "#{base}/out"
28
+ File.write "#{base}/in/sample1.txt", "hello"
29
+ File.write "#{base}/in/sample2.txt", "hello"
30
+ say "created in and out folders in #{base}"
31
+ end
32
+
33
+ protected
34
+
35
+ def copy_config_template(path)
36
+ raise InputError, "File already exists: #{path}" if File.exist? path
37
+ content = File.read template
38
+ File.deep_write path, content
39
+ say "saved #{path}"
40
+ end
41
+
42
+ def template
43
+ @template ||= File.expand_path "../../sample.yml", __dir__
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,72 @@
1
+ module Voicemaker
2
+ module Commands
3
+ class ProjectCommand < Base
4
+ help "Create multiple audio files"
5
+
6
+ usage "voicemaker project PATH [--debug]"
7
+ usage "voicemaker project (-h|--help)"
8
+
9
+ param "PATH", "Path to the project directory"
10
+
11
+ option "--debug", "Show API parameters"
12
+
13
+ api_environment
14
+
15
+ example "voicemaker project sample-project"
16
+
17
+ def run
18
+ text_files.each do |file|
19
+ text = File.read file
20
+ tts.params.text = text
21
+ audio_file = File.basename(file, '.txt') + ".#{tts.params.output_format}"
22
+ output_path = "#{outdir}/#{audio_file}"
23
+
24
+ say "in: !txtblu!#{file}"
25
+ say "url: !txtblu!#{tts.url}"
26
+ say "out: !txtblu!#{output_path}"
27
+ show_details tts if args['--debug']
28
+
29
+ tts.save output_path
30
+ say "---"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def tts
37
+ @tts ||= Voicemaker::TTS.new **config
38
+ end
39
+
40
+ def show_details(tts, output: nil)
41
+ lp tts.params.api_params
42
+ end
43
+
44
+ def text_files
45
+ @text_files ||= Dir["#{indir}/*.txt"]
46
+ end
47
+
48
+ def config
49
+ @config ||= begin
50
+ raise InputError, "Cannot find config file: #{config_path}" unless File.exist? config_path
51
+ YAML.load_file(config_path).transform_keys &:to_sym
52
+ end
53
+ end
54
+
55
+ def config_path
56
+ "#{project_dir}/config.yml"
57
+ end
58
+
59
+ def project_dir
60
+ args['PATH']
61
+ end
62
+
63
+ def indir
64
+ "#{project_dir}/in"
65
+ end
66
+
67
+ def outdir
68
+ "#{project_dir}/out"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,90 @@
1
+ module Voicemaker
2
+ module Commands
3
+ class TTSCommand < Base
4
+ help "Generate audio files from text"
5
+
6
+ usage "voicemaker tts [options]"
7
+ usage "voicemaker tts (-h|--help)"
8
+
9
+ option "-v --voice NAME", "Voice ID or Webname"
10
+ option "-t --text TEXT", "Text to say"
11
+ option "-f --file PATH", "Load text from file"
12
+ option "-c --config PATH", "Use a YAML configuration file"
13
+ option "-s --save PATH", <<~EOF
14
+ Save audio file.
15
+ If not provided, a URL to the audio file will be printed instead.
16
+ When used with the --config option, omit the file extension, as it will be determined based on the config file.
17
+ EOF
18
+ option "-d --debug", "Show API parameters"
19
+
20
+ api_environment
21
+
22
+ example %q[voicemaker tts --voice ai3-Jony --text "Hello world" --save out.mp3]
23
+ example %q[voicemaker tts -v ai3-Jony --file hello.txt --save out.mp3]
24
+ example %q[voicemaker tts --config english-female.yml -f text.txt -s outfile]
25
+
26
+ def run
27
+ verify_args
28
+ tts = Voicemaker::TTS.new **tts_params
29
+ say "url: !txtblu!#{tts.url}"
30
+
31
+ if output_file
32
+ tts.save output_file
33
+ say "out: !txtblu!#{output_file}"
34
+ end
35
+
36
+ show_details tts if args['--debug']
37
+ end
38
+
39
+ private
40
+
41
+ def show_details(tts, output: nil)
42
+ lp tts.params.api_params
43
+ end
44
+
45
+ def verify_args
46
+ raise InputError, "Please provide either --config or --voice" unless args['--config'] || args['--voice']
47
+ raise InputError, "Please provide either --text or --file" unless args['--text'] || args['--file']
48
+ end
49
+
50
+ def voice
51
+ args['--voice']
52
+ end
53
+
54
+ def text
55
+ args['--text'] || text_from_file
56
+ end
57
+
58
+ def output_file
59
+ args['--save']
60
+ end
61
+
62
+ def tts_params
63
+ result = if config
64
+ YAML.load_file(config).transform_keys &:to_sym
65
+ else
66
+ {}
67
+ end
68
+
69
+ result[:voice] = voice if voice
70
+ result[:text] = text
71
+ result[:output_format] = File.extname(output_file)[1..] if output_file
72
+ result
73
+ end
74
+
75
+ def config
76
+ if args['--config']
77
+ raise InputError, "Cannot find config file #{args['--config']}" unless File.exist? args['--config']
78
+ args['--config']
79
+ end
80
+ end
81
+
82
+ def text_from_file
83
+ path = args['--file']
84
+ raise InputError, "Cannot find text file: #{path}" unless File.exist? path
85
+ File.read path
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,63 @@
1
+ module Voicemaker
2
+ module Commands
3
+ class VoicesCommand < Base
4
+ help "Get a list of available voices"
5
+
6
+ usage "voicemaker voices [--save PATH --count --verbose] [SEARCH...]"
7
+ usage "voicemaker voices (-h|--help)"
8
+
9
+ option "-s --save PATH", "Save output to output YAML file"
10
+ option "-v --verbose", "Show the full voices data structure"
11
+ option "-c --count", "Add number of voices to the result"
12
+
13
+ param "SEARCH", "Provide one or more text strings to search for (case insensitive AND search)"
14
+
15
+ api_environment
16
+
17
+ example "voicemaker voices en-us"
18
+ example "voicemaker voices --save out.yml en-us"
19
+ example "voicemaker voices en-us female"
20
+ example "voicemaker voices en-us --verbose"
21
+
22
+ def run
23
+ result = voices.search *args['SEARCH']
24
+ result['count'] = result.count if args['--count'] and args['--verbose']
25
+ send_output result
26
+ end
27
+
28
+ private
29
+
30
+ def voices
31
+ @voices ||= Voicemaker::Voices.new
32
+ end
33
+
34
+ def send_output(data)
35
+ save = args['--save']
36
+ if save
37
+ say "saved #{save}"
38
+ File.write save, args['--verbose'] ? data.to_yaml : compact(data)
39
+ elsif args['--verbose']
40
+ lp data
41
+ else
42
+ puts compact(data)
43
+ end
44
+ end
45
+
46
+ def compact(data)
47
+ result = data.values.map do |v|
48
+ [
49
+ v['VoiceId'].ljust(16),
50
+ # v['VoiceWebname'].ljust(10),
51
+ v['Language'],
52
+ v['VoiceGender'].ljust(10),
53
+ v['Engine']
54
+ ].join "\t"
55
+ end
56
+
57
+ result.push "count: #{data.count}" if args['--count']
58
+ result.join "\n"
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,11 @@
1
+ require "fileutils"
2
+
3
+ class File
4
+ class << self
5
+ def deep_write(file, content)
6
+ dir = File.dirname file
7
+ FileUtils.mkdir_p dir unless Dir.exist? dir
8
+ File.write file, content
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ require 'down'
2
+
3
+ module Voicemaker
4
+ class TTS
5
+ attr_reader :params
6
+
7
+ def initialize(params = {})
8
+ @params = TTSParams.new params
9
+ end
10
+
11
+ # Returns the URL for the generated file
12
+ def url
13
+ response = API.post "api", params.api_params
14
+ url = response['path']
15
+ raise BadResponse, "Received empty URL: #{response}" unless url
16
+ url
17
+ end
18
+
19
+ # Saves the audio file
20
+ def save(outpath)
21
+ Down.download url, destination: outpath
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,90 @@
1
+ require 'down'
2
+
3
+ module Voicemaker
4
+ # Provides normalized access to all parameters needed for the TTS endpoint,
5
+ # including the auto identification of the Engine and Language based on
6
+ # voice ID, and including sensible defaults.
7
+ class TTSParams
8
+ attr_reader :input_params
9
+ attr_writer :text, :voice, :output_format, :effect, :sample_rate,
10
+ :master_speed, :master_volume, :master_pitch
11
+
12
+ def initialize(input_params = {})
13
+ @input_params = input_params
14
+ end
15
+
16
+ def inspect
17
+ %Q[#<Voicemaker::TTSParams api_params="#{api_params}"]
18
+ end
19
+
20
+ def voice
21
+ @voice ||= find_voice || raise(InputError, "Missing or invalid parameter: voice")
22
+ end
23
+
24
+ def text
25
+ @text ||= input_params[:text] || raise(InputError, "Missing parameter: text")
26
+ end
27
+
28
+ def output_format
29
+ @output_format ||= input_params[:output_format] || 'mp3'
30
+ end
31
+
32
+ def effect
33
+ @effect ||= input_params[:effect] || 'default'
34
+ end
35
+
36
+ def sample_rate
37
+ @sample_rate ||= (input_params[:sample_rate] || 48000).to_s
38
+ end
39
+
40
+ def master_speed
41
+ @master_speed ||= (input_params[:master_speed] || 0).to_s
42
+ end
43
+
44
+ def master_volume
45
+ @master_volume ||= (input_params[:master_volume] || 0).to_s
46
+ end
47
+
48
+ def master_pitch
49
+ @master_pitch ||= (input_params[:master_pitch] || 0).to_s
50
+ end
51
+
52
+ def api_params
53
+ {
54
+ "Engine" => engine,
55
+ "VoiceId" => voice,
56
+ "LanguageCode" => language,
57
+ "OutputFormat" => output_format,
58
+ "SampleRate" => sample_rate,
59
+ "Effect" => effect,
60
+ "MasterSpeed" => master_speed,
61
+ "MasterVolume" => master_volume,
62
+ "MasterPitch" => master_pitch,
63
+ "Text" => text
64
+ }
65
+ end
66
+
67
+ protected
68
+
69
+ def engine
70
+ voice_params['Engine']
71
+ end
72
+
73
+ def language
74
+ voice_params['Language']
75
+ end
76
+
77
+ def find_voice
78
+ return nil unless input_params[:voice]
79
+ voices.find(input_params[:voice])&.dig 'VoiceId'
80
+ end
81
+
82
+ def voice_params
83
+ voices[voice] || raise(InputError, "Cannot find definition for voice: #{voice}")
84
+ end
85
+
86
+ def voices
87
+ @voices ||= Voices.new
88
+ end
89
+ end
90
+ end
@@ -1,3 +1,3 @@
1
1
  module Voicemaker
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,41 @@
1
+ require 'forwardable'
2
+
3
+ module Voicemaker
4
+ # Provides easy and cached access to the list of available voices.
5
+ class Voices
6
+ extend Forwardable
7
+
8
+ # Enable some of the hash methods idrectly on the Voices object
9
+ def_delegators :all, :[], :count, :size, :first, :last, :select, :reject,
10
+ :map, :keys, :values, :each
11
+
12
+ # Returns all voices
13
+ def all
14
+ @all ||= begin
15
+ response = API.get "/list"
16
+ result = response.dig 'data', 'voices_list'
17
+ raise BadResponse, "Unexpected response: #{response}" unless result
18
+ result.map { |voice| [voice['VoiceId'], voice] }
19
+ .sort_by { |_, v| v['VoiceId'] }.to_h
20
+ end
21
+ end
22
+
23
+ # Returns a filtered list of voices, with all queries partially included
24
+ # in the voice parameter values
25
+ def search(*queries)
26
+ queries = nil if queries&.empty?
27
+ return all unless queries
28
+
29
+ all.select do |key, data|
30
+ haystack = data.values.join(' ').downcase
31
+ queries.all? { |query| haystack.include? query.downcase }
32
+ end
33
+ end
34
+
35
+ # Returns a single voice, by Voice ID or Voice Webname
36
+ def find(id_or_webname)
37
+ all[id_or_webname] || all.find { |_, a| a['VoiceWebname'] == id_or_webname }&.last
38
+ end
39
+
40
+ end
41
+ end
data/lib/voicemaker.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  require 'voicemaker/version'
2
2
  require 'voicemaker/exceptions'
3
+ require 'voicemaker/extensions/file'
3
4
  require 'voicemaker/api'
5
+ require 'voicemaker/voices'
6
+ require 'voicemaker/tts_params'
7
+ require 'voicemaker/tts'
4
8
 
5
9
  require 'byebug' if ENV['BYEBUG']
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: voicemaker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Ben Shitrit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-16 00:00:00.000000000 Z
11
+ date: 2022-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mister_bin
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: lightly
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.3'
69
83
  description: Easy to use API for Voicemaker.in TTS service, with a command line interface
70
84
  email: db@dannyben.com
71
85
  executables:
@@ -79,9 +93,17 @@ files:
79
93
  - lib/voicemaker.rb
80
94
  - lib/voicemaker/api.rb
81
95
  - lib/voicemaker/cli.rb
82
- - lib/voicemaker/command.rb
96
+ - lib/voicemaker/commands/base.rb
97
+ - lib/voicemaker/commands/new_command.rb
98
+ - lib/voicemaker/commands/project_command.rb
99
+ - lib/voicemaker/commands/tts_command.rb
100
+ - lib/voicemaker/commands/voices_command.rb
83
101
  - lib/voicemaker/exceptions.rb
102
+ - lib/voicemaker/extensions/file.rb
103
+ - lib/voicemaker/tts.rb
104
+ - lib/voicemaker/tts_params.rb
84
105
  - lib/voicemaker/version.rb
106
+ - lib/voicemaker/voices.rb
85
107
  homepage: https://github.com/DannyBen/voicemaker
86
108
  licenses:
87
109
  - MIT
@@ -1,111 +0,0 @@
1
- require 'lp'
2
- require 'mister_bin'
3
-
4
- module Voicemaker
5
- class Command < MisterBin::Command
6
- help "Voicemaker API\n\n API Documentation:\n https://developer.voicemaker.in/apidocs"
7
- version Voicemaker::VERSION
8
-
9
- usage "voicemaker voices [--save PATH] [SEARCH...]"
10
- usage "voicemaker new CONFIG"
11
- usage "voicemaker generate CONFIG [OUTPUT]"
12
- usage "voicemaker batch INDIR OUTDIR"
13
- usage "voicemaker (-h|--help|--version)"
14
-
15
- command "voices", "Get list of voices, optionally in a given language"
16
- command "new", "Generate a sample config file"
17
- command "generate", "Generate audio file. The output filename will be the same as the config filename, with the proper mp3 or wav extension"
18
- command "batch", "Generate multiple audio files from multiple config files"
19
-
20
- option "-l --language LANG", "Limit results to a specific language"
21
- option "-s --save PATH", "Save output to output YAML file"
22
-
23
- param "SEARCH", "Provide one or more text strings to search for (case insensitive)"
24
- param "CONFIG", "Path to config file"
25
- param "OUTPUT", "Path to output mp3/wav file. If not provided, the filename will be the same as the config file, with wav/mp3 extension"
26
- param "INDIR", "Path to input directory, containing config YAML files"
27
- param "OURDIR", "Path to output directory, where mp3/wav files will be saved"
28
-
29
- environment "VOICEMAKER_API_KEY", "Your Voicemaker API key [required]"
30
- environment "VOICEMAKER_API_HOST", "Override the API host URL"
31
-
32
- example "voicemaker voices en-us"
33
- example "voicemaker voices --save out.yml en-us"
34
- example "voicemaker voices en-us female"
35
- example "voicemaker new test.yml"
36
- example "voicemaker generate test.yml out.mp3"
37
- example "voicemaker batch configs out"
38
-
39
- def voices_command
40
- send_output api.voices(args['SEARCH'])
41
- end
42
-
43
- def new_command
44
- template = File.expand_path "../sample.yml", __dir__
45
- content = File.read template
46
- File.write args['CONFIG'], content
47
- say "Saved #{args['CONFIG']}"
48
- end
49
-
50
- def generate_command
51
- config_path = args['CONFIG']
52
- raise InputError, "Cannot find config file #{config_path}" unless File.exist? config_path
53
- outpath = args['OUTPUT'] || outpath_from_config(config_path)
54
- generate config_path, outpath
55
- end
56
-
57
- def batch_command
58
- files = Dir["#{args['INDIR']}/*.yml"].sort
59
- raise InputError, "No config files in #{args['INDIR']}" if files.empty?
60
-
61
- files.each do |config_path|
62
- extension = extension_from_config(config_path)
63
- basename = File.basename config_path, '.yml'
64
- outpath = "#{args['OUTDIR']}/#{basename}.#{extension}"
65
- generate config_path, outpath
66
- puts ""
67
- end
68
- end
69
-
70
- private
71
-
72
- def generate(config_path, outpath)
73
- params = YAML.load_file config_path
74
-
75
- say "Config : !txtgrn!#{config_path}"
76
- api.download outpath, params do |url|
77
- say "URL : !txtblu!#{url}"
78
- end
79
-
80
- say "Path : !txtblu!#{outpath}"
81
- end
82
-
83
- def send_output(data)
84
- save = args['--save']
85
- if save
86
- say "Saved #{save}"
87
- File.write save, data.to_yaml
88
- else
89
- lp data
90
- end
91
- end
92
-
93
- def api
94
- @api ||= Voicemaker::API.new(api_key)
95
- end
96
-
97
- def api_key
98
- ENV['VOICEMAKER_API_KEY'] or raise MissingAuth, "Please set the 'VOICEMAKER_API_KEY' environment variable"
99
- end
100
-
101
- def outpath_from_config(config_path)
102
- ext = extension_from_config config_path
103
- config_path.gsub(/yml$/, ext)
104
- end
105
-
106
- def extension_from_config(config_path)
107
- YAML.load_file(config_path)['OutputFormat']
108
- end
109
-
110
- end
111
- end