scitex 2.8.1__py3-none-any.whl → 2.10.0__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 (415) hide show
  1. scitex/__init__.py +15 -7
  2. scitex/__version__.py +1 -2
  3. scitex/_install_guide.py +250 -0
  4. scitex/_optional_deps.py +206 -39
  5. scitex/ai/_gen_ai/_Groq.py +2 -4
  6. scitex/ai/_gen_ai/_OpenAI.py +5 -2
  7. scitex/ai/_gen_ai/_Perplexity.py +20 -6
  8. scitex/audio/__init__.py +24 -15
  9. scitex/audio/_cross_process_lock.py +139 -0
  10. scitex/audio/_mcp_handlers.py +256 -0
  11. scitex/audio/_mcp_tool_schemas.py +203 -0
  12. scitex/audio/engines/elevenlabs_engine.py +5 -2
  13. scitex/audio/mcp_server.py +98 -457
  14. scitex/bridge/__init__.py +30 -19
  15. scitex/bridge/_figrecipe.py +245 -0
  16. scitex/bridge/_helpers.py +2 -1
  17. scitex/bridge/_plt_vis.py +23 -10
  18. scitex/bridge/_stats_plt.py +18 -5
  19. scitex/bridge/_stats_vis.py +16 -2
  20. scitex/browser/__init__.py +84 -44
  21. scitex/browser/automation/__init__.py +5 -1
  22. scitex/browser/core/BrowserMixin.py +17 -4
  23. scitex/browser/core/__init__.py +11 -2
  24. scitex/browser/remote/CaptchaHandler.py +1 -1
  25. scitex/browser/remote/ZenRowsAPIClient.py +1 -1
  26. scitex/capture/grid.py +487 -0
  27. scitex/capture/mcp_handlers.py +401 -0
  28. scitex/capture/mcp_tool_defs.py +192 -0
  29. scitex/capture/mcp_tools.py +241 -0
  30. scitex/capture/mcp_utils.py +30 -0
  31. scitex/cli/convert.py +421 -0
  32. scitex/cli/main.py +6 -4
  33. scitex/datetime/__init__.py +46 -0
  34. scitex/datetime/_linspace.py +100 -0
  35. scitex/datetime/_normalize_timestamp.py +306 -0
  36. scitex/db/_delete_duplicates.py +4 -4
  37. scitex/db/_sqlite3/_delete_duplicates.py +11 -2
  38. scitex/dev/plt/__init__.py +61 -62
  39. scitex/dev/plt/demo_plotters/__init__.py +0 -0
  40. scitex/dev/plt/demo_plotters/plot_mpl_axhline.py +28 -0
  41. scitex/dev/plt/demo_plotters/plot_mpl_axhspan.py +28 -0
  42. scitex/dev/plt/demo_plotters/plot_mpl_axvline.py +28 -0
  43. scitex/dev/plt/demo_plotters/plot_mpl_axvspan.py +28 -0
  44. scitex/dev/plt/demo_plotters/plot_mpl_bar.py +29 -0
  45. scitex/dev/plt/demo_plotters/plot_mpl_barh.py +29 -0
  46. scitex/dev/plt/demo_plotters/plot_mpl_boxplot.py +28 -0
  47. scitex/dev/plt/demo_plotters/plot_mpl_contour.py +31 -0
  48. scitex/dev/plt/demo_plotters/plot_mpl_contourf.py +31 -0
  49. scitex/dev/plt/demo_plotters/plot_mpl_errorbar.py +30 -0
  50. scitex/dev/plt/demo_plotters/plot_mpl_eventplot.py +28 -0
  51. scitex/dev/plt/demo_plotters/plot_mpl_fill.py +30 -0
  52. scitex/dev/plt/demo_plotters/plot_mpl_fill_between.py +31 -0
  53. scitex/dev/plt/demo_plotters/plot_mpl_hexbin.py +28 -0
  54. scitex/dev/plt/demo_plotters/plot_mpl_hist.py +28 -0
  55. scitex/dev/plt/demo_plotters/plot_mpl_hist2d.py +28 -0
  56. scitex/dev/plt/demo_plotters/plot_mpl_imshow.py +29 -0
  57. scitex/dev/plt/demo_plotters/plot_mpl_pcolormesh.py +31 -0
  58. scitex/dev/plt/demo_plotters/plot_mpl_pie.py +29 -0
  59. scitex/dev/plt/demo_plotters/plot_mpl_plot.py +29 -0
  60. scitex/dev/plt/demo_plotters/plot_mpl_quiver.py +31 -0
  61. scitex/dev/plt/demo_plotters/plot_mpl_scatter.py +28 -0
  62. scitex/dev/plt/demo_plotters/plot_mpl_stackplot.py +31 -0
  63. scitex/dev/plt/demo_plotters/plot_mpl_stem.py +29 -0
  64. scitex/dev/plt/demo_plotters/plot_mpl_step.py +29 -0
  65. scitex/dev/plt/demo_plotters/plot_mpl_violinplot.py +28 -0
  66. scitex/dev/plt/demo_plotters/plot_sns_barplot.py +29 -0
  67. scitex/dev/plt/demo_plotters/plot_sns_boxplot.py +29 -0
  68. scitex/dev/plt/demo_plotters/plot_sns_heatmap.py +28 -0
  69. scitex/dev/plt/demo_plotters/plot_sns_histplot.py +29 -0
  70. scitex/dev/plt/demo_plotters/plot_sns_kdeplot.py +29 -0
  71. scitex/dev/plt/demo_plotters/plot_sns_lineplot.py +31 -0
  72. scitex/dev/plt/demo_plotters/plot_sns_scatterplot.py +29 -0
  73. scitex/dev/plt/demo_plotters/plot_sns_stripplot.py +29 -0
  74. scitex/dev/plt/demo_plotters/plot_sns_swarmplot.py +29 -0
  75. scitex/dev/plt/demo_plotters/plot_sns_violinplot.py +29 -0
  76. scitex/dev/plt/demo_plotters/plot_stx_bar.py +29 -0
  77. scitex/dev/plt/demo_plotters/plot_stx_barh.py +29 -0
  78. scitex/dev/plt/demo_plotters/plot_stx_box.py +28 -0
  79. scitex/dev/plt/demo_plotters/plot_stx_boxplot.py +28 -0
  80. scitex/dev/plt/demo_plotters/plot_stx_conf_mat.py +28 -0
  81. scitex/dev/plt/demo_plotters/plot_stx_contour.py +31 -0
  82. scitex/dev/plt/demo_plotters/plot_stx_ecdf.py +28 -0
  83. scitex/dev/plt/demo_plotters/plot_stx_errorbar.py +30 -0
  84. scitex/dev/plt/demo_plotters/plot_stx_fill_between.py +31 -0
  85. scitex/dev/plt/demo_plotters/plot_stx_fillv.py +28 -0
  86. scitex/dev/plt/demo_plotters/plot_stx_heatmap.py +28 -0
  87. scitex/dev/plt/demo_plotters/plot_stx_image.py +28 -0
  88. scitex/dev/plt/demo_plotters/plot_stx_imshow.py +28 -0
  89. scitex/dev/plt/demo_plotters/plot_stx_joyplot.py +28 -0
  90. scitex/dev/plt/demo_plotters/plot_stx_kde.py +28 -0
  91. scitex/dev/plt/demo_plotters/plot_stx_line.py +28 -0
  92. scitex/dev/plt/demo_plotters/plot_stx_mean_ci.py +28 -0
  93. scitex/dev/plt/demo_plotters/plot_stx_mean_std.py +28 -0
  94. scitex/dev/plt/demo_plotters/plot_stx_median_iqr.py +28 -0
  95. scitex/dev/plt/demo_plotters/plot_stx_raster.py +28 -0
  96. scitex/dev/plt/demo_plotters/plot_stx_rectangle.py +28 -0
  97. scitex/dev/plt/demo_plotters/plot_stx_scatter.py +29 -0
  98. scitex/dev/plt/demo_plotters/plot_stx_shaded_line.py +29 -0
  99. scitex/dev/plt/demo_plotters/plot_stx_violin.py +28 -0
  100. scitex/dev/plt/demo_plotters/plot_stx_violinplot.py +28 -0
  101. scitex/dev/plt/mpl/get_dir_ax.py +46 -0
  102. scitex/dev/plt/mpl/get_signatures.py +176 -0
  103. scitex/dev/plt/mpl/get_signatures_details.py +522 -0
  104. scitex/dict/_pop_keys.py +1 -7
  105. scitex/dsp/__init__.py +15 -10
  106. scitex/dsp/add_noise.py +5 -2
  107. scitex/dsp/example.py +35 -22
  108. scitex/dsp/filt.py +8 -3
  109. scitex/dsp/reference.py +3 -2
  110. scitex/dsp/utils/__init__.py +2 -1
  111. scitex/dsp/utils/_differential_bandpass_filters.py +14 -4
  112. scitex/dt/__init__.py +39 -2
  113. scitex/errors.py +82 -521
  114. scitex/fig/__init__.py +4 -4
  115. scitex/fig/editor/edit/panel_loader.py +1 -1
  116. scitex/fig/io/_bundle.py +7 -7
  117. scitex/fts/README.md +262 -0
  118. scitex/fts/TODO.md +66 -0
  119. scitex/fts/__init__.py +90 -0
  120. scitex/fts/_bundle/README_IN_BUNDLE.md +102 -0
  121. scitex/fts/_bundle/_FTS.py +657 -0
  122. scitex/fts/_bundle/__init__.py +38 -0
  123. scitex/fts/_bundle/_children.py +216 -0
  124. scitex/fts/_bundle/_conversion/__init__.py +15 -0
  125. scitex/fts/_bundle/_conversion/_bundle2dict.py +44 -0
  126. scitex/fts/_bundle/_conversion/_dict2bundle.py +50 -0
  127. scitex/fts/_bundle/_dataclasses/_Axes.py +57 -0
  128. scitex/fts/_bundle/_dataclasses/_BBox.py +54 -0
  129. scitex/fts/_bundle/_dataclasses/_ColumnDef.py +72 -0
  130. scitex/fts/_bundle/_dataclasses/_DataFormat.py +40 -0
  131. scitex/fts/_bundle/_dataclasses/_DataInfo.py +135 -0
  132. scitex/fts/_bundle/_dataclasses/_DataSource.py +44 -0
  133. scitex/fts/_bundle/_dataclasses/_Node.py +319 -0
  134. scitex/fts/_bundle/_dataclasses/_NodeRefs.py +45 -0
  135. scitex/fts/_bundle/_dataclasses/_SizeMM.py +38 -0
  136. scitex/fts/_bundle/_dataclasses/__init__.py +35 -0
  137. scitex/fts/_bundle/_extractors/__init__.py +32 -0
  138. scitex/fts/_bundle/_extractors/_extract_bar.py +131 -0
  139. scitex/fts/_bundle/_extractors/_extract_line.py +71 -0
  140. scitex/fts/_bundle/_extractors/_extract_scatter.py +79 -0
  141. scitex/fts/_bundle/_loader.py +134 -0
  142. scitex/fts/_bundle/_mpl_helpers.py +389 -0
  143. scitex/fts/_bundle/_saver.py +269 -0
  144. scitex/fts/_bundle/_storage.py +200 -0
  145. scitex/fts/_bundle/_utils/__init__.py +55 -0
  146. scitex/fts/_bundle/_utils/_const.py +26 -0
  147. scitex/fts/_bundle/_utils/_errors.py +73 -0
  148. scitex/fts/_bundle/_utils/_generate.py +21 -0
  149. scitex/fts/_bundle/_utils/_types.py +76 -0
  150. scitex/fts/_bundle/_validation.py +434 -0
  151. scitex/fts/_bundle/_zipbundle.py +165 -0
  152. scitex/fts/_fig/__init__.py +22 -0
  153. scitex/fts/_fig/_backend/__init__.py +53 -0
  154. scitex/fts/_fig/_backend/_export.py +165 -0
  155. scitex/fts/_fig/_backend/_parser.py +188 -0
  156. scitex/fts/_fig/_backend/_render.py +538 -0
  157. scitex/fts/_fig/_composite.py +345 -0
  158. scitex/fts/_fig/_dataclasses/_ChannelEncoding.py +46 -0
  159. scitex/fts/_fig/_dataclasses/_Encoding.py +82 -0
  160. scitex/fts/_fig/_dataclasses/_Theme.py +441 -0
  161. scitex/fts/_fig/_dataclasses/_TraceEncoding.py +52 -0
  162. scitex/fts/_fig/_dataclasses/__init__.py +47 -0
  163. scitex/fts/_fig/_editor/__init__.py +14 -0
  164. scitex/fts/_fig/_editor/_cui/__init__.py +33 -0
  165. scitex/fts/_fig/_editor/_cui/_backend_detector.py +39 -0
  166. scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +366 -0
  167. scitex/fts/_fig/_editor/_cui/_editor_launcher.py +175 -0
  168. scitex/fts/_fig/_editor/_cui/_manual_handler.py +52 -0
  169. scitex/fts/_fig/_editor/_cui/_panel_loader.py +246 -0
  170. scitex/fts/_fig/_editor/_cui/_path_resolver.py +66 -0
  171. scitex/fts/_fig/_editor/_defaults.py +300 -0
  172. scitex/fts/_fig/_editor/_gui/__init__.py +11 -0
  173. scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +20 -0
  174. scitex/fts/_fig/_editor/_gui/_flask_editor/_bbox.py +1339 -0
  175. scitex/fts/_fig/_editor/_gui/_flask_editor/_core.py +1688 -0
  176. scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +664 -0
  177. scitex/fts/_fig/_editor/_gui/_flask_editor/_renderer.py +853 -0
  178. scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +79 -0
  179. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +41 -0
  180. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +16 -0
  181. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +85 -0
  182. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +217 -0
  183. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +93 -0
  184. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +57 -0
  185. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +112 -0
  186. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +59 -0
  187. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +212 -0
  188. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/canvas.css +176 -0
  189. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +190 -0
  190. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +59 -0
  191. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +45 -0
  192. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +95 -0
  193. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +101 -0
  194. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +138 -0
  195. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +31 -0
  196. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +7 -0
  197. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +56 -0
  198. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +78 -0
  199. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +314 -0
  200. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +107 -0
  201. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +54 -0
  202. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +172 -0
  203. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +258 -0
  204. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +48 -0
  205. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +71 -0
  206. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +288 -0
  207. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +143 -0
  208. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +245 -0
  209. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +992 -0
  210. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +339 -0
  211. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +286 -0
  212. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +371 -0
  213. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +293 -0
  214. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +426 -0
  215. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +152 -0
  216. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +265 -0
  217. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +184 -0
  218. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +57 -0
  219. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +100 -0
  220. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +34 -0
  221. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +124 -0
  222. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +851 -0
  223. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +4932 -0
  224. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +1657 -0
  225. scitex/fts/_fig/_editor/_gui/_flask_editor.py +36 -0
  226. scitex/fts/_fig/_models/_Annotations.py +115 -0
  227. scitex/fts/_fig/_models/_Axes.py +152 -0
  228. scitex/fts/_fig/_models/_Figure.py +138 -0
  229. scitex/fts/_fig/_models/_Guides.py +104 -0
  230. scitex/fts/_fig/_models/_Plot.py +123 -0
  231. scitex/fts/_fig/_models/_Styles.py +245 -0
  232. scitex/fts/_fig/_models/__init__.py +80 -0
  233. scitex/fts/_fig/_models/_plot_types/__init__.py +156 -0
  234. scitex/fts/_fig/_models/_plot_types/_bar.py +43 -0
  235. scitex/fts/_fig/_models/_plot_types/_box.py +38 -0
  236. scitex/fts/_fig/_models/_plot_types/_distribution.py +36 -0
  237. scitex/fts/_fig/_models/_plot_types/_errorbar.py +60 -0
  238. scitex/fts/_fig/_models/_plot_types/_histogram.py +30 -0
  239. scitex/fts/_fig/_models/_plot_types/_image.py +61 -0
  240. scitex/fts/_fig/_models/_plot_types/_line.py +57 -0
  241. scitex/fts/_fig/_models/_plot_types/_scatter.py +30 -0
  242. scitex/fts/_fig/_models/_plot_types/_seaborn.py +121 -0
  243. scitex/fts/_fig/_models/_plot_types/_violin.py +36 -0
  244. scitex/fts/_fig/_utils/__init__.py +129 -0
  245. scitex/fts/_fig/_utils/_auto_layout.py +127 -0
  246. scitex/fts/_fig/_utils/_calc_bounds.py +111 -0
  247. scitex/fts/_fig/_utils/_const_sizes.py +48 -0
  248. scitex/fts/_fig/_utils/_convert_coords.py +77 -0
  249. scitex/fts/_fig/_utils/_get_template.py +178 -0
  250. scitex/fts/_fig/_utils/_normalize.py +73 -0
  251. scitex/fts/_fig/_utils/_plot_layout.py +397 -0
  252. scitex/fts/_fig/_utils/_validate.py +197 -0
  253. scitex/fts/_kinds/__init__.py +45 -0
  254. scitex/fts/_kinds/_figure/__init__.py +19 -0
  255. scitex/fts/_kinds/_figure/_composite.py +345 -0
  256. scitex/fts/_kinds/_plot/__init__.py +25 -0
  257. scitex/fts/_kinds/_plot/_backend/__init__.py +53 -0
  258. scitex/fts/_kinds/_plot/_backend/_export.py +165 -0
  259. scitex/fts/_kinds/_plot/_backend/_parser.py +188 -0
  260. scitex/fts/_kinds/_plot/_backend/_render.py +538 -0
  261. scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +46 -0
  262. scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +82 -0
  263. scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +441 -0
  264. scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +52 -0
  265. scitex/fts/_kinds/_plot/_dataclasses/__init__.py +47 -0
  266. scitex/fts/_kinds/_plot/_models/_Annotations.py +115 -0
  267. scitex/fts/_kinds/_plot/_models/_Axes.py +152 -0
  268. scitex/fts/_kinds/_plot/_models/_Figure.py +138 -0
  269. scitex/fts/_kinds/_plot/_models/_Guides.py +104 -0
  270. scitex/fts/_kinds/_plot/_models/_Plot.py +123 -0
  271. scitex/fts/_kinds/_plot/_models/_Styles.py +245 -0
  272. scitex/fts/_kinds/_plot/_models/__init__.py +80 -0
  273. scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +156 -0
  274. scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +43 -0
  275. scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +38 -0
  276. scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +36 -0
  277. scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +60 -0
  278. scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +30 -0
  279. scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +61 -0
  280. scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +57 -0
  281. scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +30 -0
  282. scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +121 -0
  283. scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +36 -0
  284. scitex/fts/_kinds/_plot/_utils/__init__.py +129 -0
  285. scitex/fts/_kinds/_plot/_utils/_auto_layout.py +127 -0
  286. scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +111 -0
  287. scitex/fts/_kinds/_plot/_utils/_const_sizes.py +48 -0
  288. scitex/fts/_kinds/_plot/_utils/_convert_coords.py +77 -0
  289. scitex/fts/_kinds/_plot/_utils/_get_template.py +178 -0
  290. scitex/fts/_kinds/_plot/_utils/_normalize.py +73 -0
  291. scitex/fts/_kinds/_plot/_utils/_plot_layout.py +397 -0
  292. scitex/fts/_kinds/_plot/_utils/_validate.py +197 -0
  293. scitex/fts/_kinds/_shape/__init__.py +141 -0
  294. scitex/fts/_kinds/_stats/__init__.py +56 -0
  295. scitex/fts/_kinds/_stats/_dataclasses/_Stats.py +423 -0
  296. scitex/fts/_kinds/_stats/_dataclasses/__init__.py +48 -0
  297. scitex/fts/_kinds/_table/__init__.py +72 -0
  298. scitex/fts/_kinds/_table/_latex/__init__.py +93 -0
  299. scitex/fts/_kinds/_table/_latex/_editor/__init__.py +11 -0
  300. scitex/fts/_kinds/_table/_latex/_editor/_app.py +725 -0
  301. scitex/fts/_kinds/_table/_latex/_export.py +279 -0
  302. scitex/fts/_kinds/_table/_latex/_figure_exporter.py +153 -0
  303. scitex/fts/_kinds/_table/_latex/_stats_formatter.py +274 -0
  304. scitex/fts/_kinds/_table/_latex/_table_exporter.py +362 -0
  305. scitex/fts/_kinds/_table/_latex/_utils.py +369 -0
  306. scitex/fts/_kinds/_table/_latex/_validator.py +445 -0
  307. scitex/fts/_kinds/_text/__init__.py +77 -0
  308. scitex/fts/_schemas/data_info.schema.json +75 -0
  309. scitex/fts/_schemas/encoding.schema.json +90 -0
  310. scitex/fts/_schemas/node.schema.json +145 -0
  311. scitex/fts/_schemas/render_manifest.schema.json +62 -0
  312. scitex/fts/_schemas/stats.schema.json +132 -0
  313. scitex/fts/_schemas/theme.schema.json +141 -0
  314. scitex/fts/_stats/__init__.py +48 -0
  315. scitex/fts/_stats/_dataclasses/_Stats.py +423 -0
  316. scitex/fts/_stats/_dataclasses/__init__.py +48 -0
  317. scitex/fts/_tables/__init__.py +65 -0
  318. scitex/fts/_tables/_latex/__init__.py +93 -0
  319. scitex/fts/_tables/_latex/_editor/__init__.py +11 -0
  320. scitex/fts/_tables/_latex/_editor/_app.py +725 -0
  321. scitex/fts/_tables/_latex/_export.py +279 -0
  322. scitex/fts/_tables/_latex/_figure_exporter.py +153 -0
  323. scitex/fts/_tables/_latex/_stats_formatter.py +274 -0
  324. scitex/fts/_tables/_latex/_table_exporter.py +362 -0
  325. scitex/fts/_tables/_latex/_utils.py +369 -0
  326. scitex/fts/_tables/_latex/_validator.py +445 -0
  327. scitex/gen/__init__.py +66 -25
  328. scitex/gen/misc.py +28 -0
  329. scitex/io/__init__.py +47 -32
  330. scitex/io/_load.py +87 -36
  331. scitex/io/_load_modules/__init__.py +10 -7
  332. scitex/io/_load_modules/_pandas.py +6 -1
  333. scitex/io/_save.py +299 -1556
  334. scitex/io/_save_modules/__init__.py +76 -19
  335. scitex/io/_save_modules/_figure_utils.py +90 -0
  336. scitex/io/_save_modules/_image_csv.py +497 -0
  337. scitex/io/_save_modules/_legends.py +91 -0
  338. scitex/io/_save_modules/_pltz_bundle.py +356 -0
  339. scitex/io/_save_modules/_pltz_stx.py +536 -0
  340. scitex/io/_save_modules/_stx_bundle.py +104 -0
  341. scitex/io/_save_modules/_symlink.py +96 -0
  342. scitex/io/_save_modules/_yaml.py +1 -1
  343. scitex/io/_save_modules/_zarr.py +64 -18
  344. scitex/io/bundle/README.md +212 -0
  345. scitex/io/bundle/__init__.py +110 -0
  346. scitex/io/{_bundle.py → bundle/_core.py} +168 -97
  347. scitex/io/bundle/_nested.py +713 -0
  348. scitex/io/bundle/_types.py +74 -0
  349. scitex/io/{_zip_bundle.py → bundle/_zip.py} +93 -45
  350. scitex/io/utils/h5_to_zarr.py +1 -1
  351. scitex/logging/__init__.py +108 -13
  352. scitex/logging/_errors.py +508 -0
  353. scitex/logging/_formatters.py +30 -6
  354. scitex/logging/_warnings.py +261 -0
  355. scitex/plt/__init__.py +4 -1
  356. scitex/plt/_figrecipe.py +236 -0
  357. scitex/plt/_subplots/_AxisWrapper.py +6 -0
  358. scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +112 -1
  359. scitex/plt/_subplots/_FigWrapper.py +15 -0
  360. scitex/plt/_subplots/_SubplotsWrapper.py +125 -489
  361. scitex/plt/_subplots/_export_as_csv.py +11 -0
  362. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +2 -0
  363. scitex/plt/_subplots/_export_as_csv_formatters/_format_pcolormesh.py +66 -0
  364. scitex/plt/_subplots/_export_as_csv_formatters/_format_stackplot.py +62 -0
  365. scitex/plt/_subplots/_export_as_csv_formatters/test_formatters.py +208 -0
  366. scitex/plt/_subplots/_fonts.py +71 -0
  367. scitex/plt/_subplots/_mm_layout.py +282 -0
  368. scitex/plt/gallery/__init__.py +99 -2
  369. scitex/plt/styles/_plot_postprocess.py +3 -1
  370. scitex/plt/utils/_configure_mpl.py +16 -19
  371. scitex/repro/_RandomStateManager.py +13 -8
  372. scitex/resource/__init__.py +19 -1
  373. scitex/resource/_utils/_get_env_info.py +13 -25
  374. scitex/schema/__init__.py +149 -160
  375. scitex/schema/_encoding.py +273 -0
  376. scitex/schema/_figure_elements.py +406 -0
  377. scitex/schema/_theme.py +360 -0
  378. scitex/schema/_validation.py +0 -98
  379. scitex/scholar/__init__.py +56 -14
  380. scitex/scholar/auth/ScholarAuthManager.py +1 -1
  381. scitex/scholar/auth/__init__.py +11 -2
  382. scitex/scholar/auth/providers/BaseAuthenticator.py +1 -1
  383. scitex/scholar/auth/providers/EZProxyAuthenticator.py +1 -1
  384. scitex/scholar/auth/providers/OpenAthensAuthenticator.py +1 -1
  385. scitex/scholar/auth/providers/ShibbolethAuthenticator.py +1 -1
  386. scitex/scholar/config/ScholarConfig.py +1 -1
  387. scitex/scholar/core/Scholar.py +1 -1
  388. scitex/session/_decorator.py +18 -16
  389. scitex/session/_lifecycle.py +9 -11
  390. scitex/session/template.py +9 -8
  391. scitex/sh/test_sh.py +72 -0
  392. scitex/sh/test_sh_simple.py +61 -0
  393. scitex/stats/__init__.py +221 -97
  394. scitex/stats/_schema.py +21 -22
  395. scitex/stats/descriptive/_circular.py +212 -351
  396. scitex/stats/descriptive/_describe.py +81 -132
  397. scitex/stats/descriptive/_nan.py +205 -433
  398. scitex/stats/descriptive/_real.py +127 -141
  399. scitex/str/_format_plot_text.py +5 -5
  400. scitex/str/_latex.py +26 -84
  401. scitex/str/_latex_fallback.py +53 -47
  402. scitex/web/_search_pubmed.py +5 -4
  403. scitex/writer/tests/test_diff_between.py +451 -0
  404. scitex/writer/tests/test_document_section.py +311 -0
  405. scitex/writer/tests/test_document_workflow.py +393 -0
  406. scitex/writer/tests/test_writer.py +361 -0
  407. scitex/writer/tests/test_writer_integration.py +303 -0
  408. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/METADATA +364 -181
  409. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/RECORD +412 -97
  410. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/ARCHITECTURE_EXAMPLE.md +0 -905
  411. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/BULLETIN_BOARD_EXAMPLE.md +0 -99
  412. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/PROJECT_DESCRIPTION_EXAMPLE.md +0 -96
  413. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/WHEEL +0 -0
  414. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/entry_points.txt +0 -0
  415. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,13 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # Timestamp: "2025-12-11 (ywatanabe)"
