mudança no serviço, adição de redis, interface criada
This commit is contained in:
parent
08cc60128b
commit
a299805fee
@ -2,4 +2,12 @@
|
||||
docker-composer.yaml
|
||||
docker-compose.yaml
|
||||
.gitignore
|
||||
.git
|
||||
.git
|
||||
__pycache__
|
||||
*.pyc
|
||||
.git
|
||||
.env
|
||||
.venv
|
||||
*.md
|
||||
*.postman_collection.json
|
||||
deploy_*.sh
|
22
.env.example
22
.env.example
@ -1,14 +1,12 @@
|
||||
# Chave da API para transcrição (Groq ou qualquer outro serviço que você utilizar)
|
||||
GROQ_API_KEY=substitua_sua_chave_GROQ_aqui
|
||||
# Debug e Logs
|
||||
DEBUG_MODE=false
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Comportamento da transcrição
|
||||
PROCESS_SELF_MESSAGES=true
|
||||
BUSINESS_MESSAGE="substitua_sua_mensagem_de_servico_aqui"
|
||||
PROCESS_GROUP_MESSAGES=false
|
||||
# Credenciais do Gerenciador
|
||||
MANAGER_USER=admin
|
||||
MANAGER_PASSWORD=impacteai2024
|
||||
|
||||
# Host e porta do Redis (caso esteja utilizando)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
DEBUG_MODE=true
|
||||
LOG_LEVEL=INFO
|
||||
# Configurações do Servidor
|
||||
FASTAPI_PORT=8005
|
||||
STREAMLIT_PORT=8501
|
||||
HOST=0.0.0.0
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -2,4 +2,7 @@
|
||||
*.pyc
|
||||
docker-composer.yaml
|
||||
GPT.postman_collection.json
|
||||
.venv/
|
||||
.venv/
|
||||
.gitignore
|
||||
deploy_producao.sh
|
||||
Dockerfile
|
26
Dockerfile
26
Dockerfile
@ -1,19 +1,31 @@
|
||||
# Usar uma imagem oficial do Python como base
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Instalar dependências do sistema
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Definir o diretório de trabalho
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar o requirements.txt e instalar dependências
|
||||
# Copiar o arquivo requirements.txt e instalar dependências
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copiar todo o código para dentro do contêiner
|
||||
# Copiar todo o código da aplicação
|
||||
COPY . .
|
||||
|
||||
# Expor a porta onde o FastAPI vai rodar
|
||||
EXPOSE 8005
|
||||
# Garantir que o diretório static existe
|
||||
RUN mkdir -p /app/static
|
||||
|
||||
# Comando para iniciar a aplicação
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8005"]
|
||||
# Copiar arquivos estáticos para o diretório apropriado
|
||||
COPY static/ /app/static/
|
||||
|
||||
# Garantir permissões de execução ao script inicial
|
||||
RUN chmod +x start.sh
|
||||
|
||||
# Expor as portas usadas pela aplicação
|
||||
EXPOSE 8005 8501
|
||||
|
||||
# Definir o comando inicial
|
||||
CMD ["./start.sh"]
|
149
config.py
149
config.py
@ -1,102 +1,111 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import redis
|
||||
import os
|
||||
|
||||
# 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 = redis.Redis(
|
||||
host=os.getenv('REDIS_HOST', 'localhost'),
|
||||
port=int(os.getenv('REDIS_PORT', 6380)),
|
||||
db=0,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
class Settings:
|
||||
"""Classe para gerenciar configurações do sistema."""
|
||||
def __init__(self):
|
||||
logger.debug("Iniciando carregamento das configurações...")
|
||||
|
||||
# Carregamento das variáveis com logs detalhados
|
||||
self.DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true'
|
||||
logger.debug(f"DEBUG_MODE configurado como: {self.DEBUG_MODE}")
|
||||
|
||||
self.GROQ_API_KEY = os.getenv('GROQ_API_KEY')
|
||||
"""Inicializa as configurações."""
|
||||
logger.debug("Carregando configurações do Redis...")
|
||||
|
||||
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.LOG_LEVEL = self.get_redis_value("LOG_LEVEL", "INFO").upper()
|
||||
|
||||
# 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,32 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
transcricaoaudio:
|
||||
tcaudio:
|
||||
image: impacteai/transcrevezap:latest
|
||||
build: .
|
||||
networks:
|
||||
- suarededocker #troque pela sua rede do docker
|
||||
- suarededocker
|
||||
ports:
|
||||
- 8005:8005
|
||||
- 8005:8005 # FastAPI
|
||||
- 8501:8501 # 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
|
||||
GROQ_API_KEY: "${GROQ_API_KEY}"
|
||||
BUSINESS_MESSAGE: "*Impacte AI* Premium Services"
|
||||
PROCESS_GROUP_MESSAGES: "false"
|
||||
PROCESS_SELF_MESSAGES: "true"
|
||||
DEBUG_MODE: "false"
|
||||
LOG_LEVEL: "INFO"
|
||||
MANAGER_USER: "admin"
|
||||
MANAGER_PASSWORD: "impacte2024"
|
||||
volumes:
|
||||
- ./config.json:/app/config.json
|
||||
- ./backups:/app/backups
|
||||
- ./transcription_logs.json:/app/transcription_logs.json
|
||||
- ./static:/app/static
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
@ -27,20 +35,22 @@ 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(`transcrevezap.seudominio.com.br`)
|
||||
- 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.transcrevezap.seudominio.com.br`)
|
||||
- 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
|
||||
|
||||
networks:
|
||||
suarededocker: #troque pela sua rede do docker
|
||||
suarededocker:
|
||||
external: true
|
||||
name: suarededocker #troque pela sua rede do docker
|
||||
name: suarededocker
|
||||
|
BIN
fluxo.png
BIN
fluxo.png
Binary file not shown.
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 39 KiB |
166
main.py
166
main.py
@ -7,19 +7,52 @@ from services import (
|
||||
summarize_text_if_needed,
|
||||
)
|
||||
from models import WebhookRequest
|
||||
import aiohttp
|
||||
from config import settings, logger
|
||||
from config import logger, settings, redis_client
|
||||
from storage import StorageHandler
|
||||
import traceback
|
||||
import os
|
||||
|
||||
app = FastAPI()
|
||||
storage = StorageHandler()
|
||||
|
||||
# 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",
|
||||
}
|
||||
|
||||
@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()
|
||||
|
||||
if settings.DEBUG_MODE:
|
||||
logger.debug(f"Payload recebido: {body}")
|
||||
# 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 dynamic_settings["DEBUG_MODE"]:
|
||||
storage.add_log("DEBUG", "Payload completo recebido", {
|
||||
"body": body
|
||||
})
|
||||
|
||||
# Extraindo informações
|
||||
server_url = body["server_url"]
|
||||
@ -30,58 +63,101 @@ 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"}
|
||||
|
||||
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"]:
|
||||
audio_source = body["data"]["message"]["mediaUrl"]
|
||||
storage.add_log("DEBUG", "Usando mediaUrl para áudio", {
|
||||
"mediaUrl": audio_source
|
||||
})
|
||||
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}")
|
||||
# Transcrever áudio
|
||||
storage.add_log("INFO", "Iniciando transcrição")
|
||||
transcription_text, _ = await transcribe_audio(audio_source)
|
||||
|
||||
# Resumir se necessário
|
||||
summary_text = await summarize_text_if_needed(transcription_text)
|
||||
|
||||
# Formatar 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"{dynamic_settings['BUSINESS_MESSAGE']}"
|
||||
)
|
||||
|
||||
# Transcrever o áudio
|
||||
transcription_text, _ = await transcribe_audio(audio_source)
|
||||
summary_text = await summarize_text_if_needed(transcription_text)
|
||||
# Enviar resposta
|
||||
await send_message_to_whatsapp(
|
||||
server_url,
|
||||
instance,
|
||||
apikey,
|
||||
summary_message,
|
||||
remote_jid,
|
||||
audio_key,
|
||||
)
|
||||
|
||||
# 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]}...")
|
||||
# Registrar sucesso
|
||||
storage.record_processing(remote_jid)
|
||||
storage.add_log("INFO", "Áudio processado com sucesso", {
|
||||
"remote_jid": remote_jid,
|
||||
"transcription_length": len(transcription_text),
|
||||
"summary_length": len(summary_text)
|
||||
})
|
||||
|
||||
# Enviar a mensagem formatada via WhatsApp
|
||||
await send_message_to_whatsapp(
|
||||
server_url,
|
||||
instance,
|
||||
apikey,
|
||||
summary_message,
|
||||
remote_jid,
|
||||
audio_key,
|
||||
)
|
||||
return {"message": "Áudio transcrito e resposta enviada com sucesso"}
|
||||
|
||||
logger.info("Áudio processado e resposta enviada com sucesso")
|
||||
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)}"
|
||||
)
|
222
manager.py
Normal file
222
manager.py
Normal file
@ -0,0 +1,222 @@
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from storage import StorageHandler
|
||||
import plotly.express as px
|
||||
import os
|
||||
import redis
|
||||
|
||||
# Conectar ao Redis
|
||||
redis_client = redis.Redis(host=os.getenv('REDIS_HOST', 'localhost'), port=int(os.getenv('REDIS_PORT', 6380)), decode_responses=True)
|
||||
|
||||
# Função para salvar configurações no Redis
|
||||
def save_to_redis(key, value):
|
||||
try:
|
||||
redis_client.set(key, value)
|
||||
st.success(f"Configuração {key} salva com sucesso!")
|
||||
except Exception as e:
|
||||
st.error(f"Erro ao salvar no Redis: {key} -> {e}")
|
||||
|
||||
# Função para buscar configurações no Redis
|
||||
def get_from_redis(key, default=None):
|
||||
try:
|
||||
value = redis_client.get(key)
|
||||
return value if value is not None else default
|
||||
except Exception as e:
|
||||
st.error(f"Erro ao buscar no Redis: {key} -> {e}")
|
||||
return default
|
||||
|
||||
# Configuração da página
|
||||
st.set_page_config(
|
||||
page_title="TranscreveZAP by Impacte AI",
|
||||
page_icon="🎙️",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# Configuração do storage
|
||||
storage = StorageHandler()
|
||||
|
||||
# Função para carregar configurações do Redis para o Streamlit
|
||||
def load_settings():
|
||||
try:
|
||||
st.session_state.settings = {
|
||||
"GROQ_API_KEY": get_from_redis("GROQ_API_KEY", "default_key"),
|
||||
"BUSINESS_MESSAGE": get_from_redis("BUSINESS_MESSAGE", "*Impacte AI* Premium Services"),
|
||||
"PROCESS_GROUP_MESSAGES": get_from_redis("PROCESS_GROUP_MESSAGES", "false"),
|
||||
"PROCESS_SELF_MESSAGES": get_from_redis("PROCESS_SELF_MESSAGES", "true"),
|
||||
}
|
||||
except Exception as e:
|
||||
st.error(f"Erro ao carregar configurações do Redis: {e}")
|
||||
|
||||
# Carregar configurações na sessão, se necessário
|
||||
if "settings" not in st.session_state:
|
||||
load_settings()
|
||||
|
||||
# Função para salvar configurações do Streamlit no Redis
|
||||
def save_settings():
|
||||
try:
|
||||
save_to_redis("GROQ_API_KEY", st.session_state.groq_api_key)
|
||||
save_to_redis("BUSINESS_MESSAGE", st.session_state.business_message)
|
||||
save_to_redis("PROCESS_GROUP_MESSAGES", st.session_state.process_group_messages)
|
||||
save_to_redis("PROCESS_SELF_MESSAGES", st.session_state.process_self_messages)
|
||||
st.success("Configurações salvas com sucesso!")
|
||||
except Exception as e:
|
||||
st.error(f"Erro ao salvar configurações: {e}")
|
||||
|
||||
def show_logo():
|
||||
try:
|
||||
logo_path = os.path.join(os.path.dirname(__file__), "static", "fluxo.png")
|
||||
if os.path.exists(logo_path):
|
||||
st.image(logo_path, width=150)
|
||||
else:
|
||||
st.warning("Logo não encontrada.")
|
||||
except Exception as e:
|
||||
st.error(f"Erro ao carregar logo: {e}")
|
||||
|
||||
def show_footer():
|
||||
st.markdown(
|
||||
"""
|
||||
<div class="footer">
|
||||
<p>Desenvolvido por <a href="https://impacte.ai" target="_blank">Impacte AI</a> |
|
||||
Código fonte no <a href="https://github.com/impacte-ai/transcrevezap" target="_blank">GitHub</a></p>
|
||||
</div>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
def login_page():
|
||||
show_logo()
|
||||
col1, col2, col3 = st.columns([1, 2, 1])
|
||||
with col2:
|
||||
with st.form("login_form"):
|
||||
st.markdown("<h2 style='text-align: center;'>Login</h2>", unsafe_allow_html=True)
|
||||
username = st.text_input('Usuário')
|
||||
password = st.text_input('Senha', type='password')
|
||||
if st.form_submit_button('Entrar', use_container_width=True):
|
||||
if username == os.getenv('MANAGER_USER') and password == os.getenv('MANAGER_PASSWORD'):
|
||||
st.session_state.authenticated = True
|
||||
st.experimental_rerun()
|
||||
else:
|
||||
st.error('Credenciais inválidas')
|
||||
|
||||
def dashboard():
|
||||
show_logo()
|
||||
st.sidebar.markdown('<div class="sidebar-header">TranscreveZAP - Menu</div>', unsafe_allow_html=True)
|
||||
page = st.sidebar.radio(
|
||||
"Navegação",
|
||||
["📊 Painel de Controle", "👥 Gerenciar Grupos", "🚫 Gerenciar Bloqueios", "⚙️ Configurações"]
|
||||
)
|
||||
if st.sidebar.button("Sair"):
|
||||
st.session_state.authenticated = False
|
||||
st.experimental_rerun()
|
||||
|
||||
if page == "📊 Painel de Controle":
|
||||
show_statistics()
|
||||
elif page == "👥 Gerenciar Grupos":
|
||||
manage_groups()
|
||||
elif page == "🚫 Gerenciar Bloqueios":
|
||||
manage_blocks()
|
||||
elif page == "⚙️ Configurações":
|
||||
manage_settings()
|
||||
|
||||
def show_statistics():
|
||||
st.title("📊 Painel de Controle")
|
||||
try:
|
||||
stats = storage.get_statistics()
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("Total de Áudios Processados", stats.get("total_processed", 0))
|
||||
with col2:
|
||||
last_processed = stats.get("last_processed", "Nunca")
|
||||
st.metric("Último Processamento", last_processed)
|
||||
with col3:
|
||||
total_groups = len(storage.get_allowed_groups())
|
||||
st.metric("Grupos Permitidos", total_groups)
|
||||
|
||||
daily_data = stats["stats"]["daily_count"]
|
||||
if daily_data:
|
||||
df = pd.DataFrame(list(daily_data.items()), columns=['Data', 'Processamentos'])
|
||||
df['Data'] = pd.to_datetime(df['Data'])
|
||||
fig = px.line(df, x='Data', y='Processamentos', title='Processamentos por Dia')
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
else:
|
||||
st.info("Ainda não há dados de processamento disponíveis.")
|
||||
except Exception as e:
|
||||
st.error(f"Erro ao carregar estatísticas: {e}")
|
||||
|
||||
def manage_groups():
|
||||
st.title("👥 Gerenciar Grupos")
|
||||
st.subheader("Adicionar Grupo Permitido")
|
||||
col1, col2 = st.columns([3, 1])
|
||||
with col1:
|
||||
new_group = st.text_input("Número do Grupo", placeholder="Ex: 5521999999999")
|
||||
with col2:
|
||||
if st.button("Adicionar"):
|
||||
formatted_group = f"{new_group}@g.us"
|
||||
storage.add_allowed_group(formatted_group)
|
||||
st.success(f"Grupo {formatted_group} adicionado com sucesso!")
|
||||
st.experimental_rerun()
|
||||
|
||||
st.subheader("Grupos Permitidos")
|
||||
allowed_groups = storage.get_allowed_groups()
|
||||
if allowed_groups:
|
||||
for group in allowed_groups:
|
||||
col1, col2 = st.columns([4, 1])
|
||||
with col1:
|
||||
st.text(group)
|
||||
with col2:
|
||||
if st.button("Remover", key=f"remove_{group}"):
|
||||
storage.remove_allowed_group(group)
|
||||
st.success(f"Grupo {group} removido!")
|
||||
st.experimental_rerun()
|
||||
else:
|
||||
st.info("Nenhum grupo permitido.")
|
||||
|
||||
def manage_blocks():
|
||||
st.title("🚫 Gerenciar Bloqueios")
|
||||
st.subheader("Bloquear Usuário")
|
||||
col1, col2 = st.columns([3, 1])
|
||||
with col1:
|
||||
new_user = st.text_input("Número do Usuário", placeholder="Ex: 5521999999999")
|
||||
with col2:
|
||||
if st.button("Bloquear"):
|
||||
formatted_user = f"{new_user}@s.whatsapp.net"
|
||||
storage.add_blocked_user(formatted_user)
|
||||
st.success(f"Usuário {formatted_user} bloqueado!")
|
||||
st.experimental_rerun()
|
||||
|
||||
st.subheader("Usuários Bloqueados")
|
||||
blocked_users = storage.get_blocked_users()
|
||||
if blocked_users:
|
||||
for user in blocked_users:
|
||||
col1, col2 = st.columns([4, 1])
|
||||
with col1:
|
||||
st.text(user)
|
||||
with col2:
|
||||
if st.button("Desbloquear", key=f"unblock_{user}"):
|
||||
storage.remove_blocked_user(user)
|
||||
st.success(f"Usuário {user} desbloqueado!")
|
||||
st.experimental_rerun()
|
||||
else:
|
||||
st.info("Nenhum usuário bloqueado.")
|
||||
|
||||
def manage_settings():
|
||||
st.title("⚙️ Configurações")
|
||||
st.subheader("Configurações do Sistema")
|
||||
st.text_input("GROQ_API_KEY", value=st.session_state.settings["GROQ_API_KEY"], key="groq_api_key")
|
||||
st.text_input("Mensagem de Boas-Vindas", value=st.session_state.settings["BUSINESS_MESSAGE"], key="business_message")
|
||||
st.selectbox("Processar Mensagens em Grupos", options=["true", "false"], index=["true", "false"].index(st.session_state.settings["PROCESS_GROUP_MESSAGES"]), key="process_group_messages")
|
||||
st.selectbox("Processar Mensagens Próprias", options=["true", "false"], index=["true", "false"].index(st.session_state.settings["PROCESS_SELF_MESSAGES"]), key="process_self_messages")
|
||||
if st.button("Salvar Configurações"):
|
||||
save_settings()
|
||||
|
||||
if "authenticated" not in st.session_state:
|
||||
st.session_state.authenticated = False
|
||||
|
||||
if st.session_state.authenticated:
|
||||
dashboard()
|
||||
else:
|
||||
login_page()
|
||||
|
||||
show_footer()
|
@ -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
|
149
services.py
149
services.py
@ -2,27 +2,40 @@ 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
|
||||
|
||||
# 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 summarize_text_if_needed(text):
|
||||
"""Resumir texto usando a API GROQ"""
|
||||
logger.debug("Iniciando processo de resumo do texto")
|
||||
storage.add_log("DEBUG", "Iniciando processo de resumo", {
|
||||
"text_length": len(text)
|
||||
})
|
||||
|
||||
url_completions = "https://api.groq.com/openai/v1/chat/completions"
|
||||
headers = {
|
||||
@ -50,25 +63,33 @@ async def summarize_text_if_needed(text):
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
logger.debug("Enviando requisição para API GROQ")
|
||||
storage.add_log("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]}...")
|
||||
storage.add_log("INFO", "Resumo gerado com sucesso", {
|
||||
"original_length": len(text),
|
||||
"summary_length": len(summary_text)
|
||||
})
|
||||
return summary_text
|
||||
else:
|
||||
error_text = await summary_response.text()
|
||||
logger.error(f"Erro na API GROQ: {error_text}")
|
||||
storage.add_log("ERROR", "Erro na API GROQ", {
|
||||
"error": error_text,
|
||||
"status": summary_response.status
|
||||
})
|
||||
raise Exception(f"Erro ao resumir o texto: {error_text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erro no processo de resumo: {str(e)}", exc_info=settings.DEBUG_MODE)
|
||||
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")
|
||||
storage.add_log("INFO", "Iniciando processo de transcrição")
|
||||
url = "https://api.groq.com/openai/v1/audio/transcriptions"
|
||||
groq_headers = {"Authorization": f"Bearer {settings.GROQ_API_KEY}"}
|
||||
|
||||
@ -76,21 +97,27 @@ async def transcribe_audio(audio_source, apikey=None):
|
||||
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}")
|
||||
storage.add_log("DEBUG", "Baixando áudio da URL", {
|
||||
"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}")
|
||||
storage.add_log("ERROR", "Erro no download do áudio", {
|
||||
"status": response.status,
|
||||
"error": 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}")
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
|
||||
temp_file.write(audio_data)
|
||||
audio_source = temp_file.name
|
||||
storage.add_log("DEBUG", "Áudio salvo temporariamente", {
|
||||
"path": audio_source
|
||||
})
|
||||
|
||||
# Preparar dados para transcrição
|
||||
data = aiohttp.FormData()
|
||||
@ -98,51 +125,73 @@ async def transcribe_audio(audio_source, apikey=None):
|
||||
data.add_field('model', 'whisper-large-v3')
|
||||
data.add_field('language', 'pt')
|
||||
|
||||
logger.debug("Enviando áudio para transcrição")
|
||||
storage.add_log("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]}...")
|
||||
storage.add_log("INFO", "Transcrição concluída com sucesso", {
|
||||
"text_length": len(message)
|
||||
})
|
||||
|
||||
is_summary = False
|
||||
if len(message) > 1000:
|
||||
logger.debug("Texto longo detectado, iniciando resumo")
|
||||
storage.add_log("DEBUG", "Texto longo detectado, iniciando resumo", {
|
||||
"text_length": len(message)
|
||||
})
|
||||
is_summary = True
|
||||
message = await summarize_text_if_needed(message)
|
||||
|
||||
return message, is_summary
|
||||
else:
|
||||
error_text = await response.text()
|
||||
logger.error(f"Erro na transcrição: {error_text}")
|
||||
storage.add_log("ERROR", "Erro na transcrição", {
|
||||
"error": error_text,
|
||||
"status": response.status
|
||||
})
|
||||
raise Exception(f"Erro na transcrição: {error_text}")
|
||||
|
||||
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):
|
||||
os.unlink(audio_source)
|
||||
|
||||
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 +214,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 +249,19 @@ 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)
|
||||
storage.add_log("ERROR", "Erro na obtenção do áudio base64", {
|
||||
"error": str(e),
|
||||
"type": type(e).__name__,
|
||||
"message_id": message_id
|
||||
})
|
||||
raise
|
22
start.sh
Normal file
22
start.sh
Normal file
@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Função para inicializar configurações no Redis
|
||||
initialize_redis_config() {
|
||||
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET GROQ_API_KEY "sua_api_key_aqui"
|
||||
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET BUSINESS_MESSAGE "*Impacte AI* Premium Services"
|
||||
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET PROCESS_GROUP_MESSAGES "false"
|
||||
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET PROCESS_SELF_MESSAGES "true"
|
||||
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET DEBUG_MODE "false"
|
||||
}
|
||||
|
||||
# Inicializar configurações no Redis
|
||||
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: 39 KiB |
171
storage.py
Normal file
171
storage.py
Normal file
@ -0,0 +1,171 @@
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict
|
||||
from datetime import datetime, timedelta
|
||||
import traceback
|
||||
import logging
|
||||
import redis
|
||||
|
||||
class StorageHandler:
|
||||
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 = redis.Redis(
|
||||
host=os.getenv('REDIS_HOST', 'localhost'),
|
||||
port=int(os.getenv('REDIS_PORT', 6380)),
|
||||
db=0,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
# 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))
|
||||
|
||||
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}")
|
0
transcription_logs.json
Normal file
0
transcription_logs.json
Normal file
Loading…
Reference in New Issue
Block a user