We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
tts cache creates "None" folder
the latest cache refactor is creating a folder named "None" due to the handling of persistent cache added for mimic2 in https://github.com/MycroftAI/mycroft-core/pull/2853

cache is also deleted on boot, the min_percent param is set to 100%, "Remove cache data if disk space is running low." actually means "always remove cache data". 

proposed fix: https://github.com/HelloChatterbox/HolmesV/pull/61

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mycroft/tts/cache.py`
Content:
```
1 # Copyright 2021 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #    http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 """TTS cache maintenance.
15 
16 There are two types of cache available to a TTS engine.  Both are comprised of
17 audio and phoneme files.  TTS engines can use the cache to improve performance
18 by not performing inference on sentences in the cache.
19 
20 The first type of cache is a persistent cache.  The cache is considered
21 persistent because the files are stored in a location that is not cleared on
22 reboot.  TTS inference on these sentences should only need to occur once.  The
23 persistent cache contains commonly spoken sentences.
24 
25 The second cache type is a temporary cache stored in the /tmp directory,
26 which is cleared when a device is rebooted.  Sentences are added to this cache
27 on the fly every time a TTS engine returns audio for a sentence that is not
28 already cached.
29 """
30 import base64
31 import hashlib
32 import json
33 import re
34 from pathlib import Path
35 from typing import List, Set, Tuple
36 from urllib import parse
37 
38 import requests
39 
40 from mycroft.util.file_utils import (
41     ensure_directory_exists, get_cache_directory, curate_cache
42 )
43 from mycroft.util.log import LOG
44 
45 
46 def _get_mimic2_audio(sentence: str, url: str) -> Tuple[bytes, str]:
47     """Use the Mimic2 API to retrieve the audio for a sentence.
48 
49     Args:
50         sentence: The sentence to be cached
51     """
52     LOG.debug("Retrieving Mimic2 audio for sentence \"{}\'".format(sentence))
53     mimic2_url = url + parse.quote(sentence) + '&visimes=True'
54     response = requests.get(mimic2_url)
55     response_data = response.json()
56     audio = base64.b64decode(response_data["audio_base64"])
57     phonemes = response_data["visimes"]
58 
59     return audio, phonemes
60 
61 
62 def hash_sentence(sentence: str):
63     """Convert the sentence into a hash value used for the file name
64 
65     Args:
66         sentence: The sentence to be cached
67     """
68     encoded_sentence = sentence.encode("utf-8", "ignore")
69     sentence_hash = hashlib.md5(encoded_sentence).hexdigest()
70 
71     return sentence_hash
72 
73 
74 def hash_from_path(path: Path) -> str:
75     """Returns hash from a given path.
76 
77     Simply removes extension and folder structure leaving the hash.
78 
79     Args:
80         path: path to get hash from
81 
82     Returns:
83         Hash reference for file.
84     """
85     return path.with_suffix('').name
86 
87 
88 class AudioFile:
89     def __init__(self, cache_dir: Path, sentence_hash: str, file_type: str):
90         self.name = f"{sentence_hash}.{file_type}"
91         self.path = cache_dir.joinpath(self.name)
92 
93     def save(self, audio: bytes):
94         """Write a TTS cache file containing the audio to be spoken.
95 
96         Args:
97             audio: TTS inference of a sentence
98         """
99         try:
100             with open(self.path, "wb") as audio_file:
101                 audio_file.write(audio)
102         except Exception:
103             LOG.exception("Failed to write {} to cache".format(self.name))
104 
105     def exists(self):
106         return self.path.exists()
107 
108 
109 class PhonemeFile:
110     def __init__(self, cache_dir: Path, sentence_hash: str):
111         self.name = f"{sentence_hash}.pho"
112         self.path = cache_dir.joinpath(self.name)
113 
114     def load(self) -> List:
115         """Load phonemes from cache file."""
116         phonemes = None
117         if self.path.exists():
118             try:
119                 with open(self.path) as phoneme_file:
120                     phonemes = phoneme_file.read().strip()
121             except Exception:
122                 LOG.exception("Failed to read phoneme from cache")
123 
124         return json.loads(phonemes)
125 
126     def save(self, phonemes):
127         """Write a TTS cache file containing the phoneme to be displayed.
128 
129         Args:
130             phonemes: instructions for how to make the mouth on a device move
131         """
132         try:
133             rec = json.dumps(phonemes)
134             with open(self.path, "w") as phoneme_file:
135                 phoneme_file.write(rec)
136         except Exception:
137             LOG.exception("Failed to write {} to cache".format(self.name))
138 
139     def exists(self):
140         return self.path.exists()
141 
142 
143 class TextToSpeechCache:
144     """Class for all persistent and temporary caching operations."""
145     def __init__(self, tts_config, tts_name, audio_file_type):
146         self.config = tts_config
147         self.tts_name = tts_name
148         if "preloaded_cache" in self.config:
149             self.persistent_cache_dir = Path(self.config["preloaded_cache"])
150         else:
151             self.persistent_cache_dir = None
152         self.temporary_cache_dir = Path(
153             get_cache_directory("tts/" + tts_name)
154         )
155         self.audio_file_type = audio_file_type
156         self.resource_dir = Path(__file__).parent.parent.joinpath("res")
157         self.cached_sentences = dict()
158         ensure_directory_exists(
159             str(self.persistent_cache_dir), permissions=0o755
160         )
161         ensure_directory_exists(
162             str(self.temporary_cache_dir), permissions=0o755
163         )
164 
165     def __contains__(self, sha):
166         """The cache contains a SHA if it knows of it and it exists on disk."""
167         if sha not in self.cached_sentences:
168             return False  # Doesn't know of it
169         else:
170             # Audio file must exist, phonemes are optional.
171             audio, phonemes = self.cached_sentences[sha]
172             return (audio.exists() and
173                     (phonemes is None or phonemes.exists()))
174 
175     def load_persistent_cache(self):
176         """Load the contents of dialog files to the persistent cache directory.
177 
178         Parse the dialog files in the resource directory into sentences.  Then
179         add the audio for each sentence to the cache directory.
180 
181         NOTE: There may be files pre-loaded in the persistent cache directory
182         prior to run time, such as pre-recorded audio files.  This will add
183         files that do not already exist.
184 
185         ANOTHER NOTE:  Mimic2 is the only TTS engine that supports this.  This
186         logic will need to change if another TTS engine implements it.
187         """
188         if self.persistent_cache_dir is not None:
189             LOG.info("Adding dialog resources to persistent TTS cache...")
190             self._load_existing_audio_files()
191             self._load_existing_phoneme_files()
192             dialogs = self._collect_dialogs()
193             sentences = self._parse_dialogs(dialogs)
194             for sentence in sentences:
195                 self._load_sentence(sentence)
196             LOG.info("Persistent TTS cache files added successfully.")
197 
198     def _load_existing_audio_files(self):
199         """Find the TTS audio files already in the persistent cache."""
200         glob_pattern = "*." + self.audio_file_type
201         for file_path in self.persistent_cache_dir.glob(glob_pattern):
202             sentence_hash = file_path.name.split(".")[0]
203             audio_file = AudioFile(
204                 self.persistent_cache_dir, sentence_hash, self.audio_file_type
205             )
206             self.cached_sentences[sentence_hash] = audio_file, None
207 
208     def _load_existing_phoneme_files(self):
209         """Find the TTS phoneme files already in the persistent cache.
210 
211         A phoneme file is no good without an audio file to pair it with.  If
212         no audio file matches, do not load the phoneme.
213         """
214         for file_path in self.persistent_cache_dir.glob("*.pho"):
215             sentence_hash = file_path.name.split(".")[0]
216             cached_sentence = self.cached_sentences.get(sentence_hash)
217             if cached_sentence is not None:
218                 audio_file = cached_sentence[0]
219                 phoneme_file = PhonemeFile(
220                     self.persistent_cache_dir, sentence_hash
221                 )
222                 self.cached_sentences[sentence_hash] = audio_file, phoneme_file
223 
224     def _collect_dialogs(self) -> List:
225         """Build a set of unique sentences from the dialog files.
226 
227         The sentences will be parsed from *.dialog files present in
228         mycroft/res/text/en-us.
229         """
230         dialogs = []
231         dialog_directory = Path(self.resource_dir, "text", "en-us")
232         for dialog_file_path in dialog_directory.glob("*.dialog"):
233             with open(dialog_file_path) as dialog_file:
234                 for dialog in dialog_file.readlines():
235                     dialogs.append(dialog.strip())
236 
237         return dialogs
238 
239     @staticmethod
240     def _parse_dialogs(dialogs: List[str]) -> Set[str]:
241         """Split each dialog in the resources directory into sentences.
242 
243         Do not consider sentences with special characters other than
244         punctuation
245             example : <<< LOADING <<<
246 
247         Args:
248             dialogs: a list of the records in the dialog resource files
249         """
250         sentences = set()
251         dialog_split_regex = r"(?<=\.|\;|\?)\s"
252         special_characters_regex = re.compile(r"[@#$%^*()<>/|}{~:]")
253         for dialog in dialogs:
254             dialog_sentences = re.split(dialog_split_regex, dialog)
255             for sentence in dialog_sentences:
256                 match = special_characters_regex.search(sentence)
257                 if match is None:
258                     sentences.add(sentence)
259 
260         return sentences
261 
262     def _load_sentence(self, sentence: str):
263         """Build audio and phoneme files for each sentence to be cached.
264 
265         Perform TTS inference on sentences parsed from dialog files.  Store
266         the results in the persistent cache directory.
267 
268         ASSUMPTION: The only TTS that supports persistent cache right now is
269         Mimic2.  This method assumes a call to the Mimic2 API.  If other TTS
270         engines want to take advantage of the persistent cache, this logic
271         will need to be more dynamic.
272         """
273         sentence_hash = hash_sentence(sentence)
274         if sentence_hash not in self.cached_sentences:
275             LOG.info("Adding \"{}\" to cache".format(sentence))
276             try:
277                 mimic2_url = self.config["url"]
278                 audio, phonemes = _get_mimic2_audio(sentence, mimic2_url)
279             except Exception:
280                 log_msg = "Failed to get audio for sentence \"{}\""
281                 LOG.exception(log_msg.format(sentence))
282             else:
283                 self._add_to_persistent_cache(sentence_hash, audio, phonemes)
284 
285     def _add_to_persistent_cache(
286             self, sentence_hash: str, audio: bytes, phonemes: str
287     ):
288         """Add a audio/phoneme file pair to the persistent cache."""
289         audio_file = AudioFile(
290             self.persistent_cache_dir, sentence_hash, self.audio_file_type
291         )
292         audio_file.save(audio)
293         if phonemes is None:
294             phoneme_file = None
295         else:
296             phoneme_file = PhonemeFile(
297                 self.persistent_cache_dir, sentence_hash
298             )
299             phoneme_file.save(phonemes)
300         self.cached_sentences[sentence_hash] = audio_file, phoneme_file
301 
302     def clear(self):
303         """Remove all files from the temporary cache."""
304         for cache_file_path in self.temporary_cache_dir.iterdir():
305             if cache_file_path.is_dir():
306                 for sub_path in cache_file_path.iterdir():
307                     if sub_path.is_file():
308                         sub_path.unlink()
309             elif cache_file_path.is_file():
310                 cache_file_path.unlink()
311 
312     def curate(self):
313         """Remove cache data if disk space is running low."""
314         files_removed = curate_cache(self.temporary_cache_dir,
315                                      min_free_percent=100)
316 
317         hashes = set([hash_from_path(Path(path)) for path in files_removed])
318         for sentence_hash in hashes:
319             if sentence_hash in self.cached_sentences:
320                 self.cached_sentences.pop(sentence_hash)
321 
322     def define_audio_file(self, sentence_hash: str) -> AudioFile:
323         """Build an instance of an object representing an audio file."""
324         audio_file = AudioFile(
325             self.temporary_cache_dir, sentence_hash, self.audio_file_type
326         )
327         return audio_file
328 
329     def define_phoneme_file(self, sentence_hash: str) -> PhonemeFile:
330         """Build an instance of an object representing an phoneme file."""
331         phoneme_file = PhonemeFile(self.temporary_cache_dir, sentence_hash)
332         return phoneme_file
333
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/mycroft/tts/cache.py b/mycroft/tts/cache.py
--- a/mycroft/tts/cache.py
+++ b/mycroft/tts/cache.py
@@ -147,20 +147,20 @@
         self.tts_name = tts_name
         if "preloaded_cache" in self.config:
             self.persistent_cache_dir = Path(self.config["preloaded_cache"])
+            ensure_directory_exists(
+                str(self.persistent_cache_dir), permissions=0o755
+            )
         else:
             self.persistent_cache_dir = None
         self.temporary_cache_dir = Path(
             get_cache_directory("tts/" + tts_name)
         )
-        self.audio_file_type = audio_file_type
-        self.resource_dir = Path(__file__).parent.parent.joinpath("res")
-        self.cached_sentences = dict()
-        ensure_directory_exists(
-            str(self.persistent_cache_dir), permissions=0o755
-        )
         ensure_directory_exists(
             str(self.temporary_cache_dir), permissions=0o755
         )
+        self.audio_file_type = audio_file_type
+        self.resource_dir = Path(__file__).parent.parent.joinpath("res")
+        self.cached_sentences = dict()
 
     def __contains__(self, sha):
         """The cache contains a SHA if it knows of it and it exists on disk."""

{"golden_diff": "diff --git a/mycroft/tts/cache.py b/mycroft/tts/cache.py\n--- a/mycroft/tts/cache.py\n+++ b/mycroft/tts/cache.py\n@@ -147,20 +147,20 @@\n         self.tts_name = tts_name\n         if \"preloaded_cache\" in self.config:\n             self.persistent_cache_dir = Path(self.config[\"preloaded_cache\"])\n+            ensure_directory_exists(\n+                str(self.persistent_cache_dir), permissions=0o755\n+            )\n         else:\n             self.persistent_cache_dir = None\n         self.temporary_cache_dir = Path(\n             get_cache_directory(\"tts/\" + tts_name)\n         )\n-        self.audio_file_type = audio_file_type\n-        self.resource_dir = Path(__file__).parent.parent.joinpath(\"res\")\n-        self.cached_sentences = dict()\n-        ensure_directory_exists(\n-            str(self.persistent_cache_dir), permissions=0o755\n-        )\n         ensure_directory_exists(\n             str(self.temporary_cache_dir), permissions=0o755\n         )\n+        self.audio_file_type = audio_file_type\n+        self.resource_dir = Path(__file__).parent.parent.joinpath(\"res\")\n+        self.cached_sentences = dict()\n \n     def __contains__(self, sha):\n         \"\"\"The cache contains a SHA if it knows of it and it exists on disk.\"\"\"\n", "issue": "tts cache creates \"None\" folder\nthe latest cache refactor is creating a folder named \"None\" due to the handling of persistent cache added for mimic2 in https://github.com/MycroftAI/mycroft-core/pull/2853\r\n\r\ncache is also deleted on boot, the min_percent param is set to 100%, \"Remove cache data if disk space is running low.\" actually means \"always remove cache data\". \r\n\r\nproposed fix: https://github.com/HelloChatterbox/HolmesV/pull/61\n", "before_files": [{"content": "# Copyright 2021 Mycroft AI Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\"\"\"TTS cache maintenance.\n\nThere are two types of cache available to a TTS engine.  Both are comprised of\naudio and phoneme files.  TTS engines can use the cache to improve performance\nby not performing inference on sentences in the cache.\n\nThe first type of cache is a persistent cache.  The cache is considered\npersistent because the files are stored in a location that is not cleared on\nreboot.  TTS inference on these sentences should only need to occur once.  The\npersistent cache contains commonly spoken sentences.\n\nThe second cache type is a temporary cache stored in the /tmp directory,\nwhich is cleared when a device is rebooted.  Sentences are added to this cache\non the fly every time a TTS engine returns audio for a sentence that is not\nalready cached.\n\"\"\"\nimport base64\nimport hashlib\nimport json\nimport re\nfrom pathlib import Path\nfrom typing import List, Set, Tuple\nfrom urllib import parse\n\nimport requests\n\nfrom mycroft.util.file_utils import (\n    ensure_directory_exists, get_cache_directory, curate_cache\n)\nfrom mycroft.util.log import LOG\n\n\ndef _get_mimic2_audio(sentence: str, url: str) -> Tuple[bytes, str]:\n    \"\"\"Use the Mimic2 API to retrieve the audio for a sentence.\n\n    Args:\n        sentence: The sentence to be cached\n    \"\"\"\n    LOG.debug(\"Retrieving Mimic2 audio for sentence \\\"{}\\'\".format(sentence))\n    mimic2_url = url + parse.quote(sentence) + '&visimes=True'\n    response = requests.get(mimic2_url)\n    response_data = response.json()\n    audio = base64.b64decode(response_data[\"audio_base64\"])\n    phonemes = response_data[\"visimes\"]\n\n    return audio, phonemes\n\n\ndef hash_sentence(sentence: str):\n    \"\"\"Convert the sentence into a hash value used for the file name\n\n    Args:\n        sentence: The sentence to be cached\n    \"\"\"\n    encoded_sentence = sentence.encode(\"utf-8\", \"ignore\")\n    sentence_hash = hashlib.md5(encoded_sentence).hexdigest()\n\n    return sentence_hash\n\n\ndef hash_from_path(path: Path) -> str:\n    \"\"\"Returns hash from a given path.\n\n    Simply removes extension and folder structure leaving the hash.\n\n    Args:\n        path: path to get hash from\n\n    Returns:\n        Hash reference for file.\n    \"\"\"\n    return path.with_suffix('').name\n\n\nclass AudioFile:\n    def __init__(self, cache_dir: Path, sentence_hash: str, file_type: str):\n        self.name = f\"{sentence_hash}.{file_type}\"\n        self.path = cache_dir.joinpath(self.name)\n\n    def save(self, audio: bytes):\n        \"\"\"Write a TTS cache file containing the audio to be spoken.\n\n        Args:\n            audio: TTS inference of a sentence\n        \"\"\"\n        try:\n            with open(self.path, \"wb\") as audio_file:\n                audio_file.write(audio)\n        except Exception:\n            LOG.exception(\"Failed to write {} to cache\".format(self.name))\n\n    def exists(self):\n        return self.path.exists()\n\n\nclass PhonemeFile:\n    def __init__(self, cache_dir: Path, sentence_hash: str):\n        self.name = f\"{sentence_hash}.pho\"\n        self.path = cache_dir.joinpath(self.name)\n\n    def load(self) -> List:\n        \"\"\"Load phonemes from cache file.\"\"\"\n        phonemes = None\n        if self.path.exists():\n            try:\n                with open(self.path) as phoneme_file:\n                    phonemes = phoneme_file.read().strip()\n            except Exception:\n                LOG.exception(\"Failed to read phoneme from cache\")\n\n        return json.loads(phonemes)\n\n    def save(self, phonemes):\n        \"\"\"Write a TTS cache file containing the phoneme to be displayed.\n\n        Args:\n            phonemes: instructions for how to make the mouth on a device move\n        \"\"\"\n        try:\n            rec = json.dumps(phonemes)\n            with open(self.path, \"w\") as phoneme_file:\n                phoneme_file.write(rec)\n        except Exception:\n            LOG.exception(\"Failed to write {} to cache\".format(self.name))\n\n    def exists(self):\n        return self.path.exists()\n\n\nclass TextToSpeechCache:\n    \"\"\"Class for all persistent and temporary caching operations.\"\"\"\n    def __init__(self, tts_config, tts_name, audio_file_type):\n        self.config = tts_config\n        self.tts_name = tts_name\n        if \"preloaded_cache\" in self.config:\n            self.persistent_cache_dir = Path(self.config[\"preloaded_cache\"])\n        else:\n            self.persistent_cache_dir = None\n        self.temporary_cache_dir = Path(\n            get_cache_directory(\"tts/\" + tts_name)\n        )\n        self.audio_file_type = audio_file_type\n        self.resource_dir = Path(__file__).parent.parent.joinpath(\"res\")\n        self.cached_sentences = dict()\n        ensure_directory_exists(\n            str(self.persistent_cache_dir), permissions=0o755\n        )\n        ensure_directory_exists(\n            str(self.temporary_cache_dir), permissions=0o755\n        )\n\n    def __contains__(self, sha):\n        \"\"\"The cache contains a SHA if it knows of it and it exists on disk.\"\"\"\n        if sha not in self.cached_sentences:\n            return False  # Doesn't know of it\n        else:\n            # Audio file must exist, phonemes are optional.\n            audio, phonemes = self.cached_sentences[sha]\n            return (audio.exists() and\n                    (phonemes is None or phonemes.exists()))\n\n    def load_persistent_cache(self):\n        \"\"\"Load the contents of dialog files to the persistent cache directory.\n\n        Parse the dialog files in the resource directory into sentences.  Then\n        add the audio for each sentence to the cache directory.\n\n        NOTE: There may be files pre-loaded in the persistent cache directory\n        prior to run time, such as pre-recorded audio files.  This will add\n        files that do not already exist.\n\n        ANOTHER NOTE:  Mimic2 is the only TTS engine that supports this.  This\n        logic will need to change if another TTS engine implements it.\n        \"\"\"\n        if self.persistent_cache_dir is not None:\n            LOG.info(\"Adding dialog resources to persistent TTS cache...\")\n            self._load_existing_audio_files()\n            self._load_existing_phoneme_files()\n            dialogs = self._collect_dialogs()\n            sentences = self._parse_dialogs(dialogs)\n            for sentence in sentences:\n                self._load_sentence(sentence)\n            LOG.info(\"Persistent TTS cache files added successfully.\")\n\n    def _load_existing_audio_files(self):\n        \"\"\"Find the TTS audio files already in the persistent cache.\"\"\"\n        glob_pattern = \"*.\" + self.audio_file_type\n        for file_path in self.persistent_cache_dir.glob(glob_pattern):\n            sentence_hash = file_path.name.split(\".\")[0]\n            audio_file = AudioFile(\n                self.persistent_cache_dir, sentence_hash, self.audio_file_type\n            )\n            self.cached_sentences[sentence_hash] = audio_file, None\n\n    def _load_existing_phoneme_files(self):\n        \"\"\"Find the TTS phoneme files already in the persistent cache.\n\n        A phoneme file is no good without an audio file to pair it with.  If\n        no audio file matches, do not load the phoneme.\n        \"\"\"\n        for file_path in self.persistent_cache_dir.glob(\"*.pho\"):\n            sentence_hash = file_path.name.split(\".\")[0]\n            cached_sentence = self.cached_sentences.get(sentence_hash)\n            if cached_sentence is not None:\n                audio_file = cached_sentence[0]\n                phoneme_file = PhonemeFile(\n                    self.persistent_cache_dir, sentence_hash\n                )\n                self.cached_sentences[sentence_hash] = audio_file, phoneme_file\n\n    def _collect_dialogs(self) -> List:\n        \"\"\"Build a set of unique sentences from the dialog files.\n\n        The sentences will be parsed from *.dialog files present in\n        mycroft/res/text/en-us.\n        \"\"\"\n        dialogs = []\n        dialog_directory = Path(self.resource_dir, \"text\", \"en-us\")\n        for dialog_file_path in dialog_directory.glob(\"*.dialog\"):\n            with open(dialog_file_path) as dialog_file:\n                for dialog in dialog_file.readlines():\n                    dialogs.append(dialog.strip())\n\n        return dialogs\n\n    @staticmethod\n    def _parse_dialogs(dialogs: List[str]) -> Set[str]:\n        \"\"\"Split each dialog in the resources directory into sentences.\n\n        Do not consider sentences with special characters other than\n        punctuation\n            example : <<< LOADING <<<\n\n        Args:\n            dialogs: a list of the records in the dialog resource files\n        \"\"\"\n        sentences = set()\n        dialog_split_regex = r\"(?<=\\.|\\;|\\?)\\s\"\n        special_characters_regex = re.compile(r\"[@#$%^*()<>/|}{~:]\")\n        for dialog in dialogs:\n            dialog_sentences = re.split(dialog_split_regex, dialog)\n            for sentence in dialog_sentences:\n                match = special_characters_regex.search(sentence)\n                if match is None:\n                    sentences.add(sentence)\n\n        return sentences\n\n    def _load_sentence(self, sentence: str):\n        \"\"\"Build audio and phoneme files for each sentence to be cached.\n\n        Perform TTS inference on sentences parsed from dialog files.  Store\n        the results in the persistent cache directory.\n\n        ASSUMPTION: The only TTS that supports persistent cache right now is\n        Mimic2.  This method assumes a call to the Mimic2 API.  If other TTS\n        engines want to take advantage of the persistent cache, this logic\n        will need to be more dynamic.\n        \"\"\"\n        sentence_hash = hash_sentence(sentence)\n        if sentence_hash not in self.cached_sentences:\n            LOG.info(\"Adding \\\"{}\\\" to cache\".format(sentence))\n            try:\n                mimic2_url = self.config[\"url\"]\n                audio, phonemes = _get_mimic2_audio(sentence, mimic2_url)\n            except Exception:\n                log_msg = \"Failed to get audio for sentence \\\"{}\\\"\"\n                LOG.exception(log_msg.format(sentence))\n            else:\n                self._add_to_persistent_cache(sentence_hash, audio, phonemes)\n\n    def _add_to_persistent_cache(\n            self, sentence_hash: str, audio: bytes, phonemes: str\n    ):\n        \"\"\"Add a audio/phoneme file pair to the persistent cache.\"\"\"\n        audio_file = AudioFile(\n            self.persistent_cache_dir, sentence_hash, self.audio_file_type\n        )\n        audio_file.save(audio)\n        if phonemes is None:\n            phoneme_file = None\n        else:\n            phoneme_file = PhonemeFile(\n                self.persistent_cache_dir, sentence_hash\n            )\n            phoneme_file.save(phonemes)\n        self.cached_sentences[sentence_hash] = audio_file, phoneme_file\n\n    def clear(self):\n        \"\"\"Remove all files from the temporary cache.\"\"\"\n        for cache_file_path in self.temporary_cache_dir.iterdir():\n            if cache_file_path.is_dir():\n                for sub_path in cache_file_path.iterdir():\n                    if sub_path.is_file():\n                        sub_path.unlink()\n            elif cache_file_path.is_file():\n                cache_file_path.unlink()\n\n    def curate(self):\n        \"\"\"Remove cache data if disk space is running low.\"\"\"\n        files_removed = curate_cache(self.temporary_cache_dir,\n                                     min_free_percent=100)\n\n        hashes = set([hash_from_path(Path(path)) for path in files_removed])\n        for sentence_hash in hashes:\n            if sentence_hash in self.cached_sentences:\n                self.cached_sentences.pop(sentence_hash)\n\n    def define_audio_file(self, sentence_hash: str) -> AudioFile:\n        \"\"\"Build an instance of an object representing an audio file.\"\"\"\n        audio_file = AudioFile(\n            self.temporary_cache_dir, sentence_hash, self.audio_file_type\n        )\n        return audio_file\n\n    def define_phoneme_file(self, sentence_hash: str) -> PhonemeFile:\n        \"\"\"Build an instance of an object representing an phoneme file.\"\"\"\n        phoneme_file = PhonemeFile(self.temporary_cache_dir, sentence_hash)\n        return phoneme_file\n", "path": "mycroft/tts/cache.py"}], "after_files": [{"content": "# Copyright 2021 Mycroft AI Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\"\"\"TTS cache maintenance.\n\nThere are two types of cache available to a TTS engine.  Both are comprised of\naudio and phoneme files.  TTS engines can use the cache to improve performance\nby not performing inference on sentences in the cache.\n\nThe first type of cache is a persistent cache.  The cache is considered\npersistent because the files are stored in a location that is not cleared on\nreboot.  TTS inference on these sentences should only need to occur once.  The\npersistent cache contains commonly spoken sentences.\n\nThe second cache type is a temporary cache stored in the /tmp directory,\nwhich is cleared when a device is rebooted.  Sentences are added to this cache\non the fly every time a TTS engine returns audio for a sentence that is not\nalready cached.\n\"\"\"\nimport base64\nimport hashlib\nimport json\nimport re\nfrom pathlib import Path\nfrom typing import List, Set, Tuple\nfrom urllib import parse\n\nimport requests\n\nfrom mycroft.util.file_utils import (\n    ensure_directory_exists, get_cache_directory, curate_cache\n)\nfrom mycroft.util.log import LOG\n\n\ndef _get_mimic2_audio(sentence: str, url: str) -> Tuple[bytes, str]:\n    \"\"\"Use the Mimic2 API to retrieve the audio for a sentence.\n\n    Args:\n        sentence: The sentence to be cached\n    \"\"\"\n    LOG.debug(\"Retrieving Mimic2 audio for sentence \\\"{}\\'\".format(sentence))\n    mimic2_url = url + parse.quote(sentence) + '&visimes=True'\n    response = requests.get(mimic2_url)\n    response_data = response.json()\n    audio = base64.b64decode(response_data[\"audio_base64\"])\n    phonemes = response_data[\"visimes\"]\n\n    return audio, phonemes\n\n\ndef hash_sentence(sentence: str):\n    \"\"\"Convert the sentence into a hash value used for the file name\n\n    Args:\n        sentence: The sentence to be cached\n    \"\"\"\n    encoded_sentence = sentence.encode(\"utf-8\", \"ignore\")\n    sentence_hash = hashlib.md5(encoded_sentence).hexdigest()\n\n    return sentence_hash\n\n\ndef hash_from_path(path: Path) -> str:\n    \"\"\"Returns hash from a given path.\n\n    Simply removes extension and folder structure leaving the hash.\n\n    Args:\n        path: path to get hash from\n\n    Returns:\n        Hash reference for file.\n    \"\"\"\n    return path.with_suffix('').name\n\n\nclass AudioFile:\n    def __init__(self, cache_dir: Path, sentence_hash: str, file_type: str):\n        self.name = f\"{sentence_hash}.{file_type}\"\n        self.path = cache_dir.joinpath(self.name)\n\n    def save(self, audio: bytes):\n        \"\"\"Write a TTS cache file containing the audio to be spoken.\n\n        Args:\n            audio: TTS inference of a sentence\n        \"\"\"\n        try:\n            with open(self.path, \"wb\") as audio_file:\n                audio_file.write(audio)\n        except Exception:\n            LOG.exception(\"Failed to write {} to cache\".format(self.name))\n\n    def exists(self):\n        return self.path.exists()\n\n\nclass PhonemeFile:\n    def __init__(self, cache_dir: Path, sentence_hash: str):\n        self.name = f\"{sentence_hash}.pho\"\n        self.path = cache_dir.joinpath(self.name)\n\n    def load(self) -> List:\n        \"\"\"Load phonemes from cache file.\"\"\"\n        phonemes = None\n        if self.path.exists():\n            try:\n                with open(self.path) as phoneme_file:\n                    phonemes = phoneme_file.read().strip()\n            except Exception:\n                LOG.exception(\"Failed to read phoneme from cache\")\n\n        return json.loads(phonemes)\n\n    def save(self, phonemes):\n        \"\"\"Write a TTS cache file containing the phoneme to be displayed.\n\n        Args:\n            phonemes: instructions for how to make the mouth on a device move\n        \"\"\"\n        try:\n            rec = json.dumps(phonemes)\n            with open(self.path, \"w\") as phoneme_file:\n                phoneme_file.write(rec)\n        except Exception:\n            LOG.exception(\"Failed to write {} to cache\".format(self.name))\n\n    def exists(self):\n        return self.path.exists()\n\n\nclass TextToSpeechCache:\n    \"\"\"Class for all persistent and temporary caching operations.\"\"\"\n    def __init__(self, tts_config, tts_name, audio_file_type):\n        self.config = tts_config\n        self.tts_name = tts_name\n        if \"preloaded_cache\" in self.config:\n            self.persistent_cache_dir = Path(self.config[\"preloaded_cache\"])\n            ensure_directory_exists(\n                str(self.persistent_cache_dir), permissions=0o755\n            )\n        else:\n            self.persistent_cache_dir = None\n        self.temporary_cache_dir = Path(\n            get_cache_directory(\"tts/\" + tts_name)\n        )\n        ensure_directory_exists(\n            str(self.temporary_cache_dir), permissions=0o755\n        )\n        self.audio_file_type = audio_file_type\n        self.resource_dir = Path(__file__).parent.parent.joinpath(\"res\")\n        self.cached_sentences = dict()\n\n    def __contains__(self, sha):\n        \"\"\"The cache contains a SHA if it knows of it and it exists on disk.\"\"\"\n        if sha not in self.cached_sentences:\n            return False  # Doesn't know of it\n        else:\n            # Audio file must exist, phonemes are optional.\n            audio, phonemes = self.cached_sentences[sha]\n            return (audio.exists() and\n                    (phonemes is None or phonemes.exists()))\n\n    def load_persistent_cache(self):\n        \"\"\"Load the contents of dialog files to the persistent cache directory.\n\n        Parse the dialog files in the resource directory into sentences.  Then\n        add the audio for each sentence to the cache directory.\n\n        NOTE: There may be files pre-loaded in the persistent cache directory\n        prior to run time, such as pre-recorded audio files.  This will add\n        files that do not already exist.\n\n        ANOTHER NOTE:  Mimic2 is the only TTS engine that supports this.  This\n        logic will need to change if another TTS engine implements it.\n        \"\"\"\n        if self.persistent_cache_dir is not None:\n            LOG.info(\"Adding dialog resources to persistent TTS cache...\")\n            self._load_existing_audio_files()\n            self._load_existing_phoneme_files()\n            dialogs = self._collect_dialogs()\n            sentences = self._parse_dialogs(dialogs)\n            for sentence in sentences:\n                self._load_sentence(sentence)\n            LOG.info(\"Persistent TTS cache files added successfully.\")\n\n    def _load_existing_audio_files(self):\n        \"\"\"Find the TTS audio files already in the persistent cache.\"\"\"\n        glob_pattern = \"*.\" + self.audio_file_type\n        for file_path in self.persistent_cache_dir.glob(glob_pattern):\n            sentence_hash = file_path.name.split(\".\")[0]\n            audio_file = AudioFile(\n                self.persistent_cache_dir, sentence_hash, self.audio_file_type\n            )\n            self.cached_sentences[sentence_hash] = audio_file, None\n\n    def _load_existing_phoneme_files(self):\n        \"\"\"Find the TTS phoneme files already in the persistent cache.\n\n        A phoneme file is no good without an audio file to pair it with.  If\n        no audio file matches, do not load the phoneme.\n        \"\"\"\n        for file_path in self.persistent_cache_dir.glob(\"*.pho\"):\n            sentence_hash = file_path.name.split(\".\")[0]\n            cached_sentence = self.cached_sentences.get(sentence_hash)\n            if cached_sentence is not None:\n                audio_file = cached_sentence[0]\n                phoneme_file = PhonemeFile(\n                    self.persistent_cache_dir, sentence_hash\n                )\n                self.cached_sentences[sentence_hash] = audio_file, phoneme_file\n\n    def _collect_dialogs(self) -> List:\n        \"\"\"Build a set of unique sentences from the dialog files.\n\n        The sentences will be parsed from *.dialog files present in\n        mycroft/res/text/en-us.\n        \"\"\"\n        dialogs = []\n        dialog_directory = Path(self.resource_dir, \"text\", \"en-us\")\n        for dialog_file_path in dialog_directory.glob(\"*.dialog\"):\n            with open(dialog_file_path) as dialog_file:\n                for dialog in dialog_file.readlines():\n                    dialogs.append(dialog.strip())\n\n        return dialogs\n\n    @staticmethod\n    def _parse_dialogs(dialogs: List[str]) -> Set[str]:\n        \"\"\"Split each dialog in the resources directory into sentences.\n\n        Do not consider sentences with special characters other than\n        punctuation\n            example : <<< LOADING <<<\n\n        Args:\n            dialogs: a list of the records in the dialog resource files\n        \"\"\"\n        sentences = set()\n        dialog_split_regex = r\"(?<=\\.|\\;|\\?)\\s\"\n        special_characters_regex = re.compile(r\"[@#$%^*()<>/|}{~:]\")\n        for dialog in dialogs:\n            dialog_sentences = re.split(dialog_split_regex, dialog)\n            for sentence in dialog_sentences:\n                match = special_characters_regex.search(sentence)\n                if match is None:\n                    sentences.add(sentence)\n\n        return sentences\n\n    def _load_sentence(self, sentence: str):\n        \"\"\"Build audio and phoneme files for each sentence to be cached.\n\n        Perform TTS inference on sentences parsed from dialog files.  Store\n        the results in the persistent cache directory.\n\n        ASSUMPTION: The only TTS that supports persistent cache right now is\n        Mimic2.  This method assumes a call to the Mimic2 API.  If other TTS\n        engines want to take advantage of the persistent cache, this logic\n        will need to be more dynamic.\n        \"\"\"\n        sentence_hash = hash_sentence(sentence)\n        if sentence_hash not in self.cached_sentences:\n            LOG.info(\"Adding \\\"{}\\\" to cache\".format(sentence))\n            try:\n                mimic2_url = self.config[\"url\"]\n                audio, phonemes = _get_mimic2_audio(sentence, mimic2_url)\n            except Exception:\n                log_msg = \"Failed to get audio for sentence \\\"{}\\\"\"\n                LOG.exception(log_msg.format(sentence))\n            else:\n                self._add_to_persistent_cache(sentence_hash, audio, phonemes)\n\n    def _add_to_persistent_cache(\n            self, sentence_hash: str, audio: bytes, phonemes: str\n    ):\n        \"\"\"Add a audio/phoneme file pair to the persistent cache.\"\"\"\n        audio_file = AudioFile(\n            self.persistent_cache_dir, sentence_hash, self.audio_file_type\n        )\n        audio_file.save(audio)\n        if phonemes is None:\n            phoneme_file = None\n        else:\n            phoneme_file = PhonemeFile(\n                self.persistent_cache_dir, sentence_hash\n            )\n            phoneme_file.save(phonemes)\n        self.cached_sentences[sentence_hash] = audio_file, phoneme_file\n\n    def clear(self):\n        \"\"\"Remove all files from the temporary cache.\"\"\"\n        for cache_file_path in self.temporary_cache_dir.iterdir():\n            if cache_file_path.is_dir():\n                for sub_path in cache_file_path.iterdir():\n                    if sub_path.is_file():\n                        sub_path.unlink()\n            elif cache_file_path.is_file():\n                cache_file_path.unlink()\n\n    def curate(self):\n        \"\"\"Remove cache data if disk space is running low.\"\"\"\n        files_removed = curate_cache(self.temporary_cache_dir,\n                                     min_free_percent=100)\n\n        hashes = set([hash_from_path(Path(path)) for path in files_removed])\n        for sentence_hash in hashes:\n            if sentence_hash in self.cached_sentences:\n                self.cached_sentences.pop(sentence_hash)\n\n    def define_audio_file(self, sentence_hash: str) -> AudioFile:\n        \"\"\"Build an instance of an object representing an audio file.\"\"\"\n        audio_file = AudioFile(\n            self.temporary_cache_dir, sentence_hash, self.audio_file_type\n        )\n        return audio_file\n\n    def define_phoneme_file(self, sentence_hash: str) -> PhonemeFile:\n        \"\"\"Build an instance of an object representing an phoneme file.\"\"\"\n        phoneme_file = PhonemeFile(self.temporary_cache_dir, sentence_hash)\n        return phoneme_file\n", "path": "mycroft/tts/cache.py"}]}