mudança no serviço, adição de redis, interface criada

This commit is contained in:
Fábio Cavalcanti 2024-12-11 23:15:54 -03:00
parent 08cc60128b
commit a299805fee
16 changed files with 807 additions and 200 deletions

View File

@ -3,3 +3,11 @@ docker-composer.yaml
docker-compose.yaml
.gitignore
.git
__pycache__
*.pyc
.git
.env
.venv
*.md
*.postman_collection.json
deploy_*.sh

View File

@ -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
# Comportamento da transcrição
PROCESS_SELF_MESSAGES=true
BUSINESS_MESSAGE="substitua_sua_mensagem_de_servico_aqui"
PROCESS_GROUP_MESSAGES=false
# Host e porta do Redis (caso esteja utilizando)
REDIS_HOST=localhost
REDIS_PORT=6379
DEBUG_MODE=true
# Debug e Logs
DEBUG_MODE=false
LOG_LEVEL=INFO
# Credenciais do Gerenciador
MANAGER_USER=admin
MANAGER_PASSWORD=impacteai2024
# Configurações do Servidor
FASTAPI_PORT=8005
STREAMLIT_PORT=8501
HOST=0.0.0.0

3
.gitignore vendored
View File

@ -3,3 +3,6 @@
docker-composer.yaml
GPT.postman_collection.json
.venv/
.gitignore
deploy_producao.sh
Dockerfile

View File

@ -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"]

129
config.py
View File

@ -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...")
"""Inicializa as configurações."""
logger.debug("Carregando configurações do Redis...")
# 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 = 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()
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}")
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}"
)
self.PROCESS_GROUP_MESSAGES = os.getenv('PROCESS_GROUP_MESSAGES', 'false').lower() == 'true'
logger.debug(f"PROCESS_GROUP_MESSAGES configurado como: {self.PROCESS_GROUP_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
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}")
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 = []
"""Validação detalhada das configurações críticas."""
logger.debug("Validando configurações...")
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_'")
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 validation_errors:
for error in validation_errors:
logger.error(f"Erro de validação: {error}")
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'
def load_settings():
"""
Recarrega as configurações do Redis.
"""
global settings
settings = Settings()
# Ajustar nível de log
log_level = logging.DEBUG if settings.DEBUG_MODE else getattr(logging, settings.LOG_LEVEL.upper())
log_level = getattr(logging, settings.LOG_LEVEL, logging.INFO)
logger.setLevel(log_level)
logger.info(f"Nível de log definido como: {logging.getLevelName(log_level)}")
logger.info(f"Nível de log ajustado para: {logging.getLevelName(log_level)}")

7
data/config.json Normal file
View 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
}

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 39 KiB

124
main.py
View File

@ -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,43 +63,65 @@ 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"}
# Verificar se temos mediaUrl ou precisamos pegar o base64
# Obter áudio
try:
if "mediaUrl" in body["data"]["message"]:
audio_source = body["data"]["message"]["mediaUrl"]
logger.debug(f"Usando mediaUrl: {audio_source}")
storage.add_log("DEBUG", "Usando mediaUrl para áudio", {
"mediaUrl": audio_source
})
else:
logger.debug("MediaUrl não encontrada, obtendo áudio via base64")
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)
logger.debug(f"Áudio convertido e salvo em: {audio_source}")
storage.add_log("DEBUG", "Áudio convertido", {
"source": audio_source
})
# Transcrever o áudio
# 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 a mensagem
# 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"{settings.BUSINESS_MESSAGE}"
f"{dynamic_settings['BUSINESS_MESSAGE']}"
)
logger.debug(f"Mensagem formatada: {summary_message[:100]}...")
# Enviar a mensagem formatada via WhatsApp
# Enviar resposta
await send_message_to_whatsapp(
server_url,
instance,
@ -76,12 +131,33 @@ async def transcreve_audios(request: Request):
audio_key,
)
logger.info("Áudio processado e resposta enviada com sucesso")
# 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)
})
return {"message": "Áudio transcrito e resposta enviada com sucesso"}
except Exception as e:
logger.error(f"Erro ao processar áudio: {str(e)}", exc_info=settings.DEBUG_MODE)
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 a requisição: {str(e)}",
detail=f"Erro ao processar áudio: {str(e)}"
)
except Exception as e:
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)}"
)

222
manager.py Normal file
View 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()

View File

@ -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

View File

@ -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"
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
temp_file.write(audio_data)
audio_file_path = temp_file.name
async with aiofiles.open(audio_file_path, "wb") as f:
await f.write(audio_data)
logger.debug(f"Arquivo temporário criado em: {audio_file_path}")
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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

171
storage.py Normal file
View 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
View File