Compare commits

..

No commits in common. "main" and "2.3.1" have entirely different histories.
main ... 2.3.1

8 changed files with 201 additions and 536 deletions

View File

@ -39,8 +39,6 @@ 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"

View File

@ -1,111 +0,0 @@
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,7 +5,6 @@ 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
@ -152,14 +151,17 @@ async def transcreve_audios(request: Request):
# Obter áudio # Obter áudio
try: try:
if "mediaUrl" in body["data"]["message"]: if "mediaUrl" in body["data"]["message"]:
media_url = body["data"]["message"]["mediaUrl"] audio_source = body["data"]["message"]["mediaUrl"]
storage.add_log("DEBUG", "Baixando áudio via URL", {"mediaUrl": media_url}) storage.add_log("DEBUG", "Usando mediaUrl para áudio", {
audio_source = await download_remote_audio(media_url) # Baixa o arquivo remoto e retorna o caminho local "mediaUrl": audio_source
})
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", {"source": audio_source}) storage.add_log("DEBUG", "Áudio convertido", {
"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.3" APP_VERSION = "2.3.1"
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,10 +728,9 @@ 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, tab5 = st.tabs([ tab1, tab2, tab3, tab4 = st.tabs([
"🔑 Chaves API", "🔑 Chaves API",
"🤖 Provedor LLM", "🌐 Configurações Gerais",
"🌐 Configurações Gerais",
"📝 Formatação de Mensagens", "📝 Formatação de Mensagens",
"🗣️ Idiomas e Transcrição" "🗣️ Idiomas e Transcrição"
]) ])
@ -788,46 +787,6 @@ 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
@ -891,7 +850,7 @@ def manage_settings():
) )
pass pass
with tab4: with tab3:
st.subheader("Formatação de Mensagens") st.subheader("Formatação de Mensagens")
# Headers personalizados # Headers personalizados
@ -976,7 +935,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 tab5: with tab4:
st.subheader("Idiomas e Transcrição") st.subheader("Idiomas e Transcrição")
# Adicionar estatísticas no topo # Adicionar estatísticas no topo

View File

