acapela 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/Rakefile +38 -0
- data/acapela.gemspec +24 -0
- data/init.rb +1 -0
- data/lib/acapela.rb +27 -0
- data/lib/acapela/config.rb +38 -0
- data/lib/acapela/constants.rb +31 -0
- data/lib/acapela/error.rb +33 -0
- data/lib/acapela/mocks.rb +5 -0
- data/lib/acapela/mocks/constants.rb +11 -0
- data/lib/acapela/mocks/response.rb +29 -0
- data/lib/acapela/mocks/test_file.mp3 +0 -0
- data/lib/acapela/mocks/voice_service.rb +36 -0
- data/lib/acapela/response.rb +64 -0
- data/lib/acapela/voice.rb +121 -0
- data/lib/acapela/voice_service.rb +84 -0
- data/lib/acapela/voices/default.rb +40 -0
- data/script/create_voices.rb +137 -0
- data/script/test_voices.rb +77 -0
- data/spec/acapela/config_spec.rb +40 -0
- data/spec/acapela/response_spec.rb +47 -0
- data/spec/acapela/test_config.yml +3 -0
- data/spec/acapela/voice_service_spec.rb +69 -0
- data/spec/acapela/voice_spec.rb +128 -0
- data/spec/acapela_spec.rb +24 -0
- data/spec/spec_helper.rb +45 -0
- metadata +123 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour -fd
|
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'bundler'
|
5
|
+
require 'bundler/gem_tasks'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
require 'acapela/constants'
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
10
|
+
|
11
|
+
namespace :generate do
|
12
|
+
desc "Generates MP3 with test strings for all voices."
|
13
|
+
task :test_voices do
|
14
|
+
exec('ruby -I lib script/test_voices.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Generate default voice class based on enumrated values from Acapela"
|
18
|
+
task :default_voices_class do
|
19
|
+
exec('ruby -I lib script/create_voices.rb')
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Generate default voice class based on PHP example voice list"
|
23
|
+
namespace :default_voices_class do
|
24
|
+
task :php do
|
25
|
+
exec('ruby -I lib script/create_voices.rb php')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Clean up generated and downloaded files"
|
31
|
+
task :clean do
|
32
|
+
include Acapela::Scripts
|
33
|
+
FileUtils.rm_rf(LOCAL_TEMP_DIR)
|
34
|
+
FileUtils.rm(PHP_VOICE_ARRAY_FILE) rescue nil
|
35
|
+
FileUtils.rm(VOICES_ENUMERATOR_FILE) rescue nil
|
36
|
+
end
|
37
|
+
|
38
|
+
task :default => :spec
|
data/acapela.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "acapela/constants"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "acapela"
|
7
|
+
s.version = Acapela::VERSION
|
8
|
+
s.authors = ["Birkir A. Barkarson"]
|
9
|
+
s.email = ["birkirb@stoicviking.net"]
|
10
|
+
s.homepage = "https://github.com/birkirb/acapela"
|
11
|
+
s.summary = %q{Generate speech from the Acapela text to voice service.}
|
12
|
+
s.description = %q{Ruby interface to Acapela's API for generating speech from text. More info http://www.acapela-vaas.com/}
|
13
|
+
|
14
|
+
s.rubyforge_project = "acapela"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency("backports")
|
22
|
+
s.add_development_dependency("rspec", '>= 2.6.0')
|
23
|
+
s.add_development_dependency("mocha", '>= 0.9.0')
|
24
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'acapela'
|
data/lib/acapela.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'acapela/error'
|
2
|
+
require 'acapela/config'
|
3
|
+
require 'acapela/voices/default'
|
4
|
+
require 'acapela/voice'
|
5
|
+
require 'acapela/constants'
|
6
|
+
require 'acapela/response'
|
7
|
+
require 'acapela/voice_service'
|
8
|
+
|
9
|
+
if RUBY_VERSION < "1.9"
|
10
|
+
require 'backports'
|
11
|
+
end
|
12
|
+
|
13
|
+
module Acapela
|
14
|
+
|
15
|
+
def self.config
|
16
|
+
@@config ||= Acapela::Config.read rescue nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.config=(config)
|
20
|
+
if config.is_a?(Acapela::Config)
|
21
|
+
@@config = config
|
22
|
+
else
|
23
|
+
raise Error.new("Acapela configuration required. Not #{config.class}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Acapela
|
5
|
+
class Config
|
6
|
+
attr_reader :application,
|
7
|
+
:version,
|
8
|
+
:environment,
|
9
|
+
:login,
|
10
|
+
:password,
|
11
|
+
:protocol,
|
12
|
+
:target_url
|
13
|
+
|
14
|
+
DEFAULT_PROTOCOL = '2'
|
15
|
+
DEFAULT_VERSION = '1-30'
|
16
|
+
DEFAULT_TARGET_URL = 'http://vaas.acapela-group.com/Services/Synthesizer'
|
17
|
+
|
18
|
+
def initialize(login, application, password, target_url = nil, version = nil, protocol = nil)
|
19
|
+
@login = login
|
20
|
+
@application = application
|
21
|
+
@password = password
|
22
|
+
@protocol = protocol || DEFAULT_PROTOCOL
|
23
|
+
@version = version || DEFAULT_VERSION
|
24
|
+
@target_url = URI.parse(target_url || DEFAULT_TARGET_URL)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.read(config_file = nil)
|
28
|
+
config_file ||= File.join('config', 'acapela.yml')
|
29
|
+
begin
|
30
|
+
yaml = YAML.load_file(config_file)
|
31
|
+
self.new(yaml['login'], yaml['application'], yaml['password'], yaml['target_url'] , yaml['version'], yaml['protocol'])
|
32
|
+
rescue => err
|
33
|
+
raise Error.new("Failed to read configuration file", err)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Acapela
|
4
|
+
|
5
|
+
VERSION = "0.8.1"
|
6
|
+
|
7
|
+
class Voice
|
8
|
+
QUALITY_LOW = '8k'
|
9
|
+
QUALITY_HIGH = '22k'
|
10
|
+
GENDER_FEMALE = :female
|
11
|
+
GENDER_MALE = :male
|
12
|
+
end
|
13
|
+
|
14
|
+
module Scripts
|
15
|
+
LOCAL_TEMP_DIR = 'tmp'
|
16
|
+
TEST_VOICES_DIR = File.join(LOCAL_TEMP_DIR, 'voices')
|
17
|
+
|
18
|
+
|
19
|
+
PHP_VOICE_ARRAY_URL = URI.parse("http://vaas.acapela-group.com/webservices/1-34-01/publish/export/EVAL_VAAS/voices_lists/php_code.txt")
|
20
|
+
PHP_VOICE_ARRAY_FILE = File.join('script', 'php_code.txt')
|
21
|
+
PHP_ARRAY_REGEXP = /array\((.*)?\),?$/
|
22
|
+
|
23
|
+
VOICES_ENUMERATOR_URL = URI.parse("http://vaas.acapela-group.com/Services/Enumerator")
|
24
|
+
VOICES_ENUMERATOR_FILE = File.join('script', 'voice_enumerator.out')
|
25
|
+
# Enmuerator Example: leila22k=ar_SA/Arabic (Saudi Arabia)/Leila/HQ/F/PCM/22050
|
26
|
+
VOICE_ENUMERATOR_REGEXP = /(.*)?=(\w\w_\w\w)\/(.*)?\/(\w+)\/(\w+)\/(\w+)\/(\w+)\//
|
27
|
+
|
28
|
+
DEFAULT_VOICES_RUBY_FILE = File.join('lib', 'acapela', 'voices', 'default.rb')
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Acapela
|
2
|
+
|
3
|
+
class Error < StandardError
|
4
|
+
attr_accessor :original_error
|
5
|
+
|
6
|
+
def initialize(message, original_error = nil)
|
7
|
+
super(message)
|
8
|
+
@original_error = original_error
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
if @original_error.nil?
|
13
|
+
super
|
14
|
+
else
|
15
|
+
"#{super}\nCause: #{@original_error.to_s}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ServiceError < Error
|
21
|
+
attr_accessor :code
|
22
|
+
|
23
|
+
def initialize(message, code = '')
|
24
|
+
super(message)
|
25
|
+
@code = code
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"Code: #{code}, Message: #{super}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Acapela
|
2
|
+
module Mocks
|
3
|
+
|
4
|
+
RESPONSE_TEST_FILE = File.join(File.dirname(__FILE__), 'test_file.mp3')
|
5
|
+
|
6
|
+
EXAMPLE_ACAPELA_RESPONSE_ACCESS_DENIED = "res=NOK&err_code=ACCESS_DENIED_ERROR&err_msg=Invalid%20identifiers&w=&create_echo="
|
7
|
+
EXAMPLE_ACAPELA_RESPONSE_INVALID_PARAM = "res=NOK&err_code=INVALID_PARAM_ERROR&err_msg=This%20voice%20is%20not%20available&w=&create_echo="
|
8
|
+
EXAMPLE_ACAPELA_RESPONSE_OK = "w=&snd_time=1407.94&get_count=0&snd_id=210375264_cacefb1f8f862&asw_pos_init_offset=0&asw_pos_text_offset=0&snd_url=http://vaas.acapela-group.com/MESSAGES/009086065076095086065065083/EVAL_ACCOUNT/sounds/210375264_cacefb1f8f862.mp3&snd_size=9098&res=OK&create_echo="
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Acapela
|
2
|
+
class Response
|
3
|
+
|
4
|
+
def self.mock_on
|
5
|
+
self.class_eval(<<-EVAL, __FILE__, __LINE__)
|
6
|
+
def fetch_file_from_url
|
7
|
+
fetch_file_from_url_with_mock
|
8
|
+
end
|
9
|
+
EVAL
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.mock_off
|
13
|
+
self.class_eval(<<-EVAL, __FILE__, __LINE__)
|
14
|
+
def fetch_file_from_url
|
15
|
+
fetch_file_from_url_without_mock
|
16
|
+
end
|
17
|
+
EVAL
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
alias :fetch_file_from_url_without_mock :fetch_file_from_url
|
23
|
+
|
24
|
+
def fetch_file_from_url_with_mock
|
25
|
+
File.read(Acapela::Mocks::RESPONSE_TEST_FILE)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
Binary file
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Acapela
|
2
|
+
class VoiceService
|
3
|
+
include Acapela::Mocks
|
4
|
+
|
5
|
+
@@expected_response = EXAMPLE_ACAPELA_RESPONSE_OK
|
6
|
+
@@last_posted_params = nil
|
7
|
+
|
8
|
+
def self.expected_response
|
9
|
+
@@expected_response
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.last_posted_params
|
13
|
+
@@last_posted_params
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.expect_ok_response
|
17
|
+
@@expected_response = EXAMPLE_ACAPELA_RESPONSE_OK
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.expect_invalid_param_response
|
21
|
+
@@expected_response = EXAMPLE_ACAPELA_RESPONSE_INVALID_PARAM
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.expect_access_denied_response
|
25
|
+
@@expected_response = EXAMPLE_ACAPELA_RESPONSE_ACCESS_DENIED
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def post(params)
|
31
|
+
@@last_posted_params = params
|
32
|
+
self.class.expected_response
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'net/http'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module Acapela
|
6
|
+
class Response
|
7
|
+
|
8
|
+
attr_reader :id,
|
9
|
+
:time,
|
10
|
+
:size,
|
11
|
+
:url
|
12
|
+
|
13
|
+
RESPONSE_STATE = 'res'
|
14
|
+
RESPONSE_STATE_REGEXP = /#{RESPONSE_STATE}=/
|
15
|
+
RESPONSE_SOUND_URL = 'snd_url'
|
16
|
+
RESPONSE_STATE_OK = 'OK'
|
17
|
+
RESPONSE_ERROR_MESSAGE = 'err_msg'
|
18
|
+
RESPONSE_ERROR_CODE = 'err_code'
|
19
|
+
|
20
|
+
ERROR_UNEXPECTED_RESPONSE = ServiceError.new("Unexpected response.")
|
21
|
+
ERROR_MISSING_SOUND_URL = ServiceError.new("Parameters are missing sound url.")
|
22
|
+
|
23
|
+
def initialize(response)
|
24
|
+
if RESPONSE_STATE_REGEXP.match(response)
|
25
|
+
params = CGI::parse(response)
|
26
|
+
else
|
27
|
+
raise ERROR_UNEXPECTED_RESPONSE
|
28
|
+
end
|
29
|
+
|
30
|
+
if RESPONSE_STATE_OK == params[RESPONSE_STATE].first
|
31
|
+
if url = params[RESPONSE_SOUND_URL]
|
32
|
+
@url = URI.parse(url.first)
|
33
|
+
@id = params['snd_id'].first
|
34
|
+
@time = params['snd_time'].first.to_f
|
35
|
+
@size = params['snd_size'].first.to_i
|
36
|
+
else
|
37
|
+
raise ERROR_MISSING_SOUND_URL
|
38
|
+
end
|
39
|
+
else
|
40
|
+
raise ServiceError.new(params[RESPONSE_ERROR_MESSAGE].first, params[RESPONSE_ERROR_CODE].first)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def download_to_tempfile
|
45
|
+
content = fetch_file_from_url
|
46
|
+
file = Tempfile.new(self.id)
|
47
|
+
file.write(content)
|
48
|
+
file.flush
|
49
|
+
file # Leaving open. Will be closed once object is finalized.
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def fetch_file_from_url
|
55
|
+
response = Net::HTTP.get_response(self.url)
|
56
|
+
if 200 == response.code.to_i
|
57
|
+
response.body
|
58
|
+
else
|
59
|
+
raise Error.new("Download failed with status: #{response.code}.")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Acapela
|
4
|
+
class Voice
|
5
|
+
attr_reader :languages, :speaker, :gender, :quality
|
6
|
+
|
7
|
+
def initialize(speaker, gender, *languages)
|
8
|
+
@speaker = speaker
|
9
|
+
@gender = gender
|
10
|
+
@quality = QUALITY_HIGH
|
11
|
+
|
12
|
+
@languages = Set.new
|
13
|
+
languages.each { |lang| @languages.add(lang) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def id
|
17
|
+
"#{speaker.downcase}#{quality}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def language
|
21
|
+
languages.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def low_quality!
|
25
|
+
@quality = QUALITY_LOW
|
26
|
+
end
|
27
|
+
|
28
|
+
def high_quality!
|
29
|
+
@quality = QUALITY_HIGH
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.override_map(override_voice_map = {})
|
33
|
+
current_map = self.map
|
34
|
+
override_voice_map.each do |language, voices|
|
35
|
+
language_sym = language.to_sym
|
36
|
+
if voices.nil?
|
37
|
+
# Remove voice directive.
|
38
|
+
current_map.delete(language_sym)
|
39
|
+
elsif voices.is_a?(String) || voices.is_a?(Symbol)
|
40
|
+
# Map voice directive.
|
41
|
+
current_map[language_sym] = self.map[voices]
|
42
|
+
elsif voices.is_a?(Hash)
|
43
|
+
# Replace/Override
|
44
|
+
if entry = self.map[language_sym]
|
45
|
+
new_entry = entry.dup
|
46
|
+
else
|
47
|
+
new_entry = Hash.new
|
48
|
+
end
|
49
|
+
|
50
|
+
voices.each do |gender, speakers|
|
51
|
+
if gender.is_a?(Symbol) && speakers.is_a?(Array)
|
52
|
+
new_entry[gender] = speakers
|
53
|
+
end
|
54
|
+
end
|
55
|
+
current_map[language_sym] = new_entry
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@@map = current_map
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.map
|
62
|
+
reset_map unless defined?(@@map)
|
63
|
+
@@map
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.reset_map
|
67
|
+
@@map = Voices::PER_LANGUAGE.dup
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.extract_from_options(options = {})
|
71
|
+
if speaker = options[:speaker]
|
72
|
+
named_voice(speaker)
|
73
|
+
else
|
74
|
+
language = options[:language] || :en
|
75
|
+
gender = options[:gender]
|
76
|
+
speakers_for_language = self.map[language]
|
77
|
+
|
78
|
+
speakers = case gender
|
79
|
+
when GENDER_FEMALE
|
80
|
+
speakers_for_language[GENDER_FEMALE]
|
81
|
+
when GENDER_MALE
|
82
|
+
speakers_for_language[GENDER_MALE]
|
83
|
+
else
|
84
|
+
speakers_for_language[GENDER_FEMALE] +
|
85
|
+
speakers_for_language[GENDER_MALE]
|
86
|
+
end
|
87
|
+
|
88
|
+
if speakers.empty?
|
89
|
+
nil
|
90
|
+
else
|
91
|
+
named_voice(speakers.sample)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.named_voice(speaker)
|
97
|
+
unless defined?(@@voices)
|
98
|
+
@@voices = Hash.new
|
99
|
+
self.map.each do |language, voices|
|
100
|
+
[GENDER_MALE, GENDER_FEMALE].each do |gender|
|
101
|
+
voices[gender].each do |name|
|
102
|
+
downcase_name = name.downcase.to_sym
|
103
|
+
if @@voices[downcase_name].nil?
|
104
|
+
@@voices[downcase_name] = Voice.new(name, gender, language.to_s)
|
105
|
+
else
|
106
|
+
@@voices[downcase_name].languages.add(language.to_s)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
if info = @@voices[speaker.to_s.downcase.to_sym]
|
114
|
+
info
|
115
|
+
else
|
116
|
+
raise Error.new("Voice does not exist.")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Acapela
|
4
|
+
class VoiceService
|
5
|
+
|
6
|
+
CLIENT_ENVIRONMENT = "RUBY_#{RUBY_VERSION}"
|
7
|
+
|
8
|
+
ERROR_MISSING_CONFIG = Error.new("VoiceService requires configuration.")
|
9
|
+
|
10
|
+
attr_reader :config
|
11
|
+
|
12
|
+
def initialize(config = Acapela.config)
|
13
|
+
if config.is_a?(Acapela::Config)
|
14
|
+
@config = config
|
15
|
+
else
|
16
|
+
raise ERROR_MISSING_CONFIG
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate_sound(text, options = {})
|
21
|
+
voice = Voice.extract_from_options(options)
|
22
|
+
|
23
|
+
case options[:quality]
|
24
|
+
when :low
|
25
|
+
voice.low_quality!
|
26
|
+
when :high
|
27
|
+
voice.high_quality!
|
28
|
+
end
|
29
|
+
|
30
|
+
generate_with_voice(text, voice)
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_with_voice(text, voice)
|
34
|
+
params = {
|
35
|
+
:prot_vers => @config.protocol,
|
36
|
+
:cl_env => CLIENT_ENVIRONMENT,
|
37
|
+
:cl_vers => @config.version,
|
38
|
+
:cl_login => @config.login,
|
39
|
+
:cl_app => @config.application,
|
40
|
+
:cl_pwd => @config.password,
|
41
|
+
:req_type => 'NEW',
|
42
|
+
#:req_snd_id => nil,
|
43
|
+
:req_voice => voice.id,
|
44
|
+
:req_text => text,
|
45
|
+
#:req_vol => nil, # Volume: min = 50, default = 32768, max = 65535
|
46
|
+
#:req_spd => nil, # Speed: min = 60, default = 180, max = 360
|
47
|
+
#:req_vct => nil, # Shaping: min = 50, default = 100, max = 150
|
48
|
+
# Equalizer: min = -100, default = 0, max = 100
|
49
|
+
#:req_eq1 => nil, # Band 275Hz
|
50
|
+
#:req_eq2 => nil, # Band: 2.2kHz
|
51
|
+
#:req_eq3 => nil, # Band: 5kHz
|
52
|
+
#:req_eq4 => nil, # Band: 8.3kHz
|
53
|
+
#:req_snd_type => 'MP3', # Sound file type: MP3, WAV, RAW
|
54
|
+
#:req_snd_ext => '.mp3', # Sound file extentions: .mp3 .wav .raw
|
55
|
+
#:req_snd_kbps => 'CBR_48', # Variable bit rate VBR_5 to VRB_9 (5 = max quality, 9 min) or Constant Bit Rate CBR_8,16,32,48
|
56
|
+
#:req_alt_snd_type => 'MP3', # Alternative Sound file type: MP3, WAV, RAW
|
57
|
+
#:req_alt_snd_ext => '.mp3', # Alternative Sound file extentions: .mp3 .wav .raw
|
58
|
+
#:req_wp => nil, # 'ON' to receive word file URL
|
59
|
+
#:req_bp => nil, # 'ON' to receive bookmark file URL
|
60
|
+
#:req_mp => nil, # 'ON' to receive mouth file URL
|
61
|
+
#:req_comment => '', Information to store about the operation.
|
62
|
+
#:req_start_time => nil, # The start time of the request will be used to calculate the deadline for request treatment
|
63
|
+
#:req_timeout => nil, # The time allocated to request treatment in seconds.
|
64
|
+
#:req_asw_type => 'SOUND', # Type of response:
|
65
|
+
# "INFO", key/value params.
|
66
|
+
# "SOUND", sound bytes once genearted.
|
67
|
+
# "STREAM", bytes as they are generated.
|
68
|
+
#:req_asw_as_alt_snd => 'no', # Receive alternative file as response.
|
69
|
+
#:req_err_as_id3 => 'no', # Reveive errors encapsulated in ID3 tag of MP3.
|
70
|
+
#:req_echo => '', # Receive some creation request fields in response.
|
71
|
+
#:req_asw_redirect_url => nil, # Redirect response to a different URL.
|
72
|
+
}
|
73
|
+
|
74
|
+
Response.new(post(params))
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def post(params)
|
80
|
+
body = Net::HTTP.post_form(@config.target_url, params).body
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Acapela
|
2
|
+
module Voices
|
3
|
+
PER_LANGUAGE = {
|
4
|
+
:ar => {:female=>["Leila", "Salma"], :male=>["Mehdi", "Nizar"]},
|
5
|
+
:ca => {:female=>["Laia"], :male=>[]},
|
6
|
+
:cs => {:female=>["Eliska"], :male=>[]},
|
7
|
+
:da => {:female=>["Mette"], :male=>["Rasmus"]},
|
8
|
+
:de => {:female=>["Julia", "Sarah"], :male=>["Andreas", "Klaus"]},
|
9
|
+
:el => {:female=>[], :male=>["Dimitris"]},
|
10
|
+
:en => {:female=>["Deepa", "Lucy", "QueenElizabeth", "Rachel", "Heather", "Laura", "Nelly", "Tracy"], :male=>["Graham", "Nizareng", "Peter", "Kenny", "Micah", "Ryan", "Saul"]},
|
11
|
+
:en_GB => {:female=>["Lucy", "QueenElizabeth", "Rachel"], :male=>["Graham", "Nizareng", "Peter"]},
|
12
|
+
:en_IN => {:female=>["Deepa"], :male=>[]},
|
13
|
+
:en_US => {:female=>["Heather", "Laura", "Nelly", "Tracy"], :male=>["Kenny", "Micah", "Ryan", "Saul"]},
|
14
|
+
:es => {:female=>["Ines", "Maria", "Rosa"], :male=>["Antonio"]},
|
15
|
+
:es_ES => {:female=>["Ines", "Maria"], :male=>["Antonio"]},
|
16
|
+
:es_US => {:female=>["Rosa"], :male=>[]},
|
17
|
+
:fi => {:female=>["Sanna"], :male=>[]},
|
18
|
+
:fr => {:female=>["Justine", "Louise", "Alice", "Claire", "Julie", "Margaux"], :male=>["Antoine", "Bruno"]},
|
19
|
+
:fr_BE => {:female=>["Justine"], :male=>[]},
|
20
|
+
:fr_CA => {:female=>["Louise"], :male=>[]},
|
21
|
+
:fr_FR => {:female=>["Alice", "Claire", "Julie", "Margaux"], :male=>["Antoine", "Bruno"]},
|
22
|
+
:gb => {:female=>[], :male=>["Kal"]},
|
23
|
+
:it => {:female=>["Chiara", "Fabiana", "chiara", "fabiana"], :male=>["Vittorio", "vittorio"]},
|
24
|
+
:nl => {:female=>["Sofie", "Zoe", "Femke", "Jasmijn"], :male=>["Jeroen", "Daan", "Max"]},
|
25
|
+
:nl_BE => {:female=>["Sofie", "Zoe"], :male=>["Jeroen"]},
|
26
|
+
:nl_NL => {:female=>["Femke", "Jasmijn"], :male=>["Daan", "Max"]},
|
27
|
+
:no => {:female=>["Bente", "Kari"], :male=>["Olav"]},
|
28
|
+
:pl => {:female=>["Ania"], :male=>[]},
|
29
|
+
:pt => {:female=>["Marcia", "Celia"], :male=>[]},
|
30
|
+
:pt_BR => {:female=>["Marcia"], :male=>[]},
|
31
|
+
:pt_PT => {:female=>["Celia"], :male=>[]},
|
32
|
+
:ru => {:female=>["Alyona"], :male=>[]},
|
33
|
+
:sc => {:female=>["Mia"], :male=>[]},
|
34
|
+
:sv => {:female=>["Elin", "Emma"], :male=>["Emil", "Erik", "Samuel"]},
|
35
|
+
:sv_FI => {:female=>[], :male=>["Samuel"]},
|
36
|
+
:sv_SE => {:female=>["Elin", "Emma"], :male=>["Emil", "Erik"]},
|
37
|
+
:tr => {:female=>["Ipek"], :male=>[]}
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'set'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'acapela/constants'
|
5
|
+
|
6
|
+
include Acapela::Scripts
|
7
|
+
|
8
|
+
def save_uri_to_file_if_missing(uri, filename)
|
9
|
+
if !File.exists?(filename)
|
10
|
+
response = Net::HTTP.get_response(uri)
|
11
|
+
|
12
|
+
if 200 == response.code.to_i
|
13
|
+
voice_hash = Hash.new
|
14
|
+
File.open(filename,'w') do |f|
|
15
|
+
f.write(response.body)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
puts "Download failed with: #{response.code}\nBody#{response.body}"
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def php_array_to_ruby_array_of_hashes(text)
|
25
|
+
voices = Array.new
|
26
|
+
text.lines do |line|
|
27
|
+
if match = PHP_ARRAY_REGEXP.match(line)
|
28
|
+
line_hash = eval("{#{match[1]}}")
|
29
|
+
voices << line_hash
|
30
|
+
end
|
31
|
+
end
|
32
|
+
voices
|
33
|
+
end
|
34
|
+
|
35
|
+
def enumerator_text_to_ruby_array_of_hashes(text)
|
36
|
+
voices = Array.new
|
37
|
+
text.lines do |line|
|
38
|
+
if match = VOICE_ENUMERATOR_REGEXP.match(line)
|
39
|
+
voices << {'language_locale' => match[2], 'speaker' => match[4], 'gender' => match[6]}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
voices
|
43
|
+
end
|
44
|
+
|
45
|
+
def gender_code_to_symbol(gender)
|
46
|
+
case gender
|
47
|
+
when 'F'
|
48
|
+
Acapela::Voice::GENDER_FEMALE
|
49
|
+
when 'M'
|
50
|
+
Acapela::Voice::GENDER_MALE
|
51
|
+
else
|
52
|
+
raise "Unknown gender code: #{gender}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def simple_language_code(locale)
|
57
|
+
locale.split('_').first
|
58
|
+
end
|
59
|
+
|
60
|
+
class Set
|
61
|
+
def inspect
|
62
|
+
self.to_a.inspect
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def voice_array_to_voices_per_language(voices)
|
67
|
+
language_voices = Hash.new { |h,v| h[v] = { :female => Set.new, :male => Set.new } }
|
68
|
+
|
69
|
+
voices.each do |voice_hash|
|
70
|
+
gender = gender_code_to_symbol(voice_hash['gender'])
|
71
|
+
locale = voice_hash['language_locale']
|
72
|
+
simple_locale = simple_language_code(locale)
|
73
|
+
|
74
|
+
language_voices[locale][gender].add(voice_hash['speaker'])
|
75
|
+
language_voices[simple_locale][gender].add(voice_hash['speaker'])
|
76
|
+
end
|
77
|
+
|
78
|
+
# Clean up instance where specific locale is same as generic one.
|
79
|
+
language_voices.delete_if do |key,value|
|
80
|
+
simple_locale = simple_language_code(key)
|
81
|
+
if key != simple_locale && language_voices[simple_locale] == value
|
82
|
+
true
|
83
|
+
else
|
84
|
+
false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
language_voices
|
89
|
+
end
|
90
|
+
|
91
|
+
def construct_class(voices_per_language)
|
92
|
+
text_hashes = voices_per_language.keys.sort.map do |key|
|
93
|
+
"".rjust(6) + ":" + key.to_s.ljust(8) + "=>".ljust(4) + voices_per_language[key].inspect
|
94
|
+
end
|
95
|
+
|
96
|
+
klass_text = <<-CLASS
|
97
|
+
module Acapela
|
98
|
+
module Voices
|
99
|
+
PER_LANGUAGE = {
|
100
|
+
#{text_hashes.join(",\n")}
|
101
|
+
}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
CLASS
|
105
|
+
klass_text
|
106
|
+
end
|
107
|
+
|
108
|
+
def write_klass(klass)
|
109
|
+
klass_dir = File.dirname(DEFAULT_VOICES_RUBY_FILE)
|
110
|
+
|
111
|
+
if !File.exist?(klass_dir)
|
112
|
+
FileUtils.mkdir_p(klass_dir)
|
113
|
+
end
|
114
|
+
|
115
|
+
File.open(DEFAULT_VOICES_RUBY_FILE, 'w') do |f|
|
116
|
+
f.write(klass)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def main
|
121
|
+
if ARGV[0] == 'php'
|
122
|
+
save_uri_to_file_if_missing(PHP_VOICE_ARRAY_URL, PHP_VOICE_ARRAY_FILE)
|
123
|
+
voices = php_array_to_ruby_array_of_hashes(File.read(PHP_VOICE_ARRAY_FILE))
|
124
|
+
else
|
125
|
+
save_uri_to_file_if_missing(VOICES_ENUMERATOR_URL, VOICES_ENUMERATOR_FILE)
|
126
|
+
voices = enumerator_text_to_ruby_array_of_hashes(File.read(VOICES_ENUMERATOR_FILE))
|
127
|
+
end
|
128
|
+
|
129
|
+
voices_per_language = voice_array_to_voices_per_language(voices)
|
130
|
+
klass = construct_class(voices_per_language)
|
131
|
+
write_klass(klass)
|
132
|
+
puts "Done."
|
133
|
+
end
|
134
|
+
|
135
|
+
if $0 == __FILE__
|
136
|
+
main
|
137
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
3
|
+
trap("SIGINT") do
|
4
|
+
puts "Stopping..."
|
5
|
+
@interrupted = true
|
6
|
+
end
|
7
|
+
|
8
|
+
SAMPLE_SENTENCES = {
|
9
|
+
:ar => "اسمي {name} وأنا أتكلم يمكن.", # Arabic (Saudi Arabia)
|
10
|
+
:ca => "El meu nom és {name} i que pot parlar.", # Catalan (Spain)
|
11
|
+
:cs => "Mé jméno je {name} a mohu mluvit.", # Czech
|
12
|
+
:da => "Mit navn er {name}, og jeg kan tale.", # Danish
|
13
|
+
:de => "Mein Name ist {name}, und ich kann sprechen.", # German
|
14
|
+
:el => "Το όνομά μου είναι {name}, και μπορώ να μιλήσω.", # Greek
|
15
|
+
:en => "My name is {name} and I can talk.", # English
|
16
|
+
:en_GB => "My name is {name} and I live in Great Britain.", # English (Great Britain)
|
17
|
+
:en_IN => "My name is {name} and I live in India.", # English (India)
|
18
|
+
:en_US => "My name is {name} and I live in the United States.", # English (US)
|
19
|
+
:es => "Mi nombre es {name}, y puede hablar.", # Spanish
|
20
|
+
:es_ES => "Mi nombre es {name} y vivo en España.", # Spanish (Spain)
|
21
|
+
:es_US => "Mi nombre es {name} y yo vivimos en Estados Unidos.", # Spanish (US?)
|
22
|
+
:fi => "Nimeni on {name}, ja voin puhua.", # Finnish
|
23
|
+
:fr => "Mon nom est {name}, et je peux parler.", # French
|
24
|
+
:fr_BE => "Mon nom est {name} et je vis en Belgique.", # French (Belgium)
|
25
|
+
:fr_CA => "Mon nom est {name} et je vis au Canada.", # French (Canada)
|
26
|
+
:fr_FR => "Mon nom est {name} et je vis en France.", # French (French)
|
27
|
+
:gb => "Mitt namn är {name} och jag bor i Gotenburg.", # Gotenburg (Swedish), should be sv_SV_gotenburg ?
|
28
|
+
:it => "Il mio nome è {name}, e posso parlare.", # Italian
|
29
|
+
:ja => "私の名前は{name}です。私は話せます。", # Japanese
|
30
|
+
:nl => "Mijn naam is {name}, en ik spreek kan.", # Dutch
|
31
|
+
:nl_BE => "Mijn naam is {name} en ik woon in België.", # Dutch (Belgium)
|
32
|
+
:nl_NL => "Mijn naam is {name} en ik woon in Nederland.", # Dutch (Netherlands)
|
33
|
+
:no => "Mitt navn er {name}, og jeg kan snakke.", # Norwegian
|
34
|
+
:pl => "Nazywam się {name}, i mogę mówić.", # Polish
|
35
|
+
:pt => "Meu nome é {name}, e eu posso falar.", # Portuguese
|
36
|
+
:pt_BR => "Meu nome é {name} e eu vivo no Brasil.", # Portuguese (Brazil)
|
37
|
+
:pt_PT => "Meu nome é {name} e eu vivo em Portugal.", # Portuguese (Portugal)
|
38
|
+
:ru => "Меня зовут {name}, и я могу сказать.", # Russia
|
39
|
+
:sc => "Mitt namn är {name} och jag bor i Scania.", # Scanian (Sweden), should be sv_SE_scania
|
40
|
+
:sv => "Mitt namn är {name}, och jag kan tala.", # Swedish
|
41
|
+
:sv_FI => "Mitt namn är {name} och jag bor i Finland.", # Swedish (Finland)
|
42
|
+
:sv_SE => "Mitt namn är {name} och jag bor i Sverige.", # Swedish (Sweden)
|
43
|
+
:tr => "Benim adım {name} ve konuşamıyorum.", # Turkish
|
44
|
+
:zh => "我的名字是{name},我可以說。", # Chinese
|
45
|
+
}
|
46
|
+
|
47
|
+
require 'acapela'
|
48
|
+
include Acapela::Scripts
|
49
|
+
|
50
|
+
service = Acapela::VoiceService.new(Acapela.config)
|
51
|
+
FileUtils.mkdir_p(TEST_VOICES_DIR)
|
52
|
+
|
53
|
+
Acapela::Voices::PER_LANGUAGE.each do |language, voice_gender|
|
54
|
+
voice_gender.each do |gender, speakers|
|
55
|
+
speakers.each do |speaker|
|
56
|
+
begin
|
57
|
+
if text = SAMPLE_SENTENCES[language.to_sym]
|
58
|
+
text = text.gsub('{name}', speaker)
|
59
|
+
destination_file = File.join(TEST_VOICES_DIR, "#{language}_#{speaker}.mp3")
|
60
|
+
if !File.exists?(destination_file)
|
61
|
+
response = service.generate_sound(text, :speaker => speaker)
|
62
|
+
mp3_file = response.download_to_tempfile
|
63
|
+
FileUtils.cp(mp3_file.path, destination_file)
|
64
|
+
mp3_file.close!
|
65
|
+
puts "Generated: #{text}"
|
66
|
+
end
|
67
|
+
else
|
68
|
+
puts "No text for: #{speaker} in #{language}"
|
69
|
+
end
|
70
|
+
rescue => err
|
71
|
+
puts "FAILED WITH: #{language}, #{gender}, #{speakers.inspect}, #{speaker}\nReason: #{err.message}"
|
72
|
+
end
|
73
|
+
|
74
|
+
exit 0 if @interrupted
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Acapela::Config, 'When created' do
|
4
|
+
|
5
|
+
default_config = YAML.load_file('config/acapela.yml')
|
6
|
+
default_config['login'].should_not == nil
|
7
|
+
default_config['application'].should_not == nil
|
8
|
+
default_config['password'].should_not == nil
|
9
|
+
|
10
|
+
context 'with a given login, application and password' do
|
11
|
+
it 'should report those and other default values' do
|
12
|
+
config = Acapela::Config.new(default_config['login'], default_config['application'], default_config['password'])
|
13
|
+
config.version.should == Acapela::Config::DEFAULT_VERSION
|
14
|
+
config.protocol.should == Acapela::Config::DEFAULT_PROTOCOL
|
15
|
+
config.target_url.should == URI.parse(Acapela::Config::DEFAULT_TARGET_URL)
|
16
|
+
config.environment.should be_nil
|
17
|
+
|
18
|
+
config.login.should == default_config['login']
|
19
|
+
config.application.should == default_config['application']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'from a config file' do
|
24
|
+
it 'should default to the local config/acapela.yml' do
|
25
|
+
config = Acapela::Config.read
|
26
|
+
config.login.should == default_config['login']
|
27
|
+
config.application.should == default_config['application']
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should accept an arbitary config file argument' do
|
31
|
+
config = Acapela::Config.read(test_config)
|
32
|
+
config.login.should_not be_nil
|
33
|
+
config.application.should_not be_nil
|
34
|
+
|
35
|
+
config.login.should_not == default_config['login']
|
36
|
+
config.application.should_not == default_config['application']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
describe Acapela::Response do
|
5
|
+
|
6
|
+
after(:each) do
|
7
|
+
Acapela::Response.mock_on
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should throw an error when created with bogus parameters' do
|
11
|
+
expect do
|
12
|
+
Acapela::Response.new('stuff=yeah')
|
13
|
+
end.to raise_error(Acapela::Error, Acapela::Response::ERROR_UNEXPECTED_RESPONSE.message)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should report sound related stats' do
|
17
|
+
response = Acapela::Response.new(Acapela::Mocks::EXAMPLE_ACAPELA_RESPONSE_OK)
|
18
|
+
response.url.should be_kind_of(URI)
|
19
|
+
response.time.should be_kind_of(Float)
|
20
|
+
response.size.should be_kind_of(Integer)
|
21
|
+
response.id.should be_kind_of(String)
|
22
|
+
response.time.should == 1407.94
|
23
|
+
response.size.should == 9098
|
24
|
+
response.id.should == '210375264_cacefb1f8f862'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should download and return a tempfile with the object from the response url' do
|
28
|
+
response = Acapela::Response.new(Acapela::Mocks::EXAMPLE_ACAPELA_RESPONSE_OK)
|
29
|
+
|
30
|
+
file = response.download_to_tempfile
|
31
|
+
file.should be_kind_of(Tempfile)
|
32
|
+
file.rewind
|
33
|
+
contents = file.read
|
34
|
+
file.close
|
35
|
+
contents.should == File.read(Acapela::Mocks::RESPONSE_TEST_FILE)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should raise an error when file download fails with non 200 response' do
|
39
|
+
Acapela::Response.mock_off # Returns the test file contents.
|
40
|
+
response = Acapela::Response.new(Acapela::Mocks::EXAMPLE_ACAPELA_RESPONSE_OK)
|
41
|
+
Net::HTTP.expects(:get_response).returns(Net::HTTPNotFound.new("Body?", 404, "Something went wrong."))
|
42
|
+
expect do
|
43
|
+
response.download_to_tempfile
|
44
|
+
end.to raise_error(Acapela::Error)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Acapela::VoiceService do
|
4
|
+
|
5
|
+
text_string = 'This is a test'
|
6
|
+
mock_service = true
|
7
|
+
if mock_service
|
8
|
+
require 'acapela/mocks/voice_service'
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
Acapela::VoiceService.expect_ok_response
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'When create without parameters' do
|
16
|
+
service = Acapela::VoiceService.new
|
17
|
+
|
18
|
+
it 'should use the default config' do
|
19
|
+
service.config.should be_kind_of(Acapela::Config)
|
20
|
+
service.config.should == Acapela.config
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should allow generation of MP3 sound' do
|
24
|
+
sound_url = service.generate_sound(text_string, :language => :en)
|
25
|
+
sound_url.should be_kind_of(Acapela::Response)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should throw an error when created with nil config' do
|
30
|
+
expect do
|
31
|
+
service = Acapela::VoiceService.new(nil)
|
32
|
+
end.to raise_error(Acapela::Error, Acapela::VoiceService::ERROR_MISSING_CONFIG.message)
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'When created with config with invalid login/password' do
|
36
|
+
service = Acapela::VoiceService.new(Acapela::Config.read(test_config))
|
37
|
+
|
38
|
+
it 'should raise the reported access error' do
|
39
|
+
Acapela::VoiceService.expect_access_denied_response if mock_service
|
40
|
+
expect do
|
41
|
+
service.generate_sound(text_string, :language => :en)
|
42
|
+
end.to raise_error(Acapela::ServiceError, "Code: ACCESS_DENIED_ERROR, Message: Invalid identifiers")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'When generating sound' do
|
47
|
+
it 'should allow low quality setting' do
|
48
|
+
service = Acapela::VoiceService.new
|
49
|
+
if mock_service
|
50
|
+
service.generate_sound(text_string, :speaker => 'tracy', :quality => :low)
|
51
|
+
Acapela::VoiceService.last_posted_params[:req_voice].should == 'tracy8k'
|
52
|
+
else
|
53
|
+
pending("Without mock, the returned file quality needs to be confirmed")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should allow custom voice setting' do
|
58
|
+
service = Acapela::VoiceService.new
|
59
|
+
if mock_service
|
60
|
+
voice = Acapela::Voice.new('johnny', :male, 'en')
|
61
|
+
service.generate_with_voice(text_string, voice)
|
62
|
+
Acapela::VoiceService.last_posted_params[:req_voice].should == 'johnny22k'
|
63
|
+
else
|
64
|
+
pending("Without mock, the returned file quality needs to be confirmed")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Acapela::Voice do
|
4
|
+
|
5
|
+
context 'When created' do
|
6
|
+
speaker_name = 'tom'
|
7
|
+
test_voice = Acapela::Voice.new(speaker_name, Acapela::Voice::GENDER_FEMALE)
|
8
|
+
|
9
|
+
it 'with name and gender should report default values' do
|
10
|
+
test_voice.speaker.should == speaker_name
|
11
|
+
test_voice.gender.should == Acapela::Voice::GENDER_FEMALE
|
12
|
+
test_voice.quality.should == Acapela::Voice::QUALITY_HIGH
|
13
|
+
test_voice.languages.should be_kind_of(Set)
|
14
|
+
test_voice.languages.should be_empty
|
15
|
+
test_voice.language.should be_nil
|
16
|
+
test_voice.id.should == "#{speaker_name}#{test_voice.quality}"
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should allow flipping quality setting' do
|
20
|
+
test_voice.low_quality!
|
21
|
+
test_voice.quality.should == Acapela::Voice::QUALITY_LOW
|
22
|
+
test_voice.high_quality!
|
23
|
+
test_voice.quality.should == Acapela::Voice::QUALITY_HIGH
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'With a set of defined voices' do
|
28
|
+
it 'should throw an error if the voice does not exist' do
|
29
|
+
expect do
|
30
|
+
Acapela::Voice.named_voice('Birkir')
|
31
|
+
end.to raise_error(Acapela::Error, "Voice does not exist.")
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should return the active voice map' do
|
35
|
+
Acapela::Voice.map.should == Acapela::Voices::PER_LANGUAGE
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'should find a voice' do
|
39
|
+
it 'given an existing speaker name' do
|
40
|
+
voice = Acapela::Voice.named_voice('Tracy')
|
41
|
+
voice.speaker.should == 'Tracy'
|
42
|
+
voice.language.should == 'en'
|
43
|
+
voice.gender.should == Acapela::Voice::GENDER_FEMALE
|
44
|
+
voice.languages.to_a.should == ['en']
|
45
|
+
voice.id.should == 'tracy22k'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'given an existing speaker name in lower case' do
|
49
|
+
voice = Acapela::Voice.named_voice('bruno')
|
50
|
+
voice.speaker.should == 'Bruno'
|
51
|
+
voice.gender.should == Acapela::Voice::GENDER_MALE
|
52
|
+
voice.language.should == 'fr'
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'given an existing speaker name as a symbol' do
|
56
|
+
voice = Acapela::Voice.named_voice(:rosa)
|
57
|
+
voice.speaker.should == 'Rosa'
|
58
|
+
voice.gender.should == Acapela::Voice::GENDER_FEMALE
|
59
|
+
voice.language.should == 'es'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'should extract a voice when given an option that' do
|
64
|
+
it 'specifies nothing, defaulting to English' do
|
65
|
+
voice = Acapela::Voice.extract_from_options
|
66
|
+
['Tracy', 'Heather', 'Kenny'].should include(voice.speaker)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'specifies a speaker' do
|
70
|
+
voice = Acapela::Voice.extract_from_options(:speaker => 'antoine')
|
71
|
+
voice.speaker.should == 'Antoine'
|
72
|
+
voice.gender.should == Acapela::Voice::GENDER_MALE
|
73
|
+
voice.language.should == 'fr'
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'specifies a language' do
|
77
|
+
voice = Acapela::Voice.extract_from_options(:language => :es)
|
78
|
+
voice.speaker.should == 'Rosa'
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'specifies a language and no gender' do
|
82
|
+
voice = Acapela::Voice.extract_from_options(:language => :fr)
|
83
|
+
voice.should_not be_nil
|
84
|
+
['Antoine', 'Bruno'].should include(voice.speaker)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'specifies a language and gender' do
|
88
|
+
voice = Acapela::Voice.extract_from_options(:language => :es, :gender => Acapela::Voice::GENDER_FEMALE)
|
89
|
+
voice.speaker.should == 'Rosa'
|
90
|
+
|
91
|
+
voice = Acapela::Voice.extract_from_options(:language => :es, :gender => Acapela::Voice::GENDER_MALE)
|
92
|
+
voice.should be_nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'Allows for voices to be overriden or changed' do
|
98
|
+
after(:each) do
|
99
|
+
Acapela::Voice.reset_map
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should allow removing specific entries' do
|
103
|
+
Acapela::Voice.map[:en][:male].first.should == 'Kenny'
|
104
|
+
Acapela::Voice.override_map(:en => nil)
|
105
|
+
Acapela::Voice.map[:en].should be_nil
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should allow mapping one entry to another' do
|
109
|
+
Acapela::Voice.map[:en][:male].first.should == 'Kenny'
|
110
|
+
Acapela::Voice.override_map(:en => :fr)
|
111
|
+
Acapela::Voice.map[:en][:male].first.should == 'Antoine'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should allow replacing an entry' do
|
115
|
+
Acapela::Voice.map[:en][:male].first.should == 'Kenny'
|
116
|
+
Acapela::Voice.override_map(:en => {:male => ['Johnny']})
|
117
|
+
Acapela::Voice.map[:en][:male].should == ['Johnny']
|
118
|
+
Acapela::Voice.map[:en][:female].first.should == 'Tracy'
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should allow adding an entry' do
|
122
|
+
Acapela::Voice.map[:is].should be_nil
|
123
|
+
Acapela::Voice.override_map(:is => {:male => ['Birkir'], :female => ['Harpa']})
|
124
|
+
Acapela::Voice.map[:is][:male].first.should == 'Birkir'
|
125
|
+
Acapela::Voice.map[:is][:female].first.should == 'Harpa'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Acapela do
|
4
|
+
it 'should reference a config file' do
|
5
|
+
config = Acapela.config
|
6
|
+
|
7
|
+
config.should_not be_nil
|
8
|
+
config.should be_a_kind_of(Acapela::Config)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should allow setting of the config file' do
|
12
|
+
default_config = Acapela.config
|
13
|
+
|
14
|
+
begin
|
15
|
+
test_config = Acapela::Config.read(test_config)
|
16
|
+
|
17
|
+
Acapela.config = test_config
|
18
|
+
Acapela.config.should == test_config
|
19
|
+
ensure
|
20
|
+
Acapela.config = default_config
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
CONFIG_DIR = 'config'
|
2
|
+
CONFIG_FILE = File.join(CONFIG_DIR, 'acapela.yml')
|
3
|
+
DEFAULT_CONFIG_FILE = <<-YAML
|
4
|
+
login: 'EVAL_VAAS'
|
5
|
+
application: 'MY_APPLICATION'
|
6
|
+
password: 'MY_PASSWORD'
|
7
|
+
YAML
|
8
|
+
|
9
|
+
def create_missing_config_file
|
10
|
+
unless File.exists?(CONFIG_FILE)
|
11
|
+
Dir.mkdir(CONFIG_DIR)
|
12
|
+
File.open(CONFIG_FILE, 'w') do |f|
|
13
|
+
f.write(DEFAULT_CONFIG_FILE)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_config
|
19
|
+
File.join('spec', 'acapela', 'test_config.yml')
|
20
|
+
end
|
21
|
+
|
22
|
+
create_missing_config_file
|
23
|
+
|
24
|
+
require File.join(File.dirname(__FILE__), '..', 'init')
|
25
|
+
require 'bundler'
|
26
|
+
Bundler.require(:development)
|
27
|
+
require 'acapela/mocks'
|
28
|
+
|
29
|
+
def silence_warnings
|
30
|
+
begin
|
31
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
32
|
+
yield
|
33
|
+
ensure
|
34
|
+
$VERBOSE = old_verbose
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Some test voices.
|
39
|
+
silence_warnings do
|
40
|
+
Acapela::Voices::PER_LANGUAGE = {
|
41
|
+
:en => {:female=>["Tracy", "Heather"], :male=>["Kenny"]},
|
42
|
+
:es => {:female=>["Rosa"], :male=>[]},
|
43
|
+
:fr => {:female=>[], :male=>["Antoine", "Bruno"]},
|
44
|
+
}
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acapela
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Birkir A. Barkarson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: backports
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.6.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.6.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: mocha
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.9.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.9.0
|
62
|
+
description: Ruby interface to Acapela's API for generating speech from text. More
|
63
|
+
info http://www.acapela-vaas.com/
|
64
|
+
email:
|
65
|
+
- birkirb@stoicviking.net
|
66
|
+
executables: []
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- .gitignore
|
71
|
+
- .rspec
|
72
|
+
- Gemfile
|
73
|
+
- Rakefile
|
74
|
+
- acapela.gemspec
|
75
|
+
- init.rb
|
76
|
+
- lib/acapela.rb
|
77
|
+
- lib/acapela/config.rb
|
78
|
+
- lib/acapela/constants.rb
|
79
|
+
- lib/acapela/error.rb
|
80
|
+
- lib/acapela/mocks.rb
|
81
|
+
- lib/acapela/mocks/constants.rb
|
82
|
+
- lib/acapela/mocks/response.rb
|
83
|
+
- lib/acapela/mocks/test_file.mp3
|
84
|
+
- lib/acapela/mocks/voice_service.rb
|
85
|
+
- lib/acapela/response.rb
|
86
|
+
- lib/acapela/voice.rb
|
87
|
+
- lib/acapela/voice_service.rb
|
88
|
+
- lib/acapela/voices/default.rb
|
89
|
+
- script/create_voices.rb
|
90
|
+
- script/test_voices.rb
|
91
|
+
- spec/acapela/config_spec.rb
|
92
|
+
- spec/acapela/response_spec.rb
|
93
|
+
- spec/acapela/test_config.yml
|
94
|
+
- spec/acapela/voice_service_spec.rb
|
95
|
+
- spec/acapela/voice_spec.rb
|
96
|
+
- spec/acapela_spec.rb
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
homepage: https://github.com/birkirb/acapela
|
99
|
+
licenses: []
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project: acapela
|
118
|
+
rubygems_version: 1.8.24
|
119
|
+
signing_key:
|
120
|
+
specification_version: 3
|
121
|
+
summary: Generate speech from the Acapela text to voice service.
|
122
|
+
test_files: []
|
123
|
+
has_rdoc:
|