scitex 2.14.0__py3-none-any.whl → 2.15.1__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.
- scitex/__init__.py +47 -0
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +191 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/writer.py +21 -204
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +127 -59
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/engines/elevenlabs_engine.py +6 -1
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +443 -0
- scitex/cli/main.py +198 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/scholar/__init__.py +8 -0
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/writer.py +117 -0
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +191 -0
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/introspect/__init__.py +75 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +42 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/introspect/_mcp/__init__.py +37 -0
- scitex/introspect/_mcp/handlers.py +208 -0
- scitex/introspect/_members.py +151 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/bundle/README.md +1 -1
- scitex/mcp_server.py +98 -5
- scitex/plt/__init__.py +248 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/sh/README.md +1 -1
- scitex/social/__init__.py +153 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/template/README.md +1 -1
- scitex/template/clone_writer_directory.py +5 -5
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.1.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
scitex/audio/_relay.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Audio relay client for remote TTS playback.
|
|
3
|
+
|
|
4
|
+
Forwards speak requests to a remote audio server via HTTP.
|
|
5
|
+
This allows agents running on remote machines (e.g., NAS) to play
|
|
6
|
+
audio through the user's local speakers.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from scitex.audio._relay import RelayClient
|
|
10
|
+
|
|
11
|
+
# Connect to local relay server
|
|
12
|
+
client = RelayClient("http://localhost:31293")
|
|
13
|
+
|
|
14
|
+
# Speak through relay
|
|
15
|
+
client.speak("Hello from remote!")
|
|
16
|
+
|
|
17
|
+
Environment Variables:
|
|
18
|
+
SCITEX_AUDIO_RELAY_URL: Relay server URL
|
|
19
|
+
SCITEX_AUDIO_RELAY_HOST: Relay server host (builds URL with port)
|
|
20
|
+
SCITEX_AUDIO_RELAY_PORT: Relay server port (default: 31293)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import urllib.error
|
|
25
|
+
import urllib.request
|
|
26
|
+
from typing import Any, Dict, Optional
|
|
27
|
+
|
|
28
|
+
from ._branding import DEFAULT_PORT, get_relay_url
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RelayClient:
|
|
32
|
+
"""HTTP client for audio relay server.
|
|
33
|
+
|
|
34
|
+
Forwards TTS requests to a remote server running the audio MCP
|
|
35
|
+
in HTTP mode. Enables remote agents to play audio locally.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> client = RelayClient("http://localhost:31293")
|
|
39
|
+
>>> client.speak("Hello!")
|
|
40
|
+
{'success': True, 'text': 'Hello!', ...}
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
base_url: Optional[str] = None,
|
|
46
|
+
timeout: int = 30,
|
|
47
|
+
):
|
|
48
|
+
"""Initialize relay client.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
base_url: Relay server URL. Auto-detects from env if None.
|
|
52
|
+
timeout: Request timeout in seconds.
|
|
53
|
+
"""
|
|
54
|
+
self.base_url = (
|
|
55
|
+
base_url or get_relay_url() or f"http://localhost:{DEFAULT_PORT}"
|
|
56
|
+
).rstrip("/")
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
|
|
59
|
+
def _request(
|
|
60
|
+
self,
|
|
61
|
+
endpoint: str,
|
|
62
|
+
data: Optional[Dict[str, Any]] = None,
|
|
63
|
+
method: str = "POST",
|
|
64
|
+
) -> Dict[str, Any]:
|
|
65
|
+
"""Make HTTP request to relay server."""
|
|
66
|
+
url = f"{self.base_url}{endpoint}"
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
if data is not None:
|
|
70
|
+
req_data = json.dumps(data).encode("utf-8")
|
|
71
|
+
req = urllib.request.Request(url, data=req_data, method=method)
|
|
72
|
+
req.add_header("Content-Type", "application/json")
|
|
73
|
+
else:
|
|
74
|
+
req = urllib.request.Request(url, method=method)
|
|
75
|
+
|
|
76
|
+
req.add_header("Accept", "application/json")
|
|
77
|
+
|
|
78
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as response:
|
|
79
|
+
return json.loads(response.read().decode("utf-8"))
|
|
80
|
+
|
|
81
|
+
except urllib.error.HTTPError as e:
|
|
82
|
+
raise ConnectionError(f"Relay request failed: {e.code} {e.reason}") from e
|
|
83
|
+
except urllib.error.URLError as e:
|
|
84
|
+
raise ConnectionError(
|
|
85
|
+
f"Cannot connect to relay at {self.base_url}: {e.reason}"
|
|
86
|
+
) from e
|
|
87
|
+
|
|
88
|
+
def health(self) -> Dict[str, Any]:
|
|
89
|
+
"""Check relay server health."""
|
|
90
|
+
try:
|
|
91
|
+
# Try MCP health endpoint
|
|
92
|
+
return self._request("/health", method="GET")
|
|
93
|
+
except Exception:
|
|
94
|
+
# Relay may not have /health, try simple request
|
|
95
|
+
return {"status": "unknown", "url": self.base_url}
|
|
96
|
+
|
|
97
|
+
def is_available(self) -> bool:
|
|
98
|
+
"""Check if relay server is reachable."""
|
|
99
|
+
try:
|
|
100
|
+
self.health()
|
|
101
|
+
return True
|
|
102
|
+
except Exception:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def speak(
|
|
106
|
+
self,
|
|
107
|
+
text: str,
|
|
108
|
+
backend: Optional[str] = None,
|
|
109
|
+
voice: Optional[str] = None,
|
|
110
|
+
rate: int = 150,
|
|
111
|
+
speed: float = 1.5,
|
|
112
|
+
play: bool = True,
|
|
113
|
+
save: bool = False,
|
|
114
|
+
fallback: bool = True,
|
|
115
|
+
agent_id: Optional[str] = None,
|
|
116
|
+
) -> Dict[str, Any]:
|
|
117
|
+
"""Forward speak request to relay server.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
text: Text to speak
|
|
121
|
+
backend: TTS backend (auto if None)
|
|
122
|
+
voice: Voice/language
|
|
123
|
+
rate: Speech rate (pyttsx3)
|
|
124
|
+
speed: Speed multiplier (gtts)
|
|
125
|
+
play: Play audio
|
|
126
|
+
save: Save audio file
|
|
127
|
+
fallback: Try fallback backends
|
|
128
|
+
agent_id: Agent identifier
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
Response dict with success status
|
|
133
|
+
"""
|
|
134
|
+
data = {
|
|
135
|
+
"text": text,
|
|
136
|
+
"backend": backend,
|
|
137
|
+
"voice": voice,
|
|
138
|
+
"rate": rate,
|
|
139
|
+
"speed": speed,
|
|
140
|
+
"play": play,
|
|
141
|
+
"save": save,
|
|
142
|
+
"fallback": fallback,
|
|
143
|
+
"agent_id": agent_id,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Remove None values
|
|
147
|
+
data = {k: v for k, v in data.items() if v is not None}
|
|
148
|
+
|
|
149
|
+
return self._request("/speak", data)
|
|
150
|
+
|
|
151
|
+
def list_backends(self) -> Dict[str, Any]:
|
|
152
|
+
"""Get available backends from relay server."""
|
|
153
|
+
return self._request("/list_backends", method="GET")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Module-level client singleton
|
|
157
|
+
_client: Optional[RelayClient] = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_relay_client(base_url: Optional[str] = None) -> RelayClient:
|
|
161
|
+
"""Get or create relay client singleton."""
|
|
162
|
+
global _client
|
|
163
|
+
if _client is None or (base_url and _client.base_url != base_url):
|
|
164
|
+
_client = RelayClient(base_url)
|
|
165
|
+
return _client
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def reset_relay_client() -> None:
|
|
169
|
+
"""Reset relay client singleton."""
|
|
170
|
+
global _client
|
|
171
|
+
_client = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def relay_speak(
|
|
175
|
+
text: str,
|
|
176
|
+
backend: Optional[str] = None,
|
|
177
|
+
voice: Optional[str] = None,
|
|
178
|
+
rate: int = 150,
|
|
179
|
+
speed: float = 1.5,
|
|
180
|
+
play: bool = True,
|
|
181
|
+
**kwargs,
|
|
182
|
+
) -> Dict[str, Any]:
|
|
183
|
+
"""Convenience function to speak via relay.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
text: Text to speak
|
|
187
|
+
backend: TTS backend
|
|
188
|
+
voice: Voice/language
|
|
189
|
+
rate: Speech rate
|
|
190
|
+
speed: Speed multiplier
|
|
191
|
+
play: Play audio
|
|
192
|
+
**kwargs: Additional options
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
Response dict
|
|
197
|
+
"""
|
|
198
|
+
client = get_relay_client()
|
|
199
|
+
return client.speak(
|
|
200
|
+
text=text,
|
|
201
|
+
backend=backend,
|
|
202
|
+
voice=voice,
|
|
203
|
+
rate=rate,
|
|
204
|
+
speed=speed,
|
|
205
|
+
play=play,
|
|
206
|
+
**kwargs,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def is_relay_available() -> bool:
|
|
211
|
+
"""Check if relay server is available."""
|
|
212
|
+
try:
|
|
213
|
+
client = get_relay_client()
|
|
214
|
+
return client.is_available()
|
|
215
|
+
except Exception:
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
__all__ = [
|
|
220
|
+
"RelayClient",
|
|
221
|
+
"get_relay_client",
|
|
222
|
+
"reset_relay_client",
|
|
223
|
+
"relay_speak",
|
|
224
|
+
"is_relay_available",
|
|
225
|
+
]
|
|
@@ -38,6 +38,10 @@ class ElevenLabsTTS(BaseTTS):
|
|
|
38
38
|
"sam": "yoZ06aMxZJJ28mfd3POQ",
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
# ElevenLabs API speed limits
|
|
42
|
+
MIN_SPEED = 0.7
|
|
43
|
+
MAX_SPEED = 1.2
|
|
44
|
+
|
|
41
45
|
def __init__(
|
|
42
46
|
self,
|
|
43
47
|
api_key: Optional[str] = None,
|
|
@@ -58,7 +62,8 @@ class ElevenLabsTTS(BaseTTS):
|
|
|
58
62
|
self.model_id = model_id
|
|
59
63
|
self.stability = stability
|
|
60
64
|
self.similarity_boost = similarity_boost
|
|
61
|
-
|
|
65
|
+
# Clamp speed to ElevenLabs API limits (0.7-1.2)
|
|
66
|
+
self.speed = max(self.MIN_SPEED, min(self.MAX_SPEED, speed))
|
|
62
67
|
self._client = None
|
|
63
68
|
|
|
64
69
|
@property
|
scitex/audio/mcp_server.py
CHANGED
|
@@ -10,13 +10,13 @@ Enables remote agents to connect and play audio on local speakers.
|
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
12
|
scitex audio serve # stdio (default)
|
|
13
|
-
scitex audio serve -t http --port
|
|
14
|
-
scitex audio serve -t sse --port
|
|
13
|
+
scitex audio serve -t http --port 31293 # HTTP transport
|
|
14
|
+
scitex audio serve -t sse --port 31293 # SSE transport
|
|
15
15
|
|
|
16
16
|
For remote audio:
|
|
17
|
-
1. Run locally: scitex audio serve -t http --port
|
|
18
|
-
2. SSH tunnel: ssh -R
|
|
19
|
-
3. Remote agent connects to http://localhost:
|
|
17
|
+
1. Run locally: scitex audio serve -t http --port 31293
|
|
18
|
+
2. SSH tunnel: ssh -R 31293:localhost:31293 remote-host
|
|
19
|
+
3. Remote agent connects to http://localhost:31293
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
@@ -35,16 +35,19 @@ except ImportError:
|
|
|
35
35
|
FASTMCP_AVAILABLE = False
|
|
36
36
|
FastMCP = None # type: ignore
|
|
37
37
|
|
|
38
|
-
__all__ = ["mcp", "run_server", "main", "FASTMCP_AVAILABLE"]
|
|
38
|
+
__all__ = ["mcp", "run_server", "run_relay_server", "main", "FASTMCP_AVAILABLE"]
|
|
39
|
+
|
|
40
|
+
# Import branding
|
|
41
|
+
from ._branding import (
|
|
42
|
+
get_mcp_instructions,
|
|
43
|
+
get_mcp_server_name,
|
|
44
|
+
)
|
|
39
45
|
|
|
40
46
|
# Initialize MCP server
|
|
41
47
|
if FASTMCP_AVAILABLE:
|
|
42
48
|
mcp = FastMCP(
|
|
43
|
-
name=
|
|
44
|
-
instructions=(
|
|
45
|
-
"Text-to-Speech server with multiple backends (pyttsx3, gtts, elevenlabs). "
|
|
46
|
-
"Use speak() to convert text to speech. Supports cross-process FIFO queuing."
|
|
47
|
-
),
|
|
49
|
+
name=get_mcp_server_name(),
|
|
50
|
+
instructions=get_mcp_instructions(),
|
|
48
51
|
)
|
|
49
52
|
else:
|
|
50
53
|
mcp = None
|
|
@@ -61,6 +64,16 @@ def _get_audio_dir() -> Path:
|
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
if FASTMCP_AVAILABLE:
|
|
67
|
+
import asyncio
|
|
68
|
+
|
|
69
|
+
def _run_async(coro):
|
|
70
|
+
"""Run async handler synchronously for FastMCP tools."""
|
|
71
|
+
try:
|
|
72
|
+
loop = asyncio.get_event_loop()
|
|
73
|
+
except RuntimeError:
|
|
74
|
+
loop = asyncio.new_event_loop()
|
|
75
|
+
asyncio.set_event_loop(loop)
|
|
76
|
+
return loop.run_until_complete(coro)
|
|
64
77
|
|
|
65
78
|
@mcp.tool()
|
|
66
79
|
def speak(
|
|
@@ -74,72 +87,92 @@ if FASTMCP_AVAILABLE:
|
|
|
74
87
|
fallback: bool = True,
|
|
75
88
|
agent_id: Optional[str] = None,
|
|
76
89
|
) -> str:
|
|
77
|
-
"""Convert text to speech with fallback (pyttsx3 -> gtts -> elevenlabs).
|
|
90
|
+
"""[audio] Convert text to speech with fallback (pyttsx3 -> gtts -> elevenlabs).
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
save: Save audio to file (default False)
|
|
87
|
-
fallback: Try next backend on failure (default True)
|
|
88
|
-
agent_id: Optional identifier for the agent making the request
|
|
92
|
+
NOTE: Plays on SERVER. For remote use, see speak_relay.
|
|
93
|
+
"""
|
|
94
|
+
return audio_speak_local(
|
|
95
|
+
text=text, backend=backend, voice=voice, rate=rate,
|
|
96
|
+
speed=speed, play=play, save=save, fallback=fallback,
|
|
97
|
+
agent_id=agent_id,
|
|
98
|
+
)
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
@mcp.tool()
|
|
101
|
+
def audio_speak_local(
|
|
102
|
+
text: str,
|
|
103
|
+
backend: Optional[str] = None,
|
|
104
|
+
voice: Optional[str] = None,
|
|
105
|
+
rate: int = 150,
|
|
106
|
+
speed: float = 1.5,
|
|
107
|
+
play: bool = True,
|
|
108
|
+
save: bool = False,
|
|
109
|
+
fallback: bool = True,
|
|
110
|
+
agent_id: Optional[str] = None,
|
|
111
|
+
) -> str:
|
|
112
|
+
"""[audio] Convert text to speech on the LOCAL/SERVER machine.
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
speak("Hello world")
|
|
97
|
-
speak("Bonjour", backend="gtts", voice="fr")
|
|
98
|
-
speak("Fast speech", rate=200)
|
|
99
|
-
"""
|
|
100
|
-
from . import speak as tts_speak
|
|
101
|
-
from ._cross_process_lock import AudioPlaybackLock
|
|
114
|
+
Use when running Claude Code directly on your local machine.
|
|
115
|
+
Audio plays where MCP server runs.
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
Args:
|
|
118
|
+
text: Text to convert to speech
|
|
119
|
+
backend: TTS backend (pyttsx3, gtts, elevenlabs)
|
|
120
|
+
voice: Voice/language
|
|
121
|
+
rate: Speech rate (pyttsx3 only)
|
|
122
|
+
speed: Speed multiplier (gtts)
|
|
123
|
+
play: Play audio (default True)
|
|
124
|
+
save: Save to file (default False)
|
|
125
|
+
fallback: Try next backend on failure
|
|
126
|
+
agent_id: Agent identifier
|
|
127
|
+
"""
|
|
128
|
+
from ._mcp.speak_handlers import speak_local_handler
|
|
107
129
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
text=text,
|
|
115
|
-
backend=backend,
|
|
116
|
-
voice=voice,
|
|
117
|
-
play=play,
|
|
118
|
-
output_path=output_path,
|
|
119
|
-
fallback=fallback,
|
|
120
|
-
rate=rate,
|
|
121
|
-
speed=speed,
|
|
122
|
-
)
|
|
123
|
-
finally:
|
|
124
|
-
lock.release()
|
|
130
|
+
result = _run_async(speak_local_handler(
|
|
131
|
+
text=text, backend=backend, voice=voice, rate=rate,
|
|
132
|
+
speed=speed, play=play, save=save, fallback=fallback,
|
|
133
|
+
agent_id=agent_id,
|
|
134
|
+
))
|
|
135
|
+
return json.dumps(result, indent=2)
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
@mcp.tool()
|
|
138
|
+
def audio_speak_relay(
|
|
139
|
+
text: str,
|
|
140
|
+
backend: Optional[str] = None,
|
|
141
|
+
voice: Optional[str] = None,
|
|
142
|
+
rate: int = 150,
|
|
143
|
+
speed: float = 1.5,
|
|
144
|
+
play: bool = True,
|
|
145
|
+
save: bool = False,
|
|
146
|
+
fallback: bool = True,
|
|
147
|
+
agent_id: Optional[str] = None,
|
|
148
|
+
) -> str:
|
|
149
|
+
"""[audio] Convert text to speech via RELAY server (remote playback).
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
Use when running on remote server (e.g., NAS) and want audio
|
|
152
|
+
on your local machine. REQUIRES relay server running locally.
|
|
138
153
|
|
|
139
|
-
|
|
154
|
+
Args:
|
|
155
|
+
text: Text to convert to speech
|
|
156
|
+
backend: TTS backend (pyttsx3, gtts, elevenlabs)
|
|
157
|
+
voice: Voice/language
|
|
158
|
+
rate: Speech rate (pyttsx3 only)
|
|
159
|
+
speed: Speed multiplier (gtts)
|
|
160
|
+
play: Play audio (default True)
|
|
161
|
+
save: Save to file (default False)
|
|
162
|
+
fallback: Try next backend on failure
|
|
163
|
+
agent_id: Agent identifier
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Success with playback info, or error with setup instructions
|
|
167
|
+
"""
|
|
168
|
+
from ._mcp.speak_handlers import speak_relay_handler
|
|
140
169
|
|
|
141
|
-
|
|
142
|
-
|
|
170
|
+
result = _run_async(speak_relay_handler(
|
|
171
|
+
text=text, backend=backend, voice=voice, rate=rate,
|
|
172
|
+
speed=speed, play=play, save=save, fallback=fallback,
|
|
173
|
+
agent_id=agent_id,
|
|
174
|
+
))
|
|
175
|
+
return json.dumps(result, indent=2)
|
|
143
176
|
|
|
144
177
|
@mcp.tool()
|
|
145
178
|
def list_backends() -> str:
|
|
@@ -244,21 +277,28 @@ if FASTMCP_AVAILABLE:
|
|
|
244
277
|
|
|
245
278
|
def run_server(
|
|
246
279
|
transport: str = "stdio",
|
|
247
|
-
host: str =
|
|
248
|
-
port: int =
|
|
280
|
+
host: Optional[str] = None,
|
|
281
|
+
port: Optional[int] = None,
|
|
249
282
|
) -> None:
|
|
250
283
|
"""Run the MCP server.
|
|
251
284
|
|
|
252
285
|
Args:
|
|
253
286
|
transport: Transport protocol ("stdio", "sse", or "http")
|
|
254
|
-
host: Host for HTTP/SSE transport
|
|
255
|
-
port: Port for HTTP/SSE transport
|
|
287
|
+
host: Host for HTTP/SSE transport (default from branding)
|
|
288
|
+
port: Port for HTTP/SSE transport (default from branding)
|
|
256
289
|
"""
|
|
290
|
+
from ._branding import DEFAULT_HOST, DEFAULT_PORT
|
|
291
|
+
|
|
292
|
+
host = host or DEFAULT_HOST
|
|
293
|
+
port = port or DEFAULT_PORT
|
|
294
|
+
|
|
257
295
|
if not FASTMCP_AVAILABLE:
|
|
258
296
|
import sys
|
|
259
297
|
|
|
298
|
+
from ._branding import BRAND_NAME
|
|
299
|
+
|
|
260
300
|
print("=" * 60)
|
|
261
|
-
print("MCP Server '
|
|
301
|
+
print(f"MCP Server '{BRAND_NAME}' requires the 'fastmcp' package.")
|
|
262
302
|
print()
|
|
263
303
|
print("Install with:")
|
|
264
304
|
print(" pip install fastmcp")
|
|
@@ -268,19 +308,132 @@ def run_server(
|
|
|
268
308
|
print("=" * 60)
|
|
269
309
|
sys.exit(1)
|
|
270
310
|
|
|
311
|
+
from ._branding import BRAND_NAME
|
|
312
|
+
|
|
271
313
|
if transport == "stdio":
|
|
272
314
|
mcp.run(transport="stdio")
|
|
273
315
|
elif transport == "sse":
|
|
274
|
-
print(f"Starting
|
|
316
|
+
print(f"Starting {BRAND_NAME} MCP server (SSE) on {host}:{port}")
|
|
275
317
|
mcp.run(transport="sse", host=host, port=port)
|
|
276
318
|
elif transport == "http":
|
|
277
|
-
print(f"Starting
|
|
319
|
+
print(f"Starting {BRAND_NAME} MCP server (HTTP) on {host}:{port}")
|
|
278
320
|
print(f"Connect via: http://{host}:{port}/mcp")
|
|
279
321
|
mcp.run(transport="streamable-http", host=host, port=port)
|
|
280
322
|
else:
|
|
281
323
|
raise ValueError(f"Unknown transport: {transport}")
|
|
282
324
|
|
|
283
325
|
|
|
326
|
+
def run_relay_server(host: Optional[str] = None, port: Optional[int] = None) -> None:
|
|
327
|
+
"""Run HTTP relay server for remote audio playback.
|
|
328
|
+
|
|
329
|
+
This exposes simple REST endpoints that remote agents can connect to.
|
|
330
|
+
Unlike the MCP server, this uses standard HTTP POST/GET.
|
|
331
|
+
|
|
332
|
+
Endpoints:
|
|
333
|
+
POST /speak - Speak text
|
|
334
|
+
GET /health - Health check
|
|
335
|
+
GET /list_backends - List available backends
|
|
336
|
+
"""
|
|
337
|
+
from ._branding import BRAND_NAME, DEFAULT_HOST, DEFAULT_PORT
|
|
338
|
+
|
|
339
|
+
host = host or DEFAULT_HOST
|
|
340
|
+
port = port or DEFAULT_PORT
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
344
|
+
except ImportError as e:
|
|
345
|
+
raise RuntimeError(f"HTTP server not available: {e}") from e
|
|
346
|
+
|
|
347
|
+
class RelayHandler(BaseHTTPRequestHandler):
|
|
348
|
+
"""HTTP handler for audio relay requests."""
|
|
349
|
+
|
|
350
|
+
def _send_json(self, data: dict, status: int = 200) -> None:
|
|
351
|
+
"""Send JSON response."""
|
|
352
|
+
import json
|
|
353
|
+
|
|
354
|
+
body = json.dumps(data, indent=2).encode("utf-8")
|
|
355
|
+
self.send_response(status)
|
|
356
|
+
self.send_header("Content-Type", "application/json")
|
|
357
|
+
self.send_header("Content-Length", str(len(body)))
|
|
358
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
359
|
+
self.end_headers()
|
|
360
|
+
self.wfile.write(body)
|
|
361
|
+
|
|
362
|
+
def do_OPTIONS(self) -> None:
|
|
363
|
+
"""Handle CORS preflight."""
|
|
364
|
+
self.send_response(200)
|
|
365
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
366
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
367
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
368
|
+
self.end_headers()
|
|
369
|
+
|
|
370
|
+
def do_GET(self) -> None:
|
|
371
|
+
"""Handle GET requests."""
|
|
372
|
+
import json
|
|
373
|
+
|
|
374
|
+
if self.path == "/health":
|
|
375
|
+
self._send_json({"status": "healthy", "server": BRAND_NAME})
|
|
376
|
+
elif self.path == "/list_backends":
|
|
377
|
+
result = list_backends()
|
|
378
|
+
self._send_json(json.loads(result))
|
|
379
|
+
else:
|
|
380
|
+
self._send_json({"error": "Not found"}, 404)
|
|
381
|
+
|
|
382
|
+
def do_POST(self) -> None:
|
|
383
|
+
"""Handle POST requests."""
|
|
384
|
+
import json
|
|
385
|
+
|
|
386
|
+
if self.path == "/speak":
|
|
387
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
388
|
+
body = self.rfile.read(content_length)
|
|
389
|
+
try:
|
|
390
|
+
data = json.loads(body.decode("utf-8"))
|
|
391
|
+
# Import speak directly from audio module (not MCP tool)
|
|
392
|
+
from . import speak as tts_speak
|
|
393
|
+
from ._cross_process_lock import AudioPlaybackLock
|
|
394
|
+
|
|
395
|
+
# Acquire lock for FIFO
|
|
396
|
+
lock = AudioPlaybackLock()
|
|
397
|
+
lock.acquire(timeout=120.0)
|
|
398
|
+
try:
|
|
399
|
+
tts_speak(
|
|
400
|
+
text=data.get("text", ""),
|
|
401
|
+
backend=data.get("backend"),
|
|
402
|
+
voice=data.get("voice"),
|
|
403
|
+
rate=data.get("rate", 150),
|
|
404
|
+
speed=data.get("speed", 1.5),
|
|
405
|
+
play=data.get("play", True),
|
|
406
|
+
fallback=data.get("fallback", True),
|
|
407
|
+
mode="local",
|
|
408
|
+
)
|
|
409
|
+
finally:
|
|
410
|
+
lock.release()
|
|
411
|
+
|
|
412
|
+
self._send_json({
|
|
413
|
+
"success": True,
|
|
414
|
+
"text": data.get("text", ""),
|
|
415
|
+
"played": True,
|
|
416
|
+
"timestamp": datetime.now().isoformat(),
|
|
417
|
+
})
|
|
418
|
+
except Exception as e:
|
|
419
|
+
self._send_json({"success": False, "error": str(e)}, 500)
|
|
420
|
+
else:
|
|
421
|
+
self._send_json({"error": "Not found"}, 404)
|
|
422
|
+
|
|
423
|
+
def log_message(self, format: str, *args) -> None:
|
|
424
|
+
"""Suppress default logging."""
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
print(f"Starting {BRAND_NAME} relay server on {host}:{port}")
|
|
428
|
+
print("Endpoints: POST /speak, GET /health, GET /list_backends")
|
|
429
|
+
server = HTTPServer((host, port), RelayHandler)
|
|
430
|
+
try:
|
|
431
|
+
server.serve_forever()
|
|
432
|
+
except KeyboardInterrupt:
|
|
433
|
+
print("\nShutting down relay server")
|
|
434
|
+
server.shutdown()
|
|
435
|
+
|
|
436
|
+
|
|
284
437
|
def main():
|
|
285
438
|
"""Entry point for scitex-audio-mcp command."""
|
|
286
439
|
run_server(transport="stdio")
|
scitex/canvas/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- ---
|
|
2
2
|
!-- Timestamp: 2025-12-08
|
|
3
3
|
!-- Author: ywatanabe
|
|
4
|
-
!-- File: /home/ywatanabe/proj/scitex-
|
|
4
|
+
!-- File: /home/ywatanabe/proj/scitex-python/src/scitex/vis/README.md
|
|
5
5
|
!-- --- -->
|
|
6
6
|
|
|
7
7
|
# scitex.vis - Canvas-Based Figure Composition
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/__init__.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
DearPyGui editor package.
|
|
7
|
+
|
|
8
|
+
Modular GPU-accelerated figure editor using DearPyGui.
|
|
9
|
+
|
|
10
|
+
Modules:
|
|
11
|
+
- _state: EditorState dataclass for all editor state
|
|
12
|
+
- _utils: Utility functions (checkerboard, constants)
|
|
13
|
+
- _rendering: Figure rendering and highlight drawing
|
|
14
|
+
- _plotting: CSV data plotting
|
|
15
|
+
- _selection: Element selection and hover detection
|
|
16
|
+
- _handlers: Event handlers and callbacks
|
|
17
|
+
- _panels: UI panel creation
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ._editor import DearPyGuiEditor
|
|
21
|
+
|
|
22
|
+
__all__ = ["DearPyGuiEditor"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# EOF
|