1 package org.daisy.pipeline.tts.calabash.impl;
2
3 import java.io.File;
4 import java.math.BigDecimal;
5 import java.math.MathContext;
6 import java.util.ArrayList;
7 import java.util.Collection;
8 import java.util.Collections;
9 import java.util.concurrent.BlockingQueue;
10 import java.util.concurrent.ConcurrentLinkedQueue;
11 import java.util.concurrent.PriorityBlockingQueue;
12 import java.util.HashMap;
13 import java.util.IllformedLocaleException;
14 import java.util.List;
15 import java.util.Locale;
16 import java.util.Map;
17 import java.util.Optional;
18
19 import javax.sound.sampled.AudioFileFormat;
20
21 import net.sf.saxon.s9api.Axis;
22 import net.sf.saxon.s9api.Processor;
23 import net.sf.saxon.s9api.XdmNode;
24 import net.sf.saxon.s9api.XdmSequenceIterator;
25
26 import org.daisy.common.messaging.MessageAppender;
27 import org.daisy.common.properties.Properties;
28 import org.daisy.common.properties.Properties.Property;
29 import org.daisy.pipeline.audio.AudioServices;
30 import org.daisy.pipeline.css.speech.VoiceFamilyList;
31 import org.daisy.pipeline.tts.AudioFootprintMonitor;
32 import org.daisy.pipeline.tts.calabash.impl.EncodingThread.EncodingException;
33 import org.daisy.pipeline.tts.config.VoiceConfigExtension;
34 import org.daisy.pipeline.tts.DefaultSSMLMarkSplitter;
35 import org.daisy.pipeline.tts.SSMLMarkSplitter;
36 import org.daisy.pipeline.tts.Sentence;
37 import org.daisy.pipeline.tts.TTSEngine;
38 import org.daisy.pipeline.tts.TTSLog;
39 import org.daisy.pipeline.tts.TTSLog.ErrorCode;
40 import org.daisy.pipeline.tts.TTSRegistry;
41 import org.daisy.pipeline.tts.TTSService.SynthesisException;
42 import org.daisy.pipeline.tts.TimedTTSExecutor;
43 import org.daisy.pipeline.tts.Voice;
44 import org.daisy.pipeline.tts.VoiceInfo.Gender;
45 import org.daisy.pipeline.tts.VoiceManager;
46
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import com.google.common.collect.Iterables;
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 public class SSMLtoAudio implements FormatSpecifications {
84
85 private static final Property THREADS_NUMBER = Properties.getProperty("org.daisy.pipeline.tts.threads.number",
86 false,
87 "Number of threads for speech synthesis and audio encoding",
88 false,
89 ""+Runtime.getRuntime().availableProcessors());
90 private static final Property ENCODING_THREADS_NUMBER = Properties.getProperty("org.daisy.pipeline.tts.threads.encoding.number",
91 false,
92 "Number of threads for audio encoding",
93 false,
94 null);
95 private static final Property SPEAKING_THREADS_NUMBER = Properties.getProperty("org.daisy.pipeline.tts.threads.speaking.number",
96 false,
97 "Number of threads for speech synthesis",
98 false,
99 null);
100 private static final Property THREADS_MEMLIMIT = Properties.getProperty("org.daisy.pipeline.tts.threads.each.memlimit",
101 false,
102 "Maximum amount of memory consumed by each speech synthesis thread (in Mb)",
103 false,
104 "20");
105
106 private TTSEngine mLastTTS;
107 private TTSRegistry mTTSRegistry;
108 private Logger mLogger;
109 private ContiguousText mCurrentSection;
110 private File mAudioDir;
111 private final AudioFileFormat.Type mAudioFileFormat;
112
113
114
115 private final int mMaxSentencesPerSection;
116 private int mSentenceCounter = 0;
117 private long mTotalTextSize;
118 private int mDocumentPosition;
119 private Map<TTSEngine, List<ContiguousText>> mOrganizedText;
120 private AudioFootprintMonitor mAudioFootprintMonitor;
121 private Processor mProc;
122 private VoiceManager mVoiceManager;
123 private TimedTTSExecutor mExecutor = new TimedTTSExecutor();
124 private final Map<String,String> mProperties;
125 private TTSLog mTTSlog;
126 private int mErrorCounter;
127
128 int numberOfCores = getPropertyAsInt(THREADS_NUMBER).get();
129
130 public SSMLtoAudio(File audioDir, AudioFileFormat.Type audioFileFormat,
131 int maxSentencesPerSection, TTSRegistry ttsregistry, Logger logger,
132 AudioFootprintMonitor audioFootprintMonitor, Processor proc,
133 Map<String,String> properties, VoiceConfigExtension configExt, TTSLog logs) {
134 mTTSRegistry = ttsregistry;
135 mLogger = logger;
136 mCurrentSection = null;
137 mDocumentPosition = 0;
138 mOrganizedText = new HashMap<TTSEngine, List<ContiguousText>>();
139 mAudioFootprintMonitor = audioFootprintMonitor;
140 mProc = proc;
141 mAudioDir = audioDir;
142 mAudioFileFormat = audioFileFormat;
143 mMaxSentencesPerSection = maxSentencesPerSection;
144 mTTSlog = logs;
145
146
147
148 mProperties = properties;
149 mVoiceManager = new VoiceManager(
150 ttsregistry.getWorkingEngines(properties, mTTSlog, mLogger),
151 configExt.getVoiceDeclarations());
152 }
153
154
155
156
157 public void feedSSML(XdmNode doc) throws SynthesisException {
158 XdmSequenceIterator iter = doc.axisIterator(Axis.CHILD);
159 if (iter.hasNext()) {
160 traverse((XdmNode)iter.next(), null);
161 }
162 endSection();
163 }
164
165 private void traverse(XdmNode node, Locale lang) throws SynthesisException {
166 if (SentenceTag.equals(node.getNodeName())) {
167 if (!dispatchSSML(node, lang))
168 mErrorCounter++;
169 if (mMaxSentencesPerSection > 0 && ++mSentenceCounter % mMaxSentencesPerSection == 0)
170 endSection();
171 } else {
172 String langAttr = node.getAttributeValue(Sentence_attr_lang);
173 if (langAttr != null) {
174 try {
175 lang = (new Locale.Builder()).setLanguageTag(langAttr).build();
176 } catch (IllformedLocaleException e) {
177 lang = null;
178 }
179 }
180 XdmSequenceIterator iter = node.axisIterator(Axis.CHILD);
181 while (iter.hasNext())
182 traverse((XdmNode)iter.next(), lang);
183 }
184 }
185
186
187
188
189
190
191
192
193
194 boolean dispatchSSML(XdmNode ssml, Locale lang) throws SynthesisException {
195 VoiceFamilyList voiceFamily; {
196 String attr = ssml.getAttributeValue(Sentence_attr_voice_family);
197 if (attr != null && !"".equals(attr))
198 try {
199 voiceFamily = VoiceFamilyList.of(attr);
200 } catch (IllegalArgumentException e) {
201 throw new SynthesisException("invalid voice-family attribute: " + attr, e);
202 }
203 else
204 voiceFamily = null;
205 }
206 {
207 String langAttr = ssml.getAttributeValue(Sentence_attr_lang);
208 if (langAttr != null) {
209 try {
210 lang = (new Locale.Builder()).setLanguageTag(langAttr).build();
211 } catch (IllformedLocaleException e) {
212 lang = null;
213 }
214 }
215 }
216 String id = ssml.getAttributeValue(Sentence_attr_id);
217 TTSLog.Entry logEntry = mTTSlog.getOrCreateEntry(id);
218 logEntry.setSSML(ssml);
219 Iterable<Voice> voices = mVoiceManager.findAvailableVoices(lang, voiceFamily);
220 Voice preferredVoice = Iterables.getFirst(voices, null);
221 logEntry.setSelectedVoice(preferredVoice);
222 if (preferredVoice == null) {
223 String err = "could not find any installed voice "
224 + (voiceFamily != null ? ("matching with `voice-family: " + voiceFamily + "` and ") : "")
225 + "providing the language '" + lang + "'";
226 logEntry.addError(new TTSLog.Error(TTSLog.ErrorCode.AUDIO_MISSING, err));
227 return false;
228 }
229
230 TTSEngine newSynth = mVoiceManager.getTTS(preferredVoice);
231 if (newSynth == null) {
232
233
234
235
236 String err = "could not find any TTS engine for the voice "
237 + "{engine: " + preferredVoice.getEngine()
238 + ", name: " + preferredVoice.getName() + "}";
239 logEntry.addError(new TTSLog.Error(TTSLog.ErrorCode.AUDIO_MISSING, err));
240 return false;
241 }
242
243 if (!mVoiceManager.matches(preferredVoice, lang, voiceFamily)) {
244 logEntry.addError(new TTSLog.Error(TTSLog.ErrorCode.UNEXPECTED_VOICE,
245 "no voice matches exactly with the requested characteristics"));
246 }
247
248
249
250
251
252
253 TTSEngine poolkey = null;
254 if (newSynth.reservedThreadNum() > 0) {
255 poolkey = newSynth;
256 }
257
258 if (newSynth != mLastTTS) {
259 if (mLastTTS != null
260 && (poolkey != null || mLastTTS.reservedThreadNum() > 0))
261 endSection();
262
263 mLastTTS = newSynth;
264 }
265
266 if (mCurrentSection == null) {
267
268 mCurrentSection = new ContiguousText(mDocumentPosition++, mAudioDir);
269 List<ContiguousText> listOfSections = mOrganizedText.get(poolkey);
270 if (listOfSections == null) {
271 listOfSections = new ArrayList<ContiguousText>();
272 mOrganizedText.put(poolkey, listOfSections);
273 }
274
275 listOfSections.add(mCurrentSection);
276 }
277 mCurrentSection.sentences.add(new Sentence(newSynth, voices, ssml));
278 return true;
279 }
280
281 private void endSection() {
282 mCurrentSection = null;
283 }
284
285 public Iterable<SoundFileLink> blockingRun(AudioServices audioServices)
286 throws SynthesisException, InterruptedException, EncodingException {
287
288 MessageAppender activeBlock = MessageAppender.getActiveBlock();
289
290
291 SSMLMarkSplitter ssmlSplitter = new DefaultSSMLMarkSplitter(mProc);
292
293
294
295 reorganizeSections();
296
297
298 int reservedThreadNum = 0;
299 for (TTSEngine tts : mOrganizedText.keySet()) {
300 if (tts != null && tts.reservedThreadNum() > 0)
301 reservedThreadNum += tts.reservedThreadNum();
302 }
303 int ttsThreadNum = numberOfCores;
304 int encodingThreadNum = getPropertyAsInt(ENCODING_THREADS_NUMBER).orElse(ttsThreadNum);
305 int regularTTSthreadNum = getPropertyAsInt(SPEAKING_THREADS_NUMBER).orElse(ttsThreadNum);
306 int totalTTSThreads = regularTTSthreadNum + reservedThreadNum;
307 int maxMemPerTTSThread = getPropertyAsInt(THREADS_MEMLIMIT).get() * 1048576;
308 mLogger.info("Number of encoding threads: " + encodingThreadNum);
309 mLogger.info("Number of regular text-to-speech threads: " + regularTTSthreadNum);
310 mLogger.info("Number of reserved text-to-speech threads: " + reservedThreadNum);
311 mLogger.info("Max TTS memory footprint (encoding excluded): "
312 + mAudioFootprintMonitor.getSpaceForTTS() / 1000000 + "MB");
313 mLogger.info("Max encoding memory footprint: "
314 + mAudioFootprintMonitor.getSpaceForEncoding() / 1000000 + "MB");
315
316
317 BlockingQueue<ContiguousPCM> pcmQueue = new PriorityBlockingQueue<ContiguousPCM>();
318
319
320 TextToPcmThread[] tpt = new TextToPcmThread[totalTTSThreads];
321 List<ContiguousText> text = mOrganizedText.get(null);
322 if (text == null) {
323 text = Collections.EMPTY_LIST;
324 }
325 int i = 0;
326 ConcurrentLinkedQueue<ContiguousText> stext = new ConcurrentLinkedQueue<ContiguousText>(text);
327 if (regularTTSthreadNum > 0) {
328 long textSize = stext.stream().mapToLong(ContiguousText::getStringSize).sum();
329 BigDecimal portion = textSize == 0
330 ? BigDecimal.ZERO
331 : new BigDecimal(textSize).divide(new BigDecimal(mTotalTextSize), MathContext.DECIMAL128)
332 .divide(new BigDecimal(regularTTSthreadNum), MathContext.DECIMAL128);
333 for (; i < regularTTSthreadNum; ++i) {
334 tpt[i] = new TextToPcmThread();
335 tpt[i].start(stext, pcmQueue, mExecutor, mTTSRegistry, mVoiceManager, ssmlSplitter,
336 mLogger, mAudioFootprintMonitor, maxMemPerTTSThread, mTTSlog, activeBlock,
337 mTotalTextSize, portion);
338 }
339 }
340 for (Map.Entry<TTSEngine, List<ContiguousText>> e : mOrganizedText.entrySet()) {
341 TTSEngine tts = e.getKey();
342 if (tts != null) {
343 stext = new ConcurrentLinkedQueue<ContiguousText>(e.getValue());
344 if (tts.reservedThreadNum() > 0) {
345 long textSize = stext.stream().mapToLong(ContiguousText::getStringSize).sum();
346 BigDecimal portion = textSize == 0
347 ? BigDecimal.ZERO
348 : new BigDecimal(textSize).divide(new BigDecimal(mTotalTextSize), MathContext.DECIMAL128)
349 .divide(new BigDecimal(tts.reservedThreadNum()), MathContext.DECIMAL128);
350 for (int j = 0; j < tts.reservedThreadNum(); ++i, ++j) {
351 tpt[i] = new TextToPcmThread();
352 tpt[i].start(stext, pcmQueue, mExecutor, mTTSRegistry, mVoiceManager, ssmlSplitter,
353 mLogger, mAudioFootprintMonitor, maxMemPerTTSThread, mTTSlog,
354 activeBlock, mTotalTextSize, portion);
355 }
356 }
357 }
358 }
359 mLogger.info("Text-to-speech threads started.");
360
361
362 EncodingThread[] encodingTh = new EncodingThread[encodingThreadNum];
363 for (int j = 0; j < encodingTh.length; ++j) {
364 encodingTh[j] = new EncodingThread();
365 encodingTh[j].start(mAudioFileFormat, audioServices, pcmQueue,
366 mAudioFootprintMonitor, mProperties, mTTSlog, activeBlock);
367 }
368 mLogger.info("Encoding threads started.");
369
370
371 Collection<SoundFileLink>[] fragments = new Collection[tpt.length];
372 for (int j = 0; j < tpt.length; ++j) {
373 fragments[j] = tpt[j].getSoundFragments();
374 }
375 for (int j = 0; j < tpt.length; ++j) {
376 mErrorCounter += tpt[j].getErrorCount();
377 }
378
379
380 mLogger.info("Text-to-speech finished. Waiting for audio encoding to finish...");
381 for (int k = 0; k < encodingTh.length; ++k) {
382 pcmQueue.add(ContiguousPCM.EndOfQueue);
383 }
384 for (int j = 0; j < encodingTh.length; ++j) {
385 try {
386 encodingTh[j].waitToFinish();
387 } catch (EncodingException e) {
388 while (++j < encodingTh.length)
389
390 try { encodingTh[j].waitToFinish(); }
391 catch (EncodingException _e) {}
392 throw e;
393 }
394 }
395
396 mLogger.info("Audio encoding finished.");
397
398 return Iterables.concat(fragments);
399 }
400
401 double getErrorRate() {
402 return (double)mErrorCounter/mSentenceCounter;
403 }
404
405 private void reorganizeSections() {
406 mTotalTextSize = 0;
407 int sectionCount = 0;
408 for (List<ContiguousText> sections : mOrganizedText.values()) {
409
410
411
412 for (ContiguousText section : sections) {
413 section.computeSize();
414 mTotalTextSize += section.getStringSize();
415 }
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435 Collections.sort(sections);
436
437
438
439 Collections.shuffle(sections.subList(0, sections.size() / 2));
440
441 sectionCount += sections.size();
442 }
443 mLogger.debug("Number of synthesizable TTS sections: " + sectionCount);
444 }
445
446
447
448 private void splitSection(ContiguousText section, int maxSize,
449 List<ContiguousText> newSections) {
450 int left = 0;
451 int count = 0;
452 ContiguousText currentSection = new ContiguousText(section.getDocumentPosition(),
453 section.getAudioOutputDir());
454 currentSection.setStringsize(0);
455 for (int right = 0; right < section.sentences.size(); ++right) {
456 if (currentSection.getStringSize() > maxSize) {
457 currentSection.sentences = section.sentences.subList(left, right);
458 currentSection.setDocumentSplitPosition(count);
459 newSections.add(currentSection);
460 currentSection = new ContiguousText(section.getDocumentPosition(), section
461 .getAudioOutputDir());
462 currentSection.setStringsize(0);
463 left = right;
464 ++count;
465 }
466 currentSection.setStringsize(currentSection.getStringSize()
467 + section.sentences.get(right).getSize());
468 }
469 currentSection.sentences = section.sentences.subList(left, section.sentences.size());
470 currentSection.setDocumentSplitPosition(count);
471 newSections.add(currentSection);
472 }
473
474 private Optional<Integer> getPropertyAsInt(Property prop) {
475 String str = prop.getValue(mProperties);
476 if (str != null) {
477 try {
478 return Optional.of(Integer.valueOf(str));
479 } catch (NumberFormatException e) {
480 mTTSlog.addGeneralError(
481 ErrorCode.WARNING, str + " is not a valid value for property " + prop.getName(), e);
482 }
483 }
484 return Optional.empty();
485 }
486 }