diff --git a/config.py b/config.py index 0439507..2a1043b 100644 --- a/config.py +++ b/config.py @@ -49,7 +49,8 @@ def __init__(self): self.PROCESS_SELF_MESSAGES = self.get_redis_value("PROCESS_SELF_MESSAGES", "true").lower() == "true" self.DEBUG_MODE = os.getenv("DEBUG_MODE", "false").lower() == "true" self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() - + self.TRANSCRIPTION_LANGUAGE = self.get_redis_value("TRANSCRIPTION_LANGUAGE", "pt") + # Mascarar chave ao logar if self.GROQ_API_KEY: masked_key = f"{self.GROQ_API_KEY[:10]}...{self.GROQ_API_KEY[-4:]}" diff --git a/manager.py b/manager.py index 95cc3e0..cbd7dd8 100644 --- a/manager.py +++ b/manager.py @@ -114,6 +114,7 @@ def load_settings(): "BUSINESS_MESSAGE": get_from_redis("BUSINESS_MESSAGE", "*Impacte AI* Premium Services"), "PROCESS_GROUP_MESSAGES": get_from_redis("PROCESS_GROUP_MESSAGES", "false"), "PROCESS_SELF_MESSAGES": get_from_redis("PROCESS_SELF_MESSAGES", "true"), + "TRANSCRIPTION_LANGUAGE": get_from_redis("TRANSCRIPTION_LANGUAGE", "pt"), } except Exception as e: st.error(f"Erro ao carregar configurações do Redis: {e}") @@ -344,12 +345,143 @@ def manage_blocks(): def manage_settings(): st.title("⚙️ Configurações") st.subheader("Configurações do Sistema") - st.text_input("GROQ_API_KEY", value=st.session_state.settings["GROQ_API_KEY"], key="groq_api_key") - st.text_input("Mensagem de Serviço no Rodapé", value=st.session_state.settings["BUSINESS_MESSAGE"], key="business_message") - st.selectbox("Processar Mensagens em Grupos", options=["true", "false"], index=["true", "false"].index(st.session_state.settings["PROCESS_GROUP_MESSAGES"]), key="process_group_messages") - st.selectbox("Processar Mensagens Próprias", options=["true", "false"], index=["true", "false"].index(st.session_state.settings["PROCESS_SELF_MESSAGES"]), key="process_self_messages") - if st.button("Salvar Configurações"): - save_settings() + + # Seção de chaves GROQ com sistema de rodízio + st.subheader("🔑 Gerenciamento de Chaves GROQ") + + # Campo para chave principal (mantendo compatibilidade) + main_key = st.text_input( + "GROQ API Key Principal", + value=st.session_state.settings["GROQ_API_KEY"], + key="groq_api_key", + type="password", + help="Chave GROQ principal do sistema" + ) + + # Seção de chaves adicionais + st.markdown("---") + st.subheader("Chaves GROQ Adicionais (Sistema de Rodízio)") + + # Exibir chaves existentes + groq_keys = storage.get_groq_keys() + if groq_keys: + st.write("Chaves configuradas para rodízio:") + for key in groq_keys: + col1, col2 = st.columns([4, 1]) + with col1: + masked_key = f"{key[:10]}...{key[-4:]}" + st.code(masked_key, language=None) + with col2: + if st.button("🗑️", key=f"remove_{key}", help="Remover esta chave"): + storage.remove_groq_key(key) + st.success(f"Chave removida do rodízio!") + st.experimental_rerun() + + # Adicionar nova chave + new_key = st.text_input( + "Adicionar Nova Chave GROQ", + key="new_groq_key", + type="password", + help="Insira uma nova chave GROQ para adicionar ao sistema de rodízio" + ) + col1, col2 = st.columns([4, 1]) + with col1: + if st.button("➕ Adicionar ao Rodízio", help="Adicionar esta chave ao sistema de rodízio"): + if new_key: + if new_key.startswith("gsk_"): + storage.add_groq_key(new_key) + st.success("Nova chave adicionada ao sistema de rodízio!") + st.experimental_rerun() + else: + st.error("Chave inválida! A chave deve começar com 'gsk_'") + else: + st.warning("Por favor, insira uma chave válida") + + # Outras configurações do sistema + st.markdown("---") + st.subheader("Outras Configurações") + + # Business Message + st.text_input( + "Mensagem de Serviço no Rodapé", + value=st.session_state.settings["BUSINESS_MESSAGE"], + key="business_message" + ) + + # Process Group Messages + st.selectbox( + "Processar Mensagens em Grupos", + options=["true", "false"], + index=["true", "false"].index(st.session_state.settings["PROCESS_GROUP_MESSAGES"]), + key="process_group_messages" + ) + + # Process Self Messages + st.selectbox( + "Processar Mensagens Próprias", + options=["true", "false"], + index=["true", "false"].index(st.session_state.settings["PROCESS_SELF_MESSAGES"]), + key="process_self_messages" + ) + + # Nova seção de configuração de idioma + st.markdown("---") + st.subheader("🌐 Configuração de Idioma") + + # Dicionário de idiomas em português + IDIOMAS = { + "pt": "Português", + "en": "Inglês", + "es": "Espanhol", + "fr": "Francês", + "de": "Alemão", + "it": "Italiano", + "ja": "Japonês", + "ko": "Coreano", + "zh": "Chinês", + "ru": "Russo", + "ar": "Árabe", + "hi": "Hindi", + "nl": "Holandês", + "pl": "Polonês", + "tr": "Turco" + } + + # Carregar configuração atual de idioma + current_language = get_from_redis("TRANSCRIPTION_LANGUAGE", "pt") + + # Seleção de idioma + selected_language = st.selectbox( + "Idioma para Transcrição e Resumo", + options=list(IDIOMAS.keys()), + format_func=lambda x: IDIOMAS[x], + index=list(IDIOMAS.keys()).index(current_language) if current_language in IDIOMAS else 0, + help="Selecione o idioma para transcrição dos áudios e geração dos resumos", + key="transcription_language" + ) + + # Botão de salvar com feedback visual + if st.button("💾 Salvar Todas as Configurações"): + try: + # Salvar configurações principais + save_settings() + + # Se há uma chave principal, adicionar ao sistema de rodízio + if main_key and main_key.startswith("gsk_"): + storage.add_groq_key(main_key) + + # Salvar configuração de idioma + save_to_redis("TRANSCRIPTION_LANGUAGE", selected_language) + + st.success("✅ Todas as configurações foram salvas com sucesso!") + + # Mostrar resumo das chaves ativas e idioma selecionado + total_keys = len(storage.get_groq_keys()) + st.info(f"""Sistema configurado com {total_keys} chave(s) GROQ no rodízio + Idioma definido: {IDIOMAS[selected_language]}""") + + except Exception as e: + st.error(f"Erro ao salvar configurações: {str(e)}") if "authenticated" not in st.session_state: st.session_state.authenticated = False diff --git a/readme.md b/readme.md index 45a9464..33ffc51 100644 --- a/readme.md +++ b/readme.md @@ -219,6 +219,62 @@ Para usar com Traefik, certifique-se de: - Em produção, recomenda-se DEBUG_MODE=false - Configure LOG_LEVEL=DEBUG apenas para troubleshooting +## ✨ Novos Recursos na v2.1 + +### 🌍 Suporte Multilíngue +- Transcrição e resumo em 10+ idiomas +- Mudança instantânea de idioma +- Interface intuitiva para seleção de idioma +- Mantém consistência entre transcrição e resumo + +### 🔄 Sistema Inteligente de Rodízio de Chaves +- Suporte a múltiplas chaves GROQ +- Balanceamento automático de carga +- Maior redundância e disponibilidade +- Gestão simplificada de chaves via interface + +## 🌍 Sistema de Idiomas +O TranscreveZAP agora suporta transcrição e resumo em múltiplos idiomas. Na seção "Configurações", você pode: + +1. Selecionar o idioma principal para transcrição e resumo +2. O sistema manterá Português como padrão se nenhum outro for selecionado +3. A mudança de idioma é aplicada instantaneamente após salvar + +Idiomas suportados: +- 🇧🇷 Português (padrão) +- 🇺🇸 Inglês +- 🇪🇸 Espanhol +- 🇫🇷 Francês +- 🇩🇪 Alemão +- 🇮🇹 Italiano +- 🇯🇵 Japonês +- 🇰🇷 Coreano +- 🇨🇳 Chinês +- 🇷🇺 Russo + +## 🔄 Sistema de Rodízio de Chaves GROQ +O TranscreveZAP agora suporta múltiplas chaves GROQ com sistema de rodízio automático para melhor distribuição de carga e redundância. + +### Funcionalidades: +1. Adicione múltiplas chaves GROQ para distribuição de carga +2. O sistema alterna automaticamente entre as chaves disponíveis +3. Se uma chave falhar, o sistema usa a próxima disponível +4. Visualize todas as chaves configuradas no painel +5. Adicione ou remova chaves sem interromper o serviço + +### Como Configurar: +1. Acesse a seção "Configurações" +2. Na área "🔑 Gerenciamento de Chaves GROQ": + - Adicione a chave principal + - Use "Adicionar Nova Chave GROQ" para incluir chaves adicionais + - O sistema começará a usar todas as chaves em rodízio automaticamente + +### Boas Práticas: +- Mantenha pelo menos duas chaves ativas para redundância +- Monitore o uso das chaves pelo painel administrativo +- Remova chaves expiradas ou inválidas +- Todas as chaves devem começar com 'gsk_' + ## 🔍 **Troubleshooting** Se encontrar problemas: 1. Verifique se todas as variáveis obrigatórias estão configuradas @@ -226,6 +282,18 @@ Se encontrar problemas: 3. Verifique os logs do container 4. Certifique-se que as APIs estão acessíveis +### Problemas com Múltiplas Chaves GROQ: +1. Verifique se todas as chaves começam com 'gsk_' +2. Confirme se as chaves estão ativas na console GROQ +3. Monitore os logs para identificar falhas específicas de chaves +4. Mantenha pelo menos uma chave válida no sistema + +### Problemas com Idiomas: +1. Verifique se o idioma está corretamente selecionado nas configurações +2. Confirme se a configuração foi salva com sucesso +3. Reinicie o serviço se as alterações não forem aplicadas +4. Verifique os logs para confirmar o idioma em uso + ## 📄 **Licença** Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes. diff --git a/services.py b/services.py index 4239533..91a0cc7 100644 --- a/services.py +++ b/services.py @@ -31,32 +31,107 @@ async def convert_base64_to_file(base64_data): }) raise +async def get_groq_key(): + """Obtém a próxima chave GROQ do sistema de rodízio.""" + key = storage.get_next_groq_key() + if not key: + raise HTTPException( + status_code=500, + detail="Nenhuma chave GROQ configurada. Configure pelo menos uma chave no painel administrativo." + ) + return key + async def summarize_text_if_needed(text): - """Resumir texto usando a API GROQ""" + """Resumir texto usando a API GROQ com sistema de rodízio de chaves""" storage.add_log("DEBUG", "Iniciando processo de resumo", { "text_length": len(text) }) + # Obter idioma configurado + language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt" + storage.add_log("DEBUG", "Idioma configurado para resumo", { + "language": language, + "redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE") + }) url_completions = "https://api.groq.com/openai/v1/chat/completions" + groq_key = await get_groq_key() headers = { - "Authorization": f"Bearer {settings.GROQ_API_KEY}", + "Authorization": f"Bearer {groq_key}", "Content-Type": "application/json", } - + + # Adaptar o prompt para considerar o idioma + prompt_by_language = { + "pt": """ + Entenda o contexto desse áudio e faça um resumo super enxuto sobre o que se trata. + Esse áudio foi enviado pelo whatsapp, de alguém, para Fabio. + Escreva APENAS o resumo do áudio como se fosse você que estivesse enviando + essa mensagem! Não cumprimente, não de oi, não escreva nada antes nem depois + do resumo, responda apenas um resumo enxuto do que foi falado no áudio. + """, + "en": """ + Understand the context of this audio and make a very concise summary of what it's about. + This audio was sent via WhatsApp, from someone, to Fabio. + Write ONLY the summary of the audio as if you were sending this message yourself! + Don't greet, don't say hi, don't write anything before or after the summary, + respond with just a concise summary of what was said in the audio. + """, + "es": """ + Entiende el contexto de este audio y haz un resumen muy conciso sobre de qué se trata. + Este audio fue enviado por WhatsApp, de alguien, para Fabio. + Escribe SOLO el resumen del audio como si tú estuvieras enviando este mensaje. + No saludes, no escribas nada antes ni después del resumen, responde únicamente un resumen conciso de lo dicho en el audio. + """, + "fr": """ + Comprenez le contexte de cet audio et faites un résumé très concis de ce dont il s'agit. + Cet audio a été envoyé via WhatsApp, par quelqu'un, à Fabio. + Écrivez UNIQUEMENT le résumé de l'audio comme si c'était vous qui envoyiez ce message. + Ne saluez pas, n'écrivez rien avant ou après le résumé, répondez seulement par un résumé concis de ce qui a été dit dans l'audio. + """, + "de": """ + Verstehen Sie den Kontext dieses Audios und erstellen Sie eine sehr kurze Zusammenfassung, worum es geht. + Dieses Audio wurde über WhatsApp von jemandem an Fabio gesendet. + Schreiben Sie NUR die Zusammenfassung des Audios, als ob Sie diese Nachricht senden würden. + Grüßen Sie nicht, schreiben Sie nichts vor oder nach der Zusammenfassung, antworten Sie nur mit einer kurzen Zusammenfassung dessen, was im Audio gesagt wurde. + """, + "it": """ + Comprendi il contesto di questo audio e fai un riassunto molto conciso di cosa si tratta. + Questo audio è stato inviato tramite WhatsApp, da qualcuno, a Fabio. + Scrivi SOLO il riassunto dell'audio come se fossi tu a inviare questo messaggio. + Non salutare, non scrivere nulla prima o dopo il riassunto, rispondi solo con un riassunto conciso di ciò che è stato detto nell'audio. + """, + "ja": """ + この音声の内容を理解し、それが何について話されているのかを非常に簡潔に要約してください。 + この音声は、誰かがWhatsAppでファビオに送ったものです。 + あなたがそのメッセージを送っているように、音声の要約だけを記述してください。 + 挨拶や前置き、後書きは書かず、音声で話された内容の簡潔な要約のみを返信してください。 + """, + "ko": """ + 이 오디오의 맥락을 이해하고, 무엇에 관한 것인지 매우 간략하게 요약하세요. + 이 오디오는 누군가가 WhatsApp을 통해 Fabio에게 보낸 것입니다. + 마치 당신이 메시지를 보내는 것처럼 오디오의 요약만 작성하세요. + 인사하거나, 요약 전후로 아무것도 쓰지 말고, 오디오에서 말한 내용을 간략하게 요약한 답변만 하세요. + """, + "zh": """ + 理解这个音频的上下文,并简洁地总结它的内容。 + 这个音频是某人通过WhatsApp发送给Fabio的。 + 请仅以摘要的形式回答,就好像是你在发送这条消息。 + 不要问候,也不要在摘要前后写任何内容,只需用一句简短的话总结音频中所说的内容。 + """, + "ru": """ + Поймите контекст этого аудио и сделайте очень краткое резюме, о чем идет речь. + Это аудио было отправлено через WhatsApp кем-то Фабио. + Напишите ТОЛЬКО резюме аудио, как будто вы отправляете это сообщение. + Не приветствуйте, не пишите ничего до или после резюме, ответьте только кратким резюме того, что говорилось в аудио. + """ + } + + # Usar o prompt do idioma configurado ou fallback para português + base_prompt = prompt_by_language.get(language, prompt_by_language["pt"]) json_data = { "messages": [{ "role": "user", - "content": f""" - Entenda o contexto desse áudio e faça um resumo super enxuto sobre o que se trata. - Esse áudio foi enviado pelo whatsapp, de alguém, para Fabio. - Escreva APENAS o resumo do áudio como se fosse você que estivesse enviando - essa mensagem! Não cumprimente, não de oi, não escreva nada antes nem depois - do resumo, responda apenas um resumo enxuto do que foi falado no áudio. - IMPORTANTE: Não faça esse resumo como se fosse um áudio que uma terceira - pessoa enviou, não diga coisas como 'a pessoa está falando...' etc. - Escreva o resumo com base nessa mensagem do áudio, - como se você estivesse escrevendo esse resumo e enviando em - texto pelo whatsapp de forma impessoal: {text}""", + "content": f"{base_prompt}\n\nTexto para resumir: {text}", }], "model": "llama-3.3-70b-versatile", } @@ -70,7 +145,8 @@ async def summarize_text_if_needed(text): summary_text = summary_result["choices"][0]["message"]["content"] storage.add_log("INFO", "Resumo gerado com sucesso", { "original_length": len(text), - "summary_length": len(summary_text) + "summary_length": len(summary_text), + "language": language }) return summary_text else: @@ -88,11 +164,18 @@ async def summarize_text_if_needed(text): raise async def transcribe_audio(audio_source, apikey=None): - """Transcreve áudio usando a API GROQ""" + """Transcreve áudio usando a API GROQ com sistema de rodízio de chaves""" storage.add_log("INFO", "Iniciando processo de transcrição") url = "https://api.groq.com/openai/v1/audio/transcriptions" - groq_headers = {"Authorization": f"Bearer {settings.GROQ_API_KEY}"} - + groq_key = await get_groq_key() + groq_headers = {"Authorization": f"Bearer {groq_key}"} + # Obter idioma configurado + language = redis_client.get("TRANSCRIPTION_LANGUAGE") or "pt" + storage.add_log("DEBUG", "Idioma configurado para transcrição", { + "language": language, + "redis_value": redis_client.get("TRANSCRIPTION_LANGUAGE") + }) + try: async with aiohttp.ClientSession() as session: # Se o audio_source for uma URL @@ -123,7 +206,7 @@ async def transcribe_audio(audio_source, apikey=None): data = aiohttp.FormData() data.add_field('file', open(audio_source, 'rb'), filename='audio.mp3') data.add_field('model', 'whisper-large-v3') - data.add_field('language', 'pt') + data.add_field('language', language) storage.add_log("DEBUG", "Enviando áudio para transcrição") async with session.post(url, headers=groq_headers, data=data) as response: diff --git a/storage.py b/storage.py index 862caf6..b6ef965 100644 --- a/storage.py +++ b/storage.py @@ -168,4 +168,35 @@ def clean_old_backups(self): if self.redis.ttl(key) <= 0: self.redis.delete(key) except Exception as e: - self.logger.error(f"Erro ao limpar backups antigos: {e}") \ No newline at end of file + self.logger.error(f"Erro ao limpar backups antigos: {e}") + + # Método de rotação de chaves groq + def get_groq_keys(self) -> List[str]: + """Obtém todas as chaves GROQ armazenadas.""" + return list(self.redis.smembers(self._get_redis_key("groq_keys"))) + + def add_groq_key(self, key: str): + """Adiciona uma nova chave GROQ ao conjunto.""" + if key and key.startswith("gsk_"): + self.redis.sadd(self._get_redis_key("groq_keys"), key) + return True + return False + + def remove_groq_key(self, key: str): + """Remove uma chave GROQ do conjunto.""" + self.redis.srem(self._get_redis_key("groq_keys"), key) + + def get_next_groq_key(self) -> str: + """ + Obtém a próxima chave GROQ no sistema de rodízio. + Utiliza um contador no Redis para controlar a rotação. + """ + keys = self.get_groq_keys() + if not keys: + return None + # Obtém e incrementa o contador de rodízio + counter = int(self.redis.get(self._get_redis_key("groq_key_counter")) or "0") + next_counter = (counter + 1) % len(keys) + self.redis.set(self._get_redis_key("groq_key_counter"), str(next_counter)) + + return keys[counter % len(keys)] \ No newline at end of file