Escrevendo um assistente de voz em Python

Introdução



A tecnologia de aprendizado de máquina evoluiu em um ritmo surpreendente no ano passado. Mais e mais empresas estão compartilhando suas melhores práticas, abrindo assim novas possibilidades para a criação de assistentes digitais inteligentes.



Como parte deste artigo, quero compartilhar minha experiência na implementação de um assistente de voz e oferecer algumas idéias para torná-lo ainda mais inteligente e útil.



imagem



O que meu assistente de voz pode fazer?



Descrição da habilidade Trabalho offline Dependências necessárias
Reconhecer e sintetizar a fala Suportado pip install PyAudio (usando um microfone)



pip install pyttsx3 (síntese de fala)



Você pode escolher um ou ambos para reconhecimento de fala:



  • pip install SpeechRecognition (reconhecimento online de alta qualidade, vários idiomas)
  • pip install vosk ( offline-, )


pip install pyowm (OpenWeatherMap)
Google ( ) pip install google
YouTube -
Wikipedia c pip install wikipedia-api
pip install googletrans (Google Translate)
-
« » -
( ) -
-
TODO ...


1.



Vamos começar aprendendo como lidar com a entrada de voz. Precisamos de um microfone e algumas bibliotecas instaladas: PyAudio e SpeechRecognition.



Vamos preparar as ferramentas básicas para reconhecimento de fala:



import speech_recognition

if __name__ == "__main__":

    #      
    recognizer = speech_recognition.Recognizer()
    microphone = speech_recognition.Microphone()

    while True:
        #         
        voice_input = record_and_recognize_audio()
        print(voice_input)


Agora vamos criar uma função para gravar e reconhecer fala. Para reconhecimento online, precisamos do Google, uma vez que possui alta qualidade de reconhecimento em um grande número de idiomas.



def record_and_recognize_audio(*args: tuple):
    """
       
    """
    with microphone:
        recognized_data = ""

        #    
        recognizer.adjust_for_ambient_noise(microphone, duration=2)

        try:
            print("Listening...")
            audio = recognizer.listen(microphone, 5, 5)

        except speech_recognition.WaitTimeoutError:
            print("Can you check if your microphone is on, please?")
            return

        #  online-  Google 
        try:
            print("Started recognition...")
            recognized_data = recognizer.recognize_google(audio, language="ru").lower()

        except speech_recognition.UnknownValueError:
            pass

        #          
        except speech_recognition.RequestError:
            print("Check your Internet Connection, please")

        return recognized_data


E se não houver acesso à Internet? Você pode usar soluções para reconhecimento offline. Eu pessoalmente gostei muito do projeto Vosk .

Na verdade, você não precisa implementar uma opção offline se não precisar de uma. Eu só queria mostrar os dois métodos dentro da estrutura do artigo, e você já escolhe com base nos requisitos do seu sistema (por exemplo, o Google é sem dúvida o líder em número de idiomas de reconhecimento disponíveis).
Agora, tendo implementado uma solução offline e adicionado os modelos de linguagem necessários ao projeto, se não houver acesso à rede, passaremos automaticamente para o reconhecimento offline.



Observe que para não ter que repetir a mesma frase duas vezes, decidi gravar o áudio do microfone em um arquivo wav temporário que será excluído após cada reconhecimento.



Assim, o código resultante tem a seguinte aparência:



Código completo para o reconhecimento de voz funcionar
from vosk import Model, KaldiRecognizer  # -  Vosk
import speech_recognition  #    (Speech-To-Text)
import wave  #      wav
import json  #   json-  json-
import os  #    


def record_and_recognize_audio(*args: tuple):
    """
       
    """
    with microphone:
        recognized_data = ""

        #    
        recognizer.adjust_for_ambient_noise(microphone, duration=2)

        try:
            print("Listening...")
            audio = recognizer.listen(microphone, 5, 5)

            with open("microphone-results.wav", "wb") as file:
                file.write(audio.get_wav_data())

        except speech_recognition.WaitTimeoutError:
            print("Can you check if your microphone is on, please?")
            return

        #  online-  Google 
        try:
            print("Started recognition...")
            recognized_data = recognizer.recognize_google(audio, language="ru").lower()

        except speech_recognition.UnknownValueError:
            pass

        #          
        #  offline-  Vosk
        except speech_recognition.RequestError:
            print("Trying to use offline recognition...")
            recognized_data = use_offline_recognition()

        return recognized_data


