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.
- scitex/__init__.py +71 -17
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +210 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/template.py +24 -0
- scitex/_mcp_tools/writer.py +21 -204
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
- scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
- scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +129 -61
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/_tts.py +18 -10
- scitex/audio/engines/base.py +17 -10
- scitex/audio/engines/elevenlabs_engine.py +7 -2
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
- scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
- scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
- scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
- scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
- scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
- scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
- scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
- scitex/canvas/editor/flask_editor/_core.py +25 -1684
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +481 -0
- scitex/cli/main.py +200 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/plt.py +357 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +23 -8
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/stats.py +15 -8
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +132 -8
- scitex/cloud/__init__.py +41 -2
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +256 -0
- scitex/context/__init__.py +22 -0
- scitex/dev/__init__.py +20 -1
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/gen/__init__.py +50 -14
- scitex/gen/_list_packages.py +4 -4
- scitex/introspect/__init__.py +82 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +41 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
- scitex/introspect/_mcp/__init__.py +41 -0
- scitex/introspect/_mcp/handlers.py +233 -0
- scitex/introspect/_members.py +155 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/_save.py +1 -2
- scitex/io/bundle/README.md +1 -1
- scitex/logging/_formatters.py +19 -9
- scitex/mcp_server.py +98 -5
- scitex/os/__init__.py +4 -0
- scitex/{gen → os}/_check_host.py +4 -5
- scitex/plt/__init__.py +245 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/session/__init__.py +26 -7
- scitex/session/_decorator.py +1 -1
- scitex/sh/README.md +1 -1
- scitex/sh/__init__.py +7 -4
- scitex/social/__init__.py +155 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/stats/_mcp/_handlers/__init__.py +31 -0
- scitex/stats/_mcp/_handlers/_corrections.py +113 -0
- scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
- scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
- scitex/stats/_mcp/_handlers/_format.py +94 -0
- scitex/stats/_mcp/_handlers/_normality.py +110 -0
- scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
- scitex/stats/_mcp/_handlers/_power.py +247 -0
- scitex/stats/_mcp/_handlers/_recommend.py +102 -0
- scitex/stats/_mcp/_handlers/_run_test.py +279 -0
- scitex/stats/_mcp/_handlers/_stars.py +48 -0
- scitex/stats/_mcp/handlers.py +19 -1171
- scitex/stats/auto/_stat_style.py +175 -0
- scitex/stats/auto/_style_definitions.py +411 -0
- scitex/stats/auto/_styles.py +22 -620
- scitex/stats/descriptive/__init__.py +11 -8
- scitex/stats/descriptive/_ci.py +39 -0
- scitex/stats/power/_power.py +15 -4
- scitex/str/__init__.py +2 -1
- scitex/str/_title_case.py +63 -0
- scitex/template/README.md +1 -1
- scitex/template/__init__.py +25 -10
- scitex/template/_code_templates.py +147 -0
- scitex/template/_mcp/handlers.py +81 -0
- scitex/template/_mcp/tool_schemas.py +55 -0
- scitex/template/_templates/__init__.py +51 -0
- scitex/template/_templates/audio.py +233 -0
- scitex/template/_templates/canvas.py +312 -0
- scitex/template/_templates/capture.py +268 -0
- scitex/template/_templates/config.py +43 -0
- scitex/template/_templates/diagram.py +294 -0
- scitex/template/_templates/io.py +107 -0
- scitex/template/_templates/module.py +53 -0
- scitex/template/_templates/plt.py +202 -0
- scitex/template/_templates/scholar.py +267 -0
- scitex/template/_templates/session.py +130 -0
- scitex/template/_templates/session_minimal.py +43 -0
- scitex/template/_templates/session_plot.py +67 -0
- scitex/template/_templates/session_stats.py +77 -0
- scitex/template/_templates/stats.py +323 -0
- scitex/template/_templates/writer.py +296 -0
- scitex/template/clone_writer_directory.py +5 -5
- scitex/ui/_backends/_email.py +10 -2
- scitex/ui/_backends/_webhook.py +5 -1
- scitex/web/_search_pubmed.py +10 -6
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.2.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/gen/_ci.py +0 -12
- scitex/gen/_title_case.py +0 -89
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- /scitex/{gen → context}/_detect_environment.py +0 -0
- /scitex/{gen → context}/_get_notebook_path.py +0 -0
- /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.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:
|
|
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:
|
|
149
|
-
FALLBACK_ORDER = ["
|
|
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("
|
|
177
|
-
"
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
#
|
|
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
|
+
]
|
scitex/audio/_mcp/__init__.py
CHANGED
|
@@ -2,3 +2,35 @@
|
|
|
2
2
|
# File: __init__.py
|
|
3
3
|
"""MCP server components."""
|
|
4
4
|
|
|
5
|
+
from .handlers import (
|
|
6
|
+
announce_context_handler,
|
|
7
|
+
check_audio_status_handler,
|
|
8
|
+
clear_audio_cache_handler,
|
|
9
|
+
generate_audio_handler,
|
|
10
|
+
list_audio_files_handler,
|
|
11
|
+
list_backends_handler,
|
|
12
|
+
list_voices_handler,
|
|
13
|
+
play_audio_handler,
|
|
14
|
+
speak_handler,
|
|
15
|
+
speech_queue_status_handler,
|
|
16
|
+
)
|
|
17
|
+
from .speak_handlers import (
|
|
18
|
+
speak_local_handler,
|
|
19
|
+
speak_relay_handler,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"speak_handler",
|
|
24
|
+
"speak_local_handler",
|
|
25
|
+
"speak_relay_handler",
|
|
26
|
+
"generate_audio_handler",
|
|
27
|
+
"list_backends_handler",
|
|
28
|
+
"list_voices_handler",
|
|
29
|
+
"play_audio_handler",
|
|
30
|
+
"list_audio_files_handler",
|
|
31
|
+
"clear_audio_cache_handler",
|
|
32
|
+
"check_audio_status_handler",
|
|
33
|
+
"speech_queue_status_handler",
|
|
34
|
+
"announce_context_handler",
|
|
35
|
+
]
|
|
36
|
+
|
scitex/audio/_mcp/handlers.py
CHANGED
|
@@ -36,6 +36,37 @@ def _get_audio_dir() -> Path:
|
|
|
36
36
|
return audio_dir
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def _get_signature() -> str:
|
|
40
|
+
"""Get signature string with hostname, project, and branch."""
|
|
41
|
+
import os
|
|
42
|
+
import socket
|
|
43
|
+
import subprocess
|
|
44
|
+
|
|
45
|
+
hostname = socket.gethostname()
|
|
46
|
+
cwd = os.getcwd()
|
|
47
|
+
project = os.path.basename(cwd)
|
|
48
|
+
|
|
49
|
+
branch = None
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
timeout=5,
|
|
57
|
+
)
|
|
58
|
+
if result.returncode == 0:
|
|
59
|
+
branch = result.stdout.strip()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
parts = [f"Hostname: {hostname}", f"Project: {project}"]
|
|
64
|
+
if branch:
|
|
65
|
+
parts.append(f"Branch: {branch}")
|
|
66
|
+
|
|
67
|
+
return ". ".join(parts) + ". "
|
|
68
|
+
|
|
69
|
+
|
|
39
70
|
async def generate_audio_handler(
|
|
40
71
|
text: str,
|
|
41
72
|
backend: str | None = None,
|
|
@@ -89,16 +120,16 @@ async def generate_audio_handler(
|
|
|
89
120
|
async def list_backends_handler() -> dict:
|
|
90
121
|
"""List available TTS backends."""
|
|
91
122
|
try:
|
|
92
|
-
from .. import available_backends
|
|
123
|
+
from .. import FALLBACK_ORDER, available_backends
|
|
93
124
|
|
|
94
125
|
backends = available_backends()
|
|
95
126
|
|
|
96
127
|
info = []
|
|
97
|
-
for b in
|
|
128
|
+
for b in FALLBACK_ORDER:
|
|
98
129
|
available = b in backends
|
|
99
130
|
desc = {
|
|
100
|
-
"gtts": "Google TTS - Free, requires internet",
|
|
101
131
|
"elevenlabs": "ElevenLabs - Paid, high quality",
|
|
132
|
+
"gtts": "Google TTS - Free, requires internet",
|
|
102
133
|
"pyttsx3": "System TTS - Offline, uses espeak/SAPI5",
|
|
103
134
|
}
|
|
104
135
|
info.append(
|
|
@@ -109,11 +140,18 @@ async def list_backends_handler() -> dict:
|
|
|
109
140
|
}
|
|
110
141
|
)
|
|
111
142
|
|
|
143
|
+
# Determine actual default based on FALLBACK_ORDER
|
|
144
|
+
default = None
|
|
145
|
+
for b in FALLBACK_ORDER:
|
|
146
|
+
if b in backends:
|
|
147
|
+
default = b
|
|
148
|
+
break
|
|
149
|
+
|
|
112
150
|
return {
|
|
113
151
|
"success": True,
|
|
114
152
|
"backends": info,
|
|
115
153
|
"available": backends,
|
|
116
|
-
"default":
|
|
154
|
+
"default": default,
|
|
117
155
|
}
|
|
118
156
|
|
|
119
157
|
except Exception as e:
|
|
@@ -267,13 +305,25 @@ async def speak_handler(
|
|
|
267
305
|
fallback: bool = True,
|
|
268
306
|
agent_id: str | None = None,
|
|
269
307
|
wait: bool = True,
|
|
308
|
+
signature: bool = False,
|
|
270
309
|
) -> dict:
|
|
271
|
-
"""Convert text to speech with fallback.
|
|
310
|
+
"""Convert text to speech with fallback.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
signature: If True, prepend hostname/project/branch to text.
|
|
314
|
+
"""
|
|
272
315
|
try:
|
|
273
316
|
from .. import speak as tts_speak
|
|
274
317
|
|
|
275
318
|
loop = asyncio.get_event_loop()
|
|
276
319
|
|
|
320
|
+
# Prepend signature if requested
|
|
321
|
+
final_text = text
|
|
322
|
+
sig = None
|
|
323
|
+
if signature:
|
|
324
|
+
sig = _get_signature()
|
|
325
|
+
final_text = sig + text
|
|
326
|
+
|
|
277
327
|
output_path = None
|
|
278
328
|
if save:
|
|
279
329
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
@@ -281,7 +331,7 @@ async def speak_handler(
|
|
|
281
331
|
|
|
282
332
|
def do_speak():
|
|
283
333
|
return tts_speak(
|
|
284
|
-
text=
|
|
334
|
+
text=final_text,
|
|
285
335
|
backend=backend,
|
|
286
336
|
voice=voice,
|
|
287
337
|
rate=rate,
|
|
@@ -300,6 +350,9 @@ async def speak_handler(
|
|
|
300
350
|
"played": play,
|
|
301
351
|
"timestamp": datetime.now().isoformat(),
|
|
302
352
|
}
|
|
353
|
+
if signature:
|
|
354
|
+
result["signature"] = sig
|
|
355
|
+
result["full_text"] = final_text
|
|
303
356
|
if result_path:
|
|
304
357
|
result["path"] = str(result_path)
|
|
305
358
|
|