scitex 2.14.0__py3-none-any.whl → 2.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scitex/__init__.py +47 -0
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +191 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/writer.py +21 -204
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +127 -59
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/engines/elevenlabs_engine.py +6 -1
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +443 -0
- scitex/cli/main.py +198 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/scholar/__init__.py +8 -0
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/writer.py +117 -0
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +191 -0
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/introspect/__init__.py +75 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +42 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/introspect/_mcp/__init__.py +37 -0
- scitex/introspect/_mcp/handlers.py +208 -0
- scitex/introspect/_members.py +151 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/bundle/README.md +1 -1
- scitex/mcp_server.py +98 -5
- scitex/plt/__init__.py +248 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/sh/README.md +1 -1
- scitex/social/__init__.py +153 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/template/README.md +1 -1
- scitex/template/clone_writer_directory.py +5 -5
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.1.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/ai/classification/reporters/_mixins/_storage.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Storage mixin for classification reporter.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from scitex.logging import getLogger
|
|
17
|
+
|
|
18
|
+
from ..reporter_utils.storage import save_metric
|
|
19
|
+
from ._constants import (
|
|
20
|
+
FILENAME_PATTERNS,
|
|
21
|
+
FOLD_DIR_PREFIX_PATTERN,
|
|
22
|
+
FOLD_FILE_PREFIX_PATTERN,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StorageMixin:
|
|
29
|
+
"""Mixin providing storage methods."""
|
|
30
|
+
|
|
31
|
+
def _save_fold_metrics(
|
|
32
|
+
self, metrics: Dict[str, Any], fold: int, labels: List[str]
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Save metrics for a specific fold in shallow directory structure."""
|
|
35
|
+
fold_dir = FOLD_DIR_PREFIX_PATTERN.format(fold=fold)
|
|
36
|
+
|
|
37
|
+
balanced_acc = self._extract_metric_value(metrics.get("balanced-accuracy"))
|
|
38
|
+
mcc_value = self._extract_metric_value(metrics.get("mcc"))
|
|
39
|
+
roc_auc_value = self._extract_metric_value(metrics.get("roc-auc"))
|
|
40
|
+
pr_auc_value = self._extract_metric_value(metrics.get("pr-auc"))
|
|
41
|
+
|
|
42
|
+
for metric_name, metric_value in metrics.items():
|
|
43
|
+
if isinstance(metric_value, dict) and "value" in metric_value:
|
|
44
|
+
actual_value = metric_value["value"]
|
|
45
|
+
else:
|
|
46
|
+
actual_value = metric_value
|
|
47
|
+
|
|
48
|
+
if metric_name == "confusion_matrix":
|
|
49
|
+
self._save_confusion_matrix(
|
|
50
|
+
actual_value, labels, fold, fold_dir, balanced_acc
|
|
51
|
+
)
|
|
52
|
+
elif metric_name == "classification_report":
|
|
53
|
+
self._save_classification_report(actual_value, fold, fold_dir)
|
|
54
|
+
elif metric_name == "balanced-accuracy" and balanced_acc is not None:
|
|
55
|
+
self._save_scalar_metric(
|
|
56
|
+
actual_value, "balanced-accuracy", balanced_acc, fold, fold_dir
|
|
57
|
+
)
|
|
58
|
+
elif metric_name == "mcc" and mcc_value is not None:
|
|
59
|
+
self._save_scalar_metric(actual_value, "mcc", mcc_value, fold, fold_dir)
|
|
60
|
+
elif metric_name == "roc-auc" and roc_auc_value is not None:
|
|
61
|
+
self._save_scalar_metric(
|
|
62
|
+
actual_value, "roc-auc", roc_auc_value, fold, fold_dir
|
|
63
|
+
)
|
|
64
|
+
elif metric_name == "pr-auc" and pr_auc_value is not None:
|
|
65
|
+
self._save_scalar_metric(
|
|
66
|
+
actual_value, "pr-auc", pr_auc_value, fold, fold_dir
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _save_confusion_matrix(
|
|
70
|
+
self, actual_value, labels, fold, fold_dir, balanced_acc
|
|
71
|
+
):
|
|
72
|
+
"""Save confusion matrix as CSV."""
|
|
73
|
+
try:
|
|
74
|
+
if isinstance(actual_value, pd.DataFrame):
|
|
75
|
+
cm_df = actual_value.copy()
|
|
76
|
+
cm_df.index = [f"True_{label}" for label in labels]
|
|
77
|
+
cm_df.columns = [f"Pred_{label}" for label in labels]
|
|
78
|
+
else:
|
|
79
|
+
cm_df = pd.DataFrame(
|
|
80
|
+
actual_value,
|
|
81
|
+
index=[f"True_{label}" for label in labels],
|
|
82
|
+
columns=[f"Pred_{label}" for label in labels],
|
|
83
|
+
)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Error formatting confusion matrix: {e}")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if balanced_acc is not None:
|
|
89
|
+
cm_filename = FILENAME_PATTERNS["confusion_matrix_csv"].format(
|
|
90
|
+
fold=fold, bacc=balanced_acc
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
cm_filename = FILENAME_PATTERNS["confusion_matrix_csv_no_bacc"].format(
|
|
94
|
+
fold=fold
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self.storage.save(cm_df, f"{fold_dir}/{cm_filename}", index=True)
|
|
98
|
+
|
|
99
|
+
def _save_classification_report(self, actual_value, fold, fold_dir):
|
|
100
|
+
"""Save classification report."""
|
|
101
|
+
report_filename = FILENAME_PATTERNS["classification_report"].format(fold=fold)
|
|
102
|
+
|
|
103
|
+
if isinstance(actual_value, pd.DataFrame):
|
|
104
|
+
report_df = actual_value.reset_index()
|
|
105
|
+
report_df = report_df.rename(columns={"index": "class"})
|
|
106
|
+
self.storage.save(report_df, f"{fold_dir}/{report_filename}")
|
|
107
|
+
elif isinstance(actual_value, dict):
|
|
108
|
+
try:
|
|
109
|
+
report_df = pd.DataFrame(actual_value).transpose()
|
|
110
|
+
self.storage.save(report_df, f"{fold_dir}/{report_filename}")
|
|
111
|
+
except Exception:
|
|
112
|
+
report_filename = FILENAME_PATTERNS[
|
|
113
|
+
"classification_report_json"
|
|
114
|
+
].format(fold=fold)
|
|
115
|
+
self.storage.save(actual_value, f"{fold_dir}/{report_filename}")
|
|
116
|
+
else:
|
|
117
|
+
report_filename = FILENAME_PATTERNS["classification_report_txt"].format(
|
|
118
|
+
fold=fold
|
|
119
|
+
)
|
|
120
|
+
self.storage.save(actual_value, f"{fold_dir}/{report_filename}")
|
|
121
|
+
|
|
122
|
+
def _save_scalar_metric(self, actual_value, metric_name, value, fold, fold_dir):
|
|
123
|
+
"""Save scalar metric with value in filename."""
|
|
124
|
+
filename = FILENAME_PATTERNS["fold_metric_with_value"].format(
|
|
125
|
+
fold=fold, metric_name=metric_name, value=value
|
|
126
|
+
)
|
|
127
|
+
save_metric(
|
|
128
|
+
actual_value,
|
|
129
|
+
self.output_dir / f"{fold_dir}/{filename}",
|
|
130
|
+
fold=fold,
|
|
131
|
+
precision=self.precision,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def save(
|
|
135
|
+
self,
|
|
136
|
+
data: Any,
|
|
137
|
+
relative_path: Union[str, Path],
|
|
138
|
+
fold: Optional[int] = None,
|
|
139
|
+
) -> Path:
|
|
140
|
+
"""Save custom data with automatic fold organization and filename prefixing."""
|
|
141
|
+
if fold is not None:
|
|
142
|
+
path_obj = Path(relative_path)
|
|
143
|
+
filename = path_obj.name
|
|
144
|
+
parent = path_obj.parent
|
|
145
|
+
|
|
146
|
+
prefixed_filename = (
|
|
147
|
+
f"{FOLD_FILE_PREFIX_PATTERN.format(fold=fold)}_{filename}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if parent and str(parent) != ".":
|
|
151
|
+
relative_path = f"{FOLD_DIR_PREFIX_PATTERN.format(fold=fold)}/{parent}/{prefixed_filename}"
|
|
152
|
+
else:
|
|
153
|
+
relative_path = (
|
|
154
|
+
f"{FOLD_DIR_PREFIX_PATTERN.format(fold=fold)}/{prefixed_filename}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return self.storage.save(data, relative_path)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# EOF
|
scitex/audio/README.md
CHANGED
|
@@ -27,6 +27,8 @@ scitex audio speak "Hello world"
|
|
|
27
27
|
scitex audio speak "Bonjour" --backend gtts --voice fr
|
|
28
28
|
scitex audio backends # List available backends
|
|
29
29
|
scitex audio check # Check audio status (WSL)
|
|
30
|
+
scitex audio relay # Start HTTP relay server (for remote audio)
|
|
31
|
+
scitex audio serve # Start MCP server
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
## MCP Server
|
|
@@ -46,75 +48,77 @@ Add to `~/.claude/mcp.json`:
|
|
|
46
48
|
}
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
### Remote Audio (
|
|
51
|
+
### Remote Audio (HTTP Relay)
|
|
50
52
|
|
|
51
|
-
Enable remote agents to play audio on local speakers.
|
|
53
|
+
Enable remote agents to play audio on local speakers using a simple HTTP relay.
|
|
52
54
|
|
|
53
55
|
**Architecture:**
|
|
54
56
|
```
|
|
55
57
|
┌─────────────────────────┐ ┌─────────────────────────┐
|
|
56
58
|
│ Remote (e.g., NAS) │ │ Local (WSL/Windows) │
|
|
57
59
|
│ │ │ │
|
|
58
|
-
│ Claude Agent
|
|
59
|
-
│
|
|
60
|
-
│
|
|
61
|
-
│
|
|
60
|
+
│ Claude Agent uses │ │ scitex audio relay │
|
|
61
|
+
│ audio_speak_relay ─────┼─ SSH ───────▶│ --port 31293 │
|
|
62
|
+
│ │ Reverse │ │ │
|
|
63
|
+
│ localhost:31293 │ Tunnel │ ▼ │
|
|
62
64
|
│ │ │ 🔊 Speakers │
|
|
63
65
|
└─────────────────────────┘ └─────────────────────────┘
|
|
64
66
|
```
|
|
65
67
|
|
|
66
|
-
**Step 1: Local machine - Start
|
|
68
|
+
**Step 1: Local machine - Start relay server**
|
|
67
69
|
```bash
|
|
68
|
-
scitex audio
|
|
70
|
+
scitex audio relay --port 31293
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
**Step 2: SSH
|
|
73
|
+
**Step 2: SSH with reverse tunnel**
|
|
74
|
+
```bash
|
|
75
|
+
ssh -R 31293:localhost:31293 remote-server
|
|
76
|
+
```
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
Or add to `~/.ssh/config`:
|
|
74
79
|
```
|
|
75
80
|
Host nas
|
|
76
81
|
HostName 192.168.x.x
|
|
77
82
|
User youruser
|
|
78
|
-
RemoteForward
|
|
83
|
+
RemoteForward 31293 127.0.0.1:31293
|
|
79
84
|
```
|
|
80
85
|
|
|
81
|
-
**Step 3: Remote
|
|
86
|
+
**Step 3: Remote agent uses relay**
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"scitex-audio-remote": {
|
|
88
|
-
"type": "sse",
|
|
89
|
-
"url": "http://localhost:8084/sse"
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
88
|
+
The `audio_speak_relay` MCP tool auto-detects:
|
|
89
|
+
1. `SCITEX_AUDIO_RELAY_URL` env var
|
|
90
|
+
2. Localhost:31293 (SSH reverse tunnel)
|
|
91
|
+
3. SSH_CLIENT IP (auto-detected from SSH session)
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
ssh nas # RemoteForward creates tunnel automatically
|
|
98
|
-
```
|
|
93
|
+
### Environment Variables
|
|
99
94
|
|
|
100
|
-
|
|
95
|
+
| Variable | Default | Description |
|
|
96
|
+
|----------|---------|-------------|
|
|
97
|
+
| `SCITEX_AUDIO_PORT` | 31293 | Server/relay port |
|
|
98
|
+
| `SCITEX_AUDIO_MODE` | auto | `local`, `remote`, or `auto` |
|
|
99
|
+
| `SCITEX_AUDIO_RELAY_URL` | (auto) | Full relay URL |
|
|
100
|
+
| `SCITEX_AUDIO_RELAY_HOST` | (none) | Relay host |
|
|
101
|
+
| `SCITEX_AUDIO_RELAY_PORT` | 31293 | Relay port |
|
|
101
102
|
|
|
102
103
|
### Server Transports
|
|
103
104
|
|
|
104
105
|
| Transport | Command | Use Case |
|
|
105
106
|
|-----------|---------|----------|
|
|
106
107
|
| stdio | `scitex audio serve` | Claude Desktop (default) |
|
|
107
|
-
| sse | `scitex audio serve -t sse --port
|
|
108
|
-
| http | `scitex audio serve -t http --port
|
|
108
|
+
| sse | `scitex audio serve -t sse --port 31293` | Remote MCP agents |
|
|
109
|
+
| http | `scitex audio serve -t http --port 31293` | HTTP MCP clients |
|
|
110
|
+
| relay | `scitex audio relay --port 31293` | Simple HTTP relay |
|
|
109
111
|
|
|
110
|
-
### Tools
|
|
112
|
+
### MCP Tools
|
|
111
113
|
|
|
112
114
|
| Tool | Description |
|
|
113
115
|
|------|-------------|
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
116
|
+
| `audio_speak` | Text to speech (plays on server) |
|
|
117
|
+
| `audio_speak_local` | TTS on server machine |
|
|
118
|
+
| `audio_speak_relay` | TTS via relay (remote playback) |
|
|
119
|
+
| `audio_list_backends` | Show available backends |
|
|
120
|
+
| `audio_check_audio_status` | Check WSL audio connectivity |
|
|
121
|
+
| `audio_announce_context` | Announce current directory and git branch |
|
|
118
122
|
|
|
119
123
|
## Backends
|
|
120
124
|
|
|
@@ -126,4 +130,4 @@ Now Claude agents on the remote machine can use `mcp__scitex-audio-remote__speak
|
|
|
126
130
|
|
|
127
131
|
## Cross-Process Locking
|
|
128
132
|
|
|
129
|
-
The
|
|
133
|
+
The relay server uses FIFO locking to ensure only one audio plays at a time across all Claude Code sessions. This prevents audio overlap when multiple agents are running.
|
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]:
|
|
@@ -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
|