]> git.basschouten.com Git - openhab-addons.git/blob
2c0cdaa09ed029736bca6e7b3071ba05ab7f0d9d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.pollytts.internal;
14
15 import static java.util.stream.Collectors.toSet;
16 import static org.openhab.core.audio.AudioFormat.*;
17 import static org.openhab.voice.pollytts.internal.PollyTTSService.*;
18
19 import java.io.File;
20 import java.io.IOException;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.Locale;
24 import java.util.Map;
25 import java.util.Set;
26
27 import org.openhab.core.audio.AudioException;
28 import org.openhab.core.audio.AudioFormat;
29 import org.openhab.core.audio.AudioStream;
30 import org.openhab.core.config.core.ConfigConstants;
31 import org.openhab.core.config.core.ConfigurableService;
32 import org.openhab.core.voice.TTSException;
33 import org.openhab.core.voice.TTSService;
34 import org.openhab.core.voice.Voice;
35 import org.openhab.voice.pollytts.internal.cloudapi.CachedPollyTTSCloudImpl;
36 import org.openhab.voice.pollytts.internal.cloudapi.PollyTTSConfig;
37 import org.osgi.framework.Constants;
38 import org.osgi.service.component.annotations.Activate;
39 import org.osgi.service.component.annotations.Component;
40 import org.osgi.service.component.annotations.Modified;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * This is a TTS service implementation for using Polly Text-to-Speech.
46  *
47  * @author Robert Hillman - Initial contribution
48  */
49 @Component(configurationPid = SERVICE_PID, property = { Constants.SERVICE_PID + "=" + SERVICE_PID,
50         ConfigurableService.SERVICE_PROPERTY_LABEL + "=" + SERVICE_NAME + " Text-to-Speech",
51         ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=" + SERVICE_CATEGORY + ":" + SERVICE_ID,
52         ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=" + SERVICE_CATEGORY })
53 public class PollyTTSService implements TTSService {
54
55     /**
56      * Service name
57      */
58     static final String SERVICE_NAME = "Polly";
59
60     /**
61      * Service id
62      */
63     static final String SERVICE_ID = "pollytts";
64
65     /**
66      * Service category
67      */
68     static final String SERVICE_CATEGORY = "voice";
69
70     /**
71      * Service pid
72      */
73     static final String SERVICE_PID = "org.openhab." + SERVICE_CATEGORY + "." + SERVICE_ID;
74
75     /**
76      * Cache folder under $userdata
77      */
78     private static final String CACHE_FOLDER_NAME = "cache";
79
80     private final Logger logger = LoggerFactory.getLogger(PollyTTSService.class);
81
82     /**
83      * We need the cached implementation to allow for FixedLengthAudioStream.
84      */
85     private CachedPollyTTSCloudImpl pollyTTSImpl;
86
87     /**
88      * Set of supported voices
89      */
90     private final Set<Voice> voices = new HashSet<>();
91
92     /**
93      * Set of supported audio formats
94      */
95     private final Set<AudioFormat> audioFormats = new HashSet<>();
96
97     private PollyTTSConfig pollyTTSConfig;
98
99     @Activate
100     protected void activate(Map<String, Object> config) {
101         modified(config);
102     }
103
104     @Modified
105     protected void modified(Map<String, Object> config) {
106         try {
107             pollyTTSConfig = new PollyTTSConfig(config);
108             logger.debug("Using configuration {}", config);
109
110             // create cache folder
111             File cacheFolder = new File(new File(ConfigConstants.getUserDataFolder(), CACHE_FOLDER_NAME), SERVICE_PID);
112             if (!cacheFolder.exists()) {
113                 cacheFolder.mkdirs();
114             }
115             logger.info("Using cache folder {}", cacheFolder.getAbsolutePath());
116
117             pollyTTSImpl = new CachedPollyTTSCloudImpl(pollyTTSConfig, cacheFolder);
118
119             audioFormats.clear();
120             audioFormats.addAll(initAudioFormats());
121
122             voices.clear();
123             voices.addAll(initVoices());
124
125             logger.debug("PollyTTS service initialized");
126         } catch (IllegalArgumentException e) {
127             logger.warn("Failed to initialize PollyTTS: {}", e.getMessage());
128         } catch (Exception e) {
129             logger.warn("Failed to initialize PollyTTS", e);
130         }
131     }
132
133     @Override
134     public Set<Voice> getAvailableVoices() {
135         return Collections.unmodifiableSet(voices);
136     }
137
138     @Override
139     public Set<AudioFormat> getSupportedFormats() {
140         return Collections.unmodifiableSet(audioFormats);
141     }
142
143     /**
144      * obtain audio stream from cache or Amazon Polly service and return it to play the audio
145      */
146     @Override
147     public AudioStream synthesize(String inText, Voice voice, AudioFormat requestedFormat) throws TTSException {
148         logger.debug("Synthesize '{}' in format {}", inText, requestedFormat);
149         logger.debug("voice UID: '{}' voice label: '{}' voice Locale: {}", voice.getUID(), voice.getLabel(),
150                 voice.getLocale());
151
152         // Validate arguments
153         // trim text
154         String text = inText.trim();
155         if (text == null || text.isEmpty()) {
156             throw new TTSException("The passed text is null or empty");
157         }
158         if (!voices.contains(voice)) {
159             throw new TTSException("The passed voice is unsupported");
160         }
161         boolean isAudioFormatSupported = audioFormats.stream()
162                 .filter(audioFormat -> audioFormat.isCompatible(requestedFormat)).findAny().isPresent();
163
164         if (!isAudioFormatSupported) {
165             throw new TTSException("The passed AudioFormat is unsupported");
166         }
167
168         // now create the input stream for given text, locale, format. There is
169         // only a default voice
170         try {
171             File cacheAudioFile = pollyTTSImpl.getTextToSpeechAsFile(text, voice.getLabel(),
172                     getApiAudioFormat(requestedFormat));
173             if (cacheAudioFile == null) {
174                 throw new TTSException("Could not read from PollyTTS service");
175             }
176             logger.debug("Audio Stream for '{}' in format {}", text, requestedFormat);
177             AudioStream audioStream = new PollyTTSAudioStream(cacheAudioFile, requestedFormat);
178             return audioStream;
179         } catch (AudioException ex) {
180             throw new TTSException("Could not create AudioStream: " + ex.getMessage(), ex);
181         } catch (IOException ex) {
182             throw new TTSException("Could not read from PollyTTS service: " + ex.getMessage(), ex);
183         }
184     }
185
186     private Set<Voice> initVoices() {
187         // @formatter:off
188         return pollyTTSImpl.getAvailableLocales().stream()
189             .flatMap(locale ->
190                 pollyTTSImpl.getAvailableVoices(locale).stream()
191                     .map(label -> new PollyTTSVoice(locale, label)))
192             .collect(toSet());
193         // @formatter:on
194     }
195
196     private Set<AudioFormat> initAudioFormats() {
197         // @formatter:off
198         return pollyTTSImpl.getAvailableAudioFormats().stream()
199                 .map(this::getAudioFormat)
200                 .collect(toSet());
201         // @formatter:on
202     }
203
204     private AudioFormat getAudioFormat(String apiFormat) {
205         if (CODEC_MP3.equals(apiFormat)) {
206             // use by default: MP3, 22khz_16bit_mono with bitrate 64 kbps
207             return new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, 16, 64000, 22050L);
208         } else if (CONTAINER_OGG.equals(apiFormat)) {
209             // use by default: OGG, 22khz_16bit_mono
210             return new AudioFormat(CONTAINER_OGG, CODEC_VORBIS, null, 16, null, 22050L);
211         } else {
212             throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
213         }
214     }
215
216     private String getApiAudioFormat(AudioFormat format) {
217         if (!"default".equals(pollyTTSConfig.getAudioFormat())) {
218             // Override system specified with user preferred value
219             return pollyTTSConfig.getAudioFormat();
220         }
221         if (CODEC_MP3.equals(format.getCodec())) {
222             return CODEC_MP3;
223         } else if (CODEC_VORBIS.equals(format.getCodec())) {
224             return CONTAINER_OGG;
225         } else {
226             throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
227         }
228     }
229
230     @Override
231     public String getId() {
232         return "pollytts";
233     }
234
235     @Override
236     public String getLabel(Locale locale) {
237         return "PollyTTS";
238     }
239 }