Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ AM_CONDITIONAL(ENABLE_DOC, test "$enable_doc" != "no")
# Check for python support.
# Python 3 or greater required
AC_ARG_ENABLE([python],
[AS_HELP_STRING([--disable-python], [do not install python bindings])],
[AS_HELP_STRING([--disable-python], [do not install python bindings and python output-module helpers])],
[],
[enable_python=check])
AS_IF([test $enable_python != "no"],
Expand All @@ -233,6 +233,11 @@ AS_IF([test $enable_python != "no"],
[AS_IF([test $enable_python = "yes"],
[AC_MSG_FAILURE([python 3 or greater is not available])])])])
AM_CONDITIONAL([HAVE_PYTHON], [test $enable_python = "yes"])
AS_IF([test "$enable_python" = "yes"],
[TEST_PYTHON_ENABLED=yes],
[TEST_PYTHON_ENABLED=no])
AC_SUBST([TEST_PYTHON_ENABLED])
AC_SUBST([PYTHON])

output_modules="cicero dummy festival openjtalk generic"
# checks for output modules
Expand Down Expand Up @@ -721,6 +726,7 @@ AC_CONFIG_FILES([Makefile
src/common/Makefile
src/modules/Makefile
src/server/Makefile
src/tests/atlocal
src/tests/Makefile],
[chmod +x run-speechd run-spd-say])
AC_OUTPUT
Expand Down
8 changes: 8 additions & 0 deletions src/modules/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ EXTRA_DIST += dummy-message-default.wav
EXTRA_DIST += dummy-message.txt
CLEANFILES = dummy-message.wav

if HAVE_PYTHON
speechd_python_modules_pythondir = $(pythondir)/speechd_python_modules
dist_speechd_python_modules_python_PYTHON = \
speechd_python_modules/__init__.py \
speechd_python_modules/module_utils.py \
speechd_python_modules/speechd_types.py
endif

Comment thread
sthibaul marked this conversation as resolved.
inc_local = -I$(top_srcdir)/include -I$(top_srcdir)/src/common

if DARWIN_HOST
Expand Down
30 changes: 30 additions & 0 deletions src/modules/speechd_python_modules/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#
# __init__.py - Python output module helpers package
#
# Copyright (C) 2026 Jean-François David <jeanfrancoismanutea@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#


"""Helpers for Python Speech Dispatcher output modules."""
77 changes: 77 additions & 0 deletions src/modules/speechd_python_modules/module_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#
# module_utils.py - Utilities for Python output modules
#
# Copyright (C) 2026 Jean-François David <jeanfrancoismanutea@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#


def module_strip_ssml(message: str) -> str:
Comment thread
sthibaul marked this conversation as resolved.
out = []
append = out.append
omit = False
i = 0
length = len(message)

while i < length:
char = message[i]

if char == "<":
omit = True
i += 1
continue
if char == ">":
omit = False
i += 1
continue
if omit:
i += 1
continue

if char == "&":
if message.startswith("&lt;", i):
append("<")
i += 4
continue
if message.startswith("&gt;", i):
append(">")
i += 4
continue
if message.startswith("&amp;", i):
append("&")
i += 5
continue
if message.startswith("&quot;", i):
append('"')
i += 6
continue
if message.startswith("&apos;", i):
append("'")
i += 6
continue

append(char)
i += 1

return "".join(out)
53 changes: 53 additions & 0 deletions src/modules/speechd_python_modules/speechd_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#
# speechd_types.py - Types for Python output modules
#
# Copyright (C) 2026 Jean-François David <jeanfrancoismanutea@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#


SPD_MSGTYPE_TEXT = 0
SPD_MSGTYPE_SOUND_ICON = 1
SPD_MSGTYPE_CHAR = 2
SPD_MSGTYPE_KEY = 3
SPD_MSGTYPE_SPELL = 99

SPD_AUDIO_LE = 0
SPD_AUDIO_BE = 1


class SPDVoice:
def __init__(self, name=None, language=None, variant=None):
self.name = name
self.language = language
self.variant = variant


class AudioTrack:
def __init__(self, bits, num_channels, sample_rate, num_samples, samples):
self.bits = bits
self.num_channels = num_channels
self.sample_rate = sample_rate
self.num_samples = num_samples
self.samples = samples
1 change: 1 addition & 0 deletions src/tests/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
/Makefile.in
/TAGS
/atconfig
/atlocal
/clibrary
/clibrary2
/clibrary3
Expand Down
8 changes: 4 additions & 4 deletions src/tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

## Process this file with automake to produce Makefile.in

DISTCLEANFILES = atconfig $(TESTSUITE)
DISTCLEANFILES = atconfig atlocal $(TESTSUITE)

c_api = $(top_builddir)/src/api/c

Expand All @@ -42,7 +42,7 @@ atconfig: $(top_builddir)/config.status
AUTOM4TE = autom4te
AUTOTEST = $(AUTOM4TE) --language=autotest

TESTSUITE_AT = c_api.at
TESTSUITE_AT = c_api.at python_module.at
TESTSUITE = ./testsuite
$(TESTSUITE): package.m4 testsuite.at $(TESTSUITE_AT)
$(AUTOTEST) -I '$(srcdir)' -o $@.tmp $@.at
Expand Down Expand Up @@ -79,7 +79,7 @@ run_test_LDADD = $(c_api)/libspeechd.la $(GLIB_LIBS) $(EXTRA_SOCKET_LIBS)
EXTRA_DIST= basic.test general.test keys.test priority_progress.test \
pronunciation.test punctuation.test sound_icons.test spelling.test \
ssml.test stop_and_pause.test voices.test yo.wav \
testsuite.at $(TESTSUITE_AT) sayfortune.sh
atlocal.in testsuite.at $(TESTSUITE_AT) sayfortune.sh

