scitex 2.14.0__py3-none-any.whl → 2.15.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. scitex/__init__.py +71 -17
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +210 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +21 -204
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +76 -27
  68. scitex/cli/capture.py +13 -20
  69. scitex/cli/introspect.py +481 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +357 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +23 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +314 -0
  79. scitex/cli/stats.py +15 -8
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +132 -8
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  178. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  179. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  180. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  181. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  182. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  183. scitex/security/README.md +3 -3
  184. scitex/session/README.md +1 -1
  185. scitex/session/__init__.py +26 -7
  186. scitex/session/_decorator.py +1 -1
  187. scitex/sh/README.md +1 -1
  188. scitex/sh/__init__.py +7 -4
  189. scitex/social/__init__.py +155 -0
  190. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  191. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  192. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  193. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  194. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  195. scitex/stats/_mcp/_handlers/_format.py +94 -0
  196. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  197. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  198. scitex/stats/_mcp/_handlers/_power.py +247 -0
  199. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  200. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  201. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  202. scitex/stats/_mcp/handlers.py +19 -1171
  203. scitex/stats/auto/_stat_style.py +175 -0
  204. scitex/stats/auto/_style_definitions.py +411 -0
  205. scitex/stats/auto/_styles.py +22 -620
  206. scitex/stats/descriptive/__init__.py +11 -8
  207. scitex/stats/descriptive/_ci.py +39 -0
  208. scitex/stats/power/_power.py +15 -4
  209. scitex/str/__init__.py +2 -1
  210. scitex/str/_title_case.py +63 -0
  211. scitex/template/README.md +1 -1
  212. scitex/template/__init__.py +25 -10
  213. scitex/template/_code_templates.py +147 -0
  214. scitex/template/_mcp/handlers.py +81 -0
  215. scitex/template/_mcp/tool_schemas.py +55 -0
  216. scitex/template/_templates/__init__.py +51 -0
  217. scitex/template/_templates/audio.py +233 -0
  218. scitex/template/_templates/canvas.py +312 -0
  219. scitex/template/_templates/capture.py +268 -0
  220. scitex/template/_templates/config.py +43 -0
  221. scitex/template/_templates/diagram.py +294 -0
  222. scitex/template/_templates/io.py +107 -0
  223. scitex/template/_templates/module.py +53 -0
  224. scitex/template/_templates/plt.py +202 -0
  225. scitex/template/_templates/scholar.py +267 -0
  226. scitex/template/_templates/session.py +130 -0
  227. scitex/template/_templates/session_minimal.py +43 -0
  228. scitex/template/_templates/session_plot.py +67 -0
  229. scitex/template/_templates/session_stats.py +77 -0
  230. scitex/template/_templates/stats.py +323 -0
  231. scitex/template/_templates/writer.py +296 -0
  232. scitex/template/clone_writer_directory.py +5 -5
  233. scitex/ui/_backends/_email.py +10 -2
  234. scitex/ui/_backends/_webhook.py +5 -1
  235. scitex/web/_search_pubmed.py +10 -6
  236. scitex/writer/README.md +1 -1
  237. scitex/writer/_mcp/handlers.py +11 -744
  238. scitex/writer/_mcp/tool_schemas.py +5 -335
  239. scitex-2.15.2.dist-info/METADATA +648 -0
  240. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
  241. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  242. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  243. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  244. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  245. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  246. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  247. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  248. scitex/diagram/_compile.py +0 -312
  249. scitex/diagram/_diagram.py +0 -355
  250. scitex/diagram/_mcp/__init__.py +0 -4
  251. scitex/diagram/_mcp/handlers.py +0 -400
  252. scitex/diagram/_mcp/tool_schemas.py +0 -157
  253. scitex/diagram/_presets.py +0 -173
  254. scitex/diagram/_schema.py +0 -182
  255. scitex/diagram/_split.py +0 -278
  256. scitex/gen/_ci.py +0 -12
  257. scitex/gen/_title_case.py +0 -89
  258. scitex/plt/_mcp/__init__.py +0 -4
  259. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  260. scitex/plt/_mcp/_handlers_figure.py +0 -195
  261. scitex/plt/_mcp/_handlers_plot.py +0 -252
  262. scitex/plt/_mcp/_handlers_style.py +0 -219
  263. scitex/plt/_mcp/handlers.py +0 -74
  264. scitex/plt/_mcp/tool_schemas.py +0 -497
  265. scitex/plt/mcp_server.py +0 -231
  266. scitex/scholar/data/.gitkeep +0 -0
  267. scitex/scholar/data/README.md +0 -44
  268. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  269. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  270. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  271. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  272. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  273. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  274. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  275. scitex/scholar/data/bib_files/pac.bib +0 -698
  276. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  277. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  278. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  279. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  280. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  281. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  282. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  283. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  284. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  285. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  286. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  287. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  288. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  289. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  290. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  291. scitex/scholar/data/impact_factor.db +0 -0
  292. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  293. scitex/scholar/examples/dev.py +0 -38
  294. scitex-2.14.0.dist-info/METADATA +0 -1238
  295. /scitex/{gen → context}/_detect_environment.py +0 -0
  296. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  297. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  298. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
  299. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
  300. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
