diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml new file mode 100644 index 0000000..94f22c8 --- /dev/null +++ b/.github/workflows/linux.yaml @@ -0,0 +1,90 @@ +# Test on Linux Ubuntu with festival-2.5 with various Python and espeak versions + +name: Linux + +on: [push, pull_request] + +jobs: + python-version: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ['3.6', '3.7', '3.8', '3.9'] + + steps: + - name: Checkout phonemizer + uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install system dependencies + run: sudo apt-get install espeak-ng festival mbrola mbrola-fr1 + + - name: Install phonemizer + run: | + pip install --upgrade pip pytest pytest-cov + python setup.py install + + - name: Version phonemizer + run: phonemize --version + + - name: Test phonemizer + run: pytest -v --cov=phonemizer --cov-report=xml test/ + + - name: Upload coverage to Codecov + if: ${{ matrix.python-version == '3.9' }} + uses: codecov/codecov-action@v2 + with: + files: coverage.xml + verbose: true + + espeak-version: + runs-on: ubuntu-latest + + strategy: + matrix: + espeak-version: ['1.48.03', '1.49.2', '1.50', 'master'] + + steps: + - name: Checkout phonemizer + uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + + - name: Install system dependencies + run: sudo apt-get install festival mbrola mbrola-fr1 + + - name: Install espeak-1.48 + if: ${{ matrix.espeak-version == '1.48.03' }} + run: sudo apt install espeak + + - name: Install espeak>=1.49 + if: ${{ matrix.espeak-version != '1.48.03' }} + env: + ESPEAK_VERSION: ${{ matrix.espeak-version }} + run: | + sudo apt-get install make autoconf automake libtool pkg-config gcc libsonic-dev libpcaudio-dev git + git clone --depth 1 --branch $ESPEAK_VERSION https://github.com/espeak-ng/espeak-ng.git + cd espeak-ng + ./autogen.sh + ./configure + make + sudo make install + sudo ldconfig + espeak --version + + - name: Install phonemizer + run: | + pip install --upgrade pip pytest + python setup.py install + + - name: Version phonemizer + run: phonemize --version + + - name: Test phonemizer + run: pytest -v diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml new file mode 100644 index 0000000..b9f87c6 --- /dev/null +++ b/.github/workflows/macos.yaml @@ -0,0 +1,57 @@ +# Test on macos with festival-2.4 compiled from source and espeak-1.48 from +# homebrew. To save time and ressources, festival is cached across runs. + +name: MacOS + +on: [push, pull_request] + +jobs: + test: + runs-on: macos-latest + + env: + PHONEMIZER_FESTIVAL_PATH: ${{ github.workspace }}/festival/build_festival/festival/bin/festival + + steps: + - name: Checkout phonemizer + uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + + - name: Install espeak-1.48 + run: | + brew update + brew install espeak + + - name: Cache festival + uses: actions/cache@v2 + id: cache-festival + with: + path: ${{ github.workspace }}/festival + key: ${{ runner.os }}-festival + + - name: Checkout festival + if: steps.cache-festival.outputs.cache-hit != 'true' + uses: actions/checkout@v2 + with: + repository: pettarin/setup-festival-mbrola + path: festival + + - name: Install festival + if: steps.cache-festival.outputs.cache-hit != 'true' + run: | + cd festival + bash setup_festival_mbrola.sh . festival + + - name: Install phonemizer + run: | + pip install --upgrade pip + python setup.py install + pip install --upgrade pytest + + - name: Version phonemizer + run: phonemize --version + + - name: Test phonemizer + run: pytest -v diff --git a/.github/workflows/publish_pypi.yaml b/.github/workflows/publish_pypi.yaml new file mode 100644 index 0000000..dd8eefb --- /dev/null +++ b/.github/workflows/publish_pypi.yaml @@ -0,0 +1,35 @@ +# Uppload to pypi on new tags + +name: Publish to Pypi + +on: + push: + tags: v* + branches: master + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout phonemizer + uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + + - name: Install system dependencies + run: sudo apt-get install espeak-ng festival mbrola mbrola-fr1 + + - name: Build phonemizer + run: | + pip install --upgrade pip pytest + python setup.py install + pytest + python setup.py sdist bdist_wheel + + - name: Publish to Pypi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml new file mode 100644 index 0000000..7669d8c --- /dev/null +++ b/.github/workflows/windows.yaml @@ -0,0 +1,61 @@ +# Test on windows with espeak-1.50 and festival-2.5 + +name: Windows + +on: [push, pull_request] + +jobs: + test: + runs-on: windows-latest + + env: + PHONEMIZER_ESPEAK_PATH: "C:\\Program Files\\eSpeak NG\\espeak-ng.exe" + PHONEMIZER_FESTIVAL_PATH: "C:\\festival\\src\\main\\festival.exe" + + steps: + - name: Checkout phonemizer + uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + + - name: Cache festival + uses: actions/cache@v2 + id: cache-festival + with: + path: | + C:\festival + C:\speech_tools + key: ${{ runner.os }}-festival + + - name: Install espeak + if: steps.cache-espeak.outputs.cache-hit != 'true' + run: | + $source = 'https://github.com/espeak-ng/espeak-ng/releases/download/1.50/espeak-ng-20191129-b702b03-x64.msi' + Invoke-WebRequest -Uri $source -OutFile espeak.msi + Start-Process msiexec.exe -Wait -ArgumentList '/I espeak.msi /qn' + + - name: Install festival + if: steps.cache-festival.outputs.cache-hit != 'true' + run: | + $uri = "https://sourceforge.net/projects/e-guidedog/files/related-third-party-software/0.3" + + $webclient = New-Object System.Net.WebClient + $webclient.DownloadFile("$uri" + "/festival-2.5-win.7z", "festival-2.5.7z") + $webclient.DownloadFile("$uri" + "/speech_tools-2.5-win.7z", "speech_tools-2.5.7z") + + set-alias sz "$env:ProgramFiles\7-Zip\7z.exe" + sz x -oC:\ festival-2.5.7z + sz x -oC:\ speech_tools-2.5.7z + + - name: Install phonemizer + run: | + pip install --upgrade pip pytest + python setup.py install + + - name: Version phonemizer + run: | + phonemize --version + + - name: Test phonemizer + run: pytest -v diff --git a/.gitignore b/.gitignore index 5d4a843..98fdc3b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /build/* /dist/* .coverage* +coverage.xml htmlcov/* .eggs/* test/htmlcov \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 53b55cc..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,68 +0,0 @@ -before_script: - # load the requested modules on oberon - - module load anaconda/3 festival/2.4 mbrola - -phonemizer-build: - stage: build - script: - # create a Python virtual environment dedicated to the test (if not existing) - - conda create --name phonemizer-ci python=3 2> /dev/null || true - - conda activate phonemizer-ci - - # install dependencies for testing - - pip install --upgrade pytest coverage - - # install phonemizer - - python setup.py install - -.phonemizer-test: &phonemizer-test -# run the unit tests within the CI environment -- conda activate phonemizer-ci -- phonemize --version -- coverage run && coverage report - -phonemizer-test-espeak-1-48-04: - stage: test - script: - # run the unit tests within the CI environment - - module unload espeak && module load espeak/1.48.04 - - *phonemizer-test - -phonemizer-test-espeak-1-49-2: - stage: test - script: - # run the unit tests within the CI environment - - module unload espeak && module load espeak/1.49.2 - - *phonemizer-test - -phonemizer-test-espeak-1-50: - stage: test - script: - # run the unit tests within the CI environment - - module unload espeak && module load espeak/1.50 - - *phonemizer-test - -phonemizer-oberon: - # install phonemizer in the "wordseg" virtual environment on oberon (users just - # have to type "conda activate wordseg" to use it) - stage: deploy - script: - - conda activate wordseg - - cd /shared/apps/phonemizer - - git pull origin master - - python setup.py install - only: - - tags - -phonemizer-pypi: - # upload the release on PyPI - stage: deploy - script: - - conda activate phonemizer-ci - - pip install -U twine - - rm -rf dist - - python setup.py sdist bdist_wheel - - twine check dist/* - - twine upload -u $TWINE_USERNAME -p $TWINE_PASSWORD --non-interactive dist/* - only: - - tags diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d4196dd..0000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -dist: focal -language: python -python: - - "3.6" - - "3.7" - - "3.8" - -addons: - apt: - update: true - packages: - - festival - - festvox-us1 - - festlex-cmu - - festlex-poslex - - espeak-ng - - mbrola - - mbrola-fr1 - -# no need of git history -git: - depth: 1 - -# test only the master branch -branches: - only: - - master - -install: - - pip install --upgrade pip - - python setup.py install - - pip install --upgrade pytest coverage codecov - -script: - - phonemize --version - - pytest - -after_success: - - coverage run - - codecov - -cache: - - pip - -notifications: - email: false diff --git a/README.md b/README.md index 2d7e822..a563317 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -[![Build Status](https://travis-ci.com/bootphon/phonemizer.svg?branch=master)]( -https://travis-ci.com/bootphon/phonemizer) +[![Linux](https://github.com/bootphon/phonemizer/actions/workflows/linux.yaml/badge.svg?branch=master)]( +https://github.com/bootphon/phonemizer/actions/workflows/linux.yaml) +[![MacOS](https://github.com/bootphon/phonemizer/actions/workflows/macos.yaml/badge.svg?branch=master)]( +https://github.com/bootphon/phonemizer/actions/workflows/macos.yaml) +[![Windows](https://github.com/bootphon/phonemizer/actions/workflows/windows.yaml/badge.svg?branch=master)]( +https://github.com/bootphon/phonemizer/actions/workflows/windows.yaml) [![Codecov](https://img.shields.io/codecov/c/github/bootphon/phonemizer)]( -https://codecov.io/gh/bootphon/phonemizer) -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/bootphon/phonemizer)]( +https://codecov.io/gh/bootphon/phonemizer) [![GitHub release (latest +SemVer)](https://img.shields.io/github/v/release/bootphon/phonemizer)]( https://github.com/bootphon/phonemizer/releases/latest) [![DOI](https://zenodo.org/badge/56728069.svg)]( https://doi.org/10.5281/zenodo.1045825) diff --git a/phonemizer/backend/festival.py b/phonemizer/backend/festival.py index 22fd83c..ac1d89e 100644 --- a/phonemizer/backend/festival.py +++ b/phonemizer/backend/festival.py @@ -20,6 +20,7 @@ import shlex import shutil import subprocess +import sys import tempfile import phonemizer.lispy as lispy @@ -184,8 +185,13 @@ def _process(self, text): data.write(text) data.close() + # fix the path name for windows + name = data.name + if sys.platform == 'win32': # pragma: nocover + name = name.replace('\\', '\\\\') + # the Scheme script to be send to festival - scm_script = open(self.script, 'r').read().format(data.name) + scm_script = open(self.script, 'r').read().format(name) with tempfile.NamedTemporaryFile('w+', delete=False) as scm: try: diff --git a/setup.cfg b/setup.cfg index a09d993..13e773a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,10 +13,6 @@ filterwarnings = source-dir = doc/source build-dir = doc/build -[coverage:run] -command_line = -m pytest -source = phonemizer - [coverage:report] exclude_lines = pragma: nocover diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_espeak.py b/test/test_espeak.py index aaa63f8..1ff73c6 100644 --- a/test/test_espeak.py +++ b/test/test_espeak.py @@ -68,8 +68,12 @@ def test_french(): @pytest.mark.skipif( - not EspeakBackend.is_espeak_ng(), - reason='Arabic is only supported by espeak-ng') + ( + not EspeakBackend.is_espeak_ng() or + # Arabic is not supported by the Windows msi installer from espeak-ng + # github release + not EspeakBackend.is_supported_language('ar')), + reason='Arabic is not supported') def test_arabic(): backend = EspeakBackend('ar') text = u'السلام عليكم' diff --git a/test/test_festival.py b/test/test_festival.py index fc64bd7..bcb1bbf 100644 --- a/test/test_festival.py +++ b/test/test_festival.py @@ -74,6 +74,9 @@ def test_path_good(): FestivalBackend.set_festival_path(None) +@pytest.mark.skipif( + 'PHONEMIZER_FESTIVAL_PATH' in os.environ, + reason='environment variable precedence') def test_path_bad(): try: # corrupt the default espeak path, try to use python executable instead diff --git a/test/test_main.py b/test/test_main.py index 2d45cbe..a33392b 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -14,33 +14,36 @@ # along with phonemizer. If not, see . """Test of the command line interface""" -import pytest +import pathlib import tempfile import shlex import sys +import pytest + from phonemizer.backend import EspeakBackend, EspeakMbrolaBackend from phonemizer import main, backend, logger def _test(input, expected_output, args=''): - with tempfile.NamedTemporaryFile('w') as finput: - # python2 needs additional utf8 encoding - if sys.version_info[0] == 2: - input = input.encode('utf8') - finput.write(input) - finput.seek(0) + with tempfile.TemporaryDirectory() as tmpdir: + input_file = pathlib.Path(tmpdir) / 'input.txt' + output_file = pathlib.Path(tmpdir) / 'output.txt' + with open(input_file, 'wb') as finput: + finput.write(input.encode('utf8')) + + sys.argv = ['unused', f'{input_file}', '-o', f'{output_file}'] + if args: + sys.argv += shlex.split(args) + main.main() - with tempfile.NamedTemporaryFile('w+') as foutput: - opts = '{} -o {} {}'.format(finput.name, foutput.name, args) - sys.argv = ['foo'] + shlex.split(opts) - main.main() + with open(output_file, 'rb') as foutput: + output = foutput.read().decode() - output = foutput.read() - if expected_output == '': - assert output == '' - else: - assert output == expected_output + '\n' + if expected_output == '': + assert output == '' + else: + assert output == expected_output + '\n' def test_help(): @@ -127,10 +130,15 @@ def test_espeak_mbrola(): def test_espeak_path(): espeak = backend.EspeakBackend.espeak_path() + if sys.platform == 'win32': + espeak = str(espeak).replace('\\', '\\\\').replace(' ', '\\ ') _test(u'hello world', u'həloʊ wɜːld ', f'--espeak-path={espeak}') def test_festival_path(): - festival = backend.FestivalBackend.festival_path() + festival = pathlib.Path(backend.FestivalBackend.festival_path()) + if sys.platform == 'win32': + festival = str(festival).replace('\\', '\\\\').replace(' ', '\\ ') + _test(u'hello world', u'hhaxlow werld ', f'--festival-path={festival} -b festival')