Skip to content

Commit

Permalink
[googletts] Improve exception handling (openhab#11925)
Browse files Browse the repository at this point in the history
* Improve exception handling
* Moved classes to dto package to reduce SAT warning

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
Signed-off-by: Andras Uhrin <andras.uhrin@gmail.com>
  • Loading branch information
cweitkamp authored and andrasU committed Nov 12, 2022
1 parent bdda029 commit 18dd51b
Show file tree
Hide file tree
Showing 13 changed files with 96 additions and 100 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -37,28 +36,31 @@
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.auth.AuthenticationException;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.i18n.CommunicationException;
import org.openhab.core.io.net.http.HttpRequestBuilder;
import org.openhab.voice.googletts.internal.protocol.AudioConfig;
import org.openhab.voice.googletts.internal.protocol.AudioEncoding;
import org.openhab.voice.googletts.internal.protocol.ListVoicesResponse;
import org.openhab.voice.googletts.internal.protocol.SsmlVoiceGender;
import org.openhab.voice.googletts.internal.protocol.SynthesisInput;
import org.openhab.voice.googletts.internal.protocol.SynthesizeSpeechRequest;
import org.openhab.voice.googletts.internal.protocol.SynthesizeSpeechResponse;
import org.openhab.voice.googletts.internal.protocol.Voice;
import org.openhab.voice.googletts.internal.protocol.VoiceSelectionParams;
import org.openhab.voice.googletts.internal.dto.AudioConfig;
import org.openhab.voice.googletts.internal.dto.AudioEncoding;
import org.openhab.voice.googletts.internal.dto.ListVoicesResponse;
import org.openhab.voice.googletts.internal.dto.SsmlVoiceGender;
import org.openhab.voice.googletts.internal.dto.SynthesisInput;
import org.openhab.voice.googletts.internal.dto.SynthesizeSpeechRequest;
import org.openhab.voice.googletts.internal.dto.SynthesizeSpeechResponse;
import org.openhab.voice.googletts.internal.dto.Voice;
import org.openhab.voice.googletts.internal.dto.VoiceSelectionParams;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;

/**
* Google Cloud TTS API call implementation.
Expand Down Expand Up @@ -152,8 +154,8 @@ void setConfig(GoogleTTSConfig config) {
getAccessToken();
initialized = true;
initVoices();
} catch (AuthenticationException | IOException ex) {
logger.warn("Error initializing Google Cloud TTS service: {}", ex.getMessage());
} catch (AuthenticationException | CommunicationException e) {
logger.warn("Error initializing Google Cloud TTS service: {}", e.getMessage());
oAuthService = null;
initialized = false;
voices.clear();
Expand All @@ -177,17 +179,24 @@ void setConfig(GoogleTTSConfig config) {
/**
* Fetches the OAuth2 tokens from Google Cloud Platform if the auth-code is set in the configuration. If successful
* the auth-code will be removed from the configuration.
*
* @throws AuthenticationException
* @throws CommunicationException
*/
private void getAccessToken() throws AuthenticationException, IOException {
@SuppressWarnings("null")
private void getAccessToken() throws AuthenticationException, CommunicationException {
String authcode = config.authcode;
if (authcode != null && !authcode.isEmpty()) {
logger.debug("Trying to get access and refresh tokens.");
try {
oAuthService.getAccessTokenResponseByAuthorizationCode(authcode, GCP_REDIRECT_URI);
} catch (OAuthException | OAuthResponseException ex) {
logger.debug("Error fetching access token: {}", ex.getMessage(), ex);
} catch (OAuthException | OAuthResponseException e) {
logger.debug("Error fetching access token: {}", e.getMessage(), e);
throw new AuthenticationException(
"Error fetching access token. Invalid authcode? Please generate a new one.");
} catch (IOException e) {
throw new CommunicationException(
String.format("An unexpected IOException occurred: %s", e.getMessage()));
}

config.authcode = null;
Expand All @@ -207,14 +216,17 @@ private void getAccessToken() throws AuthenticationException, IOException {
}
}

private String getAuthorizationHeader() throws AuthenticationException, IOException {
@SuppressWarnings("null")
private String getAuthorizationHeader() throws AuthenticationException, CommunicationException {
final AccessTokenResponse accessTokenResponse;
try {
accessTokenResponse = oAuthService.getAccessTokenResponse();
} catch (OAuthException | OAuthResponseException ex) {
logger.debug("Error fetching access token: {}", ex.getMessage(), ex);
} catch (OAuthException | OAuthResponseException e) {
logger.debug("Error fetching access token: {}", e.getMessage(), e);
throw new AuthenticationException(
"Error fetching access token. Invalid authcode? Please generate a new one.");
} catch (IOException e) {
throw new CommunicationException(String.format("An unexpected IOException occurred: %s", e.getMessage()));
}
if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
|| accessTokenResponse.getAccessToken().isEmpty()) {
Expand Down Expand Up @@ -255,13 +267,16 @@ Set<Locale> getSupportedLocales() {
*/
Set<GoogleTTSVoice> getVoicesForLocale(Locale locale) {
Set<GoogleTTSVoice> localeVoices = voices.get(locale);
return localeVoices != null ? localeVoices : Collections.emptySet();
return localeVoices != null ? localeVoices : Set.of();
}

/**
* Google API call to load locales and voices.
*
* @throws AuthenticationException
* @throws CommunicationException
*/
private void initVoices() throws AuthenticationException, IOException {
private void initVoices() throws AuthenticationException, CommunicationException {
if (oAuthService != null) {
voices.clear();
for (GoogleTTSVoice voice : listVoices()) {
Expand All @@ -281,25 +296,32 @@ private void initVoices() throws AuthenticationException, IOException {
}

@SuppressWarnings("null")
private List<GoogleTTSVoice> listVoices() throws AuthenticationException, IOException {
private List<GoogleTTSVoice> listVoices() throws AuthenticationException, CommunicationException {
HttpRequestBuilder builder = HttpRequestBuilder.getFrom(LIST_VOICES_URL)
.withHeader(HttpHeader.AUTHORIZATION.name(), getAuthorizationHeader());

ListVoicesResponse listVoicesResponse = gson.fromJson(builder.getContentAsString(), ListVoicesResponse.class);
try {
ListVoicesResponse listVoicesResponse = gson.fromJson(builder.getContentAsString(),
ListVoicesResponse.class);

if (listVoicesResponse == null || listVoicesResponse.getVoices() == null) {
return Collections.emptyList();
}
if (listVoicesResponse == null || listVoicesResponse.getVoices() == null) {
return List.of();
}

List<GoogleTTSVoice> result = new ArrayList<>();
for (Voice voice : listVoicesResponse.getVoices()) {
for (String languageCode : voice.getLanguageCodes()) {
result.add(new GoogleTTSVoice(Locale.forLanguageTag(languageCode), voice.getName(),
voice.getSsmlGender().name()));
List<GoogleTTSVoice> result = new ArrayList<>();
for (Voice voice : listVoicesResponse.getVoices()) {
for (String languageCode : voice.getLanguageCodes()) {
result.add(new GoogleTTSVoice(Locale.forLanguageTag(languageCode), voice.getName(),
voice.getSsmlGender().name()));
}
}
return result;
} catch (JsonSyntaxException e) {
// do nothing
} catch (IOException e) {
throw new CommunicationException(String.format("An unexpected IOException occurred: %s", e.getMessage()));
}

return result;
return List.of();
}

/**
Expand All @@ -319,7 +341,7 @@ private String[] getFormatForCodec(String codec) {
}
}

byte[] synthesizeSpeech(String text, GoogleTTSVoice voice, String codec) {
public byte[] synthesizeSpeech(String text, GoogleTTSVoice voice, String codec) {
String[] format = getFormatForCodec(codec);
String fileNameInCache = getUniqueFilenameForText(text, voice.getTechnicalName());
File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + format[1]);
Expand All @@ -336,19 +358,17 @@ byte[] synthesizeSpeech(String text, GoogleTTSVoice voice, String codec) {
saveAudioAndTextToFile(text, audioFileInCache, audio, voice.getTechnicalName());
}
return audio;
} catch (AuthenticationException ex) {
logger.warn("Error initializing Google Cloud TTS service: {}", ex.getMessage());
} catch (AuthenticationException | CommunicationException e) {
logger.warn("Error initializing Google Cloud TTS service: {}", e.getMessage());
oAuthService = null;
initialized = false;
voices.clear();
return null;
} catch (FileNotFoundException ex) {
logger.warn("Could not write {} to cache", audioFileInCache, ex);
return null;
} catch (IOException ex) {
logger.error("Could not write {} to cache", audioFileInCache, ex);
return null;
} catch (FileNotFoundException e) {
logger.warn("Could not write file {} to cache: {}", audioFileInCache, e.getMessage());
} catch (IOException e) {
logger.debug("An unexpected IOException occurred: {}", e.getMessage());
}
return null;
}

/**
Expand All @@ -358,10 +378,11 @@ byte[] synthesizeSpeech(String text, GoogleTTSVoice voice, String codec) {
* @param cacheFile Cache entry file.
* @param audio Byte array of the audio.
* @param voiceName Used voice
* @throws FileNotFoundException
* @throws IOException in case of file handling exceptions
*/
private void saveAudioAndTextToFile(String text, File cacheFile, byte[] audio, String voiceName)
throws IOException {
throws IOException, FileNotFoundException {
logger.debug("Caching audio file {}", cacheFile.getName());
try (FileOutputStream audioFileOutputStream = new FileOutputStream(cacheFile)) {
audioFileOutputStream.write(audio);
Expand Down Expand Up @@ -405,10 +426,12 @@ private String removeExtension(String fileName) {
* @param voice Voice parameter
* @param audioFormat Audio encoding format
* @return Audio input stream or {@code null} when encoding exceptions occur
* @throws AuthenticationException
* @throws CommunicationException
*/
@SuppressWarnings({ "null", "unused" })
@SuppressWarnings("null")
private byte[] synthesizeSpeechByGoogle(String text, GoogleTTSVoice voice, String audioFormat)
throws AuthenticationException, IOException {
throws AuthenticationException, CommunicationException {
AudioConfig audioConfig = new AudioConfig(AudioEncoding.valueOf(audioFormat), config.pitch, config.speakingRate,
config.volumeGainDb);
SynthesisInput synthesisInput = new SynthesisInput(text);
Expand All @@ -422,15 +445,22 @@ private byte[] synthesizeSpeechByGoogle(String text, GoogleTTSVoice voice, Strin
.withHeader(HttpHeader.AUTHORIZATION.name(), getAuthorizationHeader())
.withContent(gson.toJson(request), MimeTypes.Type.APPLICATION_JSON.name());

SynthesizeSpeechResponse synthesizeSpeechResponse = gson.fromJson(builder.getContentAsString(),
SynthesizeSpeechResponse.class);
try {
SynthesizeSpeechResponse synthesizeSpeechResponse = gson.fromJson(builder.getContentAsString(),
SynthesizeSpeechResponse.class);

if (synthesizeSpeechResponse == null) {
return null;
}
if (synthesizeSpeechResponse == null) {
return null;
}

byte[] encodedBytes = synthesizeSpeechResponse.getAudioContent().getBytes(StandardCharsets.UTF_8);
return Base64.getDecoder().decode(encodedBytes);
byte[] encodedBytes = synthesizeSpeechResponse.getAudioContent().getBytes(StandardCharsets.UTF_8);
return Base64.getDecoder().decode(encodedBytes);
} catch (JsonSyntaxException e) {
// do nothing
} catch (IOException e) {
throw new CommunicationException(String.format("An unexpected IOException occurred: %s", e.getMessage()));
}
return null;
}

/**
Expand All @@ -445,9 +475,9 @@ private String getUniqueFilenameForText(String text, String voiceName) {
byte[] bytesOfMessage = (config.toConfigString() + text).getBytes(StandardCharsets.UTF_8);
String fileNameHash = String.format("%032x", new BigInteger(1, md.digest(bytesOfMessage)));
return voiceName + "_" + fileNameHash;
} catch (NoSuchAlgorithmException ex) {
} catch (NoSuchAlgorithmException e) {
// should not happen
logger.error("Could not create MD5 hash for '{}'", text, ex);
logger.error("Could not create MD5 hash for '{}'", text, e);
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import org.openhab.core.voice.TTSException;
import org.openhab.core.voice.TTSService;
import org.openhab.core.voice.Voice;
import org.openhab.voice.googletts.internal.protocol.AudioEncoding;
import org.openhab.voice.googletts.internal.dto.AudioEncoding;
import org.osgi.framework.Constants;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Activate;
Expand Down Expand Up @@ -330,7 +330,7 @@ public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFor
// create the audio byte array for given text, locale, format
byte[] audio = apiImpl.synthesizeSpeech(trimmedText, (GoogleTTSVoice) voice, requestedFormat.getCodec());
if (audio == null) {
throw new TTSException("Could not read from Google Cloud TTS Service");
throw new TTSException("Could not synthesize text via Google Cloud TTS Service");
}
return new ByteArrayAudioStream(audio, requestedFormat);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.voice.Voice;
import org.openhab.voice.googletts.internal.protocol.SsmlVoiceGender;
import org.openhab.voice.googletts.internal.dto.SsmlVoiceGender;

/**
* Implementation of the Voice interface for Google Cloud TTS Service.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
package org.openhab.voice.googletts.internal.dto;

/**
* The configuration of the synthesized audio.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
package org.openhab.voice.googletts.internal.dto;

/**
* Configuration to set up audio encoder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
package org.openhab.voice.googletts.internal.dto;

import java.util.List;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
package org.openhab.voice.googletts.internal.dto;

/**
* Gender of the voice as described in SSML voice element.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
package org.openhab.voice.googletts.internal.dto;

/**
* Contains text input to be synthesized. Either text or ssml must be supplied. Supplying both or neither returns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
package org.openhab.voice.googletts.internal.dto;

/**
* Synthesizes speech synchronously: receive results after all text input has been processed.
Expand Down
Loading

0 comments on commit 18dd51b

Please sign in to comment.