2
+ # Timestamp: "2025-12-27 (ywatanabe)"
4
3
  # File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/mcp_server.py
5
4
  # ----------------------------------------
6
5
 
7
6
  """
8
7
  MCP Server for SciTeX Audio - Text-to-Speech with Multiple Backends
9
8
 
10
- Fallback order: pyttsx3 -> gtts -> elevenlabs
11
-
12
- Backends:
13
- - pyttsx3: System TTS (offline, free)
14
- - gtts: Google TTS (free, requires internet)
15
- - elevenlabs: ElevenLabs (paid, high quality)
9
+ Uses cross-process FIFO locking to ensure only one instance
10
+ plays audio at a time across all Claude Code sessions.
16
11
  """
17
12
 
18
13
  from __future__ import annotations
@@ -24,7 +19,6 @@ import uuid
24
19
  from dataclasses import dataclass, field
25
20
  from datetime import datetime
26
21
  from pathlib import Path
27
- from typing import Any, Optional
28
22
 
29
23
  import mcp.types as types
30
24
  from mcp.server import NotificationOptions, Server
@@ -40,16 +34,17 @@ class SpeechRequest:
40
34
 
41
35
  request_id: str
42
36
  text: str
43
- backend: Optional[str] = None
44
- voice: Optional[str] = None
45
- rate: Optional[int] = None
46
- speed: Optional[float] = None
37
+ backend: str | None = None
38
+ voice: str | None = None
39
+ rate: int | None = None
40
+ speed: float | None = None
47
41
  play: bool = True
