paint-brush
Como construir seu próprio confessionário de IA: como adicionar uma voz ao LLMby@slavasobolev
661
661

Como construir seu próprio confessionário de IA: como adicionar uma voz ao LLM

Iaroslav Sobolev11m2024/07/14
Read on Terminal Reader

No final de fevereiro, Bali acolheu o festival [Lampu](https://lampu.org/), organizado de acordo com os princípios do famoso Burning Man. Fomos inspirados pela ideia dos confessionários católicos e pelas capacidades do atual LLM. Construímos nosso próprio confessionário de IA, onde qualquer pessoa poderia conversar com uma inteligência artificial.
featured image - Como construir seu próprio confessionário de IA: como adicionar uma voz ao LLM
Iaroslav Sobolev HackerNoon profile picture
0-item
1-item

Embora a OpenAI esteja atrasando o lançamento dos modos de voz avançados para ChatGPT, quero compartilhar como construímos nosso aplicativo de voz LLM e o integramos em uma cabine interativa.

Fale com a IA na selva

No final de fevereiro, Bali sediou o festival Lampu , organizado de acordo com os princípios do famoso Burning Man. Seguindo sua tradição, os participantes criam suas próprias instalações e objetos de arte.


Meus amigos do Camp 19:19 e eu, inspirados pela ideia dos confessionários católicos e pelas capacidades dos atuais LLMs, tivemos a ideia de construir nosso próprio confessionário de IA, onde qualquer pessoa pudesse conversar com uma inteligência artificial.


Veja como imaginamos isso no início:

  • Quando o usuário entra em um estande, determinamos que precisamos iniciar uma nova sessão.


  • O usuário faz uma pergunta e a IA escuta e responde. Queríamos criar um ambiente privado e de confiança onde todos pudessem discutir abertamente seus pensamentos e experiências.


  • Quando o usuário sai da sala, o sistema encerra a sessão e esquece todos os detalhes da conversa. Isso é necessário para manter todos os diálogos privados.

Prova de conceito

Para testar o conceito e começar a experimentar um prompt para o LLM, criei uma implementação ingênua em uma noite:

  • Ouça um microfone.
  • Reconheça a fala do usuário usando o modelo Speech-to-Text (STT) .
  • Gere uma resposta via LLM .
  • Sintetize uma resposta de voz usando o modelo Text-to-Speech (TTS) .
  • Reproduza a resposta para o usuário.



Para implementar esta demonstração, confiei inteiramente nos modelos de nuvem da OpenAI: Whisper , GPT-4 e TTS . Graças à excelente biblioteca speech_recognition , construí a demonstração em apenas algumas dezenas de linhas de código.


 import os import asyncio from dotenv import load_dotenv from io import BytesIO from openai import AsyncOpenAI from soundfile import SoundFile import sounddevice as sd import speech_recognition as sr load_dotenv() aiclient = AsyncOpenAI( api_key=os.environ.get("OPENAI_API_KEY") ) SYSTEM_PROMPT = """ You are helpfull assistant. """ async def listen_mic(recognizer: sr.Recognizer, microphone: sr.Microphone): audio_data = recognizer.listen(microphone) wav_data = BytesIO(audio_data.get_wav_data()) wav_data.name = "SpeechRecognition_audio.wav" return wav_data async def say(text: str): res = await aiclient.audio.speech.create( model="tts-1", voice="alloy", response_format="opus", input=text ) buffer = BytesIO() for chunk in res.iter_bytes(chunk_size=4096): buffer.write(chunk) buffer.seek(0) with SoundFile(buffer, 'r') as sound_file: data = sound_file.read(dtype='int16') sd.play(data, sound_file.samplerate) sd.wait() async def respond(text: str, history): history.append({"role": "user", "content": text}) completion = await aiclient.chat.completions.create( model="gpt-4", temperature=0.5, messages=history, ) response = completion.choices[0].message.content await say(response) history.append({"role": "assistant", "content": response}) async def main() -> None: m = sr.Microphone() r = sr.Recognizer() messages = [{"role": "system", "content": SYSTEM_PROMPT}] with m as source: r.adjust_for_ambient_noise(source) while True: wav_data = await listen_mic(r, source) transcript = await aiclient.audio.transcriptions.create( model="whisper-1", temperature=0.5, file=wav_data, response_format="verbose_json", ) if transcript.text == '' or transcript.text is None: continue await respond(transcript.text, messages) if __name__ == '__main__': asyncio.run(main())


Os problemas que tivemos que resolver tornaram-se imediatamente aparentes após os primeiros testes desta demonstração:

  • Atraso na resposta . Em uma implementação ingênua, o atraso entre a pergunta e a resposta do usuário é de 7 a 8 segundos ou mais. Isso não é bom, mas obviamente existem muitas maneiras de otimizar o tempo de resposta.


  • Ambiente barulhento . Descobrimos que em ambientes barulhentos não podemos confiar no microfone para detectar automaticamente quando um usuário começou e terminou de falar. Reconhecer o início e o fim de uma frase ( endpointing ) não é uma tarefa trivial. Junte isso ao ambiente barulhento de um festival de música e fica claro que é necessária uma abordagem conceitualmente diferente.


  • Imitar uma conversa ao vivo . Queríamos dar ao usuário a capacidade de interromper a IA. Para conseguir isso, teríamos que manter o microfone ligado. Mas neste caso, teríamos que separar a voz do usuário não só dos sons de fundo, mas também da voz da IA.


  • Opinião . Devido ao atraso na resposta, às vezes parecia-nos que o sistema estava congelado. Percebemos que precisamos informar ao usuário quanto tempo a resposta estará processando


Tivemos que escolher como resolver esses problemas: procurando uma solução de engenharia ou de produto adequada.

Pensando na UX do estande

Antes mesmo de começarmos a codificar, tivemos que decidir como o usuário interagiria com o estande:

  • Devemos decidir como detectar um novo usuário na cabine para redefinir o histórico de diálogos anteriores.


  • Como reconhecer o início e o fim da fala de um usuário e o que fazer caso ele queira interromper a IA.


  • Como implementar feedback quando há uma resposta atrasada da IA.


Para detectar um novo usuário no estande, consideramos diversas opções: sensores de abertura de portas, sensores de peso do piso, sensores de distância e modelo câmera + YOLO. O sensor de distância atrás nos pareceu o mais confiável, pois excluía acionamentos acidentais, como quando a porta não estava bem fechada, e não exigia instalação complicada, ao contrário do sensor de peso.


Para evitar o desafio de reconhecer o início e o fim de um diálogo, decidimos adicionar um grande botão vermelho para controlar o microfone. Esta solução também permitiu ao usuário interromper a IA a qualquer momento.


Tínhamos muitas ideias diferentes sobre a implementação de feedback no processamento de uma solicitação. Optamos por uma opção com tela que mostra o que o sistema está fazendo: ouvindo o microfone, processando uma pergunta ou respondendo.


Também consideramos uma opção bastante inteligente com um telefone fixo antigo. A sessão começaria quando o usuário atendesse o telefone e o sistema ouviria o usuário até que ele desligasse. No entanto, decidimos que é mais autêntico quando o usuário é “atendido” pela cabine do que por uma voz do telefone.


Durante a instalação e no festival


No final, o fluxo final do usuário ficou assim:

  • Um usuário entra em um estande. Um sensor de distância é acionado nas suas costas e nós o cumprimentamos.


  • O usuário pressiona um botão vermelho para iniciar um diálogo. Ouvimos o microfone enquanto o botão é pressionado. Quando o usuário solta o botão, iniciamos o processamento da solicitação e a indicamos na tela.


  • Se o usuário quiser fazer uma nova pergunta enquanto a IA estiver respondendo, ele poderá pressionar o botão novamente e a IA irá parar imediatamente de responder.


  • Quando o usuário sai da cabine, o sensor de distância é acionado novamente e limpamos o histórico de diálogos.

Arquitetura


O Arduino monitora o estado do sensor de distância e do botão vermelho. Ele envia todas as alterações para nosso backend via API HTTP, o que permite ao sistema determinar se o usuário entrou ou saiu do estande e se é necessário ativar a escuta do microfone ou começar a gerar uma resposta.


A UI da web é apenas uma página da web aberta em um navegador que recebe continuamente o estado atual do sistema do backend e o exibe ao usuário.


O backend controla o microfone, interage com todos os modelos de IA necessários e dá voz às respostas do LLM. Ele contém a lógica central do aplicativo.

Hardware

Como codificar um esboço para Arduino, conectar corretamente o sensor de distância e o botão e montar tudo no estande é assunto para um artigo à parte. Vamos revisar brevemente o que obtivemos sem entrar em detalhes técnicos.


Utilizamos um Arduino, mais precisamente, o modelo ESP32 com módulo Wi-Fi embutido. O microcontrolador estava conectado à mesma rede Wi-Fi do laptop, que executava o backend.



Lista completa de hardware que usamos:

Processo interno

Os principais componentes do pipeline são Speech-To-Text (STT), LLM e Text-To-Speech (TTS). Para cada tarefa, muitos modelos diferentes estão disponíveis localmente e através da nuvem.



Como não tínhamos uma GPU poderosa em mãos, decidimos optar por versões dos modelos baseadas em nuvem. O ponto fraco desta abordagem é a necessidade de uma boa conexão com a Internet. Apesar disso, a velocidade de interação após todas as otimizações foi aceitável, mesmo com a Internet móvel que tivemos no festival.


Agora, vamos dar uma olhada mais de perto em cada componente do pipeline.

Reconhecimento de fala

Muitos dispositivos modernos há muito suportam o reconhecimento de fala. Por exemplo, a Apple Speech API está disponível para iOS e macOS, e a Web Speech API está disponível para navegadores.


Infelizmente, eles são de qualidade muito inferior ao Whisper ou Deepgram e não conseguem detectar o idioma automaticamente.


Para reduzir o tempo de processamento, a melhor opção é reconhecer a fala em tempo real enquanto o usuário fala. Aqui estão alguns projetos com exemplos de como implementá-los: sussurro_streaming , sussurro.cpp


Com nosso laptop, a velocidade de reconhecimento de fala usando essa abordagem ficou longe de ser em tempo real. Após vários experimentos, decidimos pelo modelo Whisper baseado em nuvem da OpenAI.

LLM e Engenharia Prompt

O resultado do modelo Speech To Text da etapa anterior é o texto que enviamos para o LLM com o histórico do diálogo.


Ao escolher um LLM, comparamos o GPT-3.5. GPT-4 e Cláudio. Descobriu-se que o fator chave não era tanto o modelo específico, mas sim a sua configuração. No final das contas, optamos pelo GPT-4, cujas respostas gostamos mais do que as outras.


A personalização do prompt para modelos LLM tornou-se uma forma de arte separada. Existem muitos guias na Internet sobre como ajustar seu modelo conforme necessário:



Tivemos que experimentar extensivamente as configurações de prompt e temperatura para fazer o modelo responder de forma envolvente, concisa e bem-humorada.

Texto para fala

Expressamos a resposta recebida do LLM usando o modelo Text-To-Speech e a reproduzimos para o usuário. Esta etapa foi a principal fonte de atrasos em nossa demonstração.


Os LLMs demoram muito para responder. No entanto, eles suportam a geração de resposta em modo streaming – token por token. Podemos usar esse recurso para otimizar o tempo de espera, expressando frases individuais à medida que são recebidas, sem esperar por uma resposta completa do LLM.


Expressando frases individuais


  • Faça uma consulta ao LLM.


  • Acumulamos a resposta no buffer token por token até termos uma frase completa de comprimento mínimo . O parâmetro de comprimento mínimo é significativo porque afeta tanto a entonação da abertura quanto o tempo de atraso inicial.


  • Envie a frase gerada para o modelo TTS e reproduza o resultado para o usuário. Nesta etapa é necessário garantir que não haja nenhuma condição de corrida na ordem de reprodução.


  • Repita a etapa anterior até o final da resposta LLM


Usamos o tempo enquanto o usuário escuta o fragmento inicial para ocultar o atraso no processamento das partes restantes da resposta do LLM. Graças a esta abordagem, o atraso de resposta ocorre apenas no início e é de aproximadamente 3 segundos.


 async generateResponse(history) { const completion = await this.ai.completion(history); const chunks = new DialogChunks(); for await (const chunk of completion) { const delta = chunk.choices[0]?.delta?.content; if (delta) { chunks.push(delta); if (chunks.hasCompleteSentence()) { const sentence = chunks.popSentence(); this.voice.ttsAndPlay(sentence); } } } const sentence = chunks.popSentence(); if (sentence) { this.voice.say(sentence); } return chunks.text; }


Últimos retoques

Mesmo com todas as nossas otimizações, um atraso de 3 a 4 segundos ainda é significativo. Decidimos cuidar da IU com feedback para evitar que o usuário sinta que a resposta está travada. Vimos várias abordagens:


  • Indicadores LED . Precisávamos exibir cinco estados: inativo, espera, escuta, pensamento e fala. Mas não conseguimos descobrir como fazer isso de uma forma que fosse fácil de entender com os LEDs.


  • Palavras de preenchimento , como "Deixe-me pensar", "Hmm" e assim por diante, imitam a fala da vida real. Rejeitamos esta opção porque os preenchimentos muitas vezes não correspondiam ao tom das respostas do modelo.


  • Coloque uma tela na cabine. E exiba diferentes estados com animações.


Optamos pela última opção com uma página web simples que pesquisa o backend e mostra animações de acordo com o estado atual.


Os resultados

Nossa sala de confissão de IA funcionou durante quatro dias e atraiu centenas de participantes. Gastamos cerca de US$ 50 em APIs OpenAI. Em troca, recebemos feedback positivo substancial e impressões valiosas.


Este pequeno experimento mostrou que é possível adicionar uma interface de voz intuitiva e eficiente a um LLM mesmo com recursos limitados e condições externas desafiadoras.


A propósito, as fontes de back-end disponíveis no GitHub