acapela 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|