@ -1,74 +0,0 @@
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,7 +11,6 @@ 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
@ -29,21 +28,10 @@ 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
- Chaves GROQ (começa com `gsk_`) e/ou chaves OpenAI (começa com `sk-`) configuradas ([Crie sua conta GROQ](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 * 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,8 +7,7 @@ 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()
@ -47,7 +46,6 @@ 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"
@ -55,20 +53,10 @@ 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"
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 {api_key}", "Authorization": f"Bearer {groq_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
@ -152,33 +140,29 @@ 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": model, "model": "llama-3.3-70b-versatile",
} }
try: try:
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False) async with aiohttp.ClientSession() as session:
if not success: storage.add_log("DEBUG", "Enviando requisição para API GROQ")
raise Exception(error) async with session.post(url_completions, headers=headers, json=json_data) as summary_response:
if summary_response.status == 200:
summary_text = response_data["choices"][0]["message"]["content"] summary_result = await summary_response.json()
# Validar se o resumo não está vazio summary_text = summary_result["choices"][0]["message"]["content"]
if not await validate_transcription_response(summary_text): storage.add_log("INFO", "Resumo gerado com sucesso", {
storage.add_log("ERROR", "Resumo vazio ou inválido recebido") "original_length": len(text),
raise Exception("Resumo vazio ou inválido recebido") "summary_length": len(summary_text),
# Validar se o resumo é menor que o texto original "language": language
if len(summary_text) >= len(text): })
storage.add_log("WARNING", "Resumo maior que texto original", { return summary_text
"original_length": len(text), else:
"summary_length": len(summary_text) error_text = await summary_response.text()
}) storage.add_log("ERROR", "Erro na API GROQ", {
storage.add_log("INFO", "Resumo gerado com sucesso", { "error": error_text,
"original_length": len(text), "status": summary_response.status
"summary_length": len(summary_text), })
"language": language raise Exception(f"Erro ao resumir o texto: {error_text}")
})
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),
@ -204,20 +188,10 @@ 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()
if provider == "openai": url = "https://api.groq.com/openai/v1/audio/transcriptions"
api_key = storage.get_openai_keys()[0] # Get first OpenAI key groq_key = await get_groq_key()
url = "https://api.openai.com/v1/audio/transcriptions" groq_headers = {"Authorization": f"Bearer {groq_key}"}
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
@ -252,28 +226,29 @@ 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
with open(audio_source, 'rb') as audio_file: data = aiohttp.FormData()
data = aiohttp.FormData() data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
data.add_field('file', audio_file, filename='audio.mp3') data.add_field('model', 'whisper-large-v3')
data.add_field('model', model)
async with aiohttp.ClientSession() as session:
success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True) async with session.post(url, headers=groq_headers, data=data) as response:
if success: if response.status == 200:
initial_text = response_data.get("text", "") initial_result = await response.json()
initial_text = initial_result.get("text", "")
# Detectar idioma do texto transcrito
detected_lang = await detect_language(initial_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) # Salvar no cache E na configuração do contato
storage.set_contact_language(contact_id, detected_lang) 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", { contact_language = detected_lang
"language": detected_lang, storage.add_log("INFO", "Idioma detectado e configurado", {
"remote_jid": remote_jid, "language": detected_lang,
"auto_detected": True "remote_jid": remote_jid,
}) "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),
@ -325,73 +300,76 @@ async def transcribe_audio(audio_source, apikey=None, remote_jid=None, from_me=F
try: try:
# Realizar transcrição # Realizar transcrição
with open(audio_source, 'rb') as audio_file: data = aiohttp.FormData()
data = aiohttp.FormData() data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3')
data.add_field('file', audio_file, filename='audio.mp3') data.add_field('model', 'whisper-large-v3')
data.add_field('model', model) data.add_field('language', transcription_language)
data.add_field('language', transcription_language)
if use_timestamps:
data.add_field('response_format', 'verbose_json')
if use_timestamps: # Realizar transcrição
data.add_field('response_format', 'verbose_json') async with aiohttp.ClientSession() as session:
async with session.post(url, headers=groq_headers, data=data) as response:
# Usar handle_groq_request para ter retry e validação if response.status != 200:
success, response_data, error = await handle_groq_request(url, headers, data, storage, is_form_data=True) error_text = await response.text()
if not success: storage.add_log("ERROR", "Erro na transcrição", {
raise Exception(f"Erro na transcrição: {error}") "error": error_text,
"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
}) })
except Exception as e: raise Exception(f"Erro na transcrição: {error_text}")
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", "")
# Tradução quando necessário # Detecção automática para novos contatos
need_translation = ( if (is_private and storage.get_auto_language_detection() and
is_private and contact_language and not from_me and not contact_language):
( try:
(from_me and transcription_language != target_language) or detected_lang = await detect_language(transcription)
(not from_me and target_language != transcription_language) 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)})
if need_translation: # Tradução quando necessário
try: need_translation = (
transcription = await translate_text( is_private and contact_language and
transcription, (
transcription_language, (from_me and transcription_language != target_language) or
target_language (not from_me and target_language != transcription_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 if need_translation:
used_language = contact_language if contact_language else system_language try:
storage.record_language_usage( transcription = await translate_text(
used_language, transcription,
from_me, transcription_language,
bool(contact_language and contact_language != system_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)})
return transcription, use_timestamps # 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: except Exception as e:
storage.add_log("ERROR", "Erro no processo de transcrição", { storage.add_log("ERROR", "Erro no processo de transcrição", {
@ -445,7 +423,6 @@ 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)
}) })
@ -455,19 +432,11 @@ 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":
api_key = storage.get_openai_keys()[0] url_completions = "https://api.groq.com/openai/v1/chat/completions"
url = "https://api.openai.com/v1/chat/completions" groq_key = await get_groq_key()
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 {api_key}", "Authorization": f"Bearer {groq_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
@ -496,30 +465,37 @@ 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": model, "model": "llama-3.3-70b-versatile",
"temperature": 0.1 "temperature": 0.1
} }
try: try:
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False) async with aiohttp.ClientSession() as session:
if not success: storage.add_log("DEBUG", "Enviando requisição para API GROQ - Detecção de idioma")
raise Exception(f"Falha na detecção de idioma: {error}") async with session.post(url_completions, headers=headers, json=json_data) as response:
if response.status == 200:
detected_language = response_data["choices"][0]["message"]["content"].strip().lower() result = await response.json()
detected_language = result["choices"][0]["message"]["content"].strip().lower()
# Validar o resultado
if detected_language not in SUPPORTED_LANGUAGES: # Validar o resultado
storage.add_log("WARNING", "Idioma detectado não suportado", { if detected_language not in SUPPORTED_LANGUAGES:
"detected": detected_language, storage.add_log("WARNING", "Idioma detectado não suportado", {
"fallback": "en" "detected": detected_language,
}) "fallback": "en"
detected_language = "en" })
detected_language = "en"
storage.add_log("INFO", "Idioma detectado com sucesso", {
"detected_language": detected_language storage.add_log("INFO", "Idioma detectado com sucesso", {
}) "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),
@ -674,33 +650,24 @@ 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
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 {api_key}", "Authorization": f"Bearer {groq_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.
@ -725,63 +692,43 @@ async def translate_text(text: str, source_language: str, target_language: str)
"role": "user", "role": "user",
"content": prompt "content": prompt
}], }],
"model": model, "model": "llama-3.3-70b-versatile",
"temperature": 0.3 "temperature": 0.3
} }
try: try:
success, response_data, error = await handle_groq_request(url, headers, json_data, storage, is_form_data=False) async with aiohttp.ClientSession() as session:
if not success: storage.add_log("DEBUG", "Enviando requisição de tradução")
raise Exception(f"Falha na tradução: {error}") async with session.post(url_completions, headers=headers, json=json_data) as response:
if response.status == 200:
translated_text = response_data["choices"][0]["message"]["content"].strip() 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) # Verificar se a tradução manteve aproximadamente o mesmo tamanho
if not (0.5 <= length_ratio <= 1.5): length_ratio = len(translated_text) / len(text)
storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", { if not (0.5 <= length_ratio <= 1.5):
"original_length": len(text), storage.add_log("WARNING", "Possível erro na tradução - diferença significativa no tamanho", {
"translated_length": len(translated_text), "original_length": len(text),
"ratio": length_ratio "translated_length": len(translated_text),
}) "ratio": length_ratio
})
# Validar se a tradução não está vazia
if not await validate_transcription_response(translated_text): storage.add_log("INFO", "Tradução concluída com sucesso", {
storage.add_log("ERROR", "Tradução vazia ou inválida recebida") "original_length": len(text),
raise Exception("Tradução vazia ou inválida recebida") "translated_length": len(translated_text),
"ratio": length_ratio
storage.add_log("INFO", "Tradução concluída com sucesso", { })
"original_length": len(text), return translated_text
"translated_length": len(translated_text), else:
"ratio": length_ratio error_text = await response.text()
}) storage.add_log("ERROR", "Erro na tradução", {
"status": response.status,
return translated_text "error": error_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, Optional from typing import List, Dict
from datetime import datetime, timedelta from datetime import datetime, timedelta
import traceback import traceback
import logging import logging
@ -209,29 +209,6 @@ 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 {
@ -693,25 +670,4 @@ 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