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 		 * @throws IllegalArgumentException if the argument does not represent a valid language range
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 		 * Whether the language range matches the given locale.
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 		 * Whether the language list matches the given locale.
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 		 * The primary language subtag if it is not equal to "*", {@code null} otherwise.
166 		 */
167 		public Locale getPrimaryLanguageSubTag() {
168 			return primaryLanguageSubTag;
169 		}
170 
171 		/**
172 		 * To determine whether a language range is more specific than another language range.
173 		 */
174 		public int getSpecificity() {
175 			return specificity;
176 		}
177 
178 		@Override
179 		public String toString() {
180 			return range;
181 		}
182 
183 		/**
184 		 * Print language list as comma-separated list.
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 	 * @throws IllegalArgumentException if language or gender can not be parsed.
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 }