30 Commits
2.0 ... 2.3

Author SHA1 Message Date
Fábio Cavalcanti
bac9469b05 ajuste docker standalone no readme 2025-01-10 10:27:10 -03:00
Fábio Cavalcanti
943d5be2c8 orientações do readme 2025-01-08 20:52:40 -03:00
Fábio Cavalcanti
ae74686c9b ajuste do logoff 2025-01-08 20:25:04 -03:00
Fábio Cavalcanti
2fadd723dc adicionado sessão de login persistente 2025-01-08 19:06:55 -03:00
Fábio Cavalcanti
2a296d759f correção de detecção automatica 2025-01-08 17:16:39 -03:00
Fábio Cavalcanti
f558542359 ajuste de função de tradução simultanea 2025-01-08 13:26:08 -03:00
Fábio Cavalcanti
6a9ba1f087 ajustes de tradução e fuso horario 2025-01-07 18:10:27 -03:00
Fábio Cavalcanti
b86c7ac764 adicionado funções de detalhamento de idiomas e comportamento de tradutor automatico 2025-01-07 15:44:59 -03:00
Fábio Cavalcanti
9a072aee22 corrigido modo de processamento 2025-01-07 13:50:24 -03:00
Fábio Cavalcanti
eeffecb091 adicionado configuração de modo de processamento de mensagens apenas em grupos ou grupos e privado 2025-01-07 13:35:44 -03:00
Fábio Cavalcanti
69cb3b1965 personalização de comportamento e mensagens de transcrição 2025-01-07 11:36:39 -03:00
Fábio Cavalcanti
ec65839beb correção na lógica de resumos 2025-01-07 10:15:56 -03:00
Fábio Cavalcanti
abc4c4298a romeno no readme 2024-12-19 18:08:40 -03:00
Fábio Cavalcanti
161251a403 adicionado Romeno as linguagens novas 2024-12-19 18:05:59 -03:00
Fábio Cavalcanti
97cc842eb8 Adicionado rotação de chaves groq e seleção de idioma de tradução 2024-12-19 17:34:26 -03:00
Fábio Cavalcanti
ffd916c855 Merge branch 'dev' 2024-12-18 16:14:58 -03:00
Fábio Cavalcanti
fede0057e5 ajuste prompt 2024-12-18 16:11:04 -03:00
Fábio Cavalcanti
345cc26186 ajuste readme 2024-12-12 23:27:53 -03:00
Fábio Cavalcanti
a646e724f6 ajuste readme 2024-12-12 23:26:32 -03:00
Fábio Cavalcanti
c88c014e86 ajuste readme 2024-12-12 23:25:27 -03:00
Fábio Cavalcanti
c72c143609 Ajuste de orientação de uso 2024-12-12 21:04:44 -03:00
Fábio Cavalcanti
64f7f64b17 ajuste de orientação de uso 2024-12-12 21:04:03 -03:00
Fábio Cavalcanti
8e775b7379 Corrigido chamada para buscar grupos 2024-12-12 19:57:04 -03:00
Fábio Cavalcanti
374169e56f ajuste na busca de grupos para permitir transcrição 2024-12-12 19:54:13 -03:00
Fábio Cavalcanti
d6bbb5bc6e correções de variaveis de sistema e redis 2024-12-12 19:28:30 -03:00
Fábio Cavalcanti
d0c3ffca09 readme igual ao do main 2024-12-12 19:01:38 -03:00
Fábio Cavalcanti
1f40b128fa Melhorias de Inicialização do Serviço Docker 2024-12-12 18:58:48 -03:00
Fábio Cavalcanti
b3753e768c ajustes de inicialização, adicionado orientações de uso da API no Manager 2024-12-12 18:52:35 -03:00
Fábio Cavalcanti
b6e3ea8ec3 adicionado orientação de uso da api na interface 2024-12-12 18:30:26 -03:00
Fábio Cavalcanti
bb63c590e4 branch 2.0 no readme 2024-12-12 17:36:20 -03:00
11 changed files with 1758 additions and 200 deletions

View File

@@ -11,4 +11,5 @@ __pycache__
*.md
*.postman_collection.json
deploy_*.sh
manager_atualizar.py
manager_atualizar.py
roadmap.md

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ GPT.postman_collection.json
.gitignore
deploy_producao.sh
Dockerfile
manager_atualizar.py
manager_atualizar.py
roadmap.md

View File

@@ -1,10 +1,16 @@
# Usar uma imagem oficial do Python como base
FROM python:3.10-slim
# Instalar dependências do sistema
# Instalar dependências do sistema, incluindo redis-tools e tzdata para fuso horário
RUN apt-get update && apt-get install -y --no-install-recommends \
redis-tools \
tzdata \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Configurar o fuso horário
ENV TZ=America/Sao_Paulo
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Definir o diretório de trabalho
WORKDIR /app
@@ -22,10 +28,14 @@ RUN mkdir -p /app/static
COPY static/ /app/static/
# Garantir permissões de execução ao script inicial
COPY start.sh .
RUN chmod +x start.sh
# Converter possíveis caracteres de retorno de carro do Windows
RUN apt-get update && apt-get install -y dos2unix && dos2unix start.sh && apt-get remove -y dos2unix && apt-get autoremove -y && apt-get clean
# Expor as portas usadas pela aplicação
EXPOSE 8005 8501
# Definir o comando inicial
CMD ["./start.sh"]
CMD ["/bin/bash", "/app/start.sh"]

View File

@@ -47,8 +47,10 @@ class Settings:
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.DEBUG_MODE = os.getenv("DEBUG_MODE", "false").lower() == "true"
self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
self.TRANSCRIPTION_LANGUAGE = self.get_redis_value("TRANSCRIPTION_LANGUAGE", "pt")
# Mascarar chave ao logar
if self.GROQ_API_KEY:
masked_key = f"{self.GROQ_API_KEY[:10]}...{self.GROQ_API_KEY[-4:]}"

View File

@@ -4,7 +4,7 @@ services:
tcaudio:
image: impacteai/transcrevezap:latest
networks:
- transcrevezap_network
- sua_rede_externa # Substitua pelo nome da sua rede externa
ports:
- 8005:8005 # Porta para FastAPI
- 8501:8501 # Porta para Streamlit
@@ -13,12 +13,13 @@ services:
- UVICORN_HOST=0.0.0.0
- UVICORN_RELOAD=true
- UVICORN_WORKERS=1
- API_DOMAIN=seu.dominio.com #coloque seu subdominio da API apontado aqui
- DEBUG_MODE=false
- LOG_LEVEL=INFO
- MANAGER_USER=seu_usuario_admin
- MANAGER_PASSWORD=sua_senha_segura
- MANAGER_USER=seu_usuario_admin # Defina Usuário do Manager
- MANAGER_PASSWORD=sua_senha_segura # Defina Senha do Manager
- REDIS_HOST=redis-transcrevezap
- REDIS_PORT=6380
- REDIS_PORT=6380 # Porta personalizada para o Redis do TranscreveZAP
depends_on:
- redis-transcrevezap
deploy:
@@ -29,7 +30,7 @@ services:
- node.role == manager
labels:
- traefik.enable=true
- traefik.http.routers.tcaudio.rule=Host(`seu.dominio.com`)
- traefik.http.routers.tcaudio.rule=Host(`seu.dominio.com`) #coloque seu subdominio da API apontado aqui
- traefik.http.routers.tcaudio.entrypoints=websecure
- traefik.http.routers.tcaudio.tls.certresolver=letsencryptresolver
- traefik.http.services.tcaudio.loadbalancer.server.port=8005
@@ -38,7 +39,7 @@ services:
- traefik.http.middlewares.traefik-compress.compress=true
- traefik.http.routers.tcaudio.middlewares=traefik-compress
# Configuração do Streamlit
- traefik.http.routers.tcaudio-manager.rule=Host(`manager.seu.dominio.com`)
- traefik.http.routers.tcaudio-manager.rule=Host(`manager.seu.dominio.com`) #coloque seu subdominio do Manager apontado aqui
- traefik.http.routers.tcaudio-manager.entrypoints=websecure
- traefik.http.routers.tcaudio-manager.tls.certresolver=letsencryptresolver
- traefik.http.services.tcaudio-manager.loadbalancer.server.port=8501
@@ -51,10 +52,10 @@ services:
volumes:
- redis_transcrevezap_data:/data
networks:
- transcrevezap_network
- sua_rede_externa # Substitua pelo nome da sua rede externa
networks:
transcrevezap_network:
sua_rede_externa: # Substitua pelo nome da sua rede externa
external: true
name: sua_rede_externa # Substitua pelo nome da sua rede externa

86
main.py
View File