48
42
  save: bool = False
49
43
  fallback: bool = True
50
44
  future: asyncio.Future = field(default_factory=lambda: None)
51
45
  created_at: datetime = field(default_factory=datetime.now)
52
- agent_id: Optional[str] = None # Track which agent made the request
46
+ agent_id: str | None = None
47
+
53
48
 
54
49
  # Directory configuration
55
50
  SCITEX_BASE_DIR = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
@@ -58,24 +53,18 @@ SCITEX_AUDIO_DIR = SCITEX_BASE_DIR / "audio"
58
53
 
59
54
  def get_audio_dir() -> Path:
60
55
  """Get the audio output directory."""
61
- audio_dir = SCITEX_AUDIO_DIR
62
- audio_dir.mkdir(parents=True, exist_ok=True)
63
- return audio_dir
56
+ SCITEX_AUDIO_DIR.mkdir(parents=True, exist_ok=True)
57
+ return SCITEX_AUDIO_DIR
64
58
 
65
59
 
66
60
  class AudioServer:
67
- """MCP Server for Text-to-Speech with multiple backends.
68
-
69
- Features a sequential speech queue to prevent audio overlap when
70
- multiple agents request speech simultaneously.
71
- """
61
+ """MCP Server for Text-to-Speech with cross-process FIFO queuing."""
72
62
 
