2 * Copyright (c) 2010-2022 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.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;
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(category = SERVICE_CATEGORY, label = SERVICE_NAME
51 + " Text-to-Speech", description_uri = SERVICE_CATEGORY + ":" + SERVICE_ID)
52 public class PollyTTSService implements TTSService {
57 static final String SERVICE_NAME = "Polly";
62 static final String SERVICE_ID = "pollytts";
67 static final String SERVICE_CATEGORY = "voice";
72 static final String SERVICE_PID = "org.openhab." + SERVICE_CATEGORY + "." + SERVICE_ID;
75 * Cache folder under $userdata
77 private static final String CACHE_FOLDER_NAME = "cache";
79 private final Logger logger = LoggerFactory.getLogger(PollyTTSService.class);
82 * We need the cached implementation to allow for FixedLengthAudioStream.
84 private CachedPollyTTSCloudImpl pollyTTSImpl;
87 * Set of supported voices
89 private final Set<Voice> voices = new HashSet<>();
92 * Set of supported audio formats
94 private final Set<AudioFormat> audioFormats = new HashSet<>();
96 private PollyTTSConfig pollyTTSConfig;
99 protected void activate(Map<String, Object> config) {
104 protected void modified(Map<String, Object> config) {
106 pollyTTSConfig = new PollyTTSConfig(config);
107 logger.debug("Using configuration {}", config);
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();
114 logger.info("Using cache folder {}", cacheFolder.getAbsolutePath());
116 pollyTTSImpl = new CachedPollyTTSCloudImpl(pollyTTSConfig, cacheFolder);
118 audioFormats.clear();
119 audioFormats.addAll(initAudioFormats());
122 voices.addAll(initVoices());
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);
133 public Set<Voice> getAvailableVoices() {
134 return Collections.unmodifiableSet(voices);
138 public Set<AudioFormat> getSupportedFormats() {
139 return Collections.unmodifiableSet(audioFormats);
143 * obtain audio stream from cache or Amazon Polly service and return it to play the audio
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(),
151 // Validate arguments
153 String text = inText.trim();
154 if (text == null || text.isEmpty()) {
155 throw new TTSException("The passed text is null or empty");
157 if (!voices.contains(voice)) {
158 throw new TTSException("The passed voice is unsupported");
160 boolean isAudioFormatSupported = audioFormats.stream()
161 .filter(audioFormat -> audioFormat.isCompatible(requestedFormat)).findAny().isPresent();
163 if (!isAudioFormatSupported) {
164 throw new TTSException("The passed AudioFormat is unsupported");
167 // now create the input stream for given text, locale, format. There is
168 // only a default voice
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");
175 logger.debug("Audio Stream for '{}' in format {}", text, requestedFormat);
176 AudioStream audioStream = new PollyTTSAudioStream(cacheAudioFile, requestedFormat);
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);
185 private Set<Voice> initVoices() {
187 return pollyTTSImpl.getAvailableLocales().stream()
189 pollyTTSImpl.getAvailableVoices(locale).stream()
190 .map(label -> new PollyTTSVoice(locale, label)))
195 private Set<AudioFormat> initAudioFormats() {
197 return pollyTTSImpl.getAvailableAudioFormats().stream()
198 .map(this::getAudioFormat)
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);
211 throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
215 private String getApiAudioFormat(AudioFormat format) {
216 if (!"default".equals(pollyTTSConfig.getAudioFormat())) {
217 // Override system specified with user preferred value
218 return pollyTTSConfig.getAudioFormat();
220 if (CODEC_MP3.equals(format.getCodec())) {
222 } else if (CODEC_VORBIS.equals(format.getCodec())) {
223 return CONTAINER_OGG;
225 throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
230 public String getId() {
235 public String getLabel(Locale locale) {