@@ -14,7 +14,10 @@ import os
app = FastAPI()
storage = StorageHandler()
@app.on_event("startup")
async def startup_event():
api_domain = os.getenv("API_DOMAIN", "seu.dominio.com")
redis_client.set("API_DOMAIN", api_domain)
# Função para buscar configurações do Redis com fallback para valores padrão
def get_config(key, default=None):
try:
@@ -84,6 +87,18 @@ async def transcreve_audios(request: Request):
)
return {"message": "Mensagem não autorizada para processamento"}
# Verificação do modo de processamento (grupos/todos)
process_mode = storage.get_process_mode()
is_group = "@g.us" in remote_jid
if process_mode == "groups_only" and not is_group:
storage.add_log("INFO", "Mensagem ignorada - modo apenas grupos ativo", {
"remote_jid": remote_jid,
"process_mode": process_mode,
"is_group": is_group
})
return {"message": "Modo apenas grupos ativo - mensagens privadas ignoradas"}
if from_me and not dynamic_settings["PROCESS_SELF_MESSAGES"]:
storage.add_log("INFO", "Mensagem própria ignorada", {
"remote_jid": remote_jid
@@ -105,21 +120,62 @@ async def transcreve_audios(request: Request):
"source": audio_source
})
# Carregar configurações de formatação
output_mode = get_config("output_mode", "both")
summary_header = get_config("summary_header", "🤖 *Resumo do áudio:*")
transcription_header = get_config("transcription_header", "🔊 *Transcrição do áudio:*")
character_limit = int(get_config("character_limit", "500"))
# Verificar se timestamps estão habilitados
use_timestamps = get_config("use_timestamps", "false") == "true"
storage.add_log("DEBUG", "Informações da mensagem", {
"from_me": from_me,
"remote_jid": remote_jid,
"is_group": is_group
})
# 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']}"
transcription_text, has_timestamps = await transcribe_audio(
audio_source,
apikey=apikey,
remote_jid=remote_jid,
from_me=from_me,
use_timestamps=use_timestamps
)
# Log do resultado
storage.add_log("INFO", "Transcrição concluída", {
"has_timestamps": has_timestamps,
"text_length": len(transcription_text),
"remote_jid": remote_jid
})
# Determinar se precisa de resumo baseado no modo de saída
summary_text = None
if output_mode in ["both", "summary_only"] or (
output_mode == "smart" and len(transcription_text) > character_limit
):
summary_text = await summarize_text_if_needed(transcription_text)
# Construir mensagem baseada no modo de saída
message_parts = []
if output_mode == "smart":
if len(transcription_text) > character_limit:
message_parts.append(f"{summary_header}\n\n{summary_text}")
else:
message_parts.append(f"{transcription_header}\n\n{transcription_text}")
else:
if output_mode in ["both", "summary_only"] and summary_text:
message_parts.append(f"{summary_header}\n\n{summary_text}")
if output_mode in ["both", "transcription_only"]:
message_parts.append(f"{transcription_header}\n\n{transcription_text}")
# Adicionar mensagem de negócio
message_parts.append(dynamic_settings['BUSINESS_MESSAGE'])
# Juntar todas as partes da mensagem
summary_message = "\n\n".join(message_parts)
# Enviar resposta
await send_message_to_whatsapp(
@@ -135,8 +191,8 @@ async def transcreve_audios(request: Request):
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)
"transcription_length": len(transcription_text) if transcription_text else 0,
"summary_length": len(summary_text) if summary_text else 0 # Adiciona verificação
})
return {"message": "Áudio transcrito e resposta enviada com sucesso"}

View File

