ispeech 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,121 @@
1
+ require 'set'
2
+
3
+ module Ispeech
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}"
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,65 @@
1
+ require 'net/http'
2
+
3
+ module Ispeech
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 = Ispeech.config)
13
+ if config.is_a?(Ispeech::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
+ # API Defaults:
36
+ # :bitrate => 48,
37
+ # :speed => 0,
38
+ # :startpadding => 0,
39
+ # :endpadding => 0,
40
+ # :pitch => 100,
41
+ # :format => 'mp3',
42
+ # :bitdepth => 16,
43
+ # :filename => 'rest'
44
+ :voice => voice.id,
45
+ :frequency => voice.quality,
46
+ :text => text,
47
+ }
48
+
49
+ with_action(:convert, params)
50
+ end
51
+
52
+ def with_action(action, params)
53
+ params[:action] = action
54
+ Response.new(post(params))
55
+ end
56
+
57
+ private
58
+
59
+ def post(params)
60
+ params[:apikey] = @config.api_key
61
+ Net::HTTP.post_form(@config.target_url, params)
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,42 @@
1
+ module Ispeech
2
+ module Voices
3
+ PER_LANGUAGE = {
4
+ :ar => {:female=>[], :male=>["arabicmale"]},
5
+ :ca => {:female=>["eurcatalanfemale"], :male=>[]},
6
+ :cs => {:female=>["eurczechfemale"], :male=>[]},
7
+ :da => {:female=>["eurdanishfemale"], :male=>[]},
8
+ :de => {:female=>["eurgermanfemale"], :male=>["eurgermanmale"]},
9
+ :el => {:female=>["eurgreekfemale"], :male=>[]},
10
+ :en => {:female=>["usenglishfemale", "caenglishfemale", "ukenglishfemale", "auenglishfemale"], :male=>["usenglishmale", "ukenglishmale"]},
11
+ :en_AU => {:female=>["auenglishfemale"], :male=>[]},
12
+ :en_CA => {:female=>["caenglishfemale"], :male=>[]},
13
+ :en_GB => {:female=>[], :male=>["ukenglishmale"]},
14
+ :en_UK => {:female=>["ukenglishfemale"], :male=>[]},
15
+ :en_US => {:female=>["usenglishfemale"], :male=>[]},
16
+ :es => {:female=>["eurspanishfemale", "usspanishfemale"], :male=>["usspanishmale", "eurspanishmale"]},
17
+ :es_ES => {:female=>["eurspanishfemale"], :male=>["eurspanishmale"]},
18
+ :es_MX => {:female=>[], :male=>["usspanishmale"]},
19
+ :fi => {:female=>["eurfinnishfemale"], :male=>[]},
20
+ :fr => {:female=>["cafrenchfemale", "eurfrenchfemale"], :male=>["cafrenchmale", "eurfrenchmale"]},
21
+ :fr_CA => {:female=>["cafrenchfemale"], :male=>["cafrenchmale"]},
22
+ :fr_FR => {:female=>["eurfrenchfemale"], :male=>["eurfrenchmale"]},
23
+ :hu => {:female=>["huhungarianfemale"], :male=>[]},
24
+ :it => {:female=>["euritalianfemale"], :male=>["euritalianmale"]},
25
+ :ja => {:female=>["jpjapanesefemale"], :male=>["jpjapanesemale"]},
26
+ :ko => {:female=>["krkoreanfemale"], :male=>["krkoreanmale"]},
27
+ :ko_KR => {:female=>[], :male=>["krkoreanmale"]},
28
+ :nl => {:female=>["eurdutchfemale"], :male=>[]},
29
+ :no => {:female=>["eurnorwegianfemale"], :male=>[]},
30
+ :pl => {:female=>["eurpolishfemale"], :male=>[]},
31
+ :pt => {:female=>["brportuguesefemale", "eurportuguesefemale"], :male=>["eurportuguesemale"]},
32
+ :pt_BR => {:female=>["brportuguesefemale"], :male=>[]},
33
+ :pt_PT => {:female=>["eurportuguesefemale"], :male=>["eurportuguesemale"]},
34
+ :ru => {:female=>["rurussianfemale"], :male=>["rurussianmale"]},
35
+ :sv => {:female=>["swswedishfemale"], :male=>[]},
36
+ :tr => {:female=>["eurturkishfemale"], :male=>["eurturkishmale"]},
37
+ :zh => {:female=>["hkchinesefemale", "twchinesefemale", "chchinesefemale"], :male=>["chchinesemale"]},
38
+ :zh_HK => {:female=>["hkchinesefemale", "chchinesefemale"], :male=>[]},
39
+ :zh_TW => {:female=>["twchinesefemale"], :male=>[]}
40
+ }
41
+ end
42
+ end
@@ -0,0 +1,143 @@
1
+ require 'net/http'
2
+ require 'set'
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'ispeech'
6
+ include Ispeech::Scripts
7
+
8
+ DEFAULT_VOICES_RUBY_FILE = File.join('lib', 'ispeech', 'voices', 'default.rb')
9
+ VOICE_KEY_FIELDS = /voice-([^-\d]*)-?(\d+)-?\d*$/
10
+
11
+ def save_uri_to_file_if_missing(filename)
12
+ if !File.exists?(filename)
13
+ FileUtils.mkdir_p(LOCAL_TEMP_DIR)
14
+ response = Ispeech.voice_service.with_action('information', :output => :json)
15
+ file = response.download_to_tempfile
16
+ FileUtils.mv(file.path, filename)
17
+ end
18
+ end
19
+
20
+ def convert_text_to_voices(text)
21
+ raw_voices = JSON.parse(text)
22
+ structured_voices = Hash.new { |h,k| h[k] = Hash.new}
23
+
24
+ raw_voices.each do |key,value|
25
+ if match = VOICE_KEY_FIELDS.match(key)
26
+ if match[1].empty?
27
+ structured_voices[match[2].to_i][:speaker] = value
28
+ else
29
+ structured_voices[match[2].to_i][match[1].to_sym] = value
30
+ end
31
+ else
32
+ if $VERBOSE
33
+ puts "Failed to parse: #{key}=#{value}"
34
+ end
35
+ end
36
+ end
37
+
38
+ voices = Array.new
39
+ structured_voices.each do |number, hash|
40
+ voices << {'language_locale' => hash[:locale], 'speaker' => hash[:speaker], 'gender' => hash[:gender]}
41
+ end
42
+
43
+ voices
44
+ end
45
+
46
+ def gender_code_to_symbol(gender)
47
+ case gender.downcase
48
+ when 'f','female'
49
+ Ispeech::Voice::GENDER_FEMALE
50
+ when 'm','male'
51
+ Ispeech::Voice::GENDER_MALE
52
+ else
53
+ raise "Unknown gender code: #{gender}"
54
+ end
55
+ end
56
+
57
+ def qualified_locale(locale)
58
+ split = locale.split('-') || locale.split('_')
59
+
60
+ if split.size > 1
61
+ "#{split.first}_#{split.last.upcase}"
62
+ else
63
+ split.first
64
+ end
65
+ end
66
+
67
+ def simple_language_code(locale)
68
+ locale.split('_').first
69
+ end
70
+
71
+ class Set
72
+ def inspect
73
+ self.to_a.inspect
74
+ end
75
+ end
76
+
77
+ def voice_array_to_voices_per_language(voices)
78
+ language_voices = Hash.new { |h,v| h[v] = { :female => Set.new, :male => Set.new } }
79
+
80
+ voices.each do |voice_hash|
81
+ gender = gender_code_to_symbol(voice_hash['gender'])
82
+ locale = qualified_locale(voice_hash['language_locale'])
83
+ simple_locale = simple_language_code(locale)
84
+
85
+ language_voices[locale][gender].add(voice_hash['speaker'])
86
+ language_voices[simple_locale][gender].add(voice_hash['speaker'])
87
+ end
88
+
89
+ # Clean up instance where specific locale is same as generic one.
90
+ language_voices.delete_if do |key,value|
91
+ simple_locale = simple_language_code(key)
92
+ if key != simple_locale && language_voices[simple_locale] == value
93
+ true
94
+ else
95
+ false
96
+ end
97
+ end
98
+
99
+ language_voices
100
+ end
101
+
102
+ def construct_class(voices_per_language)
103
+ text_hashes = voices_per_language.keys.sort.map do |key|
104
+ "".rjust(6) + ":" + key.to_s.ljust(8) + "=>".ljust(4) + voices_per_language[key].inspect
105
+ end
106
+
107
+ klass_text = <<-CLASS
108
+ module Ispeech
109
+ module Voices
110
+ PER_LANGUAGE = {
111
+ #{text_hashes.join(",\n")}
112
+ }
113
+ end
114
+ end
115
+ CLASS
116
+ klass_text
117
+ end
118
+
119
+ def write_klass(klass)
120
+ klass_dir = File.dirname(DEFAULT_VOICES_RUBY_FILE)
121
+
122
+ if !File.exist?(klass_dir)
123
+ FileUtils.mkdir_p(klass_dir)
124
+ end
125
+
126
+ File.open(DEFAULT_VOICES_RUBY_FILE, 'w') do |f|
127
+ f.write(klass)
128
+ end
129
+ end
130
+
131
+ def main
132
+ save_uri_to_file_if_missing(VOICES_ENUMERATOR_FILE)
133
+ voices = convert_text_to_voices(File.read(VOICES_ENUMERATOR_FILE))
134
+
135
+ voices_per_language = voice_array_to_voices_per_language(voices)
136
+ klass = construct_class(voices_per_language)
137
+ write_klass(klass)
138
+ puts "Done."
139
+ end
140
+
141
+ if $0 == __FILE__
142
+ main
143
+ end
@@ -0,0 +1,87 @@
1
+ # Encoding: UTF-8
2
+ require 'ispeech'
3
+ include Ispeech::Scripts
4
+
5
+ trap("SIGINT") do
6
+ puts "Stopping..."
7
+ @interrupted = true
8
+ end
9
+
10
+ TEST_VOICES_DIR = File.join(LOCAL_TEMP_DIR, 'voices')
11
+
12
+ SAMPLE_SENTENCES = {
13
+ :ar => "اسمي {name} وأنا أتكلم يمكن.", # Arabic (Saudi Arabia)
14
+ :ca => "El meu nom és {name} i que pot parlar.", # Catalan (Spain)
15
+ :cs => "Mé jméno je {name} a mohu mluvit.", # Czech
16
+ :da => "Mit navn er {name}, og jeg kan tale.", # Danish
17
+ :de => "Mein Name ist {name}, und ich kann sprechen.", # German
18
+ :el => "Το όνομά μου είναι {name}, και μπορώ να μιλήσω.", # Greek
19
+ :en => "My name is {name} and I can talk.", # English
20
+ :en_AU => "Hi, I live in Australia and I can speak English", # English (AU)
21
+ :en_CA => "Hi, I live in Canda and I speak English", # English (CA)
22
+ :en_GB => "Hi, I live in Great Britain and I speak English.", # English (Great Britain)
23
+ :en_IN => "Hi, I live in India and I can speak English", # English (India)
24
+ :en_UK => "Hi, I live in the United Kingdom and I can speak English", # English (UK)
25
+ :en_US => "Hi, I live in the United States and I speak English", # English (US)
26
+ :es => "Mi nombre es {name}, y puede hablar.", # Spanish
27
+ :es_ES => "Mi nombre es {name} y vivo en España.", # Spanish (Spain)
28
+ :es_MX => "Hola, vivo en Mexicon y puedo hablar español", # Spanish (Mexico)
29
+ :es_US => "Mi nombre es {name} y yo vivimos en Estados Unidos.", # Spanish (US?)
30
+ :fi => "Nimeni on {name}, ja voin puhua.", # Finnish
31
+ :fr => "Mon nom est {name}, et je peux parler.", # French
32
+ :fr_BE => "Mon nom est {name} et je vis en Belgique.", # French (Belgium)
33
+ :fr_CA => "Mon nom est {name} et je vis au Canada.", # French (Canada)
34
+ :fr_FR => "Mon nom est {name} et je vis en France.", # French (French)
35
+ :gb => "Mitt namn är {name} och jag bor i Gotenburg.", # Gotenburg (Swedish), should be sv_SV_gotenburg ?
36
+ :hu => "Szia, Magyarországon élek, és én is beszélek magyarul", # Hungarian
37
+ :it => "Il mio nome è {name}, e posso parlare.", # Italian
38
+ :ja => "こんにちは、私は日本に住んでいます。日本語話せます。", # Japanese
39
+ :ko => "안녕하세요, 저는 한국에 살고있는 나는 한국어를 쓸 수 있습니다", # Korean
40
+ :ko_KR => "안녕하세요, 저는 한국에 살고있는 나는 한국어를 쓸 수 있습니다", # Korean
41
+ :nl => "Mijn naam is {name}, en ik spreek kan.", # Dutch
42
+ :nl_BE => "Mijn naam is {name} en ik woon in België.", # Dutch (Belgium)
43
+ :nl_NL => "Mijn naam is {name} en ik woon in Nederland.", # Dutch (Netherlands)
44
+ :no => "Mitt navn er {name}, og jeg kan snakke.", # Norwegian
45
+ :pl => "Nazywam się {name}, i mogę mówić.", # Polish
46
+ :pt => "Meu nome é {name}, e eu posso falar.", # Portuguese
47
+ :pt_BR => "Meu nome é {name} e eu vivo no Brasil.", # Portuguese (Brazil)
48
+ :pt_PT => "Meu nome é {name} e eu vivo em Portugal.", # Portuguese (Portugal)
49
+ :ru => "Меня зовут {name}, и я могу сказать.", # Russia
50
+ :sc => "Mitt namn är {name} och jag bor i Scania.", # Scanian (Sweden), should be sv_SE_scania
51
+ :sv => "Mitt namn är {name}, och jag kan tala.", # Swedish
52
+ :sv_FI => "Mitt namn är {name} och jag bor i Finland.", # Swedish (Finland)
53
+ :sv_SE => "Mitt namn är {name} och jag bor i Sverige.", # Swedish (Sweden)
54
+ :tr => "Benim adım {name} ve konuşamıyorum.", # Turkish
55
+ :zh => "嗨,我住在中国,我能讲普通话", # Chinese (Mainland)
56
+ :zh_HK => "您好,我住在香港,我可以讲广东话", # Chinese (Hong Kong)
57
+ :zh_TW => "您好,我住在台灣,我可以講普通話", # Chinese (Taiwan)
58
+ }
59
+
60
+ service = Ispeech::VoiceService.new(Ispeech.config)
61
+ FileUtils.mkdir_p(TEST_VOICES_DIR)
62
+
63
+ Ispeech::Voices::PER_LANGUAGE.each do |language, voice_gender|
64
+ voice_gender.each do |gender, speakers|
65
+ speakers.each do |speaker|
66
+ begin
67
+ if text = SAMPLE_SENTENCES[language.to_sym]
68
+ text = text.gsub('{name}', 'Job')
69
+ destination_file = File.join(TEST_VOICES_DIR, "#{language}_#{speaker}.mp3")
70
+ if !File.exists?(destination_file)
71
+ response = service.generate_sound(text, :speaker => speaker)
72
+ mp3_file = response.download_to_tempfile
73
+ FileUtils.cp(mp3_file.path, destination_file)
74
+ mp3_file.close!
75
+ puts "Generated: #{text}"
76
+ end
77
+ else
78
+ puts "No text for: #{speaker} in #{language}"
79
+ end
80
+ rescue => err
81
+ puts "FAILED WITH: #{language}, #{gender}, #{speakers.inspect}, #{speaker}\nReason: #{err.message}"
82
+ end
83
+
84
+ exit 0 if @interrupted
85
+ end
86
+ end
87
+ end