1   package org.daisy.pipeline.tts;
2   
3   import java.util.ArrayList;
4   import java.util.Collection;
5   import java.util.HashMap;
6   import java.util.List;
7   import java.util.Map;
8   import java.util.Set;
9   
10  import net.sf.saxon.s9api.XdmNode;
11  
12  import org.daisy.pipeline.audio.AudioClip;
13  import org.slf4j.Logger;
14  
15  /**
16   * A class to log structured messages in a big list. TTSLog is meant to be
17   * allocated for every new Pipeline job and written to an external file after
18   * the TTS work of the job is done.
19   */
20  public class TTSLog {
21  
22  	private final Logger slf4jLogger;
23  
24  	public TTSLog() {
25  		this(null);
26  	}
27  
28  	public TTSLog(Logger slf4jLogger) {
29  		this.slf4jLogger = slf4jLogger;
30  	}
31  
32  	public enum ErrorCode {
33  		UNEXPECTED_VOICE,
34  		AUDIO_MISSING,
35  		CRITICAL_ERROR,
36  		ERROR,
37  		WARNING
38  	}
39  
40  	public static class Error {
41  
42  		private final ErrorCode key;
43  		private String message;
44  		private Throwable cause;
45  
46  		public Error(ErrorCode key, String message) {
47  			this(key, message, null);
48  		}
49  
50  		public Error(ErrorCode key, String message, Throwable cause) {
51  			this.key = key;
52  			this.message = message;
53  			this.cause = cause;
54  		}
55  
56  		public ErrorCode getErrorCode() {
57  			return key;
58  		}
59  
60  		public String getMessage() {
61  			return message;
62  		}
63  
64  		public Throwable getCause() {
65  			return cause;
66  		}
67  	}
68  
69  	public class Entry {
70  
71  		private final String sentenceId;
72  
73  		private Entry(String sentenceId) {
74  			this.sentenceId = sentenceId;
75  		}
76  
77  		public void addError(Error err) {
78  			errors.add(err);
79  			String msg = err.getMessage();
80  			if (err.getCause() != null && slf4jLogger != null)
81  				msg += " (Please see detailed log and TTS log for more info.)";
82  			else
83  				msg += " (Please see TTS log for more info.)";
84  			switch (err.getErrorCode()) {
85  			case WARNING:
86  			case ERROR:
87  				break;
88  			default:
89  				msg = err.getErrorCode() + ": " + msg;
90  			}
91  			msg = "While processing sentence with ID " + sentenceId + ": " + msg;
92  			switch (err.getErrorCode()) {
93  			case WARNING:
94  			case AUDIO_MISSING:
95  				if (slf4jLogger != null)
96  					slf4jLogger.warn(msg);
97  				break;
98  			default:
99  				if (slf4jLogger != null)
100 					slf4jLogger.error(msg);
101 			}
102 			if (err.getCause() != null)
103 				if (slf4jLogger != null)
104 					slf4jLogger.debug("Error stack trace:", err.getCause());
105 		}
106 
107 		public Collection<Error> getReadOnlyErrors() {
108 			return errors;
109 		}
110 
111 		/**
112 		 * @param ssml is the SSML before being converted to 'ttsinput'
113 		 */
114 		public void setSSML(XdmNode ssml) {
115 			this.ssml = ssml;
116 		}
117 
118 		public XdmNode getSSML() {
119 			return ssml;
120 		}
121 
122 		/**
123 		 * @param v is the voice selected by the top-level VoiceManager
124 		 */
125 		public void setSelectedVoice(Voice v) {
126 			this.selectedVoice = v;
127 		}
128 
129 		public Voice getSelectedVoice() {
130 			return selectedVoice;
131 		}
132 
133 		/**
134 		 * @param v is the actual voice used by the TTS processor (the same as
135 		 *            selectedVoice in the general case, but can be different if
136 		 *            something went wrong)
137 		 */
138 		public void setActualVoice(Voice v) {
139 			this.actualVoice = v;
140 		}
141 
142 		public Voice getActualVoice() {
143 			return actualVoice;
144 		}
145 
146 		/**
147 		 * @param clip is a path of a wave, mp3 or ogg file, with begin and end offsets
148 		 */
149 		public void setClip(AudioClip clip) {
150 			this.clip = clip;
151 		}
152 
153 		public AudioClip getClip() {
154 			return clip;
155 		}
156 
157 		/**
158 		 * @param secs is the timeout value used while synthesizing the entry
159 		 */
160 		public void setTimeout(float secs) {
161 			this.timeout = secs;
162 		}
163 
164 		public float getTimeout() {
165 			return timeout;
166 		}
167 
168 		/**
169 		 * @param secs is the actual time elapsed while synthesizing the entry in seconds
170 		 */
171 		public void setTimeElapsed(float secs) {
172 			this.timeElapsed = secs;
173 		}
174 
175 		public float getTimeElapsed() {
176 			return this.timeElapsed;
177 		}
178 
179 		private List<Error> errors = new ArrayList<Error>();
180 		private XdmNode ssml; //SSML
181 		private Voice selectedVoice;
182 		private Voice actualVoice;
183 		private AudioClip clip;
184 		private float timeout;
185 		private float timeElapsed;
186 	}
187 
188 	/**
189 	 * Supposed to be called within a single-threaded context
190 	 */
191 	public Entry getOrCreateEntry(String sentenceId) {
192 		Entry res = mLog.get(sentenceId);
193 		if (res != null)
194 			return res;
195 		res = new Entry(sentenceId);
196 		mLog.put(sentenceId, res);
197 		return res;
198 	}
199 
200 	/**
201 	 * Can be called within a multi-threaded context once all the calls to
202 	 * getOrCreateEntry() are done.
203 	 */
204 	public Entry getWritableEntry(String sentenceId) {
205 		return mLog.get(sentenceId);
206 	}
207 
208 	public Set<Map.Entry<String, Entry>> getEntries() {
209 		return mLog.entrySet();
210 	}
211 
212 	public void addGeneralError(ErrorCode errcode, String message) {
213 		addGeneralError(errcode, message, null);
214 	}
215 
216 	public void addGeneralError(ErrorCode errcode, String message, Throwable cause) {
217 		synchronized (generalErrors) {
218 			generalErrors.add(new Error(errcode, message, cause));
219 		}
220 		if (cause != null)
221 			message += " (Please see detailed log for more info.)";
222 		switch (errcode) {
223 		case WARNING:
224 		case ERROR:
225 			break;
226 		default:
227 			message = "" + errcode + ": " + message;
228 		}
229 		switch (errcode) {
230 		case WARNING:
231 		case AUDIO_MISSING:
232 			if (slf4jLogger != null)
233 				slf4jLogger.warn(message);
234 			break;
235 		default:
236 			if (slf4jLogger != null)
237 				slf4jLogger.error(message);
238 		}
239 		if (cause != null)
240 			if (slf4jLogger != null)
241 				slf4jLogger.debug("Error stack trace:", cause);
242 	}
243 
244 	public Collection<Error> readonlyGeneralErrors() {
245 		return generalErrors;
246 	}
247 
248 	private List<Error> generalErrors = new ArrayList<Error>();
249 	private Map<String, Entry> mLog = new HashMap<String, Entry>();
250 }
251