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  	 * Gets the resource.
77  	 *
78  	 * @return the resource
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("&", "&amp;").replace("<", "&lt;").replace("\"", "&quot;")
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); // should not happen
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 }