def use_offline_recognition():
    """
      - 
    :return:  
    """
    recognized_data = ""
    try:
        #         
        if not os.path.exists("models/vosk-model-small-ru-0.4"):
            print("Please download the model from:\n"
                  "https://alphacephei.com/vosk/models and unpack as 'model' in the current folder.")
            exit(1)

        #      (   )
        wave_audio_file = wave.open("microphone-results.wav", "rb")
        model = Model("models/vosk-model-small-ru-0.4")
        offline_recognizer = KaldiRecognizer(model, wave_audio_file.getframerate())

        data = wave_audio_file.readframes(wave_audio_file.getnframes())
        if len(data) > 0:
            if offline_recognizer.AcceptWaveform(data):
                recognized_data = offline_recognizer.Result()

                #      JSON-
                # (      )
                recognized_data = json.loads(recognized_data)
                recognized_data = recognized_data["text"]
    except:
        print("Sorry, speech service is unavailable. Try again later")

    return recognized_data


if __name__ == "__main__":

    #      
    recognizer = speech_recognition.Recognizer()
    microphone = speech_recognition.Microphone()

    while True:
        #        
        #      
        voice_input = record_and_recognize_audio()
        os.remove("microphone-results.wav")
        print(voice_input)




Você pode estar se perguntando "Por que oferecer suporte a recursos offline?"



Na minha opinião, vale sempre a pena considerar que o usuário pode ficar desligado da rede. Neste caso, o assistente de voz ainda pode ser útil se você usá-lo como um bot de conversação ou para resolver uma série de tarefas simples, por exemplo, contar algo, recomendar um filme, ajudar a escolher uma cozinha, jogar um jogo, etc.



Etapa 2. Configurando o assistente de voz



Como nosso assistente de voz pode ter um gênero, um idioma de fala e, de acordo com os clássicos, um nome, vamos selecionar uma classe separada para esses dados, com a qual trabalharemos no futuro.



Para pedir uma voz ao nosso assistente, usaremos a biblioteca de síntese de voz offline pyttsx3. Ele irá encontrar automaticamente as vozes disponíveis para síntese em nosso computador, dependendo das configurações do sistema operacional (portanto, é possível que você tenha outras vozes disponíveis e você precise de índices diferentes).



Também adicionaremos à função principal a inicialização da síntese de voz e uma função separada para reproduzi-la. Para ter certeza de que tudo funciona, vamos fazer uma pequena verificação se o usuário nos cumprimentou e dar a ele uma saudação de retorno do assistente:



Código completo para estrutura de assistente de voz (síntese e reconhecimento de fala)
from vosk import Model, KaldiRecognizer  # -  Vosk
import speech_recognition  #    (Speech-To-Text)
import pyttsx3  #   (Text-To-Speech)
import wave  #      wav
import json  #   json-  json-
import os  #    


class VoiceAssistant:
    """
      ,  , ,  
    """
    name = ""
    sex = ""
    speech_language = ""
    recognition_language = ""


def setup_assistant_voice():
    """
        (    
        )
    """
    voices = ttsEngine.getProperty("voices")

    if assistant.speech_language == "en":
        assistant.recognition_language = "en-US"
        if assistant.sex == "female":
            # Microsoft Zira Desktop - English (United States)
            ttsEngine.setProperty("voice", voices[1].id)
        else:
            # Microsoft David Desktop - English (United States)
            ttsEngine.setProperty("voice", voices[2].id)
    else:
        assistant.recognition_language = "ru-RU"
        # Microsoft Irina Desktop - Russian
        ttsEngine.setProperty("voice", voices[0].id)


def play_voice_assistant_speech(text_to_speech):
    """
         (  )
    :param text_to_speech: ,     
    """
    ttsEngine.say(str(text_to_speech))
    ttsEngine.runAndWait()


