1 package org.daisy.pipeline.tts.impl;
2
3 import java.io.File;
4 import java.io.InputStream;
5 import java.io.StringReader;
6 import java.nio.file.Files;
7 import java.nio.file.StandardOpenOption;
8 import java.util.regex.Pattern;
9
10 import javax.sound.sampled.AudioFileFormat;
11 import javax.sound.sampled.AudioInputStream;
12 import javax.xml.transform.sax.SAXSource;
13
14 import net.sf.saxon.s9api.DocumentBuilder;
15 import net.sf.saxon.s9api.Processor;
16 import net.sf.saxon.s9api.SaxonApiException;
17 import net.sf.saxon.s9api.XdmNode;
18
19 import org.daisy.common.properties.Properties;
20 import org.daisy.pipeline.audio.AudioEncoder;
21 import org.daisy.pipeline.audio.AudioFileTypes;
22 import org.daisy.pipeline.audio.AudioServices;
23 import org.daisy.pipeline.tts.Sentence;
24 import org.daisy.pipeline.tts.TimedTTSExecutor;
25 import org.daisy.pipeline.tts.TTSEngine;
26 import org.daisy.pipeline.tts.TTSLog;
27 import org.daisy.pipeline.tts.TTSRegistry.TTSResource;
28 import org.daisy.pipeline.tts.TTSTimeout;
29 import org.daisy.pipeline.tts.TTSTimeout.ThreadFreeInterrupter;
30 import org.daisy.pipeline.tts.Voice;
31 import org.daisy.pipeline.tts.VoiceManager;
32 import org.daisy.pipeline.webservice.restlet.AuthenticatedResource;
33
34 import org.restlet.data.Disposition;
35 import org.restlet.data.MediaType;
36 import org.restlet.data.Status;
37 import org.restlet.representation.InputRepresentation;
38 import org.restlet.representation.Representation;
39 import org.restlet.resource.Get;
40
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 import org.xml.sax.InputSource;
45
46 public class VoicePreviewResource extends AuthenticatedResource {
47
48 private static final Pattern SPEECH_RATE_REGEX = Pattern.compile("(x-)?(slow|fast)|medium|default|[0-9]+(\\.[0-9]+)?%?");
49
50 private static final Logger logger = LoggerFactory.getLogger(VoiceResource.class.getName());
51 private static final DocumentBuilder xmlParser = new Processor(false).newDocumentBuilder();
52 private static final AudioFileFormat.Type audioFileType = AudioFileTypes.WAVE;
53 private static final MediaType mediaType = MediaType.AUDIO_WAV;
54
55 private VoicesWebServiceExtension provider;
56 private AudioEncoder encoder;
57 private String voiceID;
58 private String text;
59 private String speechRate;
60
61 @SuppressWarnings("unchecked")
62 @Override
63 public void doInit() {
64 super.doInit();
65 if (!isAuthenticated()) {
66 return;
67 }
68 provider = (VoicesWebServiceExtension)getContext().getAttributes().get(VoicesWebServiceExtension.CONTEXT_ATTRIBUTE_KEY);
69 encoder = provider.getAudioEncoder(audioFileType);
70 voiceID = (String)getRequestAttributes().get("id");
71 text = getQuery().getFirstValue("text");
72 speechRate = getQuery().getFirstValue("speech-rate");
73 }
74
75
76
77
78
79
80 @Get("xml")
81 public Representation getResource() {
82 logRequest();
83 maybeEnableCORS();
84 if (!isAuthenticated()) {
85 setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
86 return null;
87 }
88 try {
89 VoiceManager voiceManager = provider.getRememberedVoiceManager(Properties.getSnapshot());
90 if (voiceManager == null) {
91 setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
92 return getErrorRepresentation("No /voices call preceeded this call, or settings were changed");
93 }
94 Voice voice = voiceManager.getVoiceForID(voiceID);
95 if (voice == null) {
96 setStatus(Status.CLIENT_ERROR_NOT_FOUND);
97 return getErrorRepresentation("Voice not found");
98 }
99 TTSEngine tts = voiceManager.getTTS(voice);
100 if (text == null)
101 text = "Hi, my name is " + voice.getName();
102 if (speechRate != null) {
103 if (!SPEECH_RATE_REGEX.matcher(speechRate).matches()) {
104 setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
105 return getErrorRepresentation("Invalid speech-rate parameter specified");
106 } else if (!tts.handlesSpeakingRate()) {
107 logger.warn("Voice " + voice.getName() + " does not support changing the speaking rate.");
108 } else {
109 text = "<prosody rate=\"" + speechRate + "\">"
110 + text.replace("&", "&").replace("<", "<").replace("\"", """)
111 + "</prosody>";
112 }
113 }
114 XdmNode ssml = null; {
115 try {
116 ssml = xmlParser.build(new SAXSource(new InputSource(new StringReader(
117 "<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\"><s>" + text + "</s></speak>"))));
118 } catch (SaxonApiException e) {
119 throw new IllegalStateException(e);
120 }
121 }
122 if (encoder == null) {
123 throw new IllegalStateException("No audio encoder found");
124 }
125 TTSTimeout timeout = new TTSTimeout();
126 TTSResource res; {
127 timeout.enableForCurrentThread(2);
128 try {
129 res = tts.allocateThreadResources();
130 } catch (Exception e) {
131 throw new Exception("Could not allocate resources: " + e.getMessage(), e);
132 } finally {
133 timeout.disable();
134 }
135 }
136 TTSLog ttsLog = new TTSLog(logger);
137 AudioInputStream pcm; {
138 try {
139 ThreadFreeInterrupter interrupter = new ThreadFreeInterrupter() {
140 @Override
141 public void threadFreeInterrupt() {
142 ttsLog.addGeneralError(
143 TTSLog.ErrorCode.WARNING,
144 "Timeout while initializing " + tts.getProvider().getName()
145 + ". Forcing interruption of the current work of " + tts.getProvider().getName() + "...");
146 tts.interruptCurrentWork(res);
147 }
148 };
149 pcm = new TimedTTSExecutor().synthesizeWithTimeout(
150 timeout, interrupter, null, ssml, Sentence.computeSize(ssml), tts, voice, res
151 ).audio;
152 } finally {
153 if (res != null)
154 timeout.enableForCurrentThread(2);
155 try {
156 tts.releaseThreadResources(res);
157 } catch (Exception e) {
158 ttsLog.addGeneralError(
159 TTSLog.ErrorCode.WARNING,
160 "Error while releasing resource of " + tts.getProvider().getName() + ": " + e.getMessage(),
161 e);
162 } finally {
163 timeout.disable();
164 }
165 }
166 }
167 timeout.close();
168 File tmpFile = File.createTempFile("pipeline-audio-encoder", ".tmp");
169 tmpFile.deleteOnExit();
170 encoder.encode(pcm, audioFileType, tmpFile);
171 long size = tmpFile.length();
172 InputStream audio = Files.newInputStream(tmpFile.toPath(), StandardOpenOption.DELETE_ON_CLOSE);
173 Representation rep = new InputRepresentation(audio, mediaType);
174 rep.setSize(size);
175 Disposition disposition = new Disposition();
176 disposition.setFilename(voice.getName() + "." + audioFileType.getExtension());
177 disposition.setType(Disposition.TYPE_INLINE);
178 disposition.setSize(size);
179 rep.setDisposition(disposition);
180 setStatus(Status.SUCCESS_OK);
181 return rep;
182 } catch (Throwable e) {
183 setStatus(Status.SERVER_ERROR_INTERNAL);
184 return getErrorRepresentation(e.getMessage());
185 }
186 }
187 }