scitex 2.7.0__py3-none-any.whl → 2.8.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 (355) hide show
  1. scitex/__init__.py +6 -2
  2. scitex/__version__.py +1 -1
  3. scitex/audio/README.md +52 -0
  4. scitex/audio/__init__.py +384 -0
  5. scitex/audio/__main__.py +129 -0
  6. scitex/audio/_tts.py +334 -0
  7. scitex/audio/engines/__init__.py +44 -0
  8. scitex/audio/engines/base.py +275 -0
  9. scitex/audio/engines/elevenlabs_engine.py +143 -0
  10. scitex/audio/engines/gtts_engine.py +162 -0
  11. scitex/audio/engines/pyttsx3_engine.py +131 -0
  12. scitex/audio/mcp_server.py +757 -0
  13. scitex/bridge/_helpers.py +1 -1
  14. scitex/bridge/_plt_vis.py +1 -1
  15. scitex/bridge/_stats_vis.py +1 -1
  16. scitex/dev/plt/__init__.py +272 -0
  17. scitex/dev/plt/plot_mpl_axhline.py +28 -0
  18. scitex/dev/plt/plot_mpl_axhspan.py +28 -0
  19. scitex/dev/plt/plot_mpl_axvline.py +28 -0
  20. scitex/dev/plt/plot_mpl_axvspan.py +28 -0
  21. scitex/dev/plt/plot_mpl_bar.py +29 -0
  22. scitex/dev/plt/plot_mpl_barh.py +29 -0
  23. scitex/dev/plt/plot_mpl_boxplot.py +28 -0
  24. scitex/dev/plt/plot_mpl_contour.py +31 -0
  25. scitex/dev/plt/plot_mpl_contourf.py +31 -0
  26. scitex/dev/plt/plot_mpl_errorbar.py +30 -0
  27. scitex/dev/plt/plot_mpl_eventplot.py +28 -0
  28. scitex/dev/plt/plot_mpl_fill.py +30 -0
  29. scitex/dev/plt/plot_mpl_fill_between.py +31 -0
  30. scitex/dev/plt/plot_mpl_hexbin.py +28 -0
  31. scitex/dev/plt/plot_mpl_hist.py +28 -0
  32. scitex/dev/plt/plot_mpl_hist2d.py +28 -0
  33. scitex/dev/plt/plot_mpl_imshow.py +29 -0
  34. scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
  35. scitex/dev/plt/plot_mpl_pie.py +29 -0
  36. scitex/dev/plt/plot_mpl_plot.py +29 -0
  37. scitex/dev/plt/plot_mpl_quiver.py +31 -0
  38. scitex/dev/plt/plot_mpl_scatter.py +28 -0
  39. scitex/dev/plt/plot_mpl_stackplot.py +31 -0
  40. scitex/dev/plt/plot_mpl_stem.py +29 -0
  41. scitex/dev/plt/plot_mpl_step.py +29 -0
  42. scitex/dev/plt/plot_mpl_violinplot.py +28 -0
  43. scitex/dev/plt/plot_sns_barplot.py +29 -0
  44. scitex/dev/plt/plot_sns_boxplot.py +29 -0
  45. scitex/dev/plt/plot_sns_heatmap.py +28 -0
  46. scitex/dev/plt/plot_sns_histplot.py +29 -0
  47. scitex/dev/plt/plot_sns_kdeplot.py +29 -0
  48. scitex/dev/plt/plot_sns_lineplot.py +31 -0
  49. scitex/dev/plt/plot_sns_scatterplot.py +29 -0
  50. scitex/dev/plt/plot_sns_stripplot.py +29 -0
  51. scitex/dev/plt/plot_sns_swarmplot.py +29 -0
  52. scitex/dev/plt/plot_sns_violinplot.py +29 -0
  53. scitex/dev/plt/plot_stx_bar.py +29 -0
  54. scitex/dev/plt/plot_stx_barh.py +29 -0
  55. scitex/dev/plt/plot_stx_box.py +28 -0
  56. scitex/dev/plt/plot_stx_boxplot.py +28 -0
  57. scitex/dev/plt/plot_stx_conf_mat.py +28 -0
  58. scitex/dev/plt/plot_stx_contour.py +31 -0
  59. scitex/dev/plt/plot_stx_ecdf.py +28 -0
  60. scitex/dev/plt/plot_stx_errorbar.py +30 -0
  61. scitex/dev/plt/plot_stx_fill_between.py +31 -0
  62. scitex/dev/plt/plot_stx_fillv.py +28 -0
  63. scitex/dev/plt/plot_stx_heatmap.py +28 -0
  64. scitex/dev/plt/plot_stx_image.py +28 -0
  65. scitex/dev/plt/plot_stx_imshow.py +28 -0
  66. scitex/dev/plt/plot_stx_joyplot.py +28 -0
  67. scitex/dev/plt/plot_stx_kde.py +28 -0
  68. scitex/dev/plt/plot_stx_line.py +28 -0
  69. scitex/dev/plt/plot_stx_mean_ci.py +28 -0
  70. scitex/dev/plt/plot_stx_mean_std.py +28 -0
  71. scitex/dev/plt/plot_stx_median_iqr.py +28 -0
  72. scitex/dev/plt/plot_stx_raster.py +28 -0
  73. scitex/dev/plt/plot_stx_rectangle.py +28 -0
  74. scitex/dev/plt/plot_stx_scatter.py +29 -0
  75. scitex/dev/plt/plot_stx_shaded_line.py +29 -0
  76. scitex/dev/plt/plot_stx_violin.py +28 -0
  77. scitex/dev/plt/plot_stx_violinplot.py +28 -0
  78. scitex/diagram/README.md +197 -0
  79. scitex/diagram/__init__.py +48 -0
  80. scitex/diagram/_compile.py +312 -0
  81. scitex/diagram/_diagram.py +355 -0
  82. scitex/diagram/_presets.py +173 -0
  83. scitex/diagram/_schema.py +182 -0
  84. scitex/diagram/_split.py +278 -0
  85. scitex/fig/__init__.py +352 -0
  86. scitex/{vis → fig}/backend/_parser.py +1 -1
  87. scitex/{vis → fig}/canvas.py +1 -1
  88. scitex/{vis → fig}/editor/__init__.py +5 -2
  89. scitex/{vis → fig}/editor/_dearpygui_editor.py +1 -1
  90. scitex/{vis → fig}/editor/_defaults.py +70 -5
  91. scitex/{vis → fig}/editor/_mpl_editor.py +1 -1
  92. scitex/{vis → fig}/editor/_qt_editor.py +182 -2
  93. scitex/{vis → fig}/editor/_tkinter_editor.py +1 -1
  94. scitex/fig/editor/edit/__init__.py +50 -0
  95. scitex/fig/editor/edit/backend_detector.py +109 -0
  96. scitex/fig/editor/edit/bundle_resolver.py +240 -0
  97. scitex/fig/editor/edit/editor_launcher.py +239 -0
  98. scitex/fig/editor/edit/manual_handler.py +53 -0
  99. scitex/fig/editor/edit/panel_loader.py +232 -0
  100. scitex/fig/editor/edit/path_resolver.py +67 -0
  101. scitex/fig/editor/flask_editor/_bbox.py +1299 -0
  102. scitex/fig/editor/flask_editor/_core.py +1429 -0
  103. scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
  104. scitex/fig/editor/flask_editor/_renderer.py +813 -0
  105. scitex/fig/editor/flask_editor/static/css/base/reset.css +41 -0
  106. scitex/fig/editor/flask_editor/static/css/base/typography.css +16 -0
  107. scitex/fig/editor/flask_editor/static/css/base/variables.css +85 -0
  108. scitex/fig/editor/flask_editor/static/css/components/buttons.css +217 -0
  109. scitex/fig/editor/flask_editor/static/css/components/context-menu.css +93 -0
  110. scitex/fig/editor/flask_editor/static/css/components/dropdown.css +57 -0
  111. scitex/fig/editor/flask_editor/static/css/components/forms.css +112 -0
  112. scitex/fig/editor/flask_editor/static/css/components/modal.css +59 -0
  113. scitex/fig/editor/flask_editor/static/css/components/sections.css +212 -0
  114. scitex/fig/editor/flask_editor/static/css/features/canvas.css +176 -0
  115. scitex/fig/editor/flask_editor/static/css/features/element-inspector.css +190 -0
  116. scitex/fig/editor/flask_editor/static/css/features/loading.css +59 -0
  117. scitex/fig/editor/flask_editor/static/css/features/overlay.css +45 -0
  118. scitex/fig/editor/flask_editor/static/css/features/panel-grid.css +95 -0
  119. scitex/fig/editor/flask_editor/static/css/features/selection.css +101 -0
  120. scitex/fig/editor/flask_editor/static/css/features/statistics.css +138 -0
  121. scitex/fig/editor/flask_editor/static/css/index.css +31 -0
  122. scitex/fig/editor/flask_editor/static/css/layout/container.css +7 -0
  123. scitex/fig/editor/flask_editor/static/css/layout/controls.css +56 -0
  124. scitex/fig/editor/flask_editor/static/css/layout/preview.css +78 -0
  125. scitex/fig/editor/flask_editor/static/js/alignment/axis.js +314 -0
  126. scitex/fig/editor/flask_editor/static/js/alignment/basic.js +107 -0
  127. scitex/fig/editor/flask_editor/static/js/alignment/distribute.js +54 -0
  128. scitex/fig/editor/flask_editor/static/js/canvas/canvas.js +172 -0
  129. scitex/fig/editor/flask_editor/static/js/canvas/dragging.js +258 -0
  130. scitex/fig/editor/flask_editor/static/js/canvas/resize.js +48 -0
  131. scitex/fig/editor/flask_editor/static/js/canvas/selection.js +71 -0
  132. scitex/fig/editor/flask_editor/static/js/core/api.js +288 -0
  133. scitex/fig/editor/flask_editor/static/js/core/state.js +143 -0
  134. scitex/fig/editor/flask_editor/static/js/core/utils.js +245 -0
  135. scitex/fig/editor/flask_editor/static/js/dev/element-inspector.js +992 -0
  136. scitex/fig/editor/flask_editor/static/js/editor/bbox.js +339 -0
  137. scitex/fig/editor/flask_editor/static/js/editor/element-drag.js +286 -0
  138. scitex/fig/editor/flask_editor/static/js/editor/overlay.js +371 -0
  139. scitex/fig/editor/flask_editor/static/js/editor/preview.js +293 -0
  140. scitex/fig/editor/flask_editor/static/js/main.js +426 -0
  141. scitex/fig/editor/flask_editor/static/js/shortcuts/context-menu.js +152 -0
  142. scitex/fig/editor/flask_editor/static/js/shortcuts/keyboard.js +265 -0
  143. scitex/fig/editor/flask_editor/static/js/ui/controls.js +184 -0
  144. scitex/fig/editor/flask_editor/static/js/ui/download.js +57 -0
  145. scitex/fig/editor/flask_editor/static/js/ui/help.js +100 -0
  146. scitex/fig/editor/flask_editor/static/js/ui/theme.js +34 -0
  147. scitex/fig/editor/flask_editor/templates/__init__.py +123 -0
  148. scitex/fig/editor/flask_editor/templates/_html.py +852 -0
  149. scitex/fig/editor/flask_editor/templates/_scripts.py +4933 -0
  150. scitex/fig/editor/flask_editor/templates/_styles.py +1658 -0
  151. scitex/{vis → fig}/io/__init__.py +13 -1
  152. scitex/fig/io/_bundle.py +1058 -0
  153. scitex/{vis → fig}/io/_canvas.py +1 -1
  154. scitex/{vis → fig}/io/_data.py +1 -1
  155. scitex/{vis → fig}/io/_export.py +1 -1
  156. scitex/{vis → fig}/io/_load.py +1 -1
  157. scitex/{vis → fig}/io/_panel.py +1 -1
  158. scitex/{vis → fig}/io/_save.py +1 -1
  159. scitex/{vis → fig}/model/__init__.py +1 -1
  160. scitex/{vis → fig}/model/_annotations.py +1 -1
  161. scitex/{vis → fig}/model/_axes.py +1 -1
  162. scitex/{vis → fig}/model/_figure.py +1 -1
  163. scitex/{vis → fig}/model/_guides.py +1 -1
  164. scitex/{vis → fig}/model/_plot.py +1 -1
  165. scitex/{vis → fig}/model/_styles.py +1 -1
  166. scitex/{vis → fig}/utils/__init__.py +1 -1
  167. scitex/io/__init__.py +22 -26
  168. scitex/io/_bundle.py +493 -0
  169. scitex/io/_flush.py +5 -2
  170. scitex/io/_load.py +98 -0
  171. scitex/io/_load_modules/_H5Explorer.py +5 -2
  172. scitex/io/_load_modules/_canvas.py +2 -2
  173. scitex/io/_load_modules/_image.py +3 -4
  174. scitex/io/_load_modules/_txt.py +4 -2
  175. scitex/io/_metadata.py +34 -324
  176. scitex/io/_metadata_modules/__init__.py +46 -0
  177. scitex/io/_metadata_modules/_embed.py +70 -0
  178. scitex/io/_metadata_modules/_read.py +64 -0
  179. scitex/io/_metadata_modules/_utils.py +79 -0
  180. scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
  181. scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
  182. scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
  183. scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
  184. scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
  185. scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
  186. scitex/io/_metadata_modules/read_metadata_png.py +39 -0
  187. scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
  188. scitex/io/_qr_utils.py +5 -3
  189. scitex/io/_save.py +548 -30
  190. scitex/io/_save_modules/_canvas.py +3 -3
  191. scitex/io/_save_modules/_image.py +5 -9
  192. scitex/io/_save_modules/_tex.py +7 -4
  193. scitex/io/_zip_bundle.py +439 -0
  194. scitex/io/utils/h5_to_zarr.py +11 -9
  195. scitex/msword/__init__.py +255 -0
  196. scitex/msword/profiles.py +357 -0
  197. scitex/msword/reader.py +753 -0
  198. scitex/msword/utils.py +289 -0
  199. scitex/msword/writer.py +362 -0
  200. scitex/plt/__init__.py +5 -2
  201. scitex/plt/_subplots/_AxesWrapper.py +6 -6
  202. scitex/plt/_subplots/_AxisWrapper.py +15 -9
  203. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
  204. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
  205. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
  206. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
  207. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
  208. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
  209. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
  210. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
  211. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
  212. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
  213. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
  214. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
  215. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
  216. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
  217. scitex/plt/_subplots/_FigWrapper.py +6 -6
  218. scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
  219. scitex/plt/_subplots/_export_as_csv.py +35 -5
  220. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
  221. scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
  222. scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
  223. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
  224. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
  225. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
  226. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
  227. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
  228. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
  229. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
  230. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
  231. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
  232. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
  233. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
  234. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
  235. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
  236. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
  237. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
  238. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
  239. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
  240. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
  241. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
  242. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
  243. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
  244. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
  245. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
  246. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
  247. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
  248. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
  249. scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
  250. scitex/plt/gallery/_generate.py +421 -14
  251. scitex/plt/io/__init__.py +53 -0
  252. scitex/plt/io/_bundle.py +490 -0
  253. scitex/plt/io/_layered_bundle.py +1343 -0
  254. scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
  255. scitex/plt/styles/__init__.py +14 -0
  256. scitex/plt/styles/presets.py +78 -0
  257. scitex/plt/utils/__init__.py +13 -1
  258. scitex/plt/utils/_collect_figure_metadata.py +10 -14
  259. scitex/plt/utils/_configure_mpl.py +6 -18
  260. scitex/plt/utils/_crop.py +32 -14
  261. scitex/plt/utils/_csv_column_naming.py +54 -0
  262. scitex/plt/utils/_figure_mm.py +116 -1
  263. scitex/plt/utils/_hitmap.py +1643 -0
  264. scitex/plt/utils/metadata/__init__.py +25 -0
  265. scitex/plt/utils/metadata/_core.py +9 -10
  266. scitex/plt/utils/metadata/_dimensions.py +6 -3
  267. scitex/plt/utils/metadata/_editable_export.py +405 -0
  268. scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
  269. scitex/schema/__init__.py +109 -16
  270. scitex/schema/_canvas.py +1 -1
  271. scitex/schema/_plot.py +1015 -0
  272. scitex/schema/_stats.py +2 -2
  273. scitex/stats/__init__.py +117 -0
  274. scitex/stats/io/__init__.py +29 -0
  275. scitex/stats/io/_bundle.py +156 -0
  276. scitex/tex/__init__.py +4 -0
  277. scitex/tex/_export.py +890 -0
  278. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/METADATA +11 -1
  279. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/RECORD +294 -170
  280. scitex/io/memo.md +0 -2827
  281. scitex/plt/REQUESTS.md +0 -191
  282. scitex/plt/_subplots/TODO.md +0 -53
  283. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
  284. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
  285. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
  286. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
  287. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
  288. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
  289. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
  290. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
  291. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
  292. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
  293. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
  294. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
  295. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
  296. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
  297. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
  298. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
  299. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
  300. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
  301. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
  302. scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
  303. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
  304. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
  305. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
  306. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
  307. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
  308. scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
  309. scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
  310. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
  311. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
  312. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
  313. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
  314. scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
  315. scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
  316. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
  317. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
  318. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
  319. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
  320. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
  321. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
  322. scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
  323. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
  324. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
  325. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
  326. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
  327. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
  328. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
  329. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
  330. scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
  331. scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
  332. scitex/vis/__init__.py +0 -177
  333. scitex/vis/editor/_edit.py +0 -390
  334. scitex/vis/editor/flask_editor/_bbox.py +0 -529
  335. scitex/vis/editor/flask_editor/_core.py +0 -168
  336. scitex/vis/editor/flask_editor/_renderer.py +0 -393
  337. scitex/vis/editor/flask_editor/templates/__init__.py +0 -33
  338. scitex/vis/editor/flask_editor/templates/_html.py +0 -513
  339. scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
  340. scitex/vis/editor/flask_editor/templates/_styles.py +0 -739
  341. /scitex/{vis → fig}/README.md +0 -0
  342. /scitex/{vis → fig}/backend/__init__.py +0 -0
  343. /scitex/{vis → fig}/backend/_export.py +0 -0
  344. /scitex/{vis → fig}/backend/_render.py +0 -0
  345. /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
  346. /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
  347. /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
  348. /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
  349. /scitex/{vis → fig}/io/_directory.py +0 -0
  350. /scitex/{vis → fig}/model/_plot_types.py +0 -0
  351. /scitex/{vis → fig}/utils/_defaults.py +0 -0
  352. /scitex/{vis → fig}/utils/_validate.py +0 -0
  353. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
  354. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
  355. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,757 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: "2025-12-11 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/mcp_server.py
