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,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  # Time-stamp: "2024-11-11 04:11:10 (ywatanabe)"
4
3
  # File: ./scitex_repo/src/scitex/ai/_gen_ai/_Perplexity.py
5
4
 
@@ -45,6 +44,12 @@ class Perplexity(BaseGenAI):
45
44
  chat_history: Optional[List[Dict[str, str]]] = None,
46
45
  max_tokens: Optional[int] = None, # Added parameter
47
46
  ) -> None:
47
+ # Validate API key
48
+ if not api_key:
49
+ api_key = os.getenv("PERPLEXITY_API_KEY", "")
50
+ if not api_key:
51
+ raise ValueError("PERPLEXITY_API_KEY environment variable not set")
52
+
48
53
  # Set max_tokens based on model if not provided
49
54
  if max_tokens is None:
50
55
  max_tokens = 128_000 if "128k" in model else 32_000
@@ -54,6 +59,7 @@ class Perplexity(BaseGenAI):
54
59
  model=model,
55
60
  api_key=api_key,
56
61
  stream=stream,
62
+ seed=seed,
57
63
  n_keep=n_keep,
58
64
  temperature=temperature,
59
65
  provider="Perplexity",
@@ -61,6 +67,11 @@ class Perplexity(BaseGenAI):
61
67
  max_tokens=max_tokens,
62
68
  )
63
69
 
70
+ @property
71
+ def chat_history(self) -> List[Dict[str, str]]:
72
+ """Alias for history to maintain backward compatibility."""
73
+ return self.history
74
+
64
75
  def _init_client(self) -> OpenAI:
65
76
  return OpenAI(api_key=self.api_key, base_url="https://api.perplexity.ai")
66
77
  # return OpenAI(
@@ -95,7 +106,11 @@ class Perplexity(BaseGenAI):
95
106
  )
96
107
 
97
108
  for chunk in stream:
98
- if chunk and chunk.choices[0].finish_reason == "stop":
109
+ # Handle empty chunks or chunks without choices
110
+ if not chunk or not chunk.choices:
111
+ continue
112
+
113
+ if chunk.choices[0].finish_reason == "stop":
99
114
  print(chunk.choices)
100
115
  try:
101
116
  self.input_tokens += chunk.usage.prompt_tokens
@@ -103,10 +118,9 @@ class Perplexity(BaseGenAI):
103
118
  except AttributeError:
104
119
  pass
105
120
 
106
- if chunk.choices:
107
- current_text = chunk.choices[0].delta.content
108
- if current_text:
109
- yield current_text
121
+ current_text = chunk.choices[0].delta.content
122
+ if current_text:
123
+ yield current_text
110
124
 
111
125
  def _get_available_models(self) -> List[str]:
