scitex 2.7.0__py3-none-any.whl → 2.7.3__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/fig/__init__.py +352 -0
- scitex/{vis → fig}/backend/_parser.py +1 -1
- scitex/{vis → fig}/canvas.py +1 -1
- scitex/{vis → fig}/editor/_defaults.py +70 -5
- scitex/fig/editor/_edit.py +751 -0
- scitex/{vis → fig}/editor/_qt_editor.py +181 -1
- scitex/fig/editor/flask_editor/_bbox.py +1276 -0
- scitex/fig/editor/flask_editor/_core.py +624 -0
- scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
- scitex/fig/editor/flask_editor/_renderer.py +739 -0
- scitex/{vis → fig}/editor/flask_editor/templates/__init__.py +1 -1
- scitex/fig/editor/flask_editor/templates/_html.py +834 -0
- scitex/fig/editor/flask_editor/templates/_scripts.py +3136 -0
- scitex/{vis → fig}/editor/flask_editor/templates/_styles.py +625 -18
- scitex/{vis → fig}/io/__init__.py +13 -1
- scitex/fig/io/_bundle.py +973 -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 +10 -26
- scitex/io/_bundle.py +434 -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/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.7.3.dist-info}/METADATA +11 -1
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/RECORD +238 -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/_html.py +0 -513
- scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
- /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/__init__.py +0 -0
- /scitex/{vis → fig}/editor/_dearpygui_editor.py +0 -0
- /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
- /scitex/{vis → fig}/editor/_mpl_editor.py +0 -0
- /scitex/{vis → fig}/editor/_tkinter_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.7.3.dist-info}/WHEEL +0 -0
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/licenses/LICENSE +0 -0
scitex/audio/_tts.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
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/_tts.py
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Text-to-Speech implementation using ElevenLabs API.
|
|
9
|
+
|
|
10
|
+
This module provides TTS functionality that can be used:
|
|
11
|
+
1. Directly via the ElevenLabs Python SDK
|
|
12
|
+
2. Via MCP server integration
|
|
13
|
+
|
|
14
|
+
Environment Variables:
|
|
15
|
+
ELEVENLABS_API_KEY: Your ElevenLabs API key
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import subprocess
|
|
22
|
+
import tempfile
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
__all__ = ["TTS", "speak"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TTSConfig:
|
|
32
|
+
"""Configuration for TTS."""
|
|
33
|
+
|
|
34
|
+
voice_id: str = "21m00Tcm4TlvDq8ikWAM" # Rachel (default)
|
|
35
|
+
voice_name: Optional[str] = None
|
|
36
|
+
model_id: str = "eleven_multilingual_v2"
|
|
37
|
+
stability: float = 0.5
|
|
38
|
+
similarity_boost: float = 0.75
|
|
39
|
+
style: float = 0.0
|
|
40
|
+
speed: float = 1.0
|
|
41
|
+
output_format: str = "mp3_44100_128"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TTS:
|
|
45
|
+
"""Text-to-Speech using ElevenLabs API.
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
# Basic usage
|
|
49
|
+
tts = TTS()
|
|
50
|
+
tts.speak("Hello, world!")
|
|
51
|
+
|
|
52
|
+
# With custom voice
|
|
53
|
+
tts = TTS(voice_name="Rachel")
|
|
54
|
+
tts.speak("Processing complete")
|
|
55
|
+
|
|
56
|
+
# Save to file without playing
|
|
57
|
+
tts.speak("Test", output_path="/tmp/test.mp3", play=False)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# Popular voice presets
|
|
61
|
+
VOICES = {
|
|
62
|
+
"rachel": "21m00Tcm4TlvDq8ikWAM",
|
|
63
|
+
"adam": "pNInz6obpgDQGcFmaJgB",
|
|
64
|
+
"antoni": "ErXwobaYiN019PkySvjV",
|
|
65
|
+
"bella": "EXAVITQu4vr4xnSDxMaL",
|
|
66
|
+
"domi": "AZnzlk1XvdvUeBnXmlld",
|
|
67
|
+
"elli": "MF3mGyEYCl7XYWbV9V6O",
|
|
68
|
+
"josh": "TxGEqnHWrfWFTfGW9XjX",
|
|
69
|
+
"sam": "yoZ06aMxZJJ28mfd3POQ",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
api_key: Optional[str] = None,
|
|
75
|
+
voice_name: Optional[str] = None,
|
|
76
|
+
voice_id: Optional[str] = None,
|
|
77
|
+
**kwargs,
|
|
78
|
+
):
|
|
79
|
+
"""Initialize TTS.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
api_key: ElevenLabs API key. Defaults to ELEVENLABS_API_KEY env var.
|
|
83
|
+
voice_name: Voice name (e.g., "Rachel", "Adam").
|
|
84
|
+
voice_id: Direct voice ID (overrides voice_name).
|
|
85
|
+
**kwargs: Additional config options (stability, speed, etc.)
|
|
86
|
+
"""
|
|
87
|
+
self.api_key = api_key or os.environ.get("ELEVENLABS_API_KEY")
|
|
88
|
+
self.config = TTSConfig(**kwargs)
|
|
89
|
+
|
|
90
|
+
if voice_id:
|
|
91
|
+
self.config.voice_id = voice_id
|
|
92
|
+
elif voice_name:
|
|
93
|
+
self.config.voice_name = voice_name
|
|
94
|
+
normalized = voice_name.lower()
|
|
95
|
+
if normalized in self.VOICES:
|
|
96
|
+
self.config.voice_id = self.VOICES[normalized]
|
|
97
|
+
|
|
98
|
+
self._client = None
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def client(self):
|
|
102
|
+
"""Lazy-load ElevenLabs client."""
|
|
103
|
+
if self._client is None:
|
|
104
|
+
try:
|
|
105
|
+
from elevenlabs.client import ElevenLabs
|
|
106
|
+
|
|
107
|
+
self._client = ElevenLabs(api_key=self.api_key)
|
|
108
|
+
except ImportError:
|
|
109
|
+
raise ImportError(
|
|
110
|
+
"elevenlabs package not installed. "
|
|
111
|
+
"Install with: pip install elevenlabs"
|
|
112
|
+
)
|
|
113
|
+
return self._client
|
|
114
|
+
|
|
115
|
+
def speak(
|
|
116
|
+
self,
|
|
117
|
+
text: str,
|
|
118
|
+
output_path: Optional[str] = None,
|
|
119
|
+
play: bool = True,
|
|
120
|
+
voice_name: Optional[str] = None,
|
|
121
|
+
voice_id: Optional[str] = None,
|
|
122
|
+
) -> Optional[Path]:
|
|
123
|
+
"""Convert text to speech and optionally play it.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
text: Text to convert to speech.
|
|
127
|
+
output_path: Path to save audio file. Auto-generated if None.
|
|
128
|
+
play: Whether to play the audio after generation.
|
|
129
|
+
voice_name: Override voice name for this call.
|
|
130
|
+
voice_id: Override voice ID for this call.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Path to the generated audio file, or None if only played.
|
|
134
|
+
"""
|
|
135
|
+
# Determine voice
|
|
136
|
+
vid = voice_id or self.config.voice_id
|
|
137
|
+
if voice_name and not voice_id:
|
|
138
|
+
normalized = voice_name.lower()
|
|
139
|
+
vid = self.VOICES.get(normalized, vid)
|
|
140
|
+
|
|
141
|
+
# Generate audio
|
|
142
|
+
audio = self.client.text_to_speech.convert(
|
|
143
|
+
text=text,
|
|
144
|
+
voice_id=vid,
|
|
145
|
+
model_id=self.config.model_id,
|
|
146
|
+
voice_settings={
|
|
147
|
+
"stability": self.config.stability,
|
|
148
|
+
"similarity_boost": self.config.similarity_boost,
|
|
149
|
+
"style": self.config.style,
|
|
150
|
+
"speed": self.config.speed,
|
|
151
|
+
},
|
|
152
|
+
output_format=self.config.output_format,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Determine output path
|
|
156
|
+
if output_path:
|
|
157
|
+
out_path = Path(output_path)
|
|
158
|
+
else:
|
|
159
|
+
suffix = ".mp3" if "mp3" in self.config.output_format else ".wav"
|
|
160
|
+
fd, tmp_path = tempfile.mkstemp(suffix=suffix, prefix="scitex_tts_")
|
|
161
|
+
os.close(fd)
|
|
162
|
+
out_path = Path(tmp_path)
|
|
163
|
+
|
|
164
|
+
# Write audio to file
|
|
165
|
+
with open(out_path, "wb") as f:
|
|
166
|
+
for chunk in audio:
|
|
167
|
+
f.write(chunk)
|
|
168
|
+
|
|
169
|
+
# Play if requested
|
|
170
|
+
if play:
|
|
171
|
+
self._play_audio(out_path)
|
|
172
|
+
|
|
173
|
+
return out_path if output_path else None
|
|
174
|
+
|
|
175
|
+
def _play_audio(self, path: Path) -> None:
|
|
176
|
+
"""Play audio file using available system player.
|
|
177
|
+
|
|
178
|
+
Includes Windows fallback for WSL environments.
|
|
179
|
+
"""
|
|
180
|
+
# Check if we're in WSL - if so, prefer Windows playback directly
|
|
181
|
+
# to avoid double playback issues with Linux audio hanging
|
|
182
|
+
if os.path.exists("/mnt/c/Windows"):
|
|
183
|
+
if self._play_audio_windows(path):
|
|
184
|
+
return
|
|
185
|
+
# Fall through to Linux players if Windows playback fails
|
|
186
|
+
|
|
187
|
+
players = [
|
|
188
|
+
["mpv", "--no-video", str(path)],
|
|
189
|
+
["ffplay", "-nodisp", "-autoexit", str(path)],
|
|
190
|
+
["aplay", str(path)],
|
|
191
|
+
["afplay", str(path)], # macOS
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
for player_cmd in players:
|
|
195
|
+
try:
|
|
196
|
+
subprocess.run(
|
|
197
|
+
player_cmd,
|
|
198
|
+
check=True,
|
|
199
|
+
stdout=subprocess.DEVNULL,
|
|
200
|
+
stderr=subprocess.DEVNULL,
|
|
201
|
+
timeout=30,
|
|
202
|
+
)
|
|
203
|
+
return
|
|
204
|
+
except subprocess.TimeoutExpired:
|
|
205
|
+
# Audio playback hung, don't try more players
|
|
206
|
+
return
|
|
207
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
print(f"Warning: No audio player found. Audio saved to: {path}")
|
|
211
|
+
|
|
212
|
+
def _play_audio_windows(self, path: Path) -> bool:
|
|
213
|
+
"""Play audio via Windows PowerShell SoundPlayer (WSL fallback).
|
|
214
|
+
|
|
215
|
+
Uses headless SoundPlayer - no GUI popup.
|
|
216
|
+
"""
|
|
217
|
+
import shutil
|
|
218
|
+
import tempfile
|
|
219
|
+
|
|
220
|
+
# Check if we're in WSL
|
|
221
|
+
if not os.path.exists("/mnt/c/Windows"):
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
powershell = shutil.which("powershell.exe")
|
|
225
|
+
if not powershell:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
# SoundPlayer only supports WAV, so convert if needed
|
|
230
|
+
wav_path = path
|
|
231
|
+
if path.suffix.lower() in ('.mp3', '.ogg', '.m4a'):
|
|
232
|
+
try:
|
|
233
|
+
from pydub import AudioSegment
|
|
234
|
+
fd, tmp_wav = tempfile.mkstemp(suffix='.wav', prefix='scitex_')
|
|
235
|
+
os.close(fd)
|
|
236
|
+
wav_path = Path(tmp_wav)
|
|
237
|
+
audio = AudioSegment.from_file(str(path))
|
|
238
|
+
audio.export(str(wav_path), format='wav')
|
|
239
|
+
except ImportError:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
result = subprocess.run(
|
|
243
|
+
["wslpath", "-w", str(wav_path)],
|
|
244
|
+
capture_output=True,
|
|
245
|
+
text=True,
|
|
246
|
+
timeout=5,
|
|
247
|
+
)
|
|
248
|
+
if result.returncode != 0:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
windows_path = result.stdout.strip()
|
|
252
|
+
|
|
253
|
+
ps_command = f'''
|
|
254
|
+
$player = New-Object System.Media.SoundPlayer
|
|
255
|
+
$player.SoundLocation = "{windows_path}"
|
|
256
|
+
$player.PlaySync()
|
|
257
|
+
'''
|
|
258
|
+
subprocess.run(
|
|
259
|
+
[powershell, "-NoProfile", "-Command", ps_command],
|
|
260
|
+
stdout=subprocess.DEVNULL,
|
|
261
|
+
stderr=subprocess.DEVNULL,
|
|
262
|
+
timeout=60,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Clean up temp WAV
|
|
266
|
+
if wav_path != path and wav_path.exists():
|
|
267
|
+
try:
|
|
268
|
+
wav_path.unlink()
|
|
269
|
+
except Exception:
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
except Exception:
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
def list_voices(self) -> list:
|
|
278
|
+
"""List available voices from ElevenLabs."""
|
|
279
|
+
response = self.client.voices.get_all()
|
|
280
|
+
return [
|
|
281
|
+
{"name": v.name, "voice_id": v.voice_id, "labels": v.labels}
|
|
282
|
+
for v in response.voices
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# Module-level convenience function
|
|
287
|
+
_default_tts: Optional[TTS] = None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def speak(
|
|
291
|
+
text: str,
|
|
292
|
+
voice: Optional[str] = None,
|
|
293
|
+
play: bool = True,
|
|
294
|
+
output_path: Optional[str] = None,
|
|
295
|
+
**kwargs,
|
|
296
|
+
) -> Optional[Path]:
|
|
297
|
+
"""Convenience function for quick TTS.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
text: Text to speak.
|
|
301
|
+
voice: Voice name (e.g., "Rachel", "Adam").
|
|
302
|
+
play: Whether to play audio.
|
|
303
|
+
output_path: Optional path to save audio.
|
|
304
|
+
**kwargs: Additional TTS config options.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Path to audio file if output_path specified, else None.
|
|
308
|
+
|
|
309
|
+
Examples:
|
|
310
|
+
import scitex
|
|
311
|
+
|
|
312
|
+
# Simple speak
|
|
313
|
+
scitex.audio.speak("Hello!")
|
|
314
|
+
|
|
315
|
+
# With specific voice
|
|
316
|
+
scitex.audio.speak("Processing complete", voice="Adam")
|
|
317
|
+
|
|
318
|
+
# Save without playing
|
|
319
|
+
scitex.audio.speak("Test", play=False, output_path="/tmp/test.mp3")
|
|
320
|
+
"""
|
|
321
|
+
global _default_tts
|
|
322
|
+
|
|
323
|
+
if _default_tts is None or kwargs:
|
|
324
|
+
_default_tts = TTS(**kwargs)
|
|
325
|
+
|
|
326
|
+
return _default_tts.speak(
|
|
327
|
+
text=text,
|
|
328
|
+
voice_name=voice,
|
|
329
|
+
play=play,
|
|
330
|
+
output_path=output_path,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# EOF
|
|
@@ -0,0 +1,44 @@
|
|
|
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/engines/__init__.py
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
TTS Engine Backends
|
|
9
|
+
|
|
10
|
+
Fallback order: pyttsx3 -> gtts -> elevenlabs
|
|
11
|
+
|
|
12
|
+
Engines:
|
|
13
|
+
- SystemTTS (pyttsx3): Offline, free, uses system TTS
|
|
14
|
+
- GoogleTTS (gtts): Free, requires internet
|
|
15
|
+
- ElevenLabsTTS: Paid, high quality
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .base import BaseTTS, TTSBackend
|
|
19
|
+
|
|
20
|
+
# Import engines (fail gracefully if dependencies missing)
|
|
21
|
+
try:
|
|
22
|
+
from .pyttsx3_engine import SystemTTS
|
|
23
|
+
except ImportError:
|
|
24
|
+
SystemTTS = None
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from .gtts_engine import GoogleTTS
|
|
28
|
+
except ImportError:
|
|
29
|
+
GoogleTTS = None
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from .elevenlabs_engine import ElevenLabsTTS
|
|
33
|
+
except ImportError:
|
|
34
|
+
ElevenLabsTTS = None
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"BaseTTS",
|
|
38
|
+
"TTSBackend",
|
|
39
|
+
"SystemTTS",
|
|
40
|
+
"GoogleTTS",
|
|
41
|
+
"ElevenLabsTTS",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# EOF
|
|
@@ -0,0 +1,275 @@
|
|
|
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/engines/base.py
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Base TTS class defining the common interface for all TTS backends.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
|
+
__all__ = ["BaseTTS", "TTSBackend"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TTSBackend:
|
|
22
|
+
"""Enum-like class for TTS backend types."""
|
|
23
|
+
|
|
24
|
+
ELEVENLABS = "elevenlabs"
|
|
25
|
+
GTTS = "gtts"
|
|
26
|
+
PYTTSX3 = "pyttsx3"
|
|
27
|
+
EDGE = "edge" # Future: edge-tts
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def available(cls) -> List[str]:
|
|
31
|
+
"""Return list of available backends."""
|
|
32
|
+
backends = []
|
|
33
|
+
|
|
34
|
+
# Check gTTS (always available if installed, needs internet)
|
|
35
|
+
try:
|
|
36
|
+
import gtts
|
|
37
|
+
|
|
38
|
+
backends.append(cls.GTTS)
|
|
39
|
+
except ImportError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
# Check pyttsx3
|
|
43
|
+
try:
|
|
44
|
+
import pyttsx3
|
|
45
|
+
|
|
46
|
+
backends.append(cls.PYTTSX3)
|
|
47
|
+
except ImportError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# Check ElevenLabs
|
|
51
|
+
try:
|
|
52
|
+
import elevenlabs
|
|
53
|
+
import os
|
|
54
|
+
|
|
55
|
+
if os.environ.get("ELEVENLABS_API_KEY"):
|
|
56
|
+
backends.append(cls.ELEVENLABS)
|
|
57
|
+
except ImportError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
return backends
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BaseTTS(ABC):
|
|
64
|
+
"""Abstract base class for TTS implementations."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, **kwargs):
|
|
67
|
+
self.config = kwargs
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def synthesize(self, text: str, output_path: str) -> Path:
|
|
71
|
+
"""Synthesize text to audio file.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
text: Text to convert to speech.
|
|
75
|
+
output_path: Path to save the audio file.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Path to the generated audio file.
|
|
79
|
+
"""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def get_voices(self) -> List[dict]:
|
|
84
|
+
"""Get available voices for this backend.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of voice dictionaries with 'name' and 'id' keys.
|
|
88
|
+
"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def name(self) -> str:
|
|
94
|
+
"""Return the backend name."""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def requires_api_key(self) -> bool:
|
|
99
|
+
"""Whether this backend requires an API key."""
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def requires_internet(self) -> bool:
|
|
104
|
+
"""Whether this backend requires internet connection."""
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def speak(
|
|
108
|
+
self,
|
|
109
|
+
text: str,
|
|
110
|
+
output_path: Optional[str] = None,
|
|
111
|
+
play: bool = True,
|
|
112
|
+
voice: Optional[str] = None,
|
|
113
|
+
) -> Optional[Path]:
|
|
114
|
+
"""Synthesize and optionally play text.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
text: Text to speak.
|
|
118
|
+
output_path: Optional path to save audio.
|
|
119
|
+
play: Whether to play the audio.
|
|
120
|
+
voice: Optional voice name/id.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Path to audio file if output_path specified, else None.
|
|
124
|
+
"""
|
|
125
|
+
import tempfile
|
|
126
|
+
|
|
127
|
+
# Determine output path
|
|
128
|
+
if output_path:
|
|
129
|
+
out_path = Path(output_path)
|
|
130
|
+
else:
|
|
131
|
+
suffix = ".mp3"
|
|
132
|
+
fd, tmp_path = tempfile.mkstemp(suffix=suffix, prefix="scitex_tts_")
|
|
133
|
+
import os
|
|
134
|
+
|
|
135
|
+
os.close(fd)
|
|
136
|
+
out_path = Path(tmp_path)
|
|
137
|
+
|
|
138
|
+
# Set voice if provided
|
|
139
|
+
if voice:
|
|
140
|
+
self.config["voice"] = voice
|
|
141
|
+
|
|
142
|
+
# Synthesize
|
|
143
|
+
result_path = self.synthesize(text, str(out_path))
|
|
144
|
+
|
|
145
|
+
# Play if requested
|
|
146
|
+
if play:
|
|
147
|
+
self._play_audio(result_path)
|
|
148
|
+
|
|
149
|
+
# Return path only if explicitly requested
|
|
150
|
+
if output_path:
|
|
151
|
+
return result_path
|
|
152
|
+
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
def _play_audio(self, path: Path) -> None:
|
|
156
|
+
"""Play audio file using available system player.
|
|
157
|
+
|
|
158
|
+
Includes Windows fallback for WSL environments where PulseAudio
|
|
159
|
+
may be unstable.
|
|
160
|
+
"""
|
|
161
|
+
import os
|
|
162
|
+
|
|
163
|
+
# Check if we're in WSL - if so, prefer Windows playback directly
|
|
164
|
+
# to avoid double playback issues with Linux audio hanging
|
|
165
|
+
if os.path.exists("/mnt/c/Windows"):
|
|
166
|
+
if self._play_audio_windows(path):
|
|
167
|
+
return
|
|
168
|
+
# Fall through to Linux players if Windows playback fails
|
|
169
|
+
|
|
170
|
+
players = [
|
|
171
|
+
["ffplay", "-nodisp", "-autoexit", str(path)],
|
|
172
|
+
["mpv", "--no-video", str(path)],
|
|
173
|
+
["aplay", str(path)],
|
|
174
|
+
["afplay", str(path)], # macOS
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
for player_cmd in players:
|
|
178
|
+
try:
|
|
179
|
+
subprocess.run(
|
|
180
|
+
player_cmd,
|
|
181
|
+
check=True,
|
|
182
|
+
stdout=subprocess.DEVNULL,
|
|
183
|
+
stderr=subprocess.DEVNULL,
|
|
184
|
+
timeout=30,
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
except subprocess.TimeoutExpired:
|
|
188
|
+
# Audio playback hung, don't try more players
|
|
189
|
+
return
|
|
190
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
print(f"Warning: No audio player found. Audio saved to: {path}")
|
|
194
|
+
|
|
195
|
+
def _play_audio_windows(self, path: Path) -> bool:
|
|
196
|
+
"""Play audio via Windows PowerShell SoundPlayer (WSL fallback).
|
|
197
|
+
|
|
198
|
+
This is useful when WSLg PulseAudio connection is unstable.
|
|
199
|
+
Uses System.Media.SoundPlayer which is headless (no GUI).
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
path: Path to audio file (in WSL filesystem)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if playback succeeded, False otherwise
|
|
206
|
+
"""
|
|
207
|
+
import os
|
|
208
|
+
import shutil
|
|
209
|
+
import tempfile
|
|
210
|
+
|
|
211
|
+
# Check if we're in WSL
|
|
212
|
+
if not os.path.exists("/mnt/c/Windows"):
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
# Check if powershell.exe is available
|
|
216
|
+
powershell = shutil.which("powershell.exe")
|
|
217
|
+
if not powershell:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
# SoundPlayer only supports WAV, so convert if needed
|
|
222
|
+
wav_path = path
|
|
223
|
+
if path.suffix.lower() in ('.mp3', '.ogg', '.m4a'):
|
|
224
|
+
try:
|
|
225
|
+
from pydub import AudioSegment
|
|
226
|
+
# Create temp WAV file
|
|
227
|
+
fd, tmp_wav = tempfile.mkstemp(suffix='.wav', prefix='scitex_')
|
|
228
|
+
os.close(fd)
|
|
229
|
+
wav_path = Path(tmp_wav)
|
|
230
|
+
|
|
231
|
+
audio = AudioSegment.from_file(str(path))
|
|
232
|
+
audio.export(str(wav_path), format='wav')
|
|
233
|
+
except ImportError:
|
|
234
|
+
# pydub not available, try direct playback anyway
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
# Convert WSL path to Windows path
|
|
238
|
+
result = subprocess.run(
|
|
239
|
+
["wslpath", "-w", str(wav_path)],
|
|
240
|
+
capture_output=True,
|
|
241
|
+
text=True,
|
|
242
|
+
timeout=5,
|
|
243
|
+
)
|
|
244
|
+
if result.returncode != 0:
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
windows_path = result.stdout.strip()
|
|
248
|
+
|
|
249
|
+
# Play using PowerShell's SoundPlayer (headless, no GUI)
|
|
250
|
+
ps_command = f'''
|
|
251
|
+
$player = New-Object System.Media.SoundPlayer
|
|
252
|
+
$player.SoundLocation = "{windows_path}"
|
|
253
|
+
$player.PlaySync()
|
|
254
|
+
'''
|
|
255
|
+
subprocess.run(
|
|
256
|
+
[powershell, "-NoProfile", "-Command", ps_command],
|
|
257
|
+
stdout=subprocess.DEVNULL,
|
|
258
|
+
stderr=subprocess.DEVNULL,
|
|
259
|
+
timeout=60,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Clean up temp WAV if created
|
|
263
|
+
if wav_path != path and wav_path.exists():
|
|
264
|
+
try:
|
|
265
|
+
wav_path.unlink()
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, Exception):
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# EOF
|