@@ -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
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
+ ]
scitex/audio/_tts.py CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  # Timestamp: "2025-12-11 (ywatanabe)"
4
3
  # File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/_tts.py
5
4
  # ----------------------------------------
@@ -20,7 +19,7 @@ from __future__ import annotations
20
19
  import os
21
20
  import subprocess
22
21
  import tempfile
23
- from dataclasses import dataclass, field
22
+ from dataclasses import dataclass
24
23
  from pathlib import Path
25
24
  from typing import Optional
26
25
 
@@ -44,7 +43,8 @@ class TTSConfig:
44
43
  class TTS:
45
44
  """Text-to-Speech using ElevenLabs API.
46
45
 
47
- Examples:
46
+ Examples
47
+ --------
48
48
  # Basic usage
49
49
  tts = TTS()
50
50
  tts.speak("Hello, world!")
@@ -84,7 +84,11 @@ class TTS:
84
84
  voice_id: Direct voice ID (overrides voice_name).
85
85
  **kwargs: Additional config options (stability, speed, etc.)
86
86
  """
87
- self.api_key = api_key or os.environ.get("ELEVENLABS_API_KEY")
87
+ self.api_key = (
88
+ api_key
89
+ or os.environ.get("SCITEX_AUDIO_ELEVENLABS_API_KEY")
90
+ or os.environ.get("ELEVENLABS_API_KEY")
91
+ )
88
92
  self.config = TTSConfig(**kwargs)
89
93
 
90
94
  if voice_id:
@@ -129,7 +133,8 @@ class TTS:
129
133
  voice_name: Override voice name for this call.
130
134
  voice_id: Override voice ID for this call.
131
135
 
132
- Returns:
136
+ Returns
137
+ -------
133
138
  Path to the generated audio file, or None if only played.
