1 package org.daisy.pipeline.tts;
2
3 import java.util.Collection;
4 import java.util.EnumSet;
5 import java.util.HashMap;
6 import java.util.IllformedLocaleException;
7 import java.util.List;
8 import java.util.Locale;
9 import java.util.Map;
10 import java.util.Optional;
11 import java.util.regex.Pattern;
12
13 import com.google.common.base.Preconditions;
14 import com.google.common.base.Splitter;
15 import com.google.common.collect.Lists;
16
17 import org.daisy.pipeline.css.speech.VoiceFamilyList;
18
19 public class VoiceInfo {
20
21 public Gender gender;
22 public String voiceName;
23 public String voiceEngine;
24 public LanguageRange language;
25 public float priority;
26
27 public static Gender NO_DEFINITE_GENDER = Gender.ANY;
28
29 public enum Gender {
30 ANY("*", "neutral"),
31 MALE_ADULT("male", "man", "male-adult"),
32 MALE_CHILD("boy", "male-young", "male-child"),
33 MALE_ELDERLY("man-old", "male-old", "male-elder", "man-elder", "male-elderly", "man-elderly"),
34 FEMALE_CHILD("girl", "female-young", "female-child"),
35 FEMALE_ADULT("woman", "female", "female-adult"),
36 FEMALE_ELDERLY("woman-old", "female-old", "woman-elder", "female-elder", "woman-elderly", "female-elderly");
37
38 private final List<String> variants;
39
40 private Gender(String... variants) {
41 this.variants = Lists.newArrayList(variants);
42 }
43
44 private static final Map<String, Gender> lookup = new HashMap<String, Gender>();
45
46 static {
47 Splitter splitter = Splitter.on('-').omitEmptyStrings();
48 for (Gender gender : EnumSet.allOf(Gender.class)) {
49 for (String variant : gender.variants) {
50 lookup.put(variant, gender);
51 List<String> parts = splitter.splitToList(variant);
52 if (parts.size() > 1) {
53 lookup.put(parts.get(0) + parts.get(1), gender);
54 lookup.put(parts.get(1) + parts.get(0), gender);
55 lookup.put(parts.get(0) + '-' + parts.get(1), gender);
56 lookup.put(parts.get(1) + '-' + parts.get(0), gender);
57 }
58 }
59 }
60 }
61
62 public static Gender of(String gender) {
63 return gender == null ? null : lookup.get(gender.toLowerCase().replace("_", "-"));
64 }
65
66 public static Gender of(VoiceFamilyList.Gender gender) {
67 return Gender.of(gender.toString());
68 }
69
70 public static Gender of(VoiceFamilyList.Gender gender, Optional<VoiceFamilyList.Age> age) {
71 switch (gender) {
72 case NEUTRAL:
73 return Gender.ANY;
74 case MALE:
75 case FEMALE:
76 default:
77 if (age.isPresent())
78 switch (age.get()) {
79 case CHILD:
80 return Gender.of("" + gender + "-child");
81 case OLD:
82 return Gender.of("" + gender + "-eldery");
83 case YOUNG:
84 default:
85 }
86 }
87 return Gender.of(gender);
88 }
89
90 @Override
91 public String toString() {
92 return this == ANY ? "*" : super.toString().toLowerCase().replace("_", "-");
93 }
94 }
95
96 public static class LanguageRange {
97
98 private static final Pattern LANGUAGE_RANGE = Pattern.compile("(\\p{Alpha}{1,8}|\\*)(-(\\p{Alnum}{1,8}|\\*))*");
99
100 private final String range;
101 private final Locale primaryLanguageSubTag;
102 private final int specificity;
103
104
105
106
107 public LanguageRange(String range) {
108 if (range == null)
109 throw new IllegalArgumentException();
110 if ("mul".equalsIgnoreCase(range)) {
111 this.range = "*";
112 primaryLanguageSubTag = null;
113 specificity = 0;
114 } else {
115 if (!LANGUAGE_RANGE.matcher(range).matches())
116 throw new IllegalArgumentException("Not a valid language range: " + range);
117 if (range.equals("*")) {
118 primaryLanguageSubTag = null;
119 specificity = 0;
120 } else if (!range.contains("*"))
121 try {
122 Locale locale = (new Locale.Builder()).setLanguageTag(range).build();
123 primaryLanguageSubTag = new Locale(locale.getLanguage());
124 range = locale.toLanguageTag();
125 specificity = range.split("-").length;
126 } catch (IllformedLocaleException e) {
127 throw new IllegalArgumentException("Not a valid language range: '" + range, e);
128 }
129 else
130 throw new IllegalArgumentException("FIXME: Extended language ranges not implemented yet");
131 this.range = range;
132 }
133 }
134
135 public LanguageRange(Locale locale) {
136 this(locale == null ? null : locale.toLanguageTag());
137 }
138
139
140
141
142 public boolean matches(Locale locale) {
143 if (locale == null)
144 throw new IllegalArgumentException();
145 if (this.range.equals("*"))
146 return true;
147 String tag = locale.toLanguageTag().toLowerCase();
148 String range = this.range.toLowerCase();
149 return tag.equals(range) || tag.startsWith(range + "-");
150 }
151
152
153
154
155 public static boolean matches(Collection<LanguageRange> languageList, Locale locale) {
156 if (locale == null)
157 throw new IllegalArgumentException();
158 for (LanguageRange l : languageList)
159 if (l.matches(locale))
160 return true;
161 return false;
162 }
163
164
165
166
167 public Locale getPrimaryLanguageSubTag() {
168 return primaryLanguageSubTag;
169 }
170
171
172
173
174 public int getSpecificity() {
175 return specificity;
176 }
177
178 @Override
179 public String toString() {
180 return range;
181 }
182
183
184
185
186 public static String toString(Collection<LanguageRange> languageList) {
187 StringBuilder s = new StringBuilder();
188 boolean first = true;
189 for (LanguageRange l : languageList) {
190 if (!first) s.append(", ");
191 s.append(l);
192 first = false;
193 }
194 return s.toString();
195 }
196
197 @Override
198 public int hashCode() {
199 final int prime = 31;
200 int result = 1;
201 result = prime * result + range.toLowerCase().hashCode();
202 return result;
203 }
204
205 @Override
206 public boolean equals(Object obj) {
207 if (this == obj)
208 return true;
209 if (obj == null)
210 return false;
211 if (getClass() != obj.getClass())
212 return false;
213 LanguageRange other = (LanguageRange)obj;
214 if (!range.equalsIgnoreCase(other.range))
215 return false;
216 return true;
217 }
218 }
219
220
221
222
223 public VoiceInfo(String voiceEngine, String voiceName, String language, Gender gender, float priority)
224 throws IllegalArgumentException {
225 this(voiceEngine, voiceName, new LanguageRange(language), gender, priority);
226 }
227
228 public VoiceInfo(String voiceEngine, String voiceName, Locale language, Gender gender, float priority)
229 throws IllegalArgumentException {
230 this(voiceEngine, voiceName, new LanguageRange(language), gender, priority);
231 }
232
233 public VoiceInfo(String voiceEngine, String voiceName, LanguageRange language, Gender gender, float priority)
234 throws IllegalArgumentException {
235 Preconditions.checkNotNull(voiceEngine);
236 Preconditions.checkNotNull(voiceName);
237 Preconditions.checkNotNull(language);
238 Preconditions.checkNotNull(gender);
239 this.voiceEngine = voiceEngine;
240 this.voiceName = voiceName;
241 this.language = language;
242 this.gender = gender;
243 this.priority = priority;
244 }
245
246 @Override
247 public int hashCode() {
248 final int prime = 31;
249 int result = 1;
250 result = prime * result + ((gender == null) ? 0 : gender.hashCode());
251 result = prime * result + ((language == null) ? 0 : language.hashCode());
252 result = prime * result + Float.floatToIntBits(priority);
253 result = prime * result + ((voiceEngine == null) ? 0 : voiceEngine.hashCode());
254 result = prime * result + ((voiceName == null) ? 0 : voiceName.hashCode());
255 return result;
256 }
257
258 @Override
259 public boolean equals(Object obj) {
260 if (this == obj)
261 return true;
262 if (obj == null)
263 return false;
264 if (getClass() != obj.getClass())
265 return false;
266 VoiceInfo other = (VoiceInfo) obj;
267 if (gender != other.gender)
268 return false;
269 if (language == null) {
270 if (other.language != null)
271 return false;
272 } else if (!language.equals(other.language))
273 return false;
274 if (Float.floatToIntBits(priority) != Float.floatToIntBits(other.priority))
275 return false;
276 if (voiceEngine == null) {
277 if (other.voiceEngine != null)
278 return false;
279 } else if (!voiceEngine.equals(other.voiceEngine))
280 return false;
281 if (voiceName == null) {
282 if (other.voiceName != null)
283 return false;
284 } else if (!voiceName.equals(other.voiceName))
285 return false;
286 return true;
287 }
288
289 @Override
290 public String toString() {
291 String s = "";
292 if (voiceEngine != null) {
293 s += ("engine=" + voiceEngine);
294 }
295 if (voiceName != null) {
296 if (s.length() > 0) s += ", ";
297 s += ("name=" + voiceName);
298 }
299 if (language != null) {
300 if (s.length() > 0) s += ", ";
301 s += ("lang=" + language);
302 }
303 if (gender != null) {
304 if (s.length() > 0) s += ", ";
305 s += ("gender=" + gender);
306 }
307 {
308 if (s.length() > 0) s += ", ";
309 s += ("priority=" + priority);
310 }
311 s = "VoiceInfo[" + s + "]";
312 return s;
313 }
314 }