]> git.basschouten.com Git - openhab-addons.git/blob
de7cfb556cde909e601d83116a46d6fffc573423
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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                 .getInputStream(); 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     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");
91         }
92         File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioCodec.toLowerCase());
93         return audioFileInCache.exists() ? audioFileInCache : null;
94     }
95
96     /**
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".
100      *
101      * Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
102      */
103     private @Nullable String getUniqueFilenameForText(String text, String locale, String voice, String format) {
104         try {
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
111             // chars.
112             while (hashtext.length() < 32) {
113                 hashtext = "0" + hashtext;
114             }
115             String filename = locale + "_";
116             if (!DEFAULT_VOICE.equals(voice)) {
117                 filename += voice + "_";
118             }
119             filename += hashtext;
120             if (!Objects.equals(format, "44khz_16bit_mono")) {
121                 filename += "_" + format;
122             }
123             return filename;
124         } catch (NoSuchAlgorithmException ex) {
125             // should not happen
126             if (logging) {
127                 LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class).error("Could not create MD5 hash for '{}'", text,
128                         ex);
129             }
130             return null;
131         }
132     }
133
134     // helper methods
135
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);
139         while (read > 0) {
140             outputStream.write(bytes, 0, read);
141             read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
142         }
143     }
144
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));
148         }
149     }
150 }