Update app.py
Browse files
app.py
CHANGED
|
@@ -1,1048 +1,63 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
-
from fastapi.responses import JSONResponse
|
| 3 |
from pydantic import BaseModel
|
| 4 |
-
import
|
| 5 |
-
from
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
import os
|
| 8 |
-
import re
|
| 9 |
-
import requests
|
| 10 |
-
import time
|
| 11 |
-
import base64
|
| 12 |
-
import io
|
| 13 |
-
from PIL import Image
|
| 14 |
-
import json
|
| 15 |
-
import subprocess
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
# Criar diretório static se não existir
|
| 24 |
-
os.makedirs("static", exist_ok=True)
|
| 25 |
-
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 26 |
-
|
| 27 |
-
# Inicializar chatbot globalmente
|
| 28 |
-
|
| 29 |
-
# Inicializar chatbots globalmente
|
| 30 |
-
chatbots = {}
|
| 31 |
-
upscale_chatbot = None
|
| 32 |
-
|
| 33 |
-
async def update_cookie_if_needed(cookie_path: str, secure_1psid: str, secure_1psidts: str, additional_cookies: dict):
|
| 34 |
-
"""
|
| 35 |
-
Tenta atualizar o cookie __Secure-1PSIDTS se necessário.
|
| 36 |
-
Retorna o novo cookie ou o original se não precisar atualizar.
|
| 37 |
-
"""
|
| 38 |
-
# Não tentar atualizar proativamente - deixar o sistema fazer isso quando necessário
|
| 39 |
-
# Isso evita erros 401/404 quando o cookie já expirou
|
| 40 |
-
return secure_1psidts
|
| 41 |
-
|
| 42 |
-
async def init_chatbot(retry_count=0, max_retries=2):
|
| 43 |
-
"""
|
| 44 |
-
Inicializa o chatbot com os cookies de forma assíncrona.
|
| 45 |
-
Tenta atualizar cookies automaticamente se falhar.
|
| 46 |
-
"""
|
| 47 |
-
|
| 48 |
-
global chatbots, upscale_chatbot
|
| 49 |
-
cookie_path = os.getenv("COOKIE_PATH", "cookies.json")
|
| 50 |
-
|
| 51 |
-
if not os.path.exists(cookie_path):
|
| 52 |
-
raise FileNotFoundError(f"Arquivo de cookies não encontrado: {cookie_path}")
|
| 53 |
-
|
| 54 |
-
try:
|
| 55 |
-
# Carregar cookies
|
| 56 |
-
secure_1psid, secure_1psidts, additional_cookies = load_cookies(cookie_path)
|
| 57 |
-
|
| 58 |
-
# Tentar atualizar cookie proativamente antes de inicializar
|
| 59 |
-
if retry_count == 0:
|
| 60 |
-
secure_1psidts = await update_cookie_if_needed(cookie_path, secure_1psid, secure_1psidts, additional_cookies)
|
| 61 |
-
|
| 62 |
-
# Criar Chatbot Flash (Padrão/Rápido)
|
| 63 |
-
chatbots['flash'] = await AsyncChatbot.create(
|
| 64 |
-
secure_1psid=secure_1psid,
|
| 65 |
-
secure_1psidts=secure_1psidts,
|
| 66 |
-
model=Model.G_3_0_FLASH,
|
| 67 |
-
additional_cookies=additional_cookies,
|
| 68 |
-
cookie_path=cookie_path
|
| 69 |
-
)
|
| 70 |
-
print(f"Chatbot Flash (3.0) inicializado com sucesso")
|
| 71 |
-
|
| 72 |
-
# Criar Chatbot Thinking (Raciocínio) - Timeout maior
|
| 73 |
-
chatbots['thinking'] = await AsyncChatbot.create(
|
| 74 |
-
secure_1psid=secure_1psid,
|
| 75 |
-
secure_1psidts=secure_1psidts,
|
| 76 |
-
model=Model.G_3_0_THINKING,
|
| 77 |
-
additional_cookies=additional_cookies,
|
| 78 |
-
cookie_path=cookie_path,
|
| 79 |
-
timeout=120 # Timeout maior para thinking
|
| 80 |
-
)
|
| 81 |
-
print(f"Chatbot Thinking (3.0) inicializado com sucesso")
|
| 82 |
-
|
| 83 |
-
# Fallback/Default
|
| 84 |
-
chatbots['default'] = chatbots['flash']
|
| 85 |
-
|
| 86 |
-
# Criar instância de Upscale separada
|
| 87 |
-
upscale_chatbot = await AsyncChatbot.create(
|
| 88 |
-
secure_1psid=secure_1psid,
|
| 89 |
-
secure_1psidts=secure_1psidts,
|
| 90 |
-
model=Model.NANO_BANANA,
|
| 91 |
-
additional_cookies=additional_cookies,
|
| 92 |
-
cookie_path=cookie_path
|
| 93 |
-
)
|
| 94 |
-
print(f"Upscale Chatbot inicializado com sucesso usando modelo NANO_BANANA")
|
| 95 |
-
|
| 96 |
-
except (ValueError, PermissionError) as e:
|
| 97 |
-
error_str = str(e).lower()
|
| 98 |
-
# Se o erro é relacionado a cookie expirado, não tentar atualizar novamente
|
| 99 |
-
# O sistema já tentou atualizar automaticamente e falhou
|
| 100 |
-
print(f"Erro ao inicializar chatbot: {e}")
|
| 101 |
-
print(f"AVISO: Cookies podem estar expirados. Por favor, atualize manualmente os cookies em {cookie_path}")
|
| 102 |
-
print(f"Para atualizar: acesse https://gemini.google.com/app e copie os novos cookies __Secure-1PSID e __Secure-1PSIDTS")
|
| 103 |
-
raise
|
| 104 |
-
except Exception as e:
|
| 105 |
-
print(f"Erro ao inicializar chatbot: {e}")
|
| 106 |
-
raise
|
| 107 |
-
|
| 108 |
-
# Inicializar na startup
|
| 109 |
@app.on_event("startup")
|
| 110 |
async def startup_event():
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
"""
|
| 148 |
-
if start_time is None or end_time is None:
|
| 149 |
-
return srt_content
|
| 150 |
-
|
| 151 |
-
# Padrão para capturar legendas
|
| 152 |
-
pattern = re.compile(r"(\d+)\s*\n([^-\n]+?) --> ([^-\n]+?)\s*\n((?:(?!^\d+\s*\n).+\n?)*)", re.MULTILINE)
|
| 153 |
-
matches = pattern.findall(srt_content)
|
| 154 |
-
|
| 155 |
-
filtered_subtitles = []
|
| 156 |
-
for num, start, end, text in matches:
|
| 157 |
-
start_seconds = srt_time_to_seconds(start.strip())
|
| 158 |
-
end_seconds = srt_time_to_seconds(end.strip())
|
| 159 |
-
|
| 160 |
-
# Verificar se a legenda está dentro do intervalo [start_time, end_time]
|
| 161 |
-
# Incluir legendas que se sobrepõem parcialmente
|
| 162 |
-
if end_seconds > start_time and start_seconds < end_time:
|
| 163 |
-
# Ajustar timestamps para começar do zero
|
| 164 |
-
new_start = max(0, start_seconds - start_time)
|
| 165 |
-
new_end = min(end_time - start_time, end_seconds - start_time)
|
| 166 |
-
|
| 167 |
-
# Garantir que new_end > new_start
|
| 168 |
-
if new_end > new_start:
|
| 169 |
-
filtered_subtitles.append({
|
| 170 |
-
'start': new_start,
|
| 171 |
-
'end': new_end,
|
| 172 |
-
'text': text.strip()
|
| 173 |
-
})
|
| 174 |
-
|
| 175 |
-
# Gerar SRT cortado
|
| 176 |
-
srt_cut = ""
|
| 177 |
-
for i, sub in enumerate(filtered_subtitles, 1):
|
| 178 |
-
start_srt = seconds_to_srt_time(sub['start'])
|
| 179 |
-
end_srt = seconds_to_srt_time(sub['end'])
|
| 180 |
-
srt_cut += f"{i}\n{start_srt} --> {end_srt}\n{sub['text']}\n\n"
|
| 181 |
-
|
| 182 |
-
return srt_cut.strip()
|
| 183 |
-
|
| 184 |
-
def clean_and_validate_srt(srt_content):
|
| 185 |
-
"""Limpa e valida conteúdo SRT seguindo o padrão do example.py"""
|
| 186 |
-
# Tentar extrair conteúdo de blocos de código ```srt ou ```
|
| 187 |
-
if "```" in srt_content:
|
| 188 |
-
# Padrão regex para capturar conteúdo dentro de ```srt ... ``` ou ``` ... ```
|
| 189 |
-
code_block_pattern = re.compile(r"```(?:srt)?\n(.*?)```", re.DOTALL | re.IGNORECASE)
|
| 190 |
-
match = code_block_pattern.search(srt_content)
|
| 191 |
-
if match:
|
| 192 |
-
srt_content = match.group(1).strip()
|
| 193 |
-
|
| 194 |
-
# Se ainda tiver muito texto antes do primeiro timestamp, tentar limpar
|
| 195 |
-
# Procura pelo primeiro padrão "1\n00:00"
|
| 196 |
-
first_block_pattern = re.compile(r"^\s*\d+\s*\n\d{2}:\d{2}:\d{2},\d{3}", re.MULTILINE)
|
| 197 |
-
match = first_block_pattern.search(srt_content)
|
| 198 |
-
if match:
|
| 199 |
-
srt_content = srt_content[match.start():]
|
| 200 |
-
|
| 201 |
-
# Padrão mais flexível para capturar timestamps mal formatados
|
| 202 |
-
pattern = re.compile(r"(\d+)\s*\n([^-\n]+?) --> ([^-\n]+?)\s*\n((?:(?!^\d+\s*\n).+\n?)*)", re.MULTILINE)
|
| 203 |
-
matches = pattern.findall(srt_content)
|
| 204 |
-
|
| 205 |
-
def corrigir_timestamp(timestamp):
|
| 206 |
-
timestamp = timestamp.strip()
|
| 207 |
-
|
| 208 |
-
# Se já está correto, retorna
|
| 209 |
-
if re.match(r"\d{2}:\d{2}:\d{2},\d{3}", timestamp):
|
| 210 |
-
return timestamp
|
| 211 |
-
|
| 212 |
-
# Formato: MM:SS,mmm -> HH:MM:SS,mmm
|
| 213 |
-
if re.match(r"\d{2}:\d{2},\d{3}", timestamp):
|
| 214 |
-
return f"00:{timestamp}"
|
| 215 |
-
|
| 216 |
-
# Formato: M:SS,mmm -> HH:MM:SS,mmm
|
| 217 |
-
if re.match(r"\d{1}:\d{2},\d{3}", timestamp):
|
| 218 |
-
parts = timestamp.split(":")
|
| 219 |
-
minutes = parts[0].zfill(2)
|
| 220 |
-
return f"00:{minutes}:{parts[1]}"
|
| 221 |
-
|
| 222 |
-
# Formato: SS,mmm -> HH:MM:SS,mmm
|
| 223 |
-
if re.match(r"\d{1,2},\d{3}", timestamp):
|
| 224 |
-
seconds_ms = timestamp.split(",")
|
| 225 |
-
seconds = seconds_ms[0].zfill(2)
|
| 226 |
-
return f"00:00:{seconds},{seconds_ms[1]}"
|
| 227 |
-
|
| 228 |
-
# Outros formatos problemáticos
|
| 229 |
-
if re.match(r"\d{2}:\d{2}:\d{3}", timestamp):
|
| 230 |
-
parts = timestamp.split(":")
|
| 231 |
-
if len(parts) == 3:
|
| 232 |
-
h, m, s_ms = parts
|
| 233 |
-
if len(s_ms) == 3:
|
| 234 |
-
return f"{h}:{m}:00,{s_ms}"
|
| 235 |
-
elif len(s_ms) >= 4:
|
| 236 |
-
s = s_ms[:-3]
|
| 237 |
-
ms = s_ms[-3:]
|
| 238 |
-
return f"{h}:{m}:{s.zfill(2)},{ms}"
|
| 239 |
-
|
| 240 |
-
return timestamp
|
| 241 |
-
|
| 242 |
-
srt_corrigido = ""
|
| 243 |
-
for i, (num, start, end, text) in enumerate(matches, 1):
|
| 244 |
-
text = text.strip()
|
| 245 |
-
if not text:
|
| 246 |
-
continue
|
| 247 |
-
|
| 248 |
-
# Verificar se a legenda tem mais de 2 linhas
|
| 249 |
-
text_lines = [line.strip() for line in text.split('\n') if line.strip()]
|
| 250 |
-
if len(text_lines) > 2:
|
| 251 |
-
# Limitar a 2 linhas, juntando as extras na segunda linha
|
| 252 |
-
text = text_lines[0] + '\n' + ' '.join(text_lines[1:])
|
| 253 |
-
|
| 254 |
-
start_corrigido = corrigir_timestamp(start)
|
| 255 |
-
end_corrigido = corrigir_timestamp(end)
|
| 256 |
-
srt_corrigido += f"{i}\n{start_corrigido} --> {end_corrigido}\n{text}\n\n"
|
| 257 |
-
|
| 258 |
-
return srt_corrigido.strip()
|
| 259 |
-
|
| 260 |
-
def download_file_with_retry(url: str, max_retries: int = 3, timeout: int = 300):
|
| 261 |
-
"""
|
| 262 |
-
Baixa arquivo com retry logic e tratamento de rate limiting.
|
| 263 |
-
|
| 264 |
-
Parâmetros:
|
| 265 |
-
- url: URL do arquivo
|
| 266 |
-
- max_retries: Número máximo de tentativas
|
| 267 |
-
- timeout: Timeout em segundos
|
| 268 |
-
|
| 269 |
-
Retorna: Response object do requests
|
| 270 |
-
"""
|
| 271 |
-
headers = {
|
| 272 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 273 |
-
'Accept': '*/*',
|
| 274 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
| 275 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
| 276 |
-
'Connection': 'keep-alive',
|
| 277 |
-
'Upgrade-Insecure-Requests': '1'
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
for attempt in range(max_retries):
|
| 281 |
-
try:
|
| 282 |
-
if attempt > 0:
|
| 283 |
-
# Backoff exponencial: 2^attempt segundos
|
| 284 |
-
wait_time = 2 ** attempt
|
| 285 |
-
print(f"⏳ Aguardando {wait_time}s antes de tentar novamente (tentativa {attempt + 1}/{max_retries})...")
|
| 286 |
-
time.sleep(wait_time)
|
| 287 |
-
|
| 288 |
-
print(f"📥 Tentativa {attempt + 1}/{max_retries} - Baixando arquivo de: {url}")
|
| 289 |
-
response = requests.get(url, headers=headers, timeout=timeout, stream=True)
|
| 290 |
-
|
| 291 |
-
# Tratar erro 429 (Too Many Requests)
|
| 292 |
-
if response.status_code == 429:
|
| 293 |
-
retry_after = response.headers.get('Retry-After')
|
| 294 |
-
if retry_after:
|
| 295 |
-
wait_time = int(retry_after)
|
| 296 |
-
print(f"⚠️ Rate limit atingido. Aguardando {wait_time}s conforme Retry-After header...")
|
| 297 |
-
time.sleep(wait_time)
|
| 298 |
-
elif attempt < max_retries - 1:
|
| 299 |
-
# Se não houver Retry-After, usar backoff exponencial
|
| 300 |
-
wait_time = (2 ** attempt) * 5 # 5s, 10s, 20s...
|
| 301 |
-
print(f"⚠️ Rate limit atingido. Aguardando {wait_time}s antes de tentar novamente...")
|
| 302 |
-
time.sleep(wait_time)
|
| 303 |
-
continue
|
| 304 |
-
else:
|
| 305 |
-
raise HTTPException(
|
| 306 |
-
status_code=429,
|
| 307 |
-
detail=f"Rate limit atingido após {max_retries} tentativas. Tente novamente mais tarde."
|
| 308 |
-
)
|
| 309 |
-
|
| 310 |
-
response.raise_for_status()
|
| 311 |
-
return response
|
| 312 |
-
|
| 313 |
-
except requests.exceptions.HTTPError as e:
|
| 314 |
-
if e.response.status_code == 429 and attempt < max_retries - 1:
|
| 315 |
-
continue
|
| 316 |
-
elif attempt == max_retries - 1:
|
| 317 |
-
raise HTTPException(
|
| 318 |
-
status_code=400,
|
| 319 |
-
detail=f"Erro ao baixar arquivo após {max_retries} tentativas: {str(e)}"
|
| 320 |
-
)
|
| 321 |
-
else:
|
| 322 |
-
raise
|
| 323 |
-
except requests.exceptions.RequestException as e:
|
| 324 |
-
if attempt == max_retries - 1:
|
| 325 |
-
raise HTTPException(
|
| 326 |
-
status_code=400,
|
| 327 |
-
detail=f"Erro ao baixar arquivo após {max_retries} tentativas: {str(e)}"
|
| 328 |
-
)
|
| 329 |
-
continue
|
| 330 |
-
|
| 331 |
-
raise HTTPException(
|
| 332 |
-
status_code=400,
|
| 333 |
-
detail=f"Falha ao baixar arquivo após {max_retries} tentativas"
|
| 334 |
-
)
|
| 335 |
-
|
| 336 |
-
class ChatRequest(BaseModel):
|
| 337 |
-
message: str
|
| 338 |
-
context: Optional[str] = None
|
| 339 |
-
model: Optional[str] = "flash" # 'flash' or 'thinking'
|
| 340 |
-
|
| 341 |
-
@app.post("/chat")
|
| 342 |
-
async def chat_endpoint(request: ChatRequest):
|
| 343 |
-
"""
|
| 344 |
-
Endpoint para conversas de texto simples.
|
| 345 |
-
"""
|
| 346 |
-
if not chatbots:
|
| 347 |
-
raise HTTPException(status_code=500, detail="Chatbot não inicializado")
|
| 348 |
-
|
| 349 |
-
try:
|
| 350 |
-
requested_model = request.model.lower() if request.model else "flash"
|
| 351 |
-
if "thinking" in requested_model:
|
| 352 |
-
selected_chatbot = chatbots.get('thinking', chatbots['default'])
|
| 353 |
-
else:
|
| 354 |
-
selected_chatbot = chatbots.get('flash', chatbots['default'])
|
| 355 |
-
|
| 356 |
-
prompt = request.message
|
| 357 |
-
if request.context:
|
| 358 |
-
prompt = f"Contexto: {request.context}\n\nMensagem: {request.message}"
|
| 359 |
-
|
| 360 |
-
print(f"💬 Chat request ({requested_model}): {prompt[:50]}...")
|
| 361 |
-
response_gemini = await selected_chatbot.ask(prompt)
|
| 362 |
-
|
| 363 |
-
if response_gemini.get("error"):
|
| 364 |
-
raise HTTPException(
|
| 365 |
-
status_code=500,
|
| 366 |
-
detail=f"Erro no Gemini: {response_gemini.get('content', 'Erro desconhecido')}"
|
| 367 |
-
)
|
| 368 |
-
|
| 369 |
-
return {"response": response_gemini.get("content", "")}
|
| 370 |
-
|
| 371 |
-
except Exception as e:
|
| 372 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
def extract_json_from_text(text: str):
|
| 376 |
-
"""
|
| 377 |
-
Extrai JSON válido de uma string que pode conter markdown.
|
| 378 |
-
Remove vírgulas finais (trailing commas) que quebram o json.loads.
|
| 379 |
-
"""
|
| 380 |
-
text = text.strip()
|
| 381 |
-
|
| 382 |
-
# Remover blocos de código markdown
|
| 383 |
-
if "```json" in text:
|
| 384 |
-
text = text.split("```json")[1].split("```")[0].strip()
|
| 385 |
-
elif "```" in text:
|
| 386 |
-
# Tenta achar onde fecha o bloco
|
| 387 |
-
parts = text.split("```")
|
| 388 |
-
if len(parts) >= 2:
|
| 389 |
-
text = parts[1].strip()
|
| 390 |
-
|
| 391 |
-
# Tentar encontrar o início e fim de um array JSON ou objeto
|
| 392 |
-
start_idx = text.find('[')
|
| 393 |
-
end_idx = text.rfind(']')
|
| 394 |
-
|
| 395 |
-
if start_idx != -1 and end_idx != -1:
|
| 396 |
-
text = text[start_idx:end_idx+1]
|
| 397 |
-
else:
|
| 398 |
-
# Tentar objeto se não for array
|
| 399 |
-
start_idx = text.find('{')
|
| 400 |
-
end_idx = text.rfind('}')
|
| 401 |
-
if start_idx != -1 and end_idx != -1:
|
| 402 |
-
text = text[start_idx:end_idx+1]
|
| 403 |
-
|
| 404 |
-
# Remover trailing commas (vírgulas antes de fechamento de } ou ])
|
| 405 |
-
# Ex: {"a": 1,} -> {"a": 1}
|
| 406 |
-
# Ex: [1, 2,] -> [1, 2]
|
| 407 |
-
import re
|
| 408 |
-
text = re.sub(r',\s*([\]}])', r'\1', text)
|
| 409 |
-
|
| 410 |
-
try:
|
| 411 |
-
return json.loads(text)
|
| 412 |
-
except json.JSONDecodeError as e:
|
| 413 |
-
print(f"Erro ao decodificar JSON: {e}")
|
| 414 |
-
# Tentativa desesperada: se falhar, tentar usar ast.literal_eval se parecer python dict/list
|
| 415 |
-
# Mas cuidado com segurança. Melhor retornar erro por enquanto.
|
| 416 |
-
return None
|
| 417 |
-
|
| 418 |
-
def cut_video(input_path: str, output_path: str, start: str, end: str):
|
| 419 |
-
"""
|
| 420 |
-
Corta um vídeo usando ffmpeg.
|
| 421 |
-
"""
|
| 422 |
-
try:
|
| 423 |
-
command = [
|
| 424 |
-
"ffmpeg", "-y",
|
| 425 |
-
"-i", input_path,
|
| 426 |
-
"-ss", start,
|
| 427 |
-
"-to", end,
|
| 428 |
-
"-c:v", "libx264", "-c:a", "aac",
|
| 429 |
-
"-strict", "experimental",
|
| 430 |
-
output_path
|
| 431 |
-
]
|
| 432 |
-
# Executar comando silenciando output para não poluir logs, mas capturando erro
|
| 433 |
-
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 434 |
-
return True
|
| 435 |
-
except subprocess.CalledProcessError as e:
|
| 436 |
-
print(f"Erro ao cortar vídeo: {e.stderr.decode()}")
|
| 437 |
-
return False
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
class GenerateElementsRequest(BaseModel):
|
| 441 |
-
video_url: str
|
| 442 |
-
context: Optional[str] = None
|
| 443 |
-
start: Optional[str] = None
|
| 444 |
-
end: Optional[str] = None
|
| 445 |
-
model: Optional[str] = "flash"
|
| 446 |
-
comments: Optional[list] = None
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
@app.post("/generate-elements")
|
| 450 |
-
async def generate_elements_endpoint(request: GenerateElementsRequest):
|
| 451 |
-
"""
|
| 452 |
-
Gera elementos a partir de um vídeo (ou trecho dele).
|
| 453 |
-
Duplicação do generate-titles para personalização futura do prompt.
|
| 454 |
-
"""
|
| 455 |
-
if not chatbots:
|
| 456 |
-
raise HTTPException(status_code=500, detail="Chatbot não inicializado")
|
| 457 |
-
|
| 458 |
-
temp_file = None
|
| 459 |
-
cut_file = None
|
| 460 |
-
|
| 461 |
-
try:
|
| 462 |
-
# 1. Validar e Baixar Vídeo
|
| 463 |
-
if not request.video_url:
|
| 464 |
-
raise HTTPException(status_code=400, detail="URL do vídeo é obrigatória")
|
| 465 |
-
|
| 466 |
-
print(f"📥 [GenerateElements] Baixando vídeo: {request.video_url}")
|
| 467 |
-
|
| 468 |
-
# Baixar direto para um arquivo temporário
|
| 469 |
-
response = download_file_with_retry(request.video_url, timeout=600)
|
| 470 |
-
|
| 471 |
-
content_type = response.headers.get('content-type', '').lower()
|
| 472 |
-
ext = '.mp4'
|
| 473 |
-
if 'webm' in content_type: ext = '.webm'
|
| 474 |
-
elif 'mkv' in content_type: ext = '.mkv'
|
| 475 |
-
|
| 476 |
-
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
|
| 477 |
-
for chunk in response.iter_content(chunk_size=1024*1024):
|
| 478 |
-
if chunk:
|
| 479 |
-
temp_file.write(chunk)
|
| 480 |
-
temp_file.close()
|
| 481 |
-
|
| 482 |
-
video_path_to_analyze = temp_file.name
|
| 483 |
-
|
| 484 |
-
# 2. Cortar Vídeo se necessário
|
| 485 |
-
if request.start and request.end:
|
| 486 |
-
print(f"✂️ [GenerateElements] Cortando vídeo de {request.start} até {request.end}...")
|
| 487 |
-
cut_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
|
| 488 |
-
cut_file.close()
|
| 489 |
-
|
| 490 |
-
success = cut_video(temp_file.name, cut_file.name, request.start, request.end)
|
| 491 |
-
if success:
|
| 492 |
-
video_path_to_analyze = cut_file.name
|
| 493 |
-
print(f"✅ Vídeo cortado: {video_path_to_analyze}")
|
| 494 |
-
else:
|
| 495 |
-
print("⚠️ Falha ao cortar vídeo, usando original.")
|
| 496 |
-
|
| 497 |
-
# 3. Preparar Prompt
|
| 498 |
-
contexto_add = f"\n{request.context}" if request.context else ""
|
| 499 |
-
|
| 500 |
-
comentarios_add = ""
|
| 501 |
-
if request.comments:
|
| 502 |
-
comentarios_add = "\nCOMENTÁRIOS DO POST (Use como forte inspiração para criar títulos mais reais e humanizados):\n"
|
| 503 |
-
for c in request.comments:
|
| 504 |
-
text = c.get("text", "").strip()
|
| 505 |
-
if text:
|
| 506 |
-
likes = c.get("like_count", 0)
|
| 507 |
-
comentarios_add += f"- {text} ({likes} curtidas)\n"
|
| 508 |
-
|
| 509 |
-
prompt = f"""
|
| 510 |
-
IDIOMA: Todo o conteúdo gerado (título, descrição) DEVE ser em PORTUGUÊS DO BRASIL. Mesmo que o vídeo esteja em outro idioma, a saída final deve ser inteiramente em pt-BR.
|
| 511 |
-
|
| 512 |
-
Crie um título e uma descrição analisando o vídeo, o contexto e os comentários fornecidos. Corrija qualquer informação imprecisa, utilize técnicas modernas que prendem o leitor a ler até o final.
|
| 513 |
-
A legenda deve ser compatível e do tamanho de uma legenda do Instagram.
|
| 514 |
-
A descrição deve ser sem tópicos, apenas descrição limpa e direta, sem conclusões parecendo IA e sem enrolação ou redundâncias.
|
| 515 |
-
Quero informações concretas e factuais, não pensamentos, opiniões ou imaginações.
|
| 516 |
-
Não seja redundante, cada frase precisa adicionar informação nova.
|
| 517 |
-
LEMBRE-SE: Como os comentários são feitos por humanos reais, você DEVE olhar para eles e usá-los como inspiração, se necessário, para gerar títulos com uma pegada MUITO mais humanizada, baseando-se nas reações reais.
|
| 518 |
-
Se inspire rigorosamente no modo de escrita dos exemplos fornecidos.
|
| 519 |
-
|
| 520 |
-
ESTILO DE ESCRITA OBRIGATÓRIO:
|
| 521 |
-
|
| 522 |
-
- Tom INFORMAL e conversacional que mistura precisão factual com fluidez de quem tá contando algo pra um amigo
|
| 523 |
-
- Use LINGUAGEM COLOQUIAL brasileira: "tava" em vez de "estava", "pra" em vez de "para", "tá" em vez de "está", "pro" em vez de "para o", "num" em vez de "em um", "aí" em vez de "então", "tipo" como conectivo casual quando fizer sentido
|
| 524 |
-
- Use conectivos naturais como aliás, na verdade, por exemplo, definitivamente, inclusive, etc, pra criar ritmo
|
| 525 |
-
- Palavras-chave em MAIÚSCULA pra ênfase quando fizer sentido
|
| 526 |
-
- Informações diretas, sem rodeios, cada frase deve acrescentar um dado novo
|
| 527 |
-
- NUNCA termine com frases que pareçam conclusões de IA
|
| 528 |
-
- Evite palavras como consolidou, definiu, simboliza especialmente no final
|
| 529 |
-
- Termine a descrição sempre com um FATO CONCRETO como número, prêmio, data ou detalhe técnico relevante
|
| 530 |
-
- NUNCA use travessões em nenhuma parte do texto
|
| 531 |
-
- NUNCA faça perguntas retóricas ou diretas ao final da descrição
|
| 532 |
-
- NUNCA utilize termos como "O vídeo resgata", "O vídeo mostra", etc... a descrição deve ser sempre direta.
|
| 533 |
-
- A descrição NÃO precisa ser 100% coloquial, mas o tom geral deve soar natural e humano, nunca robótico ou acadêmico
|
| 534 |
-
|
| 535 |
-
LEGENDA:
|
| 536 |
-
- Define se o vídeo precisa de legendas (se há fala importante que precisa ser traduzida ou transcrita).
|
| 537 |
-
- Responda com true se houver diálogo/fala crucial.
|
| 538 |
-
- Responda com false se for apenas visual, música de fundo ou fala irrelevante.
|
| 539 |
-
|
| 540 |
-
TÍTULOS E EMOJIS:
|
| 541 |
-
|
| 542 |
-
- O público primário é Geração Z, portanto o tom DEVE ser descontraído e informal
|
| 543 |
-
- O uso de "Quando" no início do título é UMA OPÇÃO ESTILÍSTICA recomendada, não obrigatória
|
| 544 |
-
- Varie estruturas de título, podendo usar afirmações diretas, contrastes, dados impactantes ou frases curtas
|
| 545 |
-
- Emojis são opcionais e devem ser usados apenas quando reforçam a emoção do contexto
|
| 546 |
-
- Não use emojis em todos os títulos
|
| 547 |
-
- O título só deve ter UM emoji, NUNCA DOIS.
|
| 548 |
-
- PRIORIZE emojis de humor e emoção da Geração Z: 💀 (choque/morri), 😭 (choro de riso/emoção), 🥀 (melancolia/dor), 💅 (deboche), 🫠 (derretendo), 😵 (chocado), 🫣 (constrangimento), 🤡 (palhaçada), 🥲 (sorriso triste). Use 🥹 e 🥺 pra momentos fofos/emocionantes. Evite emojis genéricos como �, ❤️, 😊.
|
| 549 |
-
- Se for utilizado coração, SEMPRE deve ser coração sem ser o vermelho ou branco... dependendo do contexto.
|
| 550 |
-
- MUITO IMPORTANTE: Mantenha as quebras de linha na descrição utilizando `\\n\\n` no JSON pra separar os parágrafos, assim o texto não fica tudo numa única linha.
|
| 551 |
-
|
| 552 |
-
Contexto que pode ajudar: {contexto_add}
|
| 553 |
-
{comentarios_add}
|
| 554 |
-
|
| 555 |
-
EXEMPLOS (saída esperada em JSON):
|
| 556 |
-
|
| 557 |
-
[
|
| 558 |
-
{{
|
| 559 |
-
"title": "É incrível como ele era vulnerável e emotivo antes de perder qualquer traço de humanidade 💀",
|
| 560 |
-
"description": "No episódio 3 da 1ª temporada de Breaking Bad (...And the Bag's in the River), Walter White reconstrói um prato quebrado e descobre que falta um único estilhaço. Ele percebe que Krazy-8, o traficante que tava preso no porão dele, escondeu a peça pontiaguda pra usar como arma. O Walt tava decidido a libertar o cara, mas a prova física da traição forçou ele a estrangular o traficante pra sobreviver.\\n\\nFoi nesse momento que o professor de química entendeu que a empatia seria a condenação dele. Morria, naquele instante, o mestre de escola, e surgia a lógica inflexível de Heisenberg 🔥",
|
| 561 |
-
"legenda": false
|
| 562 |
-
}},
|
| 563 |
-
{{
|
| 564 |
-
"title": "Imagina escrever uma música do próprio livro e vê-la ganhar vida em live-action 🥹",
|
| 565 |
-
"description": "Jogos Vorazes deu vida a \\"The Hanging Tree\\", de Suzanne Collins, depois que a canção apareceu pela primeira vez em seu livro Mockingjay. Na Parte 1 de Mockingjay, no Distrito 12, o que começa como uma lembrança solene do pai de Katniss transforma-se em um grito de mobilização para que os distritos se oponham à Capital. Enquanto Snow manipula a mente de Peeta no silêncio do Distrito 13, a canção deixa muito claro que o poder de uma ideia é a única coisa que o medo não consegue deter. Até porque, nada representa uma ameaça maior para um tirano do que um povo que não tem mais nada a perder 🎯",
|
| 566 |
-
"legenda": true
|
| 567 |
-
}},
|
| 568 |
-
{{
|
| 569 |
-
"title": "Normal People foi tão bom porque o Paul Mescal não tava atuando 🥹",
|
| 570 |
-
"description": "🥹 Paul Mescal era um ator de teatro praticamente desconhecido até ser escalado como Connell Waldron em Normal People. A adaptação do best-seller de Sally Rooney rendeu a Mescal o BAFTA de Melhor Ator e uma indicação ao Emmy, consolidando a química dele com a Daisy Edgar-Jones como uma das mais realistas da televisão recente.\\n\\nA produção usou uma coordenadora de intimidade pra garantir que as cenas de vulnerabilidade fossem autênticas, focando mais na linguagem corporal e no silêncio do que em diálogos expositivos. Filmada na Irlanda e na Itália, a série mostra com precisão técnica a transição da vida escolar em Sligo pra universidade no Trinity College, fugindo dos clichês estéticos típicos de romances juvenis e apostando no naturalismo das atuações.",
|
| 571 |
-
"legenda": true
|
| 572 |
-
}},
|
| 573 |
-
{{
|
| 574 |
-
"title": "O exato momento em que Carl supera seu luto de décadas 🥺",
|
| 575 |
-
"description": "🍇 O broche que o Carl entrega pro Russell é uma tampa de refrigerante de uva (Grape Soda) original dos anos 30, o mesmo objeto que a Ellie deu pro Carl quando eles se conheceram na infância.\\n\\nNo final de Up: Altas Aventuras (2009), o Russell, com 8 anos, tá lidando com a falta do pai durante a cerimônia de formatura dos Exploradores da Natureza. Carl Fredricksen sobe ao palco e entrega pro garoto a \\"Insígnia Ellie\\", a mais alta distinção que ele tem. É o momento preciso em que o Carl supera o luto de décadas, passando o legado de aventura pro Russell e assumindo o papel de figura paterna pro garoto.\\n\\nDirigido por Pete Docter, o longa fez história ao ser a primeira animação a abrir o Festival de Cannes e levou os Oscars de Melhor Filme de Animação e Melhor Trilha Sonora.",
|
| 576 |
-
"legenda": true
|
| 577 |
-
}},
|
| 578 |
-
{{
|
| 579 |
-
"title": "“Ei, olha só... meu turno acabou de terminar” 😭",
|
| 580 |
-
"description": "No filme \\"Atração Perigosa\\" (2010), o Ben Affleck definitivamente caprichou no realismo tático quando mostrou a cultura criminosa de Charlestown, Boston. Nessa cena, por exemplo, logo após o assalto ao banco em North End, o policial interpretado por Jack Walsh simplesmente ignora a gangue do Doug MacRay, que tava equipada com fuzis automáticos e usando as famosas máscaras de freira. A escolha do cara é uma das reações mais pragmáticas do gênero policial... autopreservação pura diante de uma desvantagem letal óbvia. O filme, aliás, inspirado no livro \\"Prince of Thieves\\", rendeu pro Jeremy Renner uma indicação ao Oscar de Melhor Ator Coadjuvante pela atuação dele como o instável James Coughlin.",
|
| 581 |
-
"legenda": false
|
| 582 |
-
}},
|
| 583 |
-
{{
|
| 584 |
-
"title": "Quando um \\"Eu te odeio\\" carrega mais amor que um \\"Eu te amo\\" 😝",
|
| 585 |
-
"description": "No episódio \\"Fun Run\\" (4x01), Jim finge um pedido de casamento apenas para amarrar o cadarço, arrancando esse \\"eu te odeio\\" de Pam. O momento marca o MELHOR INÍCIO de temporada da série, quando o casal finalmente assume o namoro após três anos de tensão e o famoso beijo no \\"Casino Night\\" 🥹. A naturalidade da cena é, na verdade, fruto de um processo rigoroso de escalação... Greg Daniels, o showrunner, realizou inúmeros testes de química cruzada até que John Krasinski e Jenna Fischer se encontrassem. No dia do teste final, antes mesmo de começarem, Fischer perguntou a Krasinski se ele seria o Jim, e ele respondeu: \\"Você é minha Pam\\". A produção de The Office escolheu um estilo de romance \\"slow burn\\", no qual o afeto se desenvolvia em silêncio, por meio de olhares e piadas internas, evitando o melodrama típico das sitcoms dos anos 2000.",
|
| 586 |
-
"legenda": true
|
| 587 |
-
}},
|
| 588 |
-
{{
|
| 589 |
-
"title": "Dominic Monaghan simplesmente enganou Elijah Wood por 10 minutos e o resultado foi esse 😭",
|
| 590 |
-
"description": "Durante a turnê de divulgação de O Retorno do Rei em 2004, Dominic Monaghan, intérprete do hobbit Merry, assumiu o papel de um jornalista alemão fictício chamado Hans Jensen para entrevistar seu colega de elenco Elijah Wood. Monaghan estava em uma sala diferente com um modulador de voz, o que permitiu que ele fizesse perguntas cada vez mais absurdas enquanto Elijah, em um estúdio em Nova York, tentava manter o profissionalismo. O ponto alto da pegadinha ocorre quando Monaghan questiona Elijah repetidamente sobre o uso de perucas, gerando uma crise de riso incontrolável no ator ao perceber a bizarrice da situação. Curiosamente, a ironia técnica do momento reside no fato de que todos os atores principais de O Senhor dos Anéis utilizaram perucas durante os dezoito meses de filmagem na Nova Zelândia para garantir a continuidade visual dos personagens. O registro completo dessa entrevista foi incluído oficialmente como um easter egg nos extras do DVD da Versão Estendida de O Senhor dos Anéis: O Retorno do Rei.",
|
| 591 |
-
"legenda": true
|
| 592 |
-
}},
|
| 593 |
-
{{
|
| 594 |
-
"title": "E o Justin Bieber que já demonstrava um senso rítmico absurdo aos dois anos de idade? 😵",
|
| 595 |
-
"description": "O Justin Bieber tinha só dois anos quando a mãe dele, Pattie Mallette, gravou esse vídeo caseiro na cozinha da casa deles em Stratford, Ontário. A habilidade dele em manter o tempo rítmico e fazer viradas rápidas usando só as mãos e uma superfície improvisada impressiona pela coordenação motora absurda pra idade dele.\\n\\nO Justin aprendeu a tocar bateria de forma autodidata antes de passar pro piano e pro violão, instrumentos que ele dominou antes de ser descoberto no YouTube em 2007. O cara, inclusive, tocou bateria profissionalmente em várias turnês internacionais, mostrando que a base percussiva foi o fundamento da formação musical dele.\\n\\nEsse registro em particular virou uma das cenas mais icônicas do documentário \\"Never Say Never\\", que arrecadou 99 milhões de dólares no mundo todo.",
|
| 596 |
-
"legenda": true
|
| 597 |
-
}},
|
| 598 |
-
{{
|
| 599 |
-
"title": "Quando o James Franco foi apresentar o Oscar e a avó dele resolveu flertar com o Mark Wahlberg 😭",
|
| 600 |
-
"description": "A fim de atrair um público mais jovem, a 83ª edição do Academy Awards, realizada em 2011, escalou James Franco e Anne Hathaway como apresentadores. Franco fez uma apresentação espontânea de sua avó, Mitsue \\"Mitzie\\" Verne, que se encontrava na plateia. Quando pegou o microfone, ela direcionou sua atenção a Mark Wahlberg, referindo-se a ele pelo apelido de sua carreira inicial, \\"Marky Mark\\". A interação rompeu o protocolo oficial da premiação e provocou uma reação autêntica de Wahlberg, que riu ao ser apontado diante das câmeras.\\n\\nA tentativa da Academia de modernizar o evento por meio de interações não roteirizadas entre os convidados da primeira fila e os apresentadores foi um dos principais destaques da edição do Oscar.\\n\\nMitzie Verne, aliás, era uma personalidade reconhecida no cenário artístico de Cleveland, cidade onde estabeleceu a Verne Interactive Collective Gallery em 1953.",
|
| 601 |
-
"legenda": true
|
| 602 |
-
}},
|
| 603 |
-
{{
|
| 604 |
-
"title": "O mano é inocente demais pra esse mundo tão cruel 😭",
|
| 605 |
-
"description": "Estátuas vivas são artistas de rua que usam técnicas rigorosas de controle da respiração e relaxamento muscular pra ficarem imóveis por períodos de 30 a 60 minutos. O artista Donald Eleanor, por exemplo, usa maquiagem metálica e figurinos rígidos pra parecer um objeto inanimado em locais públicos. Quando o pedestre interage ou oferece uma gorjeta, o performer rompe a imobilidade com movimentos fluidos e robóticos, criando um contraste visual instantâneo. A parada exige meses de treinamento pra evitar o reflexo automático de piscar ou reagir a distrações externas, tipo sons e mudanças climáticas.\\n\\nA técnica de \\"locking\\", por exemplo, permite que o ator trave as articulações em ângulos determinados, mantendo uma postura estável e sem oscilações.",
|
| 606 |
-
"legenda": false
|
| 607 |
-
}},
|
| 608 |
-
{{
|
| 609 |
-
"title": "Os bastidores de Jumanji sendo mais engraçados que o próprio filme 😭",
|
| 610 |
-
"description": "Durante as filmagens de Jumanji: Bem-Vindo à Selva (2017) no Havaí, a produção precisou ser interrompida porque Jack Black se recusou a continuar gravando antes de terminar sua refeição. Kevin Hart registrou o momento em que o colega de elenco, ainda caracterizado como o professor Sheldon Oberon, ignora a pressão do cronograma para finalizar um prato de arroz.\\n\\nDwayne Johnson, o The Rock, aliás, aparece no vídeo sendo transportado por uma plataforma móvel enquanto Kevin Hart ironiza o \\"nível de Hollywood\\" do set. A química entre o quarteto principal foi fundamental para o sucesso do longa, que utilizou locações reais como a Reserva Kualoa para criar o ambiente imersivo do jogo.\\n\\nO filme arrecadou 962 milhões de dólares globalmente, tornando-se a maior bilheteria da Sony Pictures nos Estados Unidos até o lançamento de Homem-Aranha: Sem Volta para Casa.",
|
| 611 |
-
"legenda": true
|
| 612 |
-
}},
|
| 613 |
-
{{
|
| 614 |
-
"title": "O mano genuinamente se sentiu violado 😭",
|
| 615 |
-
"description": "O cara tava sob efeito de sedativos pesados após um procedimento cirúrgico quando esse registro foi feito numa unidade hospitalar. Ele apresenta aquele estado de desorientação típico do despertar anestésico, que afeta temporariamente as funções cognitivas e a percepção de realidade do paciente. Nas imagens, ele tenta vestir a própria camiseta enquanto interage com a equipe de enfermagem de forma confusa e cômica.\\n\\nA sedação consciente, técnica comum em procedimentos ambulatoriais, usa medicamentos que induzem ao relaxamento profundo e, frequentemente, causam amnésia retrógrada.",
|
| 616 |
-
"legenda": true
|
| 617 |
-
}}
|
| 618 |
-
]
|
| 619 |
-
|
| 620 |
-
INSTRUÇÕES FINAIS:
|
| 621 |
-
|
| 622 |
-
Mande apenas o JSON na resposta. Verifique se o JSON é válido. Responda em uma lista de objetos, mesmo que seja apenas um item.
|
| 623 |
-
|
| 624 |
-
NUNCA adicione perguntas, sugestões ou qualquer texto adicional após o JSON.
|
| 625 |
-
Se o contexto enviado pelo usuário não for verdadeiro ou estiver impreciso, ignore completamente. Gere uma legenda para o Instagram correta e factual, inspirada nos exemplos acima. NUNCA cite ou mencione a imprecisão do contexto original (ex: não escreva "Justin Bieber não teve o carro quebrado em 2018 como sugere a legenda do vídeo"). Simplesmente apresente a informação correta de forma natural.
|
| 626 |
-
"""
|
| 627 |
-
|
| 628 |
-
# 4. Enviar para o Gemini
|
| 629 |
-
model_name = request.model or "flash"
|
| 630 |
-
chatbot = chatbots.get(model_name, chatbots.get('flash', chatbots['default']))
|
| 631 |
-
|
| 632 |
-
print(f"🧠 [GenerateElements] Enviando para Gemini ({model_name})...")
|
| 633 |
-
|
| 634 |
-
response_gemini = await chatbot.ask(prompt, video=video_path_to_analyze)
|
| 635 |
-
|
| 636 |
-
if response_gemini.get("error"):
|
| 637 |
-
raise HTTPException(status_code=500, detail=f"Erro no Gemini: {response_gemini.get('content')}")
|
| 638 |
-
|
| 639 |
-
content = response_gemini.get("content", "")
|
| 640 |
-
print(f"✅ Resposta recebida ({len(content)} chars)")
|
| 641 |
-
|
| 642 |
-
# 5. Processar Resposta (JSON)
|
| 643 |
-
titles_data = extract_json_from_text(content)
|
| 644 |
-
|
| 645 |
-
if not titles_data:
|
| 646 |
-
print(f"⚠️ Falha ao extrair JSON. Conteúdo bruto: {content[:200]}...")
|
| 647 |
-
return JSONResponse(content={"raw_content": content, "error": "Failed to parse JSON"}, status_code=200)
|
| 648 |
-
|
| 649 |
-
# Garantir que seja uma lista
|
| 650 |
-
if isinstance(titles_data, dict):
|
| 651 |
-
titles_data = [titles_data]
|
| 652 |
-
|
| 653 |
-
return titles_data
|
| 654 |
-
|
| 655 |
-
except HTTPException:
|
| 656 |
-
raise
|
| 657 |
-
except Exception as e:
|
| 658 |
-
import traceback
|
| 659 |
-
traceback.print_exc()
|
| 660 |
-
raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
|
| 661 |
-
finally:
|
| 662 |
-
# Limpar arquivos temporários
|
| 663 |
-
if temp_file and os.path.exists(temp_file.name):
|
| 664 |
-
try:
|
| 665 |
-
os.unlink(temp_file.name)
|
| 666 |
-
except: pass
|
| 667 |
-
if cut_file and os.path.exists(cut_file.name):
|
| 668 |
-
try:
|
| 669 |
-
os.unlink(cut_file.name)
|
| 670 |
-
except: pass
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
# ==========================================
|
| 676 |
-
# GROQ ENDPOINT
|
| 677 |
-
# ==========================================
|
| 678 |
-
|
| 679 |
-
GROQ_API_KEY = "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"
|
| 680 |
-
|
| 681 |
-
GROQ_SUPPORTED_LANGUAGES = {
|
| 682 |
-
"af", "am", "ar", "as", "az", "ba", "be", "bg", "bn", "bo", "br", "bs", "ca", "cs", "cy", "da", "de", "el", "en", "es", "et", "eu", "fa", "fi", "fo", "fr", "gl", "gu", "ha", "haw", "he", "hi", "hr", "ht", "hu", "hy", "id", "is", "it", "ja", "jw", "ka", "kk", "km", "kn", "ko", "la", "lb", "ln", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", "ne", "nl", "nn", "no", "oc", "pa", "pl", "ps", "pt", "ro", "ru", "sa", "sd", "si", "sk", "sl", "sn", "so", "sq", "sr", "su", "sv", "sw", "ta", "te", "tg", "th", "tk", "tl", "tr", "tt", "uk", "ur", "uz", "vi", "yi", "yo", "zh", "yue"
|
| 683 |
-
}
|
| 684 |
-
|
| 685 |
-
class GroqRequest(BaseModel):
|
| 686 |
-
url: str
|
| 687 |
-
language: Optional[str] = None
|
| 688 |
-
temperature: Optional[float] = 0.4
|
| 689 |
-
has_bg_music: Optional[bool] = False # Default to False for speed/resources
|
| 690 |
-
time_start: Optional[float] = None
|
| 691 |
-
time_end: Optional[float] = None
|
| 692 |
-
|
| 693 |
-
def groq_json_to_srt(data):
|
| 694 |
-
"""Converte resposta verbose_json do Whisper/Groq para SRT usando segmentos (frases)."""
|
| 695 |
-
srt_output = ""
|
| 696 |
-
|
| 697 |
-
segments = data.get("segments") or []
|
| 698 |
-
for i, segment in enumerate(segments, 1):
|
| 699 |
-
start = seconds_to_srt_time(segment["start"])
|
| 700 |
-
end = seconds_to_srt_time(segment["end"])
|
| 701 |
-
text = segment["text"].strip()
|
| 702 |
-
srt_output += f"{i}\n{start} --> {end}\n{text}\n\n"
|
| 703 |
-
|
| 704 |
-
return srt_output
|
| 705 |
-
|
| 706 |
-
def groq_words_to_text(data):
|
| 707 |
-
"""Extrai timestamps word-level do Groq e formata como texto legível."""
|
| 708 |
-
words = data.get("words") or []
|
| 709 |
-
if not words:
|
| 710 |
-
return ""
|
| 711 |
-
|
| 712 |
-
lines = []
|
| 713 |
-
for w in words:
|
| 714 |
-
word_text = w.get("word", "").strip()
|
| 715 |
-
start = w.get("start", 0)
|
| 716 |
-
end = w.get("end", 0)
|
| 717 |
-
lines.append(f" [{start:.3f}s - {end:.3f}s] {word_text}")
|
| 718 |
-
|
| 719 |
-
return "\n".join(lines)
|
| 720 |
-
|
| 721 |
-
from srt_utils import apply_netflix_style_filter, process_audio_for_transcription, shift_srt_timestamps
|
| 722 |
-
|
| 723 |
-
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):
|
| 724 |
-
"""
|
| 725 |
-
Helper para gerar SRT base usando Groq (dando suporte a filtro Netflix).
|
| 726 |
-
Retorna (srt_filtered, srt_word_level, processed_audio_url)
|
| 727 |
-
Agora faz download e pré-processamento do áudio localmente para melhorar qualidade.
|
| 728 |
-
"""
|
| 729 |
-
if not url:
|
| 730 |
-
raise HTTPException(status_code=400, detail="URL é obrigatória para processamento Groq")
|
| 731 |
-
|
| 732 |
-
# 1. Baixar arquivo
|
| 733 |
-
print(f"⬇️ [Groq] Baixando arquivo para pré-processamento...")
|
| 734 |
-
try:
|
| 735 |
-
response = download_file_with_retry(url)
|
| 736 |
-
except Exception as e:
|
| 737 |
-
print(f"⚠️ Falha ao baixar arquivo para Groq: {e}")
|
| 738 |
-
raise HTTPException(status_code=400, detail=f"Falha ao baixar arquivo: {e}")
|
| 739 |
-
|
| 740 |
-
# Salvar temp
|
| 741 |
-
content_type = response.headers.get('content-type', '').lower()
|
| 742 |
-
ext = '.mp3' # Default fallback
|
| 743 |
-
if 'video' in content_type: ext = '.mp4'
|
| 744 |
-
elif 'audio' in content_type: ext = '.mp3'
|
| 745 |
-
|
| 746 |
-
# Usar arquivo estático para poder retornar URL
|
| 747 |
-
import uuid
|
| 748 |
-
filename = f"audio_{int(time.time())}_{uuid.uuid4().hex[:8]}{ext}"
|
| 749 |
-
filepath = os.path.join("static", filename)
|
| 750 |
-
|
| 751 |
-
with open(filepath, "wb") as f:
|
| 752 |
-
for chunk in response.iter_content(chunk_size=8192):
|
| 753 |
-
if chunk:
|
| 754 |
-
f.write(chunk)
|
| 755 |
-
|
| 756 |
-
processed_audio_url = None
|
| 757 |
-
processed_filename = None
|
| 758 |
-
|
| 759 |
-
try:
|
| 760 |
-
# 2. Pré-processar (Remover ruído, filtrar voz, etc)
|
| 761 |
-
groq_url = "https://api.groq.com/openai/v1/audio/transcriptions"
|
| 762 |
-
groq_headers = {
|
| 763 |
-
"Authorization": f"Bearer {GROQ_API_KEY}"
|
| 764 |
}
|
| 765 |
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
if processed_file_path != filepath:
|
| 770 |
-
pass
|
| 771 |
-
|
| 772 |
-
processed_filename = os.path.basename(processed_file_path)
|
| 773 |
-
processed_audio_url = f"/static/{processed_filename}"
|
| 774 |
-
|
| 775 |
-
# 3. Enviar áudio PROCESSADO para Groq (segments + word-level)
|
| 776 |
-
with open(processed_file_path, "rb") as f:
|
| 777 |
-
files = [
|
| 778 |
-
("model", (None, "whisper-large-v3")),
|
| 779 |
-
("file", ("audio.mp3", f, "audio/mpeg")),
|
| 780 |
-
("temperature", (None, str(temperature))),
|
| 781 |
-
("response_format", (None, "verbose_json")),
|
| 782 |
-
("timestamp_granularities[]", (None, "segment")),
|
| 783 |
-
("timestamp_granularities[]", (None, "word"))
|
| 784 |
-
]
|
| 785 |
-
|
| 786 |
-
if language and language in GROQ_SUPPORTED_LANGUAGES:
|
| 787 |
-
files.append(("language", (None, language)))
|
| 788 |
|
| 789 |
-
|
|
|
|
| 790 |
|
| 791 |
-
|
| 792 |
-
result = None
|
| 793 |
-
|
| 794 |
-
for attempt in range(max_retries):
|
| 795 |
-
try:
|
| 796 |
-
f.seek(0)
|
| 797 |
-
|
| 798 |
-
response_groq = requests.post(groq_url, headers=groq_headers, files=files, timeout=300)
|
| 799 |
-
|
| 800 |
-
if response_groq.status_code == 200:
|
| 801 |
-
result = response_groq.json()
|
| 802 |
-
break
|
| 803 |
-
|
| 804 |
-
error_msg = response_groq.text.lower()
|
| 805 |
-
is_deadline = "context deadline exceeded" in error_msg
|
| 806 |
-
is_server = response_groq.status_code >= 500
|
| 807 |
-
|
| 808 |
-
if (is_deadline or is_server) and attempt < max_retries - 1:
|
| 809 |
-
wait_time = 2 * (attempt + 1)
|
| 810 |
-
print(f"⚠️ Erro transiente Groq ({response_groq.status_code}). Retentando em {wait_time}s...")
|
| 811 |
-
await asyncio.sleep(wait_time)
|
| 812 |
-
continue
|
| 813 |
-
|
| 814 |
-
raise HTTPException(status_code=response_groq.status_code, detail=f"Erro Groq: {response_groq.text}")
|
| 815 |
-
|
| 816 |
-
except requests.RequestException as e:
|
| 817 |
-
if attempt < max_retries - 1:
|
| 818 |
-
print(f"⚠️ Erro conexão Groq. Retentando...")
|
| 819 |
-
await asyncio.sleep(2)
|
| 820 |
-
continue
|
| 821 |
-
raise HTTPException(status_code=500, detail=f"Erro conexão Groq: {e}")
|
| 822 |
-
|
| 823 |
-
finally:
|
| 824 |
-
# Cleanup do arquivo original
|
| 825 |
-
if filepath and os.path.exists(filepath) and filepath != processed_file_path:
|
| 826 |
-
try: os.unlink(filepath)
|
| 827 |
-
except: pass
|
| 828 |
-
|
| 829 |
-
# Converter para SRT
|
| 830 |
-
srt_base = groq_json_to_srt(result)
|
| 831 |
-
word_level_text = groq_words_to_text(result)
|
| 832 |
-
|
| 833 |
-
return srt_base, srt_base, processed_audio_url, word_level_text
|
| 834 |
-
|
| 835 |
-
@app.post("/subtitle/groq")
|
| 836 |
-
async def generate_subtitle_groq(request: GroqRequest):
|
| 837 |
-
"""
|
| 838 |
-
Endpoint para gerar legendas usando Groq API.
|
| 839 |
-
Agora envia a URL diretamente para a API do Groq e aplica filtro Netflix.
|
| 840 |
-
"""
|
| 841 |
-
try:
|
| 842 |
-
srt_filtered, srt_word, processed_audio_url, _word_level = await get_groq_srt_base(
|
| 843 |
-
url=request.url,
|
| 844 |
-
language=request.language,
|
| 845 |
-
temperature=request.temperature,
|
| 846 |
-
has_bg_music=request.has_bg_music,
|
| 847 |
-
time_start=request.time_start,
|
| 848 |
-
time_end=request.time_end
|
| 849 |
-
)
|
| 850 |
-
|
| 851 |
-
# Shift timestamps if needed
|
| 852 |
-
if request.time_start and request.time_start > 0:
|
| 853 |
-
srt_filtered = shift_srt_timestamps(srt_filtered, request.time_start)
|
| 854 |
-
srt_word = shift_srt_timestamps(srt_word, request.time_start)
|
| 855 |
-
|
| 856 |
-
return JSONResponse(content={
|
| 857 |
-
"srt": srt_filtered,
|
| 858 |
-
"srt_word": srt_word
|
| 859 |
-
})
|
| 860 |
-
|
| 861 |
-
except HTTPException:
|
| 862 |
-
raise
|
| 863 |
-
except Exception as e:
|
| 864 |
-
import traceback
|
| 865 |
-
traceback.print_exc()
|
| 866 |
-
raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
|
| 867 |
-
|
| 868 |
-
class GeminiSubtitleRequest(BaseModel):
|
| 869 |
-
url: str
|
| 870 |
-
has_bg_music: Optional[bool] = False
|
| 871 |
-
context: Optional[str] = "N/A"
|
| 872 |
-
model: Optional[str] = "flash" # 'flash' or 'thinking'
|
| 873 |
-
time_start: Optional[float] = None
|
| 874 |
-
time_end: Optional[float] = None
|
| 875 |
-
|
| 876 |
-
@app.post("/subtitle")
|
| 877 |
-
async def generate_subtitle(request: GeminiSubtitleRequest):
|
| 878 |
-
"""
|
| 879 |
-
Endpoint PRINCIPAL:
|
| 880 |
-
1. Baixa e Processa áudio (Demucs opcional + Filtros FFmpeg)
|
| 881 |
-
2. Gera SRT base via Groq (Whisper)
|
| 882 |
-
3. Envia Áudio Processado + SRT Base + Prompt para Gemini
|
| 883 |
-
4. Gemini analisa entonação/contexto e traduz/corrige.
|
| 884 |
-
"""
|
| 885 |
-
if not chatbots:
|
| 886 |
-
raise HTTPException(status_code=500, detail="Chatbot não inicializado")
|
| 887 |
-
|
| 888 |
-
try:
|
| 889 |
-
# 1. Obter SRT base + Caminho do áudio processado
|
| 890 |
-
print("🚀 Iniciando pipeline completo de legendagem Gemini...")
|
| 891 |
-
|
| 892 |
-
srt_filtered, srt_word, processed_audio_url, word_level_text = await get_groq_srt_base(
|
| 893 |
-
url=request.url,
|
| 894 |
-
language="en",
|
| 895 |
-
temperature=0.4,
|
| 896 |
-
has_bg_music=request.has_bg_music,
|
| 897 |
-
time_start=request.time_start,
|
| 898 |
-
time_end=request.time_end
|
| 899 |
-
)
|
| 900 |
-
|
| 901 |
-
# Converter URL /static/xyz.mp3 para path local
|
| 902 |
-
# processed_audio_url ex: "/static/audio_..."
|
| 903 |
-
# Converter URL /static/xyz.mp3 para path local
|
| 904 |
-
# processed_audio_url ex: "/static/audio_..."
|
| 905 |
-
filename = processed_audio_url.split("/")[-1]
|
| 906 |
-
|
| 907 |
-
# O arquivo pode estar em static/ (se não processado) ou static/processed/ (se processado)
|
| 908 |
-
processed_audio_path = os.path.join("static", filename)
|
| 909 |
-
|
| 910 |
-
if not os.path.exists(processed_audio_path):
|
| 911 |
-
# Tentar subpasta processed
|
| 912 |
-
processed_audio_path = os.path.join("static", "processed", filename)
|
| 913 |
-
|
| 914 |
-
if not os.path.exists(processed_audio_path):
|
| 915 |
-
raise HTTPException(status_code=500, detail=f"Arquivo de áudio processado não encontrado: {processed_audio_path}")
|
| 916 |
-
|
| 917 |
-
# 2. Selecionar Modelo Gemini
|
| 918 |
-
requested_model = request.model.lower()
|
| 919 |
-
chatbot_key = 'thinking' if 'thinking' in requested_model else 'flash'
|
| 920 |
-
chatbot = chatbots.get(chatbot_key, chatbots['default'])
|
| 921 |
-
|
| 922 |
-
print(f"🧠 [Gemini] Enviando SRT + Áudio para análise ({chatbot_key})...")
|
| 923 |
-
|
| 924 |
-
# 3. Montar Prompt
|
| 925 |
-
context_default = "Separe a legenda corretamente, nunca deixe muito texto em uma só legenda. Traduza corretamente e separe quem fala também, nunca bote 2 falantes numa mesma legenda. Se baseie no legenda por palavra pra se basear no timing."
|
| 926 |
-
processed_context = request.context if request.context and request.context.strip() not in ["", "N/A"] else context_default
|
| 927 |
-
|
| 928 |
-
prompt = f"""
|
| 929 |
-
IDIOMA: A legenda traduzida DEVE ser inteiramente em PORTUGUÊS DO BRASIL (pt-BR). Independente do idioma original do vídeo.
|
| 930 |
-
|
| 931 |
-
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.
|
| 932 |
-
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.
|
| 933 |
-
Mande o SRT completo, sem textos adicionais na resposta, apenas o SRT traduzido. Também analise o áudio anexado pra ver se algo foi legendado incorretamente ou errado, ou se algo não for legendado. Se não for, inclua, sem mudar o timestamp já existente. A legenda acima é uma base gerada pelo Whisper que precisa ser analisada e traduzida, não o resultado final.
|
| 934 |
-
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.
|
| 935 |
-
|
| 936 |
-
TIMING E TIMESTAMPS:
|
| 937 |
-
- Abaixo da legenda base (SRT), você receberá também os TIMESTAMPS POR PALAVRA (word-level) gerados pelo Whisper.
|
| 938 |
-
- Esses timestamps indicam o início e fim exato de cada palavra falada no áudio.
|
| 939 |
-
- USE esses timestamps para verificar se os blocos de legenda estão sincronizados corretamente.
|
| 940 |
-
- Se perceber que uma palavra está no bloco errado (começa depois do timestamp do bloco seguinte, por exemplo), MOVA-A para o bloco correto.
|
| 941 |
-
- Se precisar criar novos blocos ou ajustar timestamps, baseie-se nos timestamps word-level para garantir precisão.
|
| 942 |
-
- Os timestamps por palavra são a fonte de verdade para saber QUANDO cada palavra é falada.
|
| 943 |
-
|
| 944 |
-
MÚSICA E LETRAS:
|
| 945 |
-
- Se houver música/canto no vídeo, VOCÊ DEVE LEGENDAR A LETRA.
|
| 946 |
-
- Adicione o símbolo ♪ no início e no final de cada frase cantada. Ex: ♪ Hello, it's me ♪
|
| 947 |
-
- PESQUISE NA INTERNET a letra correta da música e sua tradução oficial/mais aceita para garantir que está correto. Tente identificar a música pelo áudio se não souber.
|
| 948 |
-
- Mantenha a sincronia com o áudio.
|
| 949 |
-
|
| 950 |
-
EXEMPLO:
|
| 951 |
-
|
| 952 |
-
(Original): 1
|
| 953 |
-
00:00:01,000 --> 00:00:04,000
|
| 954 |
-
hey what are you doing here i thought you left already
|
| 955 |
-
|
| 956 |
-
2
|
| 957 |
-
00:00:04,500 --> 00:00:07,200
|
| 958 |
-
yeah i was going to but then i realized i forgot my keys
|
| 959 |
-
|
| 960 |
-
3
|
| 961 |
-
00:00:07,900 --> 00:00:10,500
|
| 962 |
-
you always forget something man this is crazy
|
| 963 |
-
|
| 964 |
-
4
|
| 965 |
-
00:00:11,000 --> 00:00:14,000
|
| 966 |
-
relax it's not a big deal stop acting like that
|
| 967 |
-
|
| 968 |
-
5
|
| 969 |
-
00:00:14,500 --> 00:00:17,800
|
| 970 |
-
i am not acting you said you would be on time
|
| 971 |
-
|
| 972 |
-
6
|
| 973 |
-
00:00:18,000 --> 00:00:21,500
|
| 974 |
-
okay okay i'm sorry can we just go now
|
| 975 |
-
|
| 976 |
-
7
|
| 977 |
-
00:00:22,000 --> 00:00:25,000
|
| 978 |
-
fine but if we are late again it's on you
|
| 979 |
-
|
| 980 |
-
(Traduzido, como você deveria traduzir): 1
|
| 981 |
-
00:00:01,000 --> 00:00:04,000
|
| 982 |
-
Ué, o que você tá fazendo aqui? Não era pra você já ter ido embora?
|
| 983 |
-
|
| 984 |
-
2
|
| 985 |
-
00:00:04,500 --> 00:00:07,200
|
| 986 |
-
Eu ia, mas aí percebi que esqueci minhas chaves.
|
| 987 |
-
|
| 988 |
-
3
|
| 989 |
-
00:00:07,900 --> 00:00:10,500
|
| 990 |
-
Cara, você SEMPRE esquece alguma coisa, isso é surreal!
|
| 991 |
-
|
| 992 |
-
4
|
| 993 |
-
00:00:11,000 --> 00:00:14,000
|
| 994 |
-
Ah, relaxa! Não é o fim do mundo, para de drama.
|
| 995 |
-
|
| 996 |
-
5
|
| 997 |
-
00:00:14,500 --> 00:00:17,800
|
| 998 |
-
Não é drama! Você falou que ia chegar no horário!
|
| 999 |
-
|
| 1000 |
-
6
|
| 1001 |
-
00:00:18,000 --> 00:00:21,500
|
| 1002 |
-
Tá, tá... foi mal. Bora logo?
|
| 1003 |
-
|
| 1004 |
-
7
|
| 1005 |
-
00:00:22,000 --> 00:00:25,000
|
| 1006 |
-
Tá bom. Mas se a gente se atrasar de novo, a culpa é SUA!
|
| 1007 |
-
|
| 1008 |
-
INSTRUÇÕES/CONTEXTO DO USUÁRIO (OPCIONAL): {processed_context}
|
| 1009 |
-
|
| 1010 |
-
--- LEGENDA BASE (WHISPER) ---
|
| 1011 |
-
{srt_filtered}
|
| 1012 |
-
|
| 1013 |
-
--- TIMESTAMPS POR PALAVRA (WORD-LEVEL) ---
|
| 1014 |
-
{word_level_text}
|
| 1015 |
-
"""
|
| 1016 |
-
|
| 1017 |
-
# 4. Enviar para Gemini
|
| 1018 |
-
response = await chatbot.ask(prompt, audio=processed_audio_path)
|
| 1019 |
-
|
| 1020 |
-
content = response.get("content", "")
|
| 1021 |
-
if response.get("error"):
|
| 1022 |
-
raise HTTPException(status_code=500, detail=f"Erro no Gemini: {content}")
|
| 1023 |
-
|
| 1024 |
-
# Limpar markdown do SRT se houver
|
| 1025 |
-
cleaned_srt = clean_and_validate_srt(content)
|
| 1026 |
-
|
| 1027 |
-
# Shift final timestamps if needed
|
| 1028 |
-
if request.time_start and request.time_start > 0:
|
| 1029 |
-
cleaned_srt = shift_srt_timestamps(cleaned_srt, request.time_start)
|
| 1030 |
-
# original_srt was already shifted? No, srt_filtered comes from get_groq_srt_base which is 0-based
|
| 1031 |
-
# But wait, did we shift srt_filtered before sending to Gemini?
|
| 1032 |
-
# NO. srt_filtered is 0-based.
|
| 1033 |
-
# So send 0-based to Gemini. Gemini returns 0-based.
|
| 1034 |
-
# We shift cleaned_srt.
|
| 1035 |
-
# Optionally shift original_srt for reference
|
| 1036 |
-
srt_filtered = shift_srt_timestamps(srt_filtered, request.time_start)
|
| 1037 |
-
|
| 1038 |
-
return JSONResponse(content={
|
| 1039 |
-
"srt": cleaned_srt,
|
| 1040 |
-
"original_srt": srt_filtered,
|
| 1041 |
-
"srt_word_level": word_level_text,
|
| 1042 |
-
"used_audio_processed": True
|
| 1043 |
-
})
|
| 1044 |
-
|
| 1045 |
except Exception as e:
|
| 1046 |
-
import traceback
|
| 1047 |
-
traceback.print_exc()
|
| 1048 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
|
|
|
| 2 |
from pydantic import BaseModel
|
| 3 |
+
from gemini_webapi import GeminiClient
|
| 4 |
+
from gemini_webapi.constants import Model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
app = FastAPI(title="Gemini API Wrapper")
|
| 7 |
|
| 8 |
+
# Cookies extraídos do Request Header
|
| 9 |
+
Secure_1PSID = "g.a0007Qjc5GP_JJ8G6lqxKsBvwooBDG0kaQQpdrq1eVMavCuae6YHM71QR0oHtpOONkPxs87_PQACgYKAZcSARISFQHGX2MiBLscUC-RI65KuaeNsGHqgxoVAUF8yKrfh50pYTc-6ectdvp0W-we0076"
|
| 10 |
+
Secure_1PSIDTS = "sidts-CjEBBj1CYg2xMC8tsq0_lfxB86j60YwUfK4SqcUpqZa2YB6plmNmcG7NIPUU8YX38glqEAA"
|
| 11 |
|
| 12 |
+
client = None
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
@app.on_event("startup")
|
| 15 |
async def startup_event():
|
| 16 |
+
global client
|
| 17 |
+
print("Iniciando cliente do Gemini em plano de fundo...")
|
| 18 |
+
client = GeminiClient(Secure_1PSID, Secure_1PSIDTS, proxy=None)
|
| 19 |
+
# Mantém os cookies renovando automaticamente e a conexão viva
|
| 20 |
+
await client.init(timeout=30, auto_close=False, close_delay=300, auto_refresh=True)
|
| 21 |
+
print("Cliente inicializado com sucesso!")
|
| 22 |
+
|
| 23 |
+
class PromptRequest(BaseModel):
|
| 24 |
+
prompt: str
|
| 25 |
+
model: int = 0 # 0 por padrão (unspecified)
|
| 26 |
+
|
| 27 |
+
@app.post("/ask")
|
| 28 |
+
async def ask_gemini(request: PromptRequest):
|
| 29 |
+
if not client:
|
| 30 |
+
raise HTTPException(status_code=500, detail="Gemini client is not initialized yet.")
|
| 31 |
+
|
| 32 |
+
# 0 = Padrão
|
| 33 |
+
# 2 = 3.0 Pro
|
| 34 |
+
# 3 = 3.0 Flash
|
| 35 |
+
# 4 = 3.0 Flash Thinking
|
| 36 |
+
# 5 = 3.1 Pro
|
| 37 |
+
modelo_selecionado = "unspecified"
|
| 38 |
+
if request.model == 2:
|
| 39 |
+
modelo_selecionado = Model.G_3_0_PRO
|
| 40 |
+
elif request.model == 3:
|
| 41 |
+
modelo_selecionado = Model.G_3_0_FLASH
|
| 42 |
+
elif request.model == 4:
|
| 43 |
+
modelo_selecionado = Model.G_3_0_FLASH_THINKING
|
| 44 |
+
elif request.model == 5:
|
| 45 |
+
modelo_selecionado = "gemini-3.1-pro"
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
response = await client.generate_content(request.prompt, model=modelo_selecionado)
|
| 49 |
+
|
| 50 |
+
result = {
|
| 51 |
+
"text": response.text,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
+
# Opcional: Se houver pensamentos (Flash Thinking) ou imagens, retorna também
|
| 55 |
+
if hasattr(response, 'thoughts') and response.thoughts:
|
| 56 |
+
result["thoughts"] = response.thoughts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
if hasattr(response, 'images') and response.images:
|
| 59 |
+
result["images"] = [{"title": img.title, "url": img.url} for img in response.images]
|
| 60 |
|
| 61 |
+
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
except Exception as e:
|
|
|
|
|
|
|
| 63 |
raise HTTPException(status_code=500, detail=str(e))
|