1 package org.daisy.pipeline.tts;
2
3 import java.io.StringReader;
4 import java.util.ArrayList;
5 import java.util.Collection;
6 import java.util.Collections;
7 import java.util.HashMap;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.concurrent.ConcurrentHashMap;
11
12 import javax.xml.transform.URIResolver;
13 import javax.xml.transform.sax.SAXSource;
14
15 import net.sf.saxon.s9api.DocumentBuilder;
16 import net.sf.saxon.s9api.Processor;
17 import net.sf.saxon.s9api.SaxonApiException;
18 import net.sf.saxon.s9api.XdmNode;
19
20 import org.daisy.common.properties.Properties;
21 import org.daisy.common.properties.Properties.Property;
22 import org.daisy.pipeline.tts.TTSEngine.SynthesisResult;
23 import org.daisy.pipeline.tts.TTSLog.ErrorCode;
24 import org.daisy.pipeline.tts.TTSService;
25 import org.daisy.pipeline.tts.TTSService.ServiceDisabledException;
26 import org.daisy.pipeline.tts.TTSService.SynthesisException;
27 import org.daisy.pipeline.tts.TTSTimeout;
28 import org.daisy.pipeline.tts.TTSTimeout.ThreadFreeInterrupter;
29 import org.daisy.pipeline.tts.Voice.MarkSupport;
30
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 import org.osgi.service.component.annotations.Component;
35 import org.osgi.service.component.annotations.Reference;
36 import org.osgi.service.component.annotations.ReferenceCardinality;
37 import org.osgi.service.component.annotations.ReferencePolicy;
38
39 import org.xml.sax.InputSource;
40
41 @Component(
42 name = "tts-registry",
43 service = { TTSRegistry.class }
44 )
45 public class TTSRegistry {
46
47 public static class TTSResource {
48 public boolean invalid = false;
49 }
50
51 private static Logger logger = LoggerFactory.getLogger(TTSRegistry.class);
52
53 private static DocumentBuilder xmlParser = new Processor(false).newDocumentBuilder();
54 private Map<String,TTSServiceWrapper> ttsServices = new ConcurrentHashMap<>();
55
56 private Map<String,List<TTSResource>> ttsResources = new ConcurrentHashMap<>();
57 private static XdmNode testSSMLWithMark = null;
58 private static XdmNode testSSMLWithoutMark = null;
59
60
61
62
63 @Reference(
64 name = "TTSService",
65 unbind = "-",
66 service = TTSService.class,
67 cardinality = ReferenceCardinality.MULTIPLE,
68 policy = ReferencePolicy.STATIC
69 )
70 public void addTTS(TTSService tts) {
71 String name = tts.getName();
72 logger.info("Adding TTSService " + name);
73
74 ttsServices.put(name, new TTSServiceWrapper(tts));
75 ttsResources.put(name, new ArrayList<TTSResource>());
76 }
77
78
79
80
81
82
83
84
85 public Collection<TTSService> getServices() {
86 return Collections.unmodifiableCollection(ttsServices.values());
87 }
88
89
90
91
92
93
94
95
96 public Collection<TTSEngine> getWorkingEngines(Map<String,String> properties,
97 TTSLog ttsLog,
98 Logger log) {
99 List<TTSEngine> workingEngines = new ArrayList<>();
100 List<String> workingEngineNames = log != null ? new ArrayList<>() : null;
101 List<String> disabledEngines = log != null ? new ArrayList<>() : null;
102 List<String> enginesWithError = log != null ? new ArrayList<>() : null;
103 for (TTSServiceWrapper service : ttsServices.values()) {
104 try {
105 workingEngines.add(service.newEngine(properties, ttsLog));
106 if (log != null)
107 workingEngineNames.add(service.getName());
108 } catch (ServiceDisabledException e) {
109 if (log != null) {
110 log.debug(service.getName() + " is disabled", e);
111 disabledEngines.add(service.getName());
112 }
113 } catch (Throwable e) {
114
115
116
117
118 String msg = service.getName() + " could not be activated: " + e.getMessage();
119 if (ttsLog != null)
120 ttsLog.addGeneralError(ErrorCode.WARNING, msg, e);
121 else if (log != null) {
122 log.warn(msg + " (Please see detailed log for more info.)");
123 log.debug("Error stack trace:", e);
124 }
125 if (log != null)
126 enginesWithError.add(service.getName());
127 }
128 }
129 if (log != null) {
130 if (workingEngines.size() == 0) {
131 log.error("No available TTS engines");
132 if (!enginesWithError.isEmpty())
133 log.error("Some engines could not be activated: " + String.join(", ", enginesWithError));
134 } else {
135 log.info("Available TTS engines: " + String.join(", ", workingEngineNames));
136 if (!enginesWithError.isEmpty())
137 log.warn("Some engines could not be activated: " + String.join(", ", enginesWithError));
138 }
139 if (!disabledEngines.isEmpty())
140 log.info("Some engines are disabled: " + String.join(", ", disabledEngines));
141 }
142 return workingEngines;
143 }
144
145 public TTSResource allocateResourceFor(TTSEngine tts) throws SynthesisException,
146 InterruptedException {
147 List<TTSResource> resources = null;
148 synchronized (ttsResources) {
149 resources = ttsResources.get(tts.getProvider().getName());
150 }
151 if (resources == null)
152 return null;
153 TTSResource r = tts.allocateThreadResources();
154 if (r == null)
155 r = new TTSResource();
156 resources.add(r);
157 return r;
158 }
159
160
161
162
163 private static class TTSServiceWrapper implements TTSService {
164
165 private final TTSService service;
166 private final String name;
167 private final Property enabled;
168
169 TTSServiceWrapper(TTSService wrap) {
170 this.service = wrap;
171 this.name = wrap.getName();
172 this.enabled = Properties.getProperty("org.daisy.pipeline.tts." + name + ".enabled",
173 true,
174 "Enable " + wrap.getDisplayName(),
175 false,
176 "true");
177 }
178
179 @Override
180 public String getName() {
181 return name;
182 }
183
184 @Override
185 public String getDisplayName() {
186 return service.getDisplayName();
187 }
188
189 @Override
190 public TTSEngine newEngine(Map<String,String> params) throws Throwable {
191 return newEngine(params, null);
192 }
193
194 TTSEngine newEngine(Map<String,String> params, TTSLog ttsLog) throws Throwable {
195 String str = enabled.getValue(params);
196 if (str != null && "false".equals(str.toLowerCase()) || "0".equals(str))
197 throw new ServiceDisabledException(
198 "In order to enable the " + name + " service, set property '" + enabled.getName() + "' to 'true'");
199
200
201
202 TTSTimeout timeout = new TTSTimeout();
203 TTSEngine engine; {
204 timeout.enableForCurrentThread(2);
205 try {
206 engine = service.newEngine(params);
207 } finally {
208 timeout.disable();
209 }
210 }
211
212
213
214
215
216
217
218 if (engine.handlesMarks()) {
219 if (testSSMLWithMark == null) {
220 String ssml = "<s:speak version=\"1.0\" xmlns:s=\"http://www.w3.org/2001/10/synthesis\">"
221 + "<s:s xmlns:tmp=\"http://\" id=\"s1\"><s:token>small</s:token>"
222 + "<s:mark name=\"mark\"></s:mark> sentence</s:s></s:speak>";
223 try {
224 testSSMLWithMark = xmlParser.build(new SAXSource(new InputSource(new StringReader(ssml))));
225 } catch (SaxonApiException e) {
226 throw new RuntimeException(e);
227 }
228 }
229 } else {
230 if (testSSMLWithoutMark == null) {
231 String ssml = "<s:speak version=\"1.0\" xmlns:s=\"http://www.w3.org/2001/10/synthesis\">"
232 + "<s:s xmlns:tmp=\"http://\" id=\"s1\"><s:token>small</s:token> sentence</s:s></s:speak>";
233 try {
234 testSSMLWithoutMark = xmlParser.build(new SAXSource(new InputSource(new StringReader(ssml))));
235 } catch (SaxonApiException e) {
236 throw new RuntimeException(e);
237 }
238 }
239 }
240
241 Voice firstVoice; {
242 int timeoutSecs = 30;
243 timeout.enableForCurrentThread(timeoutSecs);
244 try {
245 firstVoice = null;
246 for (Voice v : engine.getAvailableVoices()) {
247 if (!engine.handlesMarks() || v.getMarkSupport() != MarkSupport.MARK_NOT_SUPPORTED) {
248 firstVoice = v;
249 break;
250 }
251 }
252 if (firstVoice == null) {
253 throw new Exception("No voices available");
254 }
255 } catch (InterruptedException e) {
256 throw new Exception("Timeout while retrieving voices (exceeded " + timeoutSecs + " seconds)");
257 } catch (Exception e) {
258
259
260
261
262 throw e;
263 } finally {
264 timeout.disable();
265 }
266 }
267 TTSResource resource; {
268 timeout.enableForCurrentThread(2);
269 try {
270 resource = engine.allocateThreadResources();
271 } catch (Exception e) {
272 throw new Exception("Could not allocate resources: " + e.getMessage(), e);
273 } finally {
274 timeout.disable();
275 }
276 }
277
278 SynthesisResult result; {
279 try {
280 XdmNode ssml = engine.handlesMarks() ? testSSMLWithMark : testSSMLWithoutMark;
281
282 TTSTimeout.ThreadFreeInterrupter interrupter = new ThreadFreeInterrupter() {
283 @Override
284 public void threadFreeInterrupt() {
285 if (ttsLog != null)
286 ttsLog.addGeneralError(
287 ErrorCode.WARNING,
288 "Timeout while initializing " + service.getName()
289 + ". Forcing interruption of the current work of " + service.getName() + "...");
290 engine.interruptCurrentWork(resource);
291 }
292 };
293 result = new TimedTTSExecutor().synthesizeWithTimeout(
294 timeout, interrupter, null, ssml, Sentence.computeSize(ssml),
295 engine, firstVoice, resource);
296 } catch (Exception e) {
297
298
299 throw e;
300 } finally {
301 if (resource != null)
302 timeout.enableForCurrentThread(2);
303 try {
304 engine.releaseThreadResources(resource);
305 } catch (Exception e) {
306 if (ttsLog != null)
307 ttsLog.addGeneralError(
308 ErrorCode.WARNING,
309 "Error while releasing resource of " + service.getName() + ": " + e.getMessage(),
310 e);
311 } finally {
312 timeout.disable();
313 }
314 }
315 }
316 timeout.close();
317
318 String msg = "";
319 if (result.audio.getFrameLength() * result.audio.getFormat().getFrameSize() < 2500) {
320 msg = "Audio output is not big enough. ";
321 }
322 if (engine.handlesMarks()) {
323
324 String details = " voice: "+ firstVoice;
325 if (result.marks.size() != 1) {
326 msg += "One bookmark event expected, but received " + result.marks.size() + " events instead. " + details;
327 } else {
328 int offset = result.marks.get(0);
329 if (offset < 2500) {
330 msg += "Expecting mark offset to be bigger, got " + offset + " as offset. "+details;
331 }
332 }
333 }
334 if (!msg.isEmpty()) {
335 throw new Exception("Test failed: " + msg);
336 }
337 return engine;
338 }
339 }
340 }