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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. scitex/__init__.py +68 -61
  2. scitex/_mcp_tools/introspect.py +42 -23
  3. scitex/_mcp_tools/template.py +24 -0
  4. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  5. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  6. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  7. scitex/audio/__init__.py +2 -2
  8. scitex/audio/_tts.py +18 -10
  9. scitex/audio/engines/base.py +17 -10
  10. scitex/audio/engines/elevenlabs_engine.py +1 -1
  11. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  12. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  13. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  14. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  15. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  16. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  17. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  18. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  19. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  20. scitex/cli/introspect.py +112 -74
  21. scitex/cli/main.py +2 -0
  22. scitex/cli/plt.py +357 -0
  23. scitex/cli/repro.py +15 -8
  24. scitex/cli/resource.py +15 -8
  25. scitex/cli/scholar/__init__.py +15 -8
  26. scitex/cli/social.py +6 -6
  27. scitex/cli/stats.py +15 -8
  28. scitex/cli/template.py +129 -12
  29. scitex/cli/tex.py +15 -8
  30. scitex/cli/writer.py +15 -8
  31. scitex/cloud/__init__.py +41 -2
  32. scitex/config/_env_registry.py +84 -19
  33. scitex/context/__init__.py +22 -0
  34. scitex/dev/__init__.py +20 -1
  35. scitex/gen/__init__.py +50 -14
  36. scitex/gen/_list_packages.py +4 -4
  37. scitex/introspect/__init__.py +16 -9
  38. scitex/introspect/_core.py +7 -8
  39. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
  40. scitex/introspect/_mcp/__init__.py +10 -6
  41. scitex/introspect/_mcp/handlers.py +37 -12
  42. scitex/introspect/_members.py +7 -3
  43. scitex/introspect/_signature.py +3 -3
  44. scitex/introspect/_source.py +2 -2
  45. scitex/io/_save.py +1 -2
  46. scitex/logging/_formatters.py +19 -9
  47. scitex/mcp_server.py +1 -1
  48. scitex/os/__init__.py +4 -0
  49. scitex/{gen → os}/_check_host.py +4 -5
  50. scitex/plt/__init__.py +11 -14
  51. scitex/session/__init__.py +26 -7
  52. scitex/session/_decorator.py +1 -1
  53. scitex/sh/__init__.py +7 -4
  54. scitex/social/__init__.py +10 -8
  55. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  56. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  57. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  58. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  59. scitex/stats/_mcp/_handlers/_format.py +94 -0
  60. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  61. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  62. scitex/stats/_mcp/_handlers/_power.py +247 -0
  63. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  64. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  65. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  66. scitex/stats/_mcp/handlers.py +19 -1171
  67. scitex/stats/auto/_stat_style.py +175 -0
  68. scitex/stats/auto/_style_definitions.py +411 -0
  69. scitex/stats/auto/_styles.py +22 -620
  70. scitex/stats/descriptive/__init__.py +11 -8
  71. scitex/stats/descriptive/_ci.py +39 -0
  72. scitex/stats/power/_power.py +15 -4
  73. scitex/str/__init__.py +2 -1
  74. scitex/str/_title_case.py +63 -0
  75. scitex/template/__init__.py +25 -10
  76. scitex/template/_code_templates.py +147 -0
  77. scitex/template/_mcp/handlers.py +81 -0
  78. scitex/template/_mcp/tool_schemas.py +55 -0
  79. scitex/template/_templates/__init__.py +51 -0
  80. scitex/template/_templates/audio.py +233 -0
  81. scitex/template/_templates/canvas.py +312 -0
  82. scitex/template/_templates/capture.py +268 -0
  83. scitex/template/_templates/config.py +43 -0
  84. scitex/template/_templates/diagram.py +294 -0
  85. scitex/template/_templates/io.py +107 -0
  86. scitex/template/_templates/module.py +53 -0
  87. scitex/template/_templates/plt.py +202 -0
  88. scitex/template/_templates/scholar.py +267 -0
  89. scitex/template/_templates/session.py +130 -0
  90. scitex/template/_templates/session_minimal.py +43 -0
  91. scitex/template/_templates/session_plot.py +67 -0
  92. scitex/template/_templates/session_stats.py +77 -0
  93. scitex/template/_templates/stats.py +323 -0
  94. scitex/template/_templates/writer.py +296 -0
  95. scitex/ui/_backends/_email.py +10 -2
  96. scitex/ui/_backends/_webhook.py +5 -1
  97. scitex/web/_search_pubmed.py +10 -6
  98. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/METADATA +1 -1
  99. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/RECORD +105 -64
  100. scitex/gen/_ci.py +0 -12
  101. scitex/gen/_title_case.py +0 -89
  102. /scitex/{gen → context}/_detect_environment.py +0 -0
  103. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  104. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  105. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
  106. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
  107. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/ai/classification/timeseries/_sliding_window_plotting.py