73
63
  def __init__(self):
74
64
  self.server = Server("scitex-audio")
75
- # Speech queue for sequential processing
76
65
  self._speech_queue: asyncio.Queue[SpeechRequest] = asyncio.Queue()
77
- self._queue_processor_task: Optional[asyncio.Task] = None
78
- self._current_request: Optional[SpeechRequest] = None
66
+ self._queue_processor_task: asyncio.Task | None = None
67
+ self._current_request: SpeechRequest | None = None
79
68
  self._processed_count: int = 0
80
69
  self._is_processing: bool = False
81
70
  self.setup_handlers()
@@ -91,19 +80,15 @@ class AudioServer:
91
80
  """Process speech requests sequentially from the queue."""
92
81
  while True:
93
82
  try:
94
- # Wait for next request
95
83
  request = await self._speech_queue.get()
96
84
  self._current_request = request
97
85
  self._is_processing = True
98
86
 
99
87
  try:
100
- # Execute the speech request
101
88
  result = await self._execute_speak(request)
102
- # Set the result on the future if it exists
103
89
  if request.future and not request.future.done():
104
90
  request.future.set_result(result)
105
91
  except Exception as e:
106
- # Set exception on future if it exists
107
92
  if request.future and not request.future.done():
108
93
  request.future.set_exception(e)