134
139
  """
135
140
  # Determine voice
@@ -228,14 +233,15 @@ class TTS:
228
233
  try:
229
234
  # SoundPlayer only supports WAV, so convert if needed
230
235
  wav_path = path
231
- if path.suffix.lower() in ('.mp3', '.ogg', '.m4a'):
236
+ if path.suffix.lower() in (".mp3", ".ogg", ".m4a"):
232
237
  try:
233
238
  from pydub import AudioSegment
234
- fd, tmp_wav = tempfile.mkstemp(suffix='.wav', prefix='scitex_')
239
+
240
+ fd, tmp_wav = tempfile.mkstemp(suffix=".wav", prefix="scitex_")
235
241
  os.close(fd)
236
242
  wav_path = Path(tmp_wav)
237
243
  audio = AudioSegment.from_file(str(path))
238
- audio.export(str(wav_path), format='wav')
244
+ audio.export(str(wav_path), format="wav")
239
245
  except ImportError:
240
246
  pass
241
247
 
@@ -303,10 +309,12 @@ def speak(
303
309
  output_path: Optional path to save audio.
304
310
  **kwargs: Additional TTS config options.
305
311
 
306
- Returns:
312
+ Returns
313
+ -------
307
314
  Path to audio file if output_path specified, else None.
308
315
 
309
- Examples:
316
+ Examples
317
+ --------
310
318
  import scitex
311
319
 
312
320
  # Simple speak
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  # Timestamp: "2025-12-11 (ywatanabe)"
4
3
  # File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/engines/base.py
5
4
  # ----------------------------------------
@@ -49,10 +48,13 @@ class TTSBackend:
49
48
 
50
49
  # Check ElevenLabs
51
50
  try:
52
- import elevenlabs
53
51
  import os
54
52
 
55
- if os.environ.get("ELEVENLABS_API_KEY"):
53
+ import elevenlabs
54
+
55
+ if os.environ.get("SCITEX_AUDIO_ELEVENLABS_API_KEY") or os.environ.get(
56
+ "ELEVENLABS_API_KEY"
57
+ ):
56
58
  backends.append(cls.ELEVENLABS)
57
59
  except ImportError:
58
60
  pass
@@ -74,7 +76,8 @@ class BaseTTS(ABC):
74
76
  text: Text to convert to speech.
75
77
  output_path: Path to save the audio file.
76
78
 
77
- Returns:
79
+ Returns
80
+ -------
78
81
  Path to the generated audio file.
79
82
  """
80
83
  pass
@@ -83,7 +86,8 @@ class BaseTTS(ABC):
83
86
  def get_voices(self) -> List[dict]:
84
87
  """Get available voices for this backend.
85
88
 
86
- Returns:
89
+ Returns
90
+ -------
87
91
  List of voice dictionaries with 'name' and 'id' keys.
88
92
  """
89
93
  pass
@@ -119,7 +123,8 @@ class BaseTTS(ABC):
119
123
  play: Whether to play the audio.
120
124
  voice: Optional voice name/id.
121
125
 
122
- Returns:
126
+ Returns
127
+ -------
123
128
  Path to audio file if output_path specified, else None.
124
129
  """
125
130
  import tempfile
@@ -201,7 +206,8 @@ class BaseTTS(ABC):
201
206
  Args:
202
207
  path: Path to audio file (in WSL filesystem)
203
208
 
204
- Returns:
209
+ Returns
210
+ -------
205
211
  True if playback succeeded, False otherwise
206
212
  """
207
213
  import os
@@ -220,16 +226,17 @@ class BaseTTS(ABC):
220
226
  try:
221
227
  # SoundPlayer only supports WAV, so convert if needed
222
228
  wav_path = path
223
- if path.suffix.lower() in ('.mp3', '.ogg', '.m4a'):
229
+ if path.suffix.lower() in (".mp3", ".ogg", ".m4a"):
224
230
  try:
225
231
  from pydub import AudioSegment
232
+
226
233
  # Create temp WAV file
227
- fd, tmp_wav = tempfile.mkstemp(suffix='.wav', prefix='scitex_')
234
+ fd, tmp_wav = tempfile.mkstemp(suffix=".wav", prefix="scitex_")
228
235
  os.close(fd)
229
236
  wav_path = Path(tmp_wav)
230
237
 
231
238
  audio = AudioSegment.from_file(str(path))
232
- audio.export(str(wav_path), format='wav')
239
+ audio.export(str(wav_path), format="wav")
233
240
  except ImportError:
234
241
  # pydub not available, try direct playback anyway
235
242
  pass
@@ -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,
@@ -51,14 +55,15 @@ class ElevenLabsTTS(BaseTTS):
51
55
  super().__init__(**kwargs)
52
56
  self.api_key = (
53
57
  api_key
58
+ or os.environ.get("SCITEX_AUDIO_ELEVENLABS_API_KEY")
54
59
  or os.environ.get("ELEVENLABS_API_KEY")
55
- or os.environ.get("ELEVENLABS_API_KEY_SCITEX_AUDIO")
56
60
  )
57
61
  self.voice = voice
58
62
  self.model_id = model_id
59
63
  self.stability = stability
60
64
  self.similarity_boost = similarity_boost
61
- self.speed = speed
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