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,8 +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 = st.tabs([ tab1, tab2, tab3, tab4, tab5 = 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"
@ -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
if not await validate_transcription_response(summary_text):
storage.add_log("ERROR", "Resumo vazio ou inválido recebido")
raise Exception("Resumo vazio ou inválido recebido")
# Validar se o resumo é menor que o texto original
if len(summary_text) >= len(text):
storage.add_log("WARNING", "Resumo maior que texto original", {
"original_length": len(text),
"summary_length": len(summary_text)
})
storage.add_log("INFO", "Resumo gerado com sucesso", { storage.add_log("INFO", "Resumo gerado com sucesso", {
"original_length": len(text), "original_length": len(text),
"summary_length": len(summary_text), "summary_length": len(summary_text),
"language": language "language": language
}) })
return summary_text return summary_text
else:
error_text = await summary_response.text()
storage.add_log("ERROR", "Erro na API GROQ", {
"error": error_text,
"status": summary_response.status
})
raise Exception(f"Erro ao resumir o texto: {error_text}")
except Exception as e: 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()
if provider == "openai":
api_key = storage.get_openai_keys()[0] # Get first OpenAI 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" url = "https://api.groq.com/openai/v1/audio/transcriptions"
groq_key = await get_groq_key() model = "whisper-large-v3"
groq_headers = {"Authorization": f"Bearer {groq_key}"}
headers = {"Authorization": f"Bearer {api_key}"}
# Inicializar variáveis # Inicializar variáveis
contact_language = None contact_language = None
@ -226,15 +252,14 @@ 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 # Detectar idioma do texto transcrito
detected_lang = await detect_language(initial_text) detected_lang = await detect_language(initial_text)
@ -300,29 +325,26 @@ 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: if use_timestamps:
data.add_field('response_format', 'verbose_json') data.add_field('response_format', 'verbose_json')
# Realizar transcrição # Usar handle_groq_request para ter retry e validação
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 not success:
if response.status != 200: raise Exception(f"Erro na transcrição: {error}")
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() transcription = format_timestamped_result(response_data) if use_timestamps else response_data.get("text", "")
# Processar resposta baseado no formato # Validar o conteúdo da transcrição
transcription = format_timestamped_result(result) if use_timestamps else result.get("text", "") 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 # Detecção automática para novos contatos
if (is_private and storage.get_auto_language_detection() and if (is_private and storage.get_auto_language_detection() and
@ -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":
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"
url_completions = "https://api.groq.com/openai/v1/chat/completions"
groq_key = await get_groq_key()
headers = { headers = {
"Authorization": f"Bearer {groq_key}", "Authorization": f"Bearer {api_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
@ -465,17 +496,16 @@ 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:
@ -489,13 +519,7 @@ async def detect_language(text: str) -> str:
"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,6 +674,7 @@ 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,
@ -660,14 +685,22 @@ async def translate_text(text: str, source_language: str, target_language: str)
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,17 +725,16 @@ 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)
@ -713,22 +745,43 @@ async def translate_text(text: str, source_language: str, target_language: str)
"ratio": length_ratio "ratio": length_ratio
}) })
# Validar se a tradução não está vazia
if not await validate_transcription_response(translated_text):
storage.add_log("ERROR", "Tradução vazia ou inválida recebida")
raise Exception("Tradução vazia ou inválida recebida")
storage.add_log("INFO", "Tradução concluída com sucesso", { storage.add_log("INFO", "Tradução concluída com sucesso", {
"original_length": len(text), "original_length": len(text),
"translated_length": len(translated_text), "translated_length": len(translated_text),
"ratio": length_ratio "ratio": length_ratio
}) })
return translated_text 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: 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 {
@ -671,3 +694,24 @@ 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