109
94
  finally:
@@ -115,35 +100,40 @@ class AudioServer:
115
100
  except asyncio.CancelledError:
116
101
  break
117
102
  except Exception:
118
- # Continue processing even if one request fails
119
103
  continue
120
104
 
121
105
  async def _execute_speak(self, request: SpeechRequest) -> dict:
122
- """Execute a single speech request."""
106
+ """Execute a single speech request with cross-process locking."""
123
107
  from . import available_backends
124
108
  from . import speak as tts_speak
109
+ from ._cross_process_lock import AudioPlaybackLock
125
110
 
126
111
  loop = asyncio.get_event_loop()
127
112
 
128
- # Determine output path
129
113
  output_path = None
130
114
  if request.save:
131
115
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
132
116
  output_path = str(get_audio_dir() / f"tts_{timestamp}.mp3")
133
117
 
134
- def do_speak():
135
- return tts_speak(
136
- text=request.text,
137
- backend=request.backend,
138
- voice=request.voice,
139
- play=request.play,
140
- output_path=output_path,
141
- fallback=request.fallback,
142
- rate=request.rate,
143
- speed=request.speed,
144
- )
118
+ def do_speak_with_lock():
119
+ # Acquire cross-process lock to ensure FIFO across all instances
120
+ lock = AudioPlaybackLock()
121
+ lock.acquire(timeout=120.0)
122
+ try:
123
+ return tts_speak(
124
+ text=request.text,
125
+ backend=request.backend,
126
+ voice=request.voice,
127
+ play=request.play,
128
+ output_path=output_path,
129
+ fallback=request.fallback,
130
+ rate=request.rate,
131
+ speed=request.speed,
132
+ )
133
+ finally:
134
+ lock.release()
145
135
 