def record_and_recognize_audio(*args: tuple):
    """
       
    """
    with microphone:
        recognized_data = ""

        #    
        recognizer.adjust_for_ambient_noise(microphone, duration=2)

        try:
            print("Listening...")
            audio = recognizer.listen(microphone, 5, 5)

            with open("microphone-results.wav", "wb") as file:
                file.write(audio.get_wav_data())

        except speech_recognition.WaitTimeoutError:
            print("Can you check if your microphone is on, please?")
            return

        #  online-  Google 
        # (  )
        try:
            print("Started recognition...")
            recognized_data = recognizer.recognize_google(audio, language="ru").lower()

        except speech_recognition.UnknownValueError:
            pass

        #         
        #   offline-  Vosk
        except speech_recognition.RequestError:
            print("Trying to use offline recognition...")
            recognized_data = use_offline_recognition()

        return recognized_data


def use_offline_recognition():
    """
      - 
    :return:  
    """
    recognized_data = ""
    try:
        #         
        if not os.path.exists("models/vosk-model-small-ru-0.4"):
            print("Please download the model from:\n"
                  "https://alphacephei.com/vosk/models and unpack as 'model' in the current folder.")
            exit(1)

        #      (   )
        wave_audio_file = wave.open("microphone-results.wav", "rb")
        model = Model("models/vosk-model-small-ru-0.4")
        offline_recognizer = KaldiRecognizer(model, wave_audio_file.getframerate())

        data = wave_audio_file.readframes(wave_audio_file.getnframes())
        if len(data) > 0:
            if offline_recognizer.AcceptWaveform(data):
                recognized_data = offline_recognizer.Result()

                #      JSON- 
                # (      )
                recognized_data = json.loads(recognized_data)
                recognized_data = recognized_data["text"]
    except:
        print("Sorry, speech service is unavailable. Try again later")

    return recognized_data


if __name__ == "__main__":

    #      
    recognizer = speech_recognition.Recognizer()
    microphone = speech_recognition.Microphone()

    #    
    ttsEngine = pyttsx3.init()

    #    
    assistant = VoiceAssistant()
    assistant.name = "Alice"
    assistant.sex = "female"
    assistant.speech_language = "ru"

    #    
    setup_assistant_voice()

    while True:
        #        
        #      
        voice_input = record_and_recognize_audio()
        os.remove("microphone-results.wav")
        print(voice_input)

        #      ()
        voice_input = voice_input.split(" ")
        command = voice_input[0]

        if command == "":
            play_voice_assistant_speech("")




Na verdade, gostaria de aprender a escrever um sintetizador de voz sozinho, mas meu conhecimento aqui não será suficiente. Se você puder sugerir uma boa literatura, um curso ou uma solução documentada interessante que o ajudará a entender este tópico profundamente, por favor, escreva nos comentários.



Etapa 3. Processamento de comandos



Agora que "aprendemos" a reconhecer e sintetizar a fala com a ajuda dos desenvolvimentos simplesmente divinos de nossos colegas, podemos começar a reinventar nossa roda para processar os comandos de fala do usuário: D



No meu caso, uso opções multilíngues para armazenar comandos, já que não tenho muitos eventos, e estou satisfeito com a precisão da definição de um ou outro comando. No entanto, para projetos grandes, recomendo dividir as configurações por idioma.



Posso oferecer duas maneiras de armazenar comandos.



1 caminho



Você pode usar um belo objeto semelhante a JSON para armazenar intenções, cenários de desenvolvimento, respostas em caso de tentativas malsucedidas (frequentemente usados ​​para bots de bate-papo). É mais ou menos assim:



config = {
    "intents": {
        "greeting": {
            "examples": ["", "", " ",
                         "hello", "good morning"],
            "responses": play_greetings
        },
        "farewell": {
            "examples": ["", " ", "", " ",
                         "goodbye", "bye", "see you soon"],
            "responses": play_farewell_and_quit
        },
        "google_search": {
            "examples": ["  ",
                         "search on google", "google", "find on google"],
            "responses": search_for_term_on_google
        },
    },
    "failure_phrases": play_failure_phrase
}


Esta opção é adequada para quem deseja treinar um assistente para responder a frases difíceis. Além disso, aqui você pode aplicar a abordagem NLU e criar a capacidade de prever a intenção do usuário, comparando-os com os que já estão na configuração.