clean-local:
test ! -f $(TESTSUITE) || $(SHELL) $(TESTSUITE) --clean
Expand All @@ -88,7 +88,7 @@ clean-local:

# Run the test suite on the *installed* tree.
testinstall: atconfig $(TESTSUITE)
$(SHELL) $(TESTSUITE) AUTOTEST_PATH="$(bindir)" $(TESTSUITEFLAGS)
$(SHELL) $(TESTSUITE) AUTOTEST_PATH="$(bindir)" TEST_PYTHONPATH="$(DESTDIR)$(pythondir)" $(TESTSUITEFLAGS)

CLEANFILES = package.m4

Expand Down
2 changes: 2 additions & 0 deletions src/tests/atlocal.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST_PYTHON_ENABLED='@TEST_PYTHON_ENABLED@'
PYTHON='@PYTHON@'
138 changes: 138 additions & 0 deletions src/tests/python_module.at
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# python_module.at - python module tests
#
# Copyright (C) 2026 Jean-François David
#
# This is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

AT_BANNER([Python module])

AT_SETUP([Python module imports])
AT_KEYWORDS([python import])

AT_SKIP_IF([test "x$TEST_PYTHON_ENABLED" != xyes])

AT_CHECK([[test_pythonpath=${TEST_PYTHONPATH-"$abs_top_srcdir/src/modules"}
PYTHONDONTWRITEBYTECODE=1 \
PYTHONNOUSERSITE=1 \
PYTHONPATH="$test_pythonpath" \
"$PYTHON" - "$test_pythonpath" <<'PY'
import os
import sys

from speechd_python_modules import module_utils, speechd_types

pythonpath_dir = os.path.realpath(sys.argv[1])
module_dir = os.path.join(pythonpath_dir, "speechd_python_modules")
modules = [module_utils, speechd_types]


def check_module_path(module):
module_file = os.path.realpath(module.__file__)
if os.path.dirname(module_file) != module_dir:
raise SystemExit(
"%s imported from %r, expected %r"
% (module.__name__, module_file, module_dir)
)


for module in modules:
check_module_path(module)
PY
]], [0], [], [])

AT_CLEANUP

AT_SETUP([module_strip_ssml])
AT_KEYWORDS([python module_utils ssml])

AT_SKIP_IF([test "x$TEST_PYTHON_ENABLED" != xyes])

AT_CHECK([[test_pythonpath=${TEST_PYTHONPATH-"$abs_top_srcdir/src/modules"}
PYTHONDONTWRITEBYTECODE=1 \
PYTHONNOUSERSITE=1 \
PYTHONPATH="$test_pythonpath" \
"$PYTHON" - <<'PY'
from speechd_python_modules import module_utils

strip_ssml = module_utils.module_strip_ssml


def check(message, expected):
actual = strip_ssml(message)
if actual != expected:
raise SystemExit(
"strip_ssml(%r) returned %r, expected %r"
% (message, actual, expected)
)


check("Plain text.", "Plain text.")
check("Hello <emphasis>world</emphasis>.", "Hello world.")
check('<speak>Hello <break time="500ms"/>world</speak>', "Hello world")
check('<tag attr="&amp;">text</tag>', "text")
check(
"Use &lt;tag&gt;, &amp;, &quot;quotes&quot; and &apos;apostrophes&apos;.",
"Use <tag>, &, \"quotes\" and 'apostrophes'.",
)
check("Keep <unfinished", "Keep ")
check("Unknown &copy; entity stays.", "Unknown &copy; entity stays.")
PY
]], [0], [], [])

AT_CLEANUP

AT_SETUP([Python module types])
AT_KEYWORDS([python speechd_types])

AT_SKIP_IF([test "x$TEST_PYTHON_ENABLED" != xyes])

AT_CHECK([[test_pythonpath=${TEST_PYTHONPATH-"$abs_top_srcdir/src/modules"}
PYTHONDONTWRITEBYTECODE=1 \
PYTHONNOUSERSITE=1 \
PYTHONPATH="$test_pythonpath" \
"$PYTHON" - <<'PY'
from speechd_python_modules import speechd_types

if speechd_types.SPD_MSGTYPE_TEXT != 0:
raise SystemExit("unexpected SPD_MSGTYPE_TEXT value")
if speechd_types.SPD_MSGTYPE_SPELL != 99:
raise SystemExit("unexpected SPD_MSGTYPE_SPELL value")
if speechd_types.SPD_AUDIO_LE != 0:
raise SystemExit("unexpected SPD_AUDIO_LE value")
if speechd_types.SPD_AUDIO_BE != 1:
raise SystemExit("unexpected SPD_AUDIO_BE value")

voice = speechd_types.SPDVoice(name="name", language="en", variant="male")
if (voice.name, voice.language, voice.variant) != ("name", "en", "male"):
raise SystemExit("unexpected SPDVoice fields")

track = speechd_types.AudioTrack(
bits=16,
num_channels=1,
sample_rate=22050,
num_samples=2,
samples=b"\0\0\1\0",
)
if (
track.bits,
track.num_channels,
track.sample_rate,
track.num_samples,
track.samples,
) != (16, 1, 22050, 2, b"\0\0\1\0"):
raise SystemExit("unexpected AudioTrack fields")
PY
]], [0], [], [])

AT_CLEANUP
Loading
Loading