146
- result_path = await loop.run_in_executor(None, do_speak)
136
+ await loop.run_in_executor(None, do_speak_with_lock)
147
137
 
148
138
  backends = available_backends()
149
139
  used_backend = request.backend or (backends[0] if backends else None)
@@ -167,199 +157,48 @@ class AudioServer:
167
157
  return response
168
158
 
169
159
  def setup_handlers(self):
160
+ """Set up MCP server handlers."""
161
+ from ._mcp_handlers import (
162
+ check_audio_status_handler,
163
+ clear_audio_cache_handler,
164
+ generate_audio_handler,
165
+ list_audio_files_handler,
166
+ list_backends_handler,
167
+ list_voices_handler,
168
+ play_audio_handler,
169
+ )
170
+ from ._mcp_tool_schemas import get_tool_schemas
171
+
170
172
  @self.server.list_tools()
171
173
  async def handle_list_tools():
172
- return [
173
- types.Tool(
174
- name="speak",
175
- description="Convert text to speech with fallback (pyttsx3 -> gtts -> elevenlabs). Requests are queued for sequential playback to prevent audio overlap.",
176
- inputSchema={
177
- "type": "object",
178
- "properties": {
179
- "text": {
180
- "type": "string",
181
- "description": "Text to convert to speech",
182
- },
183
- "backend": {
184
- "type": "string",
185
- "description": "TTS backend (auto-selects with fallback if not specified)",
186
- "enum": ["pyttsx3", "gtts", "elevenlabs"],
187
- },
188
- "voice": {
189
- "type": "string",
190
- "description": "Voice/language (gtts: 'en','fr'; elevenlabs: 'rachel','adam')",
191
- },
192
- "rate": {
193
- "type": "integer",
194
- "description": "Speech rate in words per minute (pyttsx3 only, default 150, faster=200+)",
195
- "default": 150,
196
- },
197
- "speed": {
198
- "type": "number",
199
- "description": "Speed multiplier for gtts (1.0=normal, 1.5=faster, 0.7=slower)",
200
- "default": 1.5,
201
- },
202
- "play": {
203
- "type": "boolean",
204
- "description": "Play audio after generation",
205
- "default": True,
206
- },
207
- "save": {
208
- "type": "boolean",
209
- "description": "Save audio to file",
210
- "default": False,
211
- },
212
- "fallback": {
213
- "type": "boolean",
214
- "description": "Try next backend on failure",
215
- "default": True,
216
- },
217
- "agent_id": {
218
- "type": "string",
219
- "description": "Optional identifier for the agent making the request",
220
- },
221
- "wait": {
222
- "type": "boolean",
223
- "description": "Wait for speech to complete before returning (default: True)",
224
- "default": True,
225
- },
226
- },
227
- "required": ["text"],
228
- },
229
- ),
230
- types.Tool(
231
- name="generate_audio",
232
- description="Generate speech audio file without playing",
233
- inputSchema={
234
- "type": "object",
235
- "properties": {
236
- "text": {
237
- "type": "string",
238
- "description": "Text to convert to speech",
239
- },
240
- "backend": {
241
- "type": "string",
242
- "description": "TTS backend",
243
- "enum": ["gtts", "elevenlabs", "pyttsx3"],
244
- "default": "gtts",
245
- },
246
- "voice": {
247
- "type": "string",
248
- "description": "Voice/language",
249
- },
250
- "output_path": {
251
- "type": "string",
252
- "description": "Output file path",
253
- },
254
- "return_base64": {
255
- "type": "boolean",
256
- "description": "Return audio as base64",
257
- "default": False,
258
- },
259
- },
260
- "required": ["text"],
261
- },
262
- ),
263
- types.Tool(
264
- name="list_backends",
265
- description="List available TTS backends and their status",
266
- inputSchema={"type": "object", "properties": {}},
267
- ),
268
- types.Tool(
269
- name="list_voices",
270
- description="List available voices for a backend",
271
- inputSchema={
272
- "type": "object",
273
- "properties": {
274
- "backend": {
275
- "type": "string",
276
- "description": "TTS backend",
277
- "enum": ["gtts", "elevenlabs", "pyttsx3"],
278
- "default": "gtts",
279
- },
280
- },
281
- },
282
- ),
283
- types.Tool(
284
- name="play_audio",
285
- description="Play an audio file",
286
- inputSchema={
287
- "type": "object",
288
- "properties": {
289
- "path": {
290
- "type": "string",
291
- "description": "Path to audio file",
292
- },
293
- },
294
- "required": ["path"],
295
- },
296
- ),
297
- types.Tool(
298
- name="list_audio_files",
299
- description="List generated audio files",
300
- inputSchema={
301
- "type": "object",
302
- "properties": {
303
- "limit": {
304
- "type": "integer",
305
- "description": "Maximum files to list",
306
- "default": 20,
307
- },
308
- },
309
- },
310
- ),
311
- types.Tool(
312
- name="clear_audio_cache",
313
- description="Clear generated audio files",
314
- inputSchema={
315
- "type": "object",
316
- "properties": {
317
- "max_age_hours": {
318
- "type": "number",
319
- "description": "Delete files older than N hours (0 = all)",
320
- "default": 24,
321
- },
322
- },
323
- },
324
- ),
325
- types.Tool(
326
- name="speech_queue_status",
327
- description="Get the current speech queue status (pending requests, currently playing, etc.)",
328
- inputSchema={"type": "object", "properties": {}},
329
- ),
330
- types.Tool(
331
- name="check_audio_status",
332
- description="Check WSL audio connectivity and available playback methods",
333
- inputSchema={"type": "object", "properties": {}},
334
- ),
335
- ]
174
+ return get_tool_schemas()
336
175
 
