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