5
+ # ----------------------------------------
6
+
7
+ """
8
+ MCP Server for SciTeX Audio - Text-to-Speech with Multiple Backends
9
+
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)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import base64
22
+ import os
23
+ import uuid
24
+ from dataclasses import dataclass, field
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+ from typing import Any, Optional
28
+
29
+ import mcp.types as types
30
+ from mcp.server import NotificationOptions, Server
31
+ from mcp.server.models import InitializationOptions
32
+ from mcp.server.stdio import stdio_server
33
+
34
+ __all__ = ["AudioServer", "main"]
35
+
36
+
37
+ @dataclass
38
+ class SpeechRequest:
39
+ """A queued speech request."""
40
+
41
+ request_id: str
42
+ text: str
43
+ backend: Optional[str] = None
44
+ voice: Optional[str] = None
45
+ rate: Optional[int] = None
46
+ speed: Optional[float] = None
47
+ play: bool = True
48
+ save: bool = False
49
+ fallback: bool = True
50
+ future: asyncio.Future = field(default_factory=lambda: None)
51
+ created_at: datetime = field(default_factory=datetime.now)
52
+ agent_id: Optional[str] = None # Track which agent made the request
53
+
54
+ # Directory configuration
55
+ SCITEX_BASE_DIR = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
56
+ SCITEX_AUDIO_DIR = SCITEX_BASE_DIR / "audio"
57
+
58
+
59
+ def get_audio_dir() -> Path:
60
+ """Get the audio output directory."""
61
+ audio_dir = SCITEX_AUDIO_DIR
62
+ audio_dir.mkdir(parents=True, exist_ok=True)
63
+ return audio_dir
64
+
65
+
66
+ 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
+ """
72
+
73
+ def __init__(self):
74
+ self.server = Server("scitex-audio")
75
+ # Speech queue for sequential processing
76
+ self._speech_queue: asyncio.Queue[SpeechRequest] = asyncio.Queue()
77
+ self._queue_processor_task: Optional[asyncio.Task] = None
78
+ self._current_request: Optional[SpeechRequest] = None
79
+ self._processed_count: int = 0
80
+ self._is_processing: bool = False
81
+ self.setup_handlers()
82
+
83
+ async def start_queue_processor(self):
84
+ """Start the background queue processor if not already running."""
85
+ if self._queue_processor_task is None or self._queue_processor_task.done():
86
+ self._queue_processor_task = asyncio.create_task(
87
+ self._process_speech_queue()
88
+ )
89
+
90
+ async def _process_speech_queue(self):
91
+ """Process speech requests sequentially from the queue."""
92
+ while True:
93
+ try:
94
+ # Wait for next request
95
+ request = await self._speech_queue.get()
96
+ self._current_request = request
97
+ self._is_processing = True
98
+
99
+ try:
100
+ # Execute the speech request
101
+ result = await self._execute_speak(request)
102
+ # Set the result on the future if it exists
103
+ if request.future and not request.future.done():
104
+ request.future.set_result(result)
105
+ except Exception as e:
106
+ # Set exception on future if it exists
107
+ if request.future and not request.future.done():
108
+ request.future.set_exception(e)
109
+ finally:
110
+ self._processed_count += 1
111
+ self._current_request = None
112
+ self._is_processing = False
113
+ self._speech_queue.task_done()
114
+
115
+ except asyncio.CancelledError:
116
+ break
117
+ except Exception:
118
+ # Continue processing even if one request fails
119
+ continue
120
+
121
+ async def _execute_speak(self, request: SpeechRequest) -> dict:
122
+ """Execute a single speech request."""
123
+ from . import available_backends
124
+ from . import speak as tts_speak
125
+
126
+ loop = asyncio.get_event_loop()
127
+
128
+ # Determine output path
129
+ output_path = None
130
+ if request.save:
131
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
132
+ output_path = str(get_audio_dir() / f"tts_{timestamp}.mp3")
133
+
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
+ )
145
+
146
+ result_path = await loop.run_in_executor(None, do_speak)
147
+
148
+ backends = available_backends()
149
+ used_backend = request.backend or (backends[0] if backends else None)
150
+
151
+ response = {
152
+ "success": True,
153
+ "request_id": request.request_id,
154
+ "text": request.text,
155
+ "backend": used_backend,
156
+ "available_backends": backends,
157
+ "voice": request.voice,
158
+ "played": request.play,
159
+ "fallback_enabled": request.fallback,
160
+ "timestamp": datetime.now().isoformat(),
161
+ "agent_id": request.agent_id,
162
+ }
163
+
164
+ if output_path:
165
+ response["saved_to"] = output_path
166
+
167
+ return response
168
+
169
+ def setup_handlers(self):
170
+ @self.server.list_tools()
171
+ 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
+ ]
336
+
337
+ @self.server.call_tool()
338
+ async def handle_call_tool(name: str, arguments: dict):
339
+ # Ensure queue processor is running for speak requests
340
+ if name == "speak":
341
+ await self.start_queue_processor()
342
+ return await self.speak(**arguments)
343
+ elif name == "generate_audio":
344
+ return await self.generate_audio(**arguments)
345
+ elif name == "list_backends":
346
+ return await self.list_backends()
347
+ elif name == "list_voices":
348
+ return await self.list_voices(**arguments)
349
+ elif name == "play_audio":
350
+ return await self.play_audio(**arguments)
351
+ elif name == "list_audio_files":
352
+ return await self.list_audio_files(**arguments)
353
+ elif name == "clear_audio_cache":
354
+ return await self.clear_audio_cache(**arguments)
355
+ elif name == "speech_queue_status":
356
+ return await self.speech_queue_status()
357
+ elif name == "check_audio_status":
358
+ return await self.check_audio_status()
359
+ else:
360
+ raise ValueError(f"Unknown tool: {name}")
361
+
362
+ # Provide audio files as resources
363
+ @self.server.list_resources()
364
+ async def handle_list_resources():
365
+ audio_dir = get_audio_dir()
366
+ if not audio_dir.exists():
367
+ return []
368
+
369
+ resources = []
370
+ audio_files = sorted(
371
+ list(audio_dir.glob("*.mp3")) + list(audio_dir.glob("*.wav")),
372
+ key=lambda p: p.stat().st_mtime,
373
+ reverse=True,
374
+ )[:20]
375
+
376
+ for audio_file in audio_files:
377
+ mtime = datetime.fromtimestamp(audio_file.stat().st_mtime)
378
+ mime_type = (
379
+ "audio/mpeg" if audio_file.suffix == ".mp3" else "audio/wav"
380
+ )
381
+ resources.append(
382
+ types.Resource(
383
+ uri=f"audio://{audio_file.name}",
384
+ name=audio_file.name,
385
+ description=f"Audio from {mtime.strftime('%Y-%m-%d %H:%M:%S')}",
386
+ mimeType=mime_type,
387
+ )
388
+ )
389
+ return resources
390
+
391
+ @self.server.read_resource()
392
+ async def handle_read_resource(uri: str):
393
+ if uri.startswith("audio://"):
394
+ filename = uri.replace("audio://", "")
395
+ filepath = get_audio_dir() / filename
396
+
397
+ if filepath.exists():
398
+ with open(filepath, "rb") as f:
399
+ content = base64.b64encode(f.read()).decode()
400
+
401
+ mime_type = (
402
+ "audio/mpeg" if filepath.suffix == ".mp3" else "audio/wav"
403
+ )
404
+ return types.ResourceContent(
405
+ uri=uri, mimeType=mime_type, content=content
406
+ )
407
+ else:
408
+ raise ValueError(f"Audio file not found: {filename}")
409
+
410
+ async def speak(
411
+ self,
412
+ text: str,
413
+ backend: Optional[str] = None,
414
+ voice: Optional[str] = None,
415
+ rate: Optional[int] = None,
416
+ speed: Optional[float] = None,
417
+ play: bool = True,
418
+ save: bool = False,
419
+ fallback: bool = True,
420
+ agent_id: Optional[str] = None,
421
+ wait: bool = True,
422
+ ):
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
+ """
440
+ try:
441
+ # Create a unique request ID
442
+ request_id = str(uuid.uuid4())[:8]
443
+
444
+ # Create a future to wait for the result
445
+ loop = asyncio.get_event_loop()
446
+ future = loop.create_future()
447
+
448
+ # Create the speech request
449
+ request = SpeechRequest(
450
+ request_id=request_id,
451
+ text=text,
452
+ backend=backend,
453
+ voice=voice,
454
+ rate=rate,
455
+ speed=speed,
456
+ play=play,
457
+ save=save,
458
+ fallback=fallback,
459
+ future=future,
460
+ agent_id=agent_id,
461
+ )
462
+
463
+ # Add to queue
464
+ await self._speech_queue.put(request)
465
+
466
+ queue_position = self._speech_queue.qsize()
467
+
468
+ if wait:
469
+ # Wait for the speech to complete
470
+ result = await future
471
+ result["queue_position"] = 0 # Already processed
472
+ return result
473
+ else:
474
+ # Return immediately with queue info
475
+ return {
476
+ "success": True,
477
+ "queued": True,
478
+ "request_id": request_id,
479
+ "queue_position": queue_position,
480
+ "text": text,
481
+ "agent_id": agent_id,
482
+ "message": f"Request queued at position {queue_position}",
483
+ "timestamp": datetime.now().isoformat(),
484
+ }
485
+
486
+ except Exception as e:
487
+ return {"success": False, "error": str(e)}
488
+
489
+ async def speech_queue_status(self):
490
+ """Get the current speech queue status."""
491
+ try:
492
+ current = None
493
+ if self._current_request:
494
+ current = {
495
+ "request_id": self._current_request.request_id,
496
+ "text": self._current_request.text[:50] + "..."
497
+ if len(self._current_request.text) > 50
498
+ else self._current_request.text,
499
+ "agent_id": self._current_request.agent_id,
500
+ "created_at": self._current_request.created_at.isoformat(),
501
+ }
502
+
503
+ return {
504
+ "success": True,
505
+ "queue_size": self._speech_queue.qsize(),
506
+ "is_processing": self._is_processing,
507
+ "current_request": current,
508
+ "total_processed": self._processed_count,
509
+ "processor_running": self._queue_processor_task is not None
510
+ and not self._queue_processor_task.done(),
511
+ "timestamp": datetime.now().isoformat(),
512
+ }
513
+ except Exception as e:
514
+ return {"success": False, "error": str(e)}
515
+
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)
613
+
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
+ 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}"}
632
+
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
+ }
674
+ )
675
+
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()
698
+
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
712
+
713
+ return {
714
+ "success": True,
715
+ "deleted": deleted,
716
+ "max_age_hours": max_age_hours,
717
+ }
718
+
719
+ except Exception as e:
720
+ return {"success": False, "error": str(e)}
721
+
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
+
736
+ async def main():
737
+ """Main entry point for the MCP server."""
738
+ server = AudioServer()
739
+ async with stdio_server() as (read_stream, write_stream):
740
+ await server.server.run(
741
+ read_stream,
742
+ write_stream,
743
+ InitializationOptions(
744
+ server_name="scitex-audio",
745
+ server_version="0.2.0",
746
+ capabilities=server.server.get_capabilities(
747
+ notification_options=NotificationOptions(),
748
+ experimental_capabilities={},
749
+ ),
750
+ ),
751
+ )
752
+
753
+
754
+ if __name__ == "__main__":
755
+ asyncio.run(main())
756
+
757
+ # EOF