scitex 2.16.0__py3-none-any.whl → 2.16.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.
- scitex/_mcp_tools/audio.py +11 -65
- scitex/audio/README.md +40 -12
- scitex/audio/__init__.py +27 -235
- scitex/audio/_audio_check.py +93 -0
- scitex/audio/_mcp/speak_handlers.py +56 -8
- scitex/audio/_speak.py +295 -0
- scitex/audio/mcp_server.py +98 -73
- 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/social/__init__.py +1 -24
- scitex/writer/README.md +25 -409
- scitex/writer/__init__.py +98 -13
- {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/METADATA +6 -1
- {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/RECORD +21 -93
- 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/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/writer/Writer.py +0 -487
- scitex/writer/_clone_writer_project.py +0 -160
- scitex/writer/_compile/__init__.py +0 -41
- scitex/writer/_compile/_compile_async.py +0 -130
- scitex/writer/_compile/_compile_unified.py +0 -148
- scitex/writer/_compile/_parser.py +0 -63
- scitex/writer/_compile/_runner.py +0 -457
- scitex/writer/_compile/_validator.py +0 -46
- scitex/writer/_compile/manuscript.py +0 -110
- scitex/writer/_compile/revision.py +0 -82
- scitex/writer/_compile/supplementary.py +0 -100
- scitex/writer/_dataclasses/__init__.py +0 -44
- scitex/writer/_dataclasses/config/_CONSTANTS.py +0 -46
- scitex/writer/_dataclasses/config/_WriterConfig.py +0 -175
- scitex/writer/_dataclasses/config/__init__.py +0 -9
- scitex/writer/_dataclasses/contents/_ManuscriptContents.py +0 -236
- scitex/writer/_dataclasses/contents/_RevisionContents.py +0 -136
- scitex/writer/_dataclasses/contents/_SupplementaryContents.py +0 -114
- scitex/writer/_dataclasses/contents/__init__.py +0 -9
- scitex/writer/_dataclasses/core/_Document.py +0 -146
- scitex/writer/_dataclasses/core/_DocumentSection.py +0 -546
- scitex/writer/_dataclasses/core/__init__.py +0 -7
- scitex/writer/_dataclasses/results/_CompilationResult.py +0 -165
- scitex/writer/_dataclasses/results/_LaTeXIssue.py +0 -102
- scitex/writer/_dataclasses/results/_SaveSectionsResponse.py +0 -118
- scitex/writer/_dataclasses/results/_SectionReadResponse.py +0 -131
- scitex/writer/_dataclasses/results/__init__.py +0 -11
- scitex/writer/_dataclasses/tree/MINIMUM_FILES.md +0 -121
- scitex/writer/_dataclasses/tree/_ConfigTree.py +0 -86
- scitex/writer/_dataclasses/tree/_ManuscriptTree.py +0 -84
- scitex/writer/_dataclasses/tree/_RevisionTree.py +0 -97
- scitex/writer/_dataclasses/tree/_ScriptsTree.py +0 -118
- scitex/writer/_dataclasses/tree/_SharedTree.py +0 -100
- scitex/writer/_dataclasses/tree/_SupplementaryTree.py +0 -101
- scitex/writer/_dataclasses/tree/__init__.py +0 -23
- scitex/writer/_mcp/__init__.py +0 -4
- scitex/writer/_mcp/handlers.py +0 -32
- scitex/writer/_mcp/tool_schemas.py +0 -33
- scitex/writer/_project/__init__.py +0 -29
- scitex/writer/_project/_create.py +0 -89
- scitex/writer/_project/_trees.py +0 -63
- scitex/writer/_project/_validate.py +0 -61
- scitex/writer/utils/.legacy_git_retry.py +0 -164
- scitex/writer/utils/__init__.py +0 -24
- scitex/writer/utils/_converters.py +0 -635
- scitex/writer/utils/_parse_latex_logs.py +0 -138
- scitex/writer/utils/_parse_script_args.py +0 -156
- scitex/writer/utils/_verify_tree_structure.py +0 -205
- scitex/writer/utils/_watch.py +0 -96
- {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/WHEEL +0 -0
- {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/entry_points.txt +0 -0
- {scitex-2.16.0.dist-info → scitex-2.16.2.dist-info}/licenses/LICENSE +0 -0
scitex/_mcp_tools/audio.py
CHANGED
|
@@ -30,7 +30,17 @@ def register_audio_tools(mcp) -> None:
|
|
|
30
30
|
wait: bool = True,
|
|
31
31
|
signature: bool = False,
|
|
32
32
|
) -> str:
|
|
33
|
-
"""[audio] Convert text to speech with
|
|
33
|
+
"""[audio] Convert text to speech with smart routing.
|
|
34
|
+
|
|
35
|
+
Smart routing (mode=auto, default):
|
|
36
|
+
- If local audio sink is SUSPENDED and relay available -> uses relay
|
|
37
|
+
- If local audio available -> uses local
|
|
38
|
+
- If neither available -> returns error with instructions
|
|
39
|
+
|
|
40
|
+
Environment variables:
|
|
41
|
+
- SCITEX_AUDIO_MODE: 'local', 'remote', or 'auto' (default: auto)
|
|
42
|
+
- SCITEX_AUDIO_RELAY_URL: Relay server URL for remote playback
|
|
43
|
+
"""
|
|
34
44
|
from scitex.audio._mcp.handlers import speak_handler
|
|
35
45
|
|
|
36
46
|
result = await speak_handler(
|
|
@@ -132,69 +142,5 @@ def register_audio_tools(mcp) -> None:
|
|
|
132
142
|
result = await announce_context_handler(include_full_path=include_full_path)
|
|
133
143
|
return _json(result)
|
|
134
144
|
|
|
135
|
-
@mcp.tool()
|
|
136
|
-
async def audio_speak_local(
|
|
137
|
-
text: str,
|
|
138
|
-
backend: Optional[str] = None,
|
|
139
|
-
voice: Optional[str] = None,
|
|
140
|
-
rate: int = 150,
|
|
141
|
-
speed: float = 1.5,
|
|
142
|
-
play: bool = True,
|
|
143
|
-
save: bool = False,
|
|
144
|
-
fallback: bool = True,
|
|
145
|
-
agent_id: Optional[str] = None,
|
|
146
|
-
) -> str:
|
|
147
|
-
"""[audio] Convert text to speech on the LOCAL/SERVER machine.
|
|
148
|
-
|
|
149
|
-
Use when running Claude Code directly on your local machine.
|
|
150
|
-
Audio plays where MCP server runs.
|
|
151
|
-
"""
|
|
152
|
-
from scitex.audio._mcp.speak_handlers import speak_local_handler
|
|
153
|
-
|
|
154
|
-
result = await speak_local_handler(
|
|
155
|
-
text=text,
|
|
156
|
-
backend=backend,
|
|
157
|
-
voice=voice,
|
|
158
|
-
rate=rate,
|
|
159
|
-
speed=speed,
|
|
160
|
-
play=play,
|
|
161
|
-
save=save,
|
|
162
|
-
fallback=fallback,
|
|
163
|
-
agent_id=agent_id,
|
|
164
|
-
)
|
|
165
|
-
return _json(result)
|
|
166
|
-
|
|
167
|
-
@mcp.tool()
|
|
168
|
-
async def audio_speak_relay(
|
|
169
|
-
text: str,
|
|
170
|
-
backend: Optional[str] = None,
|
|
171
|
-
voice: Optional[str] = None,
|
|
172
|
-
rate: int = 150,
|
|
173
|
-
speed: float = 1.5,
|
|
174
|
-
play: bool = True,
|
|
175
|
-
save: bool = False,
|
|
176
|
-
fallback: bool = True,
|
|
177
|
-
agent_id: Optional[str] = None,
|
|
178
|
-
) -> str:
|
|
179
|
-
"""[audio] Convert text to speech via RELAY server (remote playback).
|
|
180
|
-
|
|
181
|
-
Use when running on remote server (NAS) and want audio on your
|
|
182
|
-
local machine. Returns error with setup instructions if unavailable.
|
|
183
|
-
"""
|
|
184
|
-
from scitex.audio._mcp.speak_handlers import speak_relay_handler
|
|
185
|
-
|
|
186
|
-
result = await speak_relay_handler(
|
|
187
|
-
text=text,
|
|
188
|
-
backend=backend,
|
|
189
|
-
voice=voice,
|
|
190
|
-
rate=rate,
|
|
191
|
-
speed=speed,
|
|
192
|
-
play=play,
|
|
193
|
-
save=save,
|
|
194
|
-
fallback=fallback,
|
|
195
|
-
agent_id=agent_id,
|
|
196
|
-
)
|
|
197
|
-
return _json(result)
|
|
198
|
-
|
|
199
145
|
|
|
200
146
|
# EOF
|
scitex/audio/README.md
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
# SciTeX Audio
|
|
2
2
|
|
|
3
|
-
Text-to-Speech with automatic fallback
|
|
3
|
+
Text-to-Speech with automatic fallback and smart routing.
|
|
4
|
+
|
|
5
|
+
**Fallback order:** elevenlabs -> gtts -> pyttsx3
|
|
6
|
+
|
|
7
|
+
## Smart Routing
|
|
8
|
+
|
|
9
|
+
The `speak()` function automatically routes audio based on availability:
|
|
10
|
+
|
|
11
|
+
| Local Sink | Relay Available | Result (mode=auto) |
|
|
12
|
+
|------------|-----------------|-------------------|
|
|
13
|
+
| SUSPENDED | Yes | Uses relay |
|
|
14
|
+
| SUSPENDED | No | Returns error |
|
|
15
|
+
| RUNNING | Yes | Prefers relay |
|
|
16
|
+
| RUNNING | No | Uses local |
|
|
4
17
|
|
|
5
18
|
## Usage
|
|
6
19
|
|
|
7
20
|
```python
|
|
8
21
|
import scitex
|
|
9
22
|
|
|
10
|
-
# Basic
|
|
11
|
-
scitex.audio.speak("Hello!")
|
|
23
|
+
# Basic - auto mode (smart routing)
|
|
24
|
+
result = scitex.audio.speak("Hello!")
|
|
25
|
+
if result["played"]:
|
|
26
|
+
print(f"Played via {result['mode']}")
|
|
12
27
|
|
|
13
|
-
#
|
|
14
|
-
scitex.audio.speak("Hello!",
|
|
28
|
+
# Force local playback (fails if sink unavailable)
|
|
29
|
+
result = scitex.audio.speak("Hello!", mode="local")
|
|
30
|
+
|
|
31
|
+
# Force remote relay
|
|
32
|
+
result = scitex.audio.speak("Hello!", mode="remote")
|
|
33
|
+
|
|
34
|
+
# Faster speech (speed multiplier for gtts)
|
|
35
|
+
scitex.audio.speak("Hello!", speed=1.5)
|
|
15
36
|
|
|
16
37
|
# Specific backend
|
|
17
|
-
scitex.audio.speak("Hello", backend="
|
|
38
|
+
scitex.audio.speak("Hello", backend="gtts")
|
|
39
|
+
|
|
40
|
+
# Check local audio availability
|
|
41
|
+
status = scitex.audio.check_local_audio_available()
|
|
42
|
+
print(status) # {'available': False, 'state': 'SUSPENDED', 'reason': '...'}
|
|
18
43
|
|
|
19
44
|
# Stop speech
|
|
20
45
|
scitex.audio.stop_speech()
|
|
@@ -58,8 +83,8 @@ Enable remote agents to play audio on local speakers using a simple HTTP relay.
|
|
|
58
83
|
│ Remote (e.g., NAS) │ │ Local (WSL/Windows) │
|
|
59
84
|
│ │ │ │
|
|
60
85
|
│ Claude Agent uses │ │ scitex audio relay │
|
|
61
|
-
│
|
|
62
|
-
│
|
|
86
|
+
│ audio_speak ───────────┼─ SSH ───────▶│ --port 31293 │
|
|
87
|
+
│ (auto-routes to relay) │ Reverse │ │ │
|
|
63
88
|
│ localhost:31293 │ Tunnel │ ▼ │
|
|
64
89
|
│ │ │ 🔊 Speakers │
|
|
65
90
|
└─────────────────────────┘ └─────────────────────────┘
|
|
@@ -85,7 +110,7 @@ Host nas
|
|
|
85
110
|
|
|
86
111
|
**Step 3: Remote agent uses relay**
|
|
87
112
|
|
|
88
|
-
The `
|
|
113
|
+
The unified `audio_speak` MCP tool auto-detects relay:
|
|
89
114
|
1. `SCITEX_AUDIO_RELAY_URL` env var
|
|
90
115
|
2. Localhost:31293 (SSH reverse tunnel)
|
|
91
116
|
3. SSH_CLIENT IP (auto-detected from SSH session)
|
|
@@ -113,13 +138,16 @@ The `audio_speak_relay` MCP tool auto-detects:
|
|
|
113
138
|
|
|
114
139
|
| Tool | Description |
|
|
115
140
|
|------|-------------|
|
|
116
|
-
| `audio_speak` |
|
|
117
|
-
| `audio_speak_local` | TTS on server machine |
|
|
118
|
-
| `audio_speak_relay` | TTS via relay (remote playback) |
|
|
141
|
+
| `audio_speak` | **Unified TTS with smart routing** (auto-selects local/relay) |
|
|
119
142
|
| `audio_list_backends` | Show available backends |
|
|
120
143
|
| `audio_check_audio_status` | Check WSL audio connectivity |
|
|
121
144
|
| `audio_announce_context` | Announce current directory and git branch |
|
|
122
145
|
|
|
146
|
+
The `audio_speak` tool automatically:
|
|
147
|
+
- Checks if local audio sink is available (not SUSPENDED)
|
|
148
|
+
- Uses relay server when local audio unavailable
|
|
149
|
+
- Returns clear error messages when neither is available
|
|
150
|
+
|
|
123
151
|
## Backends
|
|
124
152
|
|
|
125
153
|
| Backend | Cost | Internet | Install |
|
scitex/audio/__init__.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/__init__.py
|
|
4
4
|
# ----------------------------------------
|
|
5
5
|
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
"""
|
|
7
9
|
SciTeX Audio Module - Text-to-Speech with Multiple Backends
|
|
8
10
|
|
|
@@ -31,8 +33,7 @@ Installation:
|
|
|
31
33
|
pip install scitex[audio]
|
|
32
34
|
"""
|
|
33
35
|
|
|
34
|
-
import subprocess
|
|
35
|
-
from typing import List, Optional
|
|
36
|
+
import subprocess as _subprocess
|
|
36
37
|
|
|
37
38
|
# Check for missing dependencies and warn user (internal)
|
|
38
39
|
from scitex._install_guide import warn_module_deps as _warn_module_deps
|
|
@@ -41,15 +42,17 @@ _missing = _warn_module_deps("audio")
|
|
|
41
42
|
|
|
42
43
|
# Import from engines subpackage (public TTS classes only)
|
|
43
44
|
# Internal imports (prefixed with _ to hide from API)
|
|
45
|
+
from . import engines as _engines_module
|
|
44
46
|
from .engines import ElevenLabsTTS, GoogleTTS, SystemTTS
|
|
45
47
|
from .engines._base import BaseTTS as _BaseTTS
|
|
46
48
|
from .engines._base import TTSBackend as _TTSBackend
|
|
49
|
+
del _engines_module
|
|
47
50
|
|
|
48
51
|
|
|
49
52
|
def stop_speech() -> None:
|
|
50
53
|
"""Stop any currently playing speech by killing espeak processes."""
|
|
51
54
|
try:
|
|
52
|
-
|
|
55
|
+
_subprocess.run(["pkill", "-9", "espeak"], capture_output=True)
|
|
53
56
|
except Exception:
|
|
54
57
|
pass
|
|
55
58
|
|
|
@@ -94,7 +97,7 @@ def check_wsl_audio() -> dict:
|
|
|
94
97
|
try:
|
|
95
98
|
env = os.environ.copy()
|
|
96
99
|
env["PULSE_SERVER"] = "unix:/mnt/wslg/PulseServer"
|
|
97
|
-
proc =
|
|
100
|
+
proc = _subprocess.run(
|
|
98
101
|
["pactl", "info"],
|
|
99
102
|
capture_output=True,
|
|
100
103
|
timeout=5,
|
|
@@ -126,10 +129,14 @@ def check_wsl_audio() -> dict:
|
|
|
126
129
|
# Keep legacy TTS import for backwards compatibility
|
|
127
130
|
from ._tts import TTS
|
|
128
131
|
|
|
132
|
+
# Import audio availability check
|
|
133
|
+
from ._audio_check import check_local_audio_available
|
|
134
|
+
|
|
129
135
|
__all__ = [
|
|
130
136
|
"speak",
|
|
131
137
|
"stop_speech",
|
|
132
138
|
"check_wsl_audio",
|
|
139
|
+
"check_local_audio_available",
|
|
133
140
|
"TTS",
|
|
134
141
|
"GoogleTTS",
|
|
135
142
|
"ElevenLabsTTS",
|
|
@@ -144,7 +151,7 @@ __all__ = [
|
|
|
144
151
|
FALLBACK_ORDER = ["elevenlabs", "gtts", "pyttsx3"]
|
|
145
152
|
|
|
146
153
|
|
|
147
|
-
def available_backends() ->
|
|
154
|
+
def available_backends() -> list[str]:
|
|
148
155
|
"""Return list of available TTS backends in fallback order."""
|
|
149
156
|
backends = []
|
|
150
157
|
|
|
@@ -177,7 +184,7 @@ def available_backends() -> List[str]:
|
|
|
177
184
|
return backends
|
|
178
185
|
|
|
179
186
|
|
|
180
|
-
def get_tts(backend:
|
|
187
|
+
def get_tts(backend: str | None = None, **kwargs) -> _BaseTTS:
|
|
181
188
|
"""Get a TTS instance for the specified backend.
|
|
182
189
|
|
|
183
190
|
Args:
|
|
@@ -219,235 +226,8 @@ def get_tts(backend: Optional[str] = None, **kwargs) -> _BaseTTS:
|
|
|
219
226
|
raise ValueError(f"Backend '{backend}' not available. Available: {backends}")
|
|
220
227
|
|
|
221
228
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
voice: Optional[str] = None,
|
|
225
|
-
play: bool = True,
|
|
226
|
-
output_path: Optional[str] = None,
|
|
227
|
-
**kwargs,
|
|
228
|
-
) -> tuple:
|
|
229
|
-
"""Try to speak with fallback through backends.
|
|
230
|
-
|
|
231
|
-
Returns:
|
|
232
|
-
(result_dict, backend_used, error_log)
|
|
233
|
-
result_dict has keys: path, played, success, play_requested
|
|
234
|
-
"""
|
|
235
|
-
backends = available_backends()
|
|
236
|
-
errors = []
|
|
237
|
-
|
|
238
|
-
for backend in FALLBACK_ORDER:
|
|
239
|
-
if backend not in backends:
|
|
240
|
-
continue
|
|
241
|
-
|
|
242
|
-
try:
|
|
243
|
-
tts = get_tts(backend, **kwargs)
|
|
244
|
-
result = tts.speak(
|
|
245
|
-
text=text,
|
|
246
|
-
voice=voice,
|
|
247
|
-
play=play,
|
|
248
|
-
output_path=output_path,
|
|
249
|
-
)
|
|
250
|
-
# result is now a dict with: path, played, success, play_requested
|
|
251
|
-
result["backend"] = backend
|
|
252
|
-
return (result, backend, errors)
|
|
253
|
-
except Exception as e:
|
|
254
|
-
errors.append(f"{backend}: {str(e)}")
|
|
255
|
-
continue
|
|
256
|
-
|
|
257
|
-
return (None, None, errors)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
# Cache for default TTS instance
|
|
261
|
-
_default_tts: Optional[_BaseTTS] = None
|
|
262
|
-
_default_backend: Optional[str] = None
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def _speak_local(
|
|
266
|
-
text: str,
|
|
267
|
-
backend: Optional[str] = None,
|
|
268
|
-
voice: Optional[str] = None,
|
|
269
|
-
play: bool = True,
|
|
270
|
-
output_path: Optional[str] = None,
|
|
271
|
-
fallback: bool = True,
|
|
272
|
-
**kwargs,
|
|
273
|
-
) -> dict:
|
|
274
|
-
"""Local TTS playback (original implementation).
|
|
275
|
-
|
|
276
|
-
Returns:
|
|
277
|
-
Dict with keys: success, played, play_requested, backend, path (optional).
|
|
278
|
-
"""
|
|
279
|
-
global _default_tts, _default_backend
|
|
280
|
-
|
|
281
|
-
# If specific backend requested without fallback
|
|
282
|
-
if backend and not fallback:
|
|
283
|
-
tts = get_tts(backend, **kwargs)
|
|
284
|
-
result = tts.speak(text=text, voice=voice, play=play, output_path=output_path)
|
|
285
|
-
result["backend"] = backend
|
|
286
|
-
return result
|
|
287
|
-
|
|
288
|
-
# Use fallback logic
|
|
289
|
-
if fallback and backend is None:
|
|
290
|
-
result, used_backend, errors = _try_speak_with_fallback(
|
|
291
|
-
text=text, voice=voice, play=play, output_path=output_path, **kwargs
|
|
292
|
-
)
|
|
293
|
-
if result is None and errors:
|
|
294
|
-
raise RuntimeError("All TTS backends failed:\n" + "\n".join(errors))
|
|
295
|
-
return (
|
|
296
|
-
result if result else {"success": False, "played": False, "errors": errors}
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
# Specific backend with fallback enabled
|
|
300
|
-
try:
|
|
301
|
-
tts = get_tts(backend, **kwargs)
|
|
302
|
-
result = tts.speak(text=text, voice=voice, play=play, output_path=output_path)
|
|
303
|
-
result["backend"] = backend
|
|
304
|
-
return result
|
|
305
|
-
except Exception as e:
|
|
306
|
-
if fallback:
|
|
307
|
-
result, used_backend, errors = _try_speak_with_fallback(
|
|
308
|
-
text=text, voice=voice, play=play, output_path=output_path, **kwargs
|
|
309
|
-
)
|
|
310
|
-
if result is None:
|
|
311
|
-
raise RuntimeError(
|
|
312
|
-
f"Primary backend '{backend}' failed: {e}\n"
|
|
313
|
-
f"Fallback errors:\n" + "\n".join(errors)
|
|
314
|
-
)
|
|
315
|
-
return result
|
|
316
|
-
raise
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def speak(
|
|
320
|
-
text: str,
|
|
321
|
-
backend: Optional[str] = None,
|
|
322
|
-
voice: Optional[str] = None,
|
|
323
|
-
play: bool = True,
|
|
324
|
-
output_path: Optional[str] = None,
|
|
325
|
-
fallback: bool = True,
|
|
326
|
-
rate: Optional[int] = None,
|
|
327
|
-
speed: Optional[float] = None,
|
|
328
|
-
mode: Optional[str] = None,
|
|
329
|
-
**kwargs,
|
|
330
|
-
) -> dict:
|
|
331
|
-
"""Convert text to speech with smart local/remote switching.
|
|
332
|
-
|
|
333
|
-
Modes:
|
|
334
|
-
- local: Always use local TTS backends
|
|
335
|
-
- remote: Always forward to relay server
|
|
336
|
-
- auto: Try remote first, fall back to local (default)
|
|
337
|
-
|
|
338
|
-
Fallback order (local): elevenlabs -> gtts -> pyttsx3
|
|
339
|
-
|
|
340
|
-
Args:
|
|
341
|
-
text: Text to speak.
|
|
342
|
-
backend: TTS backend ('pyttsx3', 'gtts', 'elevenlabs').
|
|
343
|
-
Auto-selects with fallback if None.
|
|
344
|
-
voice: Voice name, ID, or language code.
|
|
345
|
-
play: Whether to play the audio.
|
|
346
|
-
output_path: Path to save audio file.
|
|
347
|
-
fallback: If True, try next backend on failure.
|
|
348
|
-
rate: Speech rate in words per minute (pyttsx3 only, default 150).
|
|
349
|
-
speed: Speed multiplier for gtts (1.0=normal, >1.0=faster, <1.0=slower).
|
|
350
|
-
mode: Override mode ('local', 'remote', 'auto'). Uses env if None.
|
|
351
|
-
**kwargs: Additional backend options.
|
|
352
|
-
|
|
353
|
-
Returns:
|
|
354
|
-
Dict with: success, played, play_requested, backend, path (if saved), mode.
|
|
355
|
-
|
|
356
|
-
Environment Variables:
|
|
357
|
-
SCITEX_AUDIO_MODE: Default mode ('local', 'remote', 'auto')
|
|
358
|
-
SCITEX_AUDIO_RELAY_URL: Relay server URL for remote mode
|
|
359
|
-
|
|
360
|
-
Examples:
|
|
361
|
-
import scitex
|
|
362
|
-
|
|
363
|
-
# Simple (auto mode - tries remote, falls back to local)
|
|
364
|
-
result = scitex.audio.speak("Hello!")
|
|
365
|
-
if result["played"]:
|
|
366
|
-
print("Audio played successfully")
|
|
367
|
-
|
|
368
|
-
# Force local playback
|
|
369
|
-
scitex.audio.speak("Hello", mode="local")
|
|
370
|
-
|
|
371
|
-
# Force remote relay
|
|
372
|
-
scitex.audio.speak("Hello", mode="remote")
|
|
373
|
-
|
|
374
|
-
# Faster speech (gtts with pydub)
|
|
375
|
-
scitex.audio.speak("Hello", backend="gtts", speed=1.5)
|
|
376
|
-
"""
|
|
377
|
-
from ._branding import get_mode, get_relay_url
|
|
378
|
-
from ._relay import is_relay_available, relay_speak
|
|
379
|
-
|
|
380
|
-
# Pass rate/speed to kwargs
|
|
381
|
-
if rate is not None:
|
|
382
|
-
kwargs["rate"] = rate
|
|
383
|
-
if speed is not None:
|
|
384
|
-
kwargs["speed"] = speed
|
|
385
|
-
|
|
386
|
-
# Determine mode
|
|
387
|
-
effective_mode = mode or get_mode()
|
|
388
|
-
|
|
389
|
-
# Remote mode: always use relay
|
|
390
|
-
if effective_mode == "remote":
|
|
391
|
-
relay_url = get_relay_url()
|
|
392
|
-
if not relay_url:
|
|
393
|
-
raise RuntimeError(
|
|
394
|
-
"Remote mode requires SCITEX_AUDIO_RELAY_URL or "
|
|
395
|
-
"SCITEX_AUDIO_RELAY_HOST to be set"
|
|
396
|
-
)
|
|
397
|
-
result = relay_speak(
|
|
398
|
-
text=text,
|
|
399
|
-
backend=backend,
|
|
400
|
-
voice=voice,
|
|
401
|
-
rate=rate or 150,
|
|
402
|
-
speed=speed or 1.5,
|
|
403
|
-
play=play,
|
|
404
|
-
**kwargs,
|
|
405
|
-
)
|
|
406
|
-
return {
|
|
407
|
-
"success": result.get("success", False),
|
|
408
|
-
"played": result.get("success", False) and play,
|
|
409
|
-
"play_requested": play,
|
|
410
|
-
"mode": "remote",
|
|
411
|
-
"path": result.get("saved_to"),
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
# Auto mode: try remote first, fall back to local
|
|
415
|
-
if effective_mode == "auto":
|
|
416
|
-
relay_url = get_relay_url()
|
|
417
|
-
if relay_url and is_relay_available():
|
|
418
|
-
try:
|
|
419
|
-
result = relay_speak(
|
|
420
|
-
text=text,
|
|
421
|
-
backend=backend,
|
|
422
|
-
voice=voice,
|
|
423
|
-
rate=rate or 150,
|
|
424
|
-
speed=speed or 1.5,
|
|
425
|
-
play=play,
|
|
426
|
-
**kwargs,
|
|
427
|
-
)
|
|
428
|
-
if result.get("success"):
|
|
429
|
-
return {
|
|
430
|
-
"success": True,
|
|
431
|
-
"played": play,
|
|
432
|
-
"play_requested": play,
|
|
433
|
-
"mode": "remote",
|
|
434
|
-
"path": result.get("saved_to"),
|
|
435
|
-
}
|
|
436
|
-
except Exception:
|
|
437
|
-
pass # Fall through to local
|
|
438
|
-
|
|
439
|
-
# Local mode (or fallback from auto)
|
|
440
|
-
result = _speak_local(
|
|
441
|
-
text=text,
|
|
442
|
-
backend=backend,
|
|
443
|
-
voice=voice,
|
|
444
|
-
play=play,
|
|
445
|
-
output_path=output_path,
|
|
446
|
-
fallback=fallback,
|
|
447
|
-
**kwargs,
|
|
448
|
-
)
|
|
449
|
-
result["mode"] = "local"
|
|
450
|
-
return result
|
|
229
|
+
# Import speak function from refactored module
|
|
230
|
+
from ._speak import speak
|
|
451
231
|
|
|
452
232
|
|
|
453
233
|
def start_mcp_server():
|
|
@@ -458,4 +238,16 @@ def start_mcp_server():
|
|
|
458
238
|
main()
|
|
459
239
|
|
|
460
240
|
|
|
241
|
+
# Clean up internal imports from public namespace
|
|
242
|
+
def _cleanup_namespace():
|
|
243
|
+
import sys
|
|
244
|
+
_module = sys.modules[__name__]
|
|
245
|
+
for _name in ["annotations", "engines"]:
|
|
246
|
+
if hasattr(_module, _name):
|
|
247
|
+
delattr(_module, _name)
|
|
248
|
+
|
|
249
|
+
_cleanup_namespace()
|
|
250
|
+
del _cleanup_namespace
|
|
251
|
+
|
|
252
|
+
|
|
461
253
|
# EOF
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Audio availability checking utilities.
|
|
3
|
+
|
|
4
|
+
Provides functions to check if local audio playback is available
|
|
5
|
+
before attempting to play audio.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
__all__ = ["check_local_audio_available"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_local_audio_available() -> dict:
|
|
16
|
+
"""Check if local audio playback is available.
|
|
17
|
+
|
|
18
|
+
Checks PulseAudio sink state to determine if audio can actually be heard.
|
|
19
|
+
On NAS or headless servers, the sink is typically SUSPENDED.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
dict with keys:
|
|
23
|
+
- available: bool - True if local audio output is likely to work
|
|
24
|
+
- state: str - 'RUNNING', 'IDLE', 'SUSPENDED', 'NO_SINK', etc.
|
|
25
|
+
- reason: str - Human-readable explanation
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
["pactl", "list", "sinks", "short"],
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
timeout=5,
|
|
33
|
+
)
|
|
34
|
+
if result.returncode != 0:
|
|
35
|
+
return {
|
|
36
|
+
"available": False,
|
|
37
|
+
"state": "NO_PACTL",
|
|
38
|
+
"reason": "PulseAudio not available",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if not result.stdout.strip():
|
|
42
|
+
return {
|
|
43
|
+
"available": False,
|
|
44
|
+
"state": "NO_SINK",
|
|
45
|
+
"reason": "No audio sinks found",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Parse sink state (format: id\tname\tmodule\tformat\tstate)
|
|
49
|
+
for line in result.stdout.strip().split("\n"):
|
|
50
|
+
parts = line.split("\t")
|
|
51
|
+
if len(parts) >= 5:
|
|
52
|
+
state = parts[4]
|
|
53
|
+
if state == "SUSPENDED":
|
|
54
|
+
return {
|
|
55
|
+
"available": False,
|
|
56
|
+
"state": "SUSPENDED",
|
|
57
|
+
"reason": "Audio sink SUSPENDED (no active output device)",
|
|
58
|
+
}
|
|
59
|
+
elif state in ("RUNNING", "IDLE"):
|
|
60
|
+
return {
|
|
61
|
+
"available": True,
|
|
62
|
+
"state": state,
|
|
63
|
+
"reason": f"Audio sink is {state}",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"available": False,
|
|
68
|
+
"state": "UNKNOWN",
|
|
69
|
+
"reason": "Could not determine sink state",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
# No pactl - might be macOS or minimal system, assume available
|
|
74
|
+
return {
|
|
75
|
+
"available": True,
|
|
76
|
+
"state": "NO_PACTL",
|
|
77
|
+
"reason": "pactl not found, assuming audio available",
|
|
78
|
+
}
|
|
79
|
+
except subprocess.TimeoutExpired:
|
|
80
|
+
return {
|
|
81
|
+
"available": False,
|
|
82
|
+
"state": "TIMEOUT",
|
|
83
|
+
"reason": "PulseAudio query timed out",
|
|
84
|
+
}
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return {
|
|
87
|
+
"available": False,
|
|
88
|
+
"state": "ERROR",
|
|
89
|
+
"reason": str(e),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# EOF
|