5
\$\begingroup\$

I have a Text-To-Speech program that will ask the user to input text, convert it to speech and then output that speech from within Pygame by loading the file into memory, getting Pygame to play the file, closing Pygame, closing the file and then deleting the file, ready for the next input. It will then ask the user if they want to convert more text into speech or if they want to exit the program. For the past few days I've just been focusing on getting it working. Now that it finally works, I'm sure that it has many parts that can be improved upon, especially:

def wait_for_it(audio_length, greater_than, less_than, time_to_wait):

    if (audio_length) >= (greater_than) and (audio_length) < (less_than):
        time.sleep((audio_length) + (time_to_wait))

wait_for_it(audio_length, 0, 15, 1)
wait_for_it(audio_length, 15, 30, 2)
wait_for_it(audio_length, 30, 45, 3)
wait_for_it(audio_length, 45, 60, 4)
wait_for_it(audio_length, 60, 75, 5)
wait_for_it(audio_length, 75, 90, 6)
wait_for_it(audio_length, 90, 105, 7)
wait_for_it(audio_length, 105, 120, 8)
wait_for_it(audio_length, 120, 135, 9)
wait_for_it(audio_length, 135, 150, 10)
wait_for_it(audio_length, 150, 165, 11)
wait_for_it(audio_length, 165, 180, 12)
if audio_length >= 180:
    time.sleep((audio_length) + 15)

and

answer = input("\nDo you want to repeat? ").strip().lower()
if answer in ["n", "no", "nah", "nay", "course not", "don't", "dont", "not"] or "no" in answer or "nah" in answer or "nay" in answer or "course not" in answer or "don't" in answer or "dont" in answer or "not" in answer:
    exiting()
elif answer in ["y", "yes", "yeah", "course", "ye", "yea", "yh", "do"] or "yes" in answer or "yeah" in answer or "course" in answer or "ye" in answer or "yea" in answer or "yh" in answer or "do" in answer:
    input_for_tts("\nPlease input something for the program to say: ")

Full code:

import os
import time
import sys
import getpass
import pip
import mmap
import imp
from contextlib import contextmanager


my_file = "Text To Speech.mp3"
username = getpass.getuser()


@contextmanager
def suppress_output():

    with open(os.devnull, "w") as devnull:
        old_stdout = sys.stdout
        sys.stdout = devnull
        try:  
            yield
        finally:
            sys.stdout = old_stdout


def check_and_remove_file():

    active = pygame.mixer.get_init()
    if active != None:
        pygame.mixer.music.stop()
        pygame.mixer.quit()
        pygame.quit()
    if os.path.isfile(my_file):
        os.remove(my_file)


def wait_for_it(audio_length, greater_than, less_than, time_to_wait):

    if (audio_length) >= (greater_than) and (audio_length) < (less_than):
        time.sleep((audio_length) + (time_to_wait))


def exiting():

    check_and_remove_file()
    print("\nGoodbye!")
    sys.exit()


def input_for_tts(message):

    try:

        tts = gTTS(text = input(message))
        tts.save('Text To Speech.mp3')
        with open(my_file) as f:
            m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
        audio = MP3(my_file)
        audio_length = audio.info.length
        try:
            pygame.mixer.init()
        except pygame.error:
            print("\nSorry, no audio device was detected. The code cannot complete.")
            exiting()
        pygame.mixer.music.load(m)
        pygame.mixer.music.play()
        wait_for_it(audio_length, 0, 15, 1)
        wait_for_it(audio_length, 15, 30, 2)
        wait_for_it(audio_length, 30, 45, 3)
        wait_for_it(audio_length, 45, 60, 4)
        wait_for_it(audio_length, 60, 75, 5)
        wait_for_it(audio_length, 75, 90, 6)
        wait_for_it(audio_length, 90, 105, 7)
        wait_for_it(audio_length, 105, 120, 8)
        wait_for_it(audio_length, 120, 135, 9)
        wait_for_it(audio_length, 135, 150, 10)
        wait_for_it(audio_length, 150, 165, 11)
        wait_for_it(audio_length, 165, 180, 12)
        if audio_length >= 180:
            time.sleep((audio_length) + 15)
        try:
            m.close()
            check_and_remove_file()
        except PermissionError:
            imp.reload(pygame)
            check_and_remove_file()

    except KeyboardInterrupt:

        exiting()


with suppress_output():

    pkgs = ['mutagen', 'gTTS', 'pygame']
    for package in pkgs:
        if package not in pip.get_installed_distributions():
            pip.main(['install', package])


import pygame
from pygame.locals import *
from gtts import gTTS
from mutagen.mp3 import MP3


check_and_remove_file()


input_for_tts("Hello there " + username + ". This program is\nused to output the user's input as speech.\nPlease input something for the program to say: ")