112
126
  return [
scitex/audio/__init__.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/__init__.py
5
4
  # ----------------------------------------
@@ -27,18 +26,26 @@ Usage:
27
26
  from scitex.audio import GoogleTTS, ElevenLabsTTS, SystemTTS
28
27
  tts = SystemTTS()
29
28
  tts.speak("Hello!")
29
+
30
+ Installation:
31
+ pip install scitex[audio]
30
32
  """
31
33
 
32
34
  import subprocess
33
35
  from typing import List, Optional
34
36
 
37
+ # Check for missing dependencies and warn user
38
+ from scitex._install_guide import warn_module_deps
39
+
40
+ _missing = warn_module_deps("audio")
41
+
35
42
  # Import from engines subpackage
36
43
  from .engines import (
37
44
  BaseTTS,
38
- TTSBackend,
39
- SystemTTS,
40
- GoogleTTS,
41
45
  ElevenLabsTTS,
46
+ GoogleTTS,
47
+ SystemTTS,
48
+ TTSBackend,
42
49
  )
43
50
 
44
51
 
@@ -118,6 +125,7 @@ def check_wsl_audio() -> dict:
118
125
 
119
126
  return result
120
127
 
128
+
121
129
  # Keep legacy TTS import for backwards compatibility
122
130
  from ._tts import TTS
123
131
 
@@ -137,9 +145,8 @@ __all__ = [
137
145
  "FALLBACK_ORDER",
138
146
  ]
139
147
 
140
- # Fallback order: pyttsx3 (offline, free) -> gtts (free) -> elevenlabs (paid)
141
- # FALLBACK_ORDER = ["pyttsx3", "gtts", "elevenlabs"]
142
- FALLBACK_ORDER = ["gtts", "pyttsx3", "elevenlabs"]
148
+ # Fallback order: elevenlabs (best quality) -> gtts (free) -> pyttsx3 (offline)
149
+ FALLBACK_ORDER = ["elevenlabs", "gtts", "pyttsx3"]
143
150
 
144
151
 
145
152
  def available_backends() -> List[str]:
@@ -150,6 +157,7 @@ def available_backends() -> List[str]:
150
157
  if SystemTTS:
151
158
  try:
152
159
  import pyttsx3
160
+
153
161
  # Try to init to check if espeak is available
154
162
  engine = pyttsx3.init()
155
163
  engine.stop()
@@ -164,7 +172,11 @@ def available_backends() -> List[str]:
164
172
  # Check ElevenLabs (requires API key)
165
173
  if ElevenLabsTTS:
166
174
  import os
167
- if os.environ.get("ELEVENLABS_API_KEY"):
175
+
176
+ api_key = os.environ.get("ELEVENLABS_API_KEY") or os.environ.get(
177
+ "ELEVENLABS_API_KEY_SCITEX_AUDIO"
178
+ )
179
+ if api_key:
168
180
  backends.append("elevenlabs")
169
181
 
170
182
  return backends
@@ -209,10 +221,7 @@ def get_tts(backend: Optional[str] = None, **kwargs) -> BaseTTS:
209
221
  elif backend == "elevenlabs" and ElevenLabsTTS:
210
222
  return ElevenLabsTTS(**kwargs)
211
223
  else:
212
- raise ValueError(
213
- f"Backend '{backend}' not available. "
214
- f"Available: {backends}"
215
- )
224
+ raise ValueError(f"Backend '{backend}' not available. Available: {backends}")
216
225
 
217
226
 
218
227
  def _try_speak_with_fallback(
@@ -340,9 +349,7 @@ def speak(
340
349
  **kwargs,
341
350
  )
342
351
  if result is None and errors:
343
- raise RuntimeError(
344
- f"All TTS backends failed:\n" + "\n".join(errors)
345
- )
352
+ raise RuntimeError("All TTS backends failed:\n" + "\n".join(errors))
346
353
  return str(result) if result else None
347
354
 
348
355
  # Specific backend with fallback enabled
@@ -377,7 +384,9 @@ def speak(
377
384
  def start_mcp_server():
378
385
  """Start the MCP server for audio."""
379
386
  import asyncio
387
+
380
388
  from .mcp_server import main
389
+
381
390
  asyncio.run(main())
382
391
 
383
392
 
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2025-12-27 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/_cross_process_lock.py
4
+ # ----------------------------------------
5
+
6
+ """
7
+ Cross-process FIFO lock for audio playback.
8
+
9
+ Ensures only one MCP server instance can play audio at a time,
10
+ providing true FIFO ordering across all Claude Code sessions.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import fcntl
16
+ import os
17
+ import time
18
+ from contextlib import contextmanager
19
+ from pathlib import Path
20
+
21
+ __all__ = ["AudioPlaybackLock", "acquire_audio_lock"]
22
+
23
+ # Lock file location
24
+ SCITEX_BASE_DIR = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
25
+ LOCK_FILE = SCITEX_BASE_DIR / "audio" / ".audio_playback.lock"
26
+
27
+
28
+ class AudioPlaybackLock:
29
+ """Cross-process lock for sequential audio playback.
30
+
31
+ Uses fcntl.flock() for POSIX file locking to ensure
32
+ only one process can play audio at a time.
33
+ """
34
+
35
+ def __init__(self, lock_file: Path | None = None):
36
+ self.lock_file = lock_file or LOCK_FILE
37
+ self._fd: int | None = None
38
+ self._acquired = False
39
+
40
+ def _ensure_lock_dir(self):
41
+ """Ensure the lock file directory exists."""
42
+ self.lock_file.parent.mkdir(parents=True, exist_ok=True)
43
+
44
+ def acquire(self, timeout: float | None = None) -> bool:
45
+ """Acquire the audio playback lock.
46
+
47
+ Args:
48
+ timeout: Maximum time to wait in seconds.
49
+ None means wait indefinitely.
50
+
51
+ Returns:
52
+ True if lock acquired, False if timeout.
53
+ """
54
+ self._ensure_lock_dir()
55
+
56
+ # Open or create the lock file
57
+ self._fd = os.open(
58
+ str(self.lock_file),
59
+ os.O_RDWR | os.O_CREAT,
60
+ 0o644,
61
+ )
62
+
63
+ start_time = time.time()
64
+
65
+ while True:
66
+ try:
67
+ # Try to acquire exclusive lock (non-blocking)
68
+ fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
69
+ self._acquired = True
70
+
71
+ # Write PID to lock file for debugging
72
+ os.ftruncate(self._fd, 0)
73
+ os.lseek(self._fd, 0, os.SEEK_SET)
74
+ os.write(self._fd, f"{os.getpid()}\n".encode())
75
+
76
+ return True
77
+
78
+ except OSError:
79
+ # Lock is held by another process
80
+ if timeout is not None:
81
+ elapsed = time.time() - start_time
82
+ if elapsed >= timeout:
83
+ self._cleanup()
84
+ return False
85
+
86
+ # Wait a bit before retrying
87
+ time.sleep(0.1)
88
+
89
+ def release(self):
90
+ """Release the audio playback lock."""
91
+ if self._fd is not None and self._acquired:
92
+ try:
93
+ fcntl.flock(self._fd, fcntl.LOCK_UN)
94
+ except OSError:
95
+ pass
96
+ self._acquired = False
97
+ self._cleanup()
98
+
99
+ def _cleanup(self):
100
+ """Clean up file descriptor."""
101
+ if self._fd is not None:
102
+ try:
103
+ os.close(self._fd)
104
+ except OSError:
105
+ pass
106
+ self._fd = None
107
+
108
+ def __enter__(self):
109
+ self.acquire()
110
+ return self
111
+
112
+ def __exit__(self, exc_type, exc_val, exc_tb):
113
+ self.release()
114
+ return False
115
+
116
+
117
+ @contextmanager
118
+ def acquire_audio_lock(timeout: float | None = 60.0):
119
+ """Context manager for acquiring the audio playback lock.
120
+
121
+ Args:
122
+ timeout: Maximum time to wait in seconds (default: 60s).
123
+
124
+ Yields:
125
+ True if lock was acquired.
126
+
127
+ Raises:
128
+ TimeoutError: If lock could not be acquired within timeout.
129
+ """
130
+ lock = AudioPlaybackLock()
131
+ try:
132
+ if not lock.acquire(timeout=timeout):
133
+ raise TimeoutError(f"Could not acquire audio lock within {timeout}s")
134
+ yield True
135
+ finally:
136
+ lock.release()
137
+
138
+
139
+ # EOF
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2025-12-27 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/_mcp_handlers.py
4
+ # ----------------------------------------
5
+
6
+ """Utility handlers for the scitex-audio MCP server."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import base64
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+
15
+ __all__ = [
16
+ "generate_audio_handler",
17
+ "list_backends_handler",
18
+ "list_voices_handler",
19
+ "play_audio_handler",
20
+ "list_audio_files_handler",
21
+ "clear_audio_cache_handler",
22
+ "check_audio_status_handler",
23
+ ]
24
+
25
+
26
+ def _get_audio_dir() -> Path:
27
+ """Get the audio output directory."""
28
+ import os
29
+
30
+ base_dir = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
31
+ audio_dir = base_dir / "audio"
32
+ audio_dir.mkdir(parents=True, exist_ok=True)
33
+ return audio_dir
34
+
35
+
36
+ async def generate_audio_handler(
37
+ text: str,
38
+ backend: str | None = None,
39
+ voice: str | None = None,
40
+ output_path: str | None = None,
41
+ return_base64: bool = False,
42
+ ) -> dict:
43
+ """Generate audio file without playing."""
44
+ try:
45
+ from . import speak as tts_speak
46
+
47
+ loop = asyncio.get_event_loop()
48
+
49
+ if not output_path:
50
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
51
+ output_path = str(_get_audio_dir() / f"tts_{timestamp}.mp3")
52
+
53
+ def do_generate():
54
+ return tts_speak(
55
+ text=text,
56
+ backend=backend,
57
+ voice=voice,
58
+ play=False,
59
+ output_path=output_path,
60
+ fallback=True,
61
+ )
62
+
63
+ result_path = await loop.run_in_executor(None, do_generate)
64
+
65
+ result = {
66
+ "success": True,
67
+ "path": str(result_path),
68
+ "text": text,
69
+ "backend": backend,
70
+ "timestamp": datetime.now().isoformat(),
71
+ }
72
+
73
+ if result_path.exists():
74
+ result["size_kb"] = round(result_path.stat().st_size / 1024, 2)
75
+
76
+ if return_base64 and result_path.exists():
77
+ with open(result_path, "rb") as f:
78
+ result["base64"] = base64.b64encode(f.read()).decode()
79
+
80
+ return result
81
+
82
+ except Exception as e:
83
+ return {"success": False, "error": str(e)}
84
+
85
+
86
+ async def list_backends_handler() -> dict:
87
+ """List available TTS backends."""
88
+ try:
89
+ from . import available_backends
90
+
91
+ backends = available_backends()
92
+
93
+ info = []
94
+ for b in ["gtts", "elevenlabs", "pyttsx3"]:
95
+ available = b in backends
96
+ desc = {
97
+ "gtts": "Google TTS - Free, requires internet",
98
+ "elevenlabs": "ElevenLabs - Paid, high quality",
99
+ "pyttsx3": "System TTS - Offline, uses espeak/SAPI5",
100
+ }
101
+ info.append(
102
+ {
103
+ "name": b,
104
+ "available": available,
105
+ "description": desc.get(b, ""),
106
+ }
107
+ )
108
+
109
+ return {
110
+ "success": True,
111
+ "backends": info,
112
+ "available": backends,
113
+ "default": backends[0] if backends else None,
114
+ }
115
+
116
+ except Exception as e:
117
+ return {"success": False, "error": str(e)}
118
+
119
+
120
+ async def list_voices_handler(backend: str = "gtts") -> dict:
121
+ """List available voices for a backend."""
122
+ try:
123
+ from . import get_tts
124
+
125
+ loop = asyncio.get_event_loop()
126
+
127
+ def do_list():
128
+ tts = get_tts(backend)
129
+ return tts.get_voices()
130
+
131
+ voices = await loop.run_in_executor(None, do_list)
132
+
133
+ return {
134
+ "success": True,
135
+ "backend": backend,
136
+ "voices": voices,
137
+ "count": len(voices),
138
+ }
139
+
140
+ except Exception as e:
141
+ return {"success": False, "error": str(e)}
142
+
143
+
144
+ async def play_audio_handler(path: str) -> dict:
145
+ """Play an audio file."""
146
+ try:
147
+ from .engines.base import BaseTTS
148
+
149
+ path_obj = Path(path)
150
+ if not path_obj.exists():
151
+ return {"success": False, "error": f"File not found: {path}"}
152
+
153
+ loop = asyncio.get_event_loop()
154
+
155
+ def do_play():
156
+ BaseTTS._play_audio(None, path_obj)
157
+
158
+ await loop.run_in_executor(None, do_play)
159
+
160
+ return {
161
+ "success": True,
162
+ "played": str(path_obj),
163
+ "timestamp": datetime.now().isoformat(),
164
+ }
165
+
166
+ except Exception as e:
167
+ return {"success": False, "error": str(e)}
168
+
169
+
170
+ async def list_audio_files_handler(limit: int = 20) -> dict:
171
+ """List generated audio files."""
172
+ try:
173
+ audio_dir = _get_audio_dir()
174
+ if not audio_dir.exists():
175
+ return {"success": True, "files": [], "count": 0}
176
+
177
+ audio_files = sorted(
178
+ list(audio_dir.glob("*.mp3")) + list(audio_dir.glob("*.wav")),
179
+ key=lambda p: p.stat().st_mtime,
180
+ reverse=True,
181
+ )[:limit]
182
+
183
+ files = []
184
+ for f in audio_files:
185
+ files.append(
186
+ {
187
+ "name": f.name,
188
+ "path": str(f),
189
+ "size_kb": round(f.stat().st_size / 1024, 2),
190
+ "created": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
191
+ }
192
+ )
193
+
194
+ total_size = sum(f.stat().st_size for f in audio_dir.glob("*.*"))
195
+
196
+ return {
197
+ "success": True,
198
+ "files": files,
199
+ "count": len(files),
200
+ "total_size_mb": round(total_size / (1024 * 1024), 2),
201
+ "audio_dir": str(audio_dir),
202
+ }
203
+
204
+ except Exception as e:
205
+ return {"success": False, "error": str(e)}
206
+
207
+
208
+ async def clear_audio_cache_handler(max_age_hours: float = 24) -> dict:
209
+ """Clear audio cache."""
210
+ try:
211
+ audio_dir = _get_audio_dir()
212
+ if not audio_dir.exists():
213
+ return {"success": True, "deleted": 0}
214
+
215
+ deleted = 0
216
+ now = datetime.now()
217
+
218
+ for f in list(audio_dir.glob("*.mp3")) + list(audio_dir.glob("*.wav")):
219
+ try:
220
+ if max_age_hours == 0:
221
+ f.unlink()
222
+ deleted += 1
223
+ else:
224
+ mtime = datetime.fromtimestamp(f.stat().st_mtime)
225
+ age_hours = (now - mtime).total_seconds() / 3600
226
+ if age_hours > max_age_hours:
227
+ f.unlink()
228
+ deleted += 1
229
+ except Exception:
230
+ pass
231
+
232
+ return {
233
+ "success": True,
234
+ "deleted": deleted,
235
+ "max_age_hours": max_age_hours,
236
+ }
237
+
238
+ except Exception as e:
239
+ return {"success": False, "error": str(e)}
240
+
241
+
242
+ async def check_audio_status_handler() -> dict:
243
+ """Check WSL audio connectivity and available playback methods."""
244
+ try:
245
+ from . import check_wsl_audio
246
+
247
+ status = check_wsl_audio()
248
+ status["success"] = True
249
+ status["timestamp"] = datetime.now().isoformat()
250
+ return status
251
+
252
+ except Exception as e:
253
+ return {"success": False, "error": str(e)}
254
+
255
+
256
+ # EOF