337
176
  @self.server.call_tool()
338
177
  async def handle_call_tool(name: str, arguments: dict):
339
- # Ensure queue processor is running for speak requests
340
178
  if name == "speak":
341
179
  await self.start_queue_processor()
342
180
  return await self.speak(**arguments)
343
181
  elif name == "generate_audio":
344
- return await self.generate_audio(**arguments)
182
+ return await generate_audio_handler(**arguments)
345
183
  elif name == "list_backends":
346
- return await self.list_backends()
184
+ return await list_backends_handler()
347
185
  elif name == "list_voices":
348
- return await self.list_voices(**arguments)
186
+ return await list_voices_handler(**arguments)
349
187
  elif name == "play_audio":
350
- return await self.play_audio(**arguments)
188
+ return await play_audio_handler(**arguments)
351
189
  elif name == "list_audio_files":
352
- return await self.list_audio_files(**arguments)
190
+ return await list_audio_files_handler(**arguments)
353
191
  elif name == "clear_audio_cache":
354
- return await self.clear_audio_cache(**arguments)
192
+ return await clear_audio_cache_handler(**arguments)
355
193
  elif name == "speech_queue_status":
356
194
  return await self.speech_queue_status()
357
195
  elif name == "check_audio_status":
358
- return await self.check_audio_status()
196
+ return await check_audio_status_handler()
197
+ elif name == "announce_context":
198
+ return await self.announce_context(**arguments)
359
199
  else:
360
200
  raise ValueError(f"Unknown tool: {name}")
361
201
 
362
- # Provide audio files as resources
363
202
  @self.server.list_resources()
364
203
  async def handle_list_resources():
365
204
  audio_dir = get_audio_dir()
@@ -375,9 +214,7 @@ class AudioServer:
375
214
 
376
215
  for audio_file in audio_files:
377
216
  mtime = datetime.fromtimestamp(audio_file.stat().st_mtime)
