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.pollytts.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.Date;
25 import java.util.concurrent.TimeUnit;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
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.
34 * @author Robert Hillman - Initial contribution
36 public class CachedPollyTTSCloudImpl extends PollyTTSCloudImpl {
38 private static final int READ_BUFFER_SIZE = 4096;
40 private final Logger logger = LoggerFactory.getLogger(CachedPollyTTSCloudImpl.class);
42 private final File cacheFolder;
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
49 public CachedPollyTTSCloudImpl(PollyTTSConfig config, File cacheFolder) throws IOException {
51 this.cacheFolder = cacheFolder;
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.
61 public File getTextToSpeechAsFile(String text, String label, String audioFormat) throws IOException {
62 String fileNameInCache = getUniqueFilenameForText(text, label);
64 File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioFormat.toLowerCase());
65 if (audioFileInCache.exists()) {
67 updateTimeStamp(audioFileInCache);
68 updateTimeStamp(new File(cacheFolder, fileNameInCache + ".txt"));
70 return audioFileInCache;
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)) {
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);
82 return audioFileInCache;
83 } catch (IOException ex) {
84 logger.warn("Could not write {} to cache, return null", audioFileInCache, ex);
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.
93 * Sample: "Robert_00a2653ac5f77063bc4ea2fee87318d3"
95 private String getUniqueFilenameForText(String text, String label) {
98 md = MessageDigest.getInstance("MD5");
99 } catch (NoSuchAlgorithmException ex) {
100 logger.error("Could not create MD5 hash for '{}'", text, ex);
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
108 while (hashtext.length() < 32) {
109 hashtext = "0" + hashtext;
111 String fileName = label + "_" + hashtext;
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);
121 outputStream.write(bytes, 0, read);
122 read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
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));
132 private void updateTimeStamp(File file) throws IOException {
133 // update use date for cache management
134 file.setLastModified(System.currentTimeMillis());
137 private void purgeAgedFiles() throws IOException {
138 // just exit if expiration set to 0/disabled
139 if (config.getExpireDate() == 0) {
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) {
159 logger.debug("PollyTTS cache cleaner deleted '{}' aged files", filesDeleted);