lollms-client 1.6.1__py3-none-any.whl → 1.6.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.
Potentially problematic release.
This version of lollms-client might be problematic. Click here for more details.
- lollms_client/__init__.py +1 -1
- lollms_client/lollms_core.py +15 -10
- lollms_client/lollms_tts_binding.py +15 -13
- lollms_client/tts_bindings/xtts/__init__.py +90 -37
- lollms_client/tts_bindings/xtts/server/main.py +282 -279
- {lollms_client-1.6.1.dist-info → lollms_client-1.6.2.dist-info}/METADATA +5 -2
- {lollms_client-1.6.1.dist-info → lollms_client-1.6.2.dist-info}/RECORD +10 -10
- {lollms_client-1.6.1.dist-info → lollms_client-1.6.2.dist-info}/WHEEL +0 -0
- {lollms_client-1.6.1.dist-info → lollms_client-1.6.2.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.6.1.dist-info → lollms_client-1.6.2.dist-info}/top_level.txt +0 -0
lollms_client/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ from lollms_client.lollms_utilities import PromptReshaper # Keep general utiliti
|
|
|
8
8
|
from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
|
|
9
9
|
from lollms_client.lollms_llm_binding import LollmsLLMBindingManager
|
|
10
10
|
|
|
11
|
-
__version__ = "1.6.
|
|
11
|
+
__version__ = "1.6.2" # Updated version
|
|
12
12
|
|
|
13
13
|
# Optionally, you could define __all__ if you want to be explicit about exports
|
|
14
14
|
__all__ = [
|
lollms_client/lollms_core.py
CHANGED
|
@@ -143,16 +143,21 @@ class LollmsClient():
|
|
|
143
143
|
ASCIIColors.warning(f"Failed to create LLM binding: {llm_binding_name}. Available: {available}")
|
|
144
144
|
|
|
145
145
|
if tts_binding_name:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
try:
|
|
147
|
+
params = {
|
|
148
|
+
k: v
|
|
149
|
+
for k, v in (tts_binding_config or {}).items()
|
|
150
|
+
if k != "binding_name"
|
|
151
|
+
}
|
|
152
|
+
self.tts = self.tts_binding_manager.create_binding(
|
|
153
|
+
binding_name=tts_binding_name,
|
|
154
|
+
**params
|
|
155
|
+
)
|
|
156
|
+
if self.tts is None:
|
|
157
|
+
ASCIIColors.warning(f"Failed to create TTS binding: {tts_binding_name}. Available: {self.tts_binding_manager.get_available_bindings()}")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
trace_exception(e)
|
|
160
|
+
ASCIIColors.warning(f"Exception occurred while creating TTS binding: {str(e)}")
|
|
156
161
|
|
|
157
162
|
if tti_binding_name:
|
|
158
163
|
if tti_binding_config:
|
|
@@ -49,26 +49,28 @@ class LollmsTTSBindingManager:
|
|
|
49
49
|
except Exception as e:
|
|
50
50
|
trace_exception(e)
|
|
51
51
|
print(f"Failed to load TTS binding {binding_name}: {str(e)}")
|
|
52
|
-
|
|
53
|
-
def create_binding(self,
|
|
52
|
+
def create_binding(self,
|
|
54
53
|
binding_name: str,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
**kwargs) -> Optional[LollmsTTSBinding]:
|
|
55
|
+
"""
|
|
56
|
+
Create an instance of a specific binding.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
binding_name (str): Name of the binding to create.
|
|
60
|
+
kwargs: binding specific arguments
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Optional[LollmsLLMBinding]: Binding instance or None if creation failed.
|
|
64
|
+
"""
|
|
59
65
|
if binding_name not in self.available_bindings:
|
|
60
66
|
self._load_binding(binding_name)
|
|
61
|
-
|
|
67
|
+
|
|
62
68
|
binding_class = self.available_bindings.get(binding_name)
|
|
63
69
|
if binding_class:
|
|
64
|
-
|
|
65
|
-
return binding_class(**config)
|
|
66
|
-
except Exception as e:
|
|
67
|
-
trace_exception(e)
|
|
68
|
-
print(f"Failed to instantiate TTS binding {binding_name}: {str(e)}")
|
|
69
|
-
return None
|
|
70
|
+
return binding_class(**kwargs)
|
|
70
71
|
return None
|
|
71
72
|
|
|
73
|
+
|
|
72
74
|
@staticmethod
|
|
73
75
|
def _get_fallback_description(binding_name: str) -> Dict:
|
|
74
76
|
return {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# File: lollms_client/tts_bindings/xtts/__init__.py
|
|
2
1
|
from lollms_client.lollms_tts_binding import LollmsTTSBinding
|
|
3
2
|
from typing import Optional, List
|
|
4
3
|
from pathlib import Path
|
|
@@ -8,6 +7,14 @@ import sys
|
|
|
8
7
|
import time
|
|
9
8
|
import pipmaster as pm
|
|
10
9
|
|
|
10
|
+
# New import for process-safe file locking
|
|
11
|
+
try:
|
|
12
|
+
from filelock import FileLock, Timeout
|
|
13
|
+
except ImportError:
|
|
14
|
+
print("FATAL: The 'filelock' library is required. Please install it by running: pip install filelock")
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
|
|
11
18
|
BindingName = "XTTSClientBinding"
|
|
12
19
|
|
|
13
20
|
class XTTSClientBinding(LollmsTTSBinding):
|
|
@@ -24,21 +31,73 @@ class XTTSClientBinding(LollmsTTSBinding):
|
|
|
24
31
|
self.auto_start_server = auto_start_server
|
|
25
32
|
self.server_process = None
|
|
26
33
|
self.base_url = f"http://{self.host}:{self.port}"
|
|
34
|
+
self.binding_root = Path(__file__).parent
|
|
35
|
+
self.server_dir = self.binding_root / "server"
|
|
27
36
|
|
|
28
37
|
if self.auto_start_server:
|
|
29
|
-
self.
|
|
38
|
+
self.ensure_server_is_running()
|
|
39
|
+
|
|
40
|
+
def is_server_running(self) -> bool:
|
|
41
|
+
"""Checks if the server is already running and responsive."""
|
|
42
|
+
try:
|
|
43
|
+
response = requests.get(f"{self.base_url}/status", timeout=1)
|
|
44
|
+
if response.status_code == 200 and response.json().get("status") == "running":
|
|
45
|
+
return True
|
|
46
|
+
except requests.ConnectionError:
|
|
47
|
+
return False
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def ensure_server_is_running(self):
|
|
51
|
+
"""
|
|
52
|
+
Ensures the XTTS server is running. If not, it attempts to start it
|
|
53
|
+
in a process-safe manner using a file lock.
|
|
54
|
+
"""
|
|
55
|
+
if self.is_server_running():
|
|
56
|
+
print("XTTS Server is already running.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
lock_path = self.server_dir / "xtts_server.lock"
|
|
60
|
+
lock = FileLock(lock_path, timeout=10) # Wait a maximum of 10 seconds for the lock
|
|
61
|
+
|
|
62
|
+
print("Attempting to start or wait for the XTTS server...")
|
|
63
|
+
try:
|
|
64
|
+
with lock:
|
|
65
|
+
# Double-check after acquiring the lock to handle race conditions
|
|
66
|
+
if not self.is_server_running():
|
|
67
|
+
print("Lock acquired. Starting dedicated XTTS server...")
|
|
68
|
+
self.start_server()
|
|
69
|
+
else:
|
|
70
|
+
print("Server was started by another process while waiting for the lock.")
|
|
71
|
+
except Timeout:
|
|
72
|
+
print("Could not acquire lock. Another process is likely starting the server. Waiting...")
|
|
73
|
+
|
|
74
|
+
# All workers (the one that started the server and those that waited) will verify the server is ready
|
|
75
|
+
self._wait_for_server()
|
|
76
|
+
|
|
30
77
|
|
|
31
78
|
def start_server(self):
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
79
|
+
"""
|
|
80
|
+
Installs dependencies and launches the server as a background subprocess.
|
|
81
|
+
This method should only be called from within a file lock.
|
|
82
|
+
"""
|
|
83
|
+
requirements_file = self.server_dir / "requirements.txt"
|
|
84
|
+
server_script = self.server_dir / "main.py"
|
|
37
85
|
|
|
38
86
|
# 1. Ensure a virtual environment and dependencies
|
|
39
|
-
venv_path = server_dir / "venv"
|
|
40
|
-
|
|
41
|
-
pm_v.
|
|
87
|
+
venv_path = self.server_dir / "venv"
|
|
88
|
+
print(f"Ensuring virtual environment and dependencies in: {venv_path}")
|
|
89
|
+
pm_v = pm.PackageManager(venv_path=str(venv_path))
|
|
90
|
+
|
|
91
|
+
success = pm_v.ensure_requirements(
|
|
92
|
+
str(requirements_file),
|
|
93
|
+
verbose=True
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if not success:
|
|
97
|
+
print("FATAL: Failed to install server dependencies. Aborting launch.")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
print("Dependencies are satisfied. Proceeding to launch server...")
|
|
42
101
|
|
|
43
102
|
# 2. Get the python executable from the venv
|
|
44
103
|
if sys.platform == "win32":
|
|
@@ -46,7 +105,7 @@ class XTTSClientBinding(LollmsTTSBinding):
|
|
|
46
105
|
else:
|
|
47
106
|
python_executable = venv_path / "bin" / "python"
|
|
48
107
|
|
|
49
|
-
# 3. Launch the server as a subprocess
|
|
108
|
+
# 3. Launch the server as a detached subprocess
|
|
50
109
|
command = [
|
|
51
110
|
str(python_executable),
|
|
52
111
|
str(server_script),
|
|
@@ -54,41 +113,36 @@ class XTTSClientBinding(LollmsTTSBinding):
|
|
|
54
113
|
"--port", str(self.port)
|
|
55
114
|
]
|
|
56
115
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
stderr=None, # Inherit parent's stderr (shows in console)
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
# 4. Wait for the server to be ready
|
|
65
|
-
self._wait_for_server()
|
|
116
|
+
# The server is started as a background process and is not tied to this specific worker's lifecycle
|
|
117
|
+
subprocess.Popen(command)
|
|
118
|
+
print("XTTS Server process launched in the background.")
|
|
119
|
+
|
|
66
120
|
|
|
67
121
|
def _wait_for_server(self, timeout=60):
|
|
122
|
+
print("Waiting for XTTS server to become available...")
|
|
68
123
|
start_time = time.time()
|
|
69
124
|
while time.time() - start_time < timeout:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
75
|
-
except requests.ConnectionError:
|
|
76
|
-
time.sleep(1)
|
|
125
|
+
if self.is_server_running():
|
|
126
|
+
print("XTTS Server is up and running.")
|
|
127
|
+
return
|
|
128
|
+
time.sleep(1)
|
|
77
129
|
|
|
78
|
-
|
|
79
|
-
raise RuntimeError("Failed to start the XTTS server in the specified timeout.")
|
|
130
|
+
raise RuntimeError("Failed to connect to the XTTS server within the specified timeout.")
|
|
80
131
|
|
|
81
132
|
def stop_server(self):
|
|
133
|
+
"""
|
|
134
|
+
In a multi-worker setup, a single client instance should not stop the shared server.
|
|
135
|
+
The server will continue running until the main application is terminated.
|
|
136
|
+
"""
|
|
82
137
|
if self.server_process:
|
|
83
|
-
print("XTTS Client:
|
|
84
|
-
self.server_process.terminate()
|
|
85
|
-
self.server_process.wait()
|
|
138
|
+
print("XTTS Client: An instance is shutting down, but the shared server will remain active for other workers.")
|
|
86
139
|
self.server_process = None
|
|
87
|
-
print("Server stopped.")
|
|
88
140
|
|
|
89
141
|
def __del__(self):
|
|
90
|
-
|
|
91
|
-
|
|
142
|
+
"""
|
|
143
|
+
The destructor does not stop the server to prevent disrupting other workers.
|
|
144
|
+
"""
|
|
145
|
+
pass
|
|
92
146
|
|
|
93
147
|
def generate_audio(self, text: str, voice: Optional[str] = None, **kwargs) -> bytes:
|
|
94
148
|
"""Generate audio by calling the server's API"""
|
|
@@ -107,5 +161,4 @@ class XTTSClientBinding(LollmsTTSBinding):
|
|
|
107
161
|
"""Get available models from the server"""
|
|
108
162
|
response = requests.get(f"{self.base_url}/list_models")
|
|
109
163
|
response.raise_for_status()
|
|
110
|
-
return response.json().get("models", [])
|
|
111
|
-
|
|
164
|
+
return response.json().get("models", [])
|
|
@@ -1,314 +1,317 @@
|
|
|
1
|
-
import uvicorn
|
|
2
|
-
from fastapi import FastAPI, APIRouter, HTTPException
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
import argparse
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
import asyncio
|
|
8
|
-
import traceback
|
|
9
|
-
import os
|
|
10
|
-
from typing import Optional, List
|
|
11
|
-
import io
|
|
12
|
-
import wave
|
|
13
|
-
import numpy as np
|
|
14
|
-
import tempfile
|
|
15
|
-
|
|
16
|
-
# --- XTTS Implementation ---
|
|
17
1
|
try:
|
|
18
|
-
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
print(f"Server: Traceback:\n{traceback.format_exc()}")
|
|
33
|
-
xtts_available = False
|
|
34
|
-
|
|
35
|
-
# --- API Models ---
|
|
36
|
-
class GenerationRequest(BaseModel):
|
|
37
|
-
text: str
|
|
38
|
-
voice: Optional[str] = None
|
|
39
|
-
language: Optional[str] = "en"
|
|
40
|
-
speaker_wav: Optional[str] = None
|
|
2
|
+
import uvicorn
|
|
3
|
+
from fastapi import FastAPI, APIRouter, HTTPException
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import asyncio
|
|
9
|
+
import traceback
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional, List
|
|
12
|
+
import io
|
|
13
|
+
import wave
|
|
14
|
+
import numpy as np
|
|
15
|
+
import tempfile
|
|
41
16
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
17
|
+
# --- XTTS Implementation ---
|
|
18
|
+
try:
|
|
19
|
+
print("Server: Loading XTTS dependencies...")
|
|
20
|
+
import torch
|
|
21
|
+
import torchaudio
|
|
22
|
+
from TTS.api import TTS
|
|
23
|
+
print("Server: XTTS dependencies loaded successfully")
|
|
49
24
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# Initialize XTTS model
|
|
72
|
-
self.model = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device)
|
|
73
|
-
|
|
74
|
-
self.model_loaded = True
|
|
75
|
-
print("Server: XTTS model loaded successfully")
|
|
76
|
-
|
|
77
|
-
except Exception as e:
|
|
78
|
-
print(f"Server: Error loading XTTS model: {e}")
|
|
79
|
-
print(f"Server: Traceback:\n{traceback.format_exc()}")
|
|
25
|
+
# Check for CUDA availability
|
|
26
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
27
|
+
print(f"Server: Using device: {device}")
|
|
28
|
+
|
|
29
|
+
xtts_available = True
|
|
30
|
+
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"Server: Failed to load XTTS dependencies: {e}")
|
|
33
|
+
print(f"Server: Traceback:\n{traceback.format_exc()}")
|
|
34
|
+
xtts_available = False
|
|
35
|
+
|
|
36
|
+
# --- API Models ---
|
|
37
|
+
class GenerationRequest(BaseModel):
|
|
38
|
+
text: str
|
|
39
|
+
voice: Optional[str] = None
|
|
40
|
+
language: Optional[str] = "en"
|
|
41
|
+
speaker_wav: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
class XTTSServer:
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.model = None
|
|
80
46
|
self.model_loaded = False
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.
|
|
84
|
-
|
|
85
|
-
def _load_available_voices(self) -> List[str]:
|
|
86
|
-
"""Load and return available voices"""
|
|
87
|
-
try:
|
|
88
|
-
# Look for voice files in voices directory
|
|
89
|
-
voices_dir = Path(__file__).parent / "voices"
|
|
90
|
-
voices = []
|
|
47
|
+
self.model_loading = False # Flag to prevent concurrent loading
|
|
48
|
+
self.available_voices = self._load_available_voices()
|
|
49
|
+
self.available_models = ["xtts_v2"]
|
|
91
50
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
voices = ["default", "female", "male"]
|
|
51
|
+
# Don't initialize model here - do it lazily on first request
|
|
52
|
+
print("Server: XTTS server initialized (model will be loaded on first request)")
|
|
53
|
+
|
|
54
|
+
async def _ensure_model_loaded(self):
|
|
55
|
+
"""Ensure the XTTS model is loaded (lazy loading)"""
|
|
56
|
+
if self.model_loaded:
|
|
57
|
+
return
|
|
100
58
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
59
|
+
if self.model_loading:
|
|
60
|
+
# Another request is already loading the model, wait for it
|
|
61
|
+
while self.model_loading and not self.model_loaded:
|
|
62
|
+
await asyncio.sleep(0.1)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if not xtts_available:
|
|
66
|
+
raise RuntimeError("XTTS library not available")
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
self.model_loading = True
|
|
70
|
+
print("Server: Loading XTTS model for the first time (this may take a few minutes)...")
|
|
71
|
+
|
|
72
|
+
# Initialize XTTS model
|
|
73
|
+
self.model = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device)
|
|
74
|
+
|
|
75
|
+
self.model_loaded = True
|
|
76
|
+
print("Server: XTTS model loaded successfully")
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"Server: Error loading XTTS model: {e}")
|
|
80
|
+
print(f"Server: Traceback:\n{traceback.format_exc()}")
|
|
81
|
+
self.model_loaded = False
|
|
82
|
+
raise
|
|
83
|
+
finally:
|
|
84
|
+
self.model_loading = False
|
|
112
85
|
|
|
113
|
-
|
|
114
|
-
|
|
86
|
+
def _load_available_voices(self) -> List[str]:
|
|
87
|
+
"""Load and return available voices"""
|
|
88
|
+
try:
|
|
89
|
+
# Look for voice files in voices directory
|
|
90
|
+
voices_dir = Path(__file__).parent / "voices"
|
|
91
|
+
voices = []
|
|
92
|
+
|
|
93
|
+
if voices_dir.exists():
|
|
94
|
+
# Look for WAV files in voices directory
|
|
95
|
+
for voice_file in voices_dir.glob("*.wav"):
|
|
96
|
+
voices.append(voice_file.stem)
|
|
97
|
+
|
|
98
|
+
# If no custom voices found, provide some default names
|
|
99
|
+
if not voices:
|
|
100
|
+
voices = ["default", "female", "male"]
|
|
101
|
+
|
|
102
|
+
return voices
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(f"Server: Error loading voices: {e}")
|
|
106
|
+
return ["default"]
|
|
115
107
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
speaker_wav_path = None
|
|
122
|
-
|
|
123
|
-
# First priority: use provided speaker_wav parameter
|
|
124
|
-
if speaker_wav:
|
|
125
|
-
speaker_wav_path = speaker_wav
|
|
126
|
-
print(f"Server: Using provided speaker_wav: {speaker_wav_path}")
|
|
108
|
+
async def generate_audio(self, text: str, voice: Optional[str] = None,
|
|
109
|
+
language: str = "en", speaker_wav: Optional[str] = None) -> bytes:
|
|
110
|
+
"""Generate audio from text using XTTS"""
|
|
111
|
+
# Ensure model is loaded before proceeding
|
|
112
|
+
await self._ensure_model_loaded()
|
|
127
113
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if os.path.exists(voice):
|
|
131
|
-
# Voice parameter is a full file path
|
|
132
|
-
speaker_wav_path = voice
|
|
133
|
-
print(f"Server: Using voice as file path: {speaker_wav_path}")
|
|
134
|
-
else:
|
|
135
|
-
# Look for voice file in voices directory
|
|
136
|
-
voices_dir = Path(__file__).parent / "voices"
|
|
137
|
-
potential_voice_path = voices_dir / f"{voice}.wav"
|
|
138
|
-
if potential_voice_path.exists():
|
|
139
|
-
speaker_wav_path = str(potential_voice_path)
|
|
140
|
-
print(f"Server: Using custom voice file: {speaker_wav_path}")
|
|
141
|
-
else:
|
|
142
|
-
print(f"Server: Voice '{voice}' not found in voices directory")
|
|
143
|
-
|
|
144
|
-
# Create a temporary file for output
|
|
145
|
-
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
|
|
146
|
-
temp_output_path = temp_file.name
|
|
114
|
+
if not self.model_loaded or self.model is None:
|
|
115
|
+
raise RuntimeError("XTTS model failed to load")
|
|
147
116
|
|
|
148
117
|
try:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if
|
|
163
|
-
|
|
118
|
+
print(f"Server: Generating audio for: '{text[:50]}{'...' if len(text) > 50 else ''}'")
|
|
119
|
+
print(f"Server: Using voice: {voice}, language: {language}")
|
|
120
|
+
|
|
121
|
+
# Handle voice/speaker selection
|
|
122
|
+
speaker_wav_path = None
|
|
123
|
+
|
|
124
|
+
# First priority: use provided speaker_wav parameter
|
|
125
|
+
if speaker_wav:
|
|
126
|
+
speaker_wav_path = speaker_wav
|
|
127
|
+
print(f"Server: Using provided speaker_wav: {speaker_wav_path}")
|
|
128
|
+
|
|
129
|
+
# Second priority: check if voice parameter is a file path
|
|
130
|
+
elif voice and voice != "default":
|
|
131
|
+
if os.path.exists(voice):
|
|
132
|
+
# Voice parameter is a full file path
|
|
133
|
+
speaker_wav_path = voice
|
|
134
|
+
print(f"Server: Using voice as file path: {speaker_wav_path}")
|
|
135
|
+
else:
|
|
136
|
+
# Look for voice file in voices directory
|
|
137
|
+
voices_dir = Path(__file__).parent / "voices"
|
|
138
|
+
potential_voice_path = voices_dir / f"{voice}.wav"
|
|
139
|
+
if potential_voice_path.exists():
|
|
140
|
+
speaker_wav_path = str(potential_voice_path)
|
|
141
|
+
print(f"Server: Using custom voice file: {speaker_wav_path}")
|
|
142
|
+
else:
|
|
143
|
+
print(f"Server: Voice '{voice}' not found in voices directory")
|
|
144
|
+
|
|
145
|
+
# Create a temporary file for output
|
|
146
|
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
|
|
147
|
+
temp_output_path = temp_file.name
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# Generate audio using XTTS
|
|
151
|
+
if speaker_wav_path and os.path.exists(speaker_wav_path):
|
|
152
|
+
print(f"Server: Generating with speaker reference: {speaker_wav_path}")
|
|
164
153
|
self.model.tts_to_file(
|
|
165
154
|
text=text,
|
|
166
|
-
speaker_wav=
|
|
155
|
+
speaker_wav=speaker_wav_path,
|
|
167
156
|
language=language,
|
|
168
157
|
file_path=temp_output_path
|
|
169
158
|
)
|
|
170
159
|
else:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
160
|
+
print("Server: No valid speaker reference found, trying default")
|
|
161
|
+
# For XTTS without speaker reference, try to find a default
|
|
162
|
+
default_speaker = self._get_default_speaker_file()
|
|
163
|
+
if default_speaker and os.path.exists(default_speaker):
|
|
164
|
+
print(f"Server: Using default speaker: {default_speaker}")
|
|
165
|
+
self.model.tts_to_file(
|
|
166
|
+
text=text,
|
|
167
|
+
speaker_wav=default_speaker,
|
|
168
|
+
language=language,
|
|
169
|
+
file_path=temp_output_path
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
# Create a more helpful error message
|
|
173
|
+
available_voices = self._get_all_available_voice_files()
|
|
174
|
+
error_msg = f"No speaker reference available. XTTS requires a speaker reference file.\n"
|
|
175
|
+
error_msg += f"Attempted to use: {speaker_wav_path if speaker_wav_path else 'None'}\n"
|
|
176
|
+
error_msg += f"Available voice files: {available_voices}"
|
|
177
|
+
raise RuntimeError(error_msg)
|
|
178
|
+
|
|
179
|
+
# Read the generated audio file
|
|
180
|
+
with open(temp_output_path, 'rb') as f:
|
|
181
|
+
audio_bytes = f.read()
|
|
182
|
+
|
|
183
|
+
print(f"Server: Generated {len(audio_bytes)} bytes of audio")
|
|
184
|
+
return audio_bytes
|
|
185
|
+
|
|
186
|
+
finally:
|
|
187
|
+
# Clean up temporary file
|
|
188
|
+
if os.path.exists(temp_output_path):
|
|
189
|
+
os.unlink(temp_output_path)
|
|
184
190
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
except Exception as e:
|
|
191
|
-
print(f"Server: Error generating audio: {e}")
|
|
192
|
-
print(f"Server: Traceback:\n{traceback.format_exc()}")
|
|
193
|
-
raise
|
|
194
|
-
|
|
195
|
-
def _get_all_available_voice_files(self) -> List[str]:
|
|
196
|
-
"""Get list of all available voice files for debugging"""
|
|
197
|
-
voices_dir = Path(__file__).parent / "voices"
|
|
198
|
-
voice_files = []
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(f"Server: Error generating audio: {e}")
|
|
193
|
+
print(f"Server: Traceback:\n{traceback.format_exc()}")
|
|
194
|
+
raise
|
|
199
195
|
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
def _get_all_available_voice_files(self) -> List[str]:
|
|
197
|
+
"""Get list of all available voice files for debugging"""
|
|
198
|
+
voices_dir = Path(__file__).parent / "voices"
|
|
199
|
+
voice_files = []
|
|
202
200
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
voices_dir = Path(__file__).parent / "voices"
|
|
201
|
+
if voices_dir.exists():
|
|
202
|
+
voice_files = [str(f) for f in voices_dir.glob("*.wav")]
|
|
203
|
+
|
|
204
|
+
return voice_files
|
|
208
205
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
def _get_default_speaker_file(self) -> Optional[str]:
|
|
207
|
+
"""Get path to default speaker file"""
|
|
208
|
+
voices_dir = Path(__file__).parent / "voices"
|
|
209
|
+
|
|
210
|
+
# Look for a default speaker file
|
|
211
|
+
for filename in ["default.wav", "speaker.wav", "reference.wav"]:
|
|
212
|
+
potential_path = voices_dir / filename
|
|
213
|
+
if potential_path.exists():
|
|
214
|
+
return str(potential_path)
|
|
215
|
+
|
|
216
|
+
# If no default found, look for any wav file
|
|
217
|
+
wav_files = list(voices_dir.glob("*.wav"))
|
|
218
|
+
if wav_files:
|
|
219
|
+
return str(wav_files[0])
|
|
220
|
+
|
|
221
|
+
return None
|
|
214
222
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return str(wav_files[0])
|
|
223
|
+
def list_voices(self) -> List[str]:
|
|
224
|
+
"""Return list of available voices"""
|
|
225
|
+
return self.available_voices
|
|
219
226
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"""Return list of available voices"""
|
|
224
|
-
return self.available_voices
|
|
225
|
-
|
|
226
|
-
def list_models(self) -> List[str]:
|
|
227
|
-
"""Return list of available models"""
|
|
228
|
-
return self.available_models
|
|
227
|
+
def list_models(self) -> List[str]:
|
|
228
|
+
"""Return list of available models"""
|
|
229
|
+
return self.available_models
|
|
229
230
|
|
|
230
|
-
# --- Globals ---
|
|
231
|
-
app = FastAPI(title="XTTS Server")
|
|
232
|
-
router = APIRouter()
|
|
233
|
-
xtts_server = XTTSServer()
|
|
234
|
-
model_lock = asyncio.Lock() # Ensure thread-safe access
|
|
231
|
+
# --- Globals ---
|
|
232
|
+
app = FastAPI(title="XTTS Server")
|
|
233
|
+
router = APIRouter()
|
|
234
|
+
xtts_server = XTTSServer()
|
|
235
|
+
model_lock = asyncio.Lock() # Ensure thread-safe access
|
|
236
|
+
|
|
237
|
+
# --- API Endpoints ---
|
|
238
|
+
@router.post("/generate_audio")
|
|
239
|
+
async def generate_audio(request: GenerationRequest):
|
|
240
|
+
async with model_lock:
|
|
241
|
+
try:
|
|
242
|
+
audio_bytes = await xtts_server.generate_audio(
|
|
243
|
+
text=request.text,
|
|
244
|
+
voice=request.voice,
|
|
245
|
+
language=request.language,
|
|
246
|
+
speaker_wav=request.speaker_wav
|
|
247
|
+
)
|
|
248
|
+
from fastapi.responses import Response
|
|
249
|
+
return Response(content=audio_bytes, media_type="audio/wav")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
print(f"Server: ERROR in generate_audio endpoint: {e}")
|
|
252
|
+
print(f"Server: ERROR traceback:\n{traceback.format_exc()}")
|
|
253
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
235
254
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
async def generate_audio(request: GenerationRequest):
|
|
239
|
-
async with model_lock:
|
|
255
|
+
@router.get("/list_voices")
|
|
256
|
+
async def list_voices():
|
|
240
257
|
try:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
language=request.language,
|
|
245
|
-
speaker_wav=request.speaker_wav
|
|
246
|
-
)
|
|
247
|
-
from fastapi.responses import Response
|
|
248
|
-
return Response(content=audio_bytes, media_type="audio/wav")
|
|
258
|
+
voices = xtts_server.list_voices()
|
|
259
|
+
print(f"Server: Returning {len(voices)} voices: {voices}")
|
|
260
|
+
return {"voices": voices}
|
|
249
261
|
except Exception as e:
|
|
250
|
-
print(f"Server: ERROR in
|
|
262
|
+
print(f"Server: ERROR in list_voices endpoint: {e}")
|
|
251
263
|
print(f"Server: ERROR traceback:\n{traceback.format_exc()}")
|
|
252
264
|
raise HTTPException(status_code=500, detail=str(e))
|
|
253
265
|
|
|
254
|
-
@router.get("/
|
|
255
|
-
async def
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
@router.get("/list_models")
|
|
266
|
-
async def list_models():
|
|
267
|
-
try:
|
|
268
|
-
models = xtts_server.list_models()
|
|
269
|
-
print(f"Server: Returning {len(models)} models: {models}")
|
|
270
|
-
return {"models": models}
|
|
271
|
-
except Exception as e:
|
|
272
|
-
print(f"Server: ERROR in list_models endpoint: {e}")
|
|
273
|
-
print(f"Server: ERROR traceback:\n{traceback.format_exc()}")
|
|
274
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
266
|
+
@router.get("/list_models")
|
|
267
|
+
async def list_models():
|
|
268
|
+
try:
|
|
269
|
+
models = xtts_server.list_models()
|
|
270
|
+
print(f"Server: Returning {len(models)} models: {models}")
|
|
271
|
+
return {"models": models}
|
|
272
|
+
except Exception as e:
|
|
273
|
+
print(f"Server: ERROR in list_models endpoint: {e}")
|
|
274
|
+
print(f"Server: ERROR traceback:\n{traceback.format_exc()}")
|
|
275
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
275
276
|
|
|
276
|
-
@router.get("/status")
|
|
277
|
-
async def status():
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
277
|
+
@router.get("/status")
|
|
278
|
+
async def status():
|
|
279
|
+
return {
|
|
280
|
+
"status": "running",
|
|
281
|
+
"xtts_available": xtts_available,
|
|
282
|
+
"model_loaded": xtts_server.model_loaded,
|
|
283
|
+
"model_loading": xtts_server.model_loading,
|
|
284
|
+
"voices_count": len(xtts_server.available_voices),
|
|
285
|
+
"device": torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU"
|
|
286
|
+
}
|
|
286
287
|
|
|
287
|
-
# Add a health check endpoint that responds immediately
|
|
288
|
-
@router.get("/health")
|
|
289
|
-
async def health_check():
|
|
290
|
-
|
|
288
|
+
# Add a health check endpoint that responds immediately
|
|
289
|
+
@router.get("/health")
|
|
290
|
+
async def health_check():
|
|
291
|
+
return {"status": "healthy", "ready": True}
|
|
291
292
|
|
|
292
|
-
app.include_router(router)
|
|
293
|
+
app.include_router(router)
|
|
293
294
|
|
|
294
|
-
# --- Server Startup ---
|
|
295
|
-
if __name__ == '__main__':
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
295
|
+
# --- Server Startup ---
|
|
296
|
+
if __name__ == '__main__':
|
|
297
|
+
parser = argparse.ArgumentParser(description="XTTS TTS Server")
|
|
298
|
+
parser.add_argument("--host", type=str, default="localhost", help="Host to bind the server to.")
|
|
299
|
+
parser.add_argument("--port", type=int, default="8081", help="Port to bind the server to.")
|
|
300
|
+
|
|
301
|
+
args = parser.parse_args()
|
|
301
302
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
303
|
+
print(f"Server: Starting XTTS server on {args.host}:{args.port}")
|
|
304
|
+
print(f"Server: XTTS available: {xtts_available}")
|
|
305
|
+
print(f"Server: Model will be loaded on first audio generation request")
|
|
306
|
+
print(f"Server: Available voices: {len(xtts_server.available_voices)}")
|
|
307
|
+
if xtts_available:
|
|
308
|
+
print(f"Server: Device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU'}")
|
|
309
|
+
|
|
310
|
+
# Create voices directory if it doesn't exist
|
|
311
|
+
voices_dir = Path(__file__).parent / "voices"
|
|
312
|
+
voices_dir.mkdir(exist_ok=True)
|
|
313
|
+
print(f"Server: Voices directory: {voices_dir}")
|
|
314
|
+
|
|
315
|
+
uvicorn.run(app, host=args.host, port=args.port)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
print(f"Server: CRITICAL ERROR during startup: {e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lollms_client
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.2
|
|
4
4
|
Summary: A client library for LoLLMs generate endpoint
|
|
5
5
|
Author-email: ParisNeo <parisneoai@gmail.com>
|
|
6
6
|
License: Apache License
|
|
@@ -1302,6 +1302,7 @@ try:
|
|
|
1302
1302
|
except Exception as e:
|
|
1303
1303
|
ASCIIColors.error(f"Error initializing Hugging Face Inference API binding: {e}")
|
|
1304
1304
|
ASCIIColors.info("Please ensure your Hugging Face API token is correctly set and you have access to the specified model.")```
|
|
1305
|
+
```
|
|
1305
1306
|
|
|
1306
1307
|
---
|
|
1307
1308
|
|
|
@@ -1403,7 +1404,9 @@ else:
|
|
|
1403
1404
|
|
|
1404
1405
|
except Exception as e:
|
|
1405
1406
|
ASCIIColors.error(f"An error occurred during multi-image fusion: {e}")
|
|
1406
|
-
```
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
This powerful feature allows for complex creative tasks like character swapping, background replacement, and style transfer directly through the `lollms_client` library.
|
|
1407
1410
|
|
|
1408
1411
|
### Listing Available Models
|
|
1409
1412
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
lollms_client/__init__.py,sha256=
|
|
1
|
+
lollms_client/__init__.py,sha256=zZKNRiai-0QTZCCLS1KlMYvwRdcIEla7HeypZZBJUTs,1146
|
|
2
2
|
lollms_client/lollms_agentic.py,sha256=pQiMEuB_XkG29-SW6u4KTaMFPr6eKqacInggcCuCW3k,13914
|
|
3
3
|
lollms_client/lollms_config.py,sha256=goEseDwDxYJf3WkYJ4IrLXwg3Tfw73CXV2Avg45M_hE,21876
|
|
4
|
-
lollms_client/lollms_core.py,sha256=
|
|
4
|
+
lollms_client/lollms_core.py,sha256=TsyOvjtZtC-C2cy3-6Zag7QW-UfUN3jzfStgMsqfte4,321724
|
|
5
5
|
lollms_client/lollms_discussion.py,sha256=LZc9jYbUMRTovehiFJKEp-NXuCl_WnrqUtT3t4Nzayk,123922
|
|
6
6
|
lollms_client/lollms_js_analyzer.py,sha256=01zUvuO2F_lnUe_0NLxe1MF5aHE1hO8RZi48mNPv-aw,8361
|
|
7
7
|
lollms_client/lollms_llm_binding.py,sha256=tXuc3gxe6UrP36OBGsR-ESvQ9LpsB_nqtqL-GsEj6Uc,25019
|
|
@@ -12,7 +12,7 @@ lollms_client/lollms_python_analyzer.py,sha256=7gf1fdYgXCOkPUkBAPNmr6S-66hMH4_Ko
|
|
|
12
12
|
lollms_client/lollms_stt_binding.py,sha256=jAUhLouEhh2hmm1bK76ianfw_6B59EHfY3FmLv6DU-g,5111
|
|
13
13
|
lollms_client/lollms_tti_binding.py,sha256=B38nzBCSPV9jVRZa-x8W7l9nJEW0RyS1MMJoueb8kt0,8519
|
|
14
14
|
lollms_client/lollms_ttm_binding.py,sha256=FjVVSNXOZXK1qvcKEfxdiX6l2b4XdGOSNnZ0utAsbDg,4167
|
|
15
|
-
lollms_client/lollms_tts_binding.py,sha256=
|
|
15
|
+
lollms_client/lollms_tts_binding.py,sha256=k13rNq4YmuR50kkAEacwADW7COoDUOMLGAcnm27xjO4,5150
|
|
16
16
|
lollms_client/lollms_ttv_binding.py,sha256=KkTaHLBhEEdt4sSVBlbwr5i_g_TlhcrwrT-7DjOsjWQ,4131
|
|
17
17
|
lollms_client/lollms_types.py,sha256=0iSH1QHRRD-ddBqoL9EEKJ8wWCuwDUlN_FrfbCdg7Lw,3522
|
|
18
18
|
lollms_client/lollms_utilities.py,sha256=3DAsII2X9uhRzRL-D0QlALcEdRg82y7OIL4yHVF32gY,19446
|
|
@@ -75,13 +75,13 @@ lollms_client/tts_bindings/piper_tts/__init__.py,sha256=FbMw_m2QOn2ny7r5El_s6jBy
|
|
|
75
75
|
lollms_client/tts_bindings/piper_tts/server/install_piper.py,sha256=g71Ne2T18wAytOPipfQ9DNeTAOD9PrII5qC-vr9DtLA,3256
|
|
76
76
|
lollms_client/tts_bindings/piper_tts/server/main.py,sha256=DMozfSR1aCbrlmOXltRFjtXhYhXajsGcNKQjsWgRwZk,17402
|
|
77
77
|
lollms_client/tts_bindings/piper_tts/server/setup_voices.py,sha256=UdHaPa5aNcw8dR-aRGkZr2OfSFFejH79lXgfwT0P3ss,1964
|
|
78
|
-
lollms_client/tts_bindings/xtts/__init__.py,sha256=
|
|
79
|
-
lollms_client/tts_bindings/xtts/server/main.py,sha256=
|
|
78
|
+
lollms_client/tts_bindings/xtts/__init__.py,sha256=2a1qzF8iDbr1bt0KQhcNbZqy0B06gjsWVAMJF-YATrE,6371
|
|
79
|
+
lollms_client/tts_bindings/xtts/server/main.py,sha256=27l5FlclhqiXSBf-_qg4QR7Q7mUly72kaBtG8kBXASE,14088
|
|
80
80
|
lollms_client/tts_bindings/xtts/server/setup_voices.py,sha256=UdHaPa5aNcw8dR-aRGkZr2OfSFFejH79lXgfwT0P3ss,1964
|
|
81
81
|
lollms_client/ttv_bindings/__init__.py,sha256=UZ8o2izQOJLQgtZ1D1cXoNST7rzqW22rL2Vufc7ddRc,3141
|
|
82
82
|
lollms_client/ttv_bindings/lollms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
83
|
-
lollms_client-1.6.
|
|
84
|
-
lollms_client-1.6.
|
|
85
|
-
lollms_client-1.6.
|
|
86
|
-
lollms_client-1.6.
|
|
87
|
-
lollms_client-1.6.
|
|
83
|
+
lollms_client-1.6.2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
84
|
+
lollms_client-1.6.2.dist-info/METADATA,sha256=g8ACOGQCyj4dIDIfa3fJmyqJzoLB-aN2U_qkOKs7CVw,76834
|
|
85
|
+
lollms_client-1.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
86
|
+
lollms_client-1.6.2.dist-info/top_level.txt,sha256=Bk_kz-ri6Arwsk7YG-T5VsRorV66uVhcHGvb_g2WqgE,14
|
|
87
|
+
lollms_client-1.6.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|