nateshmbhat / pyttsx3

Offline Text To Speech synthesis for python
Mozilla Public License 2.0
2.11k stars 332 forks source link

Add test to action on commit #338

Open willwade opened 5 days ago

willwade commented 5 days ago

My suggestion is we have a pytest script in this repo to deal with future prs

Eg.


import os
import pyttsx3
import pytest
from unittest import mock
import wave

@pytest.fixture
def engine():
    """Fixture for initializing pyttsx3 engine."""
    engine = pyttsx3.init()
    yield engine
    engine.stop()  # Ensure engine stops after tests

# Test for speaking text
def test_speaking_text(engine):
    engine.say('Sally sells seashells by the seashore.')
    engine.say('The quick brown fox jumped over the lazy dog.')
    engine.runAndWait()

# Test for saving voice to a file with additional validation
def test_saving_to_file(engine, tmp_path):
    test_file = tmp_path / 'test.wav'  # Using .wav for easier validation

    # Save the speech to a file
    engine.save_to_file('Hello World', str(test_file))
    engine.runAndWait()

    # Check if the file was created
    assert test_file.exists(), "The audio file was not created"

    # Check if the file is not empty
    assert test_file.stat().st_size > 0, "The audio file is empty"

    # Check if the file is a valid .wav file using the wave module
    with wave.open(str(test_file), 'rb') as wf:
        assert wf.getnchannels() == 1, "The audio file should have 1 channel (mono)"
        assert wf.getsampwidth() == 2, "The audio file sample width should be 2 bytes"
        assert wf.getframerate() == 22050, "The audio file framerate should be 22050 Hz"

# Test for listening for events
def test_listening_for_events(engine):
    onStart = mock.Mock()
    onWord = mock.Mock()
    onEnd = mock.Mock()

    engine.connect('started-utterance', onStart)
    engine.connect('started-word', onWord)
    engine.connect('finished-utterance', onEnd)

    engine.say('The quick brown fox jumped over the lazy dog.')
    engine.runAndWait()

    # Ensure the event handlers were called
    assert onStart.called
    assert onWord.called
    assert onEnd.called

# Test for interrupting an utterance
def test_interrupting_utterance(engine):
    def onWord(name, location, length):
        if location > 10:
            engine.stop()

    onWord_mock = mock.Mock(side_effect=onWord)
    engine.connect('started-word', onWord_mock)
    engine.say('The quick brown fox jumped over the lazy dog.')
    engine.runAndWait()

    # Check that stop was called
    assert onWord_mock.called

# Test for changing voices
def test_changing_voices(engine):
    voices = engine.getProperty('voices')
    for voice in voices:
        engine.setProperty('voice', voice.id)
        engine.say('The quick brown fox jumped over the lazy dog.')
    engine.runAndWait()

# Test for changing speech rate
def test_changing_speech_rate(engine):
    rate = engine.getProperty('rate')
    engine.setProperty('rate', rate + 50)
    engine.say('The quick brown fox jumped over the lazy dog.')
    engine.runAndWait()

# Test for changing volume
def test_changing_volume(engine):
    volume = engine.getProperty('volume')
    engine.setProperty('volume', volume - 0.25)
    engine.say('The quick brown fox jumped over the lazy dog.')
    engine.runAndWait()

# Test for running a driver event loop
def test_running_driver_event_loop(engine):
    def onStart(name):
        print('starting', name)
    def onWord(name, location, length):
        print('word', name, location, length)
    def onEnd(name, completed):
        if name == 'fox':
            engine.say('What a lazy dog!', 'dog')
        elif name == 'dog':
            engine.endLoop()

    engine.connect('started-utterance', onStart)
    engine.connect('started-word', onWord)
    engine.connect('finished-utterance', onEnd)
    engine.say('The quick brown fox jumped over the lazy dog.', 'fox')
    engine.startLoop()

# Test for using an external event loop
def test_external_event_loop(engine):
    def externalLoop():
        for _ in range(5):
            engine.iterate()
    engine.say('The quick brown fox jumped over the lazy dog.', 'fox')
    engine.startLoop(False)
    externalLoop()
    engine.endLoop()

We would need GitHub action for this like


name: Build, Test, and Upload Python Package

on:
  push:
    branches:
      - main  # Run tests on push to main
  pull_request:
    branches:
      - main  # Run tests on pull requests to main
  release:
    types: [created]  # Only publish on tagged releases

jobs:
  test:
    name: Test on all platforms
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ['3.8', '3.9', '3.10']
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install setuptools wheel pytest pyttsx3

      - name: Run tests
        run: |
          pytest --maxfail=5 --disable-warnings

  build_and_deploy:
    runs-on: ubuntu-latest
    needs: [test]  # This ensures tests pass before deployment
    if: github.event_name == 'release' && github.event.action == 'created'  # Only on release creation

    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.x'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install setuptools wheel twine

      - name: Clean previous builds
        run: |
          rm -rf dist

      - name: Build package
        run: |
          python setup.py check
          python setup.py sdist bdist_wheel

      - name: Publish package distributions to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          password: ${{ secrets.PYPI_PASSWORD }}

And some more requirements for this but just for dev


setup(
    ...
    extras_require={
        'dev': [
            'pytest',
            'mock; python_version < "3.7"',
            'flake8',
            'black',
            'pydub',
        ]
    },
)
willwade commented 5 days ago

I'd suggest we do this as part of #337

willwade commented 5 days ago

@nateshmbhat this sound ok to you?

nateshmbhat commented 5 days ago

@nateshmbhat this sound ok to you?

Will check this, Gimme sometime...

nateshmbhat commented 5 days ago

Lgtm @willwade

cclauss commented 5 days ago
- python-version: ['3.8', '3.9', '3.10']
+ python-version: ['3.9', '3.11', '3.13']
cclauss commented 5 days ago

Perhaps split build and deploy into two separate jobs. Always build and then:

  deploy:
    needs: [build, test]
    runs-on: ubuntu-latest
    if: github.event_name == 'release' && github.event.action == 'created'  # Only on release creation