ispeech 1.0.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.
@@ -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