4
+
5
+ """Plotting mixin for TimeSeriesSlidingWindowSplit visualization."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import matplotlib.patches as patches
12
+ import matplotlib.pyplot as plt
13
+ import numpy as np
14
+
15
+ import scitex as stx
16
+
17
+ if TYPE_CHECKING:
18
+ from ._sliding_window_core import TimeSeriesSlidingWindowSplitCore
19
+
20
+ __all__ = ["SlidingWindowPlottingMixin"]
21
+
22
+ COLORS = stx.plt.color.PARAMS
23
+
24
+
25
+ class SlidingWindowPlottingMixin:
26
+ """Mixin class providing plot_splits visualization for sliding window splitters."""
27
+
28
+ def plot_splits(
29
+ self: TimeSeriesSlidingWindowSplitCore,
30
+ X,
31
+ y=None,
32
+ timestamps=None,
33
+ figsize=(12, 6),
34
+ save_path=None,
35
+ ):
36
+ """Visualize the sliding window splits as rectangles.
37
+
38
+ Shows train (blue), validation (green), and test (red) sets.
39
+ When val_ratio=0, only shows train and test.
40
+ When undersampling is enabled, shows dropped samples in gray.
41
+
42
+ Parameters
43
+ ----------
44
+ X : array-like
45
+ Training data
46
+ y : array-like, optional
47
+ Target variable (required for undersampling visualization)
48
+ timestamps : array-like, optional
49
+ Timestamps (if None, uses sample indices)
50
+ figsize : tuple, default (12, 6)
51
+ Figure size
52
+ save_path : str, optional
53
+ Path to save the plot
54
+
55
+ Returns
56
+ -------
57
+ fig : matplotlib.figure.Figure
58
+ The created figure
59
+ """
60
+ if timestamps is None:
61
+ timestamps = np.arange(len(X))
62
+
63
+ time_order = np.argsort(timestamps)
64
+
65
+ # Get splits WITH undersampling (if enabled)
66
+ if self.val_ratio > 0:
67
+ splits = list(self.split_with_val(X, y, timestamps))[:10]
68
+ split_type = "train-val-test"
69
+ else:
70
+ splits = list(self.split(X, y, timestamps))[:10]
71
+ split_type = "train-test"
72
+
73
+ if not splits:
74
+ raise ValueError("No splits generated")
75
+
76
+ # Get splits WITHOUT undersampling to show dropped samples
77
+ splits_no_undersample = None
78
+ if self.undersample and y is not None:
79
+ original_undersample = self.undersample
80
+ self.undersample = False
81
+ if self.val_ratio > 0:
82
+ splits_no_undersample = list(self.split_with_val(X, y, timestamps))[:10]
83
+ else:
84
+ splits_no_undersample = list(self.split(X, y, timestamps))[:10]
85
+ self.undersample = original_undersample
86
+
87
+ fig, ax = stx.plt.subplots(figsize=figsize)
88
+
89
+ # Plot each fold
90
+ for fold, split_indices in enumerate(splits):
91
+ y_pos = fold
92
+ self._plot_fold_rectangles(ax, split_indices, time_order, y_pos, fold)
93
+
94
+ # Plot dropped samples if undersampling
95
+ if splits_no_undersample is not None:
96
+ self._plot_dropped_samples(ax, splits, splits_no_undersample, time_order, y)
97
+
98
+ # Plot kept samples
99
+ self._plot_kept_samples(ax, splits, time_order, y)
100
+
101
+ # Format plot
102
+ self._format_plot(ax, X, splits, split_type, y)
103
+
104
+ plt.tight_layout()
105
+
106
+ if save_path:
107
+ fig.savefig(save_path, dpi=150, bbox_inches="tight")
108
+
109
+ return fig
110
+
111
+ def _plot_fold_rectangles(self, ax, split_indices, time_order, y_pos, fold):
112
+ """Plot train/val/test rectangles for a fold."""
113
+ if len(split_indices) == 3:
114
+ train_idx, val_idx, test_idx = split_indices
115
+ self._plot_train_rect(ax, train_idx, time_order, y_pos, fold)
116
+ if len(val_idx) > 0:
117
+ self._plot_val_rect(ax, val_idx, time_order, y_pos, fold)
118
+ self._plot_test_rect(ax, test_idx, time_order, y_pos, fold, is_3way=True)
119
+ else:
120
+ train_idx, test_idx = split_indices
121
+ self._plot_train_rect(ax, train_idx, time_order, y_pos, fold)
122
+ self._plot_test_rect(ax, test_idx, time_order, y_pos, fold, is_3way=False)
123
+
124
+ def _plot_train_rect(self, ax, train_idx, time_order, y_pos, fold):
125
+ """Plot training window rectangle."""
126
+ train_positions = [np.where(time_order == idx)[0][0] for idx in train_idx]
127
+ if train_positions:
128
+ train_start = min(train_positions)
129
+ train_end = max(train_positions)
130
+ train_rect = patches.Rectangle(
131
+ (train_start, y_pos - 0.3),
132
+ train_end - train_start + 1,
133
+ 0.6,
134
+ linewidth=1,
135
+ edgecolor="blue",
136
+ facecolor="lightblue",
137
+ alpha=0.7,
138
+ label="Train" if fold == 0 else "",
139
+ )
140
+ ax.add_patch(train_rect)
141
+
142
+ def _plot_val_rect(self, ax, val_idx, time_order, y_pos, fold):
143
+ """Plot validation window rectangle."""
144
+ val_positions = [np.where(time_order == idx)[0][0] for idx in val_idx]
145
+ if val_positions:
146
+ val_start = min(val_positions)
147
+ val_end = max(val_positions)
148
+ val_rect = patches.Rectangle(
149
+ (val_start, y_pos - 0.3),
150
+ val_end - val_start + 1,
151
+ 0.6,
152
+ linewidth=1,
153
+ edgecolor="green",
154
+ facecolor="lightgreen",
155
+ alpha=0.7,
156
+ label="Validation" if fold == 0 else "",
157
+ )
158
+ ax.add_patch(val_rect)
159
+
160
+ def _plot_test_rect(self, ax, test_idx, time_order, y_pos, fold, is_3way):
161
+ """Plot test window rectangle."""
162
+ test_positions = [np.where(time_order == idx)[0][0] for idx in test_idx]
163
+ if test_positions:
164
+ test_start = min(test_positions)
165
+ test_end = max(test_positions)
166
+ if is_3way:
167
+ edgecolor = COLORS["RGBA_NORM"]["red"]
168
+ facecolor = COLORS["RGBA_NORM"]["red"]
169
+ else:
170
+ edgecolor = "red"
171
+ facecolor = "lightcoral"
172
+ test_rect = patches.Rectangle(
173
+ (test_start, y_pos - 0.3),
174
+ test_end - test_start + 1,
175
+ 0.6,
176
+ linewidth=1,
177
+ edgecolor=edgecolor,
178
+ facecolor=facecolor,
179
+ alpha=0.7,
180
+ label="Test" if fold == 0 else "",
181
+ )
182
+ ax.add_patch(test_rect)
183
+
184
+ def _plot_dropped_samples(self, ax, splits, splits_no_undersample, time_order, y):
185
+ """Plot dropped samples from undersampling."""
186
+ np.random.seed(42)
187
+ jitter_strength = 0.15
188
+
189
+ for fold, split_indices_no_us in enumerate(splits_no_undersample):
190
+ y_pos = fold
191
+ split_indices_us = splits[fold]
192
+
193
+ if len(split_indices_no_us) == 3:
194
+ train_idx_no_us, val_idx_no_us, _ = split_indices_no_us
195
+ train_idx_us, val_idx_us, _ = split_indices_us
196
+
197
+ dropped_train = np.setdiff1d(train_idx_no_us, train_idx_us)
198
+ if len(dropped_train) > 0:
199
+ positions = [
200
+ np.where(time_order == idx)[0][0] for idx in dropped_train
201
+ ]
202
+ jitter = np.random.normal(0, jitter_strength, len(positions))
203
+ ax.scatter(
204
+ positions,
205
+ y_pos + jitter,
206
+ c="gray",
207
+ s=15,
208
+ alpha=0.3,
209
+ marker="x",
210
+ label="Dropped (train)" if fold == 0 else "",
211
+ zorder=2,
212
+ )
213
+
214
+ dropped_val = np.setdiff1d(val_idx_no_us, val_idx_us)
215
+ if len(dropped_val) > 0:
216
+ positions = [
217
+ np.where(time_order == idx)[0][0] for idx in dropped_val
218
+ ]
219
+ jitter = np.random.normal(0, jitter_strength, len(positions))
220
+ ax.scatter(
221
+ positions,
222
+ y_pos + jitter,
223
+ c="gray",
224
+ s=15,
225
+ alpha=0.3,
226
+ marker="x",
227
+ label="Dropped (val)" if fold == 0 else "",
228
+ zorder=2,
229
+ )
230
+ else:
231
+ train_idx_no_us, _ = split_indices_no_us
232
+ train_idx_us, _ = split_indices_us
233
+
234
+ dropped_train = np.setdiff1d(train_idx_no_us, train_idx_us)
235
+ if len(dropped_train) > 0:
236
+ positions = [
237
+ np.where(time_order == idx)[0][0] for idx in dropped_train
238
+ ]
239
+ jitter = np.random.normal(0, jitter_strength, len(positions))
240
+ ax.scatter(
241
+ positions,
242
+ y_pos + jitter,
243
+ c="gray",
244
+ s=15,
245
+ alpha=0.3,
246
+ marker="x",
247
+ label="Dropped samples" if fold == 0 else "",
248
+ zorder=2,
249
+ )
250
+
251
+ def _plot_kept_samples(self, ax, splits, time_order, y):
252
+ """Plot kept samples with colors."""
253
+ np.random.seed(42)
254
+ jitter_strength = 0.15
255
+
256
+ for fold, split_indices in enumerate(splits):
257
+ y_pos = fold
258
+
259
+ if len(split_indices) == 3:
260
+ train_idx, val_idx, test_idx = split_indices
261
+ self._scatter_indices(
262
+ ax, train_idx, time_order, y_pos, y, fold, "train"
263
+ )
264
+ if len(val_idx) > 0:
265
+ self._scatter_indices(
266
+ ax, val_idx, time_order, y_pos, y, fold, "val"
267
+ )
268
+ self._scatter_indices(ax, test_idx, time_order, y_pos, y, fold, "test")
269
+ else:
270
+ train_idx, test_idx = split_indices
271
+ self._scatter_indices(
272
+ ax, train_idx, time_order, y_pos, y, fold, "train"
273
+ )
274
+ self._scatter_indices(ax, test_idx, time_order, y_pos, y, fold, "test")
275
+
276
+ def _scatter_indices(self, ax, indices, time_order, y_pos, y, fold, set_type):
277
+ """Scatter plot indices with jittering."""
278
+ jitter_strength = 0.15
279
+ positions = [np.where(time_order == idx)[0][0] for idx in indices]
280
+
281
+ if not positions:
282
+ return
283
+
284
+ jitter = np.random.normal(0, jitter_strength, len(positions))
285
+
286
+ color_map = {
287
+ "train": ("darkblue", "o", "Train"),
288
+ "val": ("darkgreen", "^", "Val"),
289
+ "test": ("darkred", "s", "Test"),
290
+ }
291
+
292
+ if y is not None:
293
+ class_colors = {
294
+ "train": (
295
+ COLORS["RGBA_NORM"]["blue"],
296
+ COLORS["RGBA_NORM"]["lightblue"],
297
+ ),
298
+ "val": (COLORS["RGBA_NORM"]["yellow"], COLORS["RGBA_NORM"]["orange"]),
299
+ "test": (COLORS["RGBA_NORM"]["red"], COLORS["RGBA_NORM"]["brown"]),
300
+ }
301
+ colors = [
302
+ class_colors[set_type][0] if y[idx] == 0 else class_colors[set_type][1]
303
+ for idx in indices
304
+ ]
305
+ ax.scatter(
306
+ positions,
307
+ y_pos + jitter,
308
+ c=colors,
309
+ s=20,
310
+ alpha=0.7,
311
+ marker=color_map[set_type][1],
312
+ label=f"{color_map[set_type][2]} (class 0)" if fold == 0 else "",
313
+ zorder=3,
314
+ )
315
+ else:
316
+ ax.scatter(
317
+ positions,
318
+ y_pos + jitter,
319
+ c=color_map[set_type][0],
320
+ s=20,
321
+ alpha=0.7,
322
+ marker=color_map[set_type][1],
323
+ label=f"{color_map[set_type][2]} points" if fold == 0 else "",
324
+ zorder=3,
325
+ )
326
+
327
+ def _format_plot(self, ax, X, splits, split_type, y):
328
+ """Format the plot with labels and legend."""
329
+ ax.set_ylim(-0.5, len(splits) - 0.5)
330
+ ax.set_xlim(0, len(X))
331
+ ax.set_xlabel("Temporal Position (sorted by timestamp)")
332
+ ax.set_ylabel("Fold")
333
+
334
+ gap_text = f", Gap: {self.gap}" if self.gap > 0 else ""
335
+ val_text = f", Val ratio: {self.val_ratio:.1%}" if self.val_ratio > 0 else ""
336
+ ax.set_title(
337
+ f"Sliding Window Split Visualization ({split_type})\\n"
338
+ f"Window: {self.window_size}, Step: {self.step_size}, Test: {self.test_size}{gap_text}{val_text}\\n"
339
+ f"Rectangles show windows, dots show actual data points"
340
+ )
341
+
342
+ ax.set_yticks(range(len(splits)))
343
+ ax.set_yticklabels([f"Fold {i}" for i in range(len(splits))])
344
+
345
+ if y is not None:
346
+ unique_classes, class_counts = np.unique(y, return_counts=True)
347
+ total_class_info = ", ".join(
348
+ [
349
+ f"Class {cls}: n={count}"
350
+ for cls, count in zip(unique_classes, class_counts)
351
+ ]
352
+ )
353
+
354
+ first_split = splits[0]
355
+ if len(first_split) == 3:
356
+ train_idx, val_idx, test_idx = first_split
357
+ fold_info = f"Fold 0: Train n={len(train_idx)}, Val n={len(val_idx)}, Test n={len(test_idx)}"
358
+ else:
359
+ train_idx, test_idx = first_split
360
+ fold_info = f"Fold 0: Train n={len(train_idx)}, Test n={len(test_idx)}"
361
+
362
+ handles, labels = ax.get_legend_handles_labels()
363
+ legend_title = f"Total: {total_class_info}\\n{fold_info}"
364
+ ax.legend(handles, labels, loc="upper right", title=legend_title)
365
+ else:
366
+ ax.legend(loc="upper right")
367
+
368
+
369
+ # EOF
scitex/audio/__init__.py CHANGED
@@ -173,8 +173,8 @@ def available_backends() -> List[str]:
173
173
  if ElevenLabsTTS:
174
174
  import os
175
175
 
176
- api_key = os.environ.get("ELEVENLABS_API_KEY") or os.environ.get(
177
- "ELEVENLABS_API_KEY_SCITEX_AUDIO"
176
+ api_key = os.environ.get("SCITEX_AUDIO_ELEVENLABS_API_KEY") or os.environ.get(
177
+ "ELEVENLABS_API_KEY"
178
178
  )
179
179
  if api_key:
180
180
  backends.append("elevenlabs")
scitex/audio/_tts.py CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  # Timestamp: "2025-12-11 (ywatanabe)"
4
3
  # File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/_tts.py
5
4
  # ----------------------------------------
@@ -20,7 +19,7 @@ from __future__ import annotations
20
19
  import os
21
20
  import subprocess
22
21
  import tempfile
23
- from dataclasses import dataclass, field
22
+ from dataclasses import dataclass
24
23
  from pathlib import Path
25
24
  from typing import Optional
26
25
 
@@ -44,7 +43,8 @@ class TTSConfig:
44
43
  class TTS:
45
44
  """Text-to-Speech using ElevenLabs API.