Consideraremos esse método em detalhes na etapa 5 deste artigo. Enquanto isso, chamarei sua atenção para uma opção mais simples.



2 maneiras



Você pode pegar um dicionário simplificado, que terá a tupla do tipo hashable como chaves (já que os dicionários usam hashes para armazenar e recuperar elementos rapidamente), e os nomes das funções que serão executadas estarão na forma de valores. Para comandos curtos, a seguinte opção é adequada:



commands = {
    ("hello", "hi", "morning", ""): play_greetings,
    ("bye", "goodbye", "quit", "exit", "stop", ""): play_farewell_and_quit,
    ("search", "google", "find", ""): search_for_term_on_google,
    ("video", "youtube", "watch", ""): search_for_video_on_youtube,
    ("wikipedia", "definition", "about", "", ""): search_for_definition_on_wikipedia,
    ("translate", "interpretation", "translation", "", "", ""): get_translation,
    ("language", ""): change_language,
    ("weather", "forecast", "", ""): get_weather_forecast,
}


Para processá-lo, precisamos adicionar o código da seguinte maneira:



def execute_command_with_name(command_name: str, *args: list):
    """
          
    :param command_name:  
    :param args: ,     
    :return:
    """
    for key in commands.keys():
        if command_name in key:
            commands[key](*args)
        else:
            pass  # print("Command not found")


if __name__ == "__main__":

    #      
    recognizer = speech_recognition.Recognizer()
    microphone = speech_recognition.Microphone()

    while True:
        #        
        #      
        voice_input = record_and_recognize_audio()
        os.remove("microphone-results.wav")
        print(voice_input)

        #      ()
        voice_input = voice_input.split(" ")
        command = voice_input[0]
        command_options = [str(input_part) for input_part in voice_input[1:len(voice_input)]]
        execute_command_with_name(command, command_options)


Argumentos adicionais serão passados ​​para a função após a palavra de comando. Ou seja, se você disser a frase " vídeos são gatos fofos ", o comando " vídeo " chamará a função search_for_video_on_youtube () com o argumento " gatos fofos " e dará o seguinte resultado:



imagem



Um exemplo de tal função com processamento de argumentos recebidos:



def search_for_video_on_youtube(*args: tuple):
    """
       YouTube       
    :param args:   
    """
    if not args[0]: return
    search_term = " ".join(args[0])
    url = "https://www.youtube.com/results?search_query=" + search_term
    webbrowser.get().open(url)

    #       
    #  ,      JSON-
    play_voice_assistant_speech("Here is what I found for " + search_term + "on youtube")


É isso aí! A principal funcionalidade do bot está pronta. Então, você pode melhorá-lo infinitamente de várias maneiras. Minha implementação com comentários detalhados está disponível no meu GitHub .



Abaixo, veremos uma série de melhorias para tornar nosso assistente ainda mais inteligente.



Etapa 4. Adicionando multilinguismo



Para ensinar nosso assistente a trabalhar com modelos de vários idiomas, será mais conveniente organizar um pequeno arquivo JSON com uma estrutura simples:



{
  "Can you check if your microphone is on, please?": {
    "ru": ", ,   ",
    "en": "Can you check if your microphone is on, please?"
  },
  "What did you say again?": {
    "ru": ", ",
    "en": "What did you say again?"
  },
}


No meu caso, uso a alternância entre russo e inglês, já que modelos de reconhecimento de fala e voz para síntese de fala estão disponíveis para isso. O idioma será selecionado dependendo do idioma da fala do próprio assistente de voz.



Para receber a tradução, podemos criar uma classe separada com um método que nos retornará uma string com a tradução:



class Translation:
    """
           
      
    """
    with open("translations.json", "r", encoding="UTF-8") as file:
        translations = json.load(file)


    def get(self, text: str):
        """
                (  )
        :param text: ,   
        :return:     
        """
        if text in self.translations:
            return self.translations[text][assistant.speech_language]
        else:
            #        
            #        
            print(colored("Not translated phrase: {}".format(text), "red"))
            return text


Na função principal, antes do loop, declararemos nosso tradutor da seguinte maneira: tradutor = Tradução ()



