Compare commits

...

8 Commits
2.3.1 ... main

Author SHA1 Message Date
Fábio Cavalcanti
c42476549e ajuste audio source download 2025-02-22 08:13:38 -03:00
Fábio Cavalcanti
e89787e715 ajuste readme 2025-01-24 22:48:06 -03:00
Fábio Cavalcanti
f63dcc40d1 adicionado sistema de seleção de provedor de llm para GROQ ou Open ai 2025-01-24 22:27:02 -03:00
Fábio Cavalcanti
af20510b2b corrigido sistem de validação de chaves groq 2025-01-24 21:06:13 -03:00
Fábio Cavalcanti
a25dc9c4e7 ajuste para verificar formdata e json na chamada groq 2025-01-23 15:54:21 -03:00
Fábio Cavalcanti
a4ba9d02bc ajuste identacao 2025-01-23 15:38:49 -03:00
Fábio Cavalcanti
4c7d346a3c ajuste identacao 2025-01-23 15:27:35 -03:00
Fábio Cavalcanti
be82707ccc melhoria no sistema de chaves groq 2025-01-23 15:19:39 -03:00
8 changed files with 536 additions and 201 deletions

View File

@ -39,6 +39,8 @@ class Settings:
"""Inicializa as configurações.""" """Inicializa as configurações."""
logger.debug("Carregando configurações do Redis...") logger.debug("Carregando configurações do Redis...")
self.ACTIVE_LLM_PROVIDER = self.get_redis_value("ACTIVE_LLM_PROVIDER", "groq")
self.OPENAI_API_KEY = self.get_redis_value("OPENAI_API_KEY", "")
self.GROQ_API_KEY = self.get_redis_value("GROQ_API_KEY", "gsk_default_key") self.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.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_GROUP_MESSAGES = self.get_redis_value("PROCESS_GROUP_MESSAGES", "false").lower() == "true"

111
groq_handler.py Normal file
View File

@ -0,0 +1,111 @@
import aiohttp
import json
from typing import Optional, Tuple, Any
from datetime import datetime
import logging
from storage import StorageHandler
import asyncio
logger = logging.getLogger("GROQHandler")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
async def test_groq_key(key: str) -> bool:
"""Teste se uma chave GROQ é válida e está funcionando."""
url = "https://api.groq.com/openai/v1/models"
headers = {"Authorization": f"Bearer {key}"}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
if response.status == 200:
data = await response.json()
return bool(data.get("data"))
return False
except Exception as e:
logger.error(f"Erro ao testar chave GROQ: {e}")
return False
async def validate_transcription_response(response_text: str) -> bool:
"""Valide se a resposta da transcrição é significativa."""
try:
cleaned_text = response_text.strip()
return len(cleaned_text) >= 10
except Exception as e:
logger.error(f"Erro ao validar resposta da transcrição: {e}")
return False
async def get_working_groq_key(storage: StorageHandler) -> Optional[str]:
"""Obtenha uma chave GROQ funcional do pool disponível."""
keys = storage.get_groq_keys()
for _ in range(len(keys)):
key = storage.get_next_groq_key()
if not key:
continue
penalized_until = storage.get_penalized_until(key)
if penalized_until and penalized_until > datetime.utcnow():
continue
if await test_groq_key(key):
return key
else:
storage.penalize_key(key, penalty_duration=300)
storage.add_log("ERROR", "Nenhuma chave GROQ funcional disponível.")
return None
async def handle_groq_request(
url: str,
headers: dict,
data: Any,
storage: StorageHandler,
is_form_data: bool = False
) -> Tuple[bool, dict, str]:
"""Lida com requisições para a API GROQ com suporte a retries e rotação de chaves."""
max_retries = len(storage.get_groq_keys())
for attempt in range(max_retries):
try:
storage.add_log("DEBUG", "Iniciando tentativa de requisição para GROQ", {
"url": url,
"is_form_data": is_form_data,
"attempt": attempt + 1
})
async with aiohttp.ClientSession() as session:
if is_form_data:
async with session.post(url, headers=headers, data=data) as response:
response_data = await response.json()
if response.status == 200 and response_data.get("text"):
return True, response_data, ""
else:
async with session.post(url, headers=headers, json=data) as response:
response_data = await response.json()
if response.status == 200 and response_data.get("choices"):
return True, response_data, ""
error_msg = response_data.get("error", {}).get("message", "")
if "organization_restricted" in error_msg or "invalid_api_key" in error_msg:
new_key = await get_working_groq_key(storage)
if new_key:
headers["Authorization"] = f"Bearer {new_key}"
await asyncio.sleep(1)
continue
return False, response_data, error_msg
except Exception as e:
storage.add_log("ERROR", "Erro na requisição", {"error": str(e)})
if attempt < max_retries - 1:
await asyncio.sleep(1)
continue
return False, {}, f"Request failed: {str(e)}"
storage.add_log("ERROR", "Todas as chaves GROQ falharam.")
return False, {}, "All GROQ keys exhausted."

