2 * Copyright (c) 2010-2023 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.*;
19 import java.io.InputStream;
20 import java.util.Collections;
21 import java.util.HashSet;
22 import java.util.Locale;
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;
44 import com.amazonaws.services.polly.model.AmazonPollyException;
47 * This is a TTS service implementation for using Polly Text-to-Speech.
49 * @author Robert Hillman - Initial contribution
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 {
58 public PollyTTSService(final @Reference TTSCache ttsCache) {
65 static final String SERVICE_NAME = "Polly";
70 static final String SERVICE_ID = "pollytts";
75 static final String SERVICE_CATEGORY = "voice";
80 static final String SERVICE_PID = "org.openhab." + SERVICE_CATEGORY + "." + SERVICE_ID;
82 private final Logger logger = LoggerFactory.getLogger(PollyTTSService.class);
84 private PollyTTSCloudImpl 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 pollyTTSImpl = new PollyTTSCloudImpl(pollyTTSConfig);
111 audioFormats.clear();
112 audioFormats.addAll(initAudioFormats());
115 voices.addAll(initVoices());
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);
126 public Set<Voice> getAvailableVoices() {
127 return Collections.unmodifiableSet(voices);
131 public Set<AudioFormat> getSupportedFormats() {
132 return Collections.unmodifiableSet(audioFormats);
136 * obtain audio stream from cache or Amazon Polly service and return it to play the audio
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(),
144 // Validate arguments
146 String text = inText.trim();
147 if (text.isEmpty()) {
148 throw new TTSException("The passed text is empty");
150 if (!voices.contains(voice)) {
151 throw new TTSException("The passed voice is unsupported");
153 boolean isAudioFormatSupported = audioFormats.stream()
154 .filter(audioFormat -> audioFormat.isCompatible(requestedFormat)).findAny().isPresent();
156 if (!isAudioFormatSupported) {
157 throw new TTSException("The passed AudioFormat is unsupported");
160 // now create the input stream for given text, locale, format. There is
161 // only a default voice
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");
168 logger.debug("Audio Stream for '{}' in format {}", text, requestedFormat);
169 AudioStream audioStream = new PollyTTSAudioStream(pollyAudioStream, requestedFormat);
171 } catch (AmazonPollyException ex) {
172 throw new TTSException("Could not read from PollyTTS service: " + ex.getMessage(), ex);
176 private Set<Voice> initVoices() {
178 return pollyTTSImpl.getAvailableLocales().stream()
180 pollyTTSImpl.getAvailableVoices(locale).stream()
181 .map(label -> new PollyTTSVoice(locale, label)))
186 private Set<AudioFormat> initAudioFormats() {
188 return pollyTTSImpl.getAvailableAudioFormats().stream()
189 .map(this::getAudioFormat)
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);
202 throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
206 private String getApiAudioFormat(AudioFormat format) {
207 if (!"default".equals(pollyTTSConfig.getAudioFormat())) {
208 // Override system specified with user preferred value
209 return pollyTTSConfig.getAudioFormat();
211 if (CODEC_MP3.equals(format.getCodec())) {
213 } else if (CODEC_VORBIS.equals(format.getCodec())) {
214 return CONTAINER_OGG;
216 throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
221 public String getId() {
226 public String getLabel(Locale locale) {