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.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 .getInputStream(); 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);
86 public @Nullable File getTextToSpeechInCache(String text, String locale, String voice, String audioCodec,
87 String audioFormat) throws IOException {
88 String fileNameInCache = getUniqueFilenameForText(text, locale, voice, audioFormat);
89 if (fileNameInCache == null) {
90 throw new IOException("Could not infer cache file name");
92 File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioCodec.toLowerCase());
93 return audioFileInCache.exists() ? audioFileInCache : null;
97 * Gets a unique filename for a give text, by creating a MD5 hash of it. It
98 * will be preceded by the locale and suffixed by the format if it is not the
99 * default of "44khz_16bit_mono".
101 * Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
103 private @Nullable String getUniqueFilenameForText(String text, String locale, String voice, String format) {
105 byte[] bytesOfMessage = text.getBytes(StandardCharsets.UTF_8);
106 MessageDigest md = MessageDigest.getInstance("MD5");
107 byte[] md5Hash = md.digest(bytesOfMessage);
108 BigInteger bigInt = new BigInteger(1, md5Hash);
109 String hashtext = bigInt.toString(16);
110 // Now we need to zero pad it if you actually want the full 32
112 while (hashtext.length() < 32) {
113 hashtext = "0" + hashtext;
115 String filename = locale + "_";
116 if (!DEFAULT_VOICE.equals(voice)) {
117 filename += voice + "_";
119 filename += hashtext;
120 if (!Objects.equals(format, "44khz_16bit_mono")) {
121 filename += "_" + format;
124 } catch (NoSuchAlgorithmException ex) {
127 LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class).error("Could not create MD5 hash for '{}'", text,
136 private void copyStream(InputStream inputStream, OutputStream outputStream) throws IOException {
137 byte[] bytes = new byte[READ_BUFFER_SIZE];
138 int read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
140 outputStream.write(bytes, 0, read);
141 read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
145 private void writeText(File file, String text) throws IOException {
146 try (OutputStream outputStream = new FileOutputStream(file)) {
147 outputStream.write(text.getBytes(StandardCharsets.UTF_8));