Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c42476549e | ||
![]() |
e89787e715 | ||
![]() |
f63dcc40d1 | ||
![]() |
af20510b2b | ||
![]() |
a25dc9c4e7 | ||
![]() |
a4ba9d02bc | ||
![]() |
4c7d346a3c | ||
![]() |
be82707ccc | ||
![]() |
3cd75903fc | ||
![]() |
d43f62c316 | ||
![]() |
0745132b98 | ||
![]() |
0b5ad96508 | ||
![]() |
22217802a2 | ||
![]() |
facfcb4559 | ||
![]() |
7d8c91fbc9 | ||
![]() |
6fd1ec33e9 | ||
![]() |
bac9469b05 | ||
![]() |
943d5be2c8 | ||
![]() |
ae74686c9b | ||
![]() |
2fadd723dc | ||
![]() |
2a296d759f | ||
![]() |
f558542359 | ||
![]() |
6a9ba1f087 | ||
![]() |
b86c7ac764 | ||
![]() |
9a072aee22 | ||
![]() |
eeffecb091 | ||
![]() |
69cb3b1965 | ||
![]() |
ec65839beb | ||
![]() |
abc4c4298a | ||
![]() |
161251a403 | ||
![]() |
97cc842eb8 | ||
![]() |
ffd916c855 | ||
![]() |
fede0057e5 | ||
![]() |
345cc26186 | ||
![]() |
a646e724f6 | ||
![]() |
c88c014e86 | ||
![]() |
c72c143609 | ||
![]() |
64f7f64b17 | ||
![]() |
8e775b7379 | ||
![]() |
374169e56f | ||
![]() |
d6bbb5bc6e | ||
![]() |
d0c3ffca09 | ||
![]() |
1f40b128fa | ||
![]() |
b3753e768c | ||
![]() |
b6e3ea8ec3 | ||
![]() |
bb63c590e4 | ||
![]() |
f7d6cae928 | ||
![]() |
0664a64334 | ||
![]() |
4f9c9c7c47 | ||
![]() |
1e4dbe6a14 | ||
![]() |
0a943c3d2d | ||
![]() |
61556bcf33 | ||
![]() |
dcf1e5ebce | ||
![]() |
57b4e5b731 | ||
![]() |
74f01bfb1d | ||
![]() |
a299805fee |
@ -2,4 +2,14 @@
|
||||
docker-composer.yaml
|
||||
docker-compose.yaml
|
||||
.gitignore
|
||||
.git
|
||||
.git
|
||||
__pycache__
|
||||
*.pyc
|
||||
.git
|
||||
.env
|
||||
.venv
|
||||
*.md
|
||||
*.postman_collection.json
|
||||
deploy_*.sh
|
||||
manager_atualizar.py
|
||||
roadmap.md
|
62
.env.example
62
.env.example
@ -1,14 +1,54 @@
|
||||
# Chave da API para transcrição (Groq ou qualquer outro serviço que você utilizar)
|
||||
GROQ_API_KEY=substitua_sua_chave_GROQ_aqui
|
||||
#-----------------------------------------------
|
||||
# Configurações do Servidor
|
||||
#-----------------------------------------------
|
||||
# Configurações do UVICORN
|
||||
UVICORN_PORT=8005
|
||||
UVICORN_HOST=0.0.0.0
|
||||
UVICORN_RELOAD=true
|
||||
UVICORN_WORKERS=1
|
||||
|
||||
# Comportamento da transcrição
|
||||
PROCESS_SELF_MESSAGES=true
|
||||
BUSINESS_MESSAGE="substitua_sua_mensagem_de_servico_aqui"
|
||||
PROCESS_GROUP_MESSAGES=false
|
||||
# Domínios da Aplicação
|
||||
API_DOMAIN=seu.dominio.com # Subdomínio para a API (ex: api.seudominio.com)
|
||||
MANAGER_DOMAIN=manager.seu.dominio.com # Subdomínio para o Manager (ex: manager.seudominio.com)
|
||||
|
||||
# Host e porta do Redis (caso esteja utilizando)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
# Debug e Logs
|
||||
DEBUG_MODE=false
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
DEBUG_MODE=true
|
||||
LOG_LEVEL=INFO
|
||||
#-----------------------------------------------
|
||||
# Credenciais de Acesso
|
||||
#-----------------------------------------------
|
||||
# Credenciais do Painel Administrativo
|
||||
MANAGER_USER=seu_usuario_admin # Username para acessar o painel admin
|
||||
MANAGER_PASSWORD=sua_senha_segura # Senha para acessar o painel admin
|
||||
|
||||
#-----------------------------------------------
|
||||
# Configurações do Redis
|
||||
#-----------------------------------------------
|
||||
# Configurações Básicas
|
||||
REDIS_HOST=redis-transcrevezap # Host do Redis (use redis-transcrevezap para docker-compose)
|
||||
REDIS_PORT=6380 # Porta do Redis
|
||||
REDIS_DB=0 # Número do banco de dados Redis
|
||||
|
||||
# Autenticação Redis (opcional)
|
||||
REDIS_USERNAME= # Deixe em branco se não usar autenticação
|
||||
REDIS_PASSWORD= # Deixe em branco se não usar autenticação
|
||||
|
||||
#-----------------------------------------------
|
||||
# Configurações de Rede
|
||||
#-----------------------------------------------
|
||||
# Nome da Rede Docker Externa
|
||||
NETWORK_NAME=sua_rede_externa # Nome da sua rede Docker externa
|
||||
|
||||
#-----------------------------------------------
|
||||
# Configurações do Traefik (se estiver usando)
|
||||
#-----------------------------------------------
|
||||
# Certificados SSL
|
||||
SSL_RESOLVER=letsencryptresolver # Resolvedor SSL do Traefik
|
||||
SSL_ENTRYPOINT=websecure # Entrypoint SSL do Traefik
|
||||
|
||||
#-----------------------------------------------
|
||||
# Portas da Aplicação
|
||||
#-----------------------------------------------
|
||||
API_PORT=8005 # Porta para a API FastAPI
|
||||
MANAGER_PORT=8501 # Porta para o Streamlit Manager
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -2,4 +2,9 @@
|
||||
*.pyc
|
||||
docker-composer.yaml
|
||||
GPT.postman_collection.json
|
||||
.venv/
|
||||
.venv/
|
||||
.gitignore
|
||||
deploy_producao.sh
|
||||
Dockerfile
|
||||
manager_atualizar.py
|
||||
roadmap.md
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.pythonPath": "d:\\Estudando CODE\\ESTUDOS PYTHON\\transcreve-audio-exemplo\\.venv\\Scripts\\python.exe"
|
||||
}
|
45
Dockerfile
45
Dockerfile
@ -1,19 +1,46 @@
|
||||
# Usar uma imagem oficial do Python como base
|
||||
# Imagem base do Python 3.10-slim
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Definir o diretório de trabalho
|
||||
# Configuração básica de timezone
|
||||
ENV TZ=America/Sao_Paulo
|
||||
|
||||
# Instalação de dependências mínimas necessárias
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
redis-tools \
|
||||
tzdata \
|
||||
dos2unix \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||
&& echo $TZ > /etc/timezone
|
||||
|
||||
# Configuração do ambiente de trabalho
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar o requirements.txt e instalar dependências
|
||||
# Instalação das dependências Python
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copiar todo o código para dentro do contêiner
|
||||
# Copia dos arquivos da aplicação
|
||||
COPY . .
|
||||
|
||||
# Expor a porta onde o FastAPI vai rodar
|
||||
EXPOSE 8005
|
||||
# Preparação do diretório de estáticos
|
||||
RUN mkdir -p /app/static && \
|
||||
if [ -d "static" ]; then cp -r static/* /app/static/ 2>/dev/null || true; fi
|
||||
|
||||
# Comando para iniciar a aplicação
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8005"]
|
||||
# Configuração do script de inicialização
|
||||
RUN chmod +x start.sh && \
|
||||
dos2unix start.sh && \
|
||||
apt-get purge -y dos2unix && \
|
||||
apt-get autoremove -y
|
||||
|
||||
# Portas da aplicação
|
||||
EXPOSE 8005 8501
|
||||
|
||||
# Valores padrão para Redis
|
||||
ENV REDIS_HOST=redis-transcrevezap \
|
||||
REDIS_PORT=6380 \
|
||||
REDIS_DB=0
|
||||
|
||||
# Comando de inicialização
|
||||
CMD ["/bin/bash", "/app/start.sh"]
|
147
config.py
147
config.py
@ -1,102 +1,111 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import redis
|
||||
import os
|
||||
from utils import create_redis_client
|
||||
|
||||
# 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
|
||||
}
|
||||
"""Formatter personalizado que adiciona cores aos logs."""
|
||||
COLORS = {
|
||||
logging.DEBUG: "\x1b[38;5;39m", # Azul
|
||||
logging.INFO: "\x1b[38;21m", # Cinza
|
||||
logging.WARNING: "\x1b[38;5;226m", # Amarelo
|
||||
logging.ERROR: "\x1b[38;5;196m", # Vermelho
|
||||
logging.CRITICAL: "\x1b[31;1m", # Vermelho forte
|
||||
}
|
||||
RESET = "\x1b[0m"
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
color = self.COLORS.get(record.levelno, self.RESET)
|
||||
log_fmt = f"{color}%(asctime)s - %(name)s - %(levelname)s - %(message)s{self.RESET}"
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
# Configuração inicial do logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("TranscreveZAP")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(ColoredFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||||
handler.setFormatter(ColoredFormatter())
|
||||
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.")
|
||||
# Nível de log inicial (pode ser ajustado após o carregamento de configurações)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Conexão com o Redis
|
||||
redis_client = create_redis_client()
|
||||
|
||||
class Settings:
|
||||
"""Classe para gerenciar configurações do sistema."""
|
||||
def __init__(self):
|
||||
logger.debug("Iniciando carregamento das configurações...")
|
||||
"""Inicializa as configurações."""
|
||||
logger.debug("Carregando configurações do Redis...")
|
||||
|
||||
self.ACTIVE_LLM_PROVIDER = self.get_redis_value("ACTIVE_LLM_PROVIDER", "groq")
|
||||
self.OPENAI_API_KEY = self.get_redis_value("OPENAI_API_KEY", "")
|
||||
self.GROQ_API_KEY = self.get_redis_value("GROQ_API_KEY", "gsk_default_key")
|
||||
self.BUSINESS_MESSAGE = self.get_redis_value("BUSINESS_MESSAGE", "*Impacte AI* Premium Services")
|
||||
self.PROCESS_GROUP_MESSAGES = self.get_redis_value("PROCESS_GROUP_MESSAGES", "false").lower() == "true"
|
||||
self.PROCESS_SELF_MESSAGES = self.get_redis_value("PROCESS_SELF_MESSAGES", "true").lower() == "true"
|
||||
self.DEBUG_MODE = os.getenv("DEBUG_MODE", "false").lower() == "true"
|
||||
self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
self.TRANSCRIPTION_LANGUAGE = self.get_redis_value("TRANSCRIPTION_LANGUAGE", "pt")
|
||||
|
||||
# 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.GROQ_API_KEY = os.getenv('GROQ_API_KEY')
|
||||
# Mascarar chave ao logar
|
||||
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.PROCESS_SELF_MESSAGES = os.getenv('PROCESS_SELF_MESSAGES', 'false').lower() == 'true'
|
||||
logger.debug(f"PROCESS_SELF_MESSAGES configurado como: {self.PROCESS_SELF_MESSAGES}")
|
||||
|
||||
self.LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
|
||||
logger.debug(f"LOG_LEVEL configurado como: {self.LOG_LEVEL}")
|
||||
logger.debug(
|
||||
f"Configurações carregadas: LOG_LEVEL={self.LOG_LEVEL}, "
|
||||
f"PROCESS_GROUP_MESSAGES={self.PROCESS_GROUP_MESSAGES}, "
|
||||
f"PROCESS_SELF_MESSAGES={self.PROCESS_SELF_MESSAGES}"
|
||||
)
|
||||
|
||||
def get_redis_value(self, key, default):
|
||||
"""Obtém um valor do Redis com fallback para o valor padrão."""
|
||||
value = redis_client.get(key)
|
||||
if value is None:
|
||||
logger.warning(f"Configuração '{key}' não encontrada no Redis. Usando padrão: {default}")
|
||||
return default
|
||||
return value
|
||||
|
||||
def set_redis_value(self, key, value):
|
||||
"""Define um valor no Redis."""
|
||||
redis_client.set(key, value)
|
||||
logger.debug(f"Configuração '{key}' atualizada no Redis")
|
||||
|
||||
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_'")
|
||||
"""Validação detalhada das configurações críticas."""
|
||||
logger.debug("Validando configurações...")
|
||||
errors = []
|
||||
|
||||
if validation_errors:
|
||||
for error in validation_errors:
|
||||
logger.error(f"Erro de validação: {error}")
|
||||
if not self.GROQ_API_KEY:
|
||||
errors.append("GROQ_API_KEY não está definida.")
|
||||
elif not self.GROQ_API_KEY.startswith("gsk_"):
|
||||
errors.append("GROQ_API_KEY inválida: deve começar com 'gsk_'.")
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
logger.error(error)
|
||||
return False
|
||||
|
||||
|
||||
logger.info("Todas as configurações foram validadas com sucesso!")
|
||||
return True
|
||||
|
||||
# Criar instância das configurações
|
||||
# Instância única de 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!")
|
||||
logger.critical("Configurações inválidas detectadas durante a inicialização.")
|
||||
settings = None # Evita que seja referenciado como 'NoneType'
|
||||
|
||||
# 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)}")
|
||||
def load_settings():
|
||||
"""
|
||||
Recarrega as configurações do Redis.
|
||||
"""
|
||||
global settings
|
||||
settings = Settings()
|
||||
# Ajustar nível de log
|
||||
log_level = getattr(logging, settings.LOG_LEVEL, logging.INFO)
|
||||
logger.setLevel(log_level)
|
||||
logger.info(f"Nível de log ajustado para: {logging.getLevelName(log_level)}")
|
7
data/config.json
Normal file
7
data/config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"GROQ_API_KEY": "default_key",
|
||||
"BUSINESS_MESSAGE": "*Impacte AI* Premium Services",
|
||||
"PROCESS_GROUP_MESSAGES": false,
|
||||
"PROCESS_SELF_MESSAGES": true,
|
||||
"DEBUG_MODE": false
|
||||
}
|
@ -1,24 +1,31 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
transcricaoaudio:
|
||||
image: impacteai/transcrevezap:latest
|
||||
build: .
|
||||
tcaudio:
|
||||
image: impacteai/transcrevezap:dev
|
||||
networks:
|
||||
- suarededocker #troque pela sua rede do docker
|
||||
- sua_rede_externa # Substitua pelo nome da sua rede externa
|
||||
ports:
|
||||
- 8005:8005
|
||||
- 8005:8005 # Porta para FastAPI
|
||||
- 8501:8501 # Porta para Streamlit
|
||||
environment:
|
||||
Uvicorn_port: 8005
|
||||
Uvicorn_host: 0.0.0.0
|
||||
Uvicorn_reload: "true"
|
||||
Uvicorn_workers: 1
|
||||
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
|
||||
PROCESS_SELF_MESSAGES: "true" # Define se sua próprias mensagens devem ser processadas
|
||||
DEBUG_MODE: "false"
|
||||
LOG_LEVEL: "INFO"
|
||||
- UVICORN_PORT=8005
|
||||
- UVICORN_HOST=0.0.0.0
|
||||
- UVICORN_RELOAD=true
|
||||
- UVICORN_WORKERS=1
|
||||
- API_DOMAIN=seu.dominio.com #coloque seu subdominio da API apontado aqui
|
||||
- DEBUG_MODE=false
|
||||
- LOG_LEVEL=INFO
|
||||
- MANAGER_USER=seu_usuario_admin # Defina Usuário do Manager
|
||||
- MANAGER_PASSWORD=sua_senha_segura # Defina Senha do Manager
|
||||
- REDIS_HOST=redis-transcrevezap
|
||||
- REDIS_PORT=6380 # Porta personalizada para o Redis do TranscreveZAP
|
||||
- REDIS_DB=0 # Opcional: pode ser removida para usar o valor padrão
|
||||
# Autenticação Redis (opcional - descomente se necessário, se estiver usando autenticação)
|
||||
# - REDIS_USERNAME=${REDIS_USERNAME:-} # Nome do usuário definido no comando do Redis
|
||||
# - REDIS_PASSWORD=${REDIS_PASSWORD:-} # Senha definida no comando do Redis (sem o '>')
|
||||
depends_on:
|
||||
- redis-transcrevezap
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
@ -27,20 +34,54 @@ 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.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.routers.tcaudio.rule=Host(`seu.dominio.com`) #coloque seu subdominio da API apontado aqui
|
||||
- traefik.http.routers.tcaudio.entrypoints=websecure
|
||||
- traefik.http.routers.tcaudio.tls.certresolver=letsencryptresolver
|
||||
- traefik.http.services.tcaudio.loadbalancer.server.port=8005
|
||||
- traefik.http.services.tcaudio.loadbalancer.passHostHeader=true
|
||||
- traefik.http.routers.tcaudio.service=tcaudio
|
||||
- traefik.http.middlewares.traefik-compress.compress=true
|
||||
- traefik.http.routers.transcricaoaudio.middlewares=traefik-compress
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1"
|
||||
memory: 1024M
|
||||
- traefik.http.routers.tcaudio.middlewares=traefik-compress
|
||||
# Configuração do Streamlit
|
||||
- traefik.http.routers.tcaudio-manager.rule=Host(`manager.seu.dominio.com`) #coloque seu subdominio do Manager apontado aqui
|
||||
- traefik.http.routers.tcaudio-manager.entrypoints=websecure
|
||||
- traefik.http.routers.tcaudio-manager.tls.certresolver=letsencryptresolver
|
||||
- traefik.http.services.tcaudio-manager.loadbalancer.server.port=8501
|
||||
- traefik.http.routers.tcaudio-manager.service=tcaudio-manager
|
||||
command: ./start.sh
|
||||
|
||||
redis-transcrevezap:
|
||||
image: redis:6
|
||||
# 1. Configuração SEM autenticação (padrão):
|
||||
command: redis-server --port 6380 --appendonly yes
|
||||
# 2. Configuração COM autenticação (descomente e ajuste se necessário):
|
||||
# command: >
|
||||
# redis-server
|
||||
# --port 6380
|
||||
# --appendonly yes
|
||||
# --user seuusuario on '>minhasenha' '~*' '+@all'
|
||||
# # Explicação dos parâmetros:
|
||||
# # --user seuusuario: nome do usuário
|
||||
# # on: indica início da configuração do usuário
|
||||
# # '>minhasenha': senha do usuário (mantenha o '>')
|
||||
# # '~*': permite acesso a todas as chaves
|
||||
# # '+@all': concede todas as permissões
|
||||
volumes:
|
||||
- redis_transcrevezap_data:/data
|
||||
networks:
|
||||
- sua_rede_externa # Substitua pelo nome da sua rede externa
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
|
||||
networks:
|
||||
suarededocker: #troque pela sua rede do docker
|
||||
sua_rede_externa: # Substitua pelo nome da sua rede externa
|
||||
external: true
|
||||
name: suarededocker #troque pela sua rede do docker
|
||||
name: sua_rede_externa # Substitua pelo nome da sua rede externa
|
||||
|
||||
volumes:
|
||||
redis_transcrevezap_data:
|
||||
driver: local
|
BIN
fluxo.png
BIN
fluxo.png
Binary file not shown.
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 55 KiB |
111
groq_handler.py
Normal file
111
groq_handler.py
Normal file
@ -0,0 +1,111 @@
|
||||
import aiohttp
|
||||
import json
|
||||
from typing import Optional, Tuple, Any
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from storage import StorageHandler
|
||||
import asyncio
|
||||
|
||||
logger = logging.getLogger("GROQHandler")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
async def test_groq_key(key: str) -> bool:
|
||||
"""Teste se uma chave GROQ é válida e está funcionando."""
|
||||
url = "https://api.groq.com/openai/v1/models"
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
return bool(data.get("data"))
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao testar chave GROQ: {e}")
|
||||
return False
|
||||
|
||||
async def validate_transcription_response(response_text: str) -> bool:
|
||||
"""Valide se a resposta da transcrição é significativa."""
|
||||
try:
|
||||
cleaned_text = response_text.strip()
|
||||
return len(cleaned_text) >= 10
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao validar resposta da transcrição: {e}")
|
||||
return False
|
||||
|
||||
async def get_working_groq_key(storage: StorageHandler) -> Optional[str]:
|
||||
"""Obtenha uma chave GROQ funcional do pool disponível."""
|
||||
keys = storage.get_groq_keys()
|
||||
|
||||
for _ in range(len(keys)):
|
||||
key = storage.get_next_groq_key()
|
||||
if not key:
|
||||
continue
|
||||
|
||||
penalized_until = storage.get_penalized_until(key)
|
||||
if penalized_until and penalized_until > datetime.utcnow():
|
||||
continue
|
||||
|
||||
if await test_groq_key(key):
|
||||
return key
|
||||
else:
|
||||
storage.penalize_key(key, penalty_duration=300)
|
||||
|
||||
storage.add_log("ERROR", "Nenhuma chave GROQ funcional disponível.")
|
||||
return None
|
||||
|
||||
async def handle_groq_request(
|
||||
url: str,
|
||||
headers: dict,
|
||||
data: Any,
|
||||
storage: StorageHandler,
|
||||
is_form_data: bool = False
|
||||
) -> Tuple[bool, dict, str]:
|
||||
"""Lida com requisições para a API GROQ com suporte a retries e rotação de chaves."""
|
||||
max_retries = len(storage.get_groq_keys())
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
storage.add_log("DEBUG", "Iniciando tentativa de requisição para GROQ", {
|
||||
"url": url,
|
||||
"is_form_data": is_form_data,
|
||||
"attempt": attempt + 1
|
||||
})
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if is_form_data:
|
||||
async with session.post(url, headers=headers, data=data) as response:
|
||||
response_data = await response.json()
|
||||
if response.status == 200 and response_data.get("text"):
|
||||
return True, response_data, ""
|
||||
else:
|
||||
async with session.post(url, headers=headers, json=data) as response:
|
||||
response_data = await response.json()
|
||||
if response.status == 200 and response_data.get("choices"):
|
||||
return True, response_data, ""
|
||||
|
||||
error_msg = response_data.get("error", {}).get("message", "")
|
||||
|
||||
if "organization_restricted" in error_msg or "invalid_api_key" in error_msg:
|
||||
new_key = await get_working_groq_key(storage)
|
||||
if new_key:
|
||||
headers["Authorization"] = f"Bearer {new_key}"
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
return False, response_data, error_msg
|
||||
|
||||
except Exception as e:
|
||||
storage.add_log("ERROR", "Erro na requisição", {"error": str(e)})
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
return False, {}, f"Request failed: {str(e)}"
|
||||
|
||||
storage.add_log("ERROR", "Todas as chaves GROQ falharam.")
|
||||
return False, {}, "All GROQ keys exhausted."
|
261
main.py
261
main.py
@ -5,21 +5,101 @@ from services import (
|
||||
send_message_to_whatsapp,
|
||||
get_audio_base64,
|
||||
summarize_text_if_needed,
|
||||
download_remote_audio,
|
||||
)
|
||||
from models import WebhookRequest
|
||||
from config import logger, settings, redis_client
|
||||
from storage import StorageHandler
|
||||
import traceback
|
||||
import os
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from config import settings, logger
|
||||
|
||||
app = FastAPI()
|
||||
storage = StorageHandler()
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
api_domain = os.getenv("API_DOMAIN", "seu.dominio.com")
|
||||
redis_client.set("API_DOMAIN", api_domain)
|
||||
# Função para buscar configurações do Redis com fallback para valores padrão
|
||||
def get_config(key, default=None):
|
||||
try:
|
||||
value = redis_client.get(key)
|
||||
if value is None:
|
||||
logger.warning(f"Configuração '{key}' não encontrada no Redis. Usando padrão: {default}")
|
||||
return default
|
||||
return value
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao acessar Redis: {e}")
|
||||
return default
|
||||
|
||||
# Carregando configurações dinâmicas do Redis
|
||||
def load_dynamic_settings():
|
||||
return {
|
||||
"GROQ_API_KEY": get_config("GROQ_API_KEY", "default_key"),
|
||||
"BUSINESS_MESSAGE": get_config("BUSINESS_MESSAGE", "*Impacte AI* Premium Services"),
|
||||
"PROCESS_GROUP_MESSAGES": get_config("PROCESS_GROUP_MESSAGES", "false") == "true",
|
||||
"PROCESS_SELF_MESSAGES": get_config("PROCESS_SELF_MESSAGES", "true") == "true",
|
||||
"DEBUG_MODE": get_config("DEBUG_MODE", "false") == "true",
|
||||
}
|
||||
|
||||
async def forward_to_webhooks(body: dict, storage: StorageHandler):
|
||||
"""Encaminha o payload para todos os webhooks cadastrados."""
|
||||
webhooks = storage.get_webhook_redirects()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for webhook in webhooks:
|
||||
try:
|
||||
# Configura os headers mantendo o payload intacto
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-TranscreveZAP-Forward": "true", # Header para identificação da origem
|
||||
"X-TranscreveZAP-Webhook-ID": webhook["id"]
|
||||
}
|
||||
|
||||
async with session.post(
|
||||
webhook["url"],
|
||||
json=body, # Envia o payload original sem modificações
|
||||
headers=headers,
|
||||
timeout=10
|
||||
) as response:
|
||||
if response.status in [200, 201, 202]:
|
||||
storage.update_webhook_stats(webhook["id"], True)
|
||||
else:
|
||||
error_text = await response.text()
|
||||
storage.update_webhook_stats(
|
||||
webhook["id"],
|
||||
False,
|
||||
f"Status {response.status}: {error_text}"
|
||||
)
|
||||
# Registra falha para retry posterior
|
||||
storage.add_failed_delivery(webhook["id"], body)
|
||||
except Exception as e:
|
||||
storage.update_webhook_stats(
|
||||
webhook["id"],
|
||||
False,
|
||||
f"Erro ao encaminhar: {str(e)}"
|
||||
)
|
||||
# Registra falha para retry posterior
|
||||
storage.add_failed_delivery(webhook["id"], body)
|
||||
|
||||
@app.post("/transcreve-audios")
|
||||
async def transcreve_audios(request: Request):
|
||||
try:
|
||||
logger.info("Iniciando processamento de áudio")
|
||||
body = await request.json()
|
||||
dynamic_settings = load_dynamic_settings()
|
||||
# Iniciar o encaminhamento em background
|
||||
asyncio.create_task(forward_to_webhooks(body, storage))
|
||||
# Log inicial da requisição
|
||||
storage.add_log("INFO", "Nova requisição de transcrição recebida", {
|
||||
"instance": body.get("instance"),
|
||||
"event": body.get("event")
|
||||
})
|
||||
|
||||
if settings.DEBUG_MODE:
|
||||
logger.debug(f"Payload recebido: {body}")
|
||||
if dynamic_settings["DEBUG_MODE"]:
|
||||
storage.add_log("DEBUG", "Payload completo recebido", {
|
||||
"body": body
|
||||
})
|
||||
|
||||
# Extraindo informações
|
||||
server_url = body["server_url"]
|
||||
@ -30,58 +110,151 @@ async def transcreve_audios(request: Request):
|
||||
remote_jid = body["data"]["key"]["remoteJid"]
|
||||
message_type = body["data"]["messageType"]
|
||||
|
||||
# Verificação de tipo de mensagem
|
||||
if "audioMessage" not in message_type:
|
||||
logger.info("Mensagem recebida não é um áudio, ignorando")
|
||||
storage.add_log("INFO", "Mensagem ignorada - não é áudio", {
|
||||
"message_type": message_type,
|
||||
"remote_jid": remote_jid
|
||||
})
|
||||
return {"message": "Mensagem recebida não é um áudio"}
|
||||
|
||||
if from_me and not settings.PROCESS_SELF_MESSAGES:
|
||||
logger.info("Mensagem enviada pelo próprio usuário ignorada conforme configuração")
|
||||
# Verificação de permissões
|
||||
if not storage.can_process_message(remote_jid):
|
||||
is_group = "@g.us" in remote_jid
|
||||
storage.add_log("INFO",
|
||||
"Mensagem não autorizada para processamento",
|
||||
{
|
||||
"remote_jid": remote_jid,
|
||||
"tipo": "grupo" if is_group else "usuário",
|
||||
"motivo": "grupo não permitido" if is_group else "usuário bloqueado"
|
||||
}
|
||||
)
|
||||
return {"message": "Mensagem não autorizada para processamento"}
|
||||
|
||||
# Verificação do modo de processamento (grupos/todos)
|
||||
process_mode = storage.get_process_mode()
|
||||
is_group = "@g.us" in remote_jid
|
||||
|
||||
if process_mode == "groups_only" and not is_group:
|
||||
storage.add_log("INFO", "Mensagem ignorada - modo apenas grupos ativo", {
|
||||
"remote_jid": remote_jid,
|
||||
"process_mode": process_mode,
|
||||
"is_group": is_group
|
||||
})
|
||||
return {"message": "Modo apenas grupos ativo - mensagens privadas ignoradas"}
|
||||
|
||||
if from_me and not dynamic_settings["PROCESS_SELF_MESSAGES"]:
|
||||
storage.add_log("INFO", "Mensagem própria ignorada", {
|
||||
"remote_jid": remote_jid
|
||||
})
|
||||
return {"message": "Mensagem enviada por mim, sem operação"}
|
||||
|
||||
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"}
|
||||
# Obter áudio
|
||||
try:
|
||||
if "mediaUrl" in body["data"]["message"]:
|
||||
media_url = body["data"]["message"]["mediaUrl"]
|
||||
storage.add_log("DEBUG", "Baixando áudio via URL", {"mediaUrl": media_url})
|
||||
audio_source = await download_remote_audio(media_url) # Baixa o arquivo remoto e retorna o caminho local
|
||||
else:
|
||||
storage.add_log("DEBUG", "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)
|
||||
storage.add_log("DEBUG", "Áudio convertido", {"source": audio_source})
|
||||
|
||||
# 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:
|
||||
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}")
|
||||
# Carregar configurações de formatação
|
||||
output_mode = get_config("output_mode", "both")
|
||||
summary_header = get_config("summary_header", "🤖 *Resumo do áudio:*")
|
||||
transcription_header = get_config("transcription_header", "🔊 *Transcrição do áudio:*")
|
||||
character_limit = int(get_config("character_limit", "500"))
|
||||
|
||||
# Transcrever o áudio
|
||||
transcription_text, _ = await transcribe_audio(audio_source)
|
||||
summary_text = await summarize_text_if_needed(transcription_text)
|
||||
# Verificar se timestamps estão habilitados
|
||||
use_timestamps = get_config("use_timestamps", "false") == "true"
|
||||
|
||||
storage.add_log("DEBUG", "Informações da mensagem", {
|
||||
"from_me": from_me,
|
||||
"remote_jid": remote_jid,
|
||||
"is_group": is_group
|
||||
})
|
||||
|
||||
# Formatar a mensagem
|
||||
summary_message = (
|
||||
f"🤖 *Resumo do áudio:*\n\n"
|
||||
f"{summary_text}\n\n"
|
||||
f"🔊 *Transcrição do áudio:*\n\n"
|
||||
f"{transcription_text}\n\n"
|
||||
f"{settings.BUSINESS_MESSAGE}"
|
||||
)
|
||||
logger.debug(f"Mensagem formatada: {summary_message[:100]}...")
|
||||
# Transcrever áudio
|
||||
storage.add_log("INFO", "Iniciando transcrição")
|
||||
transcription_text, has_timestamps = await transcribe_audio(
|
||||
audio_source,
|
||||
apikey=apikey,
|
||||
remote_jid=remote_jid,
|
||||
from_me=from_me,
|
||||
use_timestamps=use_timestamps
|
||||
)
|
||||
# Log do resultado
|
||||
storage.add_log("INFO", "Transcrição concluída", {
|
||||
"has_timestamps": has_timestamps,
|
||||
"text_length": len(transcription_text),
|
||||
"remote_jid": remote_jid
|
||||
})
|
||||
# Determinar se precisa de resumo baseado no modo de saída
|
||||
summary_text = None
|
||||
if output_mode in ["both", "summary_only"] or (
|
||||
output_mode == "smart" and len(transcription_text) > character_limit
|
||||
):
|
||||
summary_text = await summarize_text_if_needed(transcription_text)
|
||||
|
||||
# Enviar a mensagem formatada via WhatsApp
|
||||
await send_message_to_whatsapp(
|
||||
server_url,
|
||||
instance,
|
||||
apikey,
|
||||
summary_message,
|
||||
remote_jid,
|
||||
audio_key,
|
||||
)
|
||||
# Construir mensagem baseada no modo de saída
|
||||
message_parts = []
|
||||
|
||||
if output_mode == "smart":
|
||||
if len(transcription_text) > character_limit:
|
||||
message_parts.append(f"{summary_header}\n\n{summary_text}")
|
||||
else:
|
||||
message_parts.append(f"{transcription_header}\n\n{transcription_text}")
|
||||
else:
|
||||
if output_mode in ["both", "summary_only"] and summary_text:
|
||||
message_parts.append(f"{summary_header}\n\n{summary_text}")
|
||||
if output_mode in ["both", "transcription_only"]:
|
||||
message_parts.append(f"{transcription_header}\n\n{transcription_text}")
|
||||
|
||||
# Adicionar mensagem de negócio
|
||||
message_parts.append(dynamic_settings['BUSINESS_MESSAGE'])
|
||||
|
||||
# Juntar todas as partes da mensagem
|
||||
summary_message = "\n\n".join(message_parts)
|
||||
|
||||
logger.info("Áudio processado e resposta enviada com sucesso")
|
||||
return {"message": "Áudio transcrito e resposta enviada com sucesso"}
|
||||
# Enviar resposta
|
||||
await send_message_to_whatsapp(
|
||||
server_url,
|
||||
instance,
|
||||
apikey,
|
||||
summary_message,
|
||||
remote_jid,
|
||||
audio_key,
|
||||
)
|
||||
|
||||
# Registrar sucesso
|
||||
storage.record_processing(remote_jid)
|
||||
storage.add_log("INFO", "Áudio processado com sucesso", {
|
||||
"remote_jid": remote_jid,
|
||||
"transcription_length": len(transcription_text) if transcription_text else 0,
|
||||
"summary_length": len(summary_text) if summary_text else 0 # Adiciona verificação
|
||||
})
|
||||
|
||||
return {"message": "Áudio transcrito e resposta enviada com sucesso"}
|
||||
|
||||
except Exception as e:
|
||||
storage.add_log("ERROR", f"Erro ao processar áudio: {str(e)}", {
|
||||
"error_type": type(e).__name__,
|
||||
"remote_jid": remote_jid,
|
||||
"traceback": traceback.format_exc()
|
||||
})
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Erro ao processar áudio: {str(e)}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao processar áudio: {str(e)}", exc_info=settings.DEBUG_MODE)
|
||||
storage.add_log("ERROR", f"Erro na requisição: {str(e)}", {
|
||||
"error_type": type(e).__name__,
|
||||
"traceback": traceback.format_exc()
|
||||
})
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Erro ao processar a requisição: {str(e)}",
|
||||
detail=f"Erro ao processar a requisição: {str(e)}"
|
||||
)
|
1100
manager.py
Normal file
1100
manager.py
Normal file
File diff suppressed because it is too large
Load Diff
74
openai_handler.py
Normal file
74
openai_handler.py
Normal file
@ -0,0 +1,74 @@
|
||||
import aiohttp
|
||||
import json
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from storage import StorageHandler
|
||||
|
||||
logger = logging.getLogger("OpenAIHandler")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
async def test_openai_key(key: str) -> bool:
|
||||
"""Test if an OpenAI key is valid and working."""
|
||||
url = "https://api.openai.com/v1/models"
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
return len(data.get("data", [])) > 0
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing OpenAI key: {e}")
|
||||
return False
|
||||
|
||||
async def handle_openai_request(
|
||||
url: str,
|
||||
headers: dict,
|
||||
data: any,
|
||||
storage: StorageHandler,
|
||||
is_form_data: bool = False
|
||||
) -> tuple[bool, dict, str]:
|
||||
"""Handle requests to OpenAI API with retries."""
|
||||
max_retries = 3
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if is_form_data:
|
||||
async with session.post(url, headers=headers, data=data) as response:
|
||||
response_data = await response.json()
|
||||
if response.status == 200:
|
||||
if is_form_data and response_data.get("text"):
|
||||
return True, response_data, ""
|
||||
elif not is_form_data and response_data.get("choices"):
|
||||
return True, response_data, ""
|
||||
else:
|
||||
async with session.post(url, headers=headers, json=data) as response:
|
||||
response_data = await response.json()
|
||||
if response.status == 200 and response_data.get("choices"):
|
||||
return True, response_data, ""
|
||||
|
||||
error_msg = response_data.get("error", {}).get("message", "")
|
||||
|
||||
if "invalid_api_key" in error_msg or "invalid authorization" in error_msg.lower():
|
||||
logger.error(f"OpenAI API key invalid or expired")
|
||||
return False, response_data, error_msg
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
continue
|
||||
|
||||
return False, response_data, error_msg
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in request: {str(e)}")
|
||||
if attempt < max_retries - 1:
|
||||
continue
|
||||
return False, {}, f"Request failed: {str(e)}"
|
||||
|
||||
return False, {}, "All retries failed"
|
484
readme.md
484
readme.md
@ -1,8 +1,22 @@
|
||||
# Transcrição e Resumo de Áudios no WhatsApp usando Python
|
||||
|
||||

|
||||
# TranscreveZAP 2.3- Plataforma de Gestão e Automação de Áudios do WhatsApp
|
||||
|
||||
Este projeto permite transcrever e resumir áudios enviados pelo WhatsApp usando inteligência artificial e integração com APIs. Ideal para automatizar o processamento de mensagens de áudio, oferecendo um resumo claro e prático.
|
||||
### Sistema Inteligente de Transcrição, Resumo e Tradução Automática de Áudios para WhatsApp
|
||||
|
||||
*Desenvolvido com Python, FastAPI e Streamlit*
|
||||
|
||||
---
|
||||
|
||||
Uma solução completa para automatizar e gerenciar mensagens de áudio no WhatsApp, oferecendo:
|
||||
- Transcrição automática multilíngue
|
||||
- Resumos inteligentes de áudios
|
||||
- Detecção e tradução automática entre idiomas
|
||||
- Seleção de plataforma LLM (GROQ ou OpenAI)
|
||||
- Interface administrativa completa
|
||||
- Sistema de rodízio de chaves API
|
||||
- Gestão avançada de grupos e usuários
|
||||
- Personalização de formatação e saída
|
||||
- Sistema de Redirecionamento de Webhooks
|
||||
|
||||
Contato de email: contato@impacte.ai
|
||||
([ACESSE NOSSO SITE](https://impacte.ai/))
|
||||
@ -15,33 +29,129 @@ 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 (começa com 'gsk_') ([Crie sua CONTA](https://console.groq.com/login))
|
||||
|
||||
- Chaves GROQ (começa com `gsk_`) e/ou chaves OpenAI (começa com `sk-`) configuradas ([Crie sua conta GROQ](https://console.groq.com/login))
|
||||
* Em caso de uso com Proxy Reverso Aponte um Subdomínio para a API e outro para o MANAGER da aplicação
|
||||
---
|
||||
|
||||
## ⚙️ **Setup Local**
|
||||
## 🚀 **Novidade: Escolha do Provedor LLM**
|
||||
Agora você pode escolher entre dois provedores para transcrições e resumos:
|
||||
1. **GROQ** (open-source): Configuração padrão.
|
||||
2. **OpenAI** (API paga): Integração com modelos GPT.
|
||||
|
||||
### Ambiente Virtual
|
||||
Configure o ambiente virtual para instalar as dependências do projeto:
|
||||
### Configuração:
|
||||
- Acesse: **Configurações > Provedor LLM** na interface administrativa.
|
||||
- Escolha entre `groq` e `openai`.
|
||||
- Adicione as chaves correspondentes para cada provedor.
|
||||
|
||||
---
|
||||
## 🚀 **Instalação e Configuração**
|
||||
|
||||
### 🐳 Docker Compose
|
||||
1. Configure o arquivo docker-compose.yaml:
|
||||
|
||||
```yaml
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
# Serviço principal do TranscreveZAP
|
||||
tcaudio:
|
||||
image: impacteai/transcrevezap:latest
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- "8005:8005" # API FastAPI - Use esta porta para configurar o webhook
|
||||
- "8501:8501" # Interface Web Streamlit - Acesse o painel por esta porta
|
||||
environment:
|
||||
# Configurações do Servidor
|
||||
- UVICORN_PORT=8005
|
||||
- UVICORN_HOST=0.0.0.0
|
||||
- UVICORN_RELOAD=true
|
||||
- UVICORN_WORKERS=1
|
||||
- API_DOMAIN=localhost # Para uso local mantenha localhost
|
||||
|
||||
# Modo Debug e Logs
|
||||
- DEBUG_MODE=false
|
||||
- LOG_LEVEL=INFO
|
||||
|
||||
# Credenciais do Painel Admin (ALTERE ESTAS CREDENCIAIS!)
|
||||
- MANAGER_USER=admin
|
||||
- MANAGER_PASSWORD=sua_senha_aqui
|
||||
|
||||
# Configurações do Redis
|
||||
- REDIS_HOST=redis-transcrevezap # Nome do serviço Redis
|
||||
- REDIS_PORT=6380 # Porta do Redis
|
||||
- REDIS_DB=0 # Banco de dados Redis
|
||||
|
||||
# Autenticação Redis (opcional - descomente se necessário)
|
||||
# - REDIS_USERNAME=seu_usuario # Nome do usuário Redis
|
||||
# - REDIS_PASSWORD=sua_senha # Senha do Redis
|
||||
depends_on:
|
||||
- redis-transcrevezap
|
||||
command: ./start.sh
|
||||
|
||||
# Serviço Redis para armazenamento de dados
|
||||
redis-transcrevezap:
|
||||
image: redis:6
|
||||
# Escolha UMA das configurações do Redis abaixo:
|
||||
|
||||
# 1. Configuração simples SEM autenticação:
|
||||
command: redis-server --port 6380 --appendonly yes
|
||||
|
||||
# 2. Configuração COM autenticação (descomente e ajuste se necessário):
|
||||
# command: >
|
||||
# redis-server
|
||||
# --port 6380
|
||||
# --appendonly yes
|
||||
# --user admin on '>sua_senha' '~*' '+@all'
|
||||
volumes:
|
||||
- redis_transcrevezap_data:/data # Persistência dos dados
|
||||
|
||||
# Volumes para persistência
|
||||
volumes:
|
||||
redis_transcrevezap_data:
|
||||
driver: local
|
||||
|
||||
# Instruções de Uso:
|
||||
# 1. Salve este arquivo como docker-compose.yml
|
||||
# 2. Execute com: docker compose up -d
|
||||
# 3. Acesse o painel em: http://localhost:8501
|
||||
# 4. Configure o webhook da Evolution API para: http://localhost:8005/transcreve-audios
|
||||
|
||||
#### **Linux ou macOS**
|
||||
```bash
|
||||
virtualenv venv
|
||||
source ./venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
#### **Windows**
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/Scripts/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Configuração do Arquivo .env
|
||||
Copie o arquivo `.env.example` para `.env` e configure suas variáveis:
|
||||
2. Inicie os serviços:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 📖 Configuração da Interface
|
||||
|
||||
Acesse a interface de gerenciamento em http://seu-ip:8501.
|
||||
Faça login com as credenciais definidas em MANAGER_USER e MANAGER_PASSWORD.
|
||||
Na seção "Configurações", defina:
|
||||
|
||||
1. GROQ_API_KEY: Sua chave da API GROQ
|
||||
2. BUSINESS_MESSAGE: Mensagem de rodapé após transcrição
|
||||
3. PROCESS_GROUP_MESSAGES: Habilitar processamento de mensagens em grupos
|
||||
4. PROCESS_SELF_MESSAGES: Habilitar processamento de mensagens próprias
|
||||
|
||||
## 🔧 Uso
|
||||
Endpoint para Webhook da Evolution API
|
||||
Configure o webhook da Evolution API para apontar para:
|
||||
```bash
|
||||
http://seu-ip:8005/transcreve-audios
|
||||
```
|
||||
## 🔍 Troubleshooting
|
||||
Se encontrar problemas:
|
||||
|
||||
1. Verifique os logs dos containers:
|
||||
```bash
|
||||
docker-compose logs
|
||||
```
|
||||
2. Certifique-se de que o Redis está rodando e acessível.
|
||||
3. Verifique se todas as configurações foram salvas corretamente na interface.
|
||||
|
||||
|
||||
## 📖 **Configuração Detalhada das Variáveis**
|
||||
|
||||
### Variáveis Essenciais
|
||||
@ -77,51 +187,39 @@ uvicorn main:app --host 0.0.0.0 --port 8005
|
||||
```bash
|
||||
http://127.0.0.1:8005/transcreve-audios
|
||||
```
|
||||
|
||||
### 🐳 Docker Compose Simples
|
||||
```yaml
|
||||
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
|
||||
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
|
||||
PROCESS_SELF_MESSAGES: "true" # Define se sua próprias mensagens devem ser processadas
|
||||
DEBUG_MODE: "false"
|
||||
LOG_LEVEL: "INFO"
|
||||
```
|
||||
1. Aponte um subomínio com o IP do seu servidor para a API da TranscreveZAP
|
||||
2. Aponte um subomínio com o IP do seu servidor para o MANAGER da TranscreveZAP
|
||||
|
||||
### 🌟 Docker Swarm com Traefik
|
||||
```yaml
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
transcricaoaudio:
|
||||
image: impacteai/transcrevezap:latest
|
||||
build: .
|
||||
tcaudio:
|
||||
image: impacteai/transcrevezap:dev
|
||||
networks:
|
||||
- suarededocker #troque pela sua rede do docker
|
||||
- sua_rede_externa # Substitua pelo nome da sua rede externa
|
||||
ports:
|
||||
- 8005:8005
|
||||
- 8005:8005 # Porta para FastAPI
|
||||
- 8501:8501 # Porta para Streamlit
|
||||
environment:
|
||||
Uvicorn_port: 8005
|
||||
Uvicorn_host: 0.0.0.0
|
||||
Uvicorn_reload: "true"
|
||||
Uvicorn_workers: 1
|
||||
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
|
||||
PROCESS_SELF_MESSAGES: "true" # Define se sua próprias mensagens devem ser processadas
|
||||
DEBUG_MODE: "false"
|
||||
LOG_LEVEL: "INFO"
|
||||
- UVICORN_PORT=8005
|
||||
- UVICORN_HOST=0.0.0.0
|
||||
- UVICORN_RELOAD=true
|
||||
- UVICORN_WORKERS=1
|
||||
- API_DOMAIN=seu.dominio.com #coloque seu subdominio da API apontado aqui
|
||||
- DEBUG_MODE=false
|
||||
- LOG_LEVEL=INFO
|
||||
- MANAGER_USER=seu_usuario_admin # Defina Usuário do Manager
|
||||
- MANAGER_PASSWORD=sua_senha_segura # Defina Senha do Manager
|
||||
- REDIS_HOST=redis-transcrevezap
|
||||
- REDIS_PORT=6380 # Porta personalizada para o Redis do TranscreveZAP
|
||||
- REDIS_DB=0 # Opcional: pode ser removida para usar o valor padrão
|
||||
# Autenticação Redis (opcional - descomente se necessário, se estiver usando autenticação)
|
||||
# - REDIS_USERNAME=${REDIS_USERNAME:-} # Nome do usuário definido no comando do Redis
|
||||
# - REDIS_PASSWORD=${REDIS_PASSWORD:-} # Senha definida no comando do Redis (sem o '>')
|
||||
depends_on:
|
||||
- redis-transcrevezap
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
@ -130,23 +228,57 @@ 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.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.routers.tcaudio.rule=Host(`seu.dominio.com`) #coloque seu subdominio da API apontado aqui
|
||||
- traefik.http.routers.tcaudio.entrypoints=websecure
|
||||
- traefik.http.routers.tcaudio.tls.certresolver=letsencryptresolver
|
||||
- traefik.http.services.tcaudio.loadbalancer.server.port=8005
|
||||
- traefik.http.services.tcaudio.loadbalancer.passHostHeader=true
|
||||
- traefik.http.routers.tcaudio.service=tcaudio
|
||||
- traefik.http.middlewares.traefik-compress.compress=true
|
||||
- traefik.http.routers.transcricaoaudio.middlewares=traefik-compress
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1"
|
||||
memory: 1024M
|
||||
- traefik.http.routers.tcaudio.middlewares=traefik-compress
|
||||
# Configuração do Streamlit
|
||||
- traefik.http.routers.tcaudio-manager.rule=Host(`manager.seu.dominio.com`) #coloque seu subdominio do Manager apontado aqui
|
||||
- traefik.http.routers.tcaudio-manager.entrypoints=websecure
|
||||
- traefik.http.routers.tcaudio-manager.tls.certresolver=letsencryptresolver
|
||||
- traefik.http.services.tcaudio-manager.loadbalancer.server.port=8501
|
||||
- traefik.http.routers.tcaudio-manager.service=tcaudio-manager
|
||||
command: ./start.sh
|
||||
|
||||
redis-transcrevezap:
|
||||
image: redis:6
|
||||
# 1. Configuração SEM autenticação (padrão):
|
||||
command: redis-server --port 6380 --appendonly yes
|
||||
# 2. Configuração COM autenticação (descomente e ajuste se necessário):
|
||||
# command: >
|
||||
# redis-server
|
||||
# --port 6380
|
||||
# --appendonly yes
|
||||
# --user seuusuario on '>minhasenha' '~*' '+@all'
|
||||
# # Explicação dos parâmetros:
|
||||
# # --user seuusuario: nome do usuário
|
||||
# # on: indica início da configuração do usuário
|
||||
# # '>minhasenha': senha do usuário (mantenha o '>')
|
||||
# # '~*': permite acesso a todas as chaves
|
||||
# # '+@all': concede todas as permissões
|
||||
volumes:
|
||||
- redis_transcrevezap_data:/data
|
||||
networks:
|
||||
- sua_rede_externa # Substitua pelo nome da sua rede externa
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
|
||||
networks:
|
||||
suarededocker: #troque pela sua rede do docker
|
||||
sua_rede_externa: # Substitua pelo nome da sua rede externa
|
||||
external: true
|
||||
name: suarededocker #troque pela sua rede do docker
|
||||
name: sua_rede_externa # Substitua pelo nome da sua rede externa
|
||||
|
||||
volumes:
|
||||
redis_transcrevezap_data:
|
||||
driver: local
|
||||
```
|
||||
|
||||
### Endpoint para inserir no webhook da Evolution API para consumir o serviço
|
||||
@ -158,9 +290,10 @@ https://transcricaoaudio.seudominio.com.br/transcreve-audios
|
||||
|
||||
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
|
||||
2. Configurar 2 DNS do seu domínio para apontar para a API e para o MANAGER
|
||||
3. Ajustar as labels do Traefik conforme seu ambiente
|
||||
4. Verificar se a rede externa existe no Docker Swarm
|
||||
5. Utilize a stack de exemplo contida no projeto para guiar a instalação
|
||||
|
||||
## 📝 **Notas Importantes**
|
||||
- A GROQ_API_KEY deve começar com 'gsk_'
|
||||
@ -169,6 +302,176 @@ Para usar com Traefik, certifique-se de:
|
||||
- Em produção, recomenda-se DEBUG_MODE=false
|
||||
- Configure LOG_LEVEL=DEBUG apenas para troubleshooting
|
||||
|
||||
## 🚀 Novo Recurso v2.3.1: Hub de Redirecionamento
|
||||
|
||||
O TranscreveZAP agora oferece um sistema robusto para redirecionamento de mensagens, permitindo que você encaminhe os webhooks da Evolution API para múltiplos destinos simultaneamente.
|
||||
|
||||
### Principais Recursos
|
||||
- Interface dedicada para gerenciamento de webhooks
|
||||
- Redirecionamento sem alteração do payload original
|
||||
- Monitoramento de saúde dos webhooks em tempo real
|
||||
- Sistema de retry automático para reenvio de mensagens falhas
|
||||
- Headers de rastreamento para identificação de origem (`X-TranscreveZAP-Forward`)
|
||||
- Suporte a descrições personalizadas para cada webhook
|
||||
- Limpeza automática de dados ao remover webhooks
|
||||
|
||||
### Compatibilidade
|
||||
- Mantém o payload da Evolution API intacto
|
||||
- Suporta múltiplos endpoints simultaneamente
|
||||
- Compatível com qualquer sistema que aceite webhooks via POST
|
||||
- Preserva todos os dados originais da mensagem
|
||||
|
||||
## ✨ Novos Recursos na v2.3
|
||||
|
||||
### 🌍 Suporte Multilíngue
|
||||
- Transcrição e resumo com suporte para 16 idiomas principais
|
||||
- Mudança instantânea de idioma
|
||||
- Interface intuitiva para seleção de idioma
|
||||
- Mantém consistência entre transcrição e resumo
|
||||
- Configuração manual de idioma por contato
|
||||
- Detecção automática de idioma
|
||||
- Tradução automática integrada
|
||||
|
||||
### 🔄 Sistema de Cache para Idiomas
|
||||
Implementação de cache inteligente para otimizar a detecção e processamento de idiomas.
|
||||
|
||||
### 🔄 Sistema Inteligente de Rodízio de Chaves
|
||||
- Suporte a múltiplas chaves GROQ
|
||||
- Balanceamento automático de carga
|
||||
- Maior redundância e disponibilidade
|
||||
- Gestão simplificada de chaves via interface
|
||||
|
||||
### ⏱️ Timestamps em Transcrições
|
||||
Nova funcionalidade de timestamps que adiciona marcadores de tempo precisos em cada trecho da transcrição.
|
||||
|
||||
## 📋 Detalhamento das Funcionalidades
|
||||
|
||||
### 🌍 Sistema de Idiomas
|
||||
O TranscreveZAP suporta transcrição e resumo em múltiplos idiomas. Na seção "Configurações", você pode:
|
||||
|
||||
1. Selecionar o idioma principal para transcrição e resumo
|
||||
2. O sistema manterá Português como padrão se nenhum outro for selecionado
|
||||
3. A mudança de idioma é aplicada instantaneamente após salvar
|
||||
|
||||
Idiomas suportados:
|
||||
- 🇩🇪 Alemão
|
||||
- 🇸🇦 Árabe
|
||||
- 🇨🇳 Chinês
|
||||
- 🇰🇷 Coreano
|
||||
- 🇪🇸 Espanhol
|
||||
- 🇫🇷 Francês
|
||||
- 🇮🇳 Hindi
|
||||
- 🇳🇱 Holandês
|
||||
- 🇬🇧 Inglês
|
||||
- 🇮🇹 Italiano
|
||||
- 🇯🇵 Japonês
|
||||
- 🇵🇱 Polonês
|
||||
- 🇧🇷 Português (padrão)
|
||||
- 🇷🇴 Romeno
|
||||
- 🇷🇺 Russo
|
||||
- 🇹🇷 Turco
|
||||
|
||||
### 🌐 Gestão de Idiomas por Contato
|
||||
|
||||
#### Configuração Manual
|
||||
```markdown
|
||||
1. Acesse o Manager > Configurações > Idiomas e Transcrição
|
||||
2. Expanda "Adicionar Novo Contato"
|
||||
3. Digite o número do contato (formato: 5521999999999)
|
||||
4. Selecione o idioma desejado
|
||||
5. Clique em "Adicionar Contato"
|
||||
```
|
||||
|
||||
### 🔄 Detecção Automática de Idioma
|
||||
Nova funcionalidade que detecta automaticamente o idioma do contato:
|
||||
- Ativação via Manager > Configurações > Idiomas e Transcrição
|
||||
- Analisa o primeiro áudio de cada contato
|
||||
- Cache inteligente de 24 horas
|
||||
- Funciona apenas em conversas privadas
|
||||
- Mantém configuração global para grupos
|
||||
|
||||
### ⚡ Tradução Automática
|
||||
Sistema inteligente de tradução que:
|
||||
- Traduz automaticamente áudios recebidos para seu idioma principal
|
||||
- Mantém o contexto e estilo original da mensagem
|
||||
- Preserva formatações especiais (emojis, negrito, itálico)
|
||||
- Otimizado para comunicação natural
|
||||
|
||||
### ⏱️ Sistema de Timestamps
|
||||
Nova funcionalidade que adiciona marcadores de tempo:
|
||||
- Formato [MM:SS] no início de cada trecho
|
||||
- Ativação via Manager > Configurações > Idiomas e Transcrição
|
||||
- Precisão de segundos
|
||||
- Ideal para referência e navegação em áudios longos
|
||||
|
||||
#### Exemplo de Saída com Timestamps:
|
||||
```
|
||||
[00:00] Bom dia pessoal
|
||||
[00:02] Hoje vamos falar sobre
|
||||
[00:05] O novo sistema de timestamps
|
||||
```
|
||||
## 🔧 Configuração e Uso
|
||||
|
||||
### Configuração de Idiomas
|
||||
1. **Configuração Global**
|
||||
- Defina o idioma padrão do sistema
|
||||
- Acesse: Manager > Configurações > Configurações Gerais
|
||||
- Selecione o idioma principal em "Idioma para Transcrição e Resumo"
|
||||
|
||||
2. **Configuração por Contato**
|
||||
- Acesse: Manager > Configurações > Idiomas e Transcrição
|
||||
- Use "Adicionar Novo Contato" ou gerencie contatos existentes
|
||||
- Cada contato pode ter seu próprio idioma configurado
|
||||
|
||||
3. **Detecção Automática**
|
||||
- Ative/Desative a detecção automática
|
||||
- Configure o tempo de cache
|
||||
- Gerencie exceções e configurações manuais
|
||||
|
||||
### Configuração de Timestamps
|
||||
1. Acesse: Manager > Configurações > Idiomas e Transcrição
|
||||
2. Localize a seção "Timestamps na Transcrição"
|
||||
3. Use o toggle para ativar/desativar
|
||||
4. As mudanças são aplicadas imediatamente
|
||||
|
||||
## 📊 Monitoramento e Estatísticas
|
||||
|
||||
### Estatísticas de Idiomas
|
||||
O sistema agora oferece estatísticas detalhadas:
|
||||
- Total de transcrições por idioma
|
||||
- Número de detecções automáticas
|
||||
- Divisão entre mensagens enviadas/recebidas
|
||||
- Histórico de uso por idioma
|
||||
|
||||
### Visualização de Dados
|
||||
- Gráficos de uso por idioma
|
||||
- Distribuição de idiomas
|
||||
- Estatísticas de tradução
|
||||
- Performance do sistema
|
||||
|
||||
## 🔄 Sistema de Rodízio de Chaves GROQ
|
||||
O TranscreveZAP suporta múltiplas chaves GROQ com sistema de rodízio automático para melhor distribuição de carga e redundância.
|
||||
|
||||
### Funcionalidades:
|
||||
1. Adicione múltiplas chaves GROQ para distribuição de carga
|
||||
2. O sistema alterna automaticamente entre as chaves disponíveis
|
||||
3. Se uma chave falhar, o sistema usa a próxima disponível
|
||||
4. Visualize todas as chaves configuradas no painel
|
||||
5. Adicione ou remova chaves sem interromper o serviço
|
||||
|
||||
### Como Configurar:
|
||||
1. Acesse a seção "Configurações"
|
||||
2. Na área "🔑 Gerenciamento de Chaves GROQ":
|
||||
- Adicione a chave principal
|
||||
- Use "Adicionar Nova Chave GROQ" para incluir chaves adicionais
|
||||
- O sistema começará a usar todas as chaves em rodízio automaticamente
|
||||
|
||||
### Boas Práticas:
|
||||
- Mantenha pelo menos duas chaves ativas para redundância
|
||||
- Monitore o uso das chaves pelo painel administrativo
|
||||
- Remova chaves expiradas ou inválidas
|
||||
- Todas as chaves devem começar com 'gsk_'
|
||||
|
||||
## 🔍 **Troubleshooting**
|
||||
Se encontrar problemas:
|
||||
1. Verifique se todas as variáveis obrigatórias estão configuradas
|
||||
@ -176,10 +479,45 @@ Se encontrar problemas:
|
||||
3. Verifique os logs do container
|
||||
4. Certifique-se que as APIs estão acessíveis
|
||||
|
||||
### Problemas com Múltiplas Chaves GROQ:
|
||||
1. Verifique se todas as chaves começam com 'gsk_'
|
||||
2. Confirme se as chaves estão ativas na console GROQ
|
||||
3. Monitore os logs para identificar falhas específicas de chaves
|
||||
4. Mantenha pelo menos uma chave válida no sistema
|
||||
|
||||
### Problemas com Idiomas:
|
||||
1. Verifique se o idioma está corretamente selecionado nas configurações
|
||||
2. Confirme se a configuração foi salva com sucesso
|
||||
3. Reinicie o serviço se as alterações não forem aplicadas
|
||||
4. Verifique os logs para confirmar o idioma em uso
|
||||
|
||||
## 📝 Notas Adicionais
|
||||
|
||||
### Recomendações de Uso
|
||||
- Configure idiomas manualmente para contatos frequentes
|
||||
- Use detecção automática como fallback
|
||||
- Monitore estatísticas de uso
|
||||
- Faça backups regulares das configurações
|
||||
|
||||
### Limitações Conhecidas
|
||||
- Detecção automática requer primeiro áudio
|
||||
- Cache limitado a 24 horas
|
||||
- Timestamps podem variar em áudios muito longos
|
||||
|
||||
## 🤝 Contribuição
|
||||
Agradecemos feedback e contribuições! Reporte issues e sugira melhorias em nosso GitHub.
|
||||
---
|
||||
|
||||
### 📞 Suporte
|
||||
Para suporte adicional ou dúvidas:
|
||||
- WhatsApp: [Entre no GRUPO](https://chat.whatsapp.com/L9jB1SlcmQFIVxzN71Y6KG)
|
||||
- Email: contato@impacte.ai
|
||||
- Site: [impacte.ai](https://impacte.ai)
|
||||
|
||||
## 📄 **Licença**
|
||||
Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes.
|
||||
|
||||
---
|
||||
### SE QUISER CONTRIBUIR COM O PROJETO, FAÇA O PIX NO QR CODE
|
||||
### AJUDE CONTRIBUINDO COM O PROJETO, FAÇA O PIX NO QR CODE
|
||||

|
||||
---
|
@ -8,21 +8,23 @@ attrs==24.2.0
|
||||
certifi==2024.8.30
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.7
|
||||
fastapi==0.109.2
|
||||
fastapi==0.115.6
|
||||
frozenlist==1.4.1
|
||||
h11==0.14.0
|
||||
idna==3.10
|
||||
multidict==6.1.0
|
||||
pip-system-certs==4.0
|
||||
plotly==5.18.0
|
||||
propcache==0.2.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.36.3,<0.37.0
|
||||
streamlit==1.31.0
|
||||
typing_extensions==4.12.2
|
||||
urllib3==1.26.20
|
||||
uvicorn==0.17.6
|
||||
uvicorn==0.23.2
|
||||
wrapt==1.16.0
|
||||
yarl==1.15.2
|
||||
redis
|
783
services.py
783
services.py
@ -2,147 +2,561 @@ import aiohttp
|
||||
import base64
|
||||
import aiofiles
|
||||
from fastapi import HTTPException
|
||||
from config import settings, logger
|
||||
from config import settings, logger, redis_client
|
||||
from storage import StorageHandler
|
||||
import os
|
||||
import json
|
||||
import tempfile
|
||||
import traceback
|
||||
from groq_handler import get_working_groq_key, validate_transcription_response, handle_groq_request
|
||||
# Inicializa o storage handler
|
||||
storage = StorageHandler()
|
||||
|
||||
async def convert_base64_to_file(base64_data):
|
||||
"""Converte dados base64 em arquivo temporário"""
|
||||
try:
|
||||
logger.debug("Iniciando conversão de base64 para arquivo")
|
||||
storage.add_log("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)
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
|
||||
temp_file.write(audio_data)
|
||||
audio_file_path = temp_file.name
|
||||
|
||||
logger.debug(f"Arquivo temporário criado em: {audio_file_path}")
|
||||
storage.add_log("DEBUG", "Arquivo temporário criado", {
|
||||
"path": 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)
|
||||
storage.add_log("ERROR", "Erro na conversão base64", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__
|
||||
})
|
||||
raise
|
||||
|
||||
async def get_groq_key():
|
||||
"""Obtém a próxima chave GROQ do sistema de rodízio."""
|
||||
key = storage.get_next_groq_key()
|
||||
if not key:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Nenhuma chave GROQ configurada. Configure pelo menos uma chave no painel administrativo."
|
||||
)
|
||||
return key
|
||||
|
||||
async def summarize_text_if_needed(text):
|
||||
"""Resumir texto usando a API GROQ"""
|
||||
logger.debug("Iniciando processo de resumo do texto")
|
||||
"""Resumir texto usando a API GROQ com sistema de rodízio de chaves"""
|
||||
storage.add_log("DEBUG", "Iniciando processo de resumo", {
|
||||
"text_length": len(text)
|
||||
})
|
||||
provider = storage.get_llm_provider()
|
||||
|
||||
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
||||
# Obter idioma configurado
|
||||
language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt"
|
||||
storage.add_log("DEBUG", "Idioma configurado para resumo", {
|
||||
"language": language,
|
||||
"redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE")
|
||||
})
|
||||
|
||||
if provider == "openai":
|
||||
api_key = storage.get_openai_keys()[0]
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
model = "gpt-4o-mini"
|
||||
else: # groq
|
||||
url = "https://api.groq.com/openai/v1/chat/completions"
|
||||
api_key = await get_working_groq_key(storage)
|
||||
if not api_key:
|
||||
raise Exception("Nenhuma chave GROQ disponível")
|
||||
model = "llama-3.3-70b-versatile"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {settings.GROQ_API_KEY}",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# Adaptar o prompt para considerar o idioma
|
||||
prompt_by_language = {
|
||||
"pt": """
|
||||
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 Fabio.
|
||||
Escreva APENAS o resumo do áudio como se fosse você que estivesse enviando
|
||||
essa mensagem! Não cumprimente, não de oi, não escreva nada antes nem depois
|
||||
do resumo, responda apenas um resumo enxuto do que foi falado no áudio.
|
||||
""",
|
||||
"en": """
|
||||
Understand the context of this audio and make a very concise summary of what it's about.
|
||||
This audio was sent via WhatsApp, from someone, to Fabio.
|
||||
Write ONLY the summary of the audio as if you were sending this message yourself!
|
||||
Don't greet, don't say hi, don't write anything before or after the summary,
|
||||
respond with just a concise summary of what was said in the audio.
|
||||
""",
|
||||
"es": """
|
||||
Entiende el contexto de este audio y haz un resumen muy conciso sobre de qué se trata.
|
||||
Este audio fue enviado por WhatsApp, de alguien, para Fabio.
|
||||
Escribe SOLO el resumen del audio como si tú estuvieras enviando este mensaje.
|
||||
No saludes, no escribas nada antes ni después del resumen, responde únicamente un resumen conciso de lo dicho en el audio.
|
||||
""",
|
||||
"fr": """
|
||||
Comprenez le contexte de cet audio et faites un résumé très concis de ce dont il s'agit.
|
||||
Cet audio a été envoyé via WhatsApp, par quelqu'un, à Fabio.
|
||||
Écrivez UNIQUEMENT le résumé de l'audio comme si c'était vous qui envoyiez ce message.
|
||||
Ne saluez pas, n'écrivez rien avant ou après le résumé, répondez seulement par un résumé concis de ce qui a été dit dans l'audio.
|
||||
""",
|
||||
"de": """
|
||||
Verstehen Sie den Kontext dieses Audios und erstellen Sie eine sehr kurze Zusammenfassung, worum es geht.
|
||||
Dieses Audio wurde über WhatsApp von jemandem an Fabio gesendet.
|
||||
Schreiben Sie NUR die Zusammenfassung des Audios, als ob Sie diese Nachricht senden würden.
|
||||
Grüßen Sie nicht, schreiben Sie nichts vor oder nach der Zusammenfassung, antworten Sie nur mit einer kurzen Zusammenfassung dessen, was im Audio gesagt wurde.
|
||||
""",
|
||||
"it": """
|
||||
Comprendi il contesto di questo audio e fai un riassunto molto conciso di cosa si tratta.
|
||||
Questo audio è stato inviato tramite WhatsApp, da qualcuno, a Fabio.
|
||||
Scrivi SOLO il riassunto dell'audio come se fossi tu a inviare questo messaggio.
|
||||
Non salutare, non scrivere nulla prima o dopo il riassunto, rispondi solo con un riassunto conciso di ciò che è stato detto nell'audio.
|
||||
""",
|
||||
"ja": """
|
||||
この音声の内容を理解し、それが何について話されているのかを非常に簡潔に要約してください。
|
||||
この音声は、誰かがWhatsAppでファビオに送ったものです。
|
||||
あなたがそのメッセージを送っているように、音声の要約だけを記述してください。
|
||||
挨拶や前置き、後書きは書かず、音声で話された内容の簡潔な要約のみを返信してください。
|
||||
""",
|
||||
"ko": """
|
||||
이 오디오의 맥락을 이해하고, 무엇에 관한 것인지 매우 간략하게 요약하세요.
|
||||
이 오디오는 누군가가 WhatsApp을 통해 Fabio에게 보낸 것입니다.
|
||||
마치 당신이 메시지를 보내는 것처럼 오디오의 요약만 작성하세요.
|
||||
인사하거나, 요약 전후로 아무것도 쓰지 말고, 오디오에서 말한 내용을 간략하게 요약한 답변만 하세요.
|
||||
""",
|
||||
"zh": """
|
||||
理解这个音频的上下文,并简洁地总结它的内容。
|
||||
这个音频是某人通过WhatsApp发送给Fabio的。
|
||||
请仅以摘要的形式回答,就好像是你在发送这条消息。
|
||||
不要问候,也不要在摘要前后写任何内容,只需用一句简短的话总结音频中所说的内容。
|
||||
""",
|
||||
"ro": """
|
||||
Înțelege contextul acestui audio și creează un rezumat foarte concis despre ce este vorba.
|
||||
Acest audio a fost trimis prin WhatsApp, de cineva, către Fabio.
|
||||
Scrie DOAR rezumatul audio-ului ca și cum tu ai trimite acest mesaj.
|
||||
Nu saluta, nu scrie nimic înainte sau după rezumat, răspunde doar cu un rezumat concis despre ce s-a spus în audio.
|
||||
""",
|
||||
|
||||
"ru": """
|
||||
Поймите контекст этого аудио и сделайте очень краткое резюме, о чем идет речь.
|
||||
Это аудио было отправлено через WhatsApp кем-то Фабио.
|
||||
Напишите ТОЛЬКО резюме аудио, как будто вы отправляете это сообщение.
|
||||
Не приветствуйте, не пишите ничего до или после резюме, ответьте только кратким резюме того, что говорилось в аудио.
|
||||
"""
|
||||
}
|
||||
|
||||
# Usar o prompt do idioma configurado ou fallback para português
|
||||
base_prompt = prompt_by_language.get(language, prompt_by_language["pt"])
|
||||
json_data = {
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": f"""
|
||||
Entenda o contexto desse áudio e faça um resumo super enxuto sobre o que se trata, coloque os pontos relevantes e mais importantes no resumo de forma muito curta.
|
||||
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}""",
|
||||
"content": f"{base_prompt}\n\nTexto para resumir: {text}",
|
||||
}],
|
||||
"model": "llama-3.3-70b-versatile",
|
||||
"model": model,
|
||||
}
|
||||
|
||||
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}")
|
||||
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
|
||||
if not success:
|
||||
raise Exception(error)
|
||||
|
||||
summary_text = response_data["choices"][0]["message"]["content"]
|
||||
# Validar se o resumo não está vazio
|
||||
if not await validate_transcription_response(summary_text):
|
||||
storage.add_log("ERROR", "Resumo vazio ou inválido recebido")
|
||||
raise Exception("Resumo vazio ou inválido recebido")
|
||||
# Validar se o resumo é menor que o texto original
|
||||
if len(summary_text) >= len(text):
|
||||
storage.add_log("WARNING", "Resumo maior que texto original", {
|
||||
"original_length": len(text),
|
||||
"summary_length": len(summary_text)
|
||||
})
|
||||
storage.add_log("INFO", "Resumo gerado com sucesso", {
|
||||
"original_length": len(text),
|
||||
"summary_length": len(summary_text),
|
||||
"language": language
|
||||
})
|
||||
|
||||
return summary_text
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro no processo de resumo: {str(e)}", exc_info=settings.DEBUG_MODE)
|
||||
storage.add_log("ERROR", "Erro no processo de resumo", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__
|
||||
})
|
||||
raise
|
||||
|
||||
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"
|
||||
groq_headers = {"Authorization": f"Bearer {settings.GROQ_API_KEY}"}
|
||||
async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=False, use_timestamps=False):
|
||||
"""
|
||||
Transcreve áudio com suporte a detecção de idioma e tradução automática.
|
||||
|
||||
Args:
|
||||
audio_source: Caminho do arquivo de áudio ou URL
|
||||
apikey: Chave da API opcional para download de áudio
|
||||
remote_jid: ID do remetente/destinatário
|
||||
from_me: Se o áudio foi enviado pelo próprio usuário
|
||||
use_timestamps: Se True, usa verbose_json para incluir timestamps
|
||||
|
||||
Returns:
|
||||
tuple: (texto_transcrito, has_timestamps)
|
||||
"""
|
||||
storage.add_log("INFO", "Iniciando processo de transcrição", {
|
||||
"from_me": from_me,
|
||||
"remote_jid": remote_jid
|
||||
})
|
||||
provider = storage.get_llm_provider()
|
||||
|
||||
if provider == "openai":
|
||||
api_key = storage.get_openai_keys()[0] # Get first OpenAI key
|
||||
url = "https://api.openai.com/v1/audio/transcriptions"
|
||||
model = "whisper-1"
|
||||
else: # groq
|
||||
api_key = await get_working_groq_key(storage)
|
||||
if not api_key:
|
||||
raise Exception("Nenhuma chave GROQ disponível")
|
||||
url = "https://api.groq.com/openai/v1/audio/transcriptions"
|
||||
model = "whisper-large-v3"
|
||||
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
|
||||
# Inicializar variáveis
|
||||
contact_language = None
|
||||
system_language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt"
|
||||
is_private = remote_jid and "@s.whatsapp.net" in remote_jid
|
||||
|
||||
# Determinar idioma do contato em conversas privadas
|
||||
if is_private:
|
||||
# Remover @s.whatsapp.net do ID para buscar no cache
|
||||
contact_id = remote_jid.split('@')[0]
|
||||
|
||||
# 1. Primeiro tentar obter idioma configurado manualmente
|
||||
contact_language = storage.get_contact_language(contact_id)
|
||||
if contact_language:
|
||||
storage.add_log("DEBUG", "Usando idioma configurado manualmente", {
|
||||
"contact_language": contact_language,
|
||||
"from_me": from_me,
|
||||
"remote_jid": remote_jid,
|
||||
"is_private": is_private
|
||||
})
|
||||
# 2. Se não houver configuração manual e detecção automática estiver ativa
|
||||
elif storage.get_auto_language_detection():
|
||||
# Verificar cache primeiro
|
||||
cached_lang = storage.get_cached_language(contact_id)
|
||||
if cached_lang:
|
||||
contact_language = cached_lang.get('language')
|
||||
storage.add_log("DEBUG", "Usando idioma do cache", {
|
||||
"contact_language": contact_language,
|
||||
"auto_detected": True
|
||||
})
|
||||
# Se não há cache ou está expirado, fazer detecção
|
||||
elif not from_me: # Só detecta em mensagens recebidas
|
||||
try:
|
||||
# Realizar transcrição inicial sem idioma específico
|
||||
with open(audio_source, 'rb') as audio_file:
|
||||
data = aiohttp.FormData()
|
||||
data.add_field('file', audio_file, filename='audio.mp3')
|
||||
data.add_field('model', model)
|
||||
|
||||
success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True)
|
||||
if success:
|
||||
initial_text = response_data.get("text", "")
|
||||
|
||||
# Detectar idioma do texto transcrito
|
||||
detected_lang = await detect_language(initial_text)
|
||||
|
||||
# Salvar no cache E na configuração do contato
|
||||
storage.cache_language_detection(contact_id, detected_lang)
|
||||
storage.set_contact_language(contact_id, detected_lang)
|
||||
|
||||
contact_language = detected_lang
|
||||
storage.add_log("INFO", "Idioma detectado e configurado", {
|
||||
"language": detected_lang,
|
||||
"remote_jid": remote_jid,
|
||||
"auto_detected": True
|
||||
})
|
||||
except Exception as e:
|
||||
storage.add_log("WARNING", "Erro na detecção automática de idioma", {
|
||||
"error": str(e),
|
||||
"remote_jid": remote_jid
|
||||
})
|
||||
|
||||
if not contact_language:
|
||||
storage.add_log("DEBUG", "Usando idioma padrão do sistema", {
|
||||
"from_me": from_me,
|
||||
"remote_jid": remote_jid,
|
||||
"is_private": is_private,
|
||||
"system_language": system_language
|
||||
})
|
||||
|
||||
# Definir idioma de transcrição e tradução baseado no contexto
|
||||
if is_private and contact_language:
|
||||
if from_me:
|
||||
# Se estou enviando para um contato com idioma configurado
|
||||
transcription_language = contact_language # Transcrever no idioma do contato
|
||||
target_language = contact_language # Não precisa traduzir
|
||||
storage.add_log("DEBUG", "Usando idioma do contato para áudio enviado", {
|
||||
"transcription_language": transcription_language,
|
||||
"target_language": target_language
|
||||
})
|
||||
else:
|
||||
# Se estou recebendo
|
||||
transcription_language = contact_language # Transcrever no idioma do contato
|
||||
target_language = system_language # Traduzir para o idioma do sistema
|
||||
storage.add_log("DEBUG", "Processando áudio recebido com tradução", {
|
||||
"transcription_language": transcription_language,
|
||||
"target_language": target_language
|
||||
})
|
||||
else:
|
||||
# Caso padrão: usar idioma do sistema
|
||||
transcription_language = system_language
|
||||
target_language = system_language
|
||||
storage.add_log("DEBUG", "Usando idioma do sistema", {
|
||||
"transcription_language": transcription_language,
|
||||
"target_language": target_language
|
||||
})
|
||||
|
||||
storage.add_log("DEBUG", "Configuração de idiomas definida", {
|
||||
"transcription_language": transcription_language,
|
||||
"target_language": target_language,
|
||||
"from_me": from_me,
|
||||
"is_private": is_private,
|
||||
"contact_language": contact_language
|
||||
})
|
||||
|
||||
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}")
|
||||
|
||||
# Preparar dados para transcrição
|
||||
# Realizar transcrição
|
||||
with open(audio_source, 'rb') as audio_file:
|
||||
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')
|
||||
data.add_field('file', audio_file, filename='audio.mp3')
|
||||
data.add_field('model', model)
|
||||
data.add_field('language', transcription_language)
|
||||
|
||||
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]}...")
|
||||
if use_timestamps:
|
||||
data.add_field('response_format', 'verbose_json')
|
||||
|
||||
is_summary = False
|
||||
if len(message) > 1000:
|
||||
logger.debug("Texto longo detectado, iniciando resumo")
|
||||
is_summary = True
|
||||
message = await summarize_text_if_needed(message)
|
||||
# Usar handle_groq_request para ter retry e validação
|
||||
success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True)
|
||||
if not success:
|
||||
raise Exception(f"Erro na transcrição: {error}")
|
||||
|
||||
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}")
|
||||
transcription = format_timestamped_result(response_data) if use_timestamps else response_data.get("text", "")
|
||||
|
||||
# Validar o conteúdo da transcrição
|
||||
if not await validate_transcription_response(transcription):
|
||||
storage.add_log("ERROR", "Transcrição vazia ou inválida recebida")
|
||||
raise Exception("Transcrição vazia ou inválida recebida")
|
||||
|
||||
# Detecção automática para novos contatos
|
||||
if (is_private and storage.get_auto_language_detection() and
|
||||
not from_me and not contact_language):
|
||||
try:
|
||||
detected_lang = await detect_language(transcription)
|
||||
storage.cache_language_detection(remote_jid, detected_lang)
|
||||
contact_language = detected_lang
|
||||
storage.add_log("INFO", "Idioma detectado e cacheado", {
|
||||
"language": detected_lang,
|
||||
"remote_jid": remote_jid
|
||||
})
|
||||
except Exception as e:
|
||||
storage.add_log("WARNING", "Erro na detecção de idioma", {"error": str(e)})
|
||||
|
||||
# Tradução quando necessário
|
||||
need_translation = (
|
||||
is_private and contact_language and
|
||||
(
|
||||
(from_me and transcription_language != target_language) or
|
||||
(not from_me and target_language != transcription_language)
|
||||
)
|
||||
)
|
||||
|
||||
if need_translation:
|
||||
try:
|
||||
transcription = await translate_text(
|
||||
transcription,
|
||||
transcription_language,
|
||||
target_language
|
||||
)
|
||||
storage.add_log("INFO", "Texto traduzido automaticamente", {
|
||||
"from": transcription_language,
|
||||
"to": target_language
|
||||
})
|
||||
except Exception as e:
|
||||
storage.add_log("ERROR", "Erro na tradução", {"error": str(e)})
|
||||
|
||||
# Registrar estatísticas de uso
|
||||
used_language = contact_language if contact_language else system_language
|
||||
storage.record_language_usage(
|
||||
used_language,
|
||||
from_me,
|
||||
bool(contact_language and contact_language != system_language)
|
||||
)
|
||||
|
||||
return transcription, use_timestamps
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro no processo de transcrição: {str(e)}", exc_info=settings.DEBUG_MODE)
|
||||
storage.add_log("ERROR", "Erro no processo de transcrição", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__
|
||||
})
|
||||
raise
|
||||
finally:
|
||||
# Limpar arquivos temporários
|
||||
if isinstance(audio_source, str) and os.path.exists(audio_source):
|
||||
try:
|
||||
os.unlink(audio_source)
|
||||
except Exception as e:
|
||||
storage.add_log("WARNING", "Erro ao remover arquivo temporário", {
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
def format_timestamped_result(result):
|
||||
"""
|
||||
Formata o resultado da transcrição com timestamps
|
||||
"""
|
||||
segments = result.get("segments", [])
|
||||
formatted_lines = []
|
||||
|
||||
for segment in segments:
|
||||
start_time = format_timestamp(segment.get("start", 0))
|
||||
end_time = format_timestamp(segment.get("end", 0))
|
||||
text = segment.get("text", "").strip()
|
||||
|
||||
if text:
|
||||
formatted_lines.append(f"[{start_time} -> {end_time}] {text}")
|
||||
|
||||
return "\n".join(formatted_lines)
|
||||
|
||||
def format_timestamp(seconds):
|
||||
"""
|
||||
Converte segundos em formato MM:SS
|
||||
"""
|
||||
minutes = int(seconds // 60)
|
||||
remaining_seconds = int(seconds % 60)
|
||||
return f"{minutes:02d}:{remaining_seconds:02d}"
|
||||
|
||||
# Função para detecção de idioma
|
||||
async def detect_language(text: str) -> str:
|
||||
"""
|
||||
Detecta o idioma do texto usando a API GROQ
|
||||
|
||||
Args:
|
||||
text: Texto para detectar idioma
|
||||
|
||||
Returns:
|
||||
str: Código ISO 639-1 do idioma detectado
|
||||
"""
|
||||
provider = storage.get_llm_provider()
|
||||
storage.add_log("DEBUG", "Iniciando detecção de idioma", {
|
||||
"text_length": len(text)
|
||||
})
|
||||
|
||||
# Lista de idiomas suportados
|
||||
SUPPORTED_LANGUAGES = {
|
||||
"pt", "en", "es", "fr", "de", "it", "ja", "ko",
|
||||
"zh", "ro", "ru", "ar", "hi", "nl", "pl", "tr"
|
||||
}
|
||||
if provider == "openai":
|
||||
api_key = storage.get_openai_keys()[0]
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
model = "gpt-4o-mini"
|
||||
else: # groq
|
||||
url = "https://api.groq.com/openai/v1/chat/completions"
|
||||
api_key = await get_working_groq_key(storage)
|
||||
if not api_key:
|
||||
raise Exception("Nenhuma chave GROQ disponível")
|
||||
model = "llama-3.3-70b-versatile"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# Prompt melhorado com exemplos e restrições
|
||||
prompt = """
|
||||
Analise o texto e retorne APENAS o código ISO 639-1 do idioma principal.
|
||||
Regras:
|
||||
1. Retorne APENAS o código de 2 letras
|
||||
2. Use somente códigos permitidos: pt, en, es, fr, de, it, ja, ko, zh, ro, ru, ar, hi, nl, pl, tr
|
||||
3. Se não tiver certeza ou o idioma não estiver na lista, retorne "en"
|
||||
4. Não inclua pontuação, espaços extras ou explicações
|
||||
|
||||
Exemplos corretos:
|
||||
"Hello world" -> en
|
||||
"Bonjour le monde" -> fr
|
||||
"Olá mundo" -> pt
|
||||
|
||||
Texto para análise:
|
||||
"""
|
||||
|
||||
json_data = {
|
||||
"messages": [{
|
||||
"role": "system",
|
||||
"content": "Você é um detector de idiomas preciso que retorna apenas códigos ISO 639-1."
|
||||
}, {
|
||||
"role": "user",
|
||||
"content": f"{prompt}\n\n{text[:500]}" # Limitando para os primeiros 500 caracteres
|
||||
}],
|
||||
"model": model,
|
||||
"temperature": 0.1
|
||||
}
|
||||
|
||||
try:
|
||||
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
|
||||
if not success:
|
||||
raise Exception(f"Falha na detecção de idioma: {error}")
|
||||
|
||||
detected_language = response_data["choices"][0]["message"]["content"].strip().lower()
|
||||
|
||||
# Validar o resultado
|
||||
if detected_language not in SUPPORTED_LANGUAGES:
|
||||
storage.add_log("WARNING", "Idioma detectado não suportado", {
|
||||
"detected": detected_language,
|
||||
"fallback": "en"
|
||||
})
|
||||
detected_language = "en"
|
||||
|
||||
storage.add_log("INFO", "Idioma detectado com sucesso", {
|
||||
"detected_language": detected_language
|
||||
})
|
||||
return detected_language
|
||||
|
||||
except Exception as e:
|
||||
storage.add_log("ERROR", "Erro no processo de detecção de idioma", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__
|
||||
})
|
||||
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}")
|
||||
storage.add_log("DEBUG", "Preparando envio de mensagem", {
|
||||
"remote_jid": remote_jid,
|
||||
"instance": instance
|
||||
})
|
||||
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")
|
||||
storage.add_log("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")
|
||||
storage.add_log("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")
|
||||
storage.add_log("INFO", "Mensagem enviada com sucesso", {
|
||||
"remote_jid": remote_jid
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erro no envio da mensagem: {str(e)}", exc_info=settings.DEBUG_MODE)
|
||||
storage.add_log("ERROR", "Erro no envio da mensagem", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__,
|
||||
"remote_jid": remote_jid
|
||||
})
|
||||
raise
|
||||
|
||||
def get_body_message_to_whatsapp_v1(message, remote_jid):
|
||||
@ -165,21 +579,32 @@ async def call_whatsapp(url, body, headers):
|
||||
"""Realiza chamada à API do WhatsApp"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
logger.debug(f"Enviando requisição para: {url}")
|
||||
storage.add_log("DEBUG", "Enviando requisição para WhatsApp", {
|
||||
"url": 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}")
|
||||
storage.add_log("ERROR", "Erro na API do WhatsApp", {
|
||||
"status": response.status,
|
||||
"error": error_text
|
||||
})
|
||||
return False
|
||||
logger.debug("Requisição bem-sucedida")
|
||||
storage.add_log("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)
|
||||
storage.add_log("ERROR", "Erro na chamada WhatsApp", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__
|
||||
})
|
||||
return False
|
||||
|
||||
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}")
|
||||
storage.add_log("DEBUG", "Obtendo áudio base64", {
|
||||
"message_id": message_id,
|
||||
"instance": instance
|
||||
})
|
||||
url = f"{server_url}/chat/getBase64FromMediaMessage/{instance}"
|
||||
headers = {"apikey": apikey}
|
||||
body = {"message": {"key": {"id": message_id}}, "convertToMp4": False}
|
||||
@ -189,12 +614,174 @@ async def get_audio_base64(server_url, instance, apikey, message_id):
|
||||
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")
|
||||
storage.add_log("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}")
|
||||
storage.add_log("ERROR", "Erro ao obter áudio base64", {
|
||||
"status": response.status,
|
||||
"error": 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
|
||||
storage.add_log("ERROR", "Erro na obtenção do áudio base64", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__,
|
||||
"message_id": message_id
|
||||
})
|
||||
raise
|
||||
|
||||
async def format_message(transcription_text, summary_text=None):
|
||||
"""Formata a mensagem baseado nas configurações."""
|
||||
settings = storage.get_message_settings()
|
||||
message_parts = []
|
||||
|
||||
# Determinar modo de saída
|
||||
output_mode = settings["output_mode"]
|
||||
char_limit = int(settings["character_limit"])
|
||||
|
||||
if output_mode == "smart":
|
||||
# Modo inteligente baseado no tamanho
|
||||
if len(transcription_text) > char_limit:
|
||||
if summary_text:
|
||||
message_parts.append(f"{settings['summary_header']}\n\n{summary_text}")
|
||||
else:
|
||||
message_parts.append(f"{settings['transcription_header']}\n\n{transcription_text}")
|
||||
elif output_mode == "summary_only":
|
||||
if summary_text:
|
||||
message_parts.append(f"{settings['summary_header']}\n\n{summary_text}")
|
||||
elif output_mode == "transcription_only":
|
||||
message_parts.append(f"{settings['transcription_header']}\n\n{transcription_text}")
|
||||
else: # both
|
||||
if summary_text:
|
||||
message_parts.append(f"{settings['summary_header']}\n\n{summary_text}")
|
||||
message_parts.append(f"{settings['transcription_header']}\n\n{transcription_text}")
|
||||
|
||||
# Adicionar mensagem de negócio
|
||||
message_parts.append(dynamic_settings['BUSINESS_MESSAGE'])
|
||||
|
||||
return "\n\n".join(message_parts)
|
||||
|
||||
async def translate_text(text: str, source_language: str, target_language: str) -> str:
|
||||
"""
|
||||
Traduz o texto usando a API GROQ
|
||||
|
||||
Args:
|
||||
text: Texto para traduzir
|
||||
source_language: Código ISO 639-1 do idioma de origem
|
||||
target_language: Código ISO 639-1 do idioma de destino
|
||||
|
||||
Returns:
|
||||
str: Texto traduzido
|
||||
"""
|
||||
provider = storage.get_llm_provider()
|
||||
storage.add_log("DEBUG", "Iniciando tradução", {
|
||||
"source_language": source_language,
|
||||
"target_language": target_language,
|
||||
"text_length": len(text)
|
||||
})
|
||||
|
||||
# Se os idiomas forem iguais, retorna o texto original
|
||||
if source_language == target_language:
|
||||
return text
|
||||
|
||||
if provider == "openai":
|
||||
api_key = storage.get_openai_keys()[0]
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
model = "gpt-4o-mini"
|
||||
else: # groq
|
||||
url = "https://api.groq.com/openai/v1/chat/completions"
|
||||
api_key = await get_working_groq_key(storage)
|
||||
if not api_key:
|
||||
raise Exception("Nenhuma chave GROQ disponível")
|
||||
model = "llama-3.3-70b-versatile"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
prompt = f"""
|
||||
Você é um tradutor profissional especializado em manter o tom e estilo do texto original.
|
||||
|
||||
Instruções:
|
||||
1. Traduza o texto de {source_language} para {target_language}
|
||||
2. Preserve todas as formatações (negrito, itálico, emojis)
|
||||
3. Mantenha os mesmos parágrafos e quebras de linha
|
||||
4. Preserve números, datas e nomes próprios
|
||||
5. Não adicione ou remova informações
|
||||
6. Não inclua notas ou explicações
|
||||
7. Mantenha o mesmo nível de formalidade
|
||||
|
||||
Texto para tradução:
|
||||
{text}
|
||||
"""
|
||||
|
||||
json_data = {
|
||||
"messages": [{
|
||||
"role": "system",
|
||||
"content": "Você é um tradutor profissional que mantém o estilo e formatação do texto original."
|
||||
}, {
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}],
|
||||
"model": model,
|
||||
"temperature": 0.3
|
||||
}
|
||||
|
||||
try:
|
||||
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
|
||||
if not success:
|
||||
raise Exception(f"Falha na tradução: {error}")
|
||||
|
||||
translated_text = response_data["choices"][0]["message"]["content"].strip()
|
||||
|
||||
# Verificar se a tradução manteve aproximadamente o mesmo tamanho
|
||||
length_ratio = len(translated_text) / len(text)
|
||||
if not (0.5 <= length_ratio <= 1.5):
|
||||
storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", {
|
||||
"original_length": len(text),
|
||||
"translated_length": len(translated_text),
|
||||
"ratio": length_ratio
|
||||
})
|
||||
|
||||
# Validar se a tradução não está vazia
|
||||
if not await validate_transcription_response(translated_text):
|
||||
storage.add_log("ERROR", "Tradução vazia ou inválida recebida")
|
||||
raise Exception("Tradução vazia ou inválida recebida")
|
||||
|
||||
storage.add_log("INFO", "Tradução concluída com sucesso", {
|
||||
"original_length": len(text),
|
||||
"translated_length": len(translated_text),
|
||||
"ratio": length_ratio
|
||||
})
|
||||
|
||||
return translated_text
|
||||
|
||||
except Exception as e:
|
||||
storage.add_log("ERROR", "Erro no processo de tradução", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__
|
||||
})
|
||||
raise
|
||||
|
||||
# Nova função para baixar áudio remoto
|
||||
async def download_remote_audio(url: str) -> str:
|
||||
"""
|
||||
Baixa um arquivo de áudio remoto e salva localmente como um arquivo temporário.
|
||||
Retorna o caminho para o arquivo salvo.
|
||||
"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
audio_data = await response.read()
|
||||
# Cria um arquivo temporário para armazenar o áudio (pode ajustar o sufixo caso necessário)
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
|
||||
temp_file.write(audio_data)
|
||||
local_path = temp_file.name
|
||||
return local_path
|
||||
else:
|
||||
raise Exception(f"Falha no download, código de status: {response.status}")
|
||||
except Exception as e:
|
||||
raise Exception(f"Erro ao baixar áudio remoto: {str(e)}")
|
54
start.sh
Normal file
54
start.sh
Normal file
@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Função para construir o comando redis-cli com autenticação condicional
|
||||
build_redis_cli_cmd() {
|
||||
cmd="redis-cli -h ${REDIS_HOST:-localhost} -p ${REDIS_PORT:-6380}"
|
||||
|
||||
if [ ! -z "$REDIS_USERNAME" ]; then
|
||||
cmd="$cmd --user $REDIS_USERNAME"
|
||||
fi
|
||||
|
||||
if [ ! -z "$REDIS_PASSWORD" ]; then
|
||||
cmd="$cmd -a $REDIS_PASSWORD"
|
||||
fi
|
||||
|
||||
if [ ! -z "$REDIS_DB" ]; then
|
||||
cmd="$cmd -n $REDIS_DB"
|
||||
fi
|
||||
|
||||
echo "$cmd"
|
||||
}
|
||||
|
||||
# Função para inicializar configurações no Redis
|
||||
initialize_redis_config() {
|
||||
redis_cmd=$(build_redis_cli_cmd)
|
||||
|
||||
$redis_cmd SET GROQ_API_KEY "sua_api_key_aqui" NX
|
||||
$redis_cmd SET BUSINESS_MESSAGE "*Impacte AI* Premium Services" NX
|
||||
$redis_cmd SET PROCESS_GROUP_MESSAGES "false" NX
|
||||
$redis_cmd SET PROCESS_SELF_MESSAGES "true" NX
|
||||
$redis_cmd SET API_DOMAIN "$API_DOMAIN" NX
|
||||
}
|
||||
|
||||
# Aguardar o Redis estar pronto
|
||||
echo "Aguardando o Redis ficar disponível..."
|
||||
redis_cmd=$(build_redis_cli_cmd)
|
||||
|
||||
until $redis_cmd PING 2>/dev/null; do
|
||||
echo "Redis não está pronto - aguardando..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "Redis disponível!"
|
||||
|
||||
# Inicializar configurações
|
||||
initialize_redis_config
|
||||
|
||||
# Iniciar o FastAPI em background
|
||||
uvicorn main:app --host 0.0.0.0 --port 8005 &
|
||||
|
||||
# Iniciar o Streamlit
|
||||
streamlit run manager.py --server.address 0.0.0.0 --server.port 8501
|
||||
|
||||
# Manter o script rodando
|
||||
wait
|
BIN
static/fluxo.png
Normal file
BIN
static/fluxo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
717
storage.py
Normal file
717
storage.py
Normal file
@ -0,0 +1,717 @@
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import traceback
|
||||
import logging
|
||||
import redis
|
||||
from utils import create_redis_client
|
||||
import uuid
|
||||
|
||||
class StorageHandler:
|
||||
# Chaves Redis para webhooks
|
||||
WEBHOOK_KEY = "webhook_redirects" # Chave para armazenar os webhooks
|
||||
WEBHOOK_STATS_KEY = "webhook_stats" # Chave para estatísticas
|
||||
|
||||
def __init__(self):
|
||||
# Configuração de logger
|
||||
self.logger = logging.getLogger("StorageHandler")
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
self.logger.addHandler(handler)
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
self.logger.info("StorageHandler inicializado.")
|
||||
|
||||
# Conexão com o Redis
|
||||
self.redis = create_redis_client()
|
||||
|
||||
# Retenção de logs e backups
|
||||
self.log_retention_hours = int(os.getenv('LOG_RETENTION_HOURS', 48))
|
||||
self.backup_retention_days = int(os.getenv('BACKUP_RETENTION_DAYS', 7))
|
||||
|
||||
# Garantir valores padrão para configurações de idioma
|
||||
if not self.redis.exists(self._get_redis_key("auto_translation")):
|
||||
self.redis.set(self._get_redis_key("auto_translation"), "false")
|
||||
|
||||
if not self.redis.exists(self._get_redis_key("auto_language_detection")):
|
||||
self.redis.set(self._get_redis_key("auto_language_detection"), "false")
|
||||
|
||||
def _get_redis_key(self, key):
|
||||
return f"transcrevezap:{key}"
|
||||
|
||||
def add_log(self, level: str, message: str, metadata: dict = None):
|
||||
log_entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": level,
|
||||
"message": message,
|
||||
"metadata": json.dumps(metadata) if metadata else None
|
||||
}
|
||||
self.redis.lpush(self._get_redis_key("logs"), json.dumps(log_entry))
|
||||
self.redis.ltrim(self._get_redis_key("logs"), 0, 999) # Manter apenas os últimos 1000 logs
|
||||
self.logger.log(getattr(logging, level.upper(), logging.INFO), f"{message} | Metadata: {metadata}")
|
||||
|
||||
def get_allowed_groups(self) -> List[str]:
|
||||
return self.redis.smembers(self._get_redis_key("allowed_groups"))
|
||||
|
||||
def add_allowed_group(self, group: str):
|
||||
self.redis.sadd(self._get_redis_key("allowed_groups"), group)
|
||||
|
||||
def remove_allowed_group(self, group: str):
|
||||
self.redis.srem(self._get_redis_key("allowed_groups"), group)
|
||||
|
||||
def get_blocked_users(self) -> List[str]:
|
||||
return self.redis.smembers(self._get_redis_key("blocked_users"))
|
||||
|
||||
def add_blocked_user(self, user: str):
|
||||
self.redis.sadd(self._get_redis_key("blocked_users"), user)
|
||||
|
||||
def remove_blocked_user(self, user: str):
|
||||
self.redis.srem(self._get_redis_key("blocked_users"), user)
|
||||
|
||||
def get_statistics(self) -> Dict:
|
||||
total_processed = int(self.redis.get(self._get_redis_key("total_processed")) or 0)
|
||||
last_processed = self.redis.get(self._get_redis_key("last_processed"))
|
||||
daily_count = json.loads(self.redis.get(self._get_redis_key("daily_count")) or "{}")
|
||||
group_count = json.loads(self.redis.get(self._get_redis_key("group_count")) or "{}")
|
||||
user_count = json.loads(self.redis.get(self._get_redis_key("user_count")) or "{}")
|
||||
error_count = int(self.redis.get(self._get_redis_key("error_count")) or 0)
|
||||
success_rate = float(self.redis.get(self._get_redis_key("success_rate")) or 100.0)
|
||||
|
||||
return {
|
||||
"total_processed": total_processed,
|
||||
"last_processed": last_processed,
|
||||
"stats": {
|
||||
"daily_count": daily_count,
|
||||
"group_count": group_count,
|
||||
"user_count": user_count,
|
||||
"error_count": error_count,
|
||||
"success_rate": success_rate,
|
||||
}
|
||||
}
|
||||
|
||||
def can_process_message(self, remote_jid):
|
||||
try:
|
||||
allowed_groups = self.get_allowed_groups()
|
||||
blocked_users = self.get_blocked_users()
|
||||
|
||||
if remote_jid in blocked_users:
|
||||
return False
|
||||
if "@g.us" in remote_jid and remote_jid not in allowed_groups:
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao verificar se pode processar mensagem: {e}")
|
||||
return False
|
||||
|
||||
def record_processing(self, remote_jid):
|
||||
try:
|
||||
# Incrementar total processado
|
||||
self.redis.incr(self._get_redis_key("total_processed"))
|
||||
|
||||
# Atualizar último processamento
|
||||
self.redis.set(self._get_redis_key("last_processed"), datetime.now().isoformat())
|
||||
|
||||
# Atualizar contagem diária
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
daily_count = json.loads(self.redis.get(self._get_redis_key("daily_count")) or "{}")
|
||||
daily_count[today] = daily_count.get(today, 0) + 1
|
||||
self.redis.set(self._get_redis_key("daily_count"), json.dumps(daily_count))
|
||||
|
||||
# Atualizar contagem de grupo ou usuário
|
||||
if "@g.us" in remote_jid:
|
||||
group_count = json.loads(self.redis.get(self._get_redis_key("group_count")) or "{}")
|
||||
group_count[remote_jid] = group_count.get(remote_jid, 0) + 1
|
||||
self.redis.set(self._get_redis_key("group_count"), json.dumps(group_count))
|
||||
else:
|
||||
user_count = json.loads(self.redis.get(self._get_redis_key("user_count")) or "{}")
|
||||
user_count[remote_jid] = user_count.get(remote_jid, 0) + 1
|
||||
self.redis.set(self._get_redis_key("user_count"), json.dumps(user_count))
|
||||
|
||||
# Atualizar taxa de sucesso
|
||||
total = int(self.redis.get(self._get_redis_key("total_processed")) or 0)
|
||||
errors = int(self.redis.get(self._get_redis_key("error_count")) or 0)
|
||||
success_rate = ((total - errors) / total) * 100 if total > 0 else 100
|
||||
self.redis.set(self._get_redis_key("success_rate"), success_rate)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao registrar processamento: {e}")
|
||||
|
||||
def record_error(self):
|
||||
self.redis.incr(self._get_redis_key("error_count"))
|
||||
|
||||
def clean_old_logs(self):
|
||||
try:
|
||||
cutoff_time = datetime.now() - timedelta(hours=self.log_retention_hours)
|
||||
logs = self.redis.lrange(self._get_redis_key("logs"), 0, -1)
|
||||
for log in logs:
|
||||
log_entry = json.loads(log)
|
||||
if datetime.fromisoformat(log_entry["timestamp"]) < cutoff_time:
|
||||
self.redis.lrem(self._get_redis_key("logs"), 0, log)
|
||||
else:
|
||||
break # Assumindo que os logs estão ordenados por tempo
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao limpar logs antigos: {e}")
|
||||
|
||||
def backup_data(self):
|
||||
try:
|
||||
data = {
|
||||
"allowed_groups": list(self.get_allowed_groups()),
|
||||
"blocked_users": list(self.get_blocked_users()),
|
||||
"statistics": self.get_statistics(),
|
||||
}
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_key = f"backup:{timestamp}"
|
||||
self.redis.set(backup_key, json.dumps(data))
|
||||
self.redis.expire(backup_key, self.backup_retention_days * 24 * 60 * 60) # Expira após os dias de retenção
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao criar backup: {e}")
|
||||
|
||||
def clean_old_backups(self):
|
||||
try:
|
||||
for key in self.redis.scan_iter("backup:*"):
|
||||
if self.redis.ttl(key) <= 0:
|
||||
self.redis.delete(key)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao limpar backups antigos: {e}")
|
||||
|
||||
# Método de rotação de chaves groq
|
||||
def get_groq_keys(self) -> List[str]:
|
||||
"""Obtém todas as chaves GROQ armazenadas."""
|
||||
return list(self.redis.smembers(self._get_redis_key("groq_keys")))
|
||||
|
||||
def add_groq_key(self, key: str):
|
||||
"""Adiciona uma nova chave GROQ ao conjunto."""
|
||||
if key and key.startswith("gsk_"):
|
||||
self.redis.sadd(self._get_redis_key("groq_keys"), key)
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_groq_key(self, key: str):
|
||||
"""Remove uma chave GROQ do conjunto."""
|
||||
self.redis.srem(self._get_redis_key("groq_keys"), key)
|
||||
|
||||
def get_next_groq_key(self) -> str:
|
||||
"""
|
||||
Obtém a próxima chave GROQ no sistema de rodízio.
|
||||
Utiliza um contador no Redis para controlar a rotação.
|
||||
"""
|
||||
keys = self.get_groq_keys()
|
||||
if not keys:
|
||||
return None
|
||||
# Obtém e incrementa o contador de rodízio
|
||||
counter = int(self.redis.get(self._get_redis_key("groq_key_counter")) or "0")
|
||||
next_counter = (counter + 1) % len(keys)
|
||||
self.redis.set(self._get_redis_key("groq_key_counter"), str(next_counter))
|
||||
|
||||
return keys[counter % len(keys)]
|
||||
|
||||
def get_penalized_until(self, key: str) -> Optional[datetime]:
|
||||
"""
|
||||
Retorna o timestamp até quando a chave está penalizada, ou None se não estiver penalizada.
|
||||
"""
|
||||
penalized_key = self._get_redis_key(f"groq_key_penalized_{key}")
|
||||
penalized_until = self.redis.get(penalized_key)
|
||||
if penalized_until:
|
||||
return datetime.fromisoformat(penalized_until)
|
||||
return None
|
||||
|
||||
def penalize_key(self, key: str, penalty_duration: int):
|
||||
"""
|
||||
Penaliza uma chave por um tempo determinado (em segundos).
|
||||
"""
|
||||
penalized_key = self._get_redis_key(f"groq_key_penalized_{key}")
|
||||
penalized_until = datetime.utcnow() + timedelta(seconds=penalty_duration)
|
||||
self.redis.set(penalized_key, penalized_until.isoformat())
|
||||
self.redis.expire(penalized_key, penalty_duration) # Expira a chave após o tempo de penalidade
|
||||
self.add_log("INFO", "Chave GROQ penalizada", {
|
||||
"key": key,
|
||||
"penalized_until": penalized_until.isoformat()
|
||||
})
|
||||
|
||||
def get_message_settings(self):
|
||||
"""Obtém as configurações de mensagens."""
|
||||
return {
|
||||
"summary_header": self.redis.get(self._get_redis_key("summary_header")) or "🤖 *Resumo do áudio:*",
|
||||
"transcription_header": self.redis.get(self._get_redis_key("transcription_header")) or "🔊 *Transcrição do áudio:*",
|
||||
"output_mode": self.redis.get(self._get_redis_key("output_mode")) or "both",
|
||||
"character_limit": int(self.redis.get(self._get_redis_key("character_limit")) or "500"),
|
||||
}
|
||||
|
||||
def save_message_settings(self, settings: dict):
|
||||
"""Salva as configurações de mensagens."""
|
||||
for key, value in settings.items():
|
||||
self.redis.set(self._get_redis_key(key), str(value))
|
||||
|
||||
def get_process_mode(self):
|
||||
"""Retorna o modo de processamento configurado"""
|
||||
mode = self.redis.get(self._get_redis_key("process_mode")) or "all"
|
||||
self.logger.debug(f"Modo de processamento atual: {mode}")
|
||||
return mode
|
||||
|
||||
def get_contact_language(self, contact_id: str) -> str:
|
||||
"""
|
||||
Obtém o idioma configurado para um contato específico.
|
||||
O contact_id pode vir com ou sem @s.whatsapp.net
|
||||
"""
|
||||
# Remover @s.whatsapp.net se presente
|
||||
contact_id = contact_id.split('@')[0]
|
||||
return self.redis.hget(self._get_redis_key("contact_languages"), contact_id)
|
||||
|
||||
def set_contact_language(self, contact_id: str, language: str):
|
||||
"""
|
||||
Define o idioma para um contato específico
|
||||
"""
|
||||
# Remover @s.whatsapp.net se presente
|
||||
contact_id = contact_id.split('@')[0]
|
||||
self.redis.hset(self._get_redis_key("contact_languages"), contact_id, language)
|
||||
self.logger.info(f"Idioma {language} definido para o contato {contact_id}")
|
||||
|
||||
def get_all_contact_languages(self) -> dict:
|
||||
"""
|
||||
Retorna um dicionário com todos os contatos e seus idiomas configurados
|
||||
"""
|
||||
return self.redis.hgetall(self._get_redis_key("contact_languages"))
|
||||
|
||||
def remove_contact_language(self, contact_id: str):
|
||||
"""
|
||||
Remove a configuração de idioma de um contato
|
||||
"""
|
||||
contact_id = contact_id.split('@')[0]
|
||||
self.redis.hdel(self._get_redis_key("contact_languages"), contact_id)
|
||||
self.logger.info(f"Configuração de idioma removida para o contato {contact_id}")
|
||||
|
||||
def get_auto_language_detection(self) -> bool:
|
||||
"""
|
||||
Verifica se a detecção automática de idioma está ativada
|
||||
"""
|
||||
return self.redis.get(self._get_redis_key("auto_language_detection")) == "true"
|
||||
|
||||
def set_auto_language_detection(self, enabled: bool):
|
||||
"""
|
||||
Ativa ou desativa a detecção automática de idioma
|
||||
"""
|
||||
self.redis.set(self._get_redis_key("auto_language_detection"), str(enabled).lower())
|
||||
self.logger.info(f"Detecção automática de idioma {'ativada' if enabled else 'desativada'}")
|
||||
|
||||
def get_auto_translation(self) -> bool:
|
||||
"""
|
||||
Verifica se a tradução automática está ativada
|
||||
"""
|
||||
return self.redis.get(self._get_redis_key("auto_translation")) == "true"
|
||||
|
||||
def set_auto_translation(self, enabled: bool):
|
||||
"""
|
||||
Ativa ou desativa a tradução automática
|
||||
"""
|
||||
self.redis.set(self._get_redis_key("auto_translation"), str(enabled).lower())
|
||||
self.logger.info(f"Tradução automática {'ativada' if enabled else 'desativada'}")
|
||||
|
||||
def record_language_usage(self, language: str, from_me: bool, auto_detected: bool = False):
|
||||
"""
|
||||
Registra estatísticas de uso de idiomas
|
||||
Args:
|
||||
language: Código do idioma (ex: 'pt', 'en')
|
||||
from_me: Se o áudio foi enviado por nós
|
||||
auto_detected: Se o idioma foi detectado automaticamente
|
||||
"""
|
||||
try:
|
||||
# Validar idioma
|
||||
if not language:
|
||||
self.add_log("WARNING", "Tentativa de registrar uso sem idioma definido")
|
||||
return
|
||||
|
||||
# Incrementar contagem total do idioma
|
||||
self.redis.hincrby(
|
||||
self._get_redis_key("language_stats"),
|
||||
f"{language}_total",
|
||||
1
|
||||
)
|
||||
|
||||
# Incrementar contagem por direção (enviado/recebido)
|
||||
direction = 'sent' if from_me else 'received'
|
||||
self.redis.hincrby(
|
||||
self._get_redis_key("language_stats"),
|
||||
f"{language}_{direction}",
|
||||
1
|
||||
)
|
||||
|
||||
# Se foi detecção automática, registrar
|
||||
if auto_detected:
|
||||
self.redis.hincrby(
|
||||
self._get_redis_key("language_stats"),
|
||||
f"{language}_auto_detected",
|
||||
1
|
||||
)
|
||||
|
||||
# Registrar última utilização
|
||||
self.redis.hset(
|
||||
self._get_redis_key("language_stats"),
|
||||
f"{language}_last_used",
|
||||
datetime.now().isoformat()
|
||||
)
|
||||
|
||||
# Log detalhado
|
||||
self.add_log("DEBUG", "Uso de idioma registrado", {
|
||||
"language": language,
|
||||
"direction": direction,
|
||||
"auto_detected": auto_detected
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
self.add_log("ERROR", "Erro ao registrar uso de idioma", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__
|
||||
})
|
||||
def get_language_statistics(self) -> Dict:
|
||||
"""
|
||||
Obtém estatísticas de uso de idiomas
|
||||
"""
|
||||
try:
|
||||
stats_raw = self.redis.hgetall(self._get_redis_key("language_stats"))
|
||||
|
||||
# Organizar estatísticas por idioma
|
||||
stats = {}
|
||||
for key, value in stats_raw.items():
|
||||
lang, metric = key.split('_', 1)
|
||||
|
||||
if lang not in stats:
|
||||
stats[lang] = {}
|
||||
|
||||
if metric == 'last_used':
|
||||
stats[lang][metric] = value
|
||||
else:
|
||||
stats[lang][metric] = int(value)
|
||||
|
||||
return stats
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao obter estatísticas de idioma: {e}")
|
||||
return {}
|
||||
|
||||
def cache_language_detection(self, contact_id: str, language: str, confidence: float = 1.0):
|
||||
"""
|
||||
Armazena em cache o idioma detectado para um contato
|
||||
"""
|
||||
contact_id = contact_id.split('@')[0]
|
||||
cache_data = {
|
||||
'language': language,
|
||||
'confidence': confidence,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'auto_detected': True
|
||||
}
|
||||
self.redis.hset(
|
||||
self._get_redis_key("language_detection_cache"),
|
||||
contact_id,
|
||||
json.dumps(cache_data)
|
||||
)
|
||||
|
||||
def get_cached_language(self, contact_id: str) -> Dict:
|
||||
"""
|
||||
Obtém o idioma em cache para um contato
|
||||
Retorna None se não houver cache ou se estiver expirado
|
||||
"""
|
||||
contact_id = contact_id.split('@')[0]
|
||||
cached = self.redis.hget(
|
||||
self._get_redis_key("language_detection_cache"),
|
||||
contact_id
|
||||
)
|
||||
|
||||
if not cached:
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(cached)
|
||||
# Verificar se o cache expirou (24 horas)
|
||||
cache_time = datetime.fromisoformat(data['timestamp'])
|
||||
if datetime.now() - cache_time > timedelta(hours=24):
|
||||
return None
|
||||
return data
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_webhook_redirects(self) -> List[Dict]:
|
||||
"""Obtém todos os webhooks de redirecionamento cadastrados."""
|
||||
webhooks_raw = self.redis.hgetall(self._get_redis_key("webhook_redirects"))
|
||||
webhooks = []
|
||||
|
||||
for webhook_id, data in webhooks_raw.items():
|
||||
webhook_data = json.loads(data)
|
||||
webhook_data['id'] = webhook_id
|
||||
webhooks.append(webhook_data)
|
||||
|
||||
return webhooks
|
||||
|
||||
def validate_webhook_url(self, url: str) -> bool:
|
||||
"""Valida se a URL do webhook é acessível."""
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
return all([parsed.scheme, parsed.netloc])
|
||||
except Exception as e:
|
||||
self.logger.error(f"URL inválida: {url} - {str(e)}")
|
||||
return False
|
||||
|
||||
def add_webhook_redirect(self, url: str, description: str = "") -> str:
|
||||
"""
|
||||
Adiciona um novo webhook de redirecionamento.
|
||||
Retorna o ID do webhook criado.
|
||||
"""
|
||||
webhook_id = str(uuid.uuid4())
|
||||
webhook_data = {
|
||||
"url": url,
|
||||
"description": description,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"status": "active",
|
||||
"error_count": 0,
|
||||
"success_count": 0,
|
||||
"last_success": None,
|
||||
"last_error": None
|
||||
}
|
||||
|
||||
self.redis.hset(
|
||||
self._get_redis_key("webhook_redirects"),
|
||||
webhook_id,
|
||||
json.dumps(webhook_data)
|
||||
)
|
||||
return webhook_id
|
||||
|
||||
def clean_webhook_data(self, webhook_id: str):
|
||||
"""
|
||||
Remove todos os dados relacionados a um webhook específico do Redis.
|
||||
|
||||
Args:
|
||||
webhook_id: ID do webhook a ser limpo
|
||||
"""
|
||||
try:
|
||||
# Lista de chaves relacionadas ao webhook que precisam ser removidas
|
||||
keys_to_remove = [
|
||||
f"webhook_failed_{webhook_id}", # Entregas falhas
|
||||
f"webhook_stats_{webhook_id}", # Estatísticas específicas
|
||||
]
|
||||
|
||||
# Remove cada chave associada ao webhook
|
||||
for key in keys_to_remove:
|
||||
full_key = self._get_redis_key(key)
|
||||
self.redis.delete(full_key)
|
||||
self.logger.debug(f"Chave removida: {full_key}")
|
||||
|
||||
self.logger.info(f"Dados do webhook {webhook_id} limpos com sucesso")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao limpar dados do webhook {webhook_id}: {str(e)}")
|
||||
raise
|
||||
|
||||
def remove_webhook_redirect(self, webhook_id: str):
|
||||
"""Remove um webhook de redirecionamento e todos os seus dados associados."""
|
||||
try:
|
||||
# Primeiro remove os dados associados
|
||||
self.clean_webhook_data(webhook_id)
|
||||
|
||||
# Depois remove o webhook em si
|
||||
self.redis.hdel(self._get_redis_key("webhook_redirects"), webhook_id)
|
||||
self.logger.info(f"Webhook {webhook_id} removido com sucesso")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao remover webhook {webhook_id}: {str(e)}")
|
||||
raise
|
||||
|
||||
def update_webhook_stats(self, webhook_id: str, success: bool, error_message: str = None):
|
||||
"""Atualiza as estatísticas de um webhook."""
|
||||
try:
|
||||
webhook_data = json.loads(
|
||||
self.redis.hget(self._get_redis_key("webhook_redirects"), webhook_id)
|
||||
)
|
||||
|
||||
if success:
|
||||
webhook_data["success_count"] += 1
|
||||
webhook_data["last_success"] = datetime.now().isoformat()
|
||||
else:
|
||||
webhook_data["error_count"] += 1
|
||||
webhook_data["last_error"] = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"message": error_message
|
||||
}
|
||||
|
||||
self.redis.hset(
|
||||
self._get_redis_key("webhook_redirects"),
|
||||
webhook_id,
|
||||
json.dumps(webhook_data)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao atualizar estatísticas do webhook {webhook_id}: {e}")
|
||||
|
||||
def retry_failed_webhooks(self):
|
||||
"""Tenta reenviar webhooks que falharam nas últimas 24h."""
|
||||
webhooks = self.get_webhook_redirects()
|
||||
for webhook in webhooks:
|
||||
if webhook.get("last_error"):
|
||||
error_time = datetime.fromisoformat(webhook["last_error"]["timestamp"])
|
||||
if datetime.now() - error_time < timedelta(hours=24):
|
||||
# Tentar reenviar
|
||||
pass
|
||||
|
||||
def test_webhook(self, url: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Testa um webhook antes de salvá-lo.
|
||||
Retorna uma tupla (sucesso, mensagem)
|
||||
"""
|
||||
try:
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
async def _test_webhook():
|
||||
async with aiohttp.ClientSession() as session:
|
||||
test_payload = {
|
||||
"test": True,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"message": "Teste de conexão do TranscreveZAP"
|
||||
}
|
||||
async with session.post(
|
||||
url,
|
||||
json=test_payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
) as response:
|
||||
return response.status, await response.text()
|
||||
|
||||
status, response = asyncio.run(_test_webhook())
|
||||
if status in [200, 201, 202]:
|
||||
return True, "Webhook testado com sucesso!"
|
||||
return False, f"Erro no teste: Status {status} - {response}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Erro ao testar webhook: {str(e)}"
|
||||
|
||||
def get_webhook_health(self, webhook_id: str) -> dict:
|
||||
"""
|
||||
Calcula métricas de saúde do webhook
|
||||
"""
|
||||
try:
|
||||
webhook_data = json.loads(
|
||||
self.redis.hget(self._get_redis_key("webhook_redirects"), webhook_id)
|
||||
)
|
||||
|
||||
total_requests = webhook_data["success_count"] + webhook_data["error_count"]
|
||||
if total_requests == 0:
|
||||
return {
|
||||
"health_status": "unknown",
|
||||
"error_rate": 0,
|
||||
"success_rate": 0,
|
||||
"total_requests": 0
|
||||
}
|
||||
|
||||
error_rate = (webhook_data["error_count"] / total_requests) * 100
|
||||
success_rate = (webhook_data["success_count"] / total_requests) * 100
|
||||
|
||||
# Definir status de saúde
|
||||
if error_rate >= 50:
|
||||
health_status = "critical"
|
||||
elif error_rate >= 20:
|
||||
health_status = "warning"
|
||||
else:
|
||||
health_status = "healthy"
|
||||
|
||||
return {
|
||||
"health_status": health_status,
|
||||
"error_rate": error_rate,
|
||||
"success_rate": success_rate,
|
||||
"total_requests": total_requests
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro ao calcular saúde do webhook {webhook_id}: {e}")
|
||||
return None
|
||||
|
||||
def retry_webhook(self, webhook_id: str, payload: dict) -> bool:
|
||||
"""
|
||||
Tenta reenviar um payload para um webhook específico mantendo o payload original intacto.
|
||||
|
||||
Args:
|
||||
webhook_id: ID do webhook para reenvio
|
||||
payload: Payload original para reenvio
|
||||
|
||||
Returns:
|
||||
bool: True se o reenvio foi bem sucedido, False caso contrário
|
||||
"""
|
||||
try:
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
webhook_data = json.loads(
|
||||
self.redis.hget(self._get_redis_key("webhook_redirects"), webhook_id)
|
||||
)
|
||||
|
||||
async def _retry_webhook():
|
||||
async with aiohttp.ClientSession() as session:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-TranscreveZAP-Forward": "true",
|
||||
"X-TranscreveZAP-Webhook-ID": webhook_id,
|
||||
"X-TranscreveZAP-Retry": "true"
|
||||
}
|
||||
|
||||
async with session.post(
|
||||
webhook_data["url"],
|
||||
json=payload, # Envia o payload original sem modificações
|
||||
headers=headers,
|
||||
timeout=10
|
||||
) as response:
|
||||
return response.status in [200, 201, 202]
|
||||
|
||||
success = asyncio.run(_retry_webhook())
|
||||
if success:
|
||||
self.update_webhook_stats(webhook_id, True)
|
||||
else:
|
||||
self.update_webhook_stats(webhook_id, False, "Falha no retry")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erro no retry do webhook {webhook_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_failed_deliveries(self, webhook_id: str) -> List[Dict]:
|
||||
"""
|
||||
Retorna lista de entregas falhas para um webhook
|
||||
"""
|
||||
key = self._get_redis_key(f"webhook_failed_{webhook_id}")
|
||||
failed = self.redis.lrange(key, 0, -1)
|
||||
return [json.loads(x) for x in failed]
|
||||
|
||||
def add_failed_delivery(self, webhook_id: str, payload: dict):
|
||||
"""
|
||||
Registra uma entrega falha para retry posterior
|
||||
"""
|
||||
key = self._get_redis_key(f"webhook_failed_{webhook_id}")
|
||||
failed_delivery = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"payload": payload,
|
||||
"retry_count": 0
|
||||
}
|
||||
self.redis.lpush(key, json.dumps(failed_delivery))
|
||||
# Manter apenas as últimas 100 falhas
|
||||
self.redis.ltrim(key, 0, 99)
|
||||
|
||||
def get_llm_provider(self) -> str:
|
||||
"""Returns active LLM provider (groq or openai)"""
|
||||
return self.redis.get(self._get_redis_key("active_llm_provider")) or "groq"
|
||||
|
||||
def set_llm_provider(self, provider: str):
|
||||
"""Sets active LLM provider"""
|
||||
if provider not in ["groq", "openai"]:
|
||||
raise ValueError("Provider must be 'groq' or 'openai'")
|
||||
self.redis.set(self._get_redis_key("active_llm_provider"), provider)
|
||||
|
||||
def get_openai_keys(self) -> List[str]:
|
||||
"""Get stored OpenAI API keys"""
|
||||
return list(self.redis.smembers(self._get_redis_key("openai_keys")))
|
||||
|
||||
def add_openai_key(self, key: str):
|
||||
"""Add OpenAI API key"""
|
||||
if key and key.startswith("sk-"):
|
||||
self.redis.sadd(self._get_redis_key("openai_keys"), key)
|
||||
return True
|
||||
return False
|
0
transcription_logs.json
Normal file
0
transcription_logs.json
Normal file
49
utils.py
Normal file
49
utils.py
Normal file
@ -0,0 +1,49 @@
|
||||
import os
|
||||
import redis
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("TranscreveZAP")
|
||||
|
||||
def get_redis_connection_params():
|
||||
"""
|
||||
Retorna os parâmetros de conexão do Redis baseado nas variáveis de ambiente.
|
||||
Retira parâmetros de autenticação se não estiverem configurados.
|
||||
"""
|
||||
params = {
|
||||
'host': os.getenv('REDIS_HOST', 'localhost'),
|
||||
'port': int(os.getenv('REDIS_PORT', 6380)),
|
||||
'db': int(os.getenv('REDIS_DB', '0')),
|
||||
'decode_responses': True
|
||||
}
|
||||
|
||||
# Adiciona credenciais apenas se estiverem configuradas
|
||||
username = os.getenv('REDIS_USERNAME')
|
||||
password = os.getenv('REDIS_PASSWORD')
|
||||
|
||||
if username and username.strip():
|
||||
params['username'] = username
|
||||
if password and password.strip():
|
||||
params['password'] = password
|
||||
|
||||
return params
|
||||
|
||||
def create_redis_client():
|
||||
"""
|
||||
Cria e testa a conexão com o Redis.
|
||||
Retorna o cliente Redis se bem sucedido.
|
||||
"""
|
||||
try:
|
||||
params = get_redis_connection_params()
|
||||
client = redis.Redis(**params)
|
||||
client.ping() # Testa a conexão
|
||||
logger.info("Conexão com Redis estabelecida com sucesso!")
|
||||
return client
|
||||
except redis.exceptions.AuthenticationError:
|
||||
logger.error("Falha de autenticação no Redis. Verifique as credenciais.")
|
||||
raise
|
||||
except redis.exceptions.ConnectionError as e:
|
||||
logger.error(f"Erro de conexão com Redis: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao configurar Redis: {e}")
|
||||
raise
|
Loading…
Reference in New Issue
Block a user