from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi.responses import JSONResponse from pydantic import BaseModel import tempfile from typing import Optional from pathlib import Path import os import re import requests import time import json import subprocess import asyncio import sys sys.path.append(str(Path(__file__).parent.parent)) from detect_crop_image import detect_and_crop_image from detect_crop_video import detect_and_crop_video from gemini_webapi import GeminiClient from gemini_webapi.constants import Model from fastapi.staticfiles import StaticFiles app = FastAPI(title="Gemini Chat API", description="API para interagir com Google Gemini (Nova versão)") os.makedirs("static", exist_ok=True) os.makedirs("static/processed", exist_ok=True) app.mount("/static", StaticFiles(directory="static"), name="static") # Cookies extraídos do Request Header (usar variáveis de ambiente para a nuvem) Secure_1PSID = os.getenv("GEMINI_SECURE_1PSID", "PRIVATE") Secure_1PSIDTS = os.getenv("GEMINI_SECURE_1PSIDTS", "PRIVATE") client = None @app.on_event("startup") async def startup_event(): global client print("Iniciando cliente do Gemini em plano de fundo...") client = GeminiClient(Secure_1PSID, Secure_1PSIDTS, proxy=None) # auto_refresh=True fará com que o token __Secure-1PSIDTS seja renovado automaticamente await client.init(timeout=180, auto_close=False, close_delay=300, auto_refresh=True, watchdog_timeout=300) print("Cliente Gemini-API inicializado com sucesso!") @app.get("/") def root(): return {"status": "ok", "message": "Gemini Chat API (gemini-webapi) está funcionando"} def srt_time_to_seconds(timestamp): try: time_part, ms_part = timestamp.split(",") h, m, s = map(int, time_part.split(":")) ms = int(ms_part) return h * 3600 + m * 60 + s + ms / 1000.0 except: return 0.0 def seconds_to_srt_time(seconds): hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) ms = int((seconds % 1) * 1000) return f"{hours:02d}:{minutes:02d}:{secs:02d},{ms:03d}" def clean_and_validate_srt(srt_content): if "```" in srt_content: code_block_pattern = re.compile(r"```(?:srt)?\n(.*?)```", re.DOTALL | re.IGNORECASE) match = code_block_pattern.search(srt_content) if match: srt_content = match.group(1).strip() first_block_pattern = re.compile(r"^\s*\d+\s*\n\d{2}:\d{2}:\d{2},\d{3}", re.MULTILINE) match = first_block_pattern.search(srt_content) if match: srt_content = srt_content[match.start():] pattern = re.compile(r"(\d+)\s*\n([^-\n]+?) --> ([^-\n]+?)\s*\n((?:(?!^\d+\s*\n).+\n?)*)", re.MULTILINE) matches = pattern.findall(srt_content) def corrigir_timestamp(timestamp): timestamp = timestamp.strip() if re.match(r"\d{2}:\d{2}:\d{2},\d{3}", timestamp): return timestamp if re.match(r"\d{2}:\d{2},\d{3}", timestamp): return f"00:{timestamp}" if re.match(r"\d{1}:\d{2},\d{3}", timestamp): parts = timestamp.split(":") return f"00:{parts[0].zfill(2)}:{parts[1]}" if re.match(r"\d{1,2},\d{3}", timestamp): seconds_ms = timestamp.split(",") return f"00:00:{seconds_ms[0].zfill(2)},{seconds_ms[1]}" if re.match(r"\d{2}:\d{2}:\d{3}", timestamp): parts = timestamp.split(":") if len(parts) == 3: h, m, s_ms = parts if len(s_ms) == 3: return f"{h}:{m}:00,{s_ms}" elif len(s_ms) >= 4: s, ms = s_ms[:-3], s_ms[-3:] return f"{h}:{m}:{s.zfill(2)},{ms}" return timestamp srt_corrigido = "" for i, (num, start, end, text) in enumerate(matches, 1): text = text.strip() if not text: continue text_lines = [line.strip() for line in text.split('\n') if line.strip()] if len(text_lines) > 2: text = text_lines[0] + '\n' + ' '.join(text_lines[1:]) srt_corrigido += f"{i}\n{corrigir_timestamp(start)} --> {corrigir_timestamp(end)}\n{text}\n\n" return srt_corrigido.strip() def download_file_with_retry(url: str, max_retries: int = 3, timeout: int = 300): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'Accept': '*/*' } for attempt in range(max_retries): try: if attempt > 0: time.sleep(2 ** attempt) response = requests.get(url, headers=headers, timeout=timeout, stream=True) if response.status_code == 429: wait_time = int(response.headers.get('Retry-After', (2 ** attempt) * 5)) time.sleep(wait_time) continue response.raise_for_status() return response except requests.exceptions.HTTPError as e: if e.response.status_code == 429 and attempt < max_retries - 1: continue elif attempt == max_retries - 1: raise HTTPException(status_code=400, detail=str(e)) except requests.exceptions.RequestException as e: if attempt == max_retries - 1: raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail="Falha ao baixar arquivo") def extract_json_from_text(text: str): text = text.strip() if "```json" in text: text = text.split("```json")[1].split("```")[0].strip() elif "```" in text: parts = text.split("```") if len(parts) >= 2: text = parts[1].strip() start_idx = text.find('[') end_idx = text.rfind(']') if start_idx != -1 and end_idx != -1: text = text[start_idx:end_idx+1] else: start_idx = text.find('{') end_idx = text.rfind('}') if start_idx != -1 and end_idx != -1: text = text[start_idx:end_idx+1] text = re.sub(r',\s*([\]}])', r'\1', text) try: return json.loads(text) except: return None def cut_video(input_path: str, output_path: str, start: str, end: str): try: command = ["ffmpeg", "-y", "-i", input_path, "-ss", start, "-to", end, "-c:v", "libx264", "-c:a", "aac", "-strict", "experimental", output_path] subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return True except subprocess.CalledProcessError as e: print(f"Erro ao cortar vídeo: {e.stderr.decode()}") return False def groq_json_to_srt(data): srt_output = "" segments = data.get("segments") or [] for i, segment in enumerate(segments, 1): srt_output += f"{i}\n{seconds_to_srt_time(segment['start'])} --> {seconds_to_srt_time(segment['end'])}\n{segment['text'].strip()}\n\n" return srt_output def groq_words_to_text(data): words = data.get("words") or [] if not words: return "" return "\n".join([f" [{w.get('start', 0):.3f}s - {w.get('end', 0):.3f}s] {w.get('word', '').strip()}" for w in words]) def get_gemini_model(model_name: str): """ Mapeador que decide qual modelo do Gemini Client será utilizado baseado na string antiga. """ model_name_lower = model_name.lower() if model_name else "flash" if "thinking" in model_name_lower: return Model.G_3_FLASH_THINKING_AI_FREE elif "pro" in model_name_lower: # Padrão ou o modelo mais inteligente que criamos return Model.G_3_PRO_AI_FREE # Default flash return Model.G_3_FLASH_AI_FREE # ========================================== # ENDPOINTS # ========================================== class ChatRequest(BaseModel): message: str context: Optional[str] = None model: Optional[str] = "flash" @app.post("/chat") async def chat_endpoint(request: ChatRequest): if not client: raise HTTPException(status_code=500, detail="Gemini client is not initialized") try: model_obj = get_gemini_model(request.model) prompt = request.message if request.context: prompt = f"Contexto: {request.context}\n\nMensagem: {request.message}" print(f"💬 Chat request ({request.model}): {prompt[:50]}...") response = await client.generate_content(prompt, model=model_obj) return {"response": response.text} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) class ProcessUrlRequest(BaseModel): url: str context: Optional[str] = "" version: str # "recurvepop" ou "girlsmoodaily" raw_url: Optional[bool] = True text_cut: Optional[bool] = True @app.post("/process-url") async def process_url_endpoint(request: ProcessUrlRequest, background_tasks: BackgroundTasks): """ Cria um registro inicial no Supabase e envia para processamento em background. """ supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/") supabase_key = os.getenv("SUPABASE_KEY", "") if not supabase_url or not supabase_key: raise HTTPException(status_code=500, detail="Credenciais do Supabase não configuradas no ambiente.") headers = { "apikey": supabase_key, "Authorization": f"Bearer {supabase_key}", "Content-Type": "application/json", "Prefer": "return=representation" } # Inserir no Supabase com user_created=True post_payload = { "ig_post_url": request.url, "ig_caption": request.context, "account_target": request.version, "user_created": True, "published": False } res_post = requests.post(f"{supabase_url}/rest/v1/posts", headers=headers, json=post_payload, timeout=10) if not res_post.ok: raise HTTPException(status_code=500, detail=f"Erro ao criar registro no Supabase: {res_post.text}") records = res_post.json() record_id = records[0].get("id") if records else None if not record_id: raise HTTPException(status_code=500, detail="Registro criado, mas ID não retornado pelo Supabase.") background_tasks.add_task(run_process_url, request, record_id) return {"status": "ok", "message": "Solicitação em processamento no background.", "record_id": record_id} async def run_process_url(request: ProcessUrlRequest, record_id: int): """ Processa um post do Instagram a partir de uma URL em background, salvando o resultado. """ if not client: print("Gemini client is not initialized") return t_start = time.time() print(f"🚀 [0.0s] Inciando process-url em background para Record: {record_id}") temp_file = None cropped_file_path = None cropped_video_path = None screenshot_path = None supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/") supabase_key = os.getenv("SUPABASE_KEY", "") headers = { "apikey": supabase_key, "Authorization": f"Bearer {supabase_key}", "Content-Type": "application/json" } try: from agent_config import AGENTS account = request.version if account not in AGENTS: raise Exception(f"Conta '{account}' não configurada.") agent_conf = AGENTS[account]["process"] agent_name = agent_conf["name"] # 1. Chamar a API externa para obter informações do post print(f"🔗 Buscando dados do post via API externa (process-url): {request.url}") api_headers = { "accept": "*/*", "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", "content-type": "application/json", "origin": "http://localhost:5173", "referer": "http://localhost:5173/", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", } api_payload = {"url": request.url, "videoQuality": "best"} api_resp = requests.post( "https://proud-paper-3751.fly.dev/api/json", headers=api_headers, json=api_payload, timeout=60, ) if not api_resp.ok: raise Exception(f"Erro na API externa: {api_resp.text}") api_data = api_resp.json() video_url = api_data.get("url") context = request.context or api_data.get("caption", "") comments = api_data.get("comments", []) if not video_url: raise Exception("API externa não retornou URL de mídia.") print(f"✅ Mídia obtida: {video_url}") # 2. Baixar mídia print(f"📥 Baixando mídia: {video_url}") response = download_file_with_retry(video_url, timeout=600) content_type = response.headers.get("content-type", "").lower() is_image = "image" in content_type post_type = "image" if is_image else "video" # Atualizar type e ig_post_url com o que baixamos patch_initial = { "type": post_type, "ig_post_url": video_url, "ig_caption": context } requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json=patch_initial) if is_image: if "png" in content_type: ext = ".png" elif "webp" in content_type: ext = ".webp" else: ext = ".jpg" else: ext = ".webm" if "webm" in content_type else ".mp4" temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext) for chunk in response.iter_content(chunk_size=1024 * 1024): if chunk: temp_file.write(chunk) temp_file.close() print(f"📥 [{time.time()-t_start:.1f}s] Download concluído.") video_path_to_analyze = temp_file.name files_to_send = [] # Ordem: [cropped, original] # 3. Crop if is_image: print(f"✂️ [{time.time()-t_start:.1f}s] Processando imagem: detectando e cortando...") try: cropped_file_path = detect_and_crop_image(video_path_to_analyze) if cropped_file_path and os.path.exists(cropped_file_path): files_to_send.append(cropped_file_path) except Exception as e: print(f"⚠️ Erro ao cortar imagem: {e}") else: print(f"✂️ [{time.time()-t_start:.1f}s] Processando vídeo: detectando e cortando bordas...") try: cropped_video_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name crop_status = detect_and_crop_video(video_path_to_analyze, cropped_video_path, text_cut=request.text_cut) if crop_status == "aborted_area_too_small": print(f"🚫 [{time.time()-t_start:.1f}s] Crop abortado: área de texto no centro. Negando filtro.") # Notificação do Agente no Discord (Octavio/Diana) try: import urllib.parse as _up reject_msg = f"Analisando aqui, notei que o texto desse vídeo está bem no centro. Se eu cortasse agora, ia acabar destruindo o conteúdo principal. Como a gente preza pela qualidade da página, preferi reprovar esse aqui automaticamente. ❌" _reject_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": reject_msg, "id": discord_id}) requests.get("https://proxy.onrecurve.com/", params={"quest": _reject_url}, timeout=5) except Exception as _e_dc: print(f"⚠️ Erro ao enviar Discord de rejeição automática: {_e_dc}") requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={ "approved_filter": False, "filter_message": "Rejeitado automaticamente: Região útil de texto insuficiente para crop seguro (texto centralizado).", "contains_image": False }) return # Para o processamento aqui if crop_status == "success" and os.path.exists(cropped_video_path): files_to_send.append(cropped_video_path) print(f"✅ [{time.time()-t_start:.1f}s] Vídeo cortado com sucesso.") # Upload antecipado para recurve-save (Performance) print(f"☁️ [{time.time()-t_start:.1f}s] Enviando vídeo cortado antecipadamente para recurve-save...") with open(cropped_video_path, "rb") as vf: vid_upload_resp = requests.post( "https://habulaj-recurve-save.hf.space/upload", files={"files": ("cropped_video.mp4", vf, "video/mp4")}, data={"long_duration": "yes"}, timeout=120, ) vid_upload_resp.raise_for_status() exported_cropped_video_url = vid_upload_resp.json().get("url", "") print(f"✅ [{time.time()-t_start:.1f}s] URL do vídeo cortado: {exported_cropped_video_url}") else: cropped_video_path = None except Exception as e: cropped_video_path = None print(f"⚠️ Erro no processo de crop: {e}") # Adiciona o vídeo original (sempre em segundo se houver crop) files_to_send.append(video_path_to_analyze) # 4. Montar contextos contexto_add = f"\n{context}" if context else "" comentarios_add = "" if comments: comentarios_add = "\nCOMENTÁRIOS DO POST (Use como forte inspiração para criar títulos mais reais e humanizados):\n" for c in comments: if isinstance(c, dict) and (text := c.get("text", "").strip()): comentarios_add += f"- {text} ({c.get('like_count', 0)} curtidas)\n" if is_image: tipo_conteudo_add = "\n\nCONTEXTO DO CONTEÚDO: Este post é uma IMAGEM. O título vai aparecer em cima da imagem." else: tipo_conteudo_add = "" filter_message_add = "" prompt = agent_conf["get_prompt"]( date_str=time.strftime("%d/%m/%Y"), contexto_add=contexto_add, comentarios_add=comentarios_add, tipo_conteudo_add=tipo_conteudo_add, filter_message_add=filter_message_add, ) # 5. Gerar com Gemini model_obj = get_gemini_model("flash") print(f"🧠 [{time.time()-t_start:.1f}s] Enviando para Gemini (flash) [{agent_name}] process-url...") response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj) print(f"✨ [{time.time()-t_start:.1f}s] Resposta do Gemini recebida.") titles_data = extract_json_from_text(response_gemini.text) if not titles_data: print("Failed to parse JSON") return result_json = titles_data if isinstance(titles_data, list) else [titles_data] # 6. Video export final_content_url = None srt_for_export = None exported_cropped_video_url = None if result_json: result_data = result_json[0] if isinstance(result_json[0], dict) else {} title_text = result_data.get("title", "") if not is_image and title_text: title_text = title_text[0].upper() + title_text[1:] if title_text else title_text try: video_for_export = ( cropped_video_path if cropped_video_path and os.path.exists(cropped_video_path) else temp_file.name ) screenshot_path = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg").name duration_probe = subprocess.run( ["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", video_for_export], capture_output=True, text=True, ) video_duration = ( float(duration_probe.stdout.strip()) if duration_probe.returncode == 0 and duration_probe.stdout.strip() else 10.0 ) middle_time = str(video_duration / 2) ffmpeg_ss = subprocess.run( ["ffmpeg", "-y", "-i", video_for_export, "-ss", middle_time, "-frames:v", "1", screenshot_path], capture_output=True, text=True, ) if ffmpeg_ss.returncode == 0: print("📸 Enviando screenshot para recurve-save...") with open(screenshot_path, "rb") as ss_f: upload_resp = requests.post( "https://habulaj-recurve-save.hf.space/upload", files={"files": ("screenshot.jpg", ss_f, "image/jpeg")}, timeout=60, ) upload_resp.raise_for_status() screenshot_url = upload_resp.json().get("url", "") if screenshot_url: # 3. Upload do vídeo cortado para recurve-save (Se ainda não foi feito) if (video_for_export != temp_file.name) and not exported_cropped_video_url: print(f"☁️ [{time.time()-t_start:.1f}s] Enviando vídeo cortado para recurve-save...") with open(video_for_export, "rb") as vf: vid_upload_resp = requests.post( "https://habulaj-recurve-save.hf.space/upload", files={"files": ("cropped_video.mp4", vf, "video/mp4")}, data={"long_duration": "yes"}, timeout=120, ) vid_upload_resp.raise_for_status() exported_cropped_video_url = vid_upload_resp.json().get("url", "") export_video_url = exported_cropped_video_url if exported_cropped_video_url else video_url print(f"📏 [{time.time()-t_start:.1f}s] Preparando export...") import cv2 cap = cv2.VideoCapture(video_for_export) crop_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) if cap.isOpened() else 1080 crop_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) if cap.isOpened() else 1920 cap.release() canvas_width = 1080 image_height = 616 video_dest_height = 1920 - image_height video_template = "horizontal" if crop_w > crop_h else "vertical" if video_template == "vertical": scaled_h = int(crop_h * canvas_width / crop_w) if crop_w > 0 else crop_h video_x, video_y = 0, max(0, (scaled_h - video_dest_height) // 2) else: video_x, video_y = 0, 0 # Legendas needs_legenda = result_data.get("legenda", False) if needs_legenda: print(f"🎙️ [{time.time()-t_start:.1f}s] Gerando legendas (Groq + Gemini)...") try: srt_raw, _, _, _ = await get_groq_srt_base( export_video_url, language="en", temperature=0.4, has_bg_music=False ) if srt_raw and srt_raw.strip(): translate_prompt = f"""IDIOMA: A legenda traduzida DEVE ser inteiramente em PORTUGUÊS DO BRASIL (pt-BR). Independente do idioma original do vídeo. Traduza essa legenda pro português do Brasil, corrija qualquer erro de formatação, pontuação e mantenha timestamps e os textos nos seus respectivos blocos de legenda. Você DEVE se basear estritamente na legenda original fornecida. NUNCA crie legendas novas e NUNCA adicione ou verifique diálogos no áudio que não estejam presentes na legenda original. Apenas traduza. Deve traduzir exatamente o texto da legenda observando o contexto, não é pra migrar, por exemplo, textos de um bloco de legenda pra outro. Deve traduzir exatamente o texto de cada bloco de legenda, manter sempre as palavras, nunca retirar. Mande o SRT completo, sem textos adicionais na resposta, apenas o SRT traduzido. A legenda acima é uma base gerada pelo Whisper que precisa ser limpa e traduzida, não o resultado final. A legenda deve ser totalmente traduzida corretamente analisando o contexto e a entonação de falar. Se alguém estiver gritando, ESCREVA MAIÚSCULO! etc... Adapte gírias e qualquer coisa do tipo. Não deve ser literal a tradução, deve se adaptar. MÚSICA E LETRAS: - NUNCA LEGENDE MÚSICAS OU CANÇÕES. - Se houver música de fundo ou pessoas cantando uma música, IGNORE COMPLETAMENTE e não inclua na legenda. - VOCÊ DEVE LEGENDAR APENAS DIÁLOGOS E FALAS REAIS. PALAVRÕES E CENSURA: - Você DEVE censurar palavras de baixo calão e palavrões pesados utilizando asteriscos. - Substitua a maior parte da palavra censurada e mantenha apenas as primeiras letras. - Exemplo: "filha da puta" se torna "filha da pu**", "caralho" se torna "caral**", "merda" se torna "merd*", "foda" se torna "fod*". EXTREMAMENTE IMPORTANTE: NUNCA legende músicas (quando detectar que é uma música, não legende), apenas diálogos falados. Nunca altere o timing das legendas, deve ser exatamente igual ao original de referência. Nunca legende ações também, exemplo: [Música alta], [Música de encerramento], etc. Deve ser apenas, unicamente, diálogo humano. EXEMPLO: (Original): 1 00:00:01,000 --> 00:00:04,000 hey what are you doing here i thought you left already 2 00:00:04,500 --> 00:00:07,200 yeah i was going to but then i realized i forgot my keys 3 00:00:07,900 --> 00:00:10,500 you always forget something man this is crazy 4 00:00:11,000 --> 00:00:14,000 relax it's not a big deal stop acting like that 5 00:00:14,500 --> 00:00:17,800 i am not acting you said you would be on time 6 00:00:18,000 --> 00:00:21,500 okay okay i'm sorry can we just go now 7 00:00:22,000 --> 00:00:25,000 fine but if we are late again you are a son of a bitch (Traduzido, como você deveria traduzir): 1 00:00:01,000 --> 00:00:04,000 Ué, o que você tá fazendo aqui? Não era pra você já ter ido embora? 2 00:00:04,500 --> 00:00:07,200 Eu ia, mas aí percebi que esqueci minhas chaves. 3 00:00:07,900 --> 00:00:10,500 Cara, você SEMPRE esquece alguma coisa, isso é surreal! 4 00:00:11,000 --> 00:00:14,000 Ah, relaxa! Não é o fim do mundo, para de drama. 5 00:00:14,500 --> 00:00:17,800 Não é drama! Você falou que ia chegar no horário! 6 00:00:18,000 --> 00:00:21,500 Tá, tá... foi mal. Bora logo? 7 00:00:22,000 --> 00:00:25,000 Tá bom, mas se a gente se atrasar de novo, você é um filha da pu**! LEGENDA ORIGINAL: {srt_raw}""" translate_model = get_gemini_model("flash") translate_resp = await client.generate_content(translate_prompt, model=translate_model) translated_srt = translate_resp.text.strip() if "```" in translated_srt: parts = translated_srt.split("```") for part in parts: clean = part.strip() if clean.startswith("srt"): clean = clean[3:].strip() if re.match(r"\d+\s*\n\d{2}:", clean): translated_srt = clean break if translated_srt: srt_for_export = translated_srt except Exception as sub_e: print(f"⚠️ Erro ao gerar legendas: {sub_e}") import urllib.parse title_params = urllib.parse.urlencode({"text": title_text, "version": account}) title_url = f"https://habulaj-recurve-api-img.hf.space/cover/title?{title_params}&image_url={screenshot_url}" result_json[0]["title_url"] = title_url export_payload = { "video_url": export_video_url, "title_url": title_url, "image_height": image_height, "cut_start": 0, "cut_end": video_duration, "video_x": video_x, "video_y": video_y, "video_width": crop_w, "video_height": crop_h, "video_template": video_template, } if srt_for_export: export_payload["subtitles"] = srt_for_export print("🎬 Chamando video export API...") export_resp = requests.post( "https://habulaj-recurve-videos-export.hf.space/video/export", json=export_payload, timeout=600, ) if not export_resp.ok: raise Exception(f"API de video export falhou: {export_resp.status_code}: {export_resp.text}") export_data = export_resp.json() final_content_url = export_data.get("video_url") print(f"✅ Vídeo exportado: {final_content_url}") except Exception as ve: print(f"⚠️ Erro no video export: {ve}") raise Exception(f"Falha no video export: {ve}") elif is_image and title_text and result_data.get("result_type") == "meme": try: img_for_meme = ( cropped_file_path if cropped_file_path and os.path.exists(cropped_file_path) else temp_file.name ) import urllib.parse meme_params = urllib.parse.urlencode({"text": title_text}) final_content_url = f"https://habulaj-recurve-api-img.hf.space/meme?{meme_params}" result_json[0]["title_url"] = final_content_url except Exception as e: print(f"⚠️ Erro ao gerar URL do meme: {e}") if result_json and srt_for_export and isinstance(result_json[0], dict): result_json[0]["subtitle_srt"] = srt_for_export if final_content_url and isinstance(result_json[0], dict): result_json[0]["final_content_url"] = final_content_url # Salvar o resultado no banco patch_final = { "result": result_json } if final_content_url: patch_final["final_content_url"] = final_content_url if exported_cropped_video_url: patch_final["crop_content_url"] = exported_cropped_video_url requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json=patch_final) print(f"✅ /process-url via Background task concluído! Record: {record_id}") except Exception as e: print(f"⚠️ Erro em run_process_url background: {e}") try: error_message = str(e) requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={"result": {"error": error_message}}) except Exception as patch_e: print(f"⚠️ Erro ao salvar erro no Supabase: {patch_e}") finally: if temp_file and os.path.exists(temp_file.name): os.unlink(temp_file.name) if cropped_file_path and os.path.exists(cropped_file_path): os.unlink(cropped_file_path) if cropped_video_path and os.path.exists(cropped_video_path): os.unlink(cropped_video_path) if screenshot_path and os.path.exists(screenshot_path): os.unlink(screenshot_path) @app.api_route("/process/{account}", methods=["GET", "POST"]) async def process_account_endpoint(account: str): if not client: raise HTTPException(status_code=500, detail="Gemini client is not initialized") temp_file = None cropped_file_path = None cropped_video_path = None screenshot_path = None record_id = None cropped_video_url = None try: supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/") supabase_key = os.getenv("SUPABASE_KEY", "") if not supabase_url or not supabase_key: raise HTTPException(status_code=500, detail="Credenciais do Supabase não configuradas no ambiente.") headers = { "apikey": supabase_key, "Authorization": f"Bearer {supabase_key}", "Content-Type": "application/json" } from agent_config import AGENTS if account not in AGENTS: raise HTTPException(status_code=400, detail="Conta não configurada.") agent_conf = AGENTS[account]["process"] agent_name = agent_conf["name"] discord_id = agent_conf["discord_id"] system_discord_id = AGENTS[account].get("system_discord_id", 0) # Buscar 1 post aprovado pelo filtro mas ainda não processado select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&approved_filter=eq.true&result=is.null&user_created=eq.false&limit=1" res_get = requests.get(select_url, headers=headers, timeout=10) if not res_get.ok: raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}") records = res_get.json() if not records: publish_res = await safe_call_publish(account) return {"status": "ok", "message": "Nenhuma postagem pendente para ser processada.", "publish_result": publish_res} t_start = time.time() record = records[0] record_id = record.get("id") video_url = record.get("ig_post_url") crop_url = record.get("crop_content_url") context = record.get("ig_caption", "") comments = record.get("comments") # Se existir no banco, pode ser uma lista contains_image = record.get("contains_image", False) filter_message = record.get("filter_message", "") shortcode = record.get("ig_id") if not comments and shortcode: try: # Chama a API do worker para pegar os comentários se for necessário print(f"📥 Buscando comentários do post {shortcode}...") bot_worker_url = "https://bot.arthurmribeiro51.workers.dev/comments" c_res = requests.get(bot_worker_url, params={"shortcode": shortcode}, timeout=15) if c_res.ok: c_data = c_res.json() fetched_comments = c_data.get("comments", []) if fetched_comments: comments = fetched_comments print(f"✅ Encontrado {len(comments)} comentários para o post.") except Exception as e: print(f"⚠️ Erro ao buscar comentários: {e}") if not video_url: raise HTTPException(status_code=400, detail=f"Registro ID {record_id} falhou: ig_post_url inválida.") try: import urllib.parse as _up _sys_msg = f"🎨 **{agent_name}** começou a processar uma postagem...\n\n📎 **Mídia:** {video_url}" _sys_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": _sys_msg, "id": system_discord_id}) requests.get("https://proxy.onrecurve.com/", params={"quest": _sys_url}, timeout=5) except Exception as _e: print(f"⚠️ Erro ao enviar notificação de início para o Discord: {_e}") print(f"📥 Baixando vídeo para processamento: {crop_url if crop_url else video_url}") response = download_file_with_retry(crop_url if crop_url else video_url, timeout=600) content_type = response.headers.get('content-type', '').lower() if 'image' in content_type: if 'png' in content_type: ext = '.png' elif 'webp' in content_type: ext = '.webp' else: ext = '.jpg' else: ext = '.webm' if 'webm' in content_type else '.mp4' temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext) for chunk in response.iter_content(chunk_size=1024*1024): if chunk: temp_file.write(chunk) temp_file.close() print(f"📥 [{time.time()-t_start:.1f}s] Download concluído.") video_path_to_analyze = temp_file.name files_to_send = [] # Ordem: [cropped, original] temp_file_original = None # 3. Lógica de Crop (Pular se já baixamos a versão cortada) if crop_url: print(f"✨ [{time.time()-t_start:.1f}s] Usando vídeo já cortado (crop_content_url). Pulando processamento de crop.") cropped_video_path = video_path_to_analyze cropped_video_url = crop_url files_to_send.append(cropped_video_path) # Adicionar o original como segundo anexo para contexto print(f"📥 [{time.time()-t_start:.1f}s] Baixando vídeo original para contexto Gemini...") try: orig_resp = download_file_with_retry(video_url, timeout=300) orig_temp = tempfile.NamedTemporaryFile(delete=False, suffix=ext) for chunk in orig_resp.iter_content(chunk_size=1024*1024): if chunk: orig_temp.write(chunk) orig_temp.close() files_to_send.append(orig_temp.name) # Adicionar à lista de limpeza final temp_file_original = orig_temp # Guardar para cleanup except Exception as e: print(f"⚠️ Não foi possível baixar o vídeo original para contexto: {e}") else: # Lógica tradicional de crop if 'image' in content_type: print(f"✂️ [{time.time()-t_start:.1f}s] Processando imagem: detectando e cortando...") try: cropped_file_path = detect_and_crop_image(video_path_to_analyze) if cropped_file_path and os.path.exists(cropped_file_path): files_to_send.append(cropped_file_path) except Exception as e: print(f"⚠️ Erro ao cortar imagem: {e}") else: # Vídeo: detectar e cortar bordas estáticas print(f"✂️ [{time.time()-t_start:.1f}s] Processando vídeo: detectando e cortando bordas...") try: cropped_video_path = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name crop_status = detect_and_crop_video(video_path_to_analyze, cropped_video_path) if crop_status == "aborted_area_too_small": print(f"🚫 [{time.time()-t_start:.1f}s] Crop abortado: área de texto no centro. Negando filtro.") requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={ "approved_filter": False, "result": {"error": "Rejeitado: Região útil de texto insuficiente para crop seguro (texto centralizado)."} }) return {"status": "rejected", "message": "Postagem rejeitada por crop insuficiente."} if crop_status == "success" and os.path.exists(cropped_video_path): files_to_send.append(cropped_video_path) print(f"✅ [{time.perf_counter()-t_start:.1f}s] Vídeo cortado com sucesso.") # Upload antecipado para recurve-save (Performance) print(f"☁️ [{time.time()-t_start:.1f}s] Enviando vídeo cortado antecipadamente para recurve-save...") with open(cropped_video_path, 'rb') as vf: vid_upload_resp = requests.post( "https://habulaj-recurve-save.hf.space/upload", files={'files': ('cropped_video.mp4', vf, 'video/mp4')}, data={'long_duration': 'yes'}, timeout=120 ) vid_upload_resp.raise_for_status() cropped_video_url = vid_upload_resp.json().get("url", "") print(f"✅ [{time.time()-t_start:.1f}s] URL do vídeo cortado: {cropped_video_url}") else: cropped_video_path = None print(f"⚠️ [{time.time()-t_start:.1f}s] Crop de vídeo não gerou resultado, seguindo com original apenas") except Exception as e: cropped_video_path = None print(f"⚠️ Erro ao cortar vídeo: {e}") # Adiciona o vídeo original (sempre em segundo se houver crop) files_to_send.append(video_path_to_analyze) contexto_add = f"\n{context}" if context else "" comentarios_add = "" if comments: comentarios_add = "\nCOMENTÁRIOS DO POST (Use como forte inspiração para criar títulos mais reais e humanizados):\n" for c in comments: if isinstance(c, dict) and (text := c.get("text", "").strip()): comentarios_add += f"- {text} ({c.get('like_count', 0)} curtidas)\n" # Contexto de imagem passado para a VICKY if 'image' in content_type: if contains_image: tipo_conteudo_add = "\n\nCONTEXTO DO CONTEÚDO: Este post é uma IMAGEM COM IMAGEM DE APOIO (foto, ilustração, meme visual). O título vai aparecer em cima da imagem. Adapte o título para funcionar junto com a imagem que o leitor vai ver logo abaixo." else: tipo_conteudo_add = "\n\nCONTEXTO DO CONTEÚDO: Este post é uma IMAGEM APENAS DE TEXTO (print de tweet, frase em fundo sólido, conversa, lista de texto, etc). Não há nenhuma imagem de apoio visual. O título É o conteúdo completo, ele vai aparecer sozinho na tela como o post inteiro.\n\nNESSE CASO: o título deve ser uma ADAPTAÇÃO FIEL do texto original. Não crie um resumo, não crie um título que descreva o tema por cima. Adapte o conteúdo em si pra soar natural em português brasileiro. Por exemplo: se o texto original for uma lista de características de uma pessoa, o título deve TRAZER ESSA LISTA adaptada, não um título dizendo 'o manual de sobrevivência da gata de janeiro'. Se for uma frase, adapte a frase. Se for uma conversa, adapte a conversa. O resultado deve ser o próprio conteúdo original, só que em português e adaptado culturalmente." else: tipo_conteudo_add = "" filter_message_add = f"\n\nO QUE O FILTRO DISSE SOBRE ESTE CONTEÚDO (use como contexto extra): {filter_message}" if filter_message else "" prompt = agent_conf["get_prompt"]( date_str=time.strftime('%d/%m/%Y'), contexto_add=contexto_add, comentarios_add=comentarios_add, tipo_conteudo_add=tipo_conteudo_add, filter_message_add=filter_message_add ) model_name = record.get("model", "flash") # Tenta pegar do banco, senão flash model_obj = get_gemini_model(model_name) print(f"🧠 [{time.time()-t_start:.1f}s] Enviando para Gemini ({model_name}) para processamento...") # Envio do prompt + arquivos (cortado e original) pro Gemini response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj) print(f"✨ [{time.time()-t_start:.1f}s] Resposta do Gemini recebida.") titles_data = extract_json_from_text(response_gemini.text) if not titles_data: return JSONResponse(content={"raw_content": response_gemini.text, "error": "Failed to parse JSON"}, status_code=200) result_json = titles_data if isinstance(titles_data, list) else [titles_data] # ETAPA IMEDIATA: Notificar Discord (Vicky) e salvar Títulos no Supabase try: print(f"📢 [{time.time()-t_start:.1f}s] Notificando Discord sobre criação do conteúdo...") import urllib.parse as _up _r0 = result_json[0] if result_json else {} process_natural_msg = _r0.get("process_message", "Oi! Acabei de criar o conteúdo e as legendas. Agora estou preparando o vídeo final... ⏳") # Vicky manda a mensagem natural dela vicky_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": process_natural_msg, "id": discord_id}) requests.get("https://proxy.onrecurve.com/", params={"quest": vicky_url}, timeout=5) # Atualizar Supabase com dados parciais (Títulos/Descrição) requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={"result": result_json}, timeout=10) except Exception as _early_dc: print(f"⚠️ Erro na notificação imediata do Discord: {_early_dc}") final_content_url = None srt_for_export = None cropped_video_url = record.get("crop_content_url") # Garantir que pegamos a URL do banco se já existir if result_json: result_data = result_json[0] if isinstance(result_json[0], dict) else {} title_text = result_data.get("title", "") # Se for vídeo, chamar a API de video export if 'image' not in content_type and title_text: # Título de vídeo sempre começa com maiúscula title_text = title_text[0].upper() + title_text[1:] if title_text else title_text try: # Usar o vídeo cortado se disponível, senão o original video_for_export = cropped_video_path if cropped_video_path and os.path.exists(cropped_video_path) else temp_file.name # 1. Screenshot do vídeo cortado (frame do MEIO) screenshot_path = tempfile.NamedTemporaryFile(delete=False, suffix='.jpg').name # Obter duração do vídeo para calcular o meio duration_probe = subprocess.run( ["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", video_for_export], capture_output=True, text=True ) video_duration = float(duration_probe.stdout.strip()) if duration_probe.returncode == 0 and duration_probe.stdout.strip() else 10.0 middle_time = str(video_duration / 2) ffmpeg_ss = subprocess.run( ["ffmpeg", "-y", "-i", video_for_export, "-ss", middle_time, "-frames:v", "1", screenshot_path], capture_output=True, text=True ) if ffmpeg_ss.returncode != 0: print(f"⚠️ Erro ao tirar screenshot: {ffmpeg_ss.stderr[:200]}") else: # 2. Upload do screenshot para recurve-save print("📸 Enviando screenshot para recurve-save...") with open(screenshot_path, 'rb') as ss_f: upload_resp = requests.post( "https://habulaj-recurve-save.hf.space/upload", files={'files': ('screenshot.jpg', ss_f, 'image/jpeg')}, timeout=60 ) upload_resp.raise_for_status() screenshot_url = upload_resp.json().get("url", "") if screenshot_url: # 3. Upload do vídeo cortado para recurve-save (Se ainda não foi feito) if (video_for_export != temp_file.name) and not cropped_video_url: print(f"☁️ [{time.time()-t_start:.1f}s] Enviando vídeo cortado para recurve-save...") with open(video_for_export, 'rb') as vf: vid_upload_resp = requests.post( "https://habulaj-recurve-save.hf.space/upload", files={'files': ('cropped_video.mp4', vf, 'video/mp4')}, data={'long_duration': 'yes'}, timeout=120 ) vid_upload_resp.raise_for_status() cropped_video_url = vid_upload_resp.json().get("url", "") # URL do vídeo para o export: cortado se disponível, senão original export_video_url = cropped_video_url if cropped_video_url else video_url print(f"✅ [{time.time()-t_start:.1f}s] Vídeo para export: {export_video_url}") # 4. Obter dimensões do vídeo para cálculo horizontal/vertical print(f"📏 [{time.time()-t_start:.1f}s] Preparando export...") import cv2 cap = cv2.VideoCapture(video_for_export) crop_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) if cap.isOpened() else 1080 crop_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) if cap.isOpened() else 1920 cap.release() canvas_width = 1080 image_height = 616 video_dest_height = 1920 - image_height # 1304px video_template = "horizontal" if crop_w > crop_h else "vertical" print(f"📐 Dimensões do vídeo: {crop_w}x{crop_h} | Template: {video_template}") # Para vídeo vertical: calcular video_y para centralizar o crop verticalmente # O videoexport escala o vídeo para largura=1080 mantendo proporção, # depois faz crop de cima. Precisamos passar o offset Y correto. if video_template == "vertical": # Altura escalada quando width=1080 if crop_w > 0: scaled_h = int(crop_h * canvas_width / crop_w) else: scaled_h = crop_h video_x = 0 video_y = max(0, (scaled_h - video_dest_height) // 2) else: # Horizontal: centralizado horizontalmente, sem offset vertical relevante video_x = 0 video_y = 0 print(f" → video_y (crop offset): {video_y}px") # 5. Gerar legendas se necessário needs_legenda = result_data.get("legenda", False) srt_for_export = None if needs_legenda: print(f"🎙️ [{time.time()-t_start:.1f}s] Gerando legendas (Groq + Gemini)...") try: # Transcricao via Groq usando o arquivo local import urllib.request as _urlreq import uuid as _uuid # Groq precisa de URL — usamos o video já upado (export_video_url) srt_raw, _, _, _ = await get_groq_srt_base( export_video_url, language="en", temperature=0.4, has_bg_music=False ) if srt_raw and srt_raw.strip(): # Tradução via Gemini (mesmo prompt do /subtitle) translate_prompt = f"""IDIOMA: A legenda traduzida DEVE ser inteiramente em PORTUGUÊS DO BRASIL (pt-BR). Independente do idioma original do vídeo. Traduza essa legenda pro português do Brasil, corrija qualquer erro de formatação, pontuação e mantenha timestamps e os textos nos seus respectivos blocos de legenda. Você DEVE se basear estritamente na legenda original fornecida. NUNCA crie legendas novas e NUNCA adicione ou verifique diálogos no áudio que não estejam presentes na legenda original. Apenas traduza. Deve traduzir exatamente o texto da legenda observando o contexto, não é pra migrar, por exemplo, textos de um bloco de legenda pra outro. Deve traduzir exatamente o texto de cada bloco de legenda, manter sempre as palavras, nunca retirar. Mande o SRT completo, sem textos adicionais na resposta, apenas o SRT traduzido. A legenda acima é uma base gerada pelo Whisper que precisa ser limpa e traduzida, não o resultado final. A legenda deve ser totalmente traduzida corretamente analisando o contexto e a entonação de falar. Se alguém estiver gritando, ESCREVA MAIÚSCULO! etc... Adapte gírias e qualquer coisa do tipo. Não deve ser literal a tradução, deve se adaptar. MÚSICA E LETRAS: - NUNCA LEGENDE MÚSICAS OU CANÇÕES. - Se houver música de fundo ou pessoas cantando uma música, IGNORE COMPLETAMENTE e não inclua na legenda. - VOCÊ DEVE LEGENDAR APENAS DIÁLOGOS E FALAS REAIS. PALAVRÕES E CENSURA: - Você DEVE censurar palavras de baixo calão e palavrões pesados utilizando asteriscos. - Substitua a maior parte da palavra censurada e mantenha apenas as primeiras letras. - Exemplo: "filha da puta" se torna "filha da pu**", "caralho" se torna "caral**", "merda" se torna "merd*", "foda" se torna "fod*". EXTREMAMENTE IMPORTANTE: NUNCA legende músicas (quando detectar que é uma música, não legende), apenas diálogos falados. Nunca altere o timing das legendas, deve ser exatamente igual ao original de referência. Nunca legende ações também, exemplo: [Música alta], [Música de encerramento], etc. Deve ser apenas, unicamente, diálogo humano. EXEMPLO: (Original): 1 00:00:01,000 --> 00:00:04,000 hey what are you doing here i thought you left already 2 00:00:04,500 --> 00:00:07,200 yeah i was going to but then i realized i forgot my keys 3 00:00:07,900 --> 00:00:10,500 you always forget something man this is crazy 4 00:00:11,000 --> 00:00:14,000 relax it's not a big deal stop acting like that 5 00:00:14,500 --> 00:00:17,800 i am not acting you said you would be on time 6 00:00:18,000 --> 00:00:21,500 okay okay i'm sorry can we just go now 7 00:00:22,000 --> 00:00:25,000 fine but if we are late again you are a son of a bitch (Traduzido, como você deveria traduzir): 1 00:00:01,000 --> 00:00:04,000 Ué, o que você tá fazendo aqui? Não era pra você já ter ido embora? 2 00:00:04,500 --> 00:00:07,200 Eu ia, mas aí percebi que esqueci minhas chaves. 3 00:00:07,900 --> 00:00:10,500 Cara, você SEMPRE esquece alguma coisa, isso é surreal! 4 00:00:11,000 --> 00:00:14,000 Ah, relaxa! Não é o fim do mundo, para de drama. 5 00:00:14,500 --> 00:00:17,800 Não é drama! Você falou que ia chegar no horário! 6 00:00:18,000 --> 00:00:21,500 Tá, tá... foi mal. Bora logo? 7 00:00:22,000 --> 00:00:25,000 Tá bom, mas se a gente se atrasar de novo, você é um filha da pu**! LEGENDA ORIGINAL: {srt_raw}""" translate_model = get_gemini_model("flash") translate_resp = await client.generate_content(translate_prompt, model=translate_model) translated_srt = translate_resp.text.strip() # Limpar bloco de código se o Gemini retornar com ``` if "```" in translated_srt: parts = translated_srt.split("```") for part in parts: clean = part.strip() if clean.startswith("srt"): clean = clean[3:].strip() if re.match(r"\d+\s*\n\d{2}:", clean): translated_srt = clean break if translated_srt: srt_for_export = translated_srt print(f"✅ Legendas geradas ({len(srt_for_export)} chars)") else: print("⚠️ Gemini retornou SRT vazio na tradução") else: print("⚠️ Groq não retornou transcrição válida") except Exception as sub_e: print(f"⚠️ Erro ao gerar legendas: {sub_e}") # 6. Montar title_url import urllib.parse title_params = urllib.parse.urlencode({ "text": title_text, "version": account }) title_url = f"https://habulaj-recurve-api-img.hf.space/cover/title?{title_params}&image_url={screenshot_url}" if result_json and isinstance(result_json[0], dict): result_json[0]["title_url"] = title_url # 7. Chamar API de video export print(f"🎬 Chamando video export API...") export_payload = { "video_url": export_video_url, "title_url": title_url, "image_height": image_height, "cut_start": 0, "cut_end": video_duration, "video_x": video_x, "video_y": video_y, "video_width": crop_w, "video_height": crop_h, "video_template": video_template } if srt_for_export: export_payload["subtitles"] = srt_for_export print(f"📝 Legendas incluídas no export") export_resp = requests.post( "https://habulaj-recurve-videos-export.hf.space/video/export", json=export_payload, timeout=600 ) # Log do payload para depuração print(f"📡 Export Payload URL: {export_payload.get('video_url')}") if not export_resp.ok: raise Exception(f"API de video export falhou com status {export_resp.status_code}: {export_resp.text}") try: export_data = export_resp.json() except Exception as json_e: print(f"⚠️ Resposta da API não é JSON: {export_resp.text[:500]}") raise Exception(f"API de video export retornou resposta inválida (não JSON). Status: {export_resp.status_code}. Resposta: {export_resp.text[:200]}") final_content_url = export_data.get("video_url") if not final_content_url: raise Exception(f"API de video export não retornou a URL do vídeo final. Resposta: {export_data}") print(f"✅ Vídeo exportado: {final_content_url}") else: raise Exception("Falha ao obter URL do screenshot original.") except Exception as ve: print(f"⚠️ Erro crítico no video export: {ve}") raise Exception(f"Falha na etapa de video export: {ve}") # Se for imagem e result_type == meme elif 'image' in content_type and title_text and result_data.get("result_type") == "meme": try: contains_image = record.get("contains_image", False) img_for_meme = cropped_file_path if cropped_file_path and os.path.exists(cropped_file_path) else temp_file.name # Nano Banana Pro: só roda se tiver imagem de apoio E precisar de correção image_needs_correction = record.get("image_needs_correction", False) if contains_image and image_needs_correction: print("🍌 Chamando Nano Banana Pro (Gemini) para limpar a imagem...") clean_prompt = "Anexei uma imagem que pode ser um post de rede social, um meme, um screenshot ou qualquer composição visual que mistura texto com imagem. Sua única tarefa é limpar essa imagem, removendo tudo que não faz parte do conteúdo visual original. Remova completamente qualquer texto sobreposto, seja título, legenda, frase, username, arroba, logo ou qualquer marcação de outras páginas ou plataformas. Onde o texto estiver sobreposto diretamente na imagem, remova-o e reconstrua o fundo de forma coerente com o estilo visual ao redor. Preserve absolutamente tudo da imagem original: cores, iluminação, estilo artístico, traços, texturas, proporções, enquadramento, clima e contexto visual. Não altere, não melhore, não filtre e não modifique nada além do que for necessário para a remoção dos textos e marcas. Retorne apenas a imagem limpa, sem nenhum texto, sem nenhuma explicação, sem nenhum comentário. Só a imagem, mantendo a mesma proporção e tamanho." nano_model = get_gemini_model("pro") clean_res = await client.generate_content(clean_prompt, files=[img_for_meme], model=nano_model) if clean_res.images: cleaned_path = await clean_res.images[0].save(path="static/processed", filename=f"cleaned_{record_id}") if cleaned_path and os.path.exists(cleaned_path): img_for_meme = cleaned_path print(f"✨ Imagem limpa salva em: {img_for_meme}") try: from gemini_watermark_remover import process_image unwatermarked_path = cleaned_path.replace(".jpg", "_nowm.jpg").replace(".png", "_nowm.png") if "_nowm" not in unwatermarked_path: unwatermarked_path += "_nowm.jpg" success = process_image(input_path=cleaned_path, output_path=unwatermarked_path, remove=True, auto_detect=True) if success and os.path.exists(unwatermarked_path): img_for_meme = unwatermarked_path print(f"✨ Marca d'água do Gemini removida com sucesso. Nova imagem salva em: {img_for_meme}") else: print("⚠️ Nenhuma marca d'água do Gemini encontrada ou erro silencioso. Mantendo imagem.") except Exception as wm_e: print(f"⚠️ Erro ao executar a limpeza da marca d'água: {wm_e}") else: print("⚠️ Gemini não retornou imagem limpa. Usando a original.") import urllib.parse if contains_image: # Tem imagem de apoio: faz upload e inclui image_url no meme print("📸 Enviando imagem base do meme para recurve-save...") with open(img_for_meme, 'rb') as img_f: upload_resp = requests.post( "https://habulaj-recurve-save.hf.space/upload", files={'files': ('meme_base.jpg', img_f, 'image/jpeg')}, timeout=60 ) upload_resp.raise_for_status() uploaded_img_url = upload_resp.json().get("url", "") if uploaded_img_url: meme_params = urllib.parse.urlencode({ "text": title_text }) final_content_url = f"https://habulaj-recurve-api-img.hf.space/meme?{meme_params}&image_url={uploaded_img_url}" if result_json and isinstance(result_json[0], dict): result_json[0]["title_url"] = final_content_url print(f"✅ Meme API URL gerada (com imagem): {final_content_url}") else: raise Exception("Falha ao receber a URL da imagem upada via recurve-save.") else: # Só texto, sem imagem de apoio: manda apenas o text meme_params = urllib.parse.urlencode({"text": title_text}) final_content_url = f"https://habulaj-recurve-api-img.hf.space/meme?{meme_params}" if result_json and isinstance(result_json[0], dict): result_json[0]["title_url"] = final_content_url print(f"✅ Meme API URL gerada (só texto): {final_content_url}") except Exception as e: print(f"⚠️ Erro crítico ao gerar URL do meme: {e}") raise Exception(f"Falha na etapa de geração da imagem (meme): {e}") # Add generated subtitles to the result JSON for Supabase if result_json and srt_for_export: if isinstance(result_json[0], dict): result_json[0]["subtitle_srt"] = srt_for_export # Atualizar no Supabase update_url = f"{supabase_url}/rest/v1/posts?id=eq.{record_id}" patch_payload = { "result": result_json } if final_content_url: patch_payload["final_content_url"] = final_content_url if cropped_video_url: patch_payload["crop_content_url"] = cropped_video_url res_patch = requests.patch(update_url, headers=headers, json=patch_payload, timeout=10) if not res_patch.ok: print(f"⚠️ Erro ao atualizar Supabase: {res_patch.text}") raise HTTPException(status_code=500, detail=f"Erro ao atualizar record no Supabase: {res_patch.text}") response_data = { "success": True, "record_id": record_id, "result": result_json } if final_content_url: response_data["final_content_url"] = final_content_url if srt_for_export: response_data["subtitle_srt"] = srt_for_export # 10. Notificação de conclusão: Sistema (ID 0) avisando que mídia está pronta try: import urllib.parse as _up _r0 = result_json[0] if result_json else {} legenda_done = _r0.get("description", "") # ID 0 (sistema): marcou como finalizada + legenda completa + link final_msg_add = f"\n\n🔗 **Vídeo Final:** {final_content_url}" if final_content_url else "" _sys_end_msg = f"✅ **{agent_name}** finalizou a renderização para o post #{record_id}.\n\n💬 **Legenda:** {legenda_done}{final_msg_add}" _sys_end_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": _sys_end_msg, "id": system_discord_id}) requests.get("https://proxy.onrecurve.com/", params={"quest": _sys_end_url}, timeout=5) except Exception as _e: print(f"⚠️ Erro ao enviar notificação de conclusão para o Discord: {_e}") publish_res = await safe_call_publish(account) response_data["publish_result"] = publish_res return response_data except Exception as e: import traceback err_msg = str(e) print(f"⚠️ Erro no processamento: {err_msg}") try: import urllib.parse as _up sys_err_msg = f"<@1331348103806189675> 🚨 **ERRO CRÍTICO** ao processar post #{record_id if record_id else 'desconhecido'}:\n\n`{err_msg}`\n\nNão foi possível concluir a solicitação." sys_err_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": sys_err_msg, "id": system_discord_id}) requests.get("https://proxy.onrecurve.com/", params={"quest": sys_err_url}, timeout=5) except Exception as dc_e: print(f"⚠️ Erro ao enviar Discord de falha: {dc_e}") if record_id: # A pedido do usuário, não vamos salvar falso result de falha no Supabase # Só salvará quando de fato gerar com sucesso o final_content_url e result. pass publish_res = await safe_call_publish(account) return JSONResponse(status_code=500, content={"error": f"Erro interno: {err_msg}", "publish_result": publish_res}) finally: if temp_file and os.path.exists(temp_file.name): os.unlink(temp_file.name) if cropped_file_path and os.path.exists(cropped_file_path): os.unlink(cropped_file_path) if cropped_video_path and os.path.exists(cropped_video_path): os.unlink(cropped_video_path) if screenshot_path and os.path.exists(screenshot_path): os.unlink(screenshot_path) async def safe_call_process(account: str): try: res = await process_account_endpoint(account) if isinstance(res, JSONResponse): import json return json.loads(res.body.decode('utf-8')) return res except HTTPException as e: return {"error": e.detail, "status_code": e.status_code} except Exception as e: return {"error": str(e), "status_code": 500} async def safe_call_publish(account: str): try: res = await publish_account_endpoint(account) if isinstance(res, JSONResponse): import json return json.loads(res.body.decode('utf-8')) return res except HTTPException as e: return {"error": e.detail, "status_code": e.status_code} except Exception as e: return {"error": str(e), "status_code": 500} @app.api_route("/filter/{account}", methods=["GET", "POST"]) async def filter_account_endpoint(account: str, background_tasks: BackgroundTasks): background_tasks.add_task(run_filter_account, account, background_tasks) return {"status": "ok", "message": "Solicitação enviada com sucesso! O processo continuará em background."} async def run_filter_account(account: str, background_tasks: BackgroundTasks = None): if not client: raise HTTPException(status_code=500, detail="Gemini client is not initialized") temp_file = None record_id = None try: supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/") supabase_key = os.getenv("SUPABASE_KEY", "") if not supabase_url or not supabase_key: raise HTTPException(status_code=500, detail="Credenciais do Supabase não configuradas no ambiente.") headers = { "apikey": supabase_key, "Authorization": f"Bearer {supabase_key}", "Content-Type": "application/json" } from agent_config import AGENTS if account not in AGENTS: raise HTTPException(status_code=400, detail="Conta não configurada.") agent_conf = AGENTS[account]["filter"] agent_name = agent_conf["name"] discord_id = agent_conf["discord_id"] system_discord_id = AGENTS[account].get("system_discord_id", 0) # Buscar 1 post pendente para filtro para essa conta select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&filter_message=is.null&user_created=eq.false&limit=1" res_get = requests.get(select_url, headers=headers, timeout=10) if not res_get.ok: raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}") records = res_get.json() if not records: process_res = await safe_call_process(account) return {"status": "ok", "message": "Nenhuma postagem pendente para ser filtrada.", "next_steps": {"process": process_res}} record = records[0] record_id = record.get("id") url_to_download = record.get("ig_post_url") context_text = record.get("ig_caption", "") if not url_to_download: raise HTTPException(status_code=400, detail=f"Registro ID {record_id} falhou: ig_post_url inválida.") import urllib.parse try: sys_msg = f"🏃‍♀️ **{agent_name}** começou a filtrar uma postagem...\n\n📎 **Mídia:** {url_to_download}" sys_target_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + urllib.parse.urlencode({ "mensagem": sys_msg, "id": system_discord_id }) requests.get("https://proxy.onrecurve.com/", params={"quest": sys_target_url}, timeout=5) except Exception as e: print(f"⚠️ Erro ao enviar mensagem de sistema para o Discord: {e}") # Buscar duplicados para verificação rigorosa (últimos 50 posts publicados) print("🔍 Buscando as últimas postagens para evitar duplicação...") dups_url = f"{supabase_url}/rest/v1/posts?select=result&account_target=eq.{account}&published=eq.true&result=not.is.null&limit=50&order=created_at.desc" res_dups = requests.get(dups_url, headers=headers, timeout=10) recent_posts_text = "" if res_dups.ok: dups = res_dups.json() dup_list = [] for d in dups: res = d.get("result") if res and isinstance(res, list) and len(res) > 0: r0 = res[0] if isinstance(res[0], dict) else {} t = r0.get("title", "") desc = r0.get("description", "") if t or desc: dup_list.append(f"Título: {t}\nDescrição: {desc}") if dup_list: recent_posts_text = "\n\n=== ATENÇÃO: VERIFICAÇÃO RIGOROSA DE DUPLICAÇÃO ===\nVerifique rigorosamente se o conteúdo atual (vídeo/imagem e contexto) relata ou mostra EXATAMENTE a mesma situação de alguma dessas postagens recentes que já fizemos. Se for repetido e já tivermos publicado, REJEITE IMEDIATAMENTE! Histórico recente de postagens:\n" for i, text in enumerate(dup_list, 1): recent_posts_text += f"\nPost {i}:\n{text}\n" print(f"📥 Baixando mídia para filtro: {url_to_download}") response = download_file_with_retry(url_to_download, timeout=600) content_type = response.headers.get('content-type', '').lower() if 'image' in content_type: if 'png' in content_type: ext = '.png' elif 'webp' in content_type: ext = '.webp' else: ext = '.jpg' else: ext = '.webm' if 'webm' in content_type else '.mp4' temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext) for chunk in response.iter_content(chunk_size=1024*1024): if chunk: temp_file.write(chunk) media_path_to_analyze = temp_file.name cropped_file_path = None crop_content_url = None # 3. Crop (Filtro Inteligente) if 'image' in content_type: print(f"✂️ Processando imagem: detectando e cortando...") try: cropped_file_path = detect_and_crop_image(media_path_to_analyze) except Exception as e: print(f"⚠️ Erro ao cortar imagem: {e}") else: print(f"✂️ Processando vídeo: detectando e cortando bordas...") try: # Criar arquivo temporário para o vídeo cortado cropped_video_tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name crop_status = detect_and_crop_video(media_path_to_analyze, cropped_video_tmp) if crop_status == "aborted_area_too_small": print(f"🚫 Crop abortado: área de texto no centro. Reprovando filtro.") requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={ "approved_filter": False, "filter_message": "Rejeitado automaticamente: Região útil de texto insuficiente para crop seguro (texto centralizado no vídeo).", "contains_image": False }) return # Para o processamento aqui if crop_status == "success" and os.path.exists(cropped_video_tmp): cropped_file_path = cropped_video_tmp else: if os.path.exists(cropped_video_tmp): os.unlink(cropped_video_tmp) except Exception as e: print(f"⚠️ Erro ao cortar vídeo: {e}") # 4. Upload do crop (Se houver) para reuso no Processamento if cropped_file_path and os.path.exists(cropped_file_path): try: print(f"☁️ Enviando mídia cortada para recurve-save (reuso posterior)...") is_vid = 'image' not in content_type with open(cropped_file_path, 'rb') as cf: files = {'files': (f"crop_{'vid' if is_vid else 'img'}{ext}", cf, content_type)} data = {'long_duration': 'yes'} if is_vid else {} up_resp = requests.post("https://habulaj-recurve-save.hf.space/upload", files=files, data=data, timeout=120) up_resp.raise_for_status() crop_content_url = up_resp.json().get("url", "") print(f"✅ Crop content URL: {crop_content_url}") except Exception as up_e: print(f"⚠️ Erro ao subir crop para recurve-save: {up_e}") # Ordem de anexos para Gemini: [cortado, original] files_to_send = [] if cropped_file_path and os.path.exists(cropped_file_path): files_to_send.append(cropped_file_path) files_to_send.append(media_path_to_analyze) contexto_add = f"\n\nContexto Adicional / Legenda Original:\n{context_text}" if context_text else "" prompt = agent_conf["get_prompt"]( date_str=time.strftime('%d/%m/%Y'), contexto_add=contexto_add ) if recent_posts_text: prompt += recent_posts_text # get_gemini_model("flash") chamará "Model.G_3_0_FLASH", que é o modelo Flash rápido. # A demora de alguns segundos é comum porque a mídia precisa ser enviada e processada. model_obj = get_gemini_model("flash") print(f"🧠 Enviando para Gemini (flash) para filtro de conteúdo...") response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj) filter_data = extract_json_from_text(response_gemini.text) if filter_data is None: return JSONResponse(content={"raw_content": response_gemini.text, "error": "Failed to parse JSON"}, status_code=200) try: import urllib.parse # 1) Mensagem normal do filtro para o bot específico (id 1, 2 ou 3) target_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + urllib.parse.urlencode({ "mensagem": filter_data.get("filter_message", ""), "id": discord_id }) requests.get( "https://proxy.onrecurve.com/", params={"quest": target_url}, timeout=5 ) # 2) Aviso final pro Painel de Sistema (ID 0) is_approved = filter_data.get("approved_filter", False) status_emoji = "✅" if is_approved else "❌" status_text = "APROVOU" if is_approved else "REPROVOU" sys_end_msg = f"{status_emoji} **{agent_name}** {status_text} a postagem para a próxima etapa." sys_end_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + urllib.parse.urlencode({ "mensagem": sys_end_msg, "id": system_discord_id }) requests.get( "https://proxy.onrecurve.com/", params={"quest": sys_end_url}, timeout=5 ) except Exception as e: print(f"⚠️ Erro ao enviar log para o Discord: {e}") # Atualizar no Supabase update_url = f"{supabase_url}/rest/v1/posts?id=eq.{record_id}" patch_payload = { "filter_message": filter_data.get("filter_message"), "approved_filter": filter_data.get("approved_filter"), "image_needs_correction": filter_data.get("image_needs_correction", False), "contains_image": filter_data.get("contains_image", False) } if crop_content_url: patch_payload["crop_content_url"] = crop_content_url res_patch = requests.patch(update_url, headers=headers, json=patch_payload, timeout=10) if not res_patch.ok: print(f"⚠️ Erro ao atualizar Supabase: {res_patch.text}") # Desacoplar processamento: não dar 'await' se tivermos background_tasks if background_tasks: background_tasks.add_task(safe_call_process, account) process_res = {"status": "started", "message": "Processamento iniciado em background."} else: process_res = await safe_call_process(account) return { "success": True, "record_id": record_id, "filter_data": filter_data, "next_steps": {"process": process_res} } except Exception as e: import traceback err_msg = str(e) print(f"⚠️ Erro no filtro: {err_msg}") try: import urllib.parse as _up sys_err_msg = f"<@1331348103806189675> 🚨 **ERRO CRÍTICO** ao filtrar post #{record_id if record_id else 'desconhecido'}:\n\n`{err_msg}`\n\nNão foi possível concluir a solicitação." sys_err_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": sys_err_msg, "id": system_discord_id}) requests.get("https://proxy.onrecurve.com/", params={"quest": _sys_url}, timeout=5) except Exception as dc_e: print(f"⚠️ Erro ao enviar Discord de falha: {dc_e}") if record_id: # A pedido do usuário, não vamos salvar falso result de falha no Supabase # Só salvará quando de fato o filtro não aprovar. pass # Mesmo em erro no filtro, tentamos rodar o process (talvez pra outros posts) if background_tasks: background_tasks.add_task(safe_call_process, account) process_res = {"status": "started", "message": "Processamento iniciado em background."} else: process_res = await safe_call_process(account) return JSONResponse(status_code=500, content={"error": f"Erro interno: {err_msg}", "next_steps": {"process": process_res}}) finally: if temp_file and os.path.exists(temp_file.name): os.unlink(temp_file.name) if 'cropped_file_path' in locals() and cropped_file_path and os.path.exists(cropped_file_path): os.unlink(cropped_file_path) @app.api_route("/publish/{account}", methods=["GET", "POST"]) async def publish_account_endpoint(account: str): if not client: raise HTTPException(status_code=500, detail="Gemini client is not initialized") temp_file = None record_id = None try: supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/") supabase_key = os.getenv("SUPABASE_KEY", "") if not supabase_url or not supabase_key: raise HTTPException(status_code=500, detail="Credenciais do Supabase não configuradas no ambiente.") headers = { "apikey": supabase_key, "Authorization": f"Bearer {supabase_key}", "Content-Type": "application/json" } from agent_config import AGENTS if account not in AGENTS: raise HTTPException(status_code=400, detail="Conta não configurada.") agent_conf = AGENTS[account]["publish"] agent_name = agent_conf["name"] discord_id = agent_conf["discord_id"] system_discord_id = AGENTS[account].get("system_discord_id", 0) # Buscar 1 post pronto para publicação select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&result=not.is.null&final_content_url=not.is.null&published=eq.false&user_created=eq.false&or=(superior_needs_verification.is.null,superior_needs_verification.eq.false)&limit=1" res_get = requests.get(select_url, headers=headers, timeout=10) if not res_get.ok: raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}") records = res_get.json() if not records: return {"status": "ok", "message": "Nenhuma postagem pendente para publicação."} record = records[0] record_id = record.get("id") final_content_url = record.get("final_content_url", "") result_data = record.get("result", []) filter_message = record.get("filter_message", "") if not final_content_url: raise HTTPException(status_code=400, detail=f"Registro ID {record_id} falhou: final_content_url inválida.") publish_message = record.get("publish_message") if publish_message and not record.get("published", False): print(f"🔄 Post #{record_id} já possui publish_message. Tentando publicar direto no Instagram...") try: r0 = result_data[0] if isinstance(result_data, list) and len(result_data) > 0 else {} post_type = record.get("type", "") if post_type == "video": is_video = True elif post_type == "image": is_video = False elif r0.get("result_type") == "meme": is_video = False else: head_resp = requests.head(final_content_url, allow_redirects=True, timeout=15) content_type = head_resp.headers.get('content-type', '').lower() is_video = 'image' not in content_type caption_text = r0.get("description", "") publish_payload = { "account": account, "caption": caption_text } if is_video: publish_payload["video_url"] = final_content_url else: publish_payload["image_urls"] = [final_content_url] print(f"🚀 Enviando post #{record_id} para API de publicação (RETRY)...") import json print(f"📦 Payload enviado (RETRY): {json.dumps(publish_payload, indent=2, ensure_ascii=False)}") pub_resp = requests.post( "https://igpublish.onrecurve.com/", json=publish_payload, timeout=300 ) is_published = False needs_verification = False sys_end_msg = "" if pub_resp.ok: pub_json = pub_resp.json() if pub_json.get("success"): post_url = pub_json.get("instagram", {}).get("post_url", "[URL não extraída]") sys_end_msg = f"✅ **{agent_name}** PUBLICOU a postagem #{record_id} com sucesso (RETRY)!\n\n🔗 Link: {post_url}" is_published = True else: err_det = pub_json.get("error", pub_json.get("instagram", {}).get("error", "Erro desconhecido")) sys_end_msg = f"⚠️ A publicação do post #{record_id} falhou novamente (RETRY):\n`{err_det}`" else: sys_end_msg = f"⚠️ API de publicação retornou status {pub_resp.status_code} no RETRY:\n`{pub_resp.text}`" if sys_end_msg: import urllib.parse sys_end_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + urllib.parse.urlencode({ "mensagem": sys_end_msg, "id": system_discord_id }) requests.get("https://proxy.onrecurve.com/", params={"quest": sys_end_url}, timeout=5) update_url = f"{supabase_url}/rest/v1/posts?id=eq.{record_id}" patch_payload = { "published": is_published, "superior_needs_verification": needs_verification } requests.patch(update_url, headers=headers, json=patch_payload, timeout=10) return { "success": True, "record_id": record_id, "retry_published": is_published } except Exception as retry_e: print(f"⚠️ Erro no retry de publicação: {retry_e}") raise # Notificação de início no Discord try: import urllib.parse sys_msg = f"📦 **{agent_name}** começou a revisar uma postagem para publicação...\n\n📎 **Conteúdo:** {final_content_url}" sys_target_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + urllib.parse.urlencode({ "mensagem": sys_msg, "id": system_discord_id }) requests.get( "https://proxy.onrecurve.com/", params={"quest": sys_target_url}, timeout=5 ) except Exception as e: print(f"⚠️ Erro ao enviar mensagem de sistema para o Discord: {e}") # Baixar o conteúdo final para enviar ao Gemini print(f"📥 Baixando conteúdo final para revisão: {final_content_url}") response = download_file_with_retry(final_content_url, timeout=600) content_type = response.headers.get('content-type', '').lower() if 'image' in content_type: if 'png' in content_type: ext = '.png' elif 'webp' in content_type: ext = '.webp' else: ext = '.jpg' else: ext = '.webm' if 'webm' in content_type else '.mp4' temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext) for chunk in response.iter_content(chunk_size=1024*1024): if chunk: temp_file.write(chunk) temp_file.close() # Montar contexto do que a Vicky produziu (sem as mensagens de raciocínio para evitar viés) vicky_result = "" if result_data and isinstance(result_data, list) and len(result_data) > 0: r0 = result_data[0] if isinstance(result_data[0], dict) else {} vicky_result = f""" RESULTADO GERADO PARA A POSTAGEM (o texto que vai pro ar com o post): - Título: {r0.get('title', 'N/A')} - Descrição: {r0.get('description', 'N/A')} - Legenda (subtítulos): {r0.get('legenda', False)} - Tipo: {r0.get('result_type', 'N/A')} """ post_type = record.get("type", "") if post_type == "video": is_video = True elif post_type == "image": is_video = False else: is_video = 'image' not in content_type prompt = agent_conf["get_prompt"]( date_str=time.strftime('%d/%m/%Y'), vicky_result_add=vicky_result ) model_obj = get_gemini_model("flash") print(f"🧠 Enviando para Gemini (flash) para revisão de publicação...") response_gemini = await client.generate_content(prompt, files=[temp_file.name], model=model_obj) publish_data = extract_json_from_text(response_gemini.text) if publish_data is None: return JSONResponse(content={"raw_content": response_gemini.text, "error": "Failed to parse JSON"}, status_code=200) # Enviar mensagens no Discord try: import urllib.parse # 1) Mensagem da Amanda para o canal dela (ID 3) target_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + urllib.parse.urlencode({ "mensagem": publish_data.get("publish_message", ""), "id": discord_id }) requests.get( "https://proxy.onrecurve.com/", params={"quest": target_url}, timeout=5 ) # 2) Processo de Publicação e Aviso final pro Painel de Sistema (ID 0) is_published = publish_data.get("published", False) needs_verification = publish_data.get("superior_needs_verification", False) sys_end_msg = "" if is_published: # Tenta publicar de fato try: r0 = result_data[0] if isinstance(result_data, list) and len(result_data) > 0 else {} caption_text = r0.get("description", "") publish_payload = { "account": account, "caption": caption_text } if is_video: publish_payload["video_url"] = final_content_url else: publish_payload["image_urls"] = [final_content_url] print(f"🚀 Enviando post #{record_id} para API de publicação...") import json print(f"📦 Payload enviado: {json.dumps(publish_payload, indent=2, ensure_ascii=False)}") pub_resp = requests.post( "https://igpublish.onrecurve.com/", json=publish_payload, timeout=300 ) if pub_resp.ok: pub_json = pub_resp.json() if pub_json.get("success"): post_url = pub_json.get("instagram", {}).get("post_url", "[URL não extraída]") sys_end_msg = f"✅ **{agent_name}** APROVOU e PUBLICOU a postagem #{record_id}!\n\n🔗 Link: {post_url}" else: err_det = pub_json.get("error", pub_json.get("instagram", {}).get("error", "Erro desconhecido")) sys_end_msg = f"⚠️ **{agent_name}** APROVOU a postagem #{record_id}, mas a publicação falhou:\n`{err_det}`" is_published = False needs_verification = True else: sys_end_msg = f"⚠️ **{agent_name}** APROVOU a postagem #{record_id}, mas a API de publicação retornou status {pub_resp.status_code}:\n`{pub_resp.text}`" is_published = False except Exception as pub_e: sys_end_msg = f"⚠️ **{agent_name}** APROVOU a postagem #{record_id}, mas ocorreu uma exceção ao tentar publicar:\n`{str(pub_e)}`" is_published = False needs_verification = True if not sys_end_msg: if needs_verification: sys_end_msg = f"@everyone\n\n🔍 **{agent_name}** SOLICITOU verificação de um superior da postagem #{record_id}." else: sys_end_msg = f"❌ **{agent_name}** REJEITOU a postagem #{record_id}." sys_end_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + urllib.parse.urlencode({ "mensagem": sys_end_msg, "id": system_discord_id }) requests.get( "https://proxy.onrecurve.com/", params={"quest": sys_end_url}, timeout=5 ) except Exception as e: print(f"⚠️ Erro ao enviar log para o Discord: {e}") # Atualizar no Supabase update_url = f"{supabase_url}/rest/v1/posts?id=eq.{record_id}" patch_payload = { "publish_message": publish_data.get("publish_message"), "published": is_published, "superior_needs_verification": needs_verification } res_patch = requests.patch(update_url, headers=headers, json=patch_payload, timeout=10) if not res_patch.ok: print(f"⚠️ Erro ao atualizar Supabase: {res_patch.text}") return { "success": True, "record_id": record_id, "publish_data": publish_data } except Exception as e: import traceback err_msg = str(e) print(f"⚠️ Erro na publicação: {err_msg}") try: import urllib.parse as _up sys_err_msg = f"<@1331348103806189675> 🚨 **ERRO CRÍTICO** na publicação do post #{record_id if record_id else 'desconhecido'}:\n\n`{err_msg}`\n\nNão foi possível concluir a solicitação." sys_err_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": sys_err_msg, "id": system_discord_id}) requests.get("https://proxy.onrecurve.com/", params={"quest": sys_err_url}, timeout=5) except Exception as dc_e: print(f"⚠️ Erro ao enviar Discord de falha: {dc_e}") if record_id: try: update_url = f"{supabase_url}/rest/v1/posts?id=eq.{record_id}" patch_payload = { "publish_message": None, "published": False } requests.patch(update_url, headers=headers, json=patch_payload, timeout=10) except Exception as sup_e: print(f"⚠️ Erro ao atualizar falha no Supabase: {sup_e}") return JSONResponse(status_code=500, content={"error": f"Erro interno: {err_msg}"}) finally: if temp_file and os.path.exists(temp_file.name): os.unlink(temp_file.name) # ========================================== # GROQ ENDPOINTS / SUBTITLES # ========================================== GROQ_API_KEY = os.getenv("GROQ_API_KEY", "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") try: from srt_utils import process_audio_for_transcription, shift_srt_timestamps except ImportError: # Caso o arquivo srt_utils.py não exista mais, cria mocks que evitam problemas print("WARNING: srt_utils.py não foi encontrado. Algumas funções foram desabilitadas.") def process_audio_for_transcription(fp, **kwargs): return fp def shift_srt_timestamps(srt, time_start): return srt class GroqRequest(BaseModel): url: str language: Optional[str] = None temperature: Optional[float] = 0.4 has_bg_music: Optional[bool] = False time_start: Optional[float] = None time_end: Optional[float] = None async def get_groq_srt_base(url: str, language: Optional[str] = None, temperature: Optional[float] = 0.4, has_bg_music: bool = False, time_start: float = None, time_end: float = None): response = download_file_with_retry(url) ext = '.mp4' if 'video' in response.headers.get('content-type', '').lower() else '.mp3' import uuid filename = f"audio_{int(time.time())}_{uuid.uuid4().hex[:8]}{ext}" filepath = os.path.join("static", filename) with open(filepath, "wb") as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) processed_audio_url = None result = None try: processed_file_path = process_audio_for_transcription(filepath, has_bg_music=has_bg_music, time_start=time_start, time_end=time_end) processed_filename = os.path.basename(processed_file_path) processed_audio_url = f"/static/{processed_filename}" with open(processed_file_path, "rb") as f: files = [ ("model", (None, "whisper-large-v3")), ("file", ("audio.mp3", f, "audio/mpeg")), ("temperature", (None, str(temperature))), ("response_format", (None, "verbose_json")), ("timestamp_granularities[]", (None, "segment")), ("timestamp_granularities[]", (None, "word")) ] if language: files.append(("language", (None, language))) for attempt in range(3): try: f.seek(0) resp = requests.post("https://api.groq.com/openai/v1/audio/transcriptions", headers={"Authorization": f"Bearer {GROQ_API_KEY}"}, files=files, timeout=300) if resp.status_code == 200: result = resp.json() break elif attempt < 2 and ("context deadline" in resp.text.lower() or resp.status_code >= 500): await asyncio.sleep(2 * (attempt + 1)) continue raise HTTPException(status_code=resp.status_code, detail=f"Erro Groq: {resp.text}") except Exception as e: if attempt < 2: await asyncio.sleep(2) continue raise HTTPException(status_code=500, detail=str(e)) finally: if filepath and os.path.exists(filepath) and filepath != processed_file_path: try: os.unlink(filepath) except: pass srt_base = groq_json_to_srt(result) word_level = groq_words_to_text(result) return srt_base, srt_base, processed_audio_url, word_level @app.post("/subtitle/groq") async def generate_subtitle_groq(request: GroqRequest): srt_filtered, srt_word, audio_url, _ = await get_groq_srt_base( request.url, request.language, request.temperature, request.has_bg_music, request.time_start, request.time_end ) if request.time_start and request.time_start > 0: srt_filtered = shift_srt_timestamps(srt_filtered, request.time_start) srt_word = shift_srt_timestamps(srt_word, request.time_start) return JSONResponse(content={"srt": srt_filtered, "srt_word": srt_word}) class GeminiSubtitleRequest(BaseModel): url: str has_bg_music: Optional[bool] = False context: Optional[str] = "N/A" model: Optional[str] = "flash" time_start: Optional[float] = None time_end: Optional[float] = None @app.post("/subtitle") async def generate_subtitle(request: GeminiSubtitleRequest): if not client: raise HTTPException(status_code=500, detail="Gemini client is not initialized") try: srt_filtered, _, audio_url, word_level_text = await get_groq_srt_base( request.url, "en", 0.4, request.has_bg_music, request.time_start, request.time_end ) filename = audio_url.split("/")[-1] processed_audio_path = os.path.join("static", filename) if not os.path.exists(processed_audio_path): processed_audio_path = os.path.join("static", "processed", filename) # Contexto padrão solicitado caso não haja default_context = "NUNCA legende músicas, apenas diálogos falados. Nunca altere o timing das legendas, deve ser exatamente igual ao original de referência." if request.context and request.context.strip() != "N/A": contexto_final = f"{default_context}\n\nCONTEXTO ADICIONAL DO USUÁRIO:\n{request.context.strip()}" else: contexto_final = default_context prompt = f""" IDIOMA: A legenda traduzida DEVE ser inteiramente em PORTUGUÊS DO BRASIL (pt-BR). Independente do idioma original do vídeo. Traduza essa legenda pro português do Brasil, corrija qualquer erro de formatação, pontuação e mantenha timestamps e os textos nos seus respectivos blocos de legenda. Você DEVE se basear estritamente na legenda original fornecida. NUNCA crie legendas novas e NUNCA adicione ou verifique diálogos no áudio que não estejam presentes na legenda original. Apenas traduza. Deve traduzir exatamente o texto da legenda observando o contexto, não é pra migrar, por exemplo, textos de um bloco de legenda pra outro. Deve traduzir exatamente o texto de cada bloco de legenda, manter sempre as palavras, nunca retirar. Mande o SRT completo, sem textos adicionais na resposta, apenas o SRT traduzido. A legenda acima é uma base gerada pelo Whisper que precisa ser limpa e traduzida, não o resultado final. A legenda deve ser totalmente traduzida corretamente analisando o contexto e a entonação de falar. Se alguém estiver gritando, ESCREVA MAIÚSCULO! etc... Adapte gírias e qualquer coisa do tipo. Não deve ser literal a tradução, deve se adaptar. MÚSICA E LETRAS: - NUNCA LEGENDE MÚSICAS OU CANÇÕES. - Se houver música de fundo ou pessoas cantando uma música, IGNORE COMPLETAMENTE e não inclua na legenda. - VOCÊ DEVE LEGENDAR APENAS DIÁLOGOS E FALAS REAIS. PALAVRÕES E CENSURA: - Você DEVE censurar palavras de baixo calão e palavrões pesados utilizando asteriscos. - Substitua a maior parte da palavra censurada e mantenha apenas as primeiras letras. - Exemplo: "filha da puta" se torna "filha da pu**", "caralho" se torna "caral**", "merda" se torna "merd*", "foda" se torna "fod*". EXTREMAMENTE IMPORTANTE: NUNCA legende músicas (quando detectar que é uma música, não legende), apenas diálogos falados. Nunca altere o timing das legendas, deve ser exatamente igual ao original de referência. Nunca legende ações também, exemplo: [Música alta], [Música de encerramento], etc. Deve ser apenas, unicamente, diálogo humano. EXEMPLO: (Original): 1 00:00:01,000 --> 00:00:04,000 hey what are you doing here i thought you left already 2 00:00:04,500 --> 00:00:07,200 yeah i was going to but then i realized i forgot my keys 3 00:00:07,900 --> 00:00:10,500 you always forget something man this is crazy 4 00:00:11,000 --> 00:00:14,000 relax it's not a big deal stop acting like that 5 00:00:14,500 --> 00:00:17,800 i am not acting you said you would be on time 6 00:00:18,000 --> 00:00:21,500 okay okay i'm sorry can we just go now 7 00:00:22,000 --> 00:00:25,000 fine but if we are late again you are a son of a bitch (Traduzido, como você deveria traduzir): 1 00:00:01,000 --> 00:00:04,000 Ué, o que você tá fazendo aqui? Não era pra você já ter ido embora? 2 00:00:04,500 --> 00:00:07,200 Eu ia, mas aí percebi que esqueci minhas chaves. 3 00:00:07,900 --> 00:00:10,500 Cara, você SEMPRE esquece alguma coisa, isso é surreal! 4 00:00:11,000 --> 00:00:14,000 Ah, relaxa! Não é o fim do mundo, para de drama. 5 00:00:14,500 --> 00:00:17,800 Não é drama! Você falou que ia chegar no horário! 6 00:00:18,000 --> 00:00:21,500 Tá, tá... foi mal. Bora logo? 7 00:00:22,000 --> 00:00:25,000 Tá bom, mas se a gente se atrasar de novo, você é um filha da pu**! INSTRUÇÕES/CONTEXTO DO USUÁRIO (OPCIONAL): {contexto_final} --- LEGENDA BASE (WHISPER) --- {srt_filtered} """ model_obj = get_gemini_model(request.model) response_gemini = await client.generate_content(prompt, files=[processed_audio_path], model=model_obj) cleaned_srt = clean_and_validate_srt(response_gemini.text) if request.time_start and request.time_start > 0: cleaned_srt = shift_srt_timestamps(cleaned_srt, request.time_start) srt_filtered = shift_srt_timestamps(srt_filtered, request.time_start) return JSONResponse(content={ "srt": cleaned_srt, "original_srt": srt_filtered, "srt_word_level": word_level_text, "used_audio_processed": True }) except Exception as e: raise HTTPException(status_code=500, detail=str(e))