| 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") |
|
|
| |
| 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) |
| |
| 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: |
| |
| return Model.G_3_PRO_AI_FREE |
| |
| |
| return Model.G_3_FLASH_AI_FREE |
|
|
|
|
| |
| |
| |
|
|
| 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 |
| 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" |
| } |
|
|
| |
| 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"] |
|
|
| |
| 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}") |
|
|
| |
| 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" |
| |
| |
| 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 = [] |
|
|
| |
| 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.") |
| |
| |
| 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 |
|
|
| 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.") |
| |
| |
| 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}") |
|
|
| |
| 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" |
|
|
| 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, |
| ) |
|
|
| |
| 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] |
|
|
| |
| 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: |
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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) |
|
|
| |
| 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") |
| contains_image = record.get("contains_image", False) |
| filter_message = record.get("filter_message", "") |
| shortcode = record.get("ig_id") |
|
|
| if not comments and shortcode: |
| try: |
| |
| 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 = [] |
| temp_file_original = None |
|
|
| |
| 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) |
| |
| |
| 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) |
| |
| temp_file_original = orig_temp |
| except Exception as e: |
| print(f"⚠️ Não foi possível baixar o vídeo original para contexto: {e}") |
| else: |
| |
| 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: |
| |
| 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.") |
| |
| |
| 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}") |
| |
| |
| 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" |
|
|
| |
| 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") |
| model_obj = get_gemini_model(model_name) |
| print(f"🧠 [{time.time()-t_start:.1f}s] Enviando para Gemini ({model_name}) para processamento...") |
| |
| |
| 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] |
|
|
| |
| 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_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) |
| |
| |
| 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") |
| if result_json: |
| result_data = result_json[0] if isinstance(result_json[0], dict) else {} |
| title_text = result_data.get("title", "") |
|
|
| |
| |
| if 'image' not in content_type 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(f"⚠️ Erro ao tirar screenshot: {ffmpeg_ss.stderr[:200]}") |
| else: |
| |
| 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: |
| |
| 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", "") |
| |
| |
| 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}") |
| |
| |
| 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" |
| print(f"📐 Dimensões do vídeo: {crop_w}x{crop_h} | Template: {video_template}") |
|
|
| |
| |
| |
| if video_template == "vertical": |
| |
| 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: |
| |
| video_x = 0 |
| video_y = 0 |
| print(f" → video_y (crop offset): {video_y}px") |
|
|
| |
| 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: |
| |
| import urllib.request as _urlreq |
| import uuid as _uuid |
|
|
| |
| 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 |
| 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}") |
|
|
| |
| 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 |
| |
| |
| 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 |
| ) |
| |
| |
| 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}") |
|
|
| |
| 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 |
| |
| |
| 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: |
| |
| 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: |
| |
| 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}") |
| |
| |
| if result_json and srt_for_export: |
| if isinstance(result_json[0], dict): |
| result_json[0]["subtitle_srt"] = srt_for_export |
|
|
| |
| 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 |
|
|
| |
| try: |
| import urllib.parse as _up |
| _r0 = result_json[0] if result_json else {} |
| legenda_done = _r0.get("description", "") |
| |
| |
| 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: |
| |
| |
| 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) |
|
|
| |
| 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}") |
|
|
| |
| 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 |
| |
| |
| 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: |
| |
| 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 |
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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 |
|
|
| |
| |
| 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 |
| |
| 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 |
| ) |
| |
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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: |
| |
| |
| pass |
|
|
| |
| 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) |
|
|
| |
| 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 |
| |
| |
| 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}") |
|
|
| |
| 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() |
|
|
| |
| 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) |
|
|
| |
| try: |
| import urllib.parse |
| |
| 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 |
| ) |
| |
| |
| is_published = publish_data.get("published", False) |
| needs_verification = publish_data.get("superior_needs_verification", False) |
| |
| sys_end_msg = "" |
| |
| if is_published: |
| |
| 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}") |
|
|
| |
| 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_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: |
| |
| 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) |
| |
| |
| 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)) |