Depois da minha última postagem sobre como construir seu próprio RAG e executá-lo localmente, hoje estamos dando um passo adiante, não apenas implementando as habilidades de conversação de grandes modelos de linguagem, mas também adicionando capacidades de escuta e fala. A ideia é simples: vamos criar um assistente de voz que lembra Jarvis ou Friday dos icônicos filmes do Homem de Ferro, que pode funcionar offline no seu computador. Como este é um tutorial introdutório, irei implementá-lo em Python e mantê-lo simples o suficiente para iniciantes. Por fim, fornecerei algumas orientações sobre como dimensionar o aplicativo. Pilha de tecnologia Primeiro, você deve configurar um ambiente Python virtual. Você tem várias opções para isso, incluindo pyenv, virtualenv, poesia e outras que atendem a um propósito semelhante. Pessoalmente, usarei Poesia para este tutorial devido às minhas preferências pessoais. Aqui estão várias bibliotecas cruciais que você precisará instalar: : Para uma saída de console visualmente atraente. rich : Uma ferramenta robusta para conversão de fala em texto. openai-whisper : Uma biblioteca de ponta para síntese de texto para fala, garantindo saída de áudio de alta qualidade. suno-bark : Uma biblioteca simples para interface com Large Language Models (LLMs). langchain , e : essenciais para gravação e reprodução de áudio. sounddevice pyaudio reconhecimento de fala Para obter uma lista detalhada de dependências, consulte o link . aqui O componente mais crítico aqui é o backend do Large Language Model (LLM), para o qual usaremos o Ollama. é amplamente reconhecido como uma ferramenta popular para executar e servir LLMs offline. Se Ollama é novo para você, recomendo verificar meu artigo anterior sobre RAG offline: Basicamente, você só precisa baixar o aplicativo Ollama, extrair seu modelo preferido e executá-lo. Ollama "Construa seu próprio RAG e execute-o localmente: Langchain + Ollama + Streamlit." Arquitetura Pronto, se tudo estiver configurado, vamos para o próximo passo. Abaixo está a arquitetura geral da nossa aplicação, que compreende fundamentalmente 3 componentes principais: : Utilizando , convertemos a linguagem falada em texto. O treinamento do Whisper em diversos conjuntos de dados garante sua proficiência em vários idiomas e dialetos. Reconhecimento de fala o Whisper da OpenAI : Para os recursos conversacionais, empregaremos a interface Langchain para o modelo , que é servido usando Ollama. Esta configuração promete um fluxo de conversação contínuo e envolvente. Cadeia Conversacional Llama-2 : A transformação de texto em fala é alcançada através , um modelo de última geração da Suno AI, conhecido por sua produção de fala realista. Sintetizador de fala do Bark O fluxo de trabalho é simples: gravar fala, transcrever para texto, gerar uma resposta usando um LLM e vocalizar a resposta usando Bark. Diagrama de sequência para assistente de voz com Whisper, Ollama e Bark. Implementação A implementação começa com a criação de um baseado em Bark, incorporando métodos para sintetizar fala a partir de texto e lidar perfeitamente com entradas de texto mais longas, como segue: TextToSpeechService import nltk import torch import warnings import numpy as np from transformers import AutoProcessor, BarkModel warnings.filterwarnings( "ignore", message="torch.nn.utils.weight_norm is deprecated in favor of torch.nn.utils.parametrizations.weight_norm.", ) class TextToSpeechService: def __init__(self, device: str = "cuda" if torch.cuda.is_available() else "cpu"): """ Initializes the TextToSpeechService class. Args: device (str, optional): The device to be used for the model, either "cuda" if a GPU is available or "cpu". Defaults to "cuda" if available, otherwise "cpu". """ self.device = device self.processor = AutoProcessor.from_pretrained("suno/bark-small") self.model = BarkModel.from_pretrained("suno/bark-small") self.model.to(self.device) def synthesize(self, text: str, voice_preset: str = "v2/en_speaker_1"): """ Synthesizes audio from the given text using the specified voice preset. Args: text (str): The input text to be synthesized. voice_preset (str, optional): The voice preset to be used for the synthesis. Defaults to "v2/en_speaker_1". Returns: tuple: A tuple containing the sample rate and the generated audio array. """ inputs = self.processor(text, voice_preset=voice_preset, return_tensors="pt") inputs = {k: v.to(self.device) for k, v in inputs.items()} with torch.no_grad(): audio_array = self.model.generate(**inputs, pad_token_id=10000) audio_array = audio_array.cpu().numpy().squeeze() sample_rate = self.model.generation_config.sample_rate return sample_rate, audio_array def long_form_synthesize(self, text: str, voice_preset: str = "v2/en_speaker_1"): """ Synthesizes audio from the given long-form text using the specified voice preset. Args: text (str): The input text to be synthesized. voice_preset (str, optional): The voice preset to be used for the synthesis. Defaults to "v2/en_speaker_1". Returns: tuple: A tuple containing the sample rate and the generated audio array. """ pieces = [] sentences = nltk.sent_tokenize(text) silence = np.zeros(int(0.25 * self.model.generation_config.sample_rate)) for sent in sentences: sample_rate, audio_array = self.synthesize(sent, voice_preset) pieces += [audio_array, silence.copy()] return self.model.generation_config.sample_rate, np.concatenate(pieces) : A classe usa um parâmetro opcional , que especifica o dispositivo a ser usado para o modelo ( se uma GPU estiver disponível, ou ). Ele carrega o modelo Bark e o processador correspondente do modelo pré-treinado . Você também pode usar a versão grande especificando para o carregador de modelo. Inicialização ( __init__ ) device cuda cpu suno/bark-small suno/bark : Este método recebe uma entrada e um parâmetro , que especifica a voz a ser usada para a síntese. Você pode verificar outro valor . Ele usa o para preparar o texto de entrada e a predefinição de voz e, em seguida, gera a matriz de áudio usando o método . A matriz de áudio gerada é convertida em uma matriz NumPy e a taxa de amostragem é retornada junto com a matriz de áudio. Sintetizar ( synthesize ) text voice_preset voice_preset aqui processor model.generate() : Este método é usado para sintetizar entradas de texto mais longas. Primeiro, ele transforma o texto de entrada em frases usando a função . Para cada frase, ele chama o método para gerar o array de áudio. Em seguida, ele concatena as matrizes de áudio geradas, com um breve silêncio (0,25 segundos) adicionado entre cada frase. Sintetização de formato longo ( long_form_synthesize ) nltk.sent_tokenize synthesize Agora que temos o configurado, precisamos preparar o servidor Ollama para o serviço do modelo de linguagem grande (LLM). Para fazer isso, você precisará seguir estas etapas: TextToSpeechService : Execute o seguinte comando para baixar o modelo Llama-2 mais recente do repositório Ollama: Extraia o modelo Llama-2 mais recente ollama pull llama2 . : Se o servidor ainda não foi iniciado, execute o seguinte comando para iniciá-lo: . Inicie o servidor Ollama ollama serve Depois de concluir essas etapas, seu aplicativo poderá usar o servidor Ollama e o modelo Llama-2 para gerar respostas à entrada do usuário. A seguir, passaremos para a lógica principal do aplicativo. Primeiro, precisamos inicializar os seguintes componentes: : usaremos a biblioteca Rich para criar um console interativo melhor para o usuário dentro do terminal. Rich Console : inicializaremos um modelo de reconhecimento de fala Whisper, que é um sistema de reconhecimento de fala de código aberto de última geração desenvolvido pela OpenAI. Usaremos o modelo básico em inglês ( ) para transcrever a entrada do usuário. Whisper Speech-to-Text base.en : inicializaremos uma instância do sintetizador de texto para fala Bark, que foi implementada acima. Bark Text-to-Speech : Usaremos o integrado da biblioteca Langchain, que fornece um modelo para gerenciar o fluxo conversacional. Iremos configurá-lo para usar o modelo de linguagem Llama-2 com o backend Ollama. Cadeia Conversacional ConversationalChain import time import threading import numpy as np import whisper import sounddevice as sd from queue import Queue from rich.console import Console from langchain.memory import ConversationBufferMemory from langchain.chains import ConversationChain from langchain.prompts import PromptTemplate from langchain_community.llms import Ollama from tts import TextToSpeechService console = Console() stt = whisper.load_model("base.en") tts = TextToSpeechService() template = """ You are a helpful and friendly AI assistant. You are polite, respectful, and aim to provide concise responses of less than 20 words. The conversation transcript is as follows: {history} And here is the user's follow-up: {input} Your response: """ PROMPT = PromptTemplate(input_variables=["history", "input"], template=template) chain = ConversationChain( prompt=PROMPT, verbose=False, memory=ConversationBufferMemory(ai_prefix="Assistant:"), llm=Ollama(), ) Agora, vamos definir as funções necessárias: : Esta função é executada em um thread separado para capturar dados de áudio do microfone do usuário usando . A função de retorno de chamada é chamada sempre que novos dados de áudio estão disponíveis e coloca os dados em para processamento posterior. record_audio sounddevice.RawInputStream data_queue : esta função utiliza a instância Whisper para transcrever os dados de áudio de em texto. transcribe data_queue : Esta função alimenta o contexto da conversa atual para o modelo de linguagem Llama-2 (por meio do Langchain ) e recupera a resposta de texto gerada. get_llm_response ConversationalChain : Esta função pega a forma de onda de áudio gerada pelo mecanismo de conversão de texto em fala Bark e a reproduz para o usuário usando uma biblioteca de reprodução de som (por exemplo, ). play_audio sounddevice def record_audio(stop_event, data_queue): """ Captures audio data from the user's microphone and adds it to a queue for further processing. Args: stop_event (threading.Event): An event that, when set, signals the function to stop recording. data_queue (queue.Queue): A queue to which the recorded audio data will be added. Returns: None """ def callback(indata, frames, time, status): if status: console.print(status) data_queue.put(bytes(indata)) with sd.RawInputStream( samplerate=16000, dtype="int16", channels=1, callback=callback ): while not stop_event.is_set(): time.sleep(0.1) def transcribe(audio_np: np.ndarray) -> str: """ Transcribes the given audio data using the Whisper speech recognition model. Args: audio_np (numpy.ndarray): The audio data to be transcribed. Returns: str: The transcribed text. """ result = stt.transcribe(audio_np, fp16=False) # Set fp16=True if using a GPU text = result["text"].strip() return text def get_llm_response(text: str) -> str: """ Generates a response to the given text using the Llama-2 language model. Args: text (str): The input text to be processed. Returns: str: The generated response. """ response = chain.predict(input=text) if response.startswith("Assistant:"): response = response[len("Assistant:") :].strip() return response def play_audio(sample_rate, audio_array): """ Plays the given audio data using the sounddevice library. Args: sample_rate (int): The sample rate of the audio data. audio_array (numpy.ndarray): The audio data to be played. Returns: None """ sd.play(audio_array, sample_rate) sd.wait() Em seguida, definimos o loop principal da aplicação. O loop principal do aplicativo orienta o usuário através da interação conversacional da seguinte forma: O usuário é solicitado a pressionar Enter para começar a registrar sua entrada. Depois que o usuário pressiona Enter, a função é chamada em um thread separado para capturar a entrada de áudio do usuário. record_audio Quando o usuário pressiona Enter novamente para interromper a gravação, os dados de áudio são transcritos usando a função . transcribe O texto transcrito é então passado para a função , que gera uma resposta usando o modelo de linguagem Llama-2. get_llm_response A resposta gerada é impressa no console e reproduzida para o usuário usando a função . play_audio if __name__ == "__main__": console.print("[cyan]Assistant started! Press Ctrl+C to exit.") try: while True: console.input( "Press Enter to start recording, then press Enter again to stop." ) data_queue = Queue() # type: ignore[var-annotated] stop_event = threading.Event() recording_thread = threading.Thread( target=record_audio, args=(stop_event, data_queue), ) recording_thread.start() input() stop_event.set() recording_thread.join() audio_data = b"".join(list(data_queue.queue)) audio_np = ( np.frombuffer(audio_data, dtype=np.int16).astype(np.float32) / 32768.0 ) if audio_np.size > 0: with console.status("Transcribing...", spinner="earth"): text = transcribe(audio_np) console.print(f"[yellow]You: {text}") with console.status("Generating response...", spinner="earth"): response = get_llm_response(text) sample_rate, audio_array = tts.long_form_synthesize(response) console.print(f"[cyan]Assistant: {response}") play_audio(sample_rate, audio_array) else: console.print( "[red]No audio recorded. Please ensure your microphone is working." ) except KeyboardInterrupt: console.print("\n[red]Exiting...") console.print("[blue]Session ended.") Resultado Depois que tudo estiver montado, podemos executar o aplicativo conforme mostrado no vídeo acima. O aplicativo roda bem devagar no meu MacBook porque o modelo Bark é grande, mesmo em sua versão menor. Portanto, acelerei um pouco o vídeo. Para aqueles com um computador habilitado para CUDA, ele pode funcionar mais rápido. Aqui estão os principais recursos do nosso aplicativo: : os usuários podem iniciar e parar a gravação de sua entrada de voz e o assistente responde reproduzindo o áudio gerado. Interação baseada em voz O assistente mantém o contexto da conversa, possibilitando respostas mais coerentes e relevantes. O uso do modelo de linguagem Llama-2 permite ao assistente fornecer respostas concisas e focadas. Contexto conversacional: Para aqueles que desejam elevar este aplicativo ao status de pronto para produção, são recomendadas as seguintes melhorias: : incorpore versões otimizadas dos modelos, como Whisper.cpp, llama.cpp e late.cpp, que são projetados para aumentar o desempenho, especialmente em computadores de baixo custo. Otimização de desempenho : implemente um sistema que permita aos usuários personalizar a persona e o prompt do bot, possibilitando a criação de diferentes tipos de assistentes (por exemplo, pessoais, profissionais ou específicos de domínio). Prompts de bot personalizáveis : Desenvolva uma GUI amigável para aprimorar a experiência geral do usuário, tornando o aplicativo mais acessível e visualmente atraente. Interface gráfica do usuário (GUI) : expanda o aplicativo para suportar interações multimodais, como a capacidade de gerar e exibir imagens, diagramas ou outro conteúdo visual, além das respostas baseadas em voz. Capacidades multimodais Finalmente, concluímos nosso aplicativo simples de assistente de voz. O código completo pode ser encontrado em: . Essa combinação de reconhecimento de fala, modelagem de linguagem e tecnologias de conversão de texto em fala demonstra como podemos construir algo que parece difícil, mas que na verdade pode ser executado em seu computador. Vamos aproveitar a codificação e não se esqueça de assinar para não perder as últimas novidades em artigos sobre IA e programação. https://github.com/vndee/local-talking-llm meu blog Também publicado aqui