378
- mime_type = (
379
- "audio/mpeg" if audio_file.suffix == ".mp3" else "audio/wav"
380
- )
217
+ mime_type = "audio/mpeg" if audio_file.suffix == ".mp3" else "audio/wav"
381
218
  resources.append(
382
219
  types.Resource(
383
220
  uri=f"audio://{audio_file.name}",
@@ -410,42 +247,22 @@ class AudioServer:
410
247
  async def speak(
411
248
  self,
412
249
  text: str,
413
- backend: Optional[str] = None,
414
- voice: Optional[str] = None,
415
- rate: Optional[int] = None,
416
- speed: Optional[float] = None,
250
+ backend: str | None = None,
251
+ voice: str | None = None,
252
+ rate: int | None = None,
253
+ speed: float | None = None,
417
254
  play: bool = True,
418
255
  save: bool = False,
419
256
  fallback: bool = True,
420
- agent_id: Optional[str] = None,
257
+ agent_id: str | None = None,
421
258
  wait: bool = True,
422
259
  ):
423
- """Convert text to speech with fallback support.
424
-
425
- Requests are queued for sequential playback to prevent audio overlap
426
- when multiple agents request speech simultaneously.
427
-
428
- Args:
429
- text: Text to convert to speech
430
- backend: TTS backend to use
431
- voice: Voice/language selection
432
- rate: Speech rate (pyttsx3 only)
433
- speed: Speed multiplier (gtts only)
434
- play: Whether to play audio after generation
435
- save: Whether to save audio to file
436
- fallback: Whether to try next backend on failure
437
- agent_id: Optional identifier for the requesting agent
438
- wait: Whether to wait for speech to complete (default: True)
439
- """
260
+ """Queue speech request with cross-process FIFO ordering."""
440
261
  try:
441
- # Create a unique request ID
442
262
  request_id = str(uuid.uuid4())[:8]
443
-
444
- # Create a future to wait for the result
445
263
  loop = asyncio.get_event_loop()
446
264
  future = loop.create_future()
447
265
 
448
- # Create the speech request
449
266
  request = SpeechRequest(
450
267
  request_id=request_id,
451
268
  text=text,
@@ -460,18 +277,14 @@ class AudioServer:
460
277
  agent_id=agent_id,
461
278
  )
462
279
 
463
- # Add to queue
464
280
  await self._speech_queue.put(request)
465
-
466
281
  queue_position = self._speech_queue.qsize()
467
282
 
468
283
  if wait:
469
- # Wait for the speech to complete
470
284
  result = await future
471
- result["queue_position"] = 0 # Already processed
285
+ result["queue_position"] = 0
472
286
  return result
473
287
  else:
474
- # Return immediately with queue info
475
288
  return {
476
289
  "success": True,
477
290
  "queued": True,
@@ -513,225 +326,52 @@ class AudioServer:
513
326
  except Exception as e:
514
327
  return {"success": False, "error": str(e)}
515
328
 
516
- async def generate_audio(
517
- self,
518
- text: str,
519
- backend: Optional[str] = None,
520
- voice: Optional[str] = None,
521
- output_path: Optional[str] = None,
522
- return_base64: bool = False,
523
- ):
524
- """Generate audio file without playing."""
525
- try:
526
- from . import speak as tts_speak, available_backends
527
-
528
- loop = asyncio.get_event_loop()
529
-
530
- # Determine output path
531
- if not output_path:
532
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
533
- output_path = str(get_audio_dir() / f"tts_{timestamp}.mp3")
534
-
535
- def do_generate():
536
- return tts_speak(
537
- text=text,
538
- backend=backend,
539
- voice=voice,
540
- play=False,
541
- output_path=output_path,
542
- fallback=True,
543
- )
544
-
545
- result_path = await loop.run_in_executor(None, do_generate)
546
-
547
- result = {
548
- "success": True,
549
- "path": str(result_path),
550
- "text": text,
551
- "backend": backend,
552
- "timestamp": datetime.now().isoformat(),
553
- }
554
-
555
- # Get file size
556
- if result_path.exists():
557
- result["size_kb"] = round(result_path.stat().st_size / 1024, 2)
558
-
559
- if return_base64 and result_path.exists():
560
- with open(result_path, "rb") as f:
561
- result["base64"] = base64.b64encode(f.read()).decode()
562
-
563
- return result
564
-
565
- except Exception as e:
566
- return {"success": False, "error": str(e)}
567
-
568
- async def list_backends(self):
569
- """List available TTS backends."""
570
- try:
571
- from . import available_backends
572
-
573
- backends = available_backends()
574
-
575
- info = []
576
- for b in ["gtts", "elevenlabs", "pyttsx3"]:
577
- available = b in backends
578
- desc = {
579
- "gtts": "Google TTS - Free, requires internet",
580
- "elevenlabs": "ElevenLabs - Paid, high quality",
581
- "pyttsx3": "System TTS - Offline, uses espeak/SAPI5",
582
- }
583
- info.append(
584
- {
585
- "name": b,
586
- "available": available,
587
- "description": desc.get(b, ""),
588
- }
589
- )
590
-
591
- return {
592
- "success": True,
593
- "backends": info,
594
- "available": backends,
595
- "default": backends[0] if backends else None,
596
- }
597
-
598
- except Exception as e:
599
- return {"success": False, "error": str(e)}
600
-
601
- async def list_voices(self, backend: str = "gtts"):
602
- """List available voices for a backend."""
603
- try:
604
- from . import get_tts
605
-
606
- loop = asyncio.get_event_loop()
607
-
608
- def do_list():
609
- tts = get_tts(backend)
610
- return tts.get_voices()
611
-
612
- voices = await loop.run_in_executor(None, do_list)
329
+ async def announce_context(self, include_full_path: bool = False):
330
+ """Announce the current working directory and git branch."""
331
+ import subprocess
613
332
 
614
- return {
615
- "success": True,
616
- "backend": backend,
617
- "voices": voices,
618
- "count": len(voices),
619
- }
620
-
621
- except Exception as e:
622
- return {"success": False, "error": str(e)}
623
-
624
- async def play_audio(self, path: str):
625
- """Play an audio file."""
626
333
  try:
627
- from ._base import BaseTTS
628
-
629
- path_obj = Path(path)
630
- if not path_obj.exists():
631
- return {"success": False, "error": f"File not found: {path}"}
334
+ # Get current working directory
335
+ cwd = Path.cwd()
336
+ dir_name = str(cwd) if include_full_path else cwd.name
632
337
 
633
- loop = asyncio.get_event_loop()
634
-
635
- def do_play():
636
- # Use the base class play method
637
- BaseTTS._play_audio(None, path_obj)
638
-
639
- await loop.run_in_executor(None, do_play)
640
-
641
- return {
642
- "success": True,
643
- "played": str(path_obj),
644
- "timestamp": datetime.now().isoformat(),
645
- }
646
-
647
- except Exception as e:
648
- return {"success": False, "error": str(e)}
649
-
650
- async def list_audio_files(self, limit: int = 20):
651
- """List generated audio files."""
652
- try:
653
- audio_dir = get_audio_dir()
654
- if not audio_dir.exists():
655
- return {"success": True, "files": [], "count": 0}
656
-
657
- audio_files = sorted(
658
- list(audio_dir.glob("*.mp3")) + list(audio_dir.glob("*.wav")),
659
- key=lambda p: p.stat().st_mtime,
660
- reverse=True,
661
- )[:limit]
662
-
663
- files = []
664
- for f in audio_files:
665
- files.append(
666
- {
667
- "name": f.name,
668
- "path": str(f),
669
- "size_kb": round(f.stat().st_size / 1024, 2),
670
- "created": datetime.fromtimestamp(
671
- f.stat().st_mtime
672
- ).isoformat(),
673
- }
338
+ # Try to get git branch
339
+ git_branch = None
340
+ try:
341
+ result = subprocess.run(
342
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
343
+ capture_output=True,
344
+ text=True,
345
+ timeout=5,
346
+ cwd=str(cwd),
674
347
  )
348
+ if result.returncode == 0:
349
+ git_branch = result.stdout.strip()
350
+ except Exception:
351
+ pass
675
352
 
676
- total_size = sum(f.stat().st_size for f in audio_dir.glob("*.*"))
677
-
678
- return {
679
- "success": True,
680
- "files": files,
681
- "count": len(files),
682
- "total_size_mb": round(total_size / (1024 * 1024), 2),
683
- "audio_dir": str(audio_dir),
684
- }
685
-
686
- except Exception as e:
687
- return {"success": False, "error": str(e)}
688
-
689
- async def clear_audio_cache(self, max_age_hours: float = 24):
690
- """Clear audio cache."""
691
- try:
692
- audio_dir = get_audio_dir()
693
- if not audio_dir.exists():
694
- return {"success": True, "deleted": 0}
695
-
696
- deleted = 0
697
- now = datetime.now()
353
+ # Build announcement text
354
+ if git_branch:
355
+ text = f"Working in {dir_name}, on branch {git_branch}"
356
+ else:
357
+ text = f"Working in {dir_name}"
698
358
 
699
- for f in list(audio_dir.glob("*.mp3")) + list(audio_dir.glob("*.wav")):
700
- try:
701
- if max_age_hours == 0:
702
- f.unlink()
703
- deleted += 1
704
- else:
705
- mtime = datetime.fromtimestamp(f.stat().st_mtime)
706
- age_hours = (now - mtime).total_seconds() / 3600
707
- if age_hours > max_age_hours:
708
- f.unlink()
709
- deleted += 1
710
- except Exception:
711
- pass
359
+ # Speak the announcement
360
+ await self.start_queue_processor()
361
+ result = await self.speak(text=text)
712
362
 
713
363
  return {
714
364
  "success": True,
715
- "deleted": deleted,
716
- "max_age_hours": max_age_hours,
365
+ "directory": str(cwd),
366
+ "directory_name": cwd.name,
367
+ "git_branch": git_branch,
368
+ "announced_text": text,
369
+ "speak_result": result,
717
370
  }
718
371
 
719
372
  except Exception as e:
720
373
  return {"success": False, "error": str(e)}
721
374
 
722
- async def check_audio_status(self):
723
- """Check WSL audio connectivity and available playback methods."""
724
- try:
725
- from . import check_wsl_audio
726
-
727
- status = check_wsl_audio()
728
- status["success"] = True
729
- status["timestamp"] = datetime.now().isoformat()
730
- return status
731
-
732
- except Exception as e:
733
- return {"success": False, "error": str(e)}
734
-
735
375
 
736
376
  async def main():
737
377
  """Main entry point for the MCP server."""
@@ -742,7 +382,7 @@ async def main():
742
382
  write_stream,
743
383
  InitializationOptions(
744
384
  server_name="scitex-audio",
745
- server_version="0.2.0",
385
+ server_version="0.3.0", # Bumped for cross-process FIFO
746
386
  capabilities=server.server.get_capabilities(
747
387
  notification_options=NotificationOptions(),
748
388
  experimental_capabilities={},
@@ -754,4 +394,5 @@ async def main():
754
394
  if __name__ == "__main__":
755
395
  asyncio.run(main())
756
396
 
397
+
757
398
  # EOF