]> git.basschouten.com Git - openhab-addons.git/blob
6d5475468e961679bcaf1bda5d9eefc4437fe982
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.voice.voicerss.internal;
14
15 import java.io.File;
16 import java.io.IOException;
17 import java.util.Collections;
18 import java.util.HashSet;
19 import java.util.Locale;
20 import java.util.Map;
21 import java.util.Set;
22
23 import org.openhab.core.OpenHAB;
24 import org.openhab.core.audio.AudioException;
25 import org.openhab.core.audio.AudioFormat;
26 import org.openhab.core.audio.AudioStream;
27 import org.openhab.core.config.core.ConfigurableService;
28 import org.openhab.core.voice.TTSException;
29 import org.openhab.core.voice.TTSService;
30 import org.openhab.core.voice.Voice;
31 import org.openhab.voice.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
32 import org.osgi.framework.Constants;
33 import org.osgi.service.component.annotations.Component;
34 import org.osgi.service.component.annotations.Modified;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * This is a TTS service implementation for using VoiceRSS TTS service.
40  *
41  * @author Jochen Hiller - Initial contribution and API
42  * @author Laurent Garnier - add support for OGG and AAC audio formats
43  */
44 @Component(configurationPid = "org.openhab.voicerss", property = Constants.SERVICE_PID + "=org.openhab.voicerss")
45 @ConfigurableService(category = "voice", label = "VoiceRSS Text-to-Speech", description_uri = "voice:voicerss")
46 public class VoiceRSSTTSService implements TTSService {
47
48     /** Cache folder name is below userdata/voicerss/cache. */
49     private static final String CACHE_FOLDER_NAME = "voicerss" + File.separator + "cache";
50
51     // API Key comes from ConfigAdmin
52     private static final String CONFIG_API_KEY = "apiKey";
53     private String apiKey;
54
55     private final Logger logger = LoggerFactory.getLogger(VoiceRSSTTSService.class);
56
57     /**
58      * We need the cached implementation to allow for FixedLengthAudioStream.
59      */
60     private CachedVoiceRSSCloudImpl voiceRssImpl;
61
62     /**
63      * Set of supported voices
64      */
65     private Set<Voice> voices;
66
67     /**
68      * Set of supported audio formats
69      */
70     private Set<AudioFormat> audioFormats;
71
72     /**
73      * DS activate, with access to ConfigAdmin
74      */
75     protected void activate(Map<String, Object> config) {
76         try {
77             modified(config);
78             voiceRssImpl = initVoiceImplementation();
79             voices = initVoices();
80             audioFormats = initAudioFormats();
81
82             logger.debug("Using VoiceRSS cache folder {}", getCacheFolderName());
83         } catch (IllegalStateException e) {
84             logger.error("Failed to activate VoiceRSS: {}", e.getMessage(), e);
85         }
86     }
87
88     @Modified
89     protected void modified(Map<String, Object> config) {
90         if (config != null) {
91             apiKey = config.containsKey(CONFIG_API_KEY) ? config.get(CONFIG_API_KEY).toString() : null;
92         }
93     }
94
95     @Override
96     public Set<Voice> getAvailableVoices() {
97         return Collections.unmodifiableSet(voices);
98     }
99
100     @Override
101     public Set<AudioFormat> getSupportedFormats() {
102         return Collections.unmodifiableSet(audioFormats);
103     }
104
105     @Override
106     public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFormat) throws TTSException {
107         logger.debug("Synthesize '{}' for voice '{}' in format {}", text, voice.getUID(), requestedFormat);
108         // Validate known api key
109         if (apiKey == null) {
110             throw new TTSException("Missing API key, configure it first before using");
111         }
112         // Validate arguments
113         if (text == null) {
114             throw new TTSException("The passed text is null");
115         }
116         // trim text
117         String trimmedText = text.trim();
118         if (trimmedText.isEmpty()) {
119             throw new TTSException("The passed text is empty");
120         }
121         if (!voices.contains(voice)) {
122             throw new TTSException("The passed voice is unsupported");
123         }
124         boolean isAudioFormatSupported = false;
125         for (AudioFormat currentAudioFormat : audioFormats) {
126             if (currentAudioFormat.isCompatible(requestedFormat)) {
127                 isAudioFormatSupported = true;
128                 break;
129             }
130         }
131         if (!isAudioFormatSupported) {
132             throw new TTSException("The passed AudioFormat is unsupported");
133         }
134
135         // now create the input stream for given text, locale, format. There is
136         // only a default voice
137         try {
138             File cacheAudioFile = voiceRssImpl.getTextToSpeechAsFile(apiKey, trimmedText,
139                     voice.getLocale().toLanguageTag(), voice.getLabel(), getApiAudioFormat(requestedFormat));
140             if (cacheAudioFile == null) {
141                 throw new TTSException("Could not read from VoiceRSS service");
142             }
143             return new VoiceRSSAudioStream(cacheAudioFile, requestedFormat);
144         } catch (AudioException ex) {
145             throw new TTSException("Could not create AudioStream: " + ex.getMessage(), ex);
146         } catch (IOException ex) {
147             throw new TTSException("Could not read from VoiceRSS service: " + ex.getMessage(), ex);
148         }
149     }
150
151     /**
152      * Initializes voices.
153      *
154      * @return The voices of this instance
155      */
156     private Set<Voice> initVoices() {
157         Set<Voice> voices = new HashSet<>();
158         for (Locale locale : voiceRssImpl.getAvailableLocales()) {
159             for (String voiceLabel : voiceRssImpl.getAvailableVoices(locale)) {
160                 voices.add(new VoiceRSSVoice(locale, voiceLabel));
161             }
162         }
163         return voices;
164     }
165
166     /**
167      * Initializes audioFormats
168      *
169      * @return The audio formats of this instance
170      */
171     private Set<AudioFormat> initAudioFormats() {
172         Set<AudioFormat> audioFormats = new HashSet<>();
173         for (String format : voiceRssImpl.getAvailableAudioFormats()) {
174             audioFormats.add(getAudioFormat(format));
175         }
176         return audioFormats;
177     }
178
179     private AudioFormat getAudioFormat(String apiFormat) {
180         Boolean bigEndian = null;
181         Integer bitDepth = 16;
182         Integer bitRate = null;
183         Long frequency = 44100L;
184
185         if ("MP3".equals(apiFormat)) {
186             // we use by default: MP3, 44khz_16bit_mono with bitrate 64 kbps
187             bitRate = 64000;
188             return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, bigEndian, bitDepth, bitRate,
189                     frequency);
190         } else if ("OGG".equals(apiFormat)) {
191             // we use by default: OGG, 44khz_16bit_mono
192             return new AudioFormat(AudioFormat.CONTAINER_OGG, AudioFormat.CODEC_VORBIS, bigEndian, bitDepth, bitRate,
193                     frequency);
194         } else if ("AAC".equals(apiFormat)) {
195             // we use by default: AAC, 44khz_16bit_mono
196             return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, bigEndian, bitDepth, bitRate,
197                     frequency);
198         } else {
199             throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
200         }
201     }
202
203     private String getApiAudioFormat(AudioFormat format) {
204         if (format.getCodec().equals(AudioFormat.CODEC_MP3)) {
205             return "MP3";
206         } else if (format.getCodec().equals(AudioFormat.CODEC_VORBIS)) {
207             return "OGG";
208         } else if (format.getCodec().equals(AudioFormat.CODEC_AAC)) {
209             return "AAC";
210         } else {
211             throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
212         }
213     }
214
215     private CachedVoiceRSSCloudImpl initVoiceImplementation() {
216         return new CachedVoiceRSSCloudImpl(getCacheFolderName());
217     }
218
219     private String getCacheFolderName() {
220         // we assume that this folder does NOT have a trailing separator
221         return OpenHAB.getUserDataFolder() + File.separator + CACHE_FOLDER_NAME;
222     }
223
224     @Override
225     public String getId() {
226         return "voicerss";
227     }
228
229     @Override
230     public String getLabel(Locale locale) {
231         return "VoiceRSS";
232     }
233 }