]> git.basschouten.com Git - openhab-addons.git/blob
6a3d5719077b09acc5826c5bdff40337459941ae
[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.pollytts.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.Date;
25 import java.util.concurrent.TimeUnit;
26
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 /**
31  * This class implements a cache for the retrieved audio data. It will preserve them in the file system,
32  * as audio files with an additional .txt file to indicate what content is in the audio file.
33  *
34  * @author Robert Hillman - Initial contribution
35  */
36 public class CachedPollyTTSCloudImpl extends PollyTTSCloudImpl {
37
38     private static final int READ_BUFFER_SIZE = 4096;
39
40     private final Logger logger = LoggerFactory.getLogger(CachedPollyTTSCloudImpl.class);
41
42     private final File cacheFolder;
43
44     /**
45      * Create the file folder to hold the the cached speech files.
46      * check to make sure the directory exist and
47      * create it if necessary
48      */
49     public CachedPollyTTSCloudImpl(PollyTTSConfig config, File cacheFolder) throws IOException {
50         super(config);
51         this.cacheFolder = cacheFolder;
52     }
53
54     /**
55      * Fetch the specified text as an audio file.
56      * The audio file will be obtained from the cached folder if it
57      * exist or generated by use to the external voice service.
58      * The cached file txt description time stamp will be updated
59      * to identify last use.
60      */
61     public File getTextToSpeechAsFile(String text, String label, String audioFormat) throws IOException {
62         String fileNameInCache = getUniqueFilenameForText(text, label);
63         // check if in cache
64         File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioFormat.toLowerCase());
65         if (audioFileInCache.exists()) {
66             // update use date
67             updateTimeStamp(audioFileInCache);
68             updateTimeStamp(new File(cacheFolder, fileNameInCache + ".txt"));
69             purgeAgedFiles();
70             return audioFileInCache;
71         }
72
73         // if not in cache, get audio data and put to cache
74         try (InputStream is = getTextToSpeech(text, label, audioFormat);
75                 FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
76             copyStream(is, fos);
77             // write text to file for transparency too
78             // this allows to know which contents is in which audio file
79             File txtFileInCache = new File(cacheFolder, fileNameInCache + ".txt");
80             writeText(txtFileInCache, text);
81             // return from cache
82             return audioFileInCache;
83         } catch (IOException ex) {
84             logger.warn("Could not write {} to cache, return null", audioFileInCache, ex);
85             return null;
86         }
87     }
88
89     /**
90      * Gets a unique filename for a give text, by creating a MD5 hash of it. It
91      * will be preceded by the voice label.
92      *
93      * Sample: "Robert_00a2653ac5f77063bc4ea2fee87318d3"
94      */
95     private String getUniqueFilenameForText(String text, String label) {
96         MessageDigest md;
97         try {
98             md = MessageDigest.getInstance("MD5");
99         } catch (NoSuchAlgorithmException ex) {
100             logger.error("Could not create MD5 hash for '{}'", text, ex);
101             return null;
102         }
103         byte[] md5Hash = md.digest(text.getBytes(StandardCharsets.UTF_8));
104         BigInteger bigInt = new BigInteger(1, md5Hash);
105         String hashtext = bigInt.toString(16);
106         // Now we need to zero pad it if you actually want the full 32
107         // chars.
108         while (hashtext.length() < 32) {
109             hashtext = "0" + hashtext;
110         }
111         String fileName = label + "_" + hashtext;
112         return fileName;
113     }
114
115     // helper methods
116
117     private void copyStream(InputStream inputStream, OutputStream outputStream) throws IOException {
118         byte[] bytes = new byte[READ_BUFFER_SIZE];
119         int read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
120         while (read > 0) {
121             outputStream.write(bytes, 0, read);
122             read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
123         }
124     }
125
126     private void writeText(File file, String text) throws IOException {
127         try (OutputStream outputStream = new FileOutputStream(file)) {
128             outputStream.write(text.getBytes(StandardCharsets.UTF_8));
129         }
130     }
131
132     private void updateTimeStamp(File file) throws IOException {
133         // update use date for cache management
134         file.setLastModified(System.currentTimeMillis());
135     }
136
137     private void purgeAgedFiles() throws IOException {
138         // just exit if expiration set to 0/disabled
139         if (config.getExpireDate() == 0) {
140             return;
141         }
142         long now = new Date().getTime();
143         long diff = now - config.getLastDelete();
144         // only execute ~ once every 2 days if cache called
145         long oneDayMillis = TimeUnit.DAYS.toMillis(1);
146         logger.debug("PollyTTS cache cleaner lastdelete {}", diff);
147         if (diff > (2 * oneDayMillis)) {
148             config.setLastDelete(now);
149             long xDaysAgo = config.getExpireDate() * oneDayMillis;
150             // Now search folders and delete old files
151             int filesDeleted = 0;
152             for (File file : cacheFolder.listFiles()) {
153                 diff = now - file.lastModified();
154                 if (diff > xDaysAgo) {
155                     filesDeleted++;
156                     file.delete();
157                 }
158             }
159             logger.debug("PollyTTS cache cleaner deleted '{}' aged files", filesDeleted);
160         }
161     }
162 }