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.voicerss.internal.cloudapi;
16 import java.io.FileOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.math.BigInteger;
21 import java.nio.charset.StandardCharsets;
22 import java.security.MessageDigest;
23 import java.security.NoSuchAlgorithmException;
24 import java.util.Objects;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.slf4j.LoggerFactory;
31 * This class implements a cache for the retrieved audio data. It will preserve
32 * them in file system, as audio files with an additional .txt file to indicate
33 * what content is in the audio file.
35 * @author Jochen Hiller - Initial contribution
38 public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
43 private static final int READ_BUFFER_SIZE = 4096;
45 private final File cacheFolder;
47 public CachedVoiceRSSCloudImpl(String cacheFolderName, boolean logging) throws IllegalStateException {
49 if (cacheFolderName.isBlank()) {
50 throw new IllegalStateException("Folder for cache must be defined");
52 // Lazy create the cache folder
53 cacheFolder = new File(cacheFolderName);
54 if (!cacheFolder.exists()) {
59 public File getTextToSpeechAsFile(String apiKey, String text, String locale, String voice, String audioCodec,
60 String audioFormat) throws IOException {
61 String fileNameInCache = getUniqueFilenameForText(text, locale, voice, audioFormat);
62 if (fileNameInCache == null) {
63 throw new IOException("Could not infer cache file name");
66 File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioCodec.toLowerCase());
67 if (audioFileInCache.exists()) {
68 return audioFileInCache;
71 // if not in cache, get audio data and put to cache
72 try (InputStream is = super.getTextToSpeech(apiKey, text, locale, voice, audioCodec, audioFormat);
73 FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
75 // write text to file for transparency too
76 // this allows to know which contents is in which audio file
77 File txtFileInCache = new File(cacheFolder, fileNameInCache + ".txt");
78 writeText(txtFileInCache, text);
80 return audioFileInCache;
81 } catch (IOException ex) {
82 throw new IOException("Could not write to cache file: " + ex.getMessage(), ex);
87 * Gets a unique filename for a give text, by creating a MD5 hash of it. It
88 * will be preceded by the locale and suffixed by the format if it is not the
89 * default of "44khz_16bit_mono".
91 * Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
93 private @Nullable String getUniqueFilenameForText(String text, String locale, String voice, String format) {
95 byte[] bytesOfMessage = text.getBytes(StandardCharsets.UTF_8);
96 MessageDigest md = MessageDigest.getInstance("MD5");
97 byte[] md5Hash = md.digest(bytesOfMessage);
98 BigInteger bigInt = new BigInteger(1, md5Hash);
99 String hashtext = bigInt.toString(16);
100 // Now we need to zero pad it if you actually want the full 32
102 while (hashtext.length() < 32) {
103 hashtext = "0" + hashtext;
105 String filename = locale + "_";
106 if (!DEFAULT_VOICE.equals(voice)) {
107 filename += voice + "_";
109 filename += hashtext;
110 if (!Objects.equals(format, "44khz_16bit_mono")) {
111 filename += "_" + format;
114 } catch (NoSuchAlgorithmException ex) {
117 LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class).error("Could not create MD5 hash for '{}'", text,
126 private void copyStream(InputStream inputStream, OutputStream outputStream) throws IOException {
127 byte[] bytes = new byte[READ_BUFFER_SIZE];
128 int read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
130 outputStream.write(bytes, 0, read);
131 read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
135 private void writeText(File file, String text) throws IOException {
136 try (OutputStream outputStream = new FileOutputStream(file)) {
137 outputStream.write(text.getBytes(StandardCharsets.UTF_8));