voice-mode 2.32.0__py3-none-any.whl → 2.33.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. voice_mode/__version__.py +1 -1
  2. voice_mode/config.py +1 -1
  3. voice_mode/frontend/.next/BUILD_ID +1 -1
  4. voice_mode/frontend/.next/app-build-manifest.json +5 -5
  5. voice_mode/frontend/.next/build-manifest.json +3 -3
  6. voice_mode/frontend/.next/next-minimal-server.js.nft.json +1 -1
  7. voice_mode/frontend/.next/next-server.js.nft.json +1 -1
  8. voice_mode/frontend/.next/prerender-manifest.json +1 -1
  9. voice_mode/frontend/.next/required-server-files.json +1 -1
  10. voice_mode/frontend/.next/server/app/_not-found/page.js +1 -1
  11. voice_mode/frontend/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. voice_mode/frontend/.next/server/app/_not-found.html +1 -1
  13. voice_mode/frontend/.next/server/app/_not-found.rsc +1 -1
  14. voice_mode/frontend/.next/server/app/api/connection-details/route.js +2 -2
  15. voice_mode/frontend/.next/server/app/favicon.ico/route.js +2 -2
  16. voice_mode/frontend/.next/server/app/index.html +1 -1
  17. voice_mode/frontend/.next/server/app/index.rsc +2 -2
  18. voice_mode/frontend/.next/server/app/page.js +2 -2
  19. voice_mode/frontend/.next/server/app/page_client-reference-manifest.js +1 -1
  20. voice_mode/frontend/.next/server/chunks/994.js +2 -2
  21. voice_mode/frontend/.next/server/middleware-build-manifest.js +1 -1
  22. voice_mode/frontend/.next/server/next-font-manifest.js +1 -1
  23. voice_mode/frontend/.next/server/next-font-manifest.json +1 -1
  24. voice_mode/frontend/.next/server/pages/404.html +1 -1
  25. voice_mode/frontend/.next/server/pages/500.html +1 -1
  26. voice_mode/frontend/.next/server/server-reference-manifest.json +1 -1
  27. voice_mode/frontend/.next/standalone/.next/BUILD_ID +1 -1
  28. voice_mode/frontend/.next/standalone/.next/app-build-manifest.json +5 -5
  29. voice_mode/frontend/.next/standalone/.next/build-manifest.json +3 -3
  30. voice_mode/frontend/.next/standalone/.next/prerender-manifest.json +1 -1
  31. voice_mode/frontend/.next/standalone/.next/required-server-files.json +1 -1
  32. voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  33. voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  34. voice_mode/frontend/.next/standalone/.next/server/app/_not-found.html +1 -1
  35. voice_mode/frontend/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  36. voice_mode/frontend/.next/standalone/.next/server/app/api/connection-details/route.js +2 -2
  37. voice_mode/frontend/.next/standalone/.next/server/app/favicon.ico/route.js +2 -2
  38. voice_mode/frontend/.next/standalone/.next/server/app/index.html +1 -1
  39. voice_mode/frontend/.next/standalone/.next/server/app/index.rsc +2 -2
  40. voice_mode/frontend/.next/standalone/.next/server/app/page.js +2 -2
  41. voice_mode/frontend/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  42. voice_mode/frontend/.next/standalone/.next/server/chunks/994.js +2 -2
  43. voice_mode/frontend/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
  44. voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.js +1 -1
  45. voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.json +1 -1
  46. voice_mode/frontend/.next/standalone/.next/server/pages/404.html +1 -1
  47. voice_mode/frontend/.next/standalone/.next/server/pages/500.html +1 -1
  48. voice_mode/frontend/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  49. voice_mode/frontend/.next/standalone/server.js +1 -1
  50. voice_mode/frontend/.next/static/chunks/app/{layout-4c59da29fcf0456f.js → layout-b6b174992f2f6afd.js} +1 -1
  51. voice_mode/frontend/.next/static/chunks/app/{page-017e11b769f3a746.js → page-52b6f77b58ca7c9d.js} +1 -1
  52. voice_mode/frontend/.next/static/chunks/{main-app-822552bd94497f44.js → main-app-436d7ffcf2166712.js} +1 -1
  53. voice_mode/frontend/.next/trace +43 -43
  54. voice_mode/frontend/.next/types/app/api/connection-details/route.ts +1 -1
  55. voice_mode/frontend/.next/types/app/layout.ts +1 -1
  56. voice_mode/frontend/.next/types/app/page.ts +1 -1
  57. voice_mode/frontend/package-lock.json +3 -3
  58. voice_mode/prompts/converse.py +0 -1
  59. voice_mode/templates/__init__.py +1 -0
  60. voice_mode/templates/launchd/com.voicemode.whisper.plist +7 -13
  61. voice_mode/templates/scripts/__init__.py +1 -0
  62. voice_mode/templates/scripts/start-whisper-server.sh +80 -0
  63. voice_mode/tools/services/whisper/install.py +100 -132
  64. voice_mode/tools/services/whisper/model_install.py +38 -47
  65. voice_mode/tools/services/whisper/models.py +1 -1
  66. voice_mode/utils/services/coreml_setup.py +234 -0
  67. voice_mode/utils/services/whisper_helpers.py +57 -32
  68. {voice_mode-2.32.0.dist-info → voice_mode-2.33.2.dist-info}/METADATA +11 -12
  69. {voice_mode-2.32.0.dist-info → voice_mode-2.33.2.dist-info}/RECORD +73 -69
  70. /voice_mode/frontend/.next/static/{e8aNOVoFA4vUks2Chn7qv → gdmR4LkC2enrnvJ9K0r0_}/_buildManifest.js +0 -0
  71. /voice_mode/frontend/.next/static/{e8aNOVoFA4vUks2Chn7qv → gdmR4LkC2enrnvJ9K0r0_}/_ssgManifest.js +0 -0
  72. {voice_mode-2.32.0.dist-info → voice_mode-2.33.2.dist-info}/WHEEL +0 -0
  73. {voice_mode-2.32.0.dist-info → voice_mode-2.33.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,234 @@
1
+ """Setup and manage CoreML Python environment for whisper.cpp."""
2
+
3
+ import os
4
+ import subprocess
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Optional, Dict, Any
8
+
9
+ logger = logging.getLogger("voice-mode")
10
+
11
+
12
+ def setup_coreml_venv(whisper_dir: Path, force: bool = False) -> Dict[str, Any]:
13
+ """
14
+ Setup a dedicated Python virtual environment for CoreML conversion.
15
+
16
+ Uses whisper.cpp's requirements-coreml.txt to ensure compatibility.
17
+
18
+ Args:
19
+ whisper_dir: Path to whisper.cpp installation
20
+ force: Force recreation of venv even if it exists
21
+
22
+ Returns:
23
+ Dict with 'success' and 'python_path' or 'error'
24
+ """
25
+ venv_dir = whisper_dir / "venv-coreml"
26
+ venv_python = venv_dir / "bin" / "python"
27
+ requirements_file = whisper_dir / "models" / "requirements-coreml.txt"
28
+
29
+ # Check if requirements file exists
30
+ if not requirements_file.exists():
31
+ return {
32
+ "success": False,
33
+ "error": f"CoreML requirements file not found at {requirements_file}"
34
+ }
35
+
36
+ # Check if venv already exists and is valid
37
+ if venv_python.exists() and not force:
38
+ # Test if the venv has the required packages
39
+ try:
40
+ result = subprocess.run(
41
+ [str(venv_python), "-c", "import torch, coremltools, whisper, ane_transformers"],
42
+ capture_output=True,
43
+ text=True,
44
+ timeout=5
45
+ )
46
+ if result.returncode == 0:
47
+ logger.info(f"CoreML venv already exists and is valid at {venv_dir}")
48
+ return {
49
+ "success": True,
50
+ "python_path": str(venv_python),
51
+ "message": "Using existing CoreML virtual environment"
52
+ }
53
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
54
+ logger.info("Existing CoreML venv is incomplete, will recreate")
55
+
56
+ # Create or recreate venv
57
+ logger.info(f"Creating CoreML virtual environment at {venv_dir}")
58
+
59
+ try:
60
+ # Remove existing venv if force or invalid
61
+ if venv_dir.exists() and (force or not venv_python.exists()):
62
+ import shutil
63
+ shutil.rmtree(venv_dir, ignore_errors=True)
64
+
65
+ # Try to use Python 3.11 as recommended by whisper.cpp
66
+ python_cmd = None
67
+ for python_version in ["python3.11", "python3.10", "python3.9", "python3"]:
68
+ if subprocess.run(["which", python_version], capture_output=True).returncode == 0:
69
+ # Check actual version
70
+ version_result = subprocess.run(
71
+ [python_version, "--version"],
72
+ capture_output=True,
73
+ text=True
74
+ )
75
+ if version_result.returncode == 0:
76
+ version = version_result.stdout.strip()
77
+ logger.info(f"Found {version}")
78
+ # Strongly prefer 3.11 as recommended
79
+ if "3.11" in version:
80
+ python_cmd = python_version
81
+ logger.info("Using Python 3.11 (recommended for CoreML)")
82
+ break
83
+ elif "3.10" in version or "3.9" in version:
84
+ if python_cmd is None: # Use as fallback if no 3.11
85
+ python_cmd = python_version
86
+ elif python_cmd is None:
87
+ python_cmd = python_version # Use as last resort
88
+
89
+ if python_cmd is None:
90
+ return {
91
+ "success": False,
92
+ "error": "No suitable Python version found. Python 3.9-3.11 recommended for CoreML."
93
+ }
94
+
95
+ # Create venv
96
+ logger.info(f"Creating venv with {python_cmd}")
97
+ result = subprocess.run(
98
+ [python_cmd, "-m", "venv", str(venv_dir)],
99
+ capture_output=True,
100
+ text=True
101
+ )
102
+
103
+ if result.returncode != 0:
104
+ return {
105
+ "success": False,
106
+ "error": f"Failed to create venv: {result.stderr}"
107
+ }
108
+
109
+ # Upgrade pip
110
+ logger.info("Upgrading pip in CoreML venv")
111
+ subprocess.run(
112
+ [str(venv_python), "-m", "pip", "install", "--upgrade", "pip"],
113
+ capture_output=True,
114
+ text=True
115
+ )
116
+
117
+ # Install requirements with proper versions
118
+ # Based on whisper.cpp documentation and coremltools compatibility
119
+ # Python 3.11 is recommended, torch 2.5.0 is known to work with coremltools
120
+ logger.info("Installing CoreML requirements with compatible versions")
121
+ packages = [
122
+ "torch==2.5.0", # Specific version mentioned in whisper.cpp for coremltools compatibility
123
+ "coremltools>=7.0",
124
+ "openai-whisper",
125
+ "ane_transformers"
126
+ ]
127
+
128
+ # Try installing all at once first
129
+ result = subprocess.run(
130
+ [str(venv_python), "-m", "pip", "install"] + packages,
131
+ capture_output=True,
132
+ text=True
133
+ )
134
+
135
+ if result.returncode != 0:
136
+ # Try installing packages one by one if bulk install fails
137
+ logger.warning("Bulk install failed, trying packages individually")
138
+
139
+ failed_packages = []
140
+ for package in packages:
141
+ logger.info(f"Installing {package}")
142
+ result = subprocess.run(
143
+ [str(venv_python), "-m", "pip", "install", package],
144
+ capture_output=True,
145
+ text=True
146
+ )
147
+ if result.returncode != 0:
148
+ logger.warning(f"Failed to install {package}: {result.stderr}")
149
+ failed_packages.append(package)
150
+
151
+ if failed_packages:
152
+ return {
153
+ "success": False,
154
+ "error": f"Failed to install packages: {', '.join(failed_packages)}",
155
+ "partial": True,
156
+ "python_path": str(venv_python)
157
+ }
158
+
159
+ # Verify installation
160
+ logger.info("Verifying CoreML dependencies")
161
+ result = subprocess.run(
162
+ [str(venv_python), "-c", "import torch, coremltools, whisper, ane_transformers; print('All packages imported successfully')"],
163
+ capture_output=True,
164
+ text=True
165
+ )
166
+
167
+ if result.returncode == 0:
168
+ logger.info("CoreML virtual environment created successfully")
169
+ return {
170
+ "success": True,
171
+ "python_path": str(venv_python),
172
+ "message": "CoreML virtual environment created with all dependencies"
173
+ }
174
+ else:
175
+ return {
176
+ "success": True, # Partial success
177
+ "python_path": str(venv_python),
178
+ "warning": "Some packages may be missing but environment was created",
179
+ "verification_error": result.stderr
180
+ }
181
+
182
+ except Exception as e:
183
+ logger.error(f"Error setting up CoreML venv: {e}")
184
+ return {
185
+ "success": False,
186
+ "error": str(e)
187
+ }
188
+
189
+
190
+ def get_coreml_python(whisper_dir: Path) -> Optional[str]:
191
+ """
192
+ Get the path to Python executable with CoreML dependencies.
193
+
194
+ Checks in order:
195
+ 1. Dedicated venv-coreml environment
196
+ 2. Existing venv environment (if it has CoreML packages)
197
+ 3. None if no suitable environment found
198
+
199
+ Args:
200
+ whisper_dir: Path to whisper.cpp installation
201
+
202
+ Returns:
203
+ Path to Python executable or None
204
+ """
205
+ # Check dedicated CoreML venv first
206
+ venv_coreml_python = whisper_dir / "venv-coreml" / "bin" / "python"
207
+ if venv_coreml_python.exists():
208
+ # Quick check if it has required packages
209
+ try:
210
+ result = subprocess.run(
211
+ [str(venv_coreml_python), "-c", "import torch, coremltools"],
212
+ capture_output=True,
213
+ timeout=5
214
+ )
215
+ if result.returncode == 0:
216
+ return str(venv_coreml_python)
217
+ except:
218
+ pass
219
+
220
+ # Check existing venv as fallback
221
+ venv_python = whisper_dir / "venv" / "bin" / "python"
222
+ if venv_python.exists():
223
+ try:
224
+ result = subprocess.run(
225
+ [str(venv_python), "-c", "import torch, coremltools, whisper"],
226
+ capture_output=True,
227
+ timeout=5
228
+ )
229
+ if result.returncode == 0:
230
+ return str(venv_python)
231
+ except:
232
+ pass
233
+
234
+ return None
@@ -9,6 +9,8 @@ import shutil
9
9
  from pathlib import Path
10
10
  from typing import Optional, List, Dict, Union
11
11
 
12
+ from .coreml_setup import setup_coreml_venv, get_coreml_python
13
+
12
14
  logger = logging.getLogger("voice-mode")
13
15
 
14
16
  def find_whisper_server() -> Optional[str]:
@@ -36,14 +38,23 @@ def find_whisper_server() -> Optional[str]:
36
38
 
37
39
 
38
40
  def find_whisper_model() -> Optional[str]:
39
- """Find a whisper model file."""
40
- from voice_mode.config import WHISPER_MODEL_PATH
41
+ """Find the active whisper model file based on VOICEMODE_WHISPER_MODEL setting."""
42
+ from voice_mode.config import WHISPER_MODEL_PATH, WHISPER_MODEL
43
+
44
+ # First try to find the specific model configured in VOICEMODE_WHISPER_MODEL
45
+ model_name = WHISPER_MODEL # This reads from env/config
46
+ model_filename = f"ggml-{model_name}.bin"
41
47
 
42
48
  # Check configured model path
43
49
  model_dir = Path(WHISPER_MODEL_PATH)
44
50
  if model_dir.exists():
45
- # Look for ggml model files
51
+ specific_model = model_dir / model_filename
52
+ if specific_model.exists():
53
+ return str(specific_model)
54
+
55
+ # Fall back to any model if configured model not found
46
56
  for model_file in model_dir.glob("ggml-*.bin"):
57
+ logger.warning(f"Configured model {model_name} not found, using {model_file.name}")
47
58
  return str(model_file)
48
59
 
49
60
  # Check default installation paths
@@ -54,7 +65,13 @@ def find_whisper_model() -> Optional[str]:
54
65
 
55
66
  for default_path in default_paths:
56
67
  if default_path.exists():
68
+ specific_model = default_path / model_filename
69
+ if specific_model.exists():
70
+ return str(specific_model)
71
+
72
+ # Fall back to any model
57
73
  for model_file in default_path.glob("ggml-*.bin"):
74
+ logger.warning(f"Configured model {model_name} not found, using {model_file.name}")
58
75
  return str(model_file)
59
76
 
60
77
  return None
@@ -63,7 +80,8 @@ def find_whisper_model() -> Optional[str]:
63
80
  async def download_whisper_model(
64
81
  model: str,
65
82
  models_dir: Union[str, Path],
66
- force_download: bool = False
83
+ force_download: bool = False,
84
+ skip_core_ml: bool = False
67
85
  ) -> Dict[str, Union[bool, str]]:
68
86
  """
69
87
  Download a single Whisper model.
@@ -72,6 +90,7 @@ async def download_whisper_model(
72
90
  model: Model name (e.g., 'large-v2', 'base.en')
73
91
  models_dir: Directory to download models to
74
92
  force_download: Re-download even if model exists
93
+ skip_core_ml: Skip Core ML conversion even on Apple Silicon
75
94
 
76
95
  Returns:
77
96
  Dict with 'success' and optional 'error' or 'path'
@@ -144,8 +163,11 @@ async def download_whisper_model(
144
163
  "error": f"Model file not found after download: {model_path}"
145
164
  }
146
165
 
147
- # Check for Core ML support on Apple Silicon
148
- if platform.system() == "Darwin" and platform.machine() == "arm64":
166
+ # Initialize core_ml_result
167
+ core_ml_result = None
168
+
169
+ # Check for Core ML support on Apple Silicon (unless explicitly skipped)
170
+ if platform.system() == "Darwin" and platform.machine() == "arm64" and not skip_core_ml:
149
171
  # Check if Core ML dependencies are needed
150
172
  requirements_file = Path(models_dir) / "requirements-coreml.txt"
151
173
  if requirements_file.exists() and shutil.which("uv"):
@@ -185,15 +207,22 @@ async def download_whisper_model(
185
207
  else:
186
208
  logger.warning(f"Core ML conversion failed ({error_category}): {core_ml_result.get('error', 'Unknown error')}")
187
209
 
188
- # Always include Core ML status in response
189
- return {
210
+ # Build response with appropriate status
211
+ response = {
190
212
  "success": True,
191
213
  "path": str(model_path),
192
- "message": f"Model {model} downloaded successfully",
193
- "core_ml_status": core_ml_result,
194
- "acceleration": "coreml" if core_ml_result.get("success") else "metal"
214
+ "message": f"Model {model} downloaded successfully"
195
215
  }
196
216
 
217
+ # Add Core ML status if attempted
218
+ if core_ml_result:
219
+ response["core_ml_status"] = core_ml_result
220
+ response["acceleration"] = "coreml" if core_ml_result.get("success") else "metal"
221
+ else:
222
+ response["acceleration"] = "metal"
223
+
224
+ return response
225
+
197
226
  except subprocess.CalledProcessError as e:
198
227
  logger.error(f"Failed to download model {model}: {e.stderr}")
199
228
  return {
@@ -255,28 +284,24 @@ async def convert_to_coreml(
255
284
  logger.info(f"Converting {model} to Core ML format...")
256
285
 
257
286
  try:
258
- # Check if we should use uv for Python dependencies
259
- # Try to find the voicemode project root for uv
260
- voicemode_root = None
261
- current = Path(__file__).parent
262
- while current != current.parent:
263
- if (current / "pyproject.toml").exists():
264
- with open(current / "pyproject.toml") as f:
265
- content = f.read()
266
- if 'name = "voice-mode"' in content or 'name = "voicemode"' in content:
267
- voicemode_root = current
268
- break
269
- current = current.parent
287
+ # First, try to get existing CoreML Python environment
288
+ coreml_python = get_coreml_python(whisper_dir)
289
+
290
+ # If no suitable environment exists, set one up
291
+ if not coreml_python:
292
+ logger.info("Setting up CoreML Python environment...")
293
+ setup_result = setup_coreml_venv(whisper_dir)
294
+ if setup_result["success"]:
295
+ coreml_python = setup_result.get("python_path")
296
+ else:
297
+ logger.warning(f"Could not setup CoreML environment: {setup_result.get('error')}")
270
298
 
271
- # If we found voicemode root and uv is available, use it
272
- if voicemode_root and shutil.which("uv"):
273
- # Run the Python script directly with uv instead of using the bash wrapper
274
- logger.info("Using uv for Core ML conversion with Python dependencies")
275
- # Run from the whisper models directory
299
+ if coreml_python:
300
+ # Use the CoreML-enabled Python environment
301
+ logger.info(f"Using CoreML Python environment: {coreml_python}")
276
302
  script_path = whisper_dir / "models" / "convert-whisper-to-coreml.py"
277
303
  result = subprocess.run(
278
- ["uv", "run", "--project", str(voicemode_root), "python",
279
- str(script_path),
304
+ [coreml_python, str(script_path),
280
305
  "--model", model, "--encoder-only", "True", "--optimize-ane", "True"],
281
306
  cwd=str(whisper_dir / "models"),
282
307
  capture_output=True,
@@ -301,8 +326,8 @@ async def convert_to_coreml(
301
326
  shutil.rmtree(coreml_path, ignore_errors=True)
302
327
  shutil.move(str(compiled_path), str(coreml_path))
303
328
  else:
304
- # Fallback to original bash script
305
- logger.info("Using standard Python for Core ML conversion")
329
+ # No suitable Python environment available
330
+ logger.warning("No suitable Python environment for CoreML conversion")
306
331
  # Run from the whisper models directory where the script is located
307
332
  script_dir = convert_script.parent
308
333
  result = subprocess.run(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-mode
3
- Version: 2.32.0
3
+ Version: 2.33.2
4
4
  Summary: VoiceMode - Voice interaction capabilities for AI assistants (formerly voice-mcp)
5
5
  Project-URL: Homepage, https://github.com/mbailey/voicemode
6
6
  Project-URL: Repository, https://github.com/mbailey/voicemode
@@ -116,7 +116,15 @@ Natural voice conversations for AI assistants. Voice Mode brings human-like voic
116
116
  Install Claude Code with Voice Mode configured and ready to run on Linux, macOS, and Windows WSL:
117
117
 
118
118
  ```bash
119
+ # Download and run the installer
119
120
  curl -O https://getvoicemode.com/install.sh && bash install.sh
121
+
122
+ # While local voice services can be installed automatically, we recommend
123
+ # providing an OpenAI API key as a fallback in case local services are unavailable
124
+ export OPENAI_API_KEY=your-openai-key # Optional but recommended
125
+
126
+ # Start a voice conversation
127
+ claude /voicemode:converse
120
128
  ```
121
129
 
122
130
  This installer will:
@@ -124,16 +132,7 @@ This installer will:
124
132
  - Install Claude Code if not already installed
125
133
  - Configure Voice Mode as an MCP server
126
134
  - Set up your system for voice conversations
127
-
128
- After installation, just run:
129
- ```bash
130
- # With OpenAI API (cloud-based, requires API key)
131
- export OPENAI_API_KEY=your-openai-key
132
- claude converse
133
-
134
- # Or use free local services (Voice Mode will offer to install them)
135
- claude converse
136
- ```
135
+ - Offer to install free local STT/TTS services if no API key is provided
137
136
 
138
137
  ### Manual Installation
139
138
 
@@ -693,7 +692,7 @@ To save all audio files (both TTS output and STT input):
693
692
  export VOICEMODE_SAVE_AUDIO=true
694
693
  ```
695
694
 
696
- Audio files are saved to: `~/voicemode_audio/` with timestamps in the filename.
695
+ Audio files are saved to: `~/.voicemode/audio/YYYY/MM/` with timestamps in the filename.
697
696
 
698
697
  ## Documentation
699
698