12
main.py
View File

@ -5,6 +5,7 @@ from services import (
send_message_to_whatsapp, send_message_to_whatsapp,
get_audio_base64, get_audio_base64,
summarize_text_if_needed, summarize_text_if_needed,
download_remote_audio,
) )
from models import WebhookRequest from models import WebhookRequest
from config import logger, settings, redis_client from config import logger, settings, redis_client
@ -151,17 +152,14 @@ async def transcreve_audios(request: Request):
# Obter áudio # Obter áudio
try: try:
if "mediaUrl" in body["data"]["message"]: if "mediaUrl" in body["data"]["message"]:
audio_source = body["data"]["message"]["mediaUrl"] media_url = body["data"]["message"]["mediaUrl"]
storage.add_log("DEBUG", "Usando mediaUrl para áudio", { storage.add_log("DEBUG", "Baixando áudio via URL", {"mediaUrl": media_url})
"mediaUrl": audio_source audio_source = await download_remote_audio(media_url) # Baixa o arquivo remoto e retorna o caminho local
})
else: else:
storage.add_log("DEBUG", "Obtendo áudio via base64") storage.add_log("DEBUG", "Obtendo áudio via base64")
base64_audio = await get_audio_base64(server_url, instance, apikey, audio_key) base64_audio = await get_audio_base64(server_url, instance, apikey, audio_key)
audio_source = await convert_base64_to_file(base64_audio) audio_source = await convert_base64_to_file(base64_audio)
storage.add_log("DEBUG", "Áudio convertido", { storage.add_log("DEBUG", "Áudio convertido", {"source": audio_source})
"source": audio_source
})
# Carregar configurações de formatação # Carregar configurações de formatação
output_mode = get_config("output_mode", "both") output_mode = get_config("output_mode", "both")

View File

