scitex 2.14.0__py3-none-any.whl → 2.15.3__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 (264) 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 +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  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 +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  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} +48 -56
  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/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
scitex/audio/__init__.py CHANGED
@@ -6,12 +6,12 @@
6
6
  """
7
7
  SciTeX Audio Module - Text-to-Speech with Multiple Backends
8
8
 
9
- Fallback order: pyttsx3 -> gtts -> elevenlabs
9
+ Fallback order: elevenlabs -> gtts -> pyttsx3
10
10
 
11
11
  Backends:
12
- - pyttsx3: System TTS (offline, free, uses espeak/SAPI5)
13
- - gtts: Google TTS (free, requires internet)
14
12
  - elevenlabs: ElevenLabs (paid, high quality)
13
+ - gtts: Google TTS (free, requires internet)
14
+ - pyttsx3: System TTS (offline, free, uses espeak/SAPI5)
15
15
 
16
16
  Usage:
17
17
  import scitex
@@ -145,8 +145,8 @@ __all__ = [
145
145
  "FALLBACK_ORDER",
146
146
  ]
147
147
 
148
- # Fallback order: pyttsx3 (offline/free) -> gtts (free) -> elevenlabs (paid)
149
- FALLBACK_ORDER = ["pyttsx3", "gtts", "elevenlabs"]
148
+ # Fallback order: elevenlabs (best quality) -> gtts (free) -> pyttsx3 (offline)
149
+ FALLBACK_ORDER = ["elevenlabs", "gtts", "pyttsx3"]
150
150
 
151
151
 
152
152
  def available_backends() -> List[str]:
@@ -173,8 +173,8 @@ def available_backends() -> List[str]:
173
173
  if ElevenLabsTTS:
174
174
  import os
175
175
 
176
- api_key = os.environ.get("ELEVENLABS_API_KEY") or os.environ.get(
177
- "ELEVENLABS_API_KEY_SCITEX_AUDIO"
176
+ api_key = os.environ.get("SCITEX_AUDIO_ELEVENLABS_API_KEY") or os.environ.get(
177
+ "ELEVENLABS_API_KEY"
178
178
  )
179
179
  if api_key:
180
180
  backends.append("elevenlabs")
@@ -264,69 +264,20 @@ _default_tts: Optional[BaseTTS] = None
264
264
  _default_backend: Optional[str] = None
265
265
 
266
266
 
267
- def speak(
267
+ def _speak_local(
268
268
  text: str,
269
269
  backend: Optional[str] = None,
270
270
  voice: Optional[str] = None,
271
271
  play: bool = True,
272
272
  output_path: Optional[str] = None,
273
273
  fallback: bool = True,
274
- rate: Optional[int] = None,
275
- speed: Optional[float] = None,
276
274
  **kwargs,
277
275
  ) -> Optional[str]:
278
- """Convert text to speech with automatic fallback.
279
-
280
- Fallback order: pyttsx3 -> gtts -> elevenlabs
281
-
282
- Args:
283
- text: Text to speak.
284
- backend: TTS backend ('pyttsx3', 'gtts', 'elevenlabs').
285
- Auto-selects with fallback if None.
286
- voice: Voice name, ID, or language code.
287
- play: Whether to play the audio.
288
- output_path: Path to save audio file.
289
- fallback: If True, try next backend on failure.
290
- rate: Speech rate in words per minute (pyttsx3 only, default 150).
291
- speed: Speed multiplier for gtts (1.0=normal, >1.0=faster, <1.0=slower).
292
- **kwargs: Additional backend options.
293
-
294
- Returns:
295
- Path to audio file if output_path specified, else None.
296
-
297
- Examples:
298
- import scitex
299
-
300
- # Simple (auto-selects with fallback)
301
- scitex.audio.speak("Hello!")
302
-
303
- # Faster speech (pyttsx3)
304
- scitex.audio.speak("Hello", rate=200)
305
-
306
- # Faster speech (gtts with pydub)
307
- scitex.audio.speak("Hello", backend="gtts", speed=1.5)
308
-
309
- # Specific backend (no fallback)
310
- scitex.audio.speak("Hello", backend="pyttsx3", fallback=False)
311
-
312
- # Different language (gTTS)
313
- scitex.audio.speak("Bonjour", backend="gtts", voice="fr")
314
-
315
- # Save to file
316
- scitex.audio.speak("Test", output_path="/tmp/test.mp3")
317
- """
276
+ """Local TTS playback (original implementation)."""
318
277
  global _default_tts, _default_backend
319
278
 
320
- # Stop any previously running speech first
321
- stop_speech()
322
-
323
- # Pass rate to kwargs for pyttsx3
324
- if rate is not None:
325
- kwargs["rate"] = rate
326
-
327
- # Pass speed to kwargs for gtts
328
- if speed is not None:
329
- kwargs["speed"] = speed
279
+ # Note: stop_speech() removed - FIFO locking handles queuing
280
+ # Call stop_speech() explicitly if you want to interrupt current audio
330
281
 
331
282
  # If specific backend requested without fallback
332
283
  if backend and not fallback:
@@ -364,7 +315,6 @@ def speak(
364
315
  return str(result) if result else None
365
316
  except Exception as e:
366
317
  if fallback:
367
- # Try other backends
368
318
  result, used_backend, errors = _try_speak_with_fallback(
369
319
  text=text,
370
320
  voice=voice,
@@ -381,6 +331,124 @@ def speak(
381
331
  raise
382
332
 
383
333
 
334
+ def speak(
335
+ text: str,
336
+ backend: Optional[str] = None,
337
+ voice: Optional[str] = None,
338
+ play: bool = True,
339
+ output_path: Optional[str] = None,
340
+ fallback: bool = True,
341
+ rate: Optional[int] = None,
342
+ speed: Optional[float] = None,
343
+ mode: Optional[str] = None,
344
+ **kwargs,
345
+ ) -> Optional[str]:
346
+ """Convert text to speech with smart local/remote switching.
347
+
348
+ Modes:
349
+ - local: Always use local TTS backends
350
+ - remote: Always forward to relay server
351
+ - auto: Try remote first, fall back to local (default)
352
+
353
+ Fallback order (local): elevenlabs -> gtts -> pyttsx3
354
+
355
+ Args:
356
+ text: Text to speak.
357
+ backend: TTS backend ('pyttsx3', 'gtts', 'elevenlabs').
358
+ Auto-selects with fallback if None.
359
+ voice: Voice name, ID, or language code.
360
+ play: Whether to play the audio.
361
+ output_path: Path to save audio file.
362
+ fallback: If True, try next backend on failure.
363
+ rate: Speech rate in words per minute (pyttsx3 only, default 150).
364
+ speed: Speed multiplier for gtts (1.0=normal, >1.0=faster, <1.0=slower).
365
+ mode: Override mode ('local', 'remote', 'auto'). Uses env if None.
366
+ **kwargs: Additional backend options.
367
+
368
+ Returns:
369
+ Path to audio file if output_path specified, else None.
370
+
371
+ Environment Variables:
372
+ SCITEX_AUDIO_MODE: Default mode ('local', 'remote', 'auto')
373
+ SCITEX_AUDIO_RELAY_URL: Relay server URL for remote mode
374
+
375
+ Examples:
376
+ import scitex
377
+
378
+ # Simple (auto mode - tries remote, falls back to local)
379
+ scitex.audio.speak("Hello!")
380
+
381
+ # Force local playback
382
+ scitex.audio.speak("Hello", mode="local")
383
+
384
+ # Force remote relay
385
+ scitex.audio.speak("Hello", mode="remote")
386
+
387
+ # Faster speech (gtts with pydub)
388
+ scitex.audio.speak("Hello", backend="gtts", speed=1.5)
389
+ """
390
+ from ._branding import get_mode, get_relay_url
391
+ from ._relay import is_relay_available, relay_speak
392
+
393
+ # Pass rate/speed to kwargs
394
+ if rate is not None:
395
+ kwargs["rate"] = rate
396
+ if speed is not None:
397
+ kwargs["speed"] = speed
398
+
399
+ # Determine mode
400
+ effective_mode = mode or get_mode()
401
+
402
+ # Remote mode: always use relay
403
+ if effective_mode == "remote":
404
+ relay_url = get_relay_url()
405
+ if not relay_url:
406
+ raise RuntimeError(
407
+ "Remote mode requires SCITEX_AUDIO_RELAY_URL or "
408
+ "SCITEX_AUDIO_RELAY_HOST to be set"
409
+ )
410
+ result = relay_speak(
411
+ text=text,
412
+ backend=backend,
413
+ voice=voice,
414
+ rate=rate or 150,
415
+ speed=speed or 1.5,
416
+ play=play,
417
+ **kwargs,
418
+ )
419
+ return result.get("saved_to") if result.get("success") else None
420
+
421
+ # Auto mode: try remote first, fall back to local
422
+ if effective_mode == "auto":
423
+ relay_url = get_relay_url()
424
+ if relay_url and is_relay_available():
425
+ try:
426
+ result = relay_speak(
427
+ text=text,
428
+ backend=backend,
429
+ voice=voice,
430
+ rate=rate or 150,
431
+ speed=speed or 1.5,
432
+ play=play,
433
+ **kwargs,
434
+ )
435
+ if result.get("success"):
436
+ return result.get("saved_to")
437
+ except Exception:
438
+ pass # Fall through to local
439
+
440
+ # Local mode (or fallback from auto)
441
+ return _speak_local(
442
+ text=text,
443
+ backend=backend,
444
+ voice=voice,
445
+ play=play,
446
+ output_path=output_path,
447
+ fallback=fallback,
448
+ **kwargs,
449
+ )
450
+
451
+
384
452
  def start_mcp_server():
385
453
  """Start the MCP server for audio."""
386
454
  from .mcp_server import main
@@ -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
+ ]
@@ -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
+
@@ -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 ["gtts", "elevenlabs", "pyttsx3"]:
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": backends[0] if backends else None,
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=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