2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.voice.pollytts.internal;
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.*;
20 import java.io.IOException;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.Locale;
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;
45 * This is a TTS service implementation for using Polly Text-to-Speech.
47 * @author Robert Hillman - Initial contribution
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 {
58 static final String SERVICE_NAME = "Polly";
63 static final String SERVICE_ID = "pollytts";
68 static final String SERVICE_CATEGORY = "voice";
73 static final String SERVICE_PID = "org.openhab." + SERVICE_CATEGORY + "." + SERVICE_ID;
76 * Cache folder under $userdata
78 private static final String CACHE_FOLDER_NAME = "cache";
80 private final Logger logger = LoggerFactory.getLogger(PollyTTSService.class);
83 * We need the cached implementation to allow for FixedLengthAudioStream.
85 private CachedPollyTTSCloudImpl pollyTTSImpl;
88 * Set of supported voices
90 private final Set<Voice> voices = new HashSet<>();
93 * Set of supported audio formats
95 private final Set<AudioFormat> audioFormats = new HashSet<>();
97 private PollyTTSConfig pollyTTSConfig;
100 protected void activate(Map<String, Object> config) {
105 protected void modified(Map<String, Object> config) {
107 pollyTTSConfig = new PollyTTSConfig(config);
108 logger.debug("Using configuration {}", config);
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();
115 logger.info("Using cache folder {}", cacheFolder.getAbsolutePath());
117 pollyTTSImpl = new CachedPollyTTSCloudImpl(pollyTTSConfig, cacheFolder);
119 audioFormats.clear();
120 audioFormats.addAll(initAudioFormats());
123 voices.addAll(initVoices());
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);
134 public Set<Voice> getAvailableVoices() {
135 return Collections.unmodifiableSet(voices);
139 public Set<AudioFormat> getSupportedFormats() {
140 return Collections.unmodifiableSet(audioFormats);
144 * obtain audio stream from cache or Amazon Polly service and return it to play the audio
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(),
152 // Validate arguments
154 String text = inText.trim();
155 if (text == null || text.isEmpty()) {
156 throw new TTSException("The passed text is null or empty");
158 if (!voices.contains(voice)) {
159 throw new TTSException("The passed voice is unsupported");
161 boolean isAudioFormatSupported = audioFormats.stream()
162 .filter(audioFormat -> audioFormat.isCompatible(requestedFormat)).findAny().isPresent();
164 if (!isAudioFormatSupported) {
165 throw new TTSException("The passed AudioFormat is unsupported");
168 // now create the input stream for given text, locale, format. There is
169 // only a default voice
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");
176 logger.debug("Audio Stream for '{}' in format {}", text, requestedFormat);
177 AudioStream audioStream = new PollyTTSAudioStream(cacheAudioFile, requestedFormat);
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);
186 private Set<Voice> initVoices() {
188 return pollyTTSImpl.getAvailableLocales().stream()
190 pollyTTSImpl.getAvailableVoices(locale).stream()
191 .map(label -> new PollyTTSVoice(locale, label)))
196 private Set<AudioFormat> initAudioFormats() {
198 return pollyTTSImpl.getAvailableAudioFormats().stream()
199 .map(this::getAudioFormat)
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);
212 throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
216 private String getApiAudioFormat(AudioFormat format) {
217 if (!"default".equals(pollyTTSConfig.getAudioFormat())) {
218 // Override system specified with user preferred value
219 return pollyTTSConfig.getAudioFormat();
221 if (CODEC_MP3.equals(format.getCodec())) {
223 } else if (CODEC_VORBIS.equals(format.getCodec())) {
224 return CONTAINER_OGG;
226 throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
231 public String getId() {
236 public String getLabel(Locale locale) {