@ -252,7 +252,7 @@ def login_page():
# Modificar a função de logout no dashboard # Modificar a função de logout no dashboard
def dashboard(): def dashboard():
# Versão do sistema # Versão do sistema
APP_VERSION = "2.3.1" APP_VERSION = "2.3.3"
show_logo() show_logo()
st.sidebar.markdown('<div class="sidebar-header">TranscreveZAP - Menu</div>', unsafe_allow_html=True) st.sidebar.markdown('<div class="sidebar-header">TranscreveZAP - Menu</div>', unsafe_allow_html=True)
@ -728,9 +728,10 @@ def manage_settings():
st.title("⚙️ Configurações") st.title("⚙️ Configurações")
# Criar tabs para melhor organização # Criar tabs para melhor organização
tab1, tab2, tab3, tab4 = st.tabs([ tab1, tab2, tab3, tab4, tab5 = st.tabs([
"🔑 Chaves API", "🔑 Chaves API",
"🌐 Configurações Gerais", "🤖 Provedor LLM",
"🌐 Configurações Gerais",
"📝 Formatação de Mensagens", "📝 Formatação de Mensagens",
"🗣️ Idiomas e Transcrição" "🗣️ Idiomas e Transcrição"
]) ])
@ -787,6 +788,46 @@ def manage_settings():
pass pass
with tab2: with tab2:
st.subheader("Configuração do Provedor LLM")
# Select provider
current_provider = storage.get_llm_provider()
provider = st.selectbox(
"Provedor de Serviço",
options=["groq", "openai"],
format_func=lambda x: "Groq (Open Source)" if x == "groq" else "OpenAI (API Paga)",
index=0 if current_provider == "groq" else 1
)
if provider == "openai":
st.info("""
A OpenAI é um serviço pago que requer uma chave API válida.
Obtenha sua chave em https://platform.openai.com
""")
# OpenAI Key Management
openai_key = st.text_input(
"OpenAI API Key",
type="password",
help="Chave que começa com 'sk-'"
)
if st.button("Adicionar Chave OpenAI"):
if openai_key and openai_key.startswith("sk-"):
storage.add_openai_key(openai_key)
st.success("✅ Chave OpenAI adicionada com sucesso!")
else:
st.error("Chave inválida! Deve começar com 'sk-'")
# Save provider selection
if st.button("💾 Salvar Configuração do Provedor"):
try:
storage.set_llm_provider(provider)
st.success(f"Provedor alterado para: {provider}")
except Exception as e:
st.error(f"Erro ao salvar provedor: {str(e)}")
with tab3:
st.subheader("Configurações do Sistema") st.subheader("Configurações do Sistema")
# Business Message # Business Message
@ -850,7 +891,7 @@ def manage_settings():
) )
pass pass
with tab3: with tab4:
st.subheader("Formatação de Mensagens") st.subheader("Formatação de Mensagens")
# Headers personalizados # Headers personalizados
@ -935,7 +976,7 @@ def manage_settings():
st.error(f"Erro ao salvar configurações: {str(e)}") st.error(f"Erro ao salvar configurações: {str(e)}")
with tab4: with tab5:
st.subheader("Idiomas e Transcrição") st.subheader("Idiomas e Transcrição")
# Adicionar estatísticas no topo # Adicionar estatísticas no topo

74
openai_handler.py Normal file
View File

@ -0,0 +1,74 @@
import aiohttp
import json
from datetime import datetime
import logging
from storage import StorageHandler
logger = logging.getLogger("OpenAIHandler")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
async def test_openai_key(key: str) -> bool:
"""Test if an OpenAI key is valid and working."""
url = "https://api.openai.com/v1/models"
headers = {"Authorization": f"Bearer {key}"}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
if response.status == 200:
data = await response.json()
return len(data.get("data", [])) > 0
return False
except Exception as e:
logger.error(f"Error testing OpenAI key: {e}")
return False
async def handle_openai_request(
url: str,
headers: dict,
data: any,
storage: StorageHandler,
is_form_data: bool = False
) -> tuple[bool, dict, str]:
"""Handle requests to OpenAI API with retries."""
max_retries = 3
for attempt in range(max_retries):
try:
async with aiohttp.ClientSession() as session:
if is_form_data:
async with session.post(url, headers=headers, data=data) as response:
response_data = await response.json()
if response.status == 200:
if is_form_data and response_data.get("text"):
return True, response_data, ""
elif not is_form_data and response_data.get("choices"):
return True, response_data, ""
else:
async with session.post(url, headers=headers, json=data) as response:
response_data = await response.json()
if response.status == 200 and response_data.get("choices"):
return True, response_data, ""
error_msg = response_data.get("error", {}).get("message", "")
if "invalid_api_key" in error_msg or "invalid authorization" in error_msg.lower():
logger.error(f"OpenAI API key invalid or expired")
return False, response_data, error_msg
if attempt < max_retries - 1:
continue
return False, response_data, error_msg
except Exception as e:
logger.error(f"Error in request: {str(e)}")
if attempt < max_retries - 1:
continue
return False, {}, f"Request failed: {str(e)}"
return False, {}, "All retries failed"

View File

