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.
Files changed (218) hide show
  1. scitex/__init__.py +47 -0
  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 +191 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {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 (SSH tunnel)
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 ──────────┼─ SSH ───────▶│ scitex audio serve
59
- connects to │ RemoteForward│ -t sse --port 8084
60
- localhost:8084 │ │ │
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 audio server**
68
+ **Step 1: Local machine - Start relay server**
67
69
  ```bash
68
- scitex audio serve -t sse --port 8084
70
+ scitex audio relay --port 31293
69
71
  ```
70
72
 
71
- **Step 2: SSH config - Add RemoteForward**
73
+ **Step 2: SSH with reverse tunnel**
74
+ ```bash
75
+ ssh -R 31293:localhost:31293 remote-server
76
+ ```
72
77
 
73
- In `~/.ssh/config` (on local machine):
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 8084 127.0.0.1:8084 # Audio: remote -> local speakers
83
+ RemoteForward 31293 127.0.0.1:31293
79
84
  ```
80
85
 
81
- **Step 3: Remote machine - MCP config**
86
+ **Step 3: Remote agent uses relay**
82
87
 
83
- On remote machine, add to `~/.claude/mcp.json`:
84
- ```json
85
- {
86
- "mcpServers": {
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
- **Step 4: Connect via SSH**
96
- ```bash
97
- ssh nas # RemoteForward creates tunnel automatically
98
- ```
93
+ ### Environment Variables
99
94
 
100
- Now Claude agents on the remote machine can use `mcp__scitex-audio-remote__speak` to play audio on your local speakers.
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 8084` | Remote agents via SSH |
108
- | http | `scitex audio serve -t http --port 8084` | HTTP clients |
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
- | `speak` | Text to speech (supports `rate`, `speed` params) |
115
- | `list_backends` | Show available backends |
116
- | `check_audio_status` | Check WSL audio connectivity |
117
- | `announce_context` | Announce current directory and git branch |
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 MCP 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.
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: 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]:
@@ -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