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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Configuration constants for scitex.audio.
|
|
3
|
+
|
|
4
|
+
Provides environment variable helpers and default values for the audio module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# Fixed branding (scitex.audio is part of scitex, not rebranded)
|
|
11
|
+
BRAND_NAME = "scitex.audio"
|
|
12
|
+
BRAND_ALIAS = "audio"
|
|
13
|
+
ENV_PREFIX = "SCITEX_AUDIO"
|
|
14
|
+
|
|
15
|
+
# Default port: 31293 (SCITEX port scheme: 3129X where X=3 for audio)
|
|
16
|
+
DEFAULT_PORT = 31293
|
|
17
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_env(key: str, default: Optional[str] = None) -> Optional[str]:
|
|
21
|
+
"""Get environment variable with SCITEX_AUDIO_ prefix.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
key: Variable name without prefix (e.g., "PORT", "HOST", "MODE")
|
|
25
|
+
default: Default value if not found
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
Environment variable value or default
|
|
30
|
+
|
|
31
|
+
Examples
|
|
32
|
+
--------
|
|
33
|
+
get_env("PORT") # Checks SCITEX_AUDIO_PORT
|
|
34
|
+
get_env("MODE", "local") # With default
|
|
35
|
+
"""
|
|
36
|
+
return os.environ.get(f"SCITEX_AUDIO_{key}", default)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_port() -> int:
|
|
40
|
+
"""Get configured port number."""
|
|
41
|
+
port_str = get_env("PORT", str(DEFAULT_PORT))
|
|
42
|
+
return int(port_str)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_host() -> str:
|
|
46
|
+
"""Get configured host."""
|
|
47
|
+
return get_env("HOST", DEFAULT_HOST)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_mode() -> str:
|
|
51
|
+
"""Get audio mode: 'local', 'remote', or 'auto'.
|
|
52
|
+
|
|
53
|
+
- local: Always play locally (direct TTS)
|
|
54
|
+
- remote: Always forward to relay server
|
|
55
|
+
- auto: Try local first, fall back to remote
|
|
56
|
+
"""
|
|
57
|
+
return get_env("MODE", "auto").lower()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_ssh_client_ip() -> Optional[str]:
|
|
61
|
+
"""Get IP address of SSH client if running in SSH session.
|
|
62
|
+
|
|
63
|
+
Extracts client IP from SSH_CLIENT or SSH_CONNECTION env vars.
|
|
64
|
+
Returns None if not in SSH session.
|
|
65
|
+
"""
|
|
66
|
+
# SSH_CLIENT format: "client_ip client_port server_port"
|
|
67
|
+
ssh_client = os.environ.get("SSH_CLIENT", "")
|
|
68
|
+
if ssh_client:
|
|
69
|
+
parts = ssh_client.split()
|
|
70
|
+
if parts:
|
|
71
|
+
return parts[0]
|
|
72
|
+
|
|
73
|
+
# SSH_CONNECTION format: "client_ip client_port server_ip server_port"
|
|
74
|
+
ssh_connection = os.environ.get("SSH_CONNECTION", "")
|
|
75
|
+
if ssh_connection:
|
|
76
|
+
parts = ssh_connection.split()
|
|
77
|
+
if parts:
|
|
78
|
+
return parts[0]
|
|
79
|
+
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _check_relay_reachable(url: str, timeout: float = 1.0) -> bool:
|
|
84
|
+
"""Quick check if relay URL is reachable."""
|
|
85
|
+
import socket
|
|
86
|
+
import urllib.parse
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
parsed = urllib.parse.urlparse(url)
|
|
90
|
+
host = parsed.hostname or "localhost"
|
|
91
|
+
port = parsed.port or DEFAULT_PORT
|
|
92
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
93
|
+
sock.settimeout(timeout)
|
|
94
|
+
result = sock.connect_ex((host, port))
|
|
95
|
+
sock.close()
|
|
96
|
+
return result == 0
|
|
97
|
+
except Exception:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_relay_url() -> Optional[str]:
|
|
102
|
+
"""Get relay server URL for remote mode.
|
|
103
|
+
|
|
104
|
+
Priority:
|
|
105
|
+
1. SCITEX_AUDIO_RELAY_URL env var
|
|
106
|
+
2. SCITEX_AUDIO_RELAY_HOST env var + port
|
|
107
|
+
3. localhost:31293 (SSH reverse tunnel - if reachable)
|
|
108
|
+
4. Auto-detect from SSH_CLIENT (if in SSH session)
|
|
109
|
+
|
|
110
|
+
Returns URL like 'http://192.168.1.100:31293' or None.
|
|
111
|
+
"""
|
|
112
|
+
url = get_env("RELAY_URL")
|
|
113
|
+
if url:
|
|
114
|
+
return url.rstrip("/")
|
|
115
|
+
|
|
116
|
+
# Build from host/port if relay host is set
|
|
117
|
+
relay_host = get_env("RELAY_HOST")
|
|
118
|
+
if relay_host:
|
|
119
|
+
relay_port = get_env("RELAY_PORT", str(DEFAULT_PORT))
|
|
120
|
+
return f"http://{relay_host}:{relay_port}"
|
|
121
|
+
|
|
122
|
+
# Check localhost first (SSH reverse tunnel)
|
|
123
|
+
localhost_url = f"http://localhost:{DEFAULT_PORT}"
|
|
124
|
+
if _check_relay_reachable(localhost_url):
|
|
125
|
+
return localhost_url
|
|
126
|
+
|
|
127
|
+
# Auto-detect from SSH client IP
|
|
128
|
+
ssh_client_ip = get_ssh_client_ip()
|
|
129
|
+
if ssh_client_ip:
|
|
130
|
+
return f"http://{ssh_client_ip}:{DEFAULT_PORT}"
|
|
131
|
+
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_mcp_server_name() -> str:
|
|
136
|
+
"""Get the MCP server name."""
|
|
137
|
+
return "scitex-audio"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_mcp_instructions() -> str:
|
|
141
|
+
"""Get MCP server instructions."""
|
|
142
|
+
return """\
|
|
143
|
+
scitex.audio - Text-to-Speech with Multiple Backends
|
|
144
|
+
|
|
145
|
+
Backends (fallback order): elevenlabs -> gtts -> pyttsx3
|
|
146
|
+
|
|
147
|
+
## Quick Start
|
|
148
|
+
```python
|
|
149
|
+
from scitex.audio import speak
|
|
150
|
+
speak("Hello, world!") # Auto-selects backend
|
|
151
|
+
speak("Fast", backend="gtts", speed=1.5)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## MCP Tools
|
|
155
|
+
- **speak**: Convert text to speech
|
|
156
|
+
- **list_backends**: Show available TTS backends
|
|
157
|
+
- **check_audio_status**: Check WSL audio connectivity
|
|
158
|
+
- **announce_context**: Announce current directory and git branch
|
|
159
|
+
|
|
160
|
+
## Remote Audio Relay
|
|
161
|
+
Run locally: `scitex audio serve -t http --port 31293`
|
|
162
|
+
Remote agents connect via HTTP to play audio on local speakers.
|
|
163
|
+
|
|
164
|
+
## Environment Variables
|
|
165
|
+
- SCITEX_AUDIO_MODE: 'local', 'remote', or 'auto' (default: auto)
|
|
166
|
+
- SCITEX_AUDIO_RELAY_URL: Remote relay server URL
|
|
167
|
+
- SCITEX_AUDIO_PORT: Server port (default: 31293)
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
__all__ = [
|
|
172
|
+
"BRAND_NAME",
|
|
173
|
+
"BRAND_ALIAS",
|
|
174
|
+
"ENV_PREFIX",
|
|
175
|
+
"DEFAULT_PORT",
|
|
176
|
+
"DEFAULT_HOST",
|
|
177
|
+
"get_env",
|
|
178
|
+
"get_port",
|
|
179
|
+
"get_host",
|
|
180
|
+
"get_mode",
|
|
181
|
+
"get_relay_url",
|
|
182
|
+
"get_ssh_client_ip",
|
|
183
|
+
"get_mcp_server_name",
|
|
184
|
+
"get_mcp_instructions",
|
|
185
|
+
]
|
scitex/audio/_mcp/__init__.py
CHANGED
|
@@ -2,3 +2,35 @@
|
|
|
2
2
|
# File: __init__.py
|
|
3
3
|
"""MCP server components."""
|
|
4
4
|
|
|
5
|
+
from .handlers import (
|
|
6
|
+
announce_context_handler,
|
|
7
|
+
check_audio_status_handler,
|
|
8
|
+
clear_audio_cache_handler,
|
|
9
|
+
generate_audio_handler,
|
|
10
|
+
list_audio_files_handler,
|
|
11
|
+
list_backends_handler,
|
|
12
|
+
list_voices_handler,
|
|
13
|
+
play_audio_handler,
|
|
14
|
+
speak_handler,
|
|
15
|
+
speech_queue_status_handler,
|
|
16
|
+
)
|
|
17
|
+
from .speak_handlers import (
|
|
18
|
+
speak_local_handler,
|
|
19
|
+
speak_relay_handler,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"speak_handler",
|
|
24
|
+
"speak_local_handler",
|
|
25
|
+
"speak_relay_handler",
|
|
26
|
+
"generate_audio_handler",
|
|
27
|
+
"list_backends_handler",
|
|
28
|
+
"list_voices_handler",
|
|
29
|
+
"play_audio_handler",
|
|
30
|
+
"list_audio_files_handler",
|
|
31
|
+
"clear_audio_cache_handler",
|
|
32
|
+
"check_audio_status_handler",
|
|
33
|
+
"speech_queue_status_handler",
|
|
34
|
+
"announce_context_handler",
|
|
35
|
+
]
|
|
36
|
+
|
scitex/audio/_mcp/handlers.py
CHANGED
|
@@ -36,6 +36,37 @@ def _get_audio_dir() -> Path:
|
|
|
36
36
|
return audio_dir
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def _get_signature() -> str:
|
|
40
|
+
"""Get signature string with hostname, project, and branch."""
|
|
41
|
+
import os
|
|
42
|
+
import socket
|
|
43
|
+
import subprocess
|
|
44
|
+
|
|
45
|
+
hostname = socket.gethostname()
|
|
46
|
+
cwd = os.getcwd()
|
|
47
|
+
project = os.path.basename(cwd)
|
|
48
|
+
|
|
49
|
+
branch = None
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
timeout=5,
|
|
57
|
+
)
|
|
58
|
+
if result.returncode == 0:
|
|
59
|
+
branch = result.stdout.strip()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
parts = [f"Hostname: {hostname}", f"Project: {project}"]
|
|
64
|
+
if branch:
|
|
65
|
+
parts.append(f"Branch: {branch}")
|
|
66
|
+
|
|
67
|
+
return ". ".join(parts) + ". "
|
|
68
|
+
|
|
69
|
+
|
|
39
70
|
async def generate_audio_handler(
|
|
40
71
|
text: str,
|
|
41
72
|
backend: str | None = None,
|
|
@@ -89,16 +120,16 @@ async def generate_audio_handler(
|
|
|
89
120
|
async def list_backends_handler() -> dict:
|
|
90
121
|
"""List available TTS backends."""
|
|
91
122
|
try:
|
|
92
|
-
from .. import available_backends
|
|
123
|
+
from .. import FALLBACK_ORDER, available_backends
|
|
93
124
|
|
|
94
125
|
backends = available_backends()
|
|
95
126
|
|
|
96
127
|
info = []
|
|
97
|
-
for b in
|
|
128
|
+
for b in FALLBACK_ORDER:
|
|
98
129
|
available = b in backends
|
|
99
130
|
desc = {
|
|
100
|
-
"gtts": "Google TTS - Free, requires internet",
|
|
101
131
|
"elevenlabs": "ElevenLabs - Paid, high quality",
|
|
132
|
+
"gtts": "Google TTS - Free, requires internet",
|
|
102
133
|
"pyttsx3": "System TTS - Offline, uses espeak/SAPI5",
|
|
103
134
|
}
|
|
104
135
|
info.append(
|
|
@@ -109,11 +140,18 @@ async def list_backends_handler() -> dict:
|
|
|
109
140
|
}
|
|
110
141
|
)
|
|
111
142
|
|
|
143
|
+
# Determine actual default based on FALLBACK_ORDER
|
|
144
|
+
default = None
|
|
145
|
+
for b in FALLBACK_ORDER:
|
|
146
|
+
if b in backends:
|
|
147
|
+
default = b
|
|
148
|
+
break
|
|
149
|
+
|
|
112
150
|
return {
|
|
113
151
|
"success": True,
|
|
114
152
|
"backends": info,
|
|
115
153
|
"available": backends,
|
|
116
|
-
"default":
|
|
154
|
+
"default": default,
|
|
117
155
|
}
|
|
118
156
|
|
|
119
157
|
except Exception as e:
|
|
@@ -267,13 +305,25 @@ async def speak_handler(
|
|
|
267
305
|
fallback: bool = True,
|
|
268
306
|
agent_id: str | None = None,
|
|
269
307
|
wait: bool = True,
|
|
308
|
+
signature: bool = False,
|
|
270
309
|
) -> dict:
|
|
271
|
-
"""Convert text to speech with fallback.
|
|
310
|
+
"""Convert text to speech with fallback.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
signature: If True, prepend hostname/project/branch to text.
|
|
314
|
+
"""
|
|
272
315
|
try:
|
|
273
316
|
from .. import speak as tts_speak
|
|
274
317
|
|
|
275
318
|
loop = asyncio.get_event_loop()
|
|
276
319
|
|
|
320
|
+
# Prepend signature if requested
|
|
321
|
+
final_text = text
|
|
322
|
+
sig = None
|
|
323
|
+
if signature:
|
|
324
|
+
sig = _get_signature()
|
|
325
|
+
final_text = sig + text
|
|
326
|
+
|
|
277
327
|
output_path = None
|
|
278
328
|
if save:
|
|
279
329
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
@@ -281,7 +331,7 @@ async def speak_handler(
|
|
|
281
331
|
|
|
282
332
|
def do_speak():
|
|
283
333
|
return tts_speak(
|
|
284
|
-
text=
|
|
334
|
+
text=final_text,
|
|
285
335
|
backend=backend,
|
|
286
336
|
voice=voice,
|
|
287
337
|
rate=rate,
|
|
@@ -300,6 +350,9 @@ async def speak_handler(
|
|
|
300
350
|
"played": play,
|
|
301
351
|
"timestamp": datetime.now().isoformat(),
|
|
302
352
|
}
|
|
353
|
+
if signature:
|
|
354
|
+
result["signature"] = sig
|
|
355
|
+
result["full_text"] = final_text
|
|
303
356
|
if result_path:
|
|
304
357
|
result["path"] = str(result_path)
|
|
305
358
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Speak handlers for scitex.audio MCP server.
|
|
3
|
+
|
|
4
|
+
Provides speak_local_handler and speak_relay_handler for explicit control
|
|
5
|
+
over audio playback location (server vs relay).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"speak_local_handler",
|
|
16
|
+
"speak_relay_handler",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_audio_dir() -> Path:
|
|
21
|
+
"""Get the audio output directory."""
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
base_dir = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
|
|
25
|
+
audio_dir = base_dir / "audio"
|
|
26
|
+
audio_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return audio_dir
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_signature() -> str:
|
|
31
|
+
"""Get signature string with hostname, project, and branch."""
|
|
32
|
+
import os
|
|
33
|
+
import socket
|
|
34
|
+
import subprocess
|
|
35
|
+
|
|
36
|
+
hostname = socket.gethostname()
|
|
37
|
+
cwd = os.getcwd()
|
|
38
|
+
project = os.path.basename(cwd)
|
|
39
|
+
|
|
40
|
+
branch = None
|
|
41
|
+
try:
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
cwd=cwd,
|
|
47
|
+
timeout=5,
|
|
48
|
+
)
|
|
49
|
+
if result.returncode == 0:
|
|
50
|
+
branch = result.stdout.strip()
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
parts = [f"Hostname: {hostname}", f"Project: {project}"]
|
|
55
|
+
if branch:
|
|
56
|
+
parts.append(f"Branch: {branch}")
|
|
57
|
+
|
|
58
|
+
return ". ".join(parts) + ". "
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def speak_local_handler(
|
|
62
|
+
text: str,
|
|
63
|
+
backend: str | None = None,
|
|
64
|
+
voice: str | None = None,
|
|
65
|
+
rate: int = 150,
|
|
66
|
+
speed: float = 1.5,
|
|
67
|
+
play: bool = True,
|
|
68
|
+
save: bool = False,
|
|
69
|
+
fallback: bool = True,
|
|
70
|
+
agent_id: str | None = None,
|
|
71
|
+
signature: bool = False,
|
|
72
|
+
) -> dict:
|
|
73
|
+
"""Play audio on the LOCAL/SERVER machine.
|
|
74
|
+
|
|
75
|
+
Use when running Claude Code directly on your local machine.
|
|
76
|
+
Audio plays on the machine where the MCP server is running.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
from .. import speak as tts_speak
|
|
80
|
+
from .._cross_process_lock import AudioPlaybackLock
|
|
81
|
+
|
|
82
|
+
loop = asyncio.get_event_loop()
|
|
83
|
+
|
|
84
|
+
final_text = text
|
|
85
|
+
sig = None
|
|
86
|
+
if signature:
|
|
87
|
+
sig = _get_signature()
|
|
88
|
+
final_text = sig + text
|
|
89
|
+
|
|
90
|
+
output_path = None
|
|
91
|
+
if save:
|
|
92
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
93
|
+
output_path = str(_get_audio_dir() / f"tts_{timestamp}.mp3")
|
|
94
|
+
|
|
95
|
+
def do_speak():
|
|
96
|
+
# Acquire cross-process lock for FIFO audio playback
|
|
97
|
+
lock = AudioPlaybackLock()
|
|
98
|
+
lock.acquire(timeout=120.0)
|
|
99
|
+
try:
|
|
100
|
+
return tts_speak(
|
|
101
|
+
text=final_text,
|
|
102
|
+
backend=backend,
|
|
103
|
+
voice=voice,
|
|
104
|
+
rate=rate,
|
|
105
|
+
speed=speed,
|
|
106
|
+
play=play,
|
|
107
|
+
output_path=output_path,
|
|
108
|
+
fallback=fallback,
|
|
109
|
+
mode="local", # Force local mode
|
|
110
|
+
)
|
|
111
|
+
finally:
|
|
112
|
+
lock.release()
|
|
113
|
+
|
|
114
|
+
result_path = await loop.run_in_executor(None, do_speak)
|
|
115
|
+
|
|
116
|
+
result = {
|
|
117
|
+
"success": True,
|
|
118
|
+
"text": text,
|
|
119
|
+
"backend": backend,
|
|
120
|
+
"played": play,
|
|
121
|
+
"played_on": "server",
|
|
122
|
+
"agent_id": agent_id,
|
|
123
|
+
"timestamp": datetime.now().isoformat(),
|
|
124
|
+
}
|
|
125
|
+
if signature:
|
|
126
|
+
result["signature"] = sig
|
|
127
|
+
result["full_text"] = final_text
|
|
128
|
+
if result_path:
|
|
129
|
+
result["path"] = str(result_path)
|
|
130
|
+
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return {"success": False, "error": str(e)}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def speak_relay_handler(
|
|
138
|
+
text: str,
|
|
139
|
+
backend: str | None = None,
|
|
140
|
+
voice: str | None = None,
|
|
141
|
+
rate: int = 150,
|
|
142
|
+
speed: float = 1.5,
|
|
143
|
+
play: bool = True,
|
|
144
|
+
save: bool = False,
|
|
145
|
+
fallback: bool = True,
|
|
146
|
+
agent_id: str | None = None,
|
|
147
|
+
) -> dict:
|
|
148
|
+
"""Forward speech to RELAY server for remote playback.
|
|
149
|
+
|
|
150
|
+
Use when running on a remote server and want audio on your local machine.
|
|
151
|
+
Returns detailed error with setup instructions if relay unavailable.
|
|
152
|
+
"""
|
|
153
|
+
from .._branding import DEFAULT_PORT, get_relay_url, get_ssh_client_ip
|
|
154
|
+
from .._relay import RelayClient, is_relay_available
|
|
155
|
+
|
|
156
|
+
# Get relay URL (auto-detects from SSH_CLIENT if not configured)
|
|
157
|
+
relay_url = get_relay_url()
|
|
158
|
+
ssh_client_ip = get_ssh_client_ip()
|
|
159
|
+
|
|
160
|
+
if not relay_url:
|
|
161
|
+
return {
|
|
162
|
+
"success": False,
|
|
163
|
+
"error": "Relay server URL not configured",
|
|
164
|
+
"reason": "No SSH session detected and no env vars set",
|
|
165
|
+
"instructions": [
|
|
166
|
+
"1. Start relay server on your LOCAL machine:",
|
|
167
|
+
f" scitex audio serve -t http --port {DEFAULT_PORT}",
|
|
168
|
+
"",
|
|
169
|
+
"2. SSH to this server (relay URL auto-detected from SSH_CLIENT)",
|
|
170
|
+
"",
|
|
171
|
+
"3. Or set env var manually:",
|
|
172
|
+
f" export SCITEX_AUDIO_RELAY_URL=http://YOUR_LOCAL_IP:{DEFAULT_PORT}",
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Check if relay server is reachable
|
|
177
|
+
if not is_relay_available():
|
|
178
|
+
source = "auto-detected from SSH_CLIENT" if ssh_client_ip else "from env var"
|
|
179
|
+
return {
|
|
180
|
+
"success": False,
|
|
181
|
+
"error": "Relay server not reachable",
|
|
182
|
+
"reason": f"Cannot connect to {relay_url} ({source})",
|
|
183
|
+
"relay_url": relay_url,
|
|
184
|
+
"auto_detected": ssh_client_ip is not None,
|
|
185
|
+
"ssh_client_ip": ssh_client_ip,
|
|
186
|
+
"instructions": [
|
|
187
|
+
"1. Start relay server on your LOCAL machine:",
|
|
188
|
+
f" scitex audio serve -t http --port {DEFAULT_PORT}",
|
|
189
|
+
"",
|
|
190
|
+
f"2. Current relay URL: {relay_url}",
|
|
191
|
+
f" Source: {source}",
|
|
192
|
+
"",
|
|
193
|
+
"3. Test connectivity:",
|
|
194
|
+
f" curl {relay_url}/health",
|
|
195
|
+
],
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Forward to relay server
|
|
199
|
+
try:
|
|
200
|
+
loop = asyncio.get_event_loop()
|
|
201
|
+
|
|
202
|
+
def do_relay():
|
|
203
|
+
client = RelayClient(relay_url)
|
|
204
|
+
return client.speak(
|
|
205
|
+
text=text,
|
|
206
|
+
backend=backend,
|
|
207
|
+
voice=voice,
|
|
208
|
+
rate=rate,
|
|
209
|
+
speed=speed,
|
|
210
|
+
play=play,
|
|
211
|
+
save=save,
|
|
212
|
+
fallback=fallback,
|
|
213
|
+
agent_id=agent_id,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
result = await loop.run_in_executor(None, do_relay)
|
|
217
|
+
|
|
218
|
+
result["played_on"] = "relay"
|
|
219
|
+
result["relay_url"] = relay_url
|
|
220
|
+
result["timestamp"] = datetime.now().isoformat()
|
|
221
|
+
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
return {
|
|
226
|
+
"success": False,
|
|
227
|
+
"error": f"Relay request failed: {str(e)}",
|
|
228
|
+
"relay_url": relay_url,
|
|
229
|
+
"instructions": [
|
|
230
|
+
"1. Check relay server is still running",
|
|
231
|
+
"2. Check network connectivity",
|
|
232
|
+
f"3. Test: curl -X POST {relay_url}/speak "
|
|
233
|
+
"-H 'Content-Type: application/json' -d '{\"text\": \"test\"}'",
|
|
234
|
+
],
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# EOF
|