From 97f50f51bd276038b7ea3157f435314e26009209 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 3 Dec 2024 19:15:22 -0300 Subject: [PATCH] =?UTF-8?q?Implementa=20configura=C3=A7=C3=A3o=20de=20vari?= =?UTF-8?q?=C3=A1veis=20de=20ambiente=20e=20logging,=20atualiza=20document?= =?UTF-8?q?a=C3=A7=C3=A3o.=20Inclui=20novos=20arquivos=20.env.example=20e?= =?UTF-8?q?=20config.py,=20al=C3=A9m=20de=20melhorias=20no=20tratamento=20?= =?UTF-8?q?de=20erros=20e=20mensagens=20de=20log.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env_sample => .env.example | 3 + config.py | 117 ++++++++++++++++ docker-compose.yaml | 2 + main.py | 62 ++++----- readme.md | 199 ++++++++++++-------------- requirements.txt | 9 +- services.py | 268 +++++++++++++++++++++--------------- 7 files changed, 397 insertions(+), 263 deletions(-) rename .env_sample => .env.example (93%) create mode 100644 config.py diff --git a/.env_sample b/.env.example similarity index 93% rename from .env_sample rename to .env.example index d8bc802..e2db834 100644 --- a/.env_sample +++ b/.env.example @@ -13,3 +13,6 @@ GROQ_API_KEY=your_groq_api_key # Host e porta do Redis (caso esteja utilizando) REDIS_HOST=localhost REDIS_PORT=6379 + +DEBUG_MODE=true +LOG_LEVEL=INFO diff --git a/config.py b/config.py new file mode 100644 index 0000000..d4ac767 --- /dev/null +++ b/config.py @@ -0,0 +1,117 @@ +import os +from dotenv import load_dotenv +import logging +from pathlib import Path + +# Configuração de logging com cores e formatação melhorada +class ColoredFormatter(logging.Formatter): + """Formatter personalizado que adiciona cores aos logs""" + grey = "\x1b[38;21m" + blue = "\x1b[38;5;39m" + yellow = "\x1b[38;5;226m" + red = "\x1b[38;5;196m" + bold_red = "\x1b[31;1m" + reset = "\x1b[0m" + + def __init__(self, fmt): + super().__init__() + self.fmt = fmt + self.FORMATS = { + logging.DEBUG: self.blue + self.fmt + self.reset, + logging.INFO: self.grey + self.fmt + self.reset, + logging.WARNING: self.yellow + self.fmt + self.reset, + logging.ERROR: self.red + self.fmt + self.reset, + logging.CRITICAL: self.bold_red + self.fmt + self.reset + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + +# Configuração inicial do logging +logger = logging.getLogger(__name__) +handler = logging.StreamHandler() +handler.setFormatter(ColoredFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) +logger.addHandler(handler) + +# Carregar variáveis de ambiente +env_path = Path('.env') +if env_path.exists(): + logger.debug(f"Arquivo .env encontrado em: {env_path.absolute()}") + load_dotenv(override=True) +else: + logger.warning("Arquivo .env não encontrado! Usando variáveis de ambiente do sistema.") + +class Settings: + def __init__(self): + logger.debug("Iniciando carregamento das configurações...") + + # Carregamento das variáveis com logs detalhados + self.DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true' + logger.debug(f"DEBUG_MODE configurado como: {self.DEBUG_MODE}") + + self.WHATSAPP_INSTANCE = os.getenv('WHATSAPP_INSTANCE') + logger.debug(f"WHATSAPP_INSTANCE configurada como: {self.WHATSAPP_INSTANCE}") + + self.WHATSAPP_API_KEY = os.getenv('WHATSAPP_API_KEY') + logger.debug(f"WHATSAPP_API_KEY configurada: {'Presente' if self.WHATSAPP_API_KEY else 'Ausente'}") + + self.WHATSAPP_API_URL = os.getenv('WHATSAPP_API_URL') + logger.debug(f"WHATSAPP_API_URL configurada como: {self.WHATSAPP_API_URL}") + + self.GROQ_API_KEY = os.getenv('GROQ_API_KEY') + if self.GROQ_API_KEY: + masked_key = f"{self.GROQ_API_KEY[:10]}...{self.GROQ_API_KEY[-4:]}" + logger.debug(f"GROQ_API_KEY carregada: {masked_key}") + else: + logger.error("GROQ_API_KEY não encontrada!") + + self.BUSINESS_MESSAGE = os.getenv('BUSINESS_MESSAGE', '*Impacte AI* Premium Services') + logger.debug(f"BUSINESS_MESSAGE configurada como: {self.BUSINESS_MESSAGE}") + + self.PROCESS_GROUP_MESSAGES = os.getenv('PROCESS_GROUP_MESSAGES', 'false').lower() == 'true' + logger.debug(f"PROCESS_GROUP_MESSAGES configurado como: {self.PROCESS_GROUP_MESSAGES}") + + self.LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + logger.debug(f"LOG_LEVEL configurado como: {self.LOG_LEVEL}") + + def validate(self): + """Validação detalhada das configurações críticas""" + logger.debug("Iniciando validação das configurações...") + + validation_errors = [] + + if not self.GROQ_API_KEY: + validation_errors.append("GROQ_API_KEY não está definida") + elif not self.GROQ_API_KEY.startswith('gsk_'): + validation_errors.append("GROQ_API_KEY inválida: deve começar com 'gsk_'") + + if not self.WHATSAPP_API_KEY: + validation_errors.append("WHATSAPP_API_KEY não está definida") + + if not self.WHATSAPP_API_URL: + validation_errors.append("WHATSAPP_API_URL não está definida") + + if not self.WHATSAPP_INSTANCE: + validation_errors.append("WHATSAPP_INSTANCE não está definida") + + if validation_errors: + for error in validation_errors: + logger.error(f"Erro de validação: {error}") + return False + + logger.info("Todas as configurações foram validadas com sucesso!") + return True + +# Criar instância das configurações +settings = Settings() + +# Validar configurações +if not settings.validate(): + logger.critical("Configurações inválidas detectadas. A aplicação pode não funcionar corretamente!") + +# Ajustar nível de log +log_level = logging.DEBUG if settings.DEBUG_MODE else getattr(logging, settings.LOG_LEVEL.upper()) +logger.setLevel(log_level) +logger.info(f"Nível de log definido como: {logging.getLevelName(log_level)}") \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 9331475..0a965c8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -19,6 +19,8 @@ services: GROQ_API_KEY: "substitua_sua_chave_GROQ_aqui" #coloque sua chave GROQ aqui BUSINESS_MESSAGE: "substitua_sua_mensagem_de_servico_aqui" #coloque a mensagem que será enviada ao final da transcrição aqui PROCESS_GROUP_MESSAGES: "false" # Define se mensagens de grupos devem ser processadas + DEBUG_MODE: "false" + LOG_LEVEL: "INFO" deploy: mode: replicated replicas: 1 diff --git a/main.py b/main.py index a95e1b6..96d3195 100644 --- a/main.py +++ b/main.py @@ -7,78 +7,72 @@ from services import ( ) from models import WebhookRequest import aiohttp -from dotenv import load_dotenv -import os -from services import get_env_var - -# Carregar variáveis do .env -load_dotenv() +from config import settings, logger app = FastAPI() -# Obter a mensagem do negócio da variável de ambiente -BUSINESS_MESSAGE = get_env_var("BUSINESS_MESSAGE", "*Impacte AI* Premium Services") -PROCESS_GROUP_MESSAGES = get_env_var("PROCESS_GROUP_MESSAGES", "false").lower() == "true" - @app.post("/transcreve-audios") async def transcreve_audios(request: Request): try: - # Receber o corpo do webhook + logger.info("Iniciando processamento de áudio") body = await request.json() + + if settings.DEBUG_MODE: + logger.debug(f"Payload recebido: {body}") - # Extraindo informações necessárias do JSON + # Extraindo informações server_url = body["server_url"] - instance = body["instance"] # os.getenv("WHATSAPP_INSTANCE") - apikey = body["apikey"] # os.getenv("WHATSAPP_API_KEY") + instance = body.get("instance", settings.WHATSAPP_INSTANCE) + apikey = body.get("apikey", settings.WHATSAPP_API_KEY) audio_key = body["data"]["key"]["id"] from_me = body["data"]["key"]["fromMe"] remote_jid = body["data"]["key"]["remoteJid"] - # Verificar se a mensagem foi enviada por mim if from_me: + logger.info("Mensagem enviada pelo próprio usuário, ignorando") return {"message": "Mensagem enviada por mim, sem operação"} - # Decidir se processa mensagens de grupos - if "@g.us" in remote_jid and not PROCESS_GROUP_MESSAGES: + if "@g.us" in remote_jid and not settings.PROCESS_GROUP_MESSAGES: + logger.info("Mensagem de grupo ignorada conforme configuração") return {"message": "Mensagem enviada por um grupo, sem operação"} - if "base64" not in body: - - # Pega o áudio em Base64 - base64_audio = await get_audio_base64( - server_url, instance, apikey, audio_key - ) - + # Verificar se temos mediaUrl ou precisamos pegar o base64 + if "mediaUrl" in body["data"]["message"]: + audio_source = body["data"]["message"]["mediaUrl"] + logger.debug(f"Usando mediaUrl: {audio_source}") else: - base64_audio = body["data"]["message"]["base64"] + logger.debug("MediaUrl não encontrada, obtendo áudio via base64") + base64_audio = await get_audio_base64(server_url, instance, apikey, audio_key) + audio_source = await convert_base64_to_file(base64_audio) + logger.debug(f"Áudio convertido e salvo em: {audio_source}") - # Converter Base64 para arquivo de áudio - audio_file = await convert_base64_to_file(base64_audio) - - # Transcrever o áudio usando o modelo da API externa - transcription_text, is_summary = await transcribe_audio(audio_file) + # Transcrever o áudio + transcription_text, is_summary = await transcribe_audio(audio_source) header_message = ( "*Resumo do áudio:*\n\n" if is_summary else "*Transcrição desse áudio:*\n\n" ) # Formatar o conteúdo da mensagem - summary_message = f"{header_message}{transcription_text}\n\n{BUSINESS_MESSAGE}" + summary_message = f"{header_message}{transcription_text}\n\n{settings.BUSINESS_MESSAGE}" + logger.debug(f"Mensagem formatada: {summary_message[:100]}...") # Enviar o resumo transcrito de volta via WhatsApp - await send_message_to_whatsapp( server_url, instance, apikey, summary_message, - body["data"]["key"]["remoteJid"], + remote_jid, audio_key, ) + logger.info("Áudio processado e resposta enviada com sucesso") return {"message": "Áudio transcrito e resposta enviada com sucesso"} except Exception as e: + logger.error(f"Erro ao processar áudio: {str(e)}", exc_info=settings.DEBUG_MODE) raise HTTPException( - status_code=500, detail=f"Erro ao processar a requisição: {str(e)}" + status_code=500, + detail=f"Erro ao processar a requisição: {str(e)}" ) diff --git a/readme.md b/readme.md index da41e8a..bf745df 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,7 @@ Antes de começar, certifique-se de ter os seguintes requisitos: - Python 3.10+ instalado ([Download](https://www.python.org/downloads/)) - Docker e Docker Compose instalados ([Instruções](https://docs.docker.com/get-docker/)) - Uma conta Evolution API com chave válida -- Uma conta GROQ API com chave válida +- Uma conta GROQ API com chave válida (começa com 'gsk_') --- @@ -33,55 +33,82 @@ python -m venv .venv source .venv/Scripts/activate pip install -r requirements.txt ``` -Para sair do ambiente virtual, use: + +### Configuração do Arquivo .env +Copie o arquivo `.env.example` para `.env` e configure suas variáveis: ```bash -deactivate +cp .env.example .env ``` -### 🚀 Como Executar Localmente -Certifique-se de que todas as dependências foram instaladas. -Rode o comando abaixo para iniciar o servidor: + +## 📖 **Configuração Detalhada das Variáveis** + +### Variáveis Essenciais + +| Variável | Descrição | Obrigatória | Exemplo | +|-----------------------|----------------------------------------------------------|-------------|----------------------------------------------------------| +| `WHATSAPP_API_KEY` | Chave da API Evolution para autenticação | Sim | `429683C4C977415CAAFCCE10F7D57E11` | +| `WHATSAPP_API_URL` | URL base da sua instância Evolution API | Sim | `https://api.evolution.com` | +| `WHATSAPP_INSTANCE` | Nome da instância configurada na Evolution API | Sim | `instance1` | +| `GROQ_API_KEY` | Chave da API GROQ (deve começar com 'gsk_') | Sim | `gsk_abc123...` | + +### Variáveis de Personalização + +| Variável | Descrição | Padrão | Exemplo | +|-----------------------|----------------------------------------------------------|-------------|----------------------------------------------------------| +| `BUSINESS_MESSAGE` | Mensagem de rodapé após transcrição | Vazio | `substitua_sua_mensagem_de_servico_aqui` | +| `PROCESS_GROUP_MESSAGES` | Habilita processamento de mensagens em grupos | `false` | `true` ou `false` | + +### Variáveis de Debug e Log + +| Variável | Descrição | Padrão | Valores Possíveis | +|-----------------------|----------------------------------------------------------|-------------|----------------------------------------------------------| +| `DEBUG_MODE` | Ativa logs detalhados para debugging | `false` | `true` ou `false` | +| `LOG_LEVEL` | Define o nível de detalhamento dos logs | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | + +--- + +## 🚀 **Métodos de Execução** + +### Execução Local ```bash uvicorn main:app --host 0.0.0.0 --port 8005 ``` -Acesse o serviço localmente em: `http://127.0.0.1:8005/transcreve-audios`. Insira este endereço na Evolution API para consumir as transcrições. -### 🌐 Configuração de Webhook na Evolution API -Endpoint para Webhook -Use o seguinte endpoint para configurar seu webhook na Evolution API: -```bash -https://transcricaoaudio.seudominio.com.br/transcreve-audios -``` -### Testando Localmente -Se estiver rodando localmente, use o comando curl para testar: -```bash -curl --location 'http://127.0.0.1:8005/transcreve-audios' -``` -### 🐳 Instalação com Docker Swarm e Traefik -Se preferir rodar o projeto em um ambiente de produção com Docker Swarm e Traefik, use o arquivo de configuração abaixo como referência. - -docker-compose.yaml -```bash +### 🐳 Docker Compose Simples +```yaml version: "3.7" - services: transcricaoaudio: image: impacteai/transcrevezap:latest - build: . - networks: - - suarededocker #troque pela sua rede do docker ports: - 8005:8005 environment: - Uvicorn_port: 8005 - Uvicorn_host: 0.0.0.0 - Uvicorn_reload: "true" - Uvicorn_workers: 1 - WHATSAPP_API_KEY: "substitua_sua_chave_aqui" #coloque sua api key evolution aqui - WHATSAPP_API_URL: "https://suaevolutionapi.sedominio.com.br/" #coloque sua url evolution aqui - WHATSAPP_INSTANCE: "substitua_sua_instancia_aqui" #coloque nome da sua instancia evolution aqui - GROQ_API_KEY: "substitua_sua_chave_GROQ_aqui" #coloque sua chave GROQ aqui - BUSINESS_MESSAGE: "substitua_sua_mensagem_de_servico_aqui" #coloque a mensagem que será enviada ao final da transcrição aqui - PROCESS_GROUP_MESSAGES: "false" # Define se mensagens de grupos devem ser processadas + WHATSAPP_API_KEY: "sua_chave_aqui" + WHATSAPP_API_URL: "https://sua_url_aqui" + WHATSAPP_INSTANCE: "sua_instancia" + GROQ_API_KEY: "sua_chave_groq" + BUSINESS_MESSAGE: "substitua_sua_mensagem_de_servico_aqui" + PROCESS_GROUP_MESSAGES: "false" + DEBUG_MODE: "false" + LOG_LEVEL: "INFO" +``` + +### 🌟 Docker Swarm com Traefik +```yaml +version: "3.7" +services: + transcricaoaudio: + image: impacteai/transcrevezap:latest + networks: + - suarededocker + environment: + WHATSAPP_API_KEY: "sua_chave_aqui" + WHATSAPP_API_URL: "https://sua_url_aqui" + WHATSAPP_INSTANCE: "sua_instancia" + GROQ_API_KEY: "sua_chave_groq" + BUSINESS_MESSAGE: "substitua_sua_mensagem_de_servico_aqui" + PROCESS_GROUP_MESSAGES: "false" + DEBUG_MODE: "false" deploy: mode: replicated replicas: 1 @@ -90,99 +117,43 @@ services: - node.role == manager labels: - traefik.enable=true - - traefik.http.routers.transcricaoaudio.rule=Host(`transcricaoaudio.seudominio.com.br`) #coloque seu subdominio apontado aqui + - traefik.http.routers.transcricaoaudio.rule=Host(`transcricaoaudio.seudominio.com.br`) - traefik.http.routers.transcricaoaudio.entrypoints=websecure - traefik.http.routers.transcricaoaudio.tls.certresolver=letsencryptresolver - traefik.http.services.transcricaoaudio.loadbalancer.server.port=8005 - - traefik.http.services.transcricaoaudio.loadbalancer.passHostHeader=true - - traefik.http.routers.transcricaoaudio.service=transcricaoaudio - - traefik.http.middlewares.traefik-compress.compress=true - - traefik.http.routers.transcricaoaudio.middlewares=traefik-compress resources: limits: cpus: "1" memory: 1024M networks: - suarededocker: #troque pela sua rede do docker + suarededocker: external: true - name: suarededocker #troque pela sua rede do docker ``` ---- -## 🐳 **Rodando com Docker Compose (Sem Traefik)** +## 🔧 **Configuração do Traefik** -Se você prefere rodar a aplicação em um ambiente simples, sem usar o Traefik para gerenciamento de subdomínios, siga as orientações abaixo. +Para usar com Traefik, certifique-se de: +1. Ter o Traefik configurado em seu ambiente Docker Swarm +2. Configurar o DNS do seu domínio para apontar para o servidor +3. Ajustar as labels do Traefik conforme seu ambiente +4. Verificar se a rede externa existe no Docker Swarm -### **1. Usando `docker run` diretamente** +## 📝 **Notas Importantes** +- A GROQ_API_KEY deve começar com 'gsk_' +- O BUSINESS_MESSAGE suporta formatação do WhatsApp (*negrito*, _itálico_) +- Para quebras de linha no BUSINESS_MESSAGE, use \n +- Em produção, recomenda-se DEBUG_MODE=false +- Configure LOG_LEVEL=DEBUG apenas para troubleshooting -Execute o seguinte comando para rodar o contêiner: - -```bash -docker run -d \ - --name transcricaoaudio \ - -p 8005:8005 \ - -e Uvicorn_port=8005 \ - -e Uvicorn_host=0.0.0.0 \ - -e Uvicorn_reload="true" \ - -e Uvicorn_workers=1 \ - -e WHATSAPP_API_KEY="substitua_sua_chave_aqui" \ - -e WHATSAPP_API_URL="https://suaevolutionapi.sedominio.com.br/" \ - -e WHATSAPP_INSTANCE="substitua_sua_instancia_aqui" \ - -e GROQ_API_KEY="substitua_sua_chave_GROQ_aqui" \ - -e BUSINESS_MESSAGE="substitua_sua_mensagem_de_servico_aqui" \ - -e PROCESS_GROUP_MESSAGES="false" \ - impacteai/transcrevezap:latest -``` -Usando `docker-compose.yaml` -Crie um arquivo chamado `docker-compose.yaml` com o seguinte conteúdo: -```bash -version: "3.7" - -services: - transcricaoaudio: - image: impacteai/transcrevezap:latest - ports: - - 8005:8005 - environment: - Uvicorn_port: 8005 - Uvicorn_host: 0.0.0.0 - Uvicorn_reload: "true" - Uvicorn_workers: 1 - WHATSAPP_API_KEY: "substitua_sua_chave_aqui" # Coloque sua chave API Evolution aqui - WHATSAPP_API_URL: "https://suaevolutionapi.sedominio.com.br/" # URL da sua instância Evolution - WHATSAPP_INSTANCE: "substitua_sua_instancia_aqui" # Nome da sua instância Evolution - GROQ_API_KEY: "substitua_sua_chave_GROQ_aqui" # Chave da API GROQ - BUSINESS_MESSAGE: "substitua_sua_mensagem_de_servico_aqui" # Mensagem adicionada ao final da transcrição - PROCESS_GROUP_MESSAGES: "false" # Define se mensagens de grupos devem ser processadas -``` -Para rodar com Docker Compose, execute: -```bash -docker-compose up -d -``` - - Acessando o serviço - - Após rodar a aplicação, acesse: - http://127.0.0.1:8005/transcreve-audios para ambiente local, inserindo este endereço na Evolution API. - Você pode substituir 127.0.0.1 pelo IP ou domínio público, se configurado. ---- -## 📖 **Configuração das Variáveis de Ambiente** -Ao usar o Docker Compose, configure as seguintes variáveis de ambiente no arquivo `docker-compose.yaml`: - -| Variável | Descrição | -|------------------------|--------------------------------------------------------------------------------------------| -| `WHATSAPP_API_KEY` | Chave da API Evolution para integração com o WhatsApp. | -| `WHATSAPP_API_URL` | URL da sua instância da Evolution API. | -| `WHATSAPP_INSTANCE` | Nome da instância configurada na Evolution API. | -| `GROQ_API_KEY` | Chave da API GROQ para realizar transcrições e resumos de áudios. | -| `BUSINESS_MESSAGE` | Mensagem de divulgação que será adicionada ao final das transcrições. | -| `PROCESS_GROUP_MESSAGES` | Define se mensagens enviadas em grupos devem ser processadas (`true`) ou ignoradas (`false`). | - ---- +## 🔍 **Troubleshooting** +Se encontrar problemas: +1. Verifique se todas as variáveis obrigatórias estão configuradas +2. Ative DEBUG_MODE=true temporariamente +3. Verifique os logs do container +4. Certifique-se que as APIs estão acessíveis ## 📄 **Licença** - -Este projeto está licenciado sob a Licença MIT. Isso significa que você pode usar, modificar e distribuir este software livremente, desde que mantenha o aviso de copyright e a licença original em todas as cópias ou partes substanciais do software. - -Você pode consultar o texto completo da licença no arquivo [LICENSE](LICENSE). +Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes. --- diff --git a/requirements.txt b/requirements.txt index 262a8a3..ceb0554 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,18 +8,19 @@ attrs==24.2.0 certifi==2024.8.30 charset-normalizer==2.1.1 click==8.1.7 -fastapi==0.78.0 +fastapi==0.109.2 frozenlist==1.4.1 h11==0.14.0 idna==3.10 multidict==6.1.0 pip-system-certs==4.0 propcache==0.2.0 -pydantic==1.10.18 -python-dotenv==0.20.0 +pydantic==2.10.3 +pydantic-settings==2.6.1 +python-dotenv==1.0.1 requests==2.28.1 sniffio==1.3.1 -starlette==0.19.1 +starlette>=0.36.3,<0.37.0 typing_extensions==4.12.2 urllib3==1.26.20 uvicorn==0.17.6 diff --git a/services.py b/services.py index 481f138..800034e 100644 --- a/services.py +++ b/services.py @@ -1,154 +1,200 @@ import aiohttp import base64 import aiofiles -from dotenv import load_dotenv -import os from fastapi import HTTPException +from config import settings, logger -# Carregar variáveis do .env -load_dotenv() - -# Função para obter as variáveis -def get_env_var(var_name, default=None): - return os.getenv(var_name, default) - -# Função assíncrona para converter base64 em arquivo temporário async def convert_base64_to_file(base64_data): - audio_data = base64.b64decode(base64_data) - audio_file_path = "/tmp/audio_file.mp3" + """Converte dados base64 em arquivo temporário""" + try: + logger.debug("Iniciando conversão de base64 para arquivo") + audio_data = base64.b64decode(base64_data) + audio_file_path = "/tmp/audio_file.mp3" - async with aiofiles.open(audio_file_path, "wb") as f: - await f.write(audio_data) + async with aiofiles.open(audio_file_path, "wb") as f: + await f.write(audio_data) + + logger.debug(f"Arquivo temporário criado em: {audio_file_path}") + return audio_file_path + except Exception as e: + logger.error(f"Erro na conversão base64: {str(e)}", exc_info=settings.DEBUG_MODE) + raise - return audio_file_path - - -# Função assíncrona para resumir o texto se necessário async def summarize_text_if_needed(text): - + """Resumir texto usando a API GROQ""" + logger.debug("Iniciando processo de resumo do texto") + url_completions = "https://api.groq.com/openai/v1/chat/completions" headers = { - "Authorization": f"Bearer {get_env_var('GROQ_API_KEY')}", + "Authorization": f"Bearer {settings.GROQ_API_KEY}", "Content-Type": "application/json", } json_data = { - "messages": [ - { - "role": "user", - "content": f""" - Entenda o contexto desse áudio e faça um resumo super enxuto sobre o que se trata. - Esse áudio foi enviado pelo whatsapp, de alguém, para Gabriel. - Escreva APENAS o resumo do áudio como se fosse você que estivesse enviando - essa mensagem! Não comprimente, não de oi, não escreva nada antes nem depois - do resumo, responda apenas um resumo enxuto do que foi falado no áudio. - IMPORTANTE: Não faça esse resumo como se fosse um áudio que uma terceira - pessoa enviou, não diga coisas como 'a pessoa está falando...' etc. - Escreva o resumo com base nessa mensagem do áudio, - como se você estivesse escrevendo esse resumo e enviando em - texto pelo whatsapp: {text}""", - } - ], + "messages": [{ + "role": "user", + "content": f""" + Entenda o contexto desse áudio e faça um resumo super enxuto sobre o que se trata. + Esse áudio foi enviado pelo whatsapp, de alguém, para Gabriel. + Escreva APENAS o resumo do áudio como se fosse você que estivesse enviando + essa mensagem! Não comprimente, não de oi, não escreva nada antes nem depois + do resumo, responda apenas um resumo enxuto do que foi falado no áudio. + IMPORTANTE: Não faça esse resumo como se fosse um áudio que uma terceira + pessoa enviou, não diga coisas como 'a pessoa está falando...' etc. + Escreva o resumo com base nessa mensagem do áudio, + como se você estivesse escrevendo esse resumo e enviando em + texto pelo whatsapp: {text}""", + }], "model": "llama-3.2-90b-vision-preview", } - async with aiohttp.ClientSession() as session: - async with session.post( - url_completions, - headers=headers, - json=json_data, - ) as summary_response: - if summary_response.status == 200: - summary_result = await summary_response.json() - summary_text = summary_result["choices"][0]["message"]["content"] - return summary_text - else: - raise Exception("Erro ao resumir o texto") + try: + async with aiohttp.ClientSession() as session: + logger.debug("Enviando requisição para API GROQ") + async with session.post(url_completions, headers=headers, json=json_data) as summary_response: + if summary_response.status == 200: + summary_result = await summary_response.json() + summary_text = summary_result["choices"][0]["message"]["content"] + logger.info("Resumo gerado com sucesso") + logger.debug(f"Resumo: {summary_text[:100]}...") + return summary_text + else: + error_text = await summary_response.text() + logger.error(f"Erro na API GROQ: {error_text}") + raise Exception(f"Erro ao resumir o texto: {error_text}") + except Exception as e: + logger.error(f"Erro no processo de resumo: {str(e)}", exc_info=settings.DEBUG_MODE) + raise - -# Função assíncrona para transcrever o áudio -async def transcribe_audio(audio_file): +async def transcribe_audio(audio_source, apikey=None): + """Transcreve áudio usando a API GROQ""" + logger.info("Iniciando processo de transcrição") url = "https://api.groq.com/openai/v1/audio/transcriptions" - headers = {"Authorization": f"Bearer {get_env_var('GROQ_API_KEY')}"} + groq_headers = {"Authorization": f"Bearer {settings.GROQ_API_KEY}"} - async with aiohttp.ClientSession() as session: - async with session.post( - url, - headers=headers, - data={ - "file": open(audio_file, "rb"), - "model": "whisper-large-v3", - "language": "pt", - }, - ) as response: + try: + async with aiohttp.ClientSession() as session: + # Se o audio_source for uma URL + if isinstance(audio_source, str) and audio_source.startswith('http'): + logger.debug(f"Baixando áudio da URL: {audio_source}") + download_headers = {"apikey": apikey} if apikey else {} + + async with session.get(audio_source, headers=download_headers) as response: + if response.status != 200: + error_text = await response.text() + logger.error(f"Erro no download do áudio: Status {response.status}, Resposta: {error_text}") + raise Exception(f"Erro ao baixar áudio: {error_text}") + + audio_data = await response.read() + temp_file = "/tmp/audio_from_url.mp3" + async with aiofiles.open(temp_file, "wb") as f: + await f.write(audio_data) + audio_source = temp_file + logger.debug(f"Áudio salvo temporariamente em: {temp_file}") - is_summary = False - if response.status == 200: - result = await response.json() - message = result.get("text", "") + # Preparar dados para transcrição + data = aiohttp.FormData() + data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3') + data.add_field('model', 'whisper-large-v3') + data.add_field('language', 'pt') - if len(message) > 1000: - is_summary = True - message = await summarize_text_if_needed(message) + logger.debug("Enviando áudio para transcrição") + async with session.post(url, headers=groq_headers, data=data) as response: + if response.status == 200: + result = await response.json() + message = result.get("text", "") + logger.info("Transcrição concluída com sucesso") + logger.debug(f"Texto transcrito: {message[:100]}...") - return message, is_summary - else: - raise Exception("Erro ao transcrever o áudio") + is_summary = False + if len(message) > 1000: + logger.debug("Texto longo detectado, iniciando resumo") + is_summary = True + message = await summarize_text_if_needed(message) + return message, is_summary + else: + error_text = await response.text() + logger.error(f"Erro na transcrição: {error_text}") + raise Exception(f"Erro na transcrição: {error_text}") -def get_body_message_to_whatsapp_v2(message, remote_jid, message_id): - return { - "number": remote_jid, - "text": message, - "quoted": {"key": {"remoteJid": remote_jid, "fromMe": False, "id": message_id}}, - } + except Exception as e: + logger.error(f"Erro no processo de transcrição: {str(e)}", exc_info=settings.DEBUG_MODE) + raise +async def send_message_to_whatsapp(server_url, instance, apikey, message, remote_jid, message_id): + """Envia mensagem via WhatsApp""" + logger.debug(f"Preparando envio de mensagem para: {remote_jid}") + url = f"{server_url}/message/sendText/{instance}" + headers = {"apikey": apikey} + + try: + # Tentar enviar na V1 + body = get_body_message_to_whatsapp_v1(message, remote_jid) + logger.debug("Tentando envio no formato V1") + result = await call_whatsapp(url, body, headers) + + # Se falhar, tenta V2 + if not result: + logger.debug("Formato V1 falhou, tentando formato V2") + body = get_body_message_to_whatsapp_v2(message, remote_jid, message_id) + await call_whatsapp(url, body, headers) + + logger.info("Mensagem enviada com sucesso") + except Exception as e: + logger.error(f"Erro no envio da mensagem: {str(e)}", exc_info=settings.DEBUG_MODE) + raise def get_body_message_to_whatsapp_v1(message, remote_jid): + """Formata mensagem no formato V1""" return { "number": remote_jid, "options": {"delay": 1200, "presence": "composing", "linkPreview": False}, "textMessage": {"text": message}, } +def get_body_message_to_whatsapp_v2(message, remote_jid, message_id): + """Formata mensagem no formato V2""" + return { + "number": remote_jid, + "text": message, + "quoted": {"key": {"remoteJid": remote_jid, "fromMe": False, "id": message_id}}, + } async def call_whatsapp(url, body, headers): - async with aiohttp.ClientSession() as session: - async with session.post(url, json=body, headers=headers) as response: - if response.status not in [200, 201]: - print(f"Erro ao enviar mensagem via WhatsApp: {await response.text()}") - return False + """Realiza chamada à API do WhatsApp""" + try: + async with aiohttp.ClientSession() as session: + logger.debug(f"Enviando requisição para: {url}") + async with session.post(url, json=body, headers=headers) as response: + if response.status not in [200, 201]: + error_text = await response.text() + logger.error(f"Erro na API do WhatsApp: Status {response.status}, Resposta: {error_text}") + return False + logger.debug("Requisição bem-sucedida") + return True + except Exception as e: + logger.error(f"Erro na chamada WhatsApp: {str(e)}", exc_info=settings.DEBUG_MODE) + return False - -# Função assíncrona para enviar a mensagem de resumo via WhatsApp -async def send_message_to_whatsapp( - server_url, instance, apikey, message, remote_jid, message_id -): - url = f"{server_url}/message/sendText/{instance}" - headers = {"apikey": apikey} - - # Tentar enviar na V1 - body = get_body_message_to_whatsapp_v1(message, remote_jid) - result = await call_whatsapp(url, body, headers) - - # Se falhar, monta novo body na V2 e reenvia - if not result: - body = get_body_message_to_whatsapp_v2(message, remote_jid, message_id) - await call_whatsapp(url, body, headers) - - -# Função para obter o áudio em Base64 via API do WhatsApp async def get_audio_base64(server_url, instance, apikey, message_id): + """Obtém áudio em Base64 via API do WhatsApp""" + logger.debug(f"Obtendo áudio base64 para mensagem: {message_id}") url = f"{server_url}/chat/getBase64FromMediaMessage/{instance}" headers = {"apikey": apikey} body = {"message": {"key": {"id": message_id}}, "convertToMp4": False} - async with aiohttp.ClientSession() as session: - async with session.post(url, json=body, headers=headers) as response: - if response.status in [200, 201]: - result = await response.json() - return result.get("base64", "") - else: - raise HTTPException( - status_code=500, detail="Falha ao obter áudio em base64" - ) + try: + async with aiohttp.ClientSession() as session: + async with session.post(url, json=body, headers=headers) as response: + if response.status in [200, 201]: + result = await response.json() + logger.info("Áudio base64 obtido com sucesso") + return result.get("base64", "") + else: + error_text = await response.text() + logger.error(f"Erro ao obter áudio base64: {error_text}") + raise HTTPException(status_code=500, detail="Falha ao obter áudio em base64") + except Exception as e: + logger.error(f"Erro na obtenção do áudio base64: {str(e)}", exc_info=settings.DEBUG_MODE) + raise