This repository contains a Python-based text-to-speech (TTS) program that converts written text into spoken words. Leveraging state-of-the-art TTS libraries, this program aims to provide clear and natural-sounding audio output for a variety of applications, from audiobook generation to accessibility tools.
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow
import design
import mylib

class AppWindow(QMainWindow, design.Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.computeButton.clicked.connect(self.on_compute)

    def on_compute(self):
        x = float(self.inputField.text())
        result = mylib.compute(x)
        self.resultLabel.setText(str(result))

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = AppWindow()
    window.show()
    sys.exit(app.exec_())

app/gui/app_window.py
thomasthaddeus/text-to-speech
"""
_summary_

_extended_summary_
"""

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QDialog
import main_window
import second_window

class MainApp(QMainWindow, main_window.Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.openSecondWindowButton.clicked.connect(self.open_second_window)

    def open_second_window(self):
        self.second_window = SecondApp(self)
        self.second_window.show()



if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_win = MainApp()
    main_win.show()
    sys.exit(app.exec_())

app/gui/main_window/main_app.py
thomasthaddeus/text-to-speech


class SecondApp(QDialog, second_window.Ui_Dialog):
    def __init__(self, parent=None):
        super(SecondApp, self).__init__(parent)
        self.setupUi(self)
app/gui/second_window/second_app.py
thomasthaddeus/text-to-speech
"""api_config.py

This module provides functionality to retrieve an API key from a configuration
file.

The get_api_key function reads a specified configuration file, and fetches an
API key if available. It raises exceptions if the file is not found or if the
key is missing.

Raises:
    FileNotFoundError: Raised when the specified configuration file does not
    exist.
    ValueError: Raised when the API key is not found in the configuration file.

Returns:
    str: Returns the API key as a string.

Example:

try:
    api_key = get_api_key()
    print(f"API Key: {api_key}")
except (FileNotFoundError, ValueError) as err:
    print(f"Error: {err}")
"""

import os
import configparser


def get_api_key(config_file_path):
    """
    Retrieves the API key from a given configuration file.

    This function reads the specified configuration file and extracts the API
    key. If the file does not exist or the API key is not found, appropriate
    exceptions are raised.

    Args:
        config_file_path (str): Path to the configuration file containing the
        API key.

    Raises:
        FileNotFoundError: Raised when the specified configuration file does
        not exist.
        ValueError: Raised when the API key is not found in the configuration
        file.

    Returns:
        str: Returns the API key as a string.
    """
    config = configparser.ConfigParser()

    if not os.path.exists(config_file_path):
        raise FileNotFoundError(
            f"Config file not found at '{config_file_path}'"
        )

    config.read(config_file_path)

    if "API" not in config or "key" not in config["API"]:
        raise ValueError("API key not found in config file")

    return config["API"]["key"]


if __name__ == "__main__":
    get_api_key(config_file_path=".env")

app/modules/api/api_config.py
thomasthaddeus/text-to-speech
class AzureTTSAPI:

    def __init__(self, subscription_key, region):
        """
        Initialize Azure TTS API client.
        """
        speech_config = SpeechConfig(subscription=subscription_key, region=region)
        speech_config.set_speech_synthesis_output_format(SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3)
        self.synthesizer = SpeechSynthesizer(speech_config=speech_config)

    def get_audio_from_ssml(self, ssml):
        """
        Convert SSML to audio and return the audio data.
        """
        result = self.synthesizer.speak_ssml_async(ssml).get()
        return result.audio_data
app/modules/api/azure_tts_api.py
thomasthaddeus/text-to-speech
"""main.py
_summary_

_extended_summary_
"""

from ssml.ssml_config import SSMLConfig
from ssml.ssml_audio_effects import SSMLAudioEffects
from ssml.ssml_voice_effects import SSMLVoiceEffects
from ssml.text_to_ssml import TextToSSML



# Text to SSML
converter = TextToSSML()
text = f"Hello, {converter.add_emphasis('how are you?', 'strong')}" \
       f"{converter.add_break('1s')} I hope" \
       f"{converter.change_rate('everything is fine.', 'slow')}"

ssml_output = converter.convert(text)
ssml_output = converter.convert("Hello, how are you?")
print(ssml_output)


# Audio Effects
audio_effects = SSMLAudioEffects()
text = "Here's a chime sound: " + audio_effects.sound_effect('chime')

# Voice Effects
config = SSMLConfig()
config.set_voice("en-US-JasonNeural")

voice_effects = SSMLVoiceEffects(config)
text = ("I am excited " + voice_effects.adjust_pitch("to be here!", "high") +
       voice_effects.add_emphasis(" It's great.", "strong") +
       voice_effects.change_voice(" How do I sound in another voice?", "en-US-JessaNeural"))

# It will use the "en-US-JasonNeural" voice set in the config
text_with_voice = voice_effects.change_voice(text)

converter = TextToSSML()
ssml_output = converter.convert(text_with_voice)
print(ssml_output)


# Initialize TTS API and the audio processor
tts_api = HypotheticalTTSAPI()  # Placeholder for the actual TTS API
processor = SSMLAudioProcessor(tts_api, './output', 'sample_audio')

audio_effects = SSMLAudioEffects()
text_list = ["Hello, how are you?", "Goodbye!", "See you soon!"]

for text in text_list:
    ssml = audio_effects.add_background_sound(text, "http://example.com/background.mp3")
    processor.generate_and_save_audio(ssml)

app/modules/main.py
thomasthaddeus/text-to-speech
"""pptx_scraper.py

_summary_

_extended_summary_

Returns:
    _type_: _description_

Ex:
pptx_path = "path_to_your_pptx_file.pptx"
csv_path = "output_file.csv"
scraper = PPTXScraper(pptx_path)
scraper.save_to_csv(csv_path)
"""

import csv
from pptx import Presentation


class PPTXScraper:
    """
     _summary_

    _extended_summary_
    """

    def __init__(self, pptx_path):
        """
        __init__ _summary_

        _extended_summary_

        Args:
            pptx_path (_type_): _description_
        """
        self.pptx_path = pptx_path
        self.presentation = Presentation(pptx_path)

    def _get_slide_text(self, slide):
        """
        _get_slide_text _summary_

        _extended_summary_

        Args:
            slide (_type_): _description_

        Returns:
            _type_: _description_
        """
        text = ""
        for shape in slide.shapes:
            if hasattr(shape, "text"):
                text += shape.text + " "
        return text.strip()

    def _get_slide_notes(self, slide):
        """
        _get_slide_notes _summary_

        _extended_summary_

        Args:
            slide (_type_): _description_

        Returns:
            _type_: _description_
        """
        if slide.has_notes_slide:
            return slide.notes_slide.notes_text_frame.text
        return ""

    def scrape(self):
        """
        scrape _summary_

        _extended_summary_

        Returns:
            _type_: _description_
        """
        data = []
        for i, slide in enumerate(self.presentation.slides, start=1):
            slide_text = self._get_slide_text(slide)
            notes_text = self._get_slide_notes(slide)
            data.append([i, slide_text, notes_text])
        return data

    def save_to_csv(self, csv_path):
        """
        save_to_csv _summary_

        _extended_summary_

        Args:
            csv_path (_type_): _description_
        """
        data = self.scrape()
        with open(csv_path, "w", newline="", encoding="utf-8") as file:
            writer = csv.writer(file)
            writer.writerow(["Slide Number", "Slide Text", "Notes"])
            writer.writerows(data)

app/modules/scraper/pptx_scraper.py
thomasthaddeus/text-to-speech
"""
_summary_

_extended_summary_

Raises:
    ValueError: _description_

Returns:
    _type_: _description_
"""

from azure.cognitiveservices.speech.audio import AudioOutputConfig

class SSMLAudioEffects:
    def play_audio(self, src):
        """
        Play an audio clip from a given URL or path.
        """
        return f'<audio src="{src}"/>'

    def sound_effect(self, effect_name):
        """
        Placeholder to play a sound effect. This should ideally link to
        specific audio files or use specific SSML tags.
        Currently, it just demonstrates the structure.
        """
        # Map of effect_name to its source URL or path
        effects = {
            'chime': 'http://example.com/chime.wav',
            'buzz': 'http://example.com/buzz.wav',
            # Add more effects here
        }

        if effect_name in effects:
            return self.play_audio(effects[effect_name])
        else:
            raise ValueError(f"Effect {effect_name} not found!")

    def add_background_sound(self, text, sound_url, sound_level="medium"):
        """Play a background sound while the text is spoken."""
        return f'<audio src="{sound_url}" soundLevel="{sound_level}">{text}</audio>'

    def fade_in(self, text, duration="2s"):
        """Gradually increase volume at the start."""
        return f'<audio fadeOutDur="{duration}">{text}</audio>'

    def fade_out(self, text, duration="2s"):
        """Gradually decrease volume at the end."""
        return f'<audio fadeInDur="{duration}">{text}</audio>'

    def insert_pause(self, text, pause_duration="1s", before=True):
        """Insert a pause before or after a certain text."""
        pause_tag = f'<break time="{pause_duration}"/>'
        return f'{pause_tag}{text}' if before else f'{text}{pause_tag}'

    def repeat_audio(self, text, count=2):
        """Repeat a certain segment of audio."""
        return f'<audio repeatCount="{count}">{text}</audio>'

app/modules/ssml/ssml_audio_effects.py
thomasthaddeus/text-to-speech
"""ssml_audio_processor.py

_summary_

_extended_summary_
"""

import os
from azure.cognitiveservices.speech import SpeechConfig, SpeechSynthesisOutputFormat, SpeechSynthesizer
from api.azure_tts_api import AzureTTSAPI

# Use the above classes
SUBSCRIPTION_KEY = 'YOUR_AZURE_SUBSCRIPTION_KEY'
REGION = 'YOUR_AZURE_REGION'


class SSMLAudioProcessor:
    """
     _summary_

    _extended_summary_
    """

    def __init__(self, tts_api, output_dir, base_filename):
        """
        __init__ _summary_

        _extended_summary_

        Args:
            tts_api (_type_): A hypothetical TTS API class instance.
            output_dir (_type_): Directory where audio files will be saved.
            base_filename (_type_): The base filename (without extension).
        """
        self.tts_api = tts_api
        self.output_dir = output_dir
        self.base_filename = base_filename
        self.counter = 0  # Counter to append to filenames

    def generate_and_save_audio(self, ssml):
        """
        Generate and save the audio file from SSML.

        _extended_summary_

        Args:
            ssml (_type_): _description_
        """
        audio_data = self.tts_api.get_audio_from_ssml(ssml)

        filename = f"{self.base_filename}_{self.counter}.mp3"
        filepath = os.path.join(self.output_dir, filename)

        with open(filepath, 'wb') as f:
            f.write(audio_data)

        self.counter += 1


tts_api = AzureTTSAPI(SUBSCRIPTION_KEY, REGION)
processor = SSMLAudioProcessor(tts_api, './output', 'sample_audio')

audio_effects = SSMLAudioEffects()
text_list = ["Hello, how are you?", "Goodbye!", "See you soon!"]

for text in text_list:
    ssml = audio_effects.add_background_sound(
        text,
        "http://example.com/background.mp3"
    )
    processor.generate_and_save_audio(ssml)

app/modules/ssml/ssml_audio_processor.py
thomasthaddeus/text-to-speech
"""ssml_config.py

_summary_

_extended_summary_

Raises:
    ValueError: _description_

Returns:
    _type_: _description_
"""

from azure.cognitiveservices.speech import SpeechConfig, SpeechSynthesizer
from azure.cognitiveservices.speech.audio import AudioOutputConfig


class SSMLConfig:
    """
     _summary_

    _extended_summary_

    Raises:
        ValueError: _description_

    Returns:
        _type_: _description_
    """

    # Define a list of supported voices.
    SUPPORTED_VOICES = [
        "en-US-JennyNeural",
        "en-US-GuyNeural",
        "en-US-AriaNeural",
        "en-US-DavisNeural",
        "en-US-AmberNeural",
        "en-US-AnaNeural",
        "en-US-AshleyNeural",
        "en-US-BrandonNeural",
        "en-US-ChristopherNeural",
        "en-US-CoraNeural",
        "en-US-ElizabethNeural",
        "en-US-EricNeural",
        "en-US-JacobNeural",
        "en-US-JaneNeural",
        "en-US-JasonNeural",
        "en-US-MichelleNeural",
        "en-US-MonicaNeural",
        "en-US-NancyNeural",
        "en-US-RogerNeural",
        "en-US-SaraNeural",
        "en-US-SteffanNeural",
        "en-US-TonyNeural"
    ]

    def __init__(self):
        """
        __init__ _summary_

        _extended_summary_
        """
        self._voice = self.SUPPORTED_VOICES[0]  # Set a default voice

    def list_voices(self):
        """
        list_voices _summary_

        _extended_summary_

        Returns:
            _type_: _description_
        """
        return self.SUPPORTED_VOICES

    def set_voice(self, voice):
        """
        set_voice _summary_

        _extended_summary_

        Args:
            voice (_type_): _description_

        Raises:
            ValueError: _description_
        """
        if voice not in self.SUPPORTED_VOICES:
            raise ValueError(f"Voice '{voice}' is not supported!")
        self._voice = voice

    def get_voice(self):
        """
        get_voice _summary_

        _extended_summary_

        Returns:
            _type_: _description_
        """
        return self._voice
app/modules/ssml/ssml_config.py
thomasthaddeus/text-to-speech
"""ssml_voice_effects.py

This module provides a set of tools for manipulating voice effects using SSML
(Speech Synthesis Markup Language).

Returns:
    str: An SSML-formatted string with the desired voice effects.
"""

import azure.cognitiveservices.speech as speechsdk


class SSMLVoiceEffects:
    """
    A class to apply various SSML voice effects to a given text.

    Raises:
        ValueError: If an unsupported voice or parameter is provided.

    Returns:
        str: An SSML-formatted string with the desired voice effects.
    """

    def __init__(
        self,
        default_voice="en-US-Guy24kRUS",
        default_rate="medium",
        default_volume="medium",
    ):
        """
        Initializes the SSMLVoiceEffects class with default voice, rate, and
        volume settings.

        Args:
            default_voice (str, optional): The default voice to use. Defaults
              to "en-US-Guy24kRUS".
            default_rate (str, optional): The default speech rate. Defaults to
              "medium".
            default_volume (str, optional): The default volume level. Defaults
              to "medium".
        """
        self.default_voice = default_voice
        self.default_rate = default_rate
        self.default_volume = default_volume

    @property
    def default_prosody(self):
        """
        Generates a prosody tag with the default rate and volume settings.

        Returns:
            str: An SSML prosody tag with default settings.
        """
        return f'<prosody rate="{self.default_rate}" volume="{self.default_volume}">{{}}</prosody>'

    def wrap_with_default_prosody(self, text):
        """
        Wraps the given text with the default prosody settings.

        Args:
            text (str): The text to be wrapped.

        Returns:
            str: Text wrapped with the default prosody settings.
        """
        return self.default_prosody.format(text)

    def change_voice(self, text, voice_name=None):
        """
        Changes the voice for a specific part of the text.

        Args:
            text (str): The text for which the voice needs to be changed.
            voice_name (str, optional): The name of the voice to be applied.
            Defaults to the class's default voice.

        Raises:
            ValueError: If the provided voice_name is not in the
            SUPPORTED_VOICES list.

        Returns:
            str: Text wrapped with the specified voice.
        """
        voice_name = voice_name if voice_name else self.default_voice
        voice_name = voice_name or self.config.get_voice()

        if voice_name not in self.config.list_voices():
            raise ValueError(f"Voice '{voice_name}' is not supported!")

        return f'<voice name="{voice_name}">{text}</voice>'


    def adjust_pitch(self, text, pitch="medium"):
        """
        Adjust the baseline pitch of the voice.

        Values can be: "x-low", "low", "medium", "high", "x-high", or a
        relative value like "+2st" or "-2st".

        Args:
            text (str): The text for which the pitch needs to be adjusted.
            pitch (str, optional): The desired pitch level. Defaults to
              "medium".

        Returns:
            str: Text with adjusted pitch.
        """
        return f'<prosody pitch="{pitch}">{text}</prosody>'


    def adjust_contour(self, text, contour):
        """
        Adjusts the pitch contour of the voice.

        Args:
            text (str): The text for which the pitch contour needs to be
              adjusted.
            contour (str): The desired pitch contour pattern.

        Returns:
            str: Text with adjusted pitch contour.

        Example: "(0%,+20Hz)(10%,-5Hz)"
        """
        return f'<prosody contour="{contour}">{text}</prosody>'


    def set_duration(self, text, duration="medium"):
        """
        Sets the time taken to read an element.

        Duration can be in milliseconds like "250ms" or "3s".

        Args:
            text (str): The text for which the duration needs to be set.
            duration (str, optional): The desired duration. Defaults to
              "medium".

        Returns:
            str: Text with adjusted duration.
        """
        return f'<prosody duration="{duration}">{text}</prosody>'


    def adjust_range(self, text, range_val="medium"):
        """
        Adjust the range of pitch variation.

        Values can be: "x-low", "low", "medium", "high", "x-high".

        Args:
            text (str): The text for which the pitch range needs to be adjusted.
            range_val (str, optional): The desired pitch range. Defaults to
            "medium".

        Returns:
            str: Text with adjusted pitch range.
        """

        return f'<prosody range="{range_val}">{text}</prosody>'


    def change_rate(self, text, rate=None):
        """
        Adjusts the speaking rate of the text.

        Args:
            text (str): The text for which the speaking rate needs to be
              adjusted.
            rate (str, optional): The desired speaking rate. Defaults to the
              class's default rate.

        Returns:
            str: Text with adjusted speaking rate.
        """
        rate = rate if rate else self.default_rate
        return f'<prosody rate="{rate}">{text}</prosody>'


    def add_emphasis(self, text, level="moderate"):
        """
        Adds emphasis to the text.

        Levels can be: "strong", "moderate", or "reduced".

        Args:
            text (str): The text to be emphasized.
            level (str, optional): The level of emphasis. Defaults to
            "moderate".

        Returns:
            str: Text with added emphasis.
        """
        return f'<emphasis level="{level}">{text}</emphasis>'


    def adjust_volume(self, text, volume=None):
        """
        Adjusts the volume of the text.

        Args:
            text (str): The text for which the volume needs to be adjusted.
            volume (str, optional): The desired volume level. Defaults to the
              class's default volume.

        Returns:
            str: Text with adjusted volume.
        """
        volume = volume if volume else self.default_volume
        return f'<prosody volume="{volume}">{text}</prosody>'

app/modules/ssml/ssml_voice_effects.py
thomasthaddeus/text-to-speech
"""text_to_ssml.py

_summary_

_extended_summary_

Returns:
    _type_: _description_
"""

class TextToSSML:
    def __init__(self, voice_name="en-US-Guy24kRUS"):
        """
        Initialize with a default voice. You can change the voice_name as per
        your requirements. See Microsoft's documentation for available voice
        names.
        """
        self.voice_name = voice_name


    def add_emphasis(self, text, level="moderate"):
        """
        Adds emphasis to the given text. Levels can be: "strong", "moderate", or "reduced".
        """
        return f'<emphasis level="{level}">{text}</emphasis>'

    def add_break(self, time="500ms"):
        """
        Adds a break. You can specify time in milliseconds or in seconds like "1s".
        """
        return f'<break time="{time}"/>'

    def change_rate(self, text, rate="medium"):
        """
        Changes the speaking rate. Rate can be: "x-slow", "slow", "medium", "fast", "x-fast".
        """
        rate = ["x-slow", "slow", "medium", "fast", "x-fast"]
        return f'<prosody rate="{rate[2]}">{text}</prosody>'

    def adjust_volume(self, text, volume="medium"):
        """
        Adjusts the volume. Volume can be: "silent", "x-soft", "soft", "medium", "loud", "x-loud".
        """
        volume = ["silent", "x-soft", "soft", "medium", "loud", "x-loud"]
        return f'<prosody volume="{volume[0]}">{text}</prosody>'

    def convert(self, text):
        """
        Convert plain text to SSML.
        """
        ssml = f"""
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
    <voice name="{self.voice_name}">
        {text}
    </voice>
</speak>
"""
        return ssml

app/modules/ssml/text_to_ssml.py
thomasthaddeus/text-to-speech
"""voices.py
_summary_

_extended_summary_
"""

def neural_voices():
    sp_female = [
        "en-US-JennyNeural",
        "en-US-AriaNeural",
        "en-US-AmberNeural",
        "en-US-AshleyNeural",
        "en-US-CoraNeural",
        "en-US-ElizabethNeural",
        "en-US-JaneNeural",
        "en-US-MichelleNeural",
        "en-US-MonicaNeural",
        "en-US-NancyNeural",
        "en-US-SaraNeural",
    ]

    sp_child = [ "en-US-AnaNeural" ]

    sp_male = [
        "en-US-GuyNeural",
        "en-US-DavisNeural",
        "en-US-BrandonNeural",
        "en-US-ChristopherNeural",
        "en-US-EricNeural",
        "en-US-JacobNeural",
        "en-US-JasonNeural",
        "en-US-RogerNeural",
        "en-US-SteffanNeural",
        "en-US-TonyNeural",
    ]

    return sp_child, sp_female, sp_male

app/modules/ssml/voices.py
thomasthaddeus/text-to-speech
"""setups the package for distribution"""

from setuptools import setup, find_packages

setup(
    name='pptx_text_to_speech',
    version='1.0',
    packages=find_packages(),
    install_requires=[

    ],
    author='Thaddeus Thomas',
    author_email='thaddeus.r.thomas@gmail.com',
    description='',
    license='MIT',
    keywords='text-to-speech',
)

setup.py
thomasthaddeus/text-to-speech