]> git.basschouten.com Git - openhab-addons.git/blob
ba6f8fdb81b9d296cbd80cd2e7a140f0a96e6f77
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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 java.io.File;
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;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.slf4j.LoggerFactory;
29
30 /**
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.
34  *
35  * @author Jochen Hiller - Initial contribution
36  */
37 @NonNullByDefault
38 public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
39
40     /**
41      * Stream buffer size
42      */
43     private static final int READ_BUFFER_SIZE = 4096;
44
45     private final File cacheFolder;
46
47     public CachedVoiceRSSCloudImpl(String cacheFolderName, boolean logging) throws IllegalStateException {
48         super(logging);
49         if (cacheFolderName.isBlank()) {
50             throw new IllegalStateException("Folder for cache must be defined");
51         }
52         // Lazy create the cache folder
53         cacheFolder = new File(cacheFolderName);
54         if (!cacheFolder.exists()) {
55             cacheFolder.mkdirs();
56         }
57     }
58
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");
64         }
65         // check if in cache
66         File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioCodec.toLowerCase());
67         if (audioFileInCache.exists()) {
68             return audioFileInCache;
69         }
70
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)) {
74             copyStream(is, fos);
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);
79             // return from cache
80             return audioFileInCache;
81         } catch (IOException ex) {
82             throw new IOException("Could not write to cache file: " + ex.getMessage(), ex);
83         }
84     }
85
86     /**
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".
90      *
91      * Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
92      */
93     private @Nullable String getUniqueFilenameForText(String text, String locale, String voice, String format) {
94         try {
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
101             // chars.
102             while (hashtext.length() < 32) {
103                 hashtext = "0" + hashtext;
104             }
105             String filename = locale + "_";
106             if (!DEFAULT_VOICE.equals(voice)) {
107                 filename += voice + "_";
108             }
109             filename += hashtext;
110             if (!Objects.equals(format, "44khz_16bit_mono")) {
111                 filename += "_" + format;
112             }
113             return filename;
114         } catch (NoSuchAlgorithmException ex) {
115             // should not happen
116             if (logging) {
117                 LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class).error("Could not create MD5 hash for '{}'", text,
118                         ex);
119             }
120             return null;
121         }
122     }
123
124     // helper methods
125
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);
129         while (read > 0) {
130             outputStream.write(bytes, 0, read);
131             read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
132         }
133     }
134
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));
138         }
139     }
140 }