46
45
 
47
- Examples:
46
+ Examples
47
+ --------
48
48
  # Basic usage
49
49
  tts = TTS()
50
50
  tts.speak("Hello, world!")
@@ -84,7 +84,11 @@ class TTS:
84
84
  voice_id: Direct voice ID (overrides voice_name).
85
85
  **kwargs: Additional config options (stability, speed, etc.)
86
86
  """
87
- self.api_key = api_key or os.environ.get("ELEVENLABS_API_KEY")
87
+ self.api_key = (
88
+ api_key
89
+ or os.environ.get("SCITEX_AUDIO_ELEVENLABS_API_KEY")
90
+ or os.environ.get("ELEVENLABS_API_KEY")
91
+ )
88
92
  self.config = TTSConfig(**kwargs)
89
93
 
90
94
  if voice_id:
@@ -129,7 +133,8 @@ class TTS:
129
133
  voice_name: Override voice name for this call.
130
134
  voice_id: Override voice ID for this call.
131
135
 
132
- Returns:
136
+ Returns
137
+ -------
133
138
  Path to the generated audio file, or None if only played.
134
139
  """
135
140
  # Determine voice
@@ -228,14 +233,15 @@ class TTS:
228
233
  try:
229
234
  # SoundPlayer only supports WAV, so convert if needed
