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.voicerss.internal.cloudapi;
15 import static java.util.stream.Collectors.toSet;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.UnsupportedEncodingException;
20 import java.net.HttpURLConnection;
22 import java.net.URLConnection;
23 import java.net.URLEncoder;
24 import java.util.Collections;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map.Entry;
30 import java.util.stream.Stream;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * This class implements the Cloud service from VoiceRSS. For more information,
37 * see API documentation at http://www.voicerss.org/api/documentation.aspx.
39 * Current state of implementation:
41 * <li>All API languages supported</li>
42 * <li>Only default voice supported with good audio quality</li>
43 * <li>Only MP3, OGG and AAC audio formats supported</li>
44 * <li>It uses HTTP and not HTTPS (for performance reasons)</li>
47 * @author Jochen Hiller - Initial contribution
48 * @author Laurent Garnier - add support for all API languages
49 * @author Laurent Garnier - add support for OGG and AAC audio formats
51 public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
53 private final Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
55 private static final Set<String> SUPPORTED_AUDIO_FORMATS = Stream.of("MP3", "OGG", "AAC").collect(toSet());
57 private static final Set<Locale> SUPPORTED_LOCALES = new HashSet<>();
59 SUPPORTED_LOCALES.add(Locale.forLanguageTag("ca-es"));
60 SUPPORTED_LOCALES.add(Locale.forLanguageTag("da-dk"));
61 SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-de"));
62 SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-au"));
63 SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-ca"));
64 SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-gb"));
65 SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-in"));
66 SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-us"));
67 SUPPORTED_LOCALES.add(Locale.forLanguageTag("es-es"));
68 SUPPORTED_LOCALES.add(Locale.forLanguageTag("es-mx"));
69 SUPPORTED_LOCALES.add(Locale.forLanguageTag("fi-fi"));
70 SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-ca"));
71 SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-fr"));
72 SUPPORTED_LOCALES.add(Locale.forLanguageTag("it-it"));
73 SUPPORTED_LOCALES.add(Locale.forLanguageTag("ja-jp"));
74 SUPPORTED_LOCALES.add(Locale.forLanguageTag("ko-kr"));
75 SUPPORTED_LOCALES.add(Locale.forLanguageTag("nb-no"));
76 SUPPORTED_LOCALES.add(Locale.forLanguageTag("nl-nl"));
77 SUPPORTED_LOCALES.add(Locale.forLanguageTag("pl-pl"));
78 SUPPORTED_LOCALES.add(Locale.forLanguageTag("pt-br"));
79 SUPPORTED_LOCALES.add(Locale.forLanguageTag("pt-pt"));
80 SUPPORTED_LOCALES.add(Locale.forLanguageTag("ru-ru"));
81 SUPPORTED_LOCALES.add(Locale.forLanguageTag("sv-se"));
82 SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-cn"));
83 SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-hk"));
84 SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-tw"));
87 private static final Set<String> SUPPORTED_VOICES = Collections.singleton("VoiceRSS");
90 public Set<String> getAvailableAudioFormats() {
91 return SUPPORTED_AUDIO_FORMATS;
95 public Set<Locale> getAvailableLocales() {
96 return SUPPORTED_LOCALES;
100 public Set<String> getAvailableVoices() {
101 return SUPPORTED_VOICES;
105 public Set<String> getAvailableVoices(Locale locale) {
106 for (Locale voiceLocale : SUPPORTED_LOCALES) {
107 if (voiceLocale.toLanguageTag().equalsIgnoreCase(locale.toLanguageTag())) {
108 return SUPPORTED_VOICES;
111 return new HashSet<>();
115 * This method will return an input stream to an audio stream for the given
118 * It will do that using a plain URL connection to avoid any external
122 public InputStream getTextToSpeech(String apiKey, String text, String locale, String audioFormat)
124 String url = createURL(apiKey, text, locale, audioFormat);
125 logger.debug("Call {}", url);
126 URLConnection connection = new URL(url).openConnection();
128 // we will check return codes. The service will ALWAYS return a HTTP
129 // 200, but for error messages, it will return a text/plain format and
130 // the error message in body
131 int status = ((HttpURLConnection) connection).getResponseCode();
132 if (HttpURLConnection.HTTP_OK != status) {
133 logger.error("Call {} returned HTTP {}", url, status);
134 throw new IOException("Could not read from service: HTTP code " + status);
136 if (logger.isTraceEnabled()) {
137 for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
138 logger.trace("Response.header: {}={}", header.getKey(), header.getValue());
141 String contentType = connection.getHeaderField("Content-Type");
142 InputStream is = connection.getInputStream();
143 // check if content type is text/plain, then we have an error
144 if (contentType.contains("text/plain")) {
145 byte[] bytes = new byte[256];
146 is.read(bytes, 0, 256);
147 // close before throwing an exception
150 } catch (IOException ex) {
151 logger.debug("Failed to close inputstream", ex);
153 throw new IOException(
154 "Could not read audio content, service return an error: " + new String(bytes, "UTF-8"));
163 * This method will create the URL for the cloud service. The text will be
164 * URI encoded as it is part of the URL.
166 * It is in package scope to be accessed by tests.
168 private String createURL(String apiKey, String text, String locale, String audioFormat) {
171 encodedMsg = URLEncoder.encode(text, "UTF-8");
172 } catch (UnsupportedEncodingException ex) {
173 logger.error("UnsupportedEncodingException for UTF-8 MUST NEVER HAPPEN! Check your JVM configuration!", ex);
174 // fall through and use msg un-encoded
177 return "http://api.voicerss.org/?key=" + apiKey + "&hl=" + locale + "&c=" + audioFormat
178 + "&f=44khz_16bit_mono&src=" + encodedMsg;