]> git.basschouten.com Git - openhab-addons.git/blob
4582d8b48e2b3727d3c7222e014f62b0f5310a78
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.voicerss.internal.cloudapi;
14
15 import static java.util.stream.Collectors.toSet;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.UnsupportedEncodingException;
20 import java.net.HttpURLConnection;
21 import java.net.URL;
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;
29 import java.util.Set;
30 import java.util.stream.Stream;
31
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * This class implements the Cloud service from VoiceRSS. For more information,
37  * see API documentation at http://www.voicerss.org/api/documentation.aspx.
38  *
39  * Current state of implementation:
40  * <ul>
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>
45  * </ul>
46  *
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
50  */
51 public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
52
53     private final Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
54
55     private static final Set<String> SUPPORTED_AUDIO_FORMATS = Stream.of("MP3", "OGG", "AAC").collect(toSet());
56
57     private static final Set<Locale> SUPPORTED_LOCALES = new HashSet<>();
58     static {
59         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ar-eg"));
60         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ar-sa"));
61         SUPPORTED_LOCALES.add(Locale.forLanguageTag("bg-bg"));
62         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ca-es"));
63         SUPPORTED_LOCALES.add(Locale.forLanguageTag("cs-cz"));
64         SUPPORTED_LOCALES.add(Locale.forLanguageTag("da-dk"));
65         SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-at"));
66         SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-ch"));
67         SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-de"));
68         SUPPORTED_LOCALES.add(Locale.forLanguageTag("el-gr"));
69         SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-au"));
70         SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-ca"));
71         SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-gb"));
72         SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-ie"));
73         SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-in"));
74         SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-us"));
75         SUPPORTED_LOCALES.add(Locale.forLanguageTag("es-es"));
76         SUPPORTED_LOCALES.add(Locale.forLanguageTag("es-mx"));
77         SUPPORTED_LOCALES.add(Locale.forLanguageTag("fi-fi"));
78         SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-ca"));
79         SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-ch"));
80         SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-fr"));
81         SUPPORTED_LOCALES.add(Locale.forLanguageTag("he-il"));
82         SUPPORTED_LOCALES.add(Locale.forLanguageTag("hi-in"));
83         SUPPORTED_LOCALES.add(Locale.forLanguageTag("hr-hr"));
84         SUPPORTED_LOCALES.add(Locale.forLanguageTag("hu-hu"));
85         SUPPORTED_LOCALES.add(Locale.forLanguageTag("id-id"));
86         SUPPORTED_LOCALES.add(Locale.forLanguageTag("it-it"));
87         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ja-jp"));
88         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ko-kr"));
89         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ms-my"));
90         SUPPORTED_LOCALES.add(Locale.forLanguageTag("nb-no"));
91         SUPPORTED_LOCALES.add(Locale.forLanguageTag("nl-be"));
92         SUPPORTED_LOCALES.add(Locale.forLanguageTag("nl-nl"));
93         SUPPORTED_LOCALES.add(Locale.forLanguageTag("pl-pl"));
94         SUPPORTED_LOCALES.add(Locale.forLanguageTag("pt-br"));
95         SUPPORTED_LOCALES.add(Locale.forLanguageTag("pt-pt"));
96         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ro-ro"));
97         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ru-ru"));
98         SUPPORTED_LOCALES.add(Locale.forLanguageTag("sk-sk"));
99         SUPPORTED_LOCALES.add(Locale.forLanguageTag("sl-si"));
100         SUPPORTED_LOCALES.add(Locale.forLanguageTag("sv-se"));
101         SUPPORTED_LOCALES.add(Locale.forLanguageTag("ta-in"));
102         SUPPORTED_LOCALES.add(Locale.forLanguageTag("th-th"));
103         SUPPORTED_LOCALES.add(Locale.forLanguageTag("tr-tr"));
104         SUPPORTED_LOCALES.add(Locale.forLanguageTag("vi-vn"));
105         SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-cn"));
106         SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-hk"));
107         SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-tw"));
108     }
109
110     private static final Set<String> SUPPORTED_VOICES = Collections.singleton("VoiceRSS");
111
112     @Override
113     public Set<String> getAvailableAudioFormats() {
114         return SUPPORTED_AUDIO_FORMATS;
115     }
116
117     @Override
118     public Set<Locale> getAvailableLocales() {
119         return SUPPORTED_LOCALES;
120     }
121
122     @Override
123     public Set<String> getAvailableVoices() {
124         return SUPPORTED_VOICES;
125     }
126
127     @Override
128     public Set<String> getAvailableVoices(Locale locale) {
129         for (Locale voiceLocale : SUPPORTED_LOCALES) {
130             if (voiceLocale.toLanguageTag().equalsIgnoreCase(locale.toLanguageTag())) {
131                 return SUPPORTED_VOICES;
132             }
133         }
134         return new HashSet<>();
135     }
136
137     /**
138      * This method will return an input stream to an audio stream for the given
139      * parameters.
140      *
141      * It will do that using a plain URL connection to avoid any external
142      * dependencies.
143      */
144     @Override
145     public InputStream getTextToSpeech(String apiKey, String text, String locale, String audioFormat)
146             throws IOException {
147         String url = createURL(apiKey, text, locale, audioFormat);
148         logger.debug("Call {}", url);
149         URLConnection connection = new URL(url).openConnection();
150
151         // we will check return codes. The service will ALWAYS return a HTTP
152         // 200, but for error messages, it will return a text/plain format and
153         // the error message in body
154         int status = ((HttpURLConnection) connection).getResponseCode();
155         if (HttpURLConnection.HTTP_OK != status) {
156             logger.error("Call {} returned HTTP {}", url, status);
157             throw new IOException("Could not read from service: HTTP code " + status);
158         }
159         if (logger.isTraceEnabled()) {
160             for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
161                 logger.trace("Response.header: {}={}", header.getKey(), header.getValue());
162             }
163         }
164         String contentType = connection.getHeaderField("Content-Type");
165         InputStream is = connection.getInputStream();
166         // check if content type is text/plain, then we have an error
167         if (contentType.contains("text/plain")) {
168             byte[] bytes = new byte[256];
169             is.read(bytes, 0, 256);
170             // close before throwing an exception
171             try {
172                 is.close();
173             } catch (IOException ex) {
174                 logger.debug("Failed to close inputstream", ex);
175             }
176             throw new IOException(
177                     "Could not read audio content, service return an error: " + new String(bytes, "UTF-8"));
178         } else {
179             return is;
180         }
181     }
182
183     // internal
184
185     /**
186      * This method will create the URL for the cloud service. The text will be
187      * URI encoded as it is part of the URL.
188      *
189      * It is in package scope to be accessed by tests.
190      */
191     private String createURL(String apiKey, String text, String locale, String audioFormat) {
192         String encodedMsg;
193         try {
194             encodedMsg = URLEncoder.encode(text, "UTF-8");
195         } catch (UnsupportedEncodingException ex) {
196             logger.error("UnsupportedEncodingException for UTF-8 MUST NEVER HAPPEN! Check your JVM configuration!", ex);
197             // fall through and use msg un-encoded
198             encodedMsg = text;
199         }
200         return "http://api.voicerss.org/?key=" + apiKey + "&hl=" + locale + "&c=" + audioFormat
201                 + "&f=44khz_16bit_mono&src=" + encodedMsg;
202     }
203 }