230
235
  wav_path = path
231
- if path.suffix.lower() in ('.mp3', '.ogg', '.m4a'):
236
+ if path.suffix.lower() in (".mp3", ".ogg", ".m4a"):
232
237
  try:
233
238
  from pydub import AudioSegment
234
- fd, tmp_wav = tempfile.mkstemp(suffix='.wav', prefix='scitex_')
239
+
240
+ fd, tmp_wav = tempfile.mkstemp(suffix=".wav", prefix="scitex_")
235
241
  os.close(fd)
236
242
  wav_path = Path(tmp_wav)
237
243
  audio = AudioSegment.from_file(str(path))
238
- audio.export(str(wav_path), format='wav')
244
+ audio.export(str(wav_path), format="wav")
239
245
  except ImportError:
240
246
  pass
241
247
 
@@ -303,10 +309,12 @@ def speak(
303
309
  output_path: Optional path to save audio.
304
310
  **kwargs: Additional TTS config options.
305
311
 
306
- Returns:
312
+ Returns
313
+ -------
307
314
  Path to audio file if output_path specified, else None.
308
315
 
309
- Examples:
316
+ Examples
317
+ --------
310
318
  import scitex
311
319
 
312
320
  # Simple speak
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  # Timestamp: "2025-12-11 (ywatanabe)"
4
3
  # File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/engines/base.py
5
4
  # ----------------------------------------
@@ -49,10 +48,13 @@ class TTSBackend:
49
48
 
50
49
  # Check ElevenLabs
51
50
  try:
52
- import elevenlabs
53
51
  import os
54
52
 
55
- if os.environ.get("ELEVENLABS_API_KEY"):
53
+ import elevenlabs
54
+
55
+ if os.environ.get("SCITEX_AUDIO_ELEVENLABS_API_KEY") or os.environ.get(
56
+ "ELEVENLABS_API_KEY"
57
+ ):
56
58
  backends.append(cls.ELEVENLABS)
57
59
  except ImportError:
58
60
  pass
@@ -74,7 +76,8 @@ class BaseTTS(ABC):
74
76
  text: Text to convert to speech.
75
77
  output_path: Path to save the audio file.
76
78
 
77
- Returns:
79
+ Returns
80
+ -------
78
81
  Path to the generated audio file.
79
82
  """
80
83
  pass
@@ -83,7 +86,8 @@ class BaseTTS(ABC):
83
86
  def get_voices(self) -> List[dict]:
84
87
  """Get available voices for this backend.
85
88
 
86
- Returns:
89
+ Returns
90
+ -------
87
91
  List of voice dictionaries with 'name' and 'id' keys.
88
92
  """
89
93
  pass
@@ -119,7 +123,8 @@ class BaseTTS(ABC):
119
123
  play: Whether to play the audio.
120
124
  voice: Optional voice name/id.
121
125
 
122
- Returns:
126
+ Returns
127
+ -------
123
128
  Path to audio file if output_path specified, else None.
124
129
  """
125
130
  import tempfile
@@ -201,7 +206,8 @@ class BaseTTS(ABC):
201
206
  Args:
202
207
  path: Path to audio file (in WSL filesystem)
203
208
 
204
- Returns:
209
+ Returns
210
+ -------
205
211
  True if playback succeeded, False otherwise
206
212
  """
207
213
  import os
@@ -220,16 +226,17 @@ class BaseTTS(ABC):
220
226
  try:
221
227
  # SoundPlayer only supports WAV, so convert if needed
222
228
  wav_path = path
223
- if path.suffix.lower() in ('.mp3', '.ogg', '.m4a'):
229
+ if path.suffix.lower() in (".mp3", ".ogg", ".m4a"):
224
230
  try:
225
231
  from pydub import AudioSegment
232
+
226
233
  # Create temp WAV file
227
- fd, tmp_wav = tempfile.mkstemp(suffix='.wav', prefix='scitex_')
234
+ fd, tmp_wav = tempfile.mkstemp(suffix=".wav", prefix="scitex_")
228
235
  os.close(fd)
229
236
  wav_path = Path(tmp_wav)
230
237
 
231
238
  audio = AudioSegment.from_file(str(path))
232
- audio.export(str(wav_path), format='wav')
239
+ audio.export(str(wav_path), format="wav")
233
240
  except ImportError:
234
241
  # pydub not available, try direct playback anyway
235
242
  pass
@@ -55,8 +55,8 @@ class ElevenLabsTTS(BaseTTS):
55
55
  super().__init__(**kwargs)
56
56
  self.api_key = (
57
57
  api_key
58
+ or os.environ.get("SCITEX_AUDIO_ELEVENLABS_API_KEY")
58
59
  or os.environ.get("ELEVENLABS_API_KEY")
59
- or os.environ.get("ELEVENLABS_API_KEY_SCITEX_AUDIO")
60
60
  )
61
61
  self.voice = voice
62
62
  self.model_id = model_id
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/__init__.py
4
+
5
+ """Core WebEditor package for Flask-based figure editing.
6
+
7
+ This package provides the WebEditor class and supporting modules for
8
+ browser-based figure editing functionality.
9
+ """
10
+
11
+ from ._bbox_extraction import extract_bboxes_from_metadata
12
+ from ._editor import WebEditor
13
+ from ._export_helpers import compose_panels_to_figure, export_composed_figure
14
+
15
+ # Backward compatibility alias
16
+ _extract_bboxes_from_metadata = extract_bboxes_from_metadata
17
+
18
+ __all__ = [
19
+ "WebEditor",
20
+ "extract_bboxes_from_metadata",
21
+ "export_composed_figure",
22
+ "compose_panels_to_figure",
23
+ "_extract_bboxes_from_metadata",
24
+ ]
25
+
26
+
27
+ # EOF