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.
- scitex/__init__.py +6 -2
- scitex/__version__.py +1 -1
- scitex/audio/README.md +52 -0
- scitex/audio/__init__.py +384 -0
- scitex/audio/__main__.py +129 -0
- scitex/audio/_tts.py +334 -0
- scitex/audio/engines/__init__.py +44 -0
- scitex/audio/engines/base.py +275 -0
- scitex/audio/engines/elevenlabs_engine.py +143 -0
- scitex/audio/engines/gtts_engine.py +162 -0
- scitex/audio/engines/pyttsx3_engine.py +131 -0
- scitex/audio/mcp_server.py +757 -0
- scitex/bridge/_helpers.py +1 -1
- scitex/bridge/_plt_vis.py +1 -1
- scitex/bridge/_stats_vis.py +1 -1
- scitex/dev/plt/__init__.py +272 -0
- scitex/dev/plt/plot_mpl_axhline.py +28 -0
- scitex/dev/plt/plot_mpl_axhspan.py +28 -0
- scitex/dev/plt/plot_mpl_axvline.py +28 -0
- scitex/dev/plt/plot_mpl_axvspan.py +28 -0
- scitex/dev/plt/plot_mpl_bar.py +29 -0
- scitex/dev/plt/plot_mpl_barh.py +29 -0
- scitex/dev/plt/plot_mpl_boxplot.py +28 -0
- scitex/dev/plt/plot_mpl_contour.py +31 -0
- scitex/dev/plt/plot_mpl_contourf.py +31 -0
- scitex/dev/plt/plot_mpl_errorbar.py +30 -0
- scitex/dev/plt/plot_mpl_eventplot.py +28 -0
- scitex/dev/plt/plot_mpl_fill.py +30 -0
- scitex/dev/plt/plot_mpl_fill_between.py +31 -0
- scitex/dev/plt/plot_mpl_hexbin.py +28 -0
- scitex/dev/plt/plot_mpl_hist.py +28 -0
- scitex/dev/plt/plot_mpl_hist2d.py +28 -0
- scitex/dev/plt/plot_mpl_imshow.py +29 -0
- scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
- scitex/dev/plt/plot_mpl_pie.py +29 -0
- scitex/dev/plt/plot_mpl_plot.py +29 -0
- scitex/dev/plt/plot_mpl_quiver.py +31 -0
- scitex/dev/plt/plot_mpl_scatter.py +28 -0
- scitex/dev/plt/plot_mpl_stackplot.py +31 -0
- scitex/dev/plt/plot_mpl_stem.py +29 -0
- scitex/dev/plt/plot_mpl_step.py +29 -0
- scitex/dev/plt/plot_mpl_violinplot.py +28 -0
- scitex/dev/plt/plot_sns_barplot.py +29 -0
- scitex/dev/plt/plot_sns_boxplot.py +29 -0
- scitex/dev/plt/plot_sns_heatmap.py +28 -0
- scitex/dev/plt/plot_sns_histplot.py +29 -0
- scitex/dev/plt/plot_sns_kdeplot.py +29 -0
- scitex/dev/plt/plot_sns_lineplot.py +31 -0
- scitex/dev/plt/plot_sns_scatterplot.py +29 -0
- scitex/dev/plt/plot_sns_stripplot.py +29 -0
- scitex/dev/plt/plot_sns_swarmplot.py +29 -0
- scitex/dev/plt/plot_sns_violinplot.py +29 -0
- scitex/dev/plt/plot_stx_bar.py +29 -0
- scitex/dev/plt/plot_stx_barh.py +29 -0
- scitex/dev/plt/plot_stx_box.py +28 -0
- scitex/dev/plt/plot_stx_boxplot.py +28 -0
- scitex/dev/plt/plot_stx_conf_mat.py +28 -0
- scitex/dev/plt/plot_stx_contour.py +31 -0
- scitex/dev/plt/plot_stx_ecdf.py +28 -0
- scitex/dev/plt/plot_stx_errorbar.py +30 -0
- scitex/dev/plt/plot_stx_fill_between.py +31 -0
- scitex/dev/plt/plot_stx_fillv.py +28 -0
- scitex/dev/plt/plot_stx_heatmap.py +28 -0
- scitex/dev/plt/plot_stx_image.py +28 -0
- scitex/dev/plt/plot_stx_imshow.py +28 -0
- scitex/dev/plt/plot_stx_joyplot.py +28 -0
- scitex/dev/plt/plot_stx_kde.py +28 -0
- scitex/dev/plt/plot_stx_line.py +28 -0
- scitex/dev/plt/plot_stx_mean_ci.py +28 -0
- scitex/dev/plt/plot_stx_mean_std.py +28 -0
- scitex/dev/plt/plot_stx_median_iqr.py +28 -0
- scitex/dev/plt/plot_stx_raster.py +28 -0
- scitex/dev/plt/plot_stx_rectangle.py +28 -0
- scitex/dev/plt/plot_stx_scatter.py +29 -0
- scitex/dev/plt/plot_stx_shaded_line.py +29 -0
- scitex/dev/plt/plot_stx_violin.py +28 -0
- scitex/dev/plt/plot_stx_violinplot.py +28 -0
- scitex/diagram/README.md +197 -0
- scitex/diagram/__init__.py +48 -0
- scitex/diagram/_compile.py +312 -0
- scitex/diagram/_diagram.py +355 -0
- scitex/diagram/_presets.py +173 -0
- scitex/diagram/_schema.py +182 -0
- scitex/diagram/_split.py +278 -0
- scitex/fig/__init__.py +352 -0
- scitex/{vis → fig}/backend/_parser.py +1 -1
- scitex/{vis → fig}/canvas.py +1 -1
- scitex/{vis → fig}/editor/__init__.py +5 -2
- scitex/{vis → fig}/editor/_dearpygui_editor.py +1 -1
- scitex/{vis → fig}/editor/_defaults.py +70 -5
- scitex/{vis → fig}/editor/_mpl_editor.py +1 -1
- scitex/{vis → fig}/editor/_qt_editor.py +182 -2
- scitex/{vis → fig}/editor/_tkinter_editor.py +1 -1
- scitex/fig/editor/edit/__init__.py +50 -0
- scitex/fig/editor/edit/backend_detector.py +109 -0
- scitex/fig/editor/edit/bundle_resolver.py +240 -0
- scitex/fig/editor/edit/editor_launcher.py +239 -0
- scitex/fig/editor/edit/manual_handler.py +53 -0
- scitex/fig/editor/edit/panel_loader.py +232 -0
- scitex/fig/editor/edit/path_resolver.py +67 -0
- scitex/fig/editor/flask_editor/_bbox.py +1299 -0
- scitex/fig/editor/flask_editor/_core.py +1429 -0
- scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
- scitex/fig/editor/flask_editor/_renderer.py +813 -0
- scitex/fig/editor/flask_editor/static/css/base/reset.css +41 -0
- scitex/fig/editor/flask_editor/static/css/base/typography.css +16 -0
- scitex/fig/editor/flask_editor/static/css/base/variables.css +85 -0
- scitex/fig/editor/flask_editor/static/css/components/buttons.css +217 -0
- scitex/fig/editor/flask_editor/static/css/components/context-menu.css +93 -0
- scitex/fig/editor/flask_editor/static/css/components/dropdown.css +57 -0
- scitex/fig/editor/flask_editor/static/css/components/forms.css +112 -0
- scitex/fig/editor/flask_editor/static/css/components/modal.css +59 -0
- scitex/fig/editor/flask_editor/static/css/components/sections.css +212 -0
- scitex/fig/editor/flask_editor/static/css/features/canvas.css +176 -0
- scitex/fig/editor/flask_editor/static/css/features/element-inspector.css +190 -0
- scitex/fig/editor/flask_editor/static/css/features/loading.css +59 -0
- scitex/fig/editor/flask_editor/static/css/features/overlay.css +45 -0
- scitex/fig/editor/flask_editor/static/css/features/panel-grid.css +95 -0
- scitex/fig/editor/flask_editor/static/css/features/selection.css +101 -0
- scitex/fig/editor/flask_editor/static/css/features/statistics.css +138 -0
- scitex/fig/editor/flask_editor/static/css/index.css +31 -0
- scitex/fig/editor/flask_editor/static/css/layout/container.css +7 -0
- scitex/fig/editor/flask_editor/static/css/layout/controls.css +56 -0
- scitex/fig/editor/flask_editor/static/css/layout/preview.css +78 -0
- scitex/fig/editor/flask_editor/static/js/alignment/axis.js +314 -0
- scitex/fig/editor/flask_editor/static/js/alignment/basic.js +107 -0
- scitex/fig/editor/flask_editor/static/js/alignment/distribute.js +54 -0
- scitex/fig/editor/flask_editor/static/js/canvas/canvas.js +172 -0
- scitex/fig/editor/flask_editor/static/js/canvas/dragging.js +258 -0
- scitex/fig/editor/flask_editor/static/js/canvas/resize.js +48 -0
- scitex/fig/editor/flask_editor/static/js/canvas/selection.js +71 -0
- scitex/fig/editor/flask_editor/static/js/core/api.js +288 -0
- scitex/fig/editor/flask_editor/static/js/core/state.js +143 -0
- scitex/fig/editor/flask_editor/static/js/core/utils.js +245 -0
- scitex/fig/editor/flask_editor/static/js/dev/element-inspector.js +992 -0
- scitex/fig/editor/flask_editor/static/js/editor/bbox.js +339 -0
- scitex/fig/editor/flask_editor/static/js/editor/element-drag.js +286 -0
- scitex/fig/editor/flask_editor/static/js/editor/overlay.js +371 -0
- scitex/fig/editor/flask_editor/static/js/editor/preview.js +293 -0
- scitex/fig/editor/flask_editor/static/js/main.js +426 -0
- scitex/fig/editor/flask_editor/static/js/shortcuts/context-menu.js +152 -0
- scitex/fig/editor/flask_editor/static/js/shortcuts/keyboard.js +265 -0
- scitex/fig/editor/flask_editor/static/js/ui/controls.js +184 -0
- scitex/fig/editor/flask_editor/static/js/ui/download.js +57 -0
- scitex/fig/editor/flask_editor/static/js/ui/help.js +100 -0
- scitex/fig/editor/flask_editor/static/js/ui/theme.js +34 -0
- scitex/fig/editor/flask_editor/templates/__init__.py +123 -0
- scitex/fig/editor/flask_editor/templates/_html.py +852 -0
- scitex/fig/editor/flask_editor/templates/_scripts.py +4933 -0
- scitex/fig/editor/flask_editor/templates/_styles.py +1658 -0
- scitex/{vis → fig}/io/__init__.py +13 -1
- scitex/fig/io/_bundle.py +1058 -0
- scitex/{vis → fig}/io/_canvas.py +1 -1
- scitex/{vis → fig}/io/_data.py +1 -1
- scitex/{vis → fig}/io/_export.py +1 -1
- scitex/{vis → fig}/io/_load.py +1 -1
- scitex/{vis → fig}/io/_panel.py +1 -1
- scitex/{vis → fig}/io/_save.py +1 -1
- scitex/{vis → fig}/model/__init__.py +1 -1
- scitex/{vis → fig}/model/_annotations.py +1 -1
- scitex/{vis → fig}/model/_axes.py +1 -1
- scitex/{vis → fig}/model/_figure.py +1 -1
- scitex/{vis → fig}/model/_guides.py +1 -1
- scitex/{vis → fig}/model/_plot.py +1 -1
- scitex/{vis → fig}/model/_styles.py +1 -1
- scitex/{vis → fig}/utils/__init__.py +1 -1
- scitex/io/__init__.py +22 -26
- scitex/io/_bundle.py +493 -0
- scitex/io/_flush.py +5 -2
- scitex/io/_load.py +98 -0
- scitex/io/_load_modules/_H5Explorer.py +5 -2
- scitex/io/_load_modules/_canvas.py +2 -2
- scitex/io/_load_modules/_image.py +3 -4
- scitex/io/_load_modules/_txt.py +4 -2
- scitex/io/_metadata.py +34 -324
- scitex/io/_metadata_modules/__init__.py +46 -0
- scitex/io/_metadata_modules/_embed.py +70 -0
- scitex/io/_metadata_modules/_read.py +64 -0
- scitex/io/_metadata_modules/_utils.py +79 -0
- scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
- scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
- scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
- scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
- scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
- scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
- scitex/io/_metadata_modules/read_metadata_png.py +39 -0
- scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
- scitex/io/_qr_utils.py +5 -3
- scitex/io/_save.py +548 -30
- scitex/io/_save_modules/_canvas.py +3 -3
- scitex/io/_save_modules/_image.py +5 -9
- scitex/io/_save_modules/_tex.py +7 -4
- scitex/io/_zip_bundle.py +439 -0
- scitex/io/utils/h5_to_zarr.py +11 -9
- scitex/msword/__init__.py +255 -0
- scitex/msword/profiles.py +357 -0
- scitex/msword/reader.py +753 -0
- scitex/msword/utils.py +289 -0
- scitex/msword/writer.py +362 -0
- scitex/plt/__init__.py +5 -2
- scitex/plt/_subplots/_AxesWrapper.py +6 -6
- scitex/plt/_subplots/_AxisWrapper.py +15 -9
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
- scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
- scitex/plt/_subplots/_FigWrapper.py +6 -6
- scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
- scitex/plt/_subplots/_export_as_csv.py +35 -5
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
- scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
- scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
- scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
- scitex/plt/gallery/_generate.py +421 -14
- scitex/plt/io/__init__.py +53 -0
- scitex/plt/io/_bundle.py +490 -0
- scitex/plt/io/_layered_bundle.py +1343 -0
- scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
- scitex/plt/styles/__init__.py +14 -0
- scitex/plt/styles/presets.py +78 -0
- scitex/plt/utils/__init__.py +13 -1
- scitex/plt/utils/_collect_figure_metadata.py +10 -14
- scitex/plt/utils/_configure_mpl.py +6 -18
- scitex/plt/utils/_crop.py +32 -14
- scitex/plt/utils/_csv_column_naming.py +54 -0
- scitex/plt/utils/_figure_mm.py +116 -1
- scitex/plt/utils/_hitmap.py +1643 -0
- scitex/plt/utils/metadata/__init__.py +25 -0
- scitex/plt/utils/metadata/_core.py +9 -10
- scitex/plt/utils/metadata/_dimensions.py +6 -3
- scitex/plt/utils/metadata/_editable_export.py +405 -0
- scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
- scitex/schema/__init__.py +109 -16
- scitex/schema/_canvas.py +1 -1
- scitex/schema/_plot.py +1015 -0
- scitex/schema/_stats.py +2 -2
- scitex/stats/__init__.py +117 -0
- scitex/stats/io/__init__.py +29 -0
- scitex/stats/io/_bundle.py +156 -0
- scitex/tex/__init__.py +4 -0
- scitex/tex/_export.py +890 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/METADATA +11 -1
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/RECORD +294 -170
- scitex/io/memo.md +0 -2827
- scitex/plt/REQUESTS.md +0 -191
- scitex/plt/_subplots/TODO.md +0 -53
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
- scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
- scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
- scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
- scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
- scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
- scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
- scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
- scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
- scitex/vis/__init__.py +0 -177
- scitex/vis/editor/_edit.py +0 -390
- scitex/vis/editor/flask_editor/_bbox.py +0 -529
- scitex/vis/editor/flask_editor/_core.py +0 -168
- scitex/vis/editor/flask_editor/_renderer.py +0 -393
- scitex/vis/editor/flask_editor/templates/__init__.py +0 -33
- scitex/vis/editor/flask_editor/templates/_html.py +0 -513
- scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
- scitex/vis/editor/flask_editor/templates/_styles.py +0 -739
- /scitex/{vis → fig}/README.md +0 -0
- /scitex/{vis → fig}/backend/__init__.py +0 -0
- /scitex/{vis → fig}/backend/_export.py +0 -0
- /scitex/{vis → fig}/backend/_render.py +0 -0
- /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
- /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
- /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
- /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
- /scitex/{vis → fig}/io/_directory.py +0 -0
- /scitex/{vis → fig}/model/_plot_types.py +0 -0
- /scitex/{vis → fig}/utils/_defaults.py +0 -0
- /scitex/{vis → fig}/utils/_validate.py +0 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
- {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
|