Agora, ao tocar a fala do assistente, podemos obter a tradução da seguinte maneira:



play_voice_assistant_speech(translator.get(
    "Here is what I found for {} on Wikipedia").format(search_term))


Como você pode ver no exemplo acima, isso funciona mesmo para as linhas que requerem a inserção de argumentos adicionais. Dessa forma, você pode traduzir os conjuntos de frases "padrão" para seus assistentes.



Etapa 5. Um pouco de aprendizado de máquina



Agora, vamos voltar ao objeto JSON para armazenar comandos de várias palavras, que é típico para a maioria dos chatbots, que mencionei no parágrafo 3. É adequado para aqueles que não desejam usar comandos estritos e planejam expandir sua compreensão da intenção do usuário usando NLU -métodos.



Grosso modo, neste caso, as frases " boa tarde ", " boa noite " e " bom dia " serão consideradas equivalentes. O assistente entenderá que, nos três casos, a intenção do usuário era cumprimentar o assistente de voz.



Usando este método, você também pode criar um bot de conversação para bate-papos ou um modo de conversação para seu assistente de voz (para os casos em que você precisa de um interlocutor).



Para implementar essa possibilidade, precisaremos adicionar algumas funções:



def prepare_corpus():
    """
         
    """
    corpus = []
    target_vector = []
    for intent_name, intent_data in config["intents"].items():
        for example in intent_data["examples"]:
            corpus.append(example)
            target_vector.append(intent_name)

    training_vector = vectorizer.fit_transform(corpus)
    classifier_probability.fit(training_vector, target_vector)
    classifier.fit(training_vector, target_vector)


def get_intent(request):
    """
            
    :param request:  
    :return:   
    """
    best_intent = classifier.predict(vectorizer.transform([request]))[0]

    index_of_best_intent = list(classifier_probability.classes_).index(best_intent)
    probabilities = classifier_probability.predict_proba(vectorizer.transform([request]))[0]

    best_intent_probability = probabilities[index_of_best_intent]

    #        
    if best_intent_probability > 0.57:
        return best_intent


E também modifique ligeiramente a função principal adicionando inicialização de variáveis ​​para preparar o modelo e alterando o loop para a versão correspondente à nova configuração:



#         
# ( )
vectorizer = TfidfVectorizer(analyzer="char", ngram_range=(2, 3))
classifier_probability = LogisticRegression()
classifier = LinearSVC()
prepare_corpus()

while True:
    #         
    #      
    voice_input = record_and_recognize_audio()

    if os.path.exists("microphone-results.wav"):
        os.remove("microphone-results.wav")

    print(colored(voice_input, "blue"))

    #      ()
    if voice_input:
        voice_input_parts = voice_input.split(" ")

        #      -    
        #   
        if len(voice_input_parts) == 1:
            intent = get_intent(voice_input)
            if intent:
                config["intents"][intent]["responses"]()
            else:
                config["failure_phrases"]()

        #     -     
        #     ,
        #     
        if len(voice_input_parts) > 1:
            for guess in range(len(voice_input_parts)):
                intent = get_intent((" ".join(voice_input_parts[0:guess])).strip())
                if intent:
                    command_options = [voice_input_parts[guess:len(voice_input_parts)]]
                    config["intents"][intent]["responses"](*command_options)
                    break
                if not intent and guess == len(voice_input_parts)-1:
                    config["failure_phrases"]()


No entanto, este método é mais difícil de controlar: requer a verificação constante de que esta ou aquela frase ainda está corretamente identificada pelo sistema como parte desta ou daquela intenção. Portanto, esse método deve ser usado com cuidado (ou experimentar o próprio modelo).



Conclusão



Isso conclui meu pequeno tutorial.



Ficarei satisfeito se você compartilhar comigo nos comentários soluções de código aberto que você conhece que podem ser implementadas neste projeto, bem como suas idéias sobre quais outras funções online e offline podem ser implementadas.



As fontes documentadas do meu assistente de voz em duas versões podem ser encontradas aqui .



PS: A solução funciona em Windows, Linux e MacOS com pequenas diferenças na instalação de bibliotecas PyAudio e Google.



All Articles