@@ -7,43 +7,7 @@ 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
# Função para buscar grupos do Whatsapp
def fetch_whatsapp_groups(server_url, instance, api_key):
url = f"{server_url}/group/fetchAllGroups/{instance}"
headers = {"apikey": api_key}
params = {"getParticipants": "false"} # Adicionando o parâmetro de query
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
st.error(f"Erro ao buscar grupos: {str(e)}")
if response.text:
st.error(f"Resposta do servidor: {response.text}")
return []
# Configuração da página
# 1. Primeiro: Configuração da página
st.set_page_config(
page_title="TranscreveZAP by Impacte AI",
page_icon="🎙️",
@@ -51,6 +15,67 @@ st.set_page_config(
initial_sidebar_state="expanded",
)
# 2. Depois: Inicialização do Redis
redis_client = redis.Redis(
host=os.getenv('REDIS_HOST', 'localhost'),
port=int(os.getenv('REDIS_PORT', 6380)),
decode_responses=True
)
# 3. Funções de sessão (atualizado para usar st.query_params)
def init_session():
"""Inicializa o sistema de sessão"""
if 'session_id' not in st.session_state:
# Verificar se existe uma sessão válida no Redis
session_token = st.query_params.get('session', None)
if session_token:
session_data = redis_client.get(f"session:{session_token}")
if session_data:
st.session_state.session_id = session_token
st.session_state.authenticated = True
return
# Se não houver sessão válida, gerar um novo ID
st.session_state.session_id = None
st.session_state.authenticated = False
# Garantir que init_session seja chamado antes de qualquer coisa
init_session()
def create_session():
"""Cria uma nova sessão no Redis"""
import uuid
session_id = str(uuid.uuid4())
expiry = 7 * 24 * 60 * 60 # 7 dias em segundos
# Salvar sessão no Redis
redis_client.setex(f"session:{session_id}", expiry, "active")
# Atualizar estado da sessão
st.session_state.session_id = session_id
st.session_state.authenticated = True
# Adicionar session_id como parâmetro de URL
st.query_params['session'] = session_id
def end_session():
"""Encerra a sessão atual"""
if 'session_id' in st.session_state and st.session_state.session_id:
# Remover sessão do Redis
redis_client.delete(f"session:{st.session_state.session_id}")
# Limpar todos os estados relevantes
for key in ['session_id', 'authenticated', 'username']:
if key in st.session_state:
del st.session_state[key]
# Remover parâmetro de sessão da URL
if 'session' in st.query_params:
del st.query_params['session']
# 4. Inicializar a sessão
init_session()
# Estilos CSS personalizados
st.markdown("""
<style>
@@ -100,6 +125,64 @@ st.markdown("""
# Configuração do storage
storage = StorageHandler()
# Dicionário de idiomas em português
IDIOMAS = {
"pt": "Português",
"en": "Inglês",
"es": "Espanhol",
"fr": "Francês",
"de": "Alemão",
"it": "Italiano",
"ja": "Japonês",
"ko": "Coreano",
"zh": "Chinês",
"ro": "Romeno",
"ru": "Russo",
"ar": "Árabe",
"hi": "Hindi",
"nl": "Holandês",
"pl": "Polonês",
"tr": "Turco"
}
# 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
# Função para buscar grupos do Whatsapp
def fetch_whatsapp_groups(server_url, instance, api_key):
url = f"{server_url}/group/fetchAllGroups/{instance}"
headers = {"apikey": api_key}
params = {"getParticipants": "false"} # Adicionando o parâmetro de query
try:
st.write(f"Requisição para URL: {url}") # Debug para URL
st.write(f"Cabeçalhos: {headers}") # Debug para headers
st.write(f"Parâmetros: {params}") # Debug para parâmetros
response = requests.get(url, headers=headers, params=params)
st.write(f"Status Code: {response.status_code}") # Debug para status HTTP
response.raise_for_status() # Levanta exceções HTTP
return response.json() # Retorna o JSON da resposta
except requests.RequestException as e:
st.error(f"Erro ao buscar grupos: {str(e)}")
if response.text:
st.error(f"Resposta do servidor: {response.text}")
return []
# Função para carregar configurações do Redis para o Streamlit
def load_settings():
try:
@@ -108,6 +191,7 @@ def load_settings():
"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"),
"TRANSCRIPTION_LANGUAGE": get_from_redis("TRANSCRIPTION_LANGUAGE", "pt"),
}
except Exception as e:
st.error(f"Erro ao carregar configurações do Redis: {e}")
@@ -162,22 +246,59 @@ def login_page():
submit_button = st.form_submit_button('Entrar')
if submit_button:
if username == os.getenv('MANAGER_USER') and password == os.getenv('MANAGER_PASSWORD'):
st.session_state.authenticated = True
create_session()
st.success("Login realizado com sucesso!")
st.experimental_rerun()
else:
st.error('Credenciais inválidas')
# Modificar a função de logout no dashboard
def dashboard():
# Versão do sistema
APP_VERSION = "2.3"
show_logo()
st.sidebar.markdown('<div class="sidebar-header">TranscreveZAP - Menu</div>', unsafe_allow_html=True)
st.sidebar.markdown(f'<div style="text-align: center; color: gray; font-size: 0.8em;">versão {APP_VERSION}</div>', unsafe_allow_html=True)
# Mostrar nome do usuário logado (se disponível)
if hasattr(st.session_state, 'session_id'):
st.sidebar.markdown("---")
st.sidebar.markdown("👤 **Usuário Conectado**")
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()
# Seção de logout com confirmação
st.sidebar.markdown("---")
logout_container = st.sidebar.container()
# Verifica se já existe um estado para confirmação de logout
if 'logout_confirmation' not in st.session_state:
st.session_state.logout_confirmation = False
# Botão principal de logout
if not st.session_state.logout_confirmation:
if logout_container.button("🚪 Sair da Conta"):
st.session_state.logout_confirmation = True
st.experimental_rerun()
# Botões de confirmação
if st.session_state.logout_confirmation:
col1, col2 = st.sidebar.columns(2)
if col1.button("✅ Confirmar"):
st.session_state.logout_confirmation = False
end_session()
st.experimental_rerun()
if col2.button("❌ Cancelar"):
st.session_state.logout_confirmation = False
st.experimental_rerun()
# Renderiza a página selecionada
if page == "📊 Painel de Controle":
show_statistics()
elif page == "👥 Gerenciar Grupos":
@@ -209,6 +330,24 @@ def show_statistics():
st.plotly_chart(fig, use_container_width=True)
else:
st.info("Ainda não há dados de processamento disponíveis.")
# Adicionar informações sobre o endpoint da API
st.subheader("Endpoint da API")
api_domain = get_from_redis("API_DOMAIN", "seu.dominio.com")
api_endpoint = f"https://{api_domain}/transcreve-audios"
st.code(api_endpoint, language="text")
if st.button(" Instruções de Uso"):
st.info(
"Para utilizar o serviço de transcrição, siga estas etapas:\n\n"
"1. Copie a URL completa acima.\n"
"2. Na configuração de webhook da Evolution API:\n"
" - Cole a URL no campo apropriado.\n"
" - Ative o webhook.\n"
" - Marque as opções 'Webhook Base64' e o Evento 'MESSAGES_UPSERT'.\n\n"
"Isso permitirá que a Evolution API envie as mensagens de áudio para o nosso serviço de transcrição."
)
except Exception as e:
st.error(f"Erro ao carregar estatísticas: {e}")
@@ -317,19 +456,443 @@ def manage_blocks():
else:
st.info("Nenhum usuário bloqueado.")
# manager.py - Adicionar na seção de configurações
def message_settings_section():
st.subheader("📝 Configurações de Mensagem")
# Carregar configurações atuais
message_settings = storage.get_message_settings()
# Headers personalizados
col1, col2 = st.columns(2)
with col1:
summary_header = st.text_input(
"Cabeçalho do Resumo",
value=message_settings["summary_header"],
help="Formato do cabeçalho para o resumo do áudio"
)
with col2:
transcription_header = st.text_input(
"Cabeçalho da Transcrição",
value=message_settings["transcription_header"],
help="Formato do cabeçalho para a transcrição do áudio"
)
# Modo de saída
output_mode = st.selectbox(
"Modo de Saída",
options=["both", "summary_only", "transcription_only", "smart"],
format_func=lambda x: {
"both": "Resumo e Transcrição",
"summary_only": "Apenas Resumo",
"transcription_only": "Apenas Transcrição",
"smart": "Modo Inteligente (baseado no tamanho)"
}[x],
value=message_settings["output_mode"]
)
# Configuração do limite de caracteres (visível apenas no modo inteligente)
if output_mode == "smart":
character_limit = st.number_input(
"Limite de Caracteres para Modo Inteligente",
min_value=100,
max_value=5000,
value=int(message_settings["character_limit"]),
help="Se a transcrição exceder este limite, será enviado apenas o resumo"
)
else:
character_limit = message_settings["character_limit"]
# Botão de salvar
if st.button("💾 Salvar Configurações de Mensagem"):
try:
new_settings = {
"summary_header": summary_header,
"transcription_header": transcription_header,
"output_mode": output_mode,
"character_limit": character_limit
}
storage.save_message_settings(new_settings)
st.success("Configurações de mensagem salvas com sucesso!")
except Exception as e:
st.error(f"Erro ao salvar configurações: {str(e)}")
def show_language_statistics():
"""Exibe estatísticas de uso de idiomas"""
stats = storage.get_language_statistics()
if not stats:
st.info("Ainda não há estatísticas de uso de idiomas.")
return
# Resumo geral
st.subheader("📊 Estatísticas de Idiomas")
# Criar métricas resumidas
total_usage = sum(s.get('total', 0) for s in stats.values())
auto_detected = sum(s.get('auto_detected', 0) for s in stats.values())
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Total de Transcrições", total_usage)
with col2:
st.metric("Detecções Automáticas", auto_detected)
with col3:
st.metric("Idiomas Diferentes", len(stats))
# Gráfico de uso por idioma
usage_data = []
for lang, data in stats.items():
usage_data.append({
'Idioma': IDIOMAS.get(lang, lang),
'Total': data.get('total', 0),
'Enviados': data.get('sent', 0),
'Recebidos': data.get('received', 0),
'Auto-detectados': data.get('auto_detected', 0)
})
if usage_data:
df = pd.DataFrame(usage_data)
# Gráfico de barras empilhadas
fig = px.bar(df,
x='Idioma',
y=['Enviados', 'Recebidos'],
title='Uso por Idioma',
barmode='stack')
st.plotly_chart(fig, use_container_width=True)
# Tabela detalhada
st.subheader("📋 Detalhamento por Idioma")
st.dataframe(df.sort_values('Total', ascending=False))
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 Serviço no Rodapé", 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()
# Criar tabs para melhor organização
tab1, tab2, tab3, tab4 = st.tabs([
"🔑 Chaves API",
"🌐 Configurações Gerais",
"📝 Formatação de Mensagens",
"🗣️ Idiomas e Transcrição"
])
with tab1:
st.subheader("Gerenciamento de Chaves GROQ")
# Campo para gerenciamento de chaves GROQ
main_key = st.text_input(
"GROQ API Key Principal",
value=st.session_state.settings["GROQ_API_KEY"],
key="groq_api_key",
type="password",
help="Chave GROQ principal do sistema"
)
if "authenticated" not in st.session_state:
st.session_state.authenticated = False
# Seção de chaves adicionais
st.markdown("---")
st.subheader("Chaves GROQ Adicionais (Sistema de Rodízio)")
# Exibir chaves existentes
groq_keys = storage.get_groq_keys()
if groq_keys:
st.write("Chaves configuradas para rodízio:")
for key in groq_keys:
col1, col2 = st.columns([4, 1])
with col1:
masked_key = f"{key[:10]}...{key[-4:]}"
st.code(masked_key, language=None)
with col2:
if st.button("🗑️", key=f"remove_{key}", help="Remover esta chave"):
storage.remove_groq_key(key)
st.success(f"Chave removida do rodízio!")
st.experimental_rerun()
# Adicionar nova chave
new_key = st.text_input(
"Adicionar Nova Chave GROQ",
key="new_groq_key",
type="password",
help="Insira uma nova chave GROQ para adicionar ao sistema de rodízio"
)
col1, col2 = st.columns([4, 1])
with col1:
if st.button(" Adicionar ao Rodízio", help="Adicionar esta chave ao sistema de rodízio"):
if new_key:
if new_key.startswith("gsk_"):
storage.add_groq_key(new_key)
st.success("Nova chave adicionada ao sistema de rodízio!")
st.experimental_rerun()
else:
st.error("Chave inválida! A chave deve começar com 'gsk_'")
else:
st.warning("Por favor, insira uma chave válida")
pass
with tab2:
st.subheader("Configurações do Sistema")
# Business Message
st.text_input(
"Mensagem de Serviço no Rodapé",
value=st.session_state.settings["BUSINESS_MESSAGE"],
key="business_message"
)
# Process Group Messages
st.selectbox(
"Processar Mensagens em Grupos",
options=["true", "false"],
index=["true", "false"].index(st.session_state.settings["PROCESS_GROUP_MESSAGES"]),
key="process_group_messages"
)
# Process Self 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"
)
st.subheader("🔄 Modo de Processamento")
# Obter o modo atual do Redis
current_mode = storage.get_process_mode()
# Definir as opções e seus rótulos
mode_options = ["all", "groups_only"]
mode_labels = {
"all": "Todos (Grupos e Privado)",
"groups_only": "Apenas Grupos"
}
# Calcular o índice atual baseado no valor do Redis
current_index = mode_options.index(current_mode) if current_mode in mode_options else 0
process_mode = st.selectbox(
"Processar mensagens de:",
options=mode_options,
format_func=lambda x: mode_labels[x],
index=current_index,
key="process_mode",
help="Escolha se deseja processar mensagens de todos os contatos ou apenas de grupos"
)
# Configuração de idioma
st.markdown("---")
st.subheader("🌐 Idioma")
# Carregar configuração atual de idioma
current_language = get_from_redis("TRANSCRIPTION_LANGUAGE", "pt")
# Seleção de idioma
selected_language = st.selectbox(
"Idioma para Transcrição e Resumo",
options=list(IDIOMAS.keys()),
format_func=lambda x: IDIOMAS[x],
index=list(IDIOMAS.keys()).index(current_language) if current_language in IDIOMAS else 0,
help="Selecione o idioma para transcrição dos áudios e geração dos resumos",
key="transcription_language"
)
pass
with tab3:
st.subheader("Formatação de Mensagens")
# Headers personalizados
col1, col2 = st.columns(2)
with col1:
summary_header = st.text_input(
"Cabeçalho do Resumo",
value=get_from_redis("summary_header", "🤖 *Resumo do áudio:*"),
key="summary_header",
help="Formato do cabeçalho para o resumo do áudio"
)
with col2:
transcription_header = st.text_input(
"Cabeçalho da Transcrição",
value=get_from_redis("transcription_header", "🔊 *Transcrição do áudio:*"),
key="transcription_header",
help="Formato do cabeçalho para a transcrição do áudio"
)
# Modo de saída - Corrigido para usar index
output_modes = ["both", "summary_only", "transcription_only", "smart"]
output_mode_labels = {
"both": "Resumo e Transcrição",
"summary_only": "Apenas Resumo",
"transcription_only": "Apenas Transcrição",
"smart": "Modo Inteligente (baseado no tamanho)"
}
current_mode = get_from_redis("output_mode", "both")
mode_index = output_modes.index(current_mode) if current_mode in output_modes else 0
output_mode = st.selectbox(
"Modo de Saída",
options=output_modes,
format_func=lambda x: output_mode_labels[x],
index=mode_index,
key="output_mode",
help="Selecione como deseja que as mensagens sejam enviadas"
)
if output_mode == "smart":
character_limit = st.number_input(
"Limite de Caracteres para Modo Inteligente",
min_value=100,
max_value=5000,
value=int(get_from_redis("character_limit", "500")),
help="Se a transcrição exceder este limite, será enviado apenas o resumo"
)
# Botão de salvar unificado
if st.button("💾 Salvar Todas as Configurações"):
try:
# Salvar configurações existentes
save_settings()
# Salvar novas configurações de mensagem
save_to_redis("summary_header", summary_header)
save_to_redis("transcription_header", transcription_header)
save_to_redis("output_mode", output_mode)
if output_mode == "smart":
save_to_redis("character_limit", str(character_limit))
# Se há uma chave principal, adicionar ao sistema de rodízio
if main_key and main_key.startswith("gsk_"):
storage.add_groq_key(main_key)
# Salvar configuração de idioma
save_to_redis("TRANSCRIPTION_LANGUAGE", selected_language)
# Salvamento do modo de processamento
storage.redis.set(storage._get_redis_key("process_mode"), process_mode)
st.success("✅ Todas as configurações foram salvas com sucesso!")
# Mostrar resumo
total_keys = len(storage.get_groq_keys())
st.info(f"""Sistema configurado com {total_keys} chave(s) GROQ no rodízio
Idioma definido: {IDIOMAS[selected_language]}
Modo de saída: {output_mode_labels[output_mode]}""")
except Exception as e:
st.error(f"Erro ao salvar configurações: {str(e)}")
with tab4:
st.subheader("Idiomas e Transcrição")
# Adicionar estatísticas no topo
show_language_statistics()
# Seção de Detecção Automática
st.markdown("---")
st.markdown("### 🔄 Detecção Automática de Idioma")
col1, col2 = st.columns(2)
with col1:
auto_detect = st.toggle(
"Ativar detecção automática",
value=storage.get_auto_language_detection(),
help="Detecta e configura automaticamente o idioma dos contatos"
)
if auto_detect:
st.info("""
A detecção automática de idioma:
1. Analisa o primeiro áudio de cada contato
2. Configura o idioma automaticamente
3. Usa cache de 24 horas para otimização
4. Funciona apenas em conversas privadas
5. Mantém o idioma global para grupos
6. Permite tradução automática entre idiomas
""")
# Seção de Timestamps
st.markdown("---")
st.markdown("### ⏱️ Timestamps na Transcrição")
use_timestamps = st.toggle(
"Incluir timestamps",
value=get_from_redis("use_timestamps", "false") == "true",
help="Adiciona marcadores de tempo em cada trecho"
)
if use_timestamps:
st.info("Os timestamps serão mostrados no formato [MM:SS] para cada trecho da transcrição")
# Seção de Configuração Manual de Idiomas por Contato
st.markdown("---")
st.markdown("### 👥 Idiomas por Contato")
# Obter contatos configurados
contact_languages = storage.get_all_contact_languages()
# Adicionar novo contato
with st.expander(" Adicionar Novo Contato", expanded=not bool(contact_languages)):
new_contact = st.text_input(
"Número do Contato",
placeholder="Ex: 5521999999999",
help="Digite apenas números, sem símbolos ou @s.whatsapp.net"
)
new_language = st.selectbox(
"Idioma do Contato",
options=list(IDIOMAS.keys()),
format_func=lambda x: IDIOMAS[x],
help="Idioma para transcrição dos áudios deste contato"
)
if st.button("Adicionar Contato"):
if new_contact and new_contact.isdigit():
storage.set_contact_language(new_contact, new_language)
st.success(f"✅ Contato configurado com idioma {IDIOMAS[new_language]}")
st.experimental_rerun()
else:
st.error("Por favor, insira um número válido")
# Listar contatos configurados
if contact_languages:
st.markdown("### Contatos Configurados")
for contact, language in contact_languages.items():
col1, col2, col3 = st.columns([2, 2, 1])
with col1:
st.text(f"+{contact}")
with col2:
current_language = st.selectbox(
"Idioma",
options=list(IDIOMAS.keys()),
format_func=lambda x: IDIOMAS[x],
key=f"lang_{contact}",
index=list(IDIOMAS.keys()).index(language) if language in IDIOMAS else 0
)
if current_language != language:
storage.set_contact_language(contact, current_language)
with col3:
if st.button("🗑️", key=f"remove_{contact}"):
storage.remove_contact_language(contact)
st.success("Contato removido")
st.experimental_rerun()
# Botão de Salvar
if st.button("💾 Salvar Configurações de Idioma e Transcrição"):
try:
storage.set_auto_language_detection(auto_detect)
save_to_redis("use_timestamps", str(use_timestamps).lower())
st.success("✅ Configurações salvas com sucesso!")
# Mostrar resumo das configurações
st.info(f"""
Configurações atuais:
- Detecção automática: {'Ativada' if auto_detect else 'Desativada'}
- Timestamps: {'Ativados' if use_timestamps else 'Desativados'}
- Contatos configurados: {len(contact_languages)}
""")
except Exception as e:
st.error(f"Erro ao salvar configurações: {str(e)}")
# Adicionar no início da execução principal
if __name__ == "__main__":
init_session()
# Modificar a parte final do código
if st.session_state.authenticated:
dashboard()
else:

304
readme.md
View File

@@ -1,9 +1,20 @@
# TranscreveZAP 2.0
## Transcrição e Resumo de Áudios no WhatsApp usando Python com interface em Streamlit
![ImpacteAI](./fluxo.png)
# TranscreveZAP 2.3- Plataforma de Gestão e Automação de Áudios do WhatsApp
Este projeto permite transcrever e resumir áudios enviados pelo WhatsApp usando inteligência artificial e integração com APIs. Ideal para automatizar o processamento de mensagens de áudio, oferecendo um resumo claro e prático.
### Sistema Inteligente de Transcrição, Resumo e Tradução Automática de Áudios para WhatsApp
*Desenvolvido com Python, FastAPI e Streamlit*
---
Uma solução completa para automatizar e gerenciar mensagens de áudio no WhatsApp, oferecendo:
- Transcrição automática multilíngue
- Resumos inteligentes de áudios
- Detecção e tradução automática entre idiomas
- Interface administrativa completa
- Sistema de rodízio de chaves API
- Gestão avançada de grupos e usuários
- Personalização de formatação e saída
Contato de email: contato@impacte.ai
([ACESSE NOSSO SITE](https://impacte.ai/))
@@ -16,45 +27,58 @@ Antes de começar, certifique-se de ter os seguintes requisitos:
- Python 3.10+ instalado ([Download](https://www.python.org/downloads/))
- Docker e Docker Compose instalados ([Instruções](https://docs.docker.com/get-docker/))
- Uma conta Evolution API com chave válida
- Uma conta GROQ API com chave válida (começa com 'gsk_') ([Crie sua CONTA](https://console.groq.com/login))
- No mínimo uma conta GROQ API com chave válida (começa com 'gsk_') ([Crie sua CONTA](https://console.groq.com/login))
* Em caso de uso com Proxy Reverso Aponte um Subdomínio para a API e outro para o MANAGER da aplicação
---
## 🚀 **Instalação e Configuração**
### 🐳 Docker Compose
1. Clone o repositório:
```bash
```bash
git clone https://github.com/seu-usuario/transcrevezap.git
cd transcrevezap
```
```
2. Configure o arquivo docker-compose.yaml:
```yaml
version: "3.7"
services:
tcaudio:
image: impacteai/transcrevezap:latest
ports:
- 8005:8005 # Porta para FastAPI
- 8501:8501 # Porta para Streamlit
environment:
- REDIS_HOST=redis
- REDIS_PORT=6380
- MANAGER_USER=admin
- MANAGER_PASSWORD=sua_senha_aqui
depends_on:
- redis
redis:
image: redis:6
command: redis-server --port 6380 --appendonly yes
volumes:
- redis_data:/data
```yaml
version: "3.7"
services:
tcaudio:
image: impacteai/transcrevezap:latest
build:
context: .
ports:
- 8005:8005 # Porta para FastAPI
- 8501:8501 # Porta para Streamlit
environment:
- UVICORN_PORT=8005
- UVICORN_HOST=0.0.0.0
- UVICORN_RELOAD=true
- UVICORN_WORKERS=1
- API_DOMAIN=localhost
- DEBUG_MODE=false
- LOG_LEVEL=INFO
- MANAGER_USER=admin
- MANAGER_PASSWORD=sua_senha_aqui
- REDIS_HOST=redis-transcrevezap
- REDIS_PORT=6380 # Porta personalizada para o Redis do TranscreveZAP
depends_on:
- redis-transcrevezap
command: ./start.sh
redis-transcrevezap:
image: redis:6
command: redis-server --port 6380 --appendonly yes
volumes:
redis_data:
```
- redis_transcrevezap_data:/data
volumes:
redis_transcrevezap_data:
driver: local
```
3. Inicie os serviços:
```bash
@@ -67,11 +91,10 @@ Acesse a interface de gerenciamento em http://seu-ip:8501.
Faça login com as credenciais definidas em MANAGER_USER e MANAGER_PASSWORD.
Na seção "Configurações", defina:
GROQ_API_KEY: Sua chave da API GROQ
BUSINESS_MESSAGE: Mensagem de rodapé após transcrição
PROCESS_GROUP_MESSAGES: Habilitar processamento de mensagens em grupos
PROCESS_SELF_MESSAGES: Habilitar processamento de mensagens próprias
1. GROQ_API_KEY: Sua chave da API GROQ
2. BUSINESS_MESSAGE: Mensagem de rodapé após transcrição
3. PROCESS_GROUP_MESSAGES: Habilitar processamento de mensagens em grupos
4. PROCESS_SELF_MESSAGES: Habilitar processamento de mensagens próprias
## 🔧 Uso
Endpoint para Webhook da Evolution API
@@ -125,7 +148,8 @@ uvicorn main:app --host 0.0.0.0 --port 8005
```bash
http://127.0.0.1:8005/transcreve-audios
```
1. Aponte um subomínio com o IP do seu servidor para a API da TranscreveZAP
2. Aponte um subomínio com o IP do seu servidor para o MANAGER da TranscreveZAP
### 🌟 Docker Swarm com Traefik
```yaml
@@ -135,7 +159,7 @@ services:
tcaudio:
image: impacteai/transcrevezap:latest
networks:
- transcrevezap_network
- sua_rede_externa # Substitua pelo nome da sua rede externa
ports:
- 8005:8005 # Porta para FastAPI
- 8501:8501 # Porta para Streamlit
@@ -144,12 +168,13 @@ services:
- UVICORN_HOST=0.0.0.0
- UVICORN_RELOAD=true
- UVICORN_WORKERS=1
- API_DOMAIN=seu.dominio.com #coloque seu subdominio da API apontado aqui
- DEBUG_MODE=false
- LOG_LEVEL=INFO
- MANAGER_USER=seu_usuario_admin
- MANAGER_PASSWORD=sua_senha_segura
- MANAGER_USER=seu_usuario_admin # Defina Usuário do Manager
- MANAGER_PASSWORD=sua_senha_segura # Defina Senha do Manager
- REDIS_HOST=redis-transcrevezap
- REDIS_PORT=6380
- REDIS_PORT=6380 # Porta personalizada para o Redis do TranscreveZAP
depends_on:
- redis-transcrevezap
deploy:
@@ -160,7 +185,7 @@ services:
- node.role == manager
labels:
- traefik.enable=true
- traefik.http.routers.tcaudio.rule=Host(`seu.dominio.com`)
- traefik.http.routers.tcaudio.rule=Host(`seu.dominio.com`) #coloque seu subdominio da API apontado aqui
- traefik.http.routers.tcaudio.entrypoints=websecure
- traefik.http.routers.tcaudio.tls.certresolver=letsencryptresolver
- traefik.http.services.tcaudio.loadbalancer.server.port=8005
@@ -169,7 +194,7 @@ services:
- traefik.http.middlewares.traefik-compress.compress=true
- traefik.http.routers.tcaudio.middlewares=traefik-compress
# Configuração do Streamlit
- traefik.http.routers.tcaudio-manager.rule=Host(`manager.seu.dominio.com`)
- traefik.http.routers.tcaudio-manager.rule=Host(`manager.seu.dominio.com`) #coloque seu subdominio do Manager apontado aqui
- traefik.http.routers.tcaudio-manager.entrypoints=websecure
- traefik.http.routers.tcaudio-manager.tls.certresolver=letsencryptresolver
- traefik.http.services.tcaudio-manager.loadbalancer.server.port=8501
@@ -182,10 +207,10 @@ services:
volumes:
- redis_transcrevezap_data:/data
networks:
- transcrevezap_network
- sua_rede_externa # Substitua pelo nome da sua rede externa
networks:
transcrevezap_network:
sua_rede_externa: # Substitua pelo nome da sua rede externa
external: true
name: sua_rede_externa # Substitua pelo nome da sua rede externa
@@ -203,9 +228,10 @@ https://transcricaoaudio.seudominio.com.br/transcreve-audios
Para usar com Traefik, certifique-se de:
1. Ter o Traefik configurado em seu ambiente Docker Swarm
2. Configurar o DNS do seu domínio para apontar para o servidor
2. Configurar 2 DNS do seu domínio para apontar para a API e para o MANAGER
3. Ajustar as labels do Traefik conforme seu ambiente
4. Verificar se a rede externa existe no Docker Swarm
5. Utilize a stack de exemplo contida no projeto para guiar a instalação
## 📝 **Notas Importantes**
- A GROQ_API_KEY deve começar com 'gsk_'
@@ -214,6 +240,157 @@ Para usar com Traefik, certifique-se de:
- Em produção, recomenda-se DEBUG_MODE=false
- Configure LOG_LEVEL=DEBUG apenas para troubleshooting
## ✨ Novos Recursos na v2.3
### 🌍 Suporte Multilíngue
- Transcrição e resumo com suporte para 16 idiomas principais
- Mudança instantânea de idioma
- Interface intuitiva para seleção de idioma
- Mantém consistência entre transcrição e resumo
- Configuração manual de idioma por contato
- Detecção automática de idioma
- Tradução automática integrada
### 🔄 Sistema de Cache para Idiomas
Implementação de cache inteligente para otimizar a detecção e processamento de idiomas.
### 🔄 Sistema Inteligente de Rodízio de Chaves
- Suporte a múltiplas chaves GROQ
- Balanceamento automático de carga
- Maior redundância e disponibilidade
- Gestão simplificada de chaves via interface
### ⏱️ Timestamps em Transcrições
Nova funcionalidade de timestamps que adiciona marcadores de tempo precisos em cada trecho da transcrição.
## 📋 Detalhamento das Funcionalidades
### 🌍 Sistema de Idiomas
O TranscreveZAP suporta transcrição e resumo em múltiplos idiomas. Na seção "Configurações", você pode:
1. Selecionar o idioma principal para transcrição e resumo
2. O sistema manterá Português como padrão se nenhum outro for selecionado
3. A mudança de idioma é aplicada instantaneamente após salvar
Idiomas suportados:
- 🇩🇪 Alemão
- 🇸🇦 Árabe
- 🇨🇳 Chinês
- 🇰🇷 Coreano
- 🇪🇸 Espanhol
- 🇫🇷 Francês
- 🇮🇳 Hindi
- 🇳🇱 Holandês
- 🇬🇧 Inglês
- 🇮🇹 Italiano
- 🇯🇵 Japonês
- 🇵🇱 Polonês
- 🇧🇷 Português (padrão)
- 🇷🇴 Romeno
- 🇷🇺 Russo
- 🇹🇷 Turco
### 🌐 Gestão de Idiomas por Contato
#### Configuração Manual
```markdown
1. Acesse o Manager > Configurações > Idiomas e Transcrição
2. Expanda "Adicionar Novo Contato"
3. Digite o número do contato (formato: 5521999999999)
4. Selecione o idioma desejado
5. Clique em "Adicionar Contato"
```
### 🔄 Detecção Automática de Idioma
Nova funcionalidade que detecta automaticamente o idioma do contato:
- Ativação via Manager > Configurações > Idiomas e Transcrição
- Analisa o primeiro áudio de cada contato
- Cache inteligente de 24 horas
- Funciona apenas em conversas privadas
- Mantém configuração global para grupos
### ⚡ Tradução Automática
Sistema inteligente de tradução que:
- Traduz automaticamente áudios recebidos para seu idioma principal
- Mantém o contexto e estilo original da mensagem
- Preserva formatações especiais (emojis, negrito, itálico)
- Otimizado para comunicação natural
### ⏱️ Sistema de Timestamps
Nova funcionalidade que adiciona marcadores de tempo:
- Formato [MM:SS] no início de cada trecho
- Ativação via Manager > Configurações > Idiomas e Transcrição
- Precisão de segundos
- Ideal para referência e navegação em áudios longos
#### Exemplo de Saída com Timestamps:
```
[00:00] Bom dia pessoal
[00:02] Hoje vamos falar sobre
[00:05] O novo sistema de timestamps
```
## 🔧 Configuração e Uso
### Configuração de Idiomas
1. **Configuração Global**
- Defina o idioma padrão do sistema
- Acesse: Manager > Configurações > Configurações Gerais
- Selecione o idioma principal em "Idioma para Transcrição e Resumo"
2. **Configuração por Contato**
- Acesse: Manager > Configurações > Idiomas e Transcrição
- Use "Adicionar Novo Contato" ou gerencie contatos existentes
- Cada contato pode ter seu próprio idioma configurado
3. **Detecção Automática**
- Ative/Desative a detecção automática
- Configure o tempo de cache
- Gerencie exceções e configurações manuais
### Configuração de Timestamps
1. Acesse: Manager > Configurações > Idiomas e Transcrição
2. Localize a seção "Timestamps na Transcrição"
3. Use o toggle para ativar/desativar
4. As mudanças são aplicadas imediatamente
## 📊 Monitoramento e Estatísticas
### Estatísticas de Idiomas
O sistema agora oferece estatísticas detalhadas:
- Total de transcrições por idioma
- Número de detecções automáticas
- Divisão entre mensagens enviadas/recebidas
- Histórico de uso por idioma
### Visualização de Dados
- Gráficos de uso por idioma
- Distribuição de idiomas
- Estatísticas de tradução
- Performance do sistema
## 🔄 Sistema de Rodízio de Chaves GROQ
O TranscreveZAP suporta múltiplas chaves GROQ com sistema de rodízio automático para melhor distribuição de carga e redundância.
### Funcionalidades:
1. Adicione múltiplas chaves GROQ para distribuição de carga
2. O sistema alterna automaticamente entre as chaves disponíveis
3. Se uma chave falhar, o sistema usa a próxima disponível
4. Visualize todas as chaves configuradas no painel
5. Adicione ou remova chaves sem interromper o serviço
### Como Configurar:
1. Acesse a seção "Configurações"
2. Na área "🔑 Gerenciamento de Chaves GROQ":
- Adicione a chave principal
- Use "Adicionar Nova Chave GROQ" para incluir chaves adicionais
- O sistema começará a usar todas as chaves em rodízio automaticamente
### Boas Práticas:
- Mantenha pelo menos duas chaves ativas para redundância
- Monitore o uso das chaves pelo painel administrativo
- Remova chaves expiradas ou inválidas
- Todas as chaves devem começar com 'gsk_'
## 🔍 **Troubleshooting**
Se encontrar problemas:
1. Verifique se todas as variáveis obrigatórias estão configuradas
@@ -221,6 +398,41 @@ Se encontrar problemas:
3. Verifique os logs do container
4. Certifique-se que as APIs estão acessíveis
### Problemas com Múltiplas Chaves GROQ:
1. Verifique se todas as chaves começam com 'gsk_'
2. Confirme se as chaves estão ativas na console GROQ
3. Monitore os logs para identificar falhas específicas de chaves
4. Mantenha pelo menos uma chave válida no sistema
### Problemas com Idiomas:
1. Verifique se o idioma está corretamente selecionado nas configurações
2. Confirme se a configuração foi salva com sucesso
3. Reinicie o serviço se as alterações não forem aplicadas
4. Verifique os logs para confirmar o idioma em uso
## 📝 Notas Adicionais
### Recomendações de Uso
- Configure idiomas manualmente para contatos frequentes
- Use detecção automática como fallback
- Monitore estatísticas de uso
- Faça backups regulares das configurações
### Limitações Conhecidas
- Detecção automática requer primeiro áudio
- Cache limitado a 24 horas
- Timestamps podem variar em áudios muito longos
## 🤝 Contribuição
Agradecemos feedback e contribuições! Reporte issues e sugira melhorias em nosso GitHub.
---
### 📞 Suporte
Para suporte adicional ou dúvidas:
- WhatsApp: [Entre no GRUPO](https://chat.whatsapp.com/L9jB1SlcmQFIVxzN71Y6KG)
- Email: contato@impacte.ai
- Site: [impacte.ai](https://impacte.ai)
## 📄 **Licença**
Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes.

View File

@@ -31,32 +31,114 @@ async def convert_base64_to_file(base64_data):
})
raise
async def get_groq_key():
"""Obtém a próxima chave GROQ do sistema de rodízio."""
key = storage.get_next_groq_key()
if not key:
raise HTTPException(
status_code=500,
detail="Nenhuma chave GROQ configurada. Configure pelo menos uma chave no painel administrativo."
)
return key
async def summarize_text_if_needed(text):
"""Resumir texto usando a API GROQ"""
"""Resumir texto usando a API GROQ com sistema de rodízio de chaves"""
storage.add_log("DEBUG", "Iniciando processo de resumo", {
"text_length": len(text)
})
# Obter idioma configurado
language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt"
storage.add_log("DEBUG", "Idioma configurado para resumo", {
"language": language,
"redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE")
})
url_completions = "https://api.groq.com/openai/v1/chat/completions"
groq_key = await get_groq_key()
headers = {
"Authorization": f"Bearer {settings.GROQ_API_KEY}",
"Authorization": f"Bearer {groq_key}",
"Content-Type": "application/json",
}
# Adaptar o prompt para considerar o idioma
prompt_by_language = {
"pt": """
Entenda o contexto desse áudio e faça um resumo super enxuto sobre o que se trata.
Esse áudio foi enviado pelo whatsapp, de alguém, para Fabio.
Escreva APENAS o resumo do áudio como se fosse você que estivesse enviando
essa mensagem! Não cumprimente, não de oi, não escreva nada antes nem depois
do resumo, responda apenas um resumo enxuto do que foi falado no áudio.
""",
"en": """
Understand the context of this audio and make a very concise summary of what it's about.
This audio was sent via WhatsApp, from someone, to Fabio.
Write ONLY the summary of the audio as if you were sending this message yourself!
Don't greet, don't say hi, don't write anything before or after the summary,
respond with just a concise summary of what was said in the audio.
""",
"es": """
Entiende el contexto de este audio y haz un resumen muy conciso sobre de qué se trata.
Este audio fue enviado por WhatsApp, de alguien, para Fabio.
Escribe SOLO el resumen del audio como si tú estuvieras enviando este mensaje.
No saludes, no escribas nada antes ni después del resumen, responde únicamente un resumen conciso de lo dicho en el audio.
""",
"fr": """
Comprenez le contexte de cet audio et faites un résumé très concis de ce dont il s'agit.
Cet audio a été envoyé via WhatsApp, par quelqu'un, à Fabio.
Écrivez UNIQUEMENT le résumé de l'audio comme si c'était vous qui envoyiez ce message.
Ne saluez pas, n'écrivez rien avant ou après le résumé, répondez seulement par un résumé concis de ce qui a été dit dans l'audio.
""",
"de": """
Verstehen Sie den Kontext dieses Audios und erstellen Sie eine sehr kurze Zusammenfassung, worum es geht.
Dieses Audio wurde über WhatsApp von jemandem an Fabio gesendet.
Schreiben Sie NUR die Zusammenfassung des Audios, als ob Sie diese Nachricht senden würden.
Grüßen Sie nicht, schreiben Sie nichts vor oder nach der Zusammenfassung, antworten Sie nur mit einer kurzen Zusammenfassung dessen, was im Audio gesagt wurde.
""",
"it": """
Comprendi il contesto di questo audio e fai un riassunto molto conciso di cosa si tratta.
Questo audio è stato inviato tramite WhatsApp, da qualcuno, a Fabio.
Scrivi SOLO il riassunto dell'audio come se fossi tu a inviare questo messaggio.
Non salutare, non scrivere nulla prima o dopo il riassunto, rispondi solo con un riassunto conciso di ciò che è stato detto nell'audio.
""",
"ja": """
この音声の内容を理解し、それが何について話されているのかを非常に簡潔に要約してください。
この音声は、誰かがWhatsAppでファビオに送ったものです。
あなたがそのメッセージを送っているように、音声の要約だけを記述してください。
挨拶や前置き、後書きは書かず、音声で話された内容の簡潔な要約のみを返信してください。
""",
"ko": """
이 오디오의 맥락을 이해하고, 무엇에 관한 것인지 매우 간략하게 요약하세요.
이 오디오는 누군가가 WhatsApp을 통해 Fabio에게 보낸 것입니다.
마치 당신이 메시지를 보내는 것처럼 오디오의 요약만 작성하세요.
인사하거나, 요약 전후로 아무것도 쓰지 말고, 오디오에서 말한 내용을 간략하게 요약한 답변만 하세요.
""",
"zh": """
理解这个音频的上下文,并简洁地总结它的内容。
这个音频是某人通过WhatsApp发送给Fabio的。
请仅以摘要的形式回答,就好像是你在发送这条消息。
不要问候,也不要在摘要前后写任何内容,只需用一句简短的话总结音频中所说的内容。
""",
"ro": """
Înțelege contextul acestui audio și creează un rezumat foarte concis despre ce este vorba.
Acest audio a fost trimis prin WhatsApp, de cineva, către Fabio.
Scrie DOAR rezumatul audio-ului ca și cum tu ai trimite acest mesaj.
Nu saluta, nu scrie nimic înainte sau după rezumat, răspunde doar cu un rezumat concis despre ce s-a spus în audio.
""",
"ru": """
Поймите контекст этого аудио и сделайте очень краткое резюме, о чем идет речь.
Это аудио было отправлено через WhatsApp кем-то Фабио.
Напишите ТОЛЬКО резюме аудио, как будто вы отправляете это сообщение.
Не приветствуйте, не пишите ничего до или после резюме, ответьте только кратким резюме того, что говорилось в аудио.
"""
}
# Usar o prompt do idioma configurado ou fallback para português
base_prompt = prompt_by_language.get(language, prompt_by_language["pt"])
json_data = {
"messages": [{
"role": "user",
"content": f"""
Entenda o contexto desse áudio e faça um resumo super enxuto sobre o que se trata, coloque os pontos relevantes e mais importantes no resumo de forma muito curta.
Esse áudio foi enviado pelo whatsapp, de alguém, para Gabriel.
Escreva APENAS o resumo do áudio como se fosse você que estivesse enviando
essa mensagem! Não comprimente, não de oi, não escreva nada antes nem depois
do resumo, responda apenas um resumo enxuto do que foi falado no áudio.
IMPORTANTE: Não faça esse resumo como se fosse um áudio que uma terceira
pessoa enviou, não diga coisas como 'a pessoa está falando...' etc.
Escreva o resumo com base nessa mensagem do áudio,
como se você estivesse escrevendo esse resumo e enviando em
texto pelo whatsapp: {text}""",
"content": f"{base_prompt}\n\nTexto para resumir: {text}",
}],
"model": "llama-3.3-70b-versatile",
}
@@ -70,7 +152,8 @@ async def summarize_text_if_needed(text):
summary_text = summary_result["choices"][0]["message"]["content"]
storage.add_log("INFO", "Resumo gerado com sucesso", {
"original_length": len(text),
"summary_length": len(summary_text)
"summary_length": len(summary_text),
"language": language
})
return summary_text
else:
@@ -87,69 +170,206 @@ async def summarize_text_if_needed(text):
})
raise
async def transcribe_audio(audio_source, apikey=None):
"""Transcreve áudio usando a API GROQ"""
storage.add_log("INFO", "Iniciando processo de transcrição")
async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=False, use_timestamps=False):
"""
Transcreve áudio com suporte a detecção de idioma e tradução automática.
Args:
audio_source: Caminho do arquivo de áudio ou URL
apikey: Chave da API opcional para download de áudio
remote_jid: ID do remetente/destinatário
from_me: Se o áudio foi enviado pelo próprio usuário
use_timestamps: Se True, usa verbose_json para incluir timestamps
Returns:
tuple: (texto_transcrito, has_timestamps)
"""
storage.add_log("INFO", "Iniciando processo de transcrição", {
"from_me": from_me,
"remote_jid": remote_jid
})
url = "https://api.groq.com/openai/v1/audio/transcriptions"
groq_headers = {"Authorization": f"Bearer {settings.GROQ_API_KEY}"}
groq_key = await get_groq_key()
groq_headers = {"Authorization": f"Bearer {groq_key}"}
# Inicializar variáveis
contact_language = None
system_language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt"
is_private = remote_jid and "@s.whatsapp.net" in remote_jid
# Determinar idioma do contato em conversas privadas
if is_private:
# Remover @s.whatsapp.net do ID para buscar no cache
contact_id = remote_jid.split('@')[0]
# 1. Primeiro tentar obter idioma configurado manualmente
contact_language = storage.get_contact_language(contact_id)
if contact_language:
storage.add_log("DEBUG", "Usando idioma configurado manualmente", {
"contact_language": contact_language,
"from_me": from_me,
"remote_jid": remote_jid,
"is_private": is_private
})
# 2. Se não houver configuração manual e detecção automática estiver ativa
elif storage.get_auto_language_detection():
# Verificar cache primeiro
cached_lang = storage.get_cached_language(contact_id)
if cached_lang:
contact_language = cached_lang.get('language')
storage.add_log("DEBUG", "Usando idioma do cache", {
"contact_language": contact_language,
"auto_detected": True
})
# Se não há cache ou está expirado, fazer detecção
elif not from_me: # Só detecta em mensagens recebidas
try:
# Realizar transcrição inicial sem idioma específico
data = aiohttp.FormData()
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
data.add_field('model', 'whisper-large-v3')
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=groq_headers, data=data) as response:
if response.status == 200:
initial_result = await response.json()
initial_text = initial_result.get("text", "")
# Detectar idioma do texto transcrito
detected_lang = await detect_language(initial_text)
# Salvar no cache E na configuração do contato
storage.cache_language_detection(contact_id, detected_lang)
storage.set_contact_language(contact_id, detected_lang)
contact_language = detected_lang
storage.add_log("INFO", "Idioma detectado e configurado", {
"language": detected_lang,
"remote_jid": remote_jid,
"auto_detected": True
})
except Exception as e:
storage.add_log("WARNING", "Erro na detecção automática de idioma", {
"error": str(e),
"remote_jid": remote_jid
})
if not contact_language:
storage.add_log("DEBUG", "Usando idioma padrão do sistema", {
"from_me": from_me,
"remote_jid": remote_jid,
"is_private": is_private,
"system_language": system_language
})
# Definir idioma de transcrição e tradução baseado no contexto
if is_private and contact_language:
if from_me:
# Se estou enviando para um contato com idioma configurado
transcription_language = contact_language # Transcrever no idioma do contato
target_language = contact_language # Não precisa traduzir
storage.add_log("DEBUG", "Usando idioma do contato para áudio enviado", {
"transcription_language": transcription_language,
"target_language": target_language
})
else:
# Se estou recebendo
transcription_language = contact_language # Transcrever no idioma do contato
target_language = system_language # Traduzir para o idioma do sistema
storage.add_log("DEBUG", "Processando áudio recebido com tradução", {
"transcription_language": transcription_language,
"target_language": target_language
})
else:
# Caso padrão: usar idioma do sistema
transcription_language = system_language
target_language = system_language
storage.add_log("DEBUG", "Usando idioma do sistema", {
"transcription_language": transcription_language,
"target_language": target_language
})
storage.add_log("DEBUG", "Configuração de idiomas definida", {
"transcription_language": transcription_language,
"target_language": target_language,
"from_me": from_me,
"is_private": is_private,
"contact_language": contact_language
})
try:
# Realizar transcrição
data = aiohttp.FormData()
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
data.add_field('model', 'whisper-large-v3')
data.add_field('language', transcription_language)
if use_timestamps:
data.add_field('response_format', 'verbose_json')
# Realizar transcrição
async with aiohttp.ClientSession() as session:
# Se o audio_source for uma URL
if isinstance(audio_source, str) and audio_source.startswith('http'):
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()
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()
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()
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
data.add_field('model', 'whisper-large-v3')
data.add_field('language', 'pt')
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", "")
storage.add_log("INFO", "Transcrição concluída com sucesso", {
"text_length": len(message)
})
is_summary = False
if len(message) > 1000:
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:
if response.status != 200:
error_text = await response.text()
storage.add_log("ERROR", "Erro na transcrição", {
"error": error_text,
"status": response.status
})
raise Exception(f"Erro na transcrição: {error_text}")
result = await response.json()
# Processar resposta baseado no formato
transcription = format_timestamped_result(result) if use_timestamps else result.get("text", "")
# Detecção automática para novos contatos
if (is_private and storage.get_auto_language_detection() and
not from_me and not contact_language):
try:
detected_lang = await detect_language(transcription)
storage.cache_language_detection(remote_jid, detected_lang)
contact_language = detected_lang
storage.add_log("INFO", "Idioma detectado e cacheado", {
"language": detected_lang,
"remote_jid": remote_jid
})
except Exception as e:
storage.add_log("WARNING", "Erro na detecção de idioma", {"error": str(e)})
# Tradução quando necessário
need_translation = (
is_private and contact_language and
(
(from_me and transcription_language != target_language) or
(not from_me and target_language != transcription_language)
)
)
if need_translation:
try:
transcription = await translate_text(
transcription,
transcription_language,
target_language
)
storage.add_log("INFO", "Texto traduzido automaticamente", {
"from": transcription_language,
"to": target_language
})
except Exception as e:
storage.add_log("ERROR", "Erro na tradução", {"error": str(e)})
# Registrar estatísticas de uso
used_language = contact_language if contact_language else system_language
storage.record_language_usage(
used_language,
from_me,
bool(contact_language and contact_language != system_language)
)
return transcription, use_timestamps
except Exception as e:
storage.add_log("ERROR", "Erro no processo de transcrição", {
@@ -160,7 +380,128 @@ async def transcribe_audio(audio_source, apikey=None):
finally:
# Limpar arquivos temporários
if isinstance(audio_source, str) and os.path.exists(audio_source):
os.unlink(audio_source)
try:
os.unlink(audio_source)
except Exception as e:
storage.add_log("WARNING", "Erro ao remover arquivo temporário", {
"error": str(e)
})
def format_timestamped_result(result):
"""
Formata o resultado da transcrição com timestamps
"""
segments = result.get("segments", [])
formatted_lines = []
for segment in segments:
start_time = format_timestamp(segment.get("start", 0))
end_time = format_timestamp(segment.get("end", 0))
text = segment.get("text", "").strip()
if text:
formatted_lines.append(f"[{start_time} -> {end_time}] {text}")
return "\n".join(formatted_lines)
def format_timestamp(seconds):
"""
Converte segundos em formato MM:SS
"""
minutes = int(seconds // 60)
remaining_seconds = int(seconds % 60)
return f"{minutes:02d}:{remaining_seconds:02d}"
# Função para detecção de idioma
async def detect_language(text: str) -> str:
"""
Detecta o idioma do texto usando a API GROQ
Args:
text: Texto para detectar idioma
Returns:
str: Código ISO 639-1 do idioma detectado
"""
storage.add_log("DEBUG", "Iniciando detecção de idioma", {
"text_length": len(text)
})
# Lista de idiomas suportados
SUPPORTED_LANGUAGES = {
"pt", "en", "es", "fr", "de", "it", "ja", "ko",
"zh", "ro", "ru", "ar", "hi", "nl", "pl", "tr"
}
url_completions = "https://api.groq.com/openai/v1/chat/completions"
groq_key = await get_groq_key()
headers = {
"Authorization": f"Bearer {groq_key}",
"Content-Type": "application/json",
}
# Prompt melhorado com exemplos e restrições
prompt = """
Analise o texto e retorne APENAS o código ISO 639-1 do idioma principal.
Regras:
1. Retorne APENAS o código de 2 letras
2. Use somente códigos permitidos: pt, en, es, fr, de, it, ja, ko, zh, ro, ru, ar, hi, nl, pl, tr
3. Se não tiver certeza ou o idioma não estiver na lista, retorne "en"
4. Não inclua pontuação, espaços extras ou explicações
Exemplos corretos:
"Hello world" -> en
"Bonjour le monde" -> fr
"Olá mundo" -> pt
Texto para análise:
"""
json_data = {
"messages": [{
"role": "system",
"content": "Você é um detector de idiomas preciso que retorna apenas códigos ISO 639-1."
}, {
"role": "user",
"content": f"{prompt}\n\n{text[:500]}" # Limitando para os primeiros 500 caracteres
}],
"model": "llama-3.3-70b-versatile",
"temperature": 0.1
}
try:
async with aiohttp.ClientSession() as session:
storage.add_log("DEBUG", "Enviando requisição para API GROQ - Detecção de idioma")
async with session.post(url_completions, headers=headers, json=json_data) as response:
if response.status == 200:
result = await response.json()
detected_language = result["choices"][0]["message"]["content"].strip().lower()
# Validar o resultado
if detected_language not in SUPPORTED_LANGUAGES:
storage.add_log("WARNING", "Idioma detectado não suportado", {
"detected": detected_language,
"fallback": "en"
})
detected_language = "en"
storage.add_log("INFO", "Idioma detectado com sucesso", {
"detected_language": detected_language
})
return detected_language
else:
error_text = await response.text()
storage.add_log("ERROR", "Erro na detecção de idioma", {
"error": error_text,
"status": response.status
})
raise Exception(f"Erro na detecção de idioma: {error_text}")
except Exception as e:
storage.add_log("ERROR", "Erro no processo de detecção de idioma", {
"error": str(e),
"type": type(e).__name__
})
raise
async def send_message_to_whatsapp(server_url, instance, apikey, message, remote_jid, message_id):
"""Envia mensagem via WhatsApp"""
@@ -264,4 +605,130 @@ async def get_audio_base64(server_url, instance, apikey, message_id):
"type": type(e).__name__,
"message_id": message_id
})
raise
async def format_message(transcription_text, summary_text=None):
"""Formata a mensagem baseado nas configurações."""
settings = storage.get_message_settings()
message_parts = []
# Determinar modo de saída
output_mode = settings["output_mode"]
char_limit = int(settings["character_limit"])
if output_mode == "smart":
# Modo inteligente baseado no tamanho
if len(transcription_text) > char_limit:
if summary_text:
message_parts.append(f"{settings['summary_header']}\n\n{summary_text}")
else:
message_parts.append(f"{settings['transcription_header']}\n\n{transcription_text}")
elif output_mode == "summary_only":
if summary_text:
message_parts.append(f"{settings['summary_header']}\n\n{summary_text}")
elif output_mode == "transcription_only":
message_parts.append(f"{settings['transcription_header']}\n\n{transcription_text}")
else: # both
if summary_text:
message_parts.append(f"{settings['summary_header']}\n\n{summary_text}")
message_parts.append(f"{settings['transcription_header']}\n\n{transcription_text}")
# Adicionar mensagem de negócio
message_parts.append(dynamic_settings['BUSINESS_MESSAGE'])
return "\n\n".join(message_parts)
async def translate_text(text: str, source_language: str, target_language: str) -> str:
"""
Traduz o texto usando a API GROQ
Args:
text: Texto para traduzir
source_language: Código ISO 639-1 do idioma de origem
target_language: Código ISO 639-1 do idioma de destino
Returns:
str: Texto traduzido
"""
storage.add_log("DEBUG", "Iniciando tradução", {
"source_language": source_language,
"target_language": target_language,
"text_length": len(text)
})
# Se os idiomas forem iguais, retorna o texto original
if source_language == target_language:
return text
url_completions = "https://api.groq.com/openai/v1/chat/completions"
groq_key = await get_groq_key()
headers = {
"Authorization": f"Bearer {groq_key}",
"Content-Type": "application/json",
}
# Prompt melhorado com contexto e instruções específicas
prompt = f"""
Você é um tradutor profissional especializado em manter o tom e estilo do texto original.
Instruções:
1. Traduza o texto de {source_language} para {target_language}
2. Preserve todas as formatações (negrito, itálico, emojis)
3. Mantenha os mesmos parágrafos e quebras de linha
4. Preserve números, datas e nomes próprios
5. Não adicione ou remova informações
6. Não inclua notas ou explicações
7. Mantenha o mesmo nível de formalidade
Texto para tradução:
{text}
"""
json_data = {
"messages": [{
"role": "system",
"content": "Você é um tradutor profissional que mantém o estilo e formatação do texto original."
}, {
"role": "user",
"content": prompt
}],
"model": "llama-3.3-70b-versatile",
"temperature": 0.3
}
try:
async with aiohttp.ClientSession() as session:
storage.add_log("DEBUG", "Enviando requisição de tradução")
async with session.post(url_completions, headers=headers, json=json_data) as response:
if response.status == 200:
result = await response.json()
translated_text = result["choices"][0]["message"]["content"].strip()
# Verificar se a tradução manteve aproximadamente o mesmo tamanho
length_ratio = len(translated_text) / len(text)
if not (0.5 <= length_ratio <= 1.5):
storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", {
"original_length": len(text),
"translated_length": len(translated_text),
"ratio": length_ratio
})
storage.add_log("INFO", "Tradução concluída com sucesso", {
"original_length": len(text),
"translated_length": len(translated_text),
"ratio": length_ratio
})
return translated_text
else:
error_text = await response.text()
storage.add_log("ERROR", "Erro na tradução", {
"status": response.status,
"error": error_text
})
raise Exception(f"Erro na tradução: {error_text}")
except Exception as e:
storage.add_log("ERROR", "Erro no processo de tradução", {
"error": str(e),
"type": type(e).__name__
})
raise

View File

@@ -1,15 +1,22 @@
#!/bin/bash
# Função para inicializar configurações no Redis
# Função para inicializar configurações no Redis se não existirem
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"
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET GROQ_API_KEY "sua_api_key_aqui" NX
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET BUSINESS_MESSAGE "*Impacte AI* Premium Services" NX
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET PROCESS_GROUP_MESSAGES "false" NX
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET PROCESS_SELF_MESSAGES "true" NX
redis-cli -h $REDIS_HOST -p $REDIS_PORT SET API_DOMAIN "$API_DOMAIN" NX
}
# Inicializar configurações no Redis
# Aguardar o Redis estar pronto
echo "Aguardando o Redis ficar disponível..."
until redis-cli -h $REDIS_HOST -p $REDIS_PORT PING; do
echo "Redis não está pronto - aguardando..."
sleep 5
done
# Inicializar configurações no Redis (apenas se não existirem)
initialize_redis_config
# Iniciar o FastAPI em background

View File

@@ -31,6 +31,13 @@ class StorageHandler:
self.log_retention_hours = int(os.getenv('LOG_RETENTION_HOURS', 48))
self.backup_retention_days = int(os.getenv('BACKUP_RETENTION_DAYS', 7))
# Garantir valores padrão para configurações de idioma
if not self.redis.exists(self._get_redis_key("auto_translation")):
self.redis.set(self._get_redis_key("auto_translation"), "false")
if not self.redis.exists(self._get_redis_key("auto_language_detection")):
self.redis.set(self._get_redis_key("auto_language_detection"), "false")
def _get_redis_key(self, key):
return f"transcrevezap:{key}"
@@ -168,4 +175,235 @@ class StorageHandler:
if self.redis.ttl(key) <= 0:
self.redis.delete(key)
except Exception as e:
self.logger.error(f"Erro ao limpar backups antigos: {e}")
self.logger.error(f"Erro ao limpar backups antigos: {e}")
# Método de rotação de chaves groq
def get_groq_keys(self) -> List[str]:
"""Obtém todas as chaves GROQ armazenadas."""
return list(self.redis.smembers(self._get_redis_key("groq_keys")))
def add_groq_key(self, key: str):
"""Adiciona uma nova chave GROQ ao conjunto."""
if key and key.startswith("gsk_"):
self.redis.sadd(self._get_redis_key("groq_keys"), key)
return True
return False
def remove_groq_key(self, key: str):
"""Remove uma chave GROQ do conjunto."""
self.redis.srem(self._get_redis_key("groq_keys"), key)
def get_next_groq_key(self) -> str:
"""
Obtém a próxima chave GROQ no sistema de rodízio.
Utiliza um contador no Redis para controlar a rotação.
"""
keys = self.get_groq_keys()
if not keys:
return None
# Obtém e incrementa o contador de rodízio
counter = int(self.redis.get(self._get_redis_key("groq_key_counter")) or "0")
next_counter = (counter + 1) % len(keys)
self.redis.set(self._get_redis_key("groq_key_counter"), str(next_counter))
return keys[counter % len(keys)]
def get_message_settings(self):
"""Obtém as configurações de mensagens."""
return {
"summary_header": self.redis.get(self._get_redis_key("summary_header")) or "🤖 *Resumo do áudio:*",
"transcription_header": self.redis.get(self._get_redis_key("transcription_header")) or "🔊 *Transcrição do áudio:*",
"output_mode": self.redis.get(self._get_redis_key("output_mode")) or "both",
"character_limit": int(self.redis.get(self._get_redis_key("character_limit")) or "500"),
}
def save_message_settings(self, settings: dict):
"""Salva as configurações de mensagens."""
for key, value in settings.items():
self.redis.set(self._get_redis_key(key), str(value))
def get_process_mode(self):
"""Retorna o modo de processamento configurado"""
mode = self.redis.get(self._get_redis_key("process_mode")) or "all"
self.logger.debug(f"Modo de processamento atual: {mode}")
return mode
def get_contact_language(self, contact_id: str) -> str:
"""
Obtém o idioma configurado para um contato específico.
O contact_id pode vir com ou sem @s.whatsapp.net
"""
# Remover @s.whatsapp.net se presente
contact_id = contact_id.split('@')[0]
return self.redis.hget(self._get_redis_key("contact_languages"), contact_id)
def set_contact_language(self, contact_id: str, language: str):
"""
Define o idioma para um contato específico
"""
# Remover @s.whatsapp.net se presente
contact_id = contact_id.split('@')[0]
self.redis.hset(self._get_redis_key("contact_languages"), contact_id, language)
self.logger.info(f"Idioma {language} definido para o contato {contact_id}")
def get_all_contact_languages(self) -> dict:
"""
Retorna um dicionário com todos os contatos e seus idiomas configurados
"""
return self.redis.hgetall(self._get_redis_key("contact_languages"))
def remove_contact_language(self, contact_id: str):
"""
Remove a configuração de idioma de um contato
"""
contact_id = contact_id.split('@')[0]
self.redis.hdel(self._get_redis_key("contact_languages"), contact_id)
self.logger.info(f"Configuração de idioma removida para o contato {contact_id}")
def get_auto_language_detection(self) -> bool:
"""
Verifica se a detecção automática de idioma está ativada
"""
return self.redis.get(self._get_redis_key("auto_language_detection")) == "true"
def set_auto_language_detection(self, enabled: bool):
"""
Ativa ou desativa a detecção automática de idioma
"""
self.redis.set(self._get_redis_key("auto_language_detection"), str(enabled).lower())
self.logger.info(f"Detecção automática de idioma {'ativada' if enabled else 'desativada'}")
def get_auto_translation(self) -> bool:
"""
Verifica se a tradução automática está ativada
"""
return self.redis.get(self._get_redis_key("auto_translation")) == "true"
def set_auto_translation(self, enabled: bool):
"""
Ativa ou desativa a tradução automática
"""
self.redis.set(self._get_redis_key("auto_translation"), str(enabled).lower())
self.logger.info(f"Tradução automática {'ativada' if enabled else 'desativada'}")
def record_language_usage(self, language: str, from_me: bool, auto_detected: bool = False):
"""
Registra estatísticas de uso de idiomas
Args:
language: Código do idioma (ex: 'pt', 'en')
from_me: Se o áudio foi enviado por nós
auto_detected: Se o idioma foi detectado automaticamente
"""
try:
# Validar idioma
if not language:
self.add_log("WARNING", "Tentativa de registrar uso sem idioma definido")
return
# Incrementar contagem total do idioma
self.redis.hincrby(
self._get_redis_key("language_stats"),
f"{language}_total",
1
)
# Incrementar contagem por direção (enviado/recebido)
direction = 'sent' if from_me else 'received'
self.redis.hincrby(
self._get_redis_key("language_stats"),
f"{language}_{direction}",
1
)
# Se foi detecção automática, registrar
if auto_detected:
self.redis.hincrby(
self._get_redis_key("language_stats"),
f"{language}_auto_detected",
1
)
# Registrar última utilização
self.redis.hset(
self._get_redis_key("language_stats"),
f"{language}_last_used",
datetime.now().isoformat()
)
# Log detalhado
self.add_log("DEBUG", "Uso de idioma registrado", {
"language": language,
"direction": direction,
"auto_detected": auto_detected
})
except Exception as e:
self.add_log("ERROR", "Erro ao registrar uso de idioma", {
"error": str(e),
"type": type(e).__name__
})
def get_language_statistics(self) -> Dict:
"""
Obtém estatísticas de uso de idiomas
"""
try:
stats_raw = self.redis.hgetall(self._get_redis_key("language_stats"))
# Organizar estatísticas por idioma
stats = {}
for key, value in stats_raw.items():
lang, metric = key.split('_', 1)
if lang not in stats:
stats[lang] = {}
if metric == 'last_used':
stats[lang][metric] = value
else:
stats[lang][metric] = int(value)
return stats
except Exception as e:
self.logger.error(f"Erro ao obter estatísticas de idioma: {e}")
return {}
def cache_language_detection(self, contact_id: str, language: str, confidence: float = 1.0):
"""
Armazena em cache o idioma detectado para um contato
"""
contact_id = contact_id.split('@')[0]
cache_data = {
'language': language,
'confidence': confidence,
'timestamp': datetime.now().isoformat(),
'auto_detected': True
}
self.redis.hset(
self._get_redis_key("language_detection_cache"),
contact_id,
json.dumps(cache_data)
)
def get_cached_language(self, contact_id: str) -> Dict:
"""
Obtém o idioma em cache para um contato
Retorna None se não houver cache ou se estiver expirado
"""
contact_id = contact_id.split('@')[0]
cached = self.redis.hget(
self._get_redis_key("language_detection_cache"),
contact_id
)
if not cached:
return None
try:
data = json.loads(cached)
# Verificar se o cache expirou (24 horas)
cache_time = datetime.fromisoformat(data['timestamp'])
if datetime.now() - cache_time > timedelta(hours=24):
return None
return data
except:
return None