while True:

    try:

        answer = input("\nDo you want to repeat? ").strip().lower()
        if answer in ["n", "no", "nah", "nay", "course not", "don't", "dont", "not"] or "no" in answer or "nah" in answer or "nay" in answer or "course not" in answer or "don't" in answer or "dont" in answer or "not" in answer:
            exiting()
        elif answer in ["y", "yes", "yeah", "course", "ye", "yea", "yh", "do"] or "yes" in answer or "yeah" in answer or "course" in answer or "ye" in answer or "yea" in answer or "yh" in answer or "do" in answer:
            input_for_tts("\nPlease input something for the program to say: ")
        else:
            print("\nSorry, I didn't understand that. Please try again with yes or no.")

    except KeyboardInterrupt:

        exiting()
\$\endgroup\$
3
\$\begingroup\$

Not a full review, only answering the "repetitive" bits here.

The relationship between the length of the speech and the length of the pause is given by a duration of the speech for each second of the pause. You should define all those upper bounds in a container and use a for loop combined to enumerate to examine all possibilities and know the pause duration. Something like:

SPEECH_LENGTH_PER_SECOND_OF_PAUSE = range(15, 181, 15)  # [15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180]


def get_pause_duration(audio_length, default_duration=15):
    for pause_duration, upper_bound in enumerate(SPEECH_LENGTH_PER_SECOND_OF_PAUSE, 1):
        if audio_length <= upper_bound:
            return pause_duration
    return default_duration

And you can then replace all your wait_for_it by a single

time.sleep(audio_length + get_pause_duration(audio_length))

However, you seem to use a very simple pattern to determine the length of the pause: each 15 seconds of audio gets you 1 second of pause, up to 15 seconds of pause… This is easily computable:

def get_pause_duration(audio_length, maximum_duration=15):
    default_pause, correction = divmod(audio_length, 15)  # <-- you should probably define a constant to give a name to this 15…
    return min(default_pause + bool(correction), maximum_duration)
\$\endgroup\$
  • \$\begingroup\$ Thanks! Would you mind explaining how the code for the pattern (def get_pause_duration(audio_length, maximum_duration=15):) works to me? \$\endgroup\$ – Gameskiller01 May 29 '17 at 10:42
  • \$\begingroup\$ @Gameskiller01 divmod(a, b) will return the couple a // b, a % b. bool(correction) is equivalent to 1 if correction else 0; this match your values more closely than default_pause + 1 but if the values are not hard limits, you can simplify the code to return min(audio_length // 15 + 1, maximum_duration) and get quasi-identical behaviour. \$\endgroup\$ – Mathias Ettinger May 29 '17 at 12:42
  • \$\begingroup\$ I'd personally use enumerate when defining SPEECH_LENGTH_TO_PAUSE_DURATION. There's pros and cons to both I guess, :) \$\endgroup\$ – Peilonrayz Jun 1 '17 at 23:48
  • \$\begingroup\$ @Peilonrayz True, didn't thought of it in the first place \$\endgroup\$ – Mathias Ettinger Jun 2 '17 at 15:57
1
\$\begingroup\$
answer = input("\nDo you want to repeat? ").strip().lower()
if answer in ["n", "no", "nah", "nay", "course not", "don't", "dont", "not"] or "no" 
in answer or "nah" in answer or "nay" in answer or "course not" in answer or 
"don't" in answer or "dont" in answer or "not" in answer:
    exiting()

elif answer in ["y", "yes", "yeah", "course", "ye", "yea", "yh", "do"] or "yes" in 
answer or "yeah" in answer or "course" in answer or "ye" in answer or "yea" 
in answer or "yh" in answer or "do" in answer:
    input_for_tts("\nPlease input something for the program to say: ")

Can be rewritten to:

choice_yes = ["y", "no"]
choice_no = ["n", "no"]

while True:
    answer = input("\nDo you want to repeat? [Y(es) / N(o)]: ")

    if answer in choice_yes or answer in " ".join(choice_yes).upper():
        # continue flow

    if answer in choice_no or answer in " ".join(choice_no).upper():
        # exit
    else:
        # error message, or pass

Since the answer is "yes", "yes" is also in the answer (and the same goes for no).

\$\endgroup\$
  • \$\begingroup\$ But then how would I do it so if someone input "I don't want to repeat", it would recognize that and call the exiting()function? Would it just be easier to put all of the possible responses in a list and then check if anything in the list is in the answer? \$\endgroup\$ – Gameskiller01 May 29 '17 at 10:45
  • \$\begingroup\$ UPDATE: Creating a list and trying to get it to check whether anything in the list is in the answer doesn't work. It says that TypeError: 'in <string>' requires string as left operand, not list. \$\endgroup\$ – Gameskiller01 May 29 '17 at 11:03
  • \$\begingroup\$ UPDATE #2: What did work is: no_choices = ["no", "nah", "nay", "course not", "don't", "dont", "not"] if answer in ["n", no_choices] or any(x in answer for x in no_choices):. \$\endgroup\$ – Gameskiller01 May 29 '17 at 12:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.