@ -11,6 +11,7 @@ Uma solução completa para automatizar e gerenciar mensagens de áudio no Whats
- Transcrição automática multilíngue - Transcrição automática multilíngue
- Resumos inteligentes de áudios - Resumos inteligentes de áudios
- Detecção e tradução automática entre idiomas - Detecção e tradução automática entre idiomas
- Seleção de plataforma LLM (GROQ ou OpenAI)
- Interface administrativa completa - Interface administrativa completa
- Sistema de rodízio de chaves API - Sistema de rodízio de chaves API
- Gestão avançada de grupos e usuários - Gestão avançada de grupos e usuários
@ -28,10 +29,21 @@ Antes de começar, certifique-se de ter os seguintes requisitos:
- Python 3.10+ instalado ([Download](https://www.python.org/downloads/)) - Python 3.10+ instalado ([Download](https://www.python.org/downloads/))
- Docker e Docker Compose instalados ([Instruções](https://docs.docker.com/get-docker/)) - Docker e Docker Compose instalados ([Instruções](https://docs.docker.com/get-docker/))
- Uma conta Evolution API com chave válida - Uma conta Evolution API com chave válida
- No mínimo uma conta GROQ API com chave válida (começa com 'gsk_') ([Crie sua CONTA](https://console.groq.com/login)) - Chaves GROQ (começa com `gsk_`) e/ou chaves OpenAI (começa com `sk-`) configuradas ([Crie sua conta GROQ](https://console.groq.com/login))
* Em caso de uso com Proxy Reverso Aponte um Subdomínio para a API e outro para o MANAGER da aplicação * Em caso de uso com Proxy Reverso Aponte um Subdomínio para a API e outro para o MANAGER da aplicação
--- ---
## 🚀 **Novidade: Escolha do Provedor LLM**
Agora você pode escolher entre dois provedores para transcrições e resumos:
1. **GROQ** (open-source): Configuração padrão.
2. **OpenAI** (API paga): Integração com modelos GPT.
### Configuração:
- Acesse: **Configurações > Provedor LLM** na interface administrativa.
- Escolha entre `groq` e `openai`.
- Adicione as chaves correspondentes para cada provedor.
---
## 🚀 **Instalação e Configuração** ## 🚀 **Instalação e Configuração**
### 🐳 Docker Compose ### 🐳 Docker Compose

View File

@ -7,7 +7,8 @@ from storage import StorageHandler
import os import os
import json import json
import tempfile import tempfile
import traceback
from groq_handler import get_working_groq_key, validate_transcription_response, handle_groq_request
# Inicializa o storage handler # Inicializa o storage handler
storage = StorageHandler() storage = StorageHandler()
@ -46,6 +47,7 @@ async def summarize_text_if_needed(text):
storage.add_log("DEBUG", "Iniciando processo de resumo", { storage.add_log("DEBUG", "Iniciando processo de resumo", {
"text_length": len(text) "text_length": len(text)
}) })
provider = storage.get_llm_provider()
# Obter idioma configurado # Obter idioma configurado
language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt" language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt"
@ -53,10 +55,20 @@ async def summarize_text_if_needed(text):
"language": language, "language": language,
"redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE") "redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE")
}) })
url_completions = "https://api.groq.com/openai/v1/chat/completions"
groq_key = await get_groq_key() if provider == "openai":
api_key = storage.get_openai_keys()[0]
url = "https://api.openai.com/v1/chat/completions"
model = "gpt-4o-mini"
else: # groq
url = "https://api.groq.com/openai/v1/chat/completions"
api_key = await get_working_groq_key(storage)
if not api_key:
raise Exception("Nenhuma chave GROQ disponível")
model = "llama-3.3-70b-versatile"
headers = { headers = {
"Authorization": f"Bearer {groq_key}", "Authorization": f"Bearer {api_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
@ -140,29 +152,33 @@ async def summarize_text_if_needed(text):
"role": "user", "role": "user",
"content": f"{base_prompt}\n\nTexto para resumir: {text}", "content": f"{base_prompt}\n\nTexto para resumir: {text}",
}], }],
"model": "llama-3.3-70b-versatile", "model": model,
} }
try: try:
async with aiohttp.ClientSession() as session: success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
storage.add_log("DEBUG", "Enviando requisição para API GROQ") if not success:
async with session.post(url_completions, headers=headers, json=json_data) as summary_response: raise Exception(error)
if summary_response.status == 200:
summary_result = await summary_response.json() summary_text = response_data["choices"][0]["message"]["content"]
summary_text = summary_result["choices"][0]["message"]["content"] # Validar se o resumo não está vazio
storage.add_log("INFO", "Resumo gerado com sucesso", { if not await validate_transcription_response(summary_text):
"original_length": len(text), storage.add_log("ERROR", "Resumo vazio ou inválido recebido")
"summary_length": len(summary_text), raise Exception("Resumo vazio ou inválido recebido")
"language": language # Validar se o resumo é menor que o texto original
}) if len(summary_text) >= len(text):
return summary_text storage.add_log("WARNING", "Resumo maior que texto original", {
else: "original_length": len(text),
error_text = await summary_response.text() "summary_length": len(summary_text)
storage.add_log("ERROR", "Erro na API GROQ", { })
"error": error_text, storage.add_log("INFO", "Resumo gerado com sucesso", {
"status": summary_response.status "original_length": len(text),
}) "summary_length": len(summary_text),
raise Exception(f"Erro ao resumir o texto: {error_text}") "language": language
})
return summary_text
except Exception as e: except Exception as e:
storage.add_log("ERROR", "Erro no processo de resumo", { storage.add_log("ERROR", "Erro no processo de resumo", {
"error": str(e), "error": str(e),
@ -188,10 +204,20 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
"from_me": from_me, "from_me": from_me,
"remote_jid": remote_jid "remote_jid": remote_jid
}) })
provider = storage.get_llm_provider()
url = "https://api.groq.com/openai/v1/audio/transcriptions" if provider == "openai":
groq_key = await get_groq_key() api_key = storage.get_openai_keys()[0] # Get first OpenAI key
groq_headers = {"Authorization": f"Bearer {groq_key}"} url = "https://api.openai.com/v1/audio/transcriptions"
model = "whisper-1"
else: # groq
api_key = await get_working_groq_key(storage)
if not api_key:
raise Exception("Nenhuma chave GROQ disponível")
url = "https://api.groq.com/openai/v1/audio/transcriptions"
model = "whisper-large-v3"
headers = {"Authorization": f"Bearer {api_key}"}
# Inicializar variáveis # Inicializar variáveis
contact_language = None contact_language = None
@ -226,29 +252,28 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
elif not from_me: # Só detecta em mensagens recebidas elif not from_me: # Só detecta em mensagens recebidas
try: try:
# Realizar transcrição inicial sem idioma específico # Realizar transcrição inicial sem idioma específico
data = aiohttp.FormData() with open(audio_source, 'rb') as audio_file:
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3') data = aiohttp.FormData()
data.add_field('model', 'whisper-large-v3') data.add_field('file', audio_file, filename='audio.mp3')
data.add_field('model', model)
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=groq_headers, data=data) as response: success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True)
if response.status == 200: if success:
initial_result = await response.json() initial_text = response_data.get("text", "")
initial_text = initial_result.get("text", "")
# Detectar idioma do texto transcrito
# Detectar idioma do texto transcrito detected_lang = await detect_language(initial_text)
detected_lang = await detect_language(initial_text)
# Salvar no cache E na configuração do contato
# Salvar no cache E na configuração do contato storage.cache_language_detection(contact_id, detected_lang)
storage.cache_language_detection(contact_id, detected_lang) storage.set_contact_language(contact_id, detected_lang)
storage.set_contact_language(contact_id, detected_lang)
contact_language = detected_lang
contact_language = detected_lang storage.add_log("INFO", "Idioma detectado e configurado", {
storage.add_log("INFO", "Idioma detectado e configurado", { "language": detected_lang,
"language": detected_lang, "remote_jid": remote_jid,
"remote_jid": remote_jid, "auto_detected": True
"auto_detected": True })
})
except Exception as e: except Exception as e:
storage.add_log("WARNING", "Erro na detecção automática de idioma", { storage.add_log("WARNING", "Erro na detecção automática de idioma", {
"error": str(e), "error": str(e),
@ -300,76 +325,73 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
try: try:
# Realizar transcrição # Realizar transcrição
data = aiohttp.FormData() with open(audio_source, 'rb') as audio_file:
data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3') data = aiohttp.FormData()
data.add_field('model', 'whisper-large-v3') data.add_field('file', audio_file, filename='audio.mp3')
data.add_field('language', transcription_language) data.add_field('model', model)
data.add_field('language', transcription_language)
if use_timestamps:
data.add_field('response_format', 'verbose_json')
# Realizar transcrição if use_timestamps:
async with aiohttp.ClientSession() as session: data.add_field('response_format', 'verbose_json')
async with session.post(url, headers=groq_headers, data=data) as response:
if response.status != 200: # Usar handle_groq_request para ter retry e validação
error_text = await response.text() success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True)
storage.add_log("ERROR", "Erro na transcrição", { if not success:
"error": error_text, raise Exception(f"Erro na transcrição: {error}")
"status": response.status
transcription = format_timestamped_result(response_data) if use_timestamps else response_data.get("text", "")
# Validar o conteúdo da transcrição
if not await validate_transcription_response(transcription):
storage.add_log("ERROR", "Transcrição vazia ou inválida recebida")
raise Exception("Transcrição vazia ou inválida recebida")
# Detecção automática para novos contatos
if (is_private and storage.get_auto_language_detection() and
not from_me and not contact_language):
try:
detected_lang = await detect_language(transcription)
storage.cache_language_detection(remote_jid, detected_lang)
contact_language = detected_lang
storage.add_log("INFO", "Idioma detectado e cacheado", {
"language": detected_lang,
"remote_jid": remote_jid
}) })
raise Exception(f"Erro na transcrição: {error_text}") except Exception as e:
storage.add_log("WARNING", "Erro na detecção de idioma", {"error": str(e)})
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 # Tradução quando necessário
if (is_private and storage.get_auto_language_detection() and need_translation = (
not from_me and not contact_language): is_private and contact_language and
try: (
detected_lang = await detect_language(transcription) (from_me and transcription_language != target_language) or
storage.cache_language_detection(remote_jid, detected_lang) (not from_me and target_language != transcription_language)
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 if need_translation:
need_translation = ( try:
is_private and contact_language and transcription = await translate_text(
( transcription,
(from_me and transcription_language != target_language) or transcription_language,
(not from_me and target_language != 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)})
if need_translation: # Registrar estatísticas de uso
try: used_language = contact_language if contact_language else system_language
transcription = await translate_text( storage.record_language_usage(
transcription, used_language,
transcription_language, from_me,
target_language bool(contact_language and contact_language != system_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 return transcription, use_timestamps
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: except Exception as e:
storage.add_log("ERROR", "Erro no processo de transcrição", { storage.add_log("ERROR", "Erro no processo de transcrição", {
@ -423,6 +445,7 @@ async def detect_language(text: str) -> str:
Returns: Returns:
str: Código ISO 639-1 do idioma detectado str: Código ISO 639-1 do idioma detectado
""" """
provider = storage.get_llm_provider()
storage.add_log("DEBUG", "Iniciando detecção de idioma", { storage.add_log("DEBUG", "Iniciando detecção de idioma", {
"text_length": len(text) "text_length": len(text)
}) })
@ -432,11 +455,19 @@ async def detect_language(text: str) -> str:
"pt", "en", "es", "fr", "de", "it", "ja", "ko", "pt", "en", "es", "fr", "de", "it", "ja", "ko",
"zh", "ro", "ru", "ar", "hi", "nl", "pl", "tr" "zh", "ro", "ru", "ar", "hi", "nl", "pl", "tr"
} }
if provider == "openai":
url_completions = "https://api.groq.com/openai/v1/chat/completions" api_key = storage.get_openai_keys()[0]
groq_key = await get_groq_key() url = "https://api.openai.com/v1/chat/completions"
model = "gpt-4o-mini"
else: # groq
url = "https://api.groq.com/openai/v1/chat/completions"
api_key = await get_working_groq_key(storage)
if not api_key:
raise Exception("Nenhuma chave GROQ disponível")
model = "llama-3.3-70b-versatile"
headers = { headers = {
"Authorization": f"Bearer {groq_key}", "Authorization": f"Bearer {api_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
@ -465,37 +496,30 @@ async def detect_language(text: str) -> str:
"role": "user", "role": "user",
"content": f"{prompt}\n\n{text[:500]}" # Limitando para os primeiros 500 caracteres "content": f"{prompt}\n\n{text[:500]}" # Limitando para os primeiros 500 caracteres
}], }],
"model": "llama-3.3-70b-versatile", "model": model,
"temperature": 0.1 "temperature": 0.1
} }
try: try:
async with aiohttp.ClientSession() as session: success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
storage.add_log("DEBUG", "Enviando requisição para API GROQ - Detecção de idioma") if not success:
async with session.post(url_completions, headers=headers, json=json_data) as response: raise Exception(f"Falha na detecção de idioma: {error}")
if response.status == 200:
result = await response.json() detected_language = response_data["choices"][0]["message"]["content"].strip().lower()
detected_language = result["choices"][0]["message"]["content"].strip().lower()
# Validar o resultado
# Validar o resultado if detected_language not in SUPPORTED_LANGUAGES:
if detected_language not in SUPPORTED_LANGUAGES: storage.add_log("WARNING", "Idioma detectado não suportado", {
storage.add_log("WARNING", "Idioma detectado não suportado", { "detected": detected_language,
"detected": detected_language, "fallback": "en"
"fallback": "en" })
}) detected_language = "en"
detected_language = "en"
storage.add_log("INFO", "Idioma detectado com sucesso", {
storage.add_log("INFO", "Idioma detectado com sucesso", { "detected_language": detected_language
"detected_language": detected_language })
}) return 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: except Exception as e:
storage.add_log("ERROR", "Erro no processo de detecção de idioma", { storage.add_log("ERROR", "Erro no processo de detecção de idioma", {
"error": str(e), "error": str(e),
@ -650,24 +674,33 @@ async def translate_text(text: str, source_language: str, target_language: str)
Returns: Returns:
str: Texto traduzido str: Texto traduzido
""" """
provider = storage.get_llm_provider()
storage.add_log("DEBUG", "Iniciando tradução", { storage.add_log("DEBUG", "Iniciando tradução", {
"source_language": source_language, "source_language": source_language,
"target_language": target_language, "target_language": target_language,
"text_length": len(text) "text_length": len(text)
}) })
# Se os idiomas forem iguais, retorna o texto original # Se os idiomas forem iguais, retorna o texto original
if source_language == target_language: if source_language == target_language:
return text return text
url_completions = "https://api.groq.com/openai/v1/chat/completions" if provider == "openai":
groq_key = await get_groq_key() api_key = storage.get_openai_keys()[0]
url = "https://api.openai.com/v1/chat/completions"
model = "gpt-4o-mini"
else: # groq
url = "https://api.groq.com/openai/v1/chat/completions"
api_key = await get_working_groq_key(storage)
if not api_key:
raise Exception("Nenhuma chave GROQ disponível")
model = "llama-3.3-70b-versatile"
headers = { headers = {
"Authorization": f"Bearer {groq_key}", "Authorization": f"Bearer {api_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
# Prompt melhorado com contexto e instruções específicas
prompt = f""" prompt = f"""
Você é um tradutor profissional especializado em manter o tom e estilo do texto original. Você é um tradutor profissional especializado em manter o tom e estilo do texto original.
@ -692,43 +725,63 @@ async def translate_text(text: str, source_language: str, target_language: str)
"role": "user", "role": "user",
"content": prompt "content": prompt
}], }],
"model": "llama-3.3-70b-versatile", "model": model,
"temperature": 0.3 "temperature": 0.3
} }
try: try:
async with aiohttp.ClientSession() as session: success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False)
storage.add_log("DEBUG", "Enviando requisição de tradução") if not success:
async with session.post(url_completions, headers=headers, json=json_data) as response: raise Exception(f"Falha na tradução: {error}")
if response.status == 200:
result = await response.json() translated_text = response_data["choices"][0]["message"]["content"].strip()
translated_text = result["choices"][0]["message"]["content"].strip()
# Verificar se a tradução manteve aproximadamente o mesmo tamanho
# Verificar se a tradução manteve aproximadamente o mesmo tamanho length_ratio = len(translated_text) / len(text)
length_ratio = len(translated_text) / len(text) if not (0.5 <= length_ratio <= 1.5):
if not (0.5 <= length_ratio <= 1.5): storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", {
storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", { "original_length": len(text),
"original_length": len(text), "translated_length": len(translated_text),
"translated_length": len(translated_text), "ratio": length_ratio
"ratio": length_ratio })
})
# Validar se a tradução não está vazia
storage.add_log("INFO", "Tradução concluída com sucesso", { if not await validate_transcription_response(translated_text):
"original_length": len(text), storage.add_log("ERROR", "Tradução vazia ou inválida recebida")
"translated_length": len(translated_text), raise Exception("Tradução vazia ou inválida recebida")
"ratio": length_ratio
}) storage.add_log("INFO", "Tradução concluída com sucesso", {
return translated_text "original_length": len(text),
else: "translated_length": len(translated_text),
error_text = await response.text() "ratio": length_ratio
storage.add_log("ERROR", "Erro na tradução", { })
"status": response.status,
"error": error_text return translated_text
})
raise Exception(f"Erro na tradução: {error_text}")
except Exception as e: except Exception as e:
storage.add_log("ERROR", "Erro no processo de tradução", { storage.add_log("ERROR", "Erro no processo de tradução", {
"error": str(e), "error": str(e),
"type": type(e).__name__ "type": type(e).__name__
}) })
raise raise
# Nova função para baixar áudio remoto
async def download_remote_audio(url: str) -> str:
"""
Baixa um arquivo de áudio remoto e salva localmente como um arquivo temporário.
Retorna o caminho para o arquivo salvo.
"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
audio_data = await response.read()
# Cria um arquivo temporário para armazenar o áudio (pode ajustar o sufixo caso necessário)
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
temp_file.write(audio_data)
local_path = temp_file.name
return local_path
else:
raise Exception(f"Falha no download, código de status: {response.status}")
except Exception as e:
raise Exception(f"Erro ao baixar áudio remoto: {str(e)}")

View File

@ -1,6 +1,6 @@
import json import json
import os import os
from typing import List, Dict from typing import List, Dict, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
import traceback import traceback
import logging import logging
@ -209,6 +209,29 @@ class StorageHandler:
return keys[counter % len(keys)] return keys[counter % len(keys)]
def get_penalized_until(self, key: str) -> Optional[datetime]:
"""
Retorna o timestamp até quando a chave está penalizada, ou None se não estiver penalizada.
"""
penalized_key = self._get_redis_key(f"groq_key_penalized_{key}")
penalized_until = self.redis.get(penalized_key)
if penalized_until:
return datetime.fromisoformat(penalized_until)
return None
def penalize_key(self, key: str, penalty_duration: int):
"""
Penaliza uma chave por um tempo determinado (em segundos).
"""
penalized_key = self._get_redis_key(f"groq_key_penalized_{key}")
penalized_until = datetime.utcnow() + timedelta(seconds=penalty_duration)
self.redis.set(penalized_key, penalized_until.isoformat())
self.redis.expire(penalized_key, penalty_duration) # Expira a chave após o tempo de penalidade
self.add_log("INFO", "Chave GROQ penalizada", {
"key": key,
"penalized_until": penalized_until.isoformat()
})
def get_message_settings(self): def get_message_settings(self):
"""Obtém as configurações de mensagens.""" """Obtém as configurações de mensagens."""
return { return {
@ -670,4 +693,25 @@ class StorageHandler:
} }
self.redis.lpush(key, json.dumps(failed_delivery)) self.redis.lpush(key, json.dumps(failed_delivery))
# Manter apenas as últimas 100 falhas # Manter apenas as últimas 100 falhas
self.redis.ltrim(key, 0, 99) self.redis.ltrim(key, 0, 99)
def get_llm_provider(self) -> str:
"""Returns active LLM provider (groq or openai)"""
return self.redis.get(self._get_redis_key("active_llm_provider")) or "groq"
def set_llm_provider(self, provider: str):
"""Sets active LLM provider"""
if provider not in ["groq", "openai"]:
raise ValueError("Provider must be 'groq' or 'openai'")
self.redis.set(self._get_redis_key("active_llm_provider"), provider)
def get_openai_keys(self) -> List[str]:
"""Get stored OpenAI API keys"""
return list(self.redis.smembers(self._get_redis_key("openai_keys")))
def add_openai_key(self, key: str):
"""Add OpenAI API key"""
if key and key.startswith("sk-"):
self.redis.sadd(self._get_redis_key("openai_keys"), key)
return True
return False