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