Update gemini_client/enums.py
Browse files- gemini_client/enums.py +151 -906
gemini_client/enums.py
CHANGED
|
@@ -1,913 +1,158 @@
|
|
| 1 |
-
|
| 2 |
-
from
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
# Carregar cookies
|
| 48 |
-
secure_1psid, secure_1psidts, additional_cookies = load_cookies(cookie_path)
|
| 49 |
-
|
| 50 |
-
# Tentar atualizar cookie proativamente antes de inicializar
|
| 51 |
-
if retry_count == 0:
|
| 52 |
-
secure_1psidts = await update_cookie_if_needed(cookie_path, secure_1psid, secure_1psidts, additional_cookies)
|
| 53 |
-
|
| 54 |
-
# Criar Chatbot Flash (Padrão/Rápido)
|
| 55 |
-
chatbots['flash'] = await AsyncChatbot.create(
|
| 56 |
-
secure_1psid=secure_1psid,
|
| 57 |
-
secure_1psidts=secure_1psidts,
|
| 58 |
-
model=Model.G_3_0_FLASH,
|
| 59 |
-
additional_cookies=additional_cookies,
|
| 60 |
-
cookie_path=cookie_path
|
| 61 |
-
)
|
| 62 |
-
print(f"Chatbot Flash (3.0) inicializado com sucesso")
|
| 63 |
-
|
| 64 |
-
# Criar Chatbot Thinking (Raciocínio) - Timeout maior
|
| 65 |
-
chatbots['thinking'] = await AsyncChatbot.create(
|
| 66 |
-
secure_1psid=secure_1psid,
|
| 67 |
-
secure_1psidts=secure_1psidts,
|
| 68 |
-
model=Model.G_3_0_THINKING,
|
| 69 |
-
additional_cookies=additional_cookies,
|
| 70 |
-
cookie_path=cookie_path,
|
| 71 |
-
timeout=120 # Timeout maior para thinking
|
| 72 |
-
)
|
| 73 |
-
print(f"Chatbot Thinking (3.0) inicializado com sucesso")
|
| 74 |
-
|
| 75 |
-
# Fallback/Default
|
| 76 |
-
chatbots['default'] = chatbots['flash']
|
| 77 |
-
|
| 78 |
-
# Criar instância de Upscale separada
|
| 79 |
-
upscale_chatbot = await AsyncChatbot.create(
|
| 80 |
-
secure_1psid=secure_1psid,
|
| 81 |
-
secure_1psidts=secure_1psidts,
|
| 82 |
-
model=Model.NANO_BANANA,
|
| 83 |
-
additional_cookies=additional_cookies,
|
| 84 |
-
cookie_path=cookie_path
|
| 85 |
-
)
|
| 86 |
-
print(f"Upscale Chatbot inicializado com sucesso usando modelo NANO_BANANA")
|
| 87 |
-
|
| 88 |
-
except (ValueError, PermissionError) as e:
|
| 89 |
-
error_str = str(e).lower()
|
| 90 |
-
# Se o erro é relacionado a cookie expirado, não tentar atualizar novamente
|
| 91 |
-
# O sistema já tentou atualizar automaticamente e falhou
|
| 92 |
-
print(f"Erro ao inicializar chatbot: {e}")
|
| 93 |
-
print(f"AVISO: Cookies podem estar expirados. Por favor, atualize manualmente os cookies em {cookie_path}")
|
| 94 |
-
print(f"Para atualizar: acesse https://gemini.google.com/app e copie os novos cookies __Secure-1PSID e __Secure-1PSIDTS")
|
| 95 |
-
raise
|
| 96 |
-
except Exception as e:
|
| 97 |
-
print(f"Erro ao inicializar chatbot: {e}")
|
| 98 |
-
raise
|
| 99 |
-
|
| 100 |
-
# Inicializar na startup
|
| 101 |
-
@app.on_event("startup")
|
| 102 |
-
async def startup_event():
|
| 103 |
-
await init_chatbot()
|
| 104 |
-
|
| 105 |
-
@app.get("/")
|
| 106 |
-
def root():
|
| 107 |
-
"""Endpoint raiz"""
|
| 108 |
-
return {"status": "ok", "message": "Gemini Chat API está funcionando"}
|
| 109 |
-
|
| 110 |
-
def srt_time_to_seconds(timestamp):
|
| 111 |
-
"""Converte timestamp SRT (HH:MM:SS,mmm) para segundos"""
|
| 112 |
-
try:
|
| 113 |
-
time_part, ms_part = timestamp.split(",")
|
| 114 |
-
h, m, s = map(int, time_part.split(":"))
|
| 115 |
-
ms = int(ms_part)
|
| 116 |
-
return h * 3600 + m * 60 + s + ms / 1000.0
|
| 117 |
-
except:
|
| 118 |
-
return 0.0
|
| 119 |
-
|
| 120 |
-
def seconds_to_srt_time(seconds):
|
| 121 |
-
"""Converte segundos para timestamp SRT (HH:MM:SS,mmm)"""
|
| 122 |
-
hours = int(seconds // 3600)
|
| 123 |
-
minutes = int((seconds % 3600) // 60)
|
| 124 |
-
secs = int(seconds % 60)
|
| 125 |
-
ms = int((seconds % 1) * 1000)
|
| 126 |
-
return f"{hours:02d}:{minutes:02d}:{secs:02d},{ms:03d}"
|
| 127 |
-
|
| 128 |
-
def cut_srt_by_time(srt_content, start_time, end_time):
|
| 129 |
-
"""
|
| 130 |
-
Corta legendas SRT baseado em tempo de início e fim.
|
| 131 |
-
Ajusta os timestamps para começar do zero.
|
| 132 |
-
|
| 133 |
-
Parâmetros:
|
| 134 |
-
- srt_content: Conteúdo SRT original
|
| 135 |
-
- start_time: Tempo de início em segundos
|
| 136 |
-
- end_time: Tempo de fim em segundos
|
| 137 |
-
|
| 138 |
-
Retorna: SRT cortado e ajustado
|
| 139 |
-
"""
|
| 140 |
-
if start_time is None or end_time is None:
|
| 141 |
-
return srt_content
|
| 142 |
-
|
| 143 |
-
# Padrão para capturar legendas
|
| 144 |
-
pattern = re.compile(r"(\d+)\s*\n([^-\n]+?) --> ([^-\n]+?)\s*\n((?:(?!^\d+\s*\n).+\n?)*)", re.MULTILINE)
|
| 145 |
-
matches = pattern.findall(srt_content)
|
| 146 |
-
|
| 147 |
-
filtered_subtitles = []
|
| 148 |
-
for num, start, end, text in matches:
|
| 149 |
-
start_seconds = srt_time_to_seconds(start.strip())
|
| 150 |
-
end_seconds = srt_time_to_seconds(end.strip())
|
| 151 |
-
|
| 152 |
-
# Verificar se a legenda está dentro do intervalo [start_time, end_time]
|
| 153 |
-
# Incluir legendas que se sobrepõem parcialmente
|
| 154 |
-
if end_seconds > start_time and start_seconds < end_time:
|
| 155 |
-
# Ajustar timestamps para começar do zero
|
| 156 |
-
new_start = max(0, start_seconds - start_time)
|
| 157 |
-
new_end = min(end_time - start_time, end_seconds - start_time)
|
| 158 |
-
|
| 159 |
-
# Garantir que new_end > new_start
|
| 160 |
-
if new_end > new_start:
|
| 161 |
-
filtered_subtitles.append({
|
| 162 |
-
'start': new_start,
|
| 163 |
-
'end': new_end,
|
| 164 |
-
'text': text.strip()
|
| 165 |
-
})
|
| 166 |
-
|
| 167 |
-
# Gerar SRT cortado
|
| 168 |
-
srt_cut = ""
|
| 169 |
-
for i, sub in enumerate(filtered_subtitles, 1):
|
| 170 |
-
start_srt = seconds_to_srt_time(sub['start'])
|
| 171 |
-
end_srt = seconds_to_srt_time(sub['end'])
|
| 172 |
-
srt_cut += f"{i}\n{start_srt} --> {end_srt}\n{sub['text']}\n\n"
|
| 173 |
-
|
| 174 |
-
return srt_cut.strip()
|
| 175 |
-
|
| 176 |
-
def clean_and_validate_srt(srt_content):
|
| 177 |
-
"""Limpa e valida conteúdo SRT seguindo o padrão do example.py"""
|
| 178 |
-
if "```" in srt_content:
|
| 179 |
-
# Remover markdown code blocks
|
| 180 |
-
parts = srt_content.split("```")
|
| 181 |
-
if len(parts) > 1:
|
| 182 |
-
# Pegar o conteúdo dentro dos blocos de código
|
| 183 |
-
for part in parts:
|
| 184 |
-
if "srt" in part.lower() or not part.strip().startswith("srt"):
|
| 185 |
-
srt_content = part.strip()
|
| 186 |
-
break
|
| 187 |
-
|
| 188 |
-
# Padrão mais flexível para capturar timestamps mal formatados
|
| 189 |
-
pattern = re.compile(r"(\d+)\s*\n([^-\n]+?) --> ([^-\n]+?)\s*\n((?:(?!^\d+\s*\n).+\n?)*)", re.MULTILINE)
|
| 190 |
-
matches = pattern.findall(srt_content)
|
| 191 |
-
|
| 192 |
-
def corrigir_timestamp(timestamp):
|
| 193 |
-
timestamp = timestamp.strip()
|
| 194 |
-
|
| 195 |
-
# Se já está correto, retorna
|
| 196 |
-
if re.match(r"\d{2}:\d{2}:\d{2},\d{3}", timestamp):
|
| 197 |
-
return timestamp
|
| 198 |
-
|
| 199 |
-
# Formato: MM:SS,mmm -> HH:MM:SS,mmm
|
| 200 |
-
if re.match(r"\d{2}:\d{2},\d{3}", timestamp):
|
| 201 |
-
return f"00:{timestamp}"
|
| 202 |
-
|
| 203 |
-
# Formato: M:SS,mmm -> HH:MM:SS,mmm
|
| 204 |
-
if re.match(r"\d{1}:\d{2},\d{3}", timestamp):
|
| 205 |
-
parts = timestamp.split(":")
|
| 206 |
-
minutes = parts[0].zfill(2)
|
| 207 |
-
return f"00:{minutes}:{parts[1]}"
|
| 208 |
-
|
| 209 |
-
# Formato: SS,mmm -> HH:MM:SS,mmm
|
| 210 |
-
if re.match(r"\d{1,2},\d{3}", timestamp):
|
| 211 |
-
seconds_ms = timestamp.split(",")
|
| 212 |
-
seconds = seconds_ms[0].zfill(2)
|
| 213 |
-
return f"00:00:{seconds},{seconds_ms[1]}"
|
| 214 |
-
|
| 215 |
-
# Outros formatos problemáticos
|
| 216 |
-
if re.match(r"\d{2}:\d{2}:\d{3}", timestamp):
|
| 217 |
-
parts = timestamp.split(":")
|
| 218 |
-
if len(parts) == 3:
|
| 219 |
-
h, m, s_ms = parts
|
| 220 |
-
if len(s_ms) == 3:
|
| 221 |
-
return f"{h}:{m}:00,{s_ms}"
|
| 222 |
-
elif len(s_ms) >= 4:
|
| 223 |
-
s = s_ms[:-3]
|
| 224 |
-
ms = s_ms[-3:]
|
| 225 |
-
return f"{h}:{m}:{s.zfill(2)},{ms}"
|
| 226 |
-
|
| 227 |
-
return timestamp
|
| 228 |
-
|
| 229 |
-
srt_corrigido = ""
|
| 230 |
-
for i, (num, start, end, text) in enumerate(matches, 1):
|
| 231 |
-
text = text.strip()
|
| 232 |
-
if not text:
|
| 233 |
-
continue
|
| 234 |
-
|
| 235 |
-
# Verificar se a legenda tem mais de 2 linhas
|
| 236 |
-
text_lines = [line.strip() for line in text.split('\n') if line.strip()]
|
| 237 |
-
if len(text_lines) > 2:
|
| 238 |
-
# Limitar a 2 linhas, juntando as extras na segunda linha
|
| 239 |
-
text = text_lines[0] + '\n' + ' '.join(text_lines[1:])
|
| 240 |
-
|
| 241 |
-
start_corrigido = corrigir_timestamp(start)
|
| 242 |
-
end_corrigido = corrigir_timestamp(end)
|
| 243 |
-
srt_corrigido += f"{i}\n{start_corrigido} --> {end_corrigido}\n{text}\n\n"
|
| 244 |
-
|
| 245 |
-
return srt_corrigido.strip()
|
| 246 |
-
|
| 247 |
-
def download_file_with_retry(url: str, max_retries: int = 3, timeout: int = 300):
|
| 248 |
-
"""
|
| 249 |
-
Baixa arquivo com retry logic e tratamento de rate limiting.
|
| 250 |
-
|
| 251 |
-
Parâmetros:
|
| 252 |
-
- url: URL do arquivo
|
| 253 |
-
- max_retries: Número máximo de tentativas
|
| 254 |
-
- timeout: Timeout em segundos
|
| 255 |
-
|
| 256 |
-
Retorna: Response object do requests
|
| 257 |
-
"""
|
| 258 |
-
headers = {
|
| 259 |
-
'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',
|
| 260 |
-
'Accept': '*/*',
|
| 261 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
| 262 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
| 263 |
-
'Connection': 'keep-alive',
|
| 264 |
-
'Upgrade-Insecure-Requests': '1'
|
| 265 |
}
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
response.raise_for_status()
|
| 298 |
-
return response
|
| 299 |
-
|
| 300 |
-
except requests.exceptions.HTTPError as e:
|
| 301 |
-
if e.response.status_code == 429 and attempt < max_retries - 1:
|
| 302 |
-
continue
|
| 303 |
-
elif attempt == max_retries - 1:
|
| 304 |
-
raise HTTPException(
|
| 305 |
-
status_code=400,
|
| 306 |
-
detail=f"Erro ao baixar arquivo após {max_retries} tentativas: {str(e)}"
|
| 307 |
-
)
|
| 308 |
-
else:
|
| 309 |
-
raise
|
| 310 |
-
except requests.exceptions.RequestException as e:
|
| 311 |
-
if attempt == max_retries - 1:
|
| 312 |
-
raise HTTPException(
|
| 313 |
-
status_code=400,
|
| 314 |
-
detail=f"Erro ao baixar arquivo após {max_retries} tentativas: {str(e)}"
|
| 315 |
-
)
|
| 316 |
-
continue
|
| 317 |
-
|
| 318 |
-
raise HTTPException(
|
| 319 |
-
status_code=400,
|
| 320 |
-
detail=f"Falha ao baixar arquivo após {max_retries} tentativas"
|
| 321 |
)
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
"""
|
| 333 |
-
if not chatbots:
|
| 334 |
-
raise HTTPException(status_code=500, detail="Chatbot não inicializado")
|
| 335 |
-
|
| 336 |
-
try:
|
| 337 |
-
requested_model = request.model.lower() if request.model else "flash"
|
| 338 |
-
if "thinking" in requested_model:
|
| 339 |
-
selected_chatbot = chatbots.get('thinking', chatbots['default'])
|
| 340 |
-
else:
|
| 341 |
-
selected_chatbot = chatbots.get('flash', chatbots['default'])
|
| 342 |
-
|
| 343 |
-
prompt = request.message
|
| 344 |
-
if request.context:
|
| 345 |
-
prompt = f"Contexto: {request.context}\n\nMensagem: {request.message}"
|
| 346 |
-
|
| 347 |
-
print(f"💬 Chat request ({requested_model}): {prompt[:50]}...")
|
| 348 |
-
response_gemini = await selected_chatbot.ask(prompt)
|
| 349 |
-
|
| 350 |
-
if response_gemini.get("error"):
|
| 351 |
-
raise HTTPException(
|
| 352 |
-
status_code=500,
|
| 353 |
-
detail=f"Erro no Gemini: {response_gemini.get('content', 'Erro desconhecido')}"
|
| 354 |
-
)
|
| 355 |
-
|
| 356 |
-
return {"response": response_gemini.get("content", "")}
|
| 357 |
-
|
| 358 |
-
except Exception as e:
|
| 359 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
@app.get("/subtitle")
|
| 363 |
-
async def generate_subtitle(
|
| 364 |
-
file: str,
|
| 365 |
-
context: Optional[str] = None,
|
| 366 |
-
start: Optional[float] = None,
|
| 367 |
-
end: Optional[float] = None,
|
| 368 |
-
model: Optional[str] = "flash" # 'flash' or 'thinking'
|
| 369 |
-
):
|
| 370 |
-
"""
|
| 371 |
-
Endpoint para gerar legendas SRT a partir de um arquivo (imagem, vídeo ou áudio).
|
| 372 |
-
|
| 373 |
-
Parâmetros:
|
| 374 |
-
- file: URL do arquivo (imagem, vídeo ou áudio)
|
| 375 |
-
- context: Contexto adicional opcional para a geração de legendas
|
| 376 |
-
- start: Tempo de início para cortar legendas (em segundos)
|
| 377 |
-
- end: Tempo de fim para cortar legendas (em segundos)
|
| 378 |
-
|
| 379 |
-
Retorna:
|
| 380 |
-
- Arquivo SRT formatado (cortado se start e end forem fornecidos)
|
| 381 |
-
"""
|
| 382 |
-
if not chatbots:
|
| 383 |
-
raise HTTPException(status_code=500, detail="Chatbot não inicializado")
|
| 384 |
-
|
| 385 |
-
requested_model = model.lower() if model else "flash"
|
| 386 |
-
if "thinking" in requested_model:
|
| 387 |
-
selected_chatbot = chatbots.get('thinking', chatbots['default'])
|
| 388 |
-
else:
|
| 389 |
-
selected_chatbot = chatbots.get('flash', chatbots['default'])
|
| 390 |
-
|
| 391 |
-
if not file:
|
| 392 |
-
raise HTTPException(status_code=400, detail="Parâmetro 'file' é obrigatório")
|
| 393 |
-
|
| 394 |
-
temp_file = None
|
| 395 |
-
try:
|
| 396 |
-
# Baixar arquivo da URL com retry
|
| 397 |
-
response = download_file_with_retry(file, max_retries=3, timeout=300)
|
| 398 |
-
|
| 399 |
-
# Determinar tipo de mídia e extensão
|
| 400 |
-
content_type = response.headers.get('content-type', '').lower()
|
| 401 |
-
file_extension = None
|
| 402 |
-
|
| 403 |
-
if 'video' in content_type:
|
| 404 |
-
file_extension = '.mp4'
|
| 405 |
-
media_type = 'video'
|
| 406 |
-
elif 'audio' in content_type:
|
| 407 |
-
file_extension = '.mp3'
|
| 408 |
-
media_type = 'audio'
|
| 409 |
-
elif 'image' in content_type:
|
| 410 |
-
# Determinar extensão da imagem
|
| 411 |
-
if 'jpeg' in content_type or 'jpg' in content_type:
|
| 412 |
-
file_extension = '.jpg'
|
| 413 |
-
elif 'png' in content_type:
|
| 414 |
-
file_extension = '.png'
|
| 415 |
-
elif 'gif' in content_type:
|
| 416 |
-
file_extension = '.gif'
|
| 417 |
-
elif 'webp' in content_type:
|
| 418 |
-
file_extension = '.webp'
|
| 419 |
-
else:
|
| 420 |
-
file_extension = '.jpg'
|
| 421 |
-
media_type = 'image'
|
| 422 |
-
else:
|
| 423 |
-
# Tentar inferir do URL
|
| 424 |
-
url_lower = file.lower()
|
| 425 |
-
if any(ext in url_lower for ext in ['.mp4', '.avi', '.mov', '.webm', '.mkv']):
|
| 426 |
-
file_extension = '.mp4'
|
| 427 |
-
media_type = 'video'
|
| 428 |
-
elif any(ext in url_lower for ext in ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a']):
|
| 429 |
-
file_extension = '.mp3'
|
| 430 |
-
media_type = 'audio'
|
| 431 |
-
elif any(ext in url_lower for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
|
| 432 |
-
file_extension = Path(file).suffix or '.jpg'
|
| 433 |
-
media_type = 'image'
|
| 434 |
-
else:
|
| 435 |
-
raise HTTPException(status_code=400, detail="Tipo de arquivo não suportado. Use imagem, vídeo ou áudio.")
|
| 436 |
-
|
| 437 |
-
# Salvar arquivo temporariamente
|
| 438 |
-
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=file_extension)
|
| 439 |
-
for chunk in response.iter_content(chunk_size=8192):
|
| 440 |
-
if chunk:
|
| 441 |
-
temp_file.write(chunk)
|
| 442 |
-
temp_file.close()
|
| 443 |
-
|
| 444 |
-
print(f"✅ Arquivo baixado: {temp_file.name} (tipo: {media_type})")
|
| 445 |
-
|
| 446 |
-
# Preparar prompt (mesmo do example.py)
|
| 447 |
-
context_text = context.strip() if context else "N/A"
|
| 448 |
-
media_desc = 'áudio' if media_type in ['video', 'audio'] else 'conteúdo'
|
| 449 |
-
media_desc_final = 'VÍDEO' if media_type in ['video', 'audio'] else 'CONTEÚDO'
|
| 450 |
-
|
| 451 |
-
prompt = f"Gere uma legenda em formato SRT para este {media_desc} seguindo RIGOROSAMENTE todas as especificações do sistema.\n\nContexto adicional: {context_text}"
|
| 452 |
-
|
| 453 |
-
# System instruction (mesmo do example.py)
|
| 454 |
-
system_instruction = f"""FORMATO TÉCNICO OBRIGATÓRIO
|
| 455 |
-
|
| 456 |
-
Estrutura de cada bloco:
|
| 457 |
-
|
| 458 |
-
[número sequencial]
|
| 459 |
-
HH:MM:SS,mmm --> HH:MM:SS,mmm
|
| 460 |
-
[texto da legenda]
|
| 461 |
-
[linha em branco]
|
| 462 |
-
|
| 463 |
-
CRÍTICO - Formato de tempo:
|
| 464 |
-
- SEMPRE usar: HH:MM:SS,mmm (exemplo: 00:01:23,456)
|
| 465 |
-
- Vírgula (,) separando segundos de milissegundos
|
| 466 |
-
- Duas casas para horas, minutos e segundos
|
| 467 |
-
- Três casas para milissegundos
|
| 468 |
-
- Nunca omitir as horas, mesmo que sejam 00
|
| 469 |
-
|
| 470 |
-
PADRÃO NETFLIX - REGRAS DE TEXTO
|
| 471 |
-
|
| 472 |
-
Limitações de caracteres:
|
| 473 |
-
- Máximo 2 linhas por legenda
|
| 474 |
-
- Máximo 42 caracteres por linha (incluindo espaços e pontuação)
|
| 475 |
-
- Quebras de linha devem respeitar unidades semânticas (não partir palavras ou expressões)
|
| 476 |
-
|
| 477 |
-
Separação de falas:
|
| 478 |
-
- NUNCA misture falas de pessoas diferentes na mesma legenda
|
| 479 |
-
- Se houver mudança de locutor, SEMPRE crie um novo bloco numerado
|
| 480 |
-
- Única exceção: diálogos rápidos marcados com hífen (veja abaixo)
|
| 481 |
-
|
| 482 |
-
Uso de hífen (-):
|
| 483 |
-
Use APENAS para:
|
| 484 |
-
1. Diálogos alternados quando o timing impede separação:
|
| 485 |
-
|
| 486 |
-
- Vamos?
|
| 487 |
-
- Vamos!
|
| 488 |
-
|
| 489 |
-
2. Interrupções abruptas de fala
|
| 490 |
-
3. Falas sobrepostas simultâneas
|
| 491 |
-
|
| 492 |
-
NÃO use hífen para:
|
| 493 |
-
- Fala única de uma pessoa
|
| 494 |
-
- Marcação desnecessária de locutor
|
| 495 |
-
|
| 496 |
-
NATURALIDADE E EMOÇÃO
|
| 497 |
-
|
| 498 |
-
Idioma:
|
| 499 |
-
- Português brasileiro natural
|
| 500 |
-
- Adaptar gírias, expressões regionais e modo de falar brasileiro
|
| 501 |
-
- Evitar traduções literais ou formais demais
|
| 502 |
-
|
| 503 |
-
Expressão emocional:
|
| 504 |
-
- Gritos, ênfase forte: LETRAS MAIÚSCULAS
|
| 505 |
-
- Hesitação, pausa: reticências (...)
|
| 506 |
-
- Surpresa, exclamação: ponto de exclamação (!)
|
| 507 |
-
- Interrogação: ponto de interrogação (?)
|
| 508 |
-
- Nunca deixe frases importantes sem pontuação
|
| 509 |
-
- Exemplos:
|
| 510 |
-
- "João" → "João..." (hesitante)
|
| 511 |
-
- "João" → "João!" (chamando com urgência)
|
| 512 |
-
- "João" → "JOÃO!" (gritando)
|
| 513 |
-
|
| 514 |
-
SINCRONIA TEMPORAL
|
| 515 |
-
|
| 516 |
-
- Precisão de milissegundos
|
| 517 |
-
- Início da legenda: EXATAMENTE quando a fala começa
|
| 518 |
-
- Fim da legenda: quando a fala termina (mínimo 1 segundo de exibição)
|
| 519 |
-
- Respeitar pausas naturais entre falas
|
| 520 |
-
|
| 521 |
-
EXEMPLO DE FORMATAÇÃO PERFEITA
|
| 522 |
-
|
| 523 |
-
1
|
| 524 |
-
00:00:01,200 --> 00:00:04,000
|
| 525 |
-
Oi, tudo bem?
|
| 526 |
-
|
| 527 |
-
2
|
| 528 |
-
00:00:04,500 --> 00:00:06,800
|
| 529 |
-
Tudo ótimo, e você?
|
| 530 |
-
|
| 531 |
-
3
|
| 532 |
-
00:00:07,100 --> 00:00:09,500
|
| 533 |
-
- Quer almoçar comigo?
|
| 534 |
-
- Claro!
|
| 535 |
-
|
| 536 |
-
4
|
| 537 |
-
00:00:10,000 --> 00:00:12,300
|
| 538 |
-
QUE LEGAL!
|
| 539 |
-
|
| 540 |
-
5
|
| 541 |
-
00:00:12,800 --> 00:00:15,100
|
| 542 |
-
Não acredito que você aceitou...
|
| 543 |
-
|
| 544 |
-
INSTRUÇÕES FINAIS
|
| 545 |
-
|
| 546 |
-
- Retorne APENAS o arquivo SRT formatado
|
| 547 |
-
- Sem explicações, comentários ou textos adicionais
|
| 548 |
-
- Sem marcadores de código (```), apenas o conteúdo puro
|
| 549 |
-
- Numere sequencialmente a partir de 1
|
| 550 |
-
- Linha em branco entre cada bloco de legenda
|
| 551 |
-
|
| 552 |
-
TRADUZA TUDO DE IMPORTANTE NO {media_desc_final}, que tenha dialogo... Nunca deixe passar nada."""
|
| 553 |
-
|
| 554 |
-
# Adicionar system instruction ao prompt
|
| 555 |
-
full_prompt = f"{system_instruction}\n\n{prompt}"
|
| 556 |
-
|
| 557 |
-
# Enviar para o Gemini
|
| 558 |
-
print(f"🧠 Enviando {media_type} para o Gemini...")
|
| 559 |
-
|
| 560 |
-
# Determinar qual parâmetro usar baseado no tipo de mídia
|
| 561 |
-
if media_type == 'image':
|
| 562 |
-
response_gemini = await selected_chatbot.ask(full_prompt, image=temp_file.name)
|
| 563 |
-
elif media_type == 'video':
|
| 564 |
-
response_gemini = await selected_chatbot.ask(full_prompt, video=temp_file.name)
|
| 565 |
-
else: # audio
|
| 566 |
-
response_gemini = await selected_chatbot.ask(full_prompt, audio=temp_file.name)
|
| 567 |
-
|
| 568 |
-
if response_gemini.get("error"):
|
| 569 |
-
raise HTTPException(
|
| 570 |
-
status_code=500,
|
| 571 |
-
detail=f"Erro ao gerar legendas: {response_gemini.get('content', 'Erro desconhecido')}"
|
| 572 |
-
)
|
| 573 |
-
|
| 574 |
-
# Extrair conteúdo SRT da resposta
|
| 575 |
-
raw_srt = response_gemini.get("content", "").strip()
|
| 576 |
-
|
| 577 |
-
if not raw_srt or len(raw_srt) < 10:
|
| 578 |
-
raise HTTPException(
|
| 579 |
-
status_code=500,
|
| 580 |
-
detail="Nenhuma legenda foi gerada - arquivo pode estar vazio ou inaudível"
|
| 581 |
-
)
|
| 582 |
-
|
| 583 |
-
# Limpar e validar SRT
|
| 584 |
-
print("📝 Processando formato SRT...")
|
| 585 |
-
srt_cleaned = clean_and_validate_srt(raw_srt)
|
| 586 |
-
|
| 587 |
-
if not srt_cleaned or len(srt_cleaned.strip()) < 10:
|
| 588 |
-
raise HTTPException(
|
| 589 |
-
status_code=500,
|
| 590 |
-
detail="Falha ao processar formato SRT - resposta inválida"
|
| 591 |
-
)
|
| 592 |
-
|
| 593 |
-
# Aplicar corte de legendas se start e end forem fornecidos
|
| 594 |
-
if start is not None and end is not None:
|
| 595 |
-
print(f"✂️ Cortando legendas: {start}s - {end}s")
|
| 596 |
-
srt_cleaned = cut_srt_by_time(srt_cleaned, start, end)
|
| 597 |
-
if not srt_cleaned or len(srt_cleaned.strip()) < 10:
|
| 598 |
-
raise HTTPException(
|
| 599 |
-
status_code=500,
|
| 600 |
-
detail="Nenhuma legenda encontrada no intervalo especificado"
|
| 601 |
-
)
|
| 602 |
-
|
| 603 |
-
# Retornar SRT em JSON
|
| 604 |
-
return JSONResponse(
|
| 605 |
-
content={
|
| 606 |
-
"srt": srt_cleaned,
|
| 607 |
-
"success": True,
|
| 608 |
-
"media_type": media_type
|
| 609 |
-
}
|
| 610 |
-
)
|
| 611 |
-
|
| 612 |
-
except HTTPException:
|
| 613 |
-
raise
|
| 614 |
-
except requests.RequestException as e:
|
| 615 |
-
raise HTTPException(status_code=400, detail=f"Erro ao baixar arquivo: {str(e)}")
|
| 616 |
-
except Exception as e:
|
| 617 |
-
raise HTTPException(status_code=500, detail=f"Erro ao gerar legendas: {str(e)}")
|
| 618 |
-
finally:
|
| 619 |
-
# Limpar arquivo temporário
|
| 620 |
-
if temp_file and os.path.exists(temp_file.name):
|
| 621 |
-
try:
|
| 622 |
-
os.unlink(temp_file.name)
|
| 623 |
-
except:
|
| 624 |
-
pass
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
def flip_image_both_axes(image_path: str) -> str:
|
| 628 |
-
"""
|
| 629 |
-
Inverte uma imagem horizontalmente e verticalmente.
|
| 630 |
-
Usa compressão JPEG com qualidade 90 e subsampling 4:2:0 para alterar
|
| 631 |
-
o fingerprint da imagem e evitar detecção de figuras públicas.
|
| 632 |
-
Retorna o caminho para um novo arquivo temporário com a imagem invertida.
|
| 633 |
-
"""
|
| 634 |
-
with Image.open(image_path) as img:
|
| 635 |
-
# Inverter horizontalmente e verticalmente
|
| 636 |
-
flipped = img.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.FLIP_TOP_BOTTOM)
|
| 637 |
-
|
| 638 |
-
# Determinar extensão e formato
|
| 639 |
-
suffix = Path(image_path).suffix.lower()
|
| 640 |
-
if suffix in ['.jpg', '.jpeg']:
|
| 641 |
-
out_suffix = '.jpg'
|
| 642 |
-
else:
|
| 643 |
-
out_suffix = suffix
|
| 644 |
-
|
| 645 |
-
# Salvar em arquivo temporário
|
| 646 |
-
temp_flipped = tempfile.NamedTemporaryFile(delete=False, suffix=out_suffix)
|
| 647 |
-
temp_flipped.close()
|
| 648 |
-
|
| 649 |
-
# Salvar com parâmetros específicos para alterar fingerprint
|
| 650 |
-
if out_suffix in ['.jpg', '.jpeg']:
|
| 651 |
-
# Usar qualidade 90 e subsampling 4:2:0 (como o Figma faz)
|
| 652 |
-
# Isso altera os artefatos de compressão JPEG
|
| 653 |
-
flipped.save(
|
| 654 |
-
temp_flipped.name,
|
| 655 |
-
format='JPEG',
|
| 656 |
-
quality=90,
|
| 657 |
-
subsampling='4:2:0', # Chroma subsampling diferente
|
| 658 |
-
optimize=True
|
| 659 |
-
)
|
| 660 |
-
else:
|
| 661 |
-
flipped.save(temp_flipped.name)
|
| 662 |
-
|
| 663 |
-
return temp_flipped.name
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
def flip_base64_image_both_axes(img_base64: str, content_type: str) -> str:
|
| 667 |
-
"""
|
| 668 |
-
Inverte uma imagem base64 horizontalmente e verticalmente.
|
| 669 |
-
Usa compressão JPEG com qualidade 90 para alterar o fingerprint.
|
| 670 |
-
Retorna o base64 da imagem invertida.
|
| 671 |
-
"""
|
| 672 |
-
# Decodificar base64 para bytes
|
| 673 |
-
img_bytes = base64.b64decode(img_base64)
|
| 674 |
-
|
| 675 |
-
# Abrir imagem dos bytes
|
| 676 |
-
with Image.open(io.BytesIO(img_bytes)) as img:
|
| 677 |
-
# Inverter horizontalmente e verticalmente
|
| 678 |
-
flipped = img.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.FLIP_TOP_BOTTOM)
|
| 679 |
-
|
| 680 |
-
# Salvar em buffer
|
| 681 |
-
buffer = io.BytesIO()
|
| 682 |
-
|
| 683 |
-
# Determinar formato baseado no content_type
|
| 684 |
-
if 'jpeg' in content_type or 'jpg' in content_type:
|
| 685 |
-
# Usar qualidade 90 e subsampling 4:2:0 para alterar fingerprint
|
| 686 |
-
flipped.save(
|
| 687 |
-
buffer,
|
| 688 |
-
format='JPEG',
|
| 689 |
-
quality=90,
|
| 690 |
-
subsampling='4:2:0',
|
| 691 |
-
optimize=True
|
| 692 |
-
)
|
| 693 |
-
elif 'webp' in content_type:
|
| 694 |
-
flipped.save(buffer, format='WEBP', quality=90)
|
| 695 |
-
else:
|
| 696 |
-
flipped.save(buffer, format='PNG')
|
| 697 |
-
|
| 698 |
-
buffer.seek(0)
|
| 699 |
-
|
| 700 |
-
# Converter de volta para base64
|
| 701 |
-
return base64.b64encode(buffer.read()).decode('utf-8')
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
async def _try_upscale(chatbot, image_path: str, prompt: str):
|
| 705 |
-
"""
|
| 706 |
-
Tenta fazer upscale de uma imagem.
|
| 707 |
-
Retorna (result, upscaled_url) ou (None, None) em caso de erro.
|
| 708 |
-
"""
|
| 709 |
-
result = await chatbot.ask(prompt, image=image_path)
|
| 710 |
-
|
| 711 |
-
if result.get("error"):
|
| 712 |
-
return None, None
|
| 713 |
-
|
| 714 |
-
upscaled_url = None
|
| 715 |
-
if result.get("images"):
|
| 716 |
-
# Preferir imagens geradas
|
| 717 |
-
for img in result["images"]:
|
| 718 |
-
if "[Generated Image" in img.get("title", ""):
|
| 719 |
-
upscaled_url = img["url"]
|
| 720 |
-
break
|
| 721 |
-
|
| 722 |
-
# Se não achou 'Generated Image', pega a última
|
| 723 |
-
if not upscaled_url and len(result["images"]) > 0:
|
| 724 |
-
upscaled_url = result["images"][-1]["url"]
|
| 725 |
-
|
| 726 |
-
return result, upscaled_url
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
async def _download_upscaled_image(upscaled_url: str, cookies_dict: dict) -> tuple:
|
| 730 |
-
"""
|
| 731 |
-
Baixa a imagem upscaled e retorna (img_base64, content_type, download_url).
|
| 732 |
-
"""
|
| 733 |
-
print(f"📥 Resolvendo redirect da URL...")
|
| 734 |
-
|
| 735 |
-
# Primeira requisição: seguir redirect para obter URL final COM cookies
|
| 736 |
-
redirect_response = requests.get(upscaled_url, timeout=60, allow_redirects=True,
|
| 737 |
-
cookies=cookies_dict,
|
| 738 |
-
headers={
|
| 739 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 740 |
-
'Referer': 'https://gemini.google.com/',
|
| 741 |
-
'Accept': 'image/*,*/*'
|
| 742 |
-
}
|
| 743 |
)
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
)
|
| 768 |
-
img_response.raise_for_status()
|
| 769 |
-
|
| 770 |
-
# Converter para base64
|
| 771 |
-
img_base64 = base64.b64encode(img_response.content).decode('utf-8')
|
| 772 |
-
content_type = img_response.headers.get('content-type', 'image/png')
|
| 773 |
-
|
| 774 |
-
print(f"✅ Imagem baixada e convertida para base64 ({len(img_base64)} chars)")
|
| 775 |
-
|
| 776 |
-
return img_base64, content_type, download_url
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
@app.get("/upscale")
|
| 780 |
-
async def upscale_image(
|
| 781 |
-
file: str
|
| 782 |
-
):
|
| 783 |
-
"""
|
| 784 |
-
Endpoint para fazer upscale 4x de uma imagem usando o Nano Banana Pro.
|
| 785 |
-
|
| 786 |
-
Se a primeira tentativa falhar (ex: erro de figuras públicas),
|
| 787 |
-
tenta novamente invertendo a imagem horizontalmente e verticalmente,
|
| 788 |
-
e depois inverte o resultado de volta.
|
| 789 |
-
|
| 790 |
-
Parâmetros:
|
| 791 |
-
- file: URL da imagem
|
| 792 |
-
|
| 793 |
-
Retorna:
|
| 794 |
-
- JSON com a imagem gerada em base64 (upscaled)
|
| 795 |
-
"""
|
| 796 |
-
if upscale_chatbot is None:
|
| 797 |
-
raise HTTPException(status_code=500, detail="Upscale Chatbot não inicializado")
|
| 798 |
-
|
| 799 |
-
if not file:
|
| 800 |
-
raise HTTPException(status_code=400, detail="Parâmetro 'file' é obrigatório")
|
| 801 |
-
|
| 802 |
-
temp_file = None
|
| 803 |
-
temp_flipped_file = None
|
| 804 |
-
|
| 805 |
-
try:
|
| 806 |
-
# Baixar arquivo da URL com retry
|
| 807 |
-
response = download_file_with_retry(file, max_retries=3, timeout=300)
|
| 808 |
-
|
| 809 |
-
# Verificar se é uma imagem
|
| 810 |
-
content_type = response.headers.get('content-type', '').lower()
|
| 811 |
-
if 'image' not in content_type and not any(ext in file.lower() for ext in ['.jpg', '.jpeg', '.png', '.webp']):
|
| 812 |
-
raise HTTPException(status_code=400, detail="O arquivo fornecido não parece ser uma imagem.")
|
| 813 |
|
| 814 |
-
file_extension = '.jpg' # default
|
| 815 |
-
if 'png' in content_type: file_extension = '.png'
|
| 816 |
-
elif 'webp' in content_type: file_extension = '.webp'
|
| 817 |
-
|
| 818 |
-
# Salvar arquivo temporariamente
|
| 819 |
-
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=file_extension)
|
| 820 |
-
for chunk in response.iter_content(chunk_size=8192):
|
| 821 |
-
if chunk:
|
| 822 |
-
temp_file.write(chunk)
|
| 823 |
-
temp_file.close()
|
| 824 |
-
|
| 825 |
-
print(f"✅ Arquivo para upscale baixado: {temp_file.name}")
|
| 826 |
-
|
| 827 |
-
prompt = "Upscale this image by 4x. Keep everything exactly as it is, including text, colors, texture, aspect ratio, zoom and proportions. Just increase the quality and sharpness. Do not add any new elements or modify the composition"
|
| 828 |
-
|
| 829 |
-
# Carregar cookies para download
|
| 830 |
-
cookies_dict = {}
|
| 831 |
-
cookie_path = os.getenv("COOKIE_PATH", "cookies.json")
|
| 832 |
-
try:
|
| 833 |
-
secure_1psid, secure_1psidts, additional_cookies = load_cookies(cookie_path)
|
| 834 |
-
cookies_dict = additional_cookies.copy()
|
| 835 |
-
cookies_dict['__Secure-1PSID'] = secure_1psid
|
| 836 |
-
cookies_dict['__Secure-1PSIDTS'] = secure_1psidts
|
| 837 |
-
print(f"✅ {len(cookies_dict)} cookies carregados para download")
|
| 838 |
-
except Exception as cookie_err:
|
| 839 |
-
print(f"⚠️ Aviso: Não foi possível carregar cookies: {cookie_err}")
|
| 840 |
-
|
| 841 |
-
# ========== PRIMEIRA TENTATIVA: Normal ==========
|
| 842 |
-
print(f"🧠 [Tentativa 1] Enviando imagem para upscale...")
|
| 843 |
-
result, upscaled_url = await _try_upscale(upscale_chatbot, temp_file.name, prompt)
|
| 844 |
-
|
| 845 |
-
used_flip_workaround = False
|
| 846 |
-
|
| 847 |
-
if result is None or upscaled_url is None:
|
| 848 |
-
# ========== SEGUNDA TENTATIVA: Inverter imagem ==========
|
| 849 |
-
print(f"⚠️ Primeira tentativa falhou. Tentando workaround de inversão...")
|
| 850 |
-
|
| 851 |
-
# Inverter a imagem horizontalmente e verticalmente
|
| 852 |
-
print(f"🔄 Invertendo imagem (horizontal + vertical)...")
|
| 853 |
-
temp_flipped_file = flip_image_both_axes(temp_file.name)
|
| 854 |
-
print(f"✅ Imagem invertida salva em: {temp_flipped_file}")
|
| 855 |
-
|
| 856 |
-
# Tentar novamente com a imagem invertida
|
| 857 |
-
print(f"🧠 [Tentativa 2] Enviando imagem INVERTIDA para upscale...")
|
| 858 |
-
result, upscaled_url = await _try_upscale(upscale_chatbot, temp_flipped_file, prompt)
|
| 859 |
-
|
| 860 |
-
if result is None or upscaled_url is None:
|
| 861 |
-
raise HTTPException(
|
| 862 |
-
status_code=500,
|
| 863 |
-
detail="Nenhuma imagem de upscale foi retornada pelo modelo após múltiplas tentativas."
|
| 864 |
-
)
|
| 865 |
-
|
| 866 |
-
used_flip_workaround = True
|
| 867 |
-
print(f"✅ Segunda tentativa (com inversão) funcionou!")
|
| 868 |
-
|
| 869 |
-
# Baixar imagem upscaled
|
| 870 |
-
try:
|
| 871 |
-
img_base64, img_content_type, download_url = await _download_upscaled_image(upscaled_url, cookies_dict)
|
| 872 |
-
except Exception as download_error:
|
| 873 |
-
print(f"❌ Erro ao baixar imagem: {download_error}")
|
| 874 |
-
raise HTTPException(
|
| 875 |
-
status_code=500,
|
| 876 |
-
detail=f"Erro ao baixar imagem upscaled: {str(download_error)}"
|
| 877 |
-
)
|
| 878 |
-
|
| 879 |
-
# Se usamos o workaround de inversão, precisamos inverter o resultado de volta
|
| 880 |
-
if used_flip_workaround:
|
| 881 |
-
print(f"🔄 Invertendo resultado de volta (restaurando orientação original)...")
|
| 882 |
-
img_base64 = flip_base64_image_both_axes(img_base64, img_content_type)
|
| 883 |
-
print(f"✅ Imagem restaurada para orientação original")
|
| 884 |
-
|
| 885 |
-
return JSONResponse(
|
| 886 |
-
content={
|
| 887 |
-
"image_base64": img_base64,
|
| 888 |
-
"content_type": img_content_type,
|
| 889 |
-
"success": True,
|
| 890 |
-
"original_url": file,
|
| 891 |
-
"upscaled_url": download_url,
|
| 892 |
-
"used_flip_workaround": used_flip_workaround
|
| 893 |
-
}
|
| 894 |
-
)
|
| 895 |
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
from enum import Enum
|
| 3 |
+
|
| 4 |
+
class Endpoint(Enum):
|
| 5 |
+
"""
|
| 6 |
+
Enum for Google Gemini API endpoints.
|
| 7 |
+
|
| 8 |
+
Attributes:
|
| 9 |
+
INIT (str): URL for initializing the Gemini session.
|
| 10 |
+
GENERATE (str): URL for generating chat responses.
|
| 11 |
+
ROTATE_COOKIES (str): URL for rotating authentication cookies.
|
| 12 |
+
UPLOAD (str): URL for uploading files/images.
|
| 13 |
+
"""
|
| 14 |
+
INIT = "https://gemini.google.com/app"
|
| 15 |
+
GENERATE = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
|
| 16 |
+
ROTATE_COOKIES = "https://accounts.google.com/RotateCookies"
|
| 17 |
+
UPLOAD = "https://content-push.googleapis.com/upload"
|
| 18 |
+
|
| 19 |
+
class Headers(Enum):
|
| 20 |
+
"""
|
| 21 |
+
Enum for HTTP headers used in Gemini API requests.
|
| 22 |
+
|
| 23 |
+
Attributes:
|
| 24 |
+
GEMINI (dict): Headers for Gemini chat requests.
|
| 25 |
+
ROTATE_COOKIES (dict): Headers for rotating cookies.
|
| 26 |
+
UPLOAD (dict): Headers for file/image upload.
|
| 27 |
+
"""
|
| 28 |
+
GEMINI = {
|
| 29 |
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
| 30 |
+
"Host": "gemini.google.com",
|
| 31 |
+
"Origin": "https://gemini.google.com",
|
| 32 |
+
"Referer": "https://gemini.google.com/",
|
| 33 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0",
|
| 34 |
+
"Accept": "*/*",
|
| 35 |
+
"Accept-Language": "pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
|
| 36 |
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
| 37 |
+
"X-Same-Domain": "1",
|
| 38 |
+
"Alt-Used": "gemini.google.com",
|
| 39 |
+
"Connection": "keep-alive",
|
| 40 |
+
"Sec-Fetch-Dest": "empty",
|
| 41 |
+
"Sec-Fetch-Mode": "cors",
|
| 42 |
+
"Sec-Fetch-Site": "same-origin",
|
| 43 |
+
"TE": "trailers",
|
| 44 |
+
}
|
| 45 |
+
ROTATE_COOKIES = {
|
| 46 |
+
"Content-Type": "application/json",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
+
UPLOAD = {"Push-ID": "feeds/mcudyrk2a4khkz"}
|
| 49 |
+
|
| 50 |
+
class Model(Enum):
|
| 51 |
+
"""
|
| 52 |
+
Enum for available Gemini model configurations.
|
| 53 |
+
|
| 54 |
+
Attributes:
|
| 55 |
+
model_name (str): Name of the model.
|
| 56 |
+
model_header (dict): Additional headers required for the model.
|
| 57 |
+
advanced_only (bool): Whether the model is available only for advanced users.
|
| 58 |
+
"""
|
| 59 |
+
# Updated model definitions based on reference implementation
|
| 60 |
+
UNSPECIFIED = ("unspecified", {}, False)
|
| 61 |
+
# Gemini 3 Models (using headers provided by user)
|
| 62 |
+
G_3_0_FLASH = (
|
| 63 |
+
"gemini-3.0-flash",
|
| 64 |
+
{
|
| 65 |
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]',
|
| 66 |
+
"x-goog-ext-73010989-jspb": '[0]',
|
| 67 |
+
"x-goog-ext-525005358-jspb": '["F6E300D3-4DBF-4D05-AC9C-78D136AE9E57",1]',
|
| 68 |
+
},
|
| 69 |
+
False,
|
| 70 |
+
)
|
| 71 |
+
G_3_0_THINKING = (
|
| 72 |
+
"gemini-3.0-thinking",
|
| 73 |
+
{
|
| 74 |
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"5bf011840784117a",null,null,0,[4],null,null,1]',
|
| 75 |
+
"x-goog-ext-73010989-jspb": '[0]',
|
| 76 |
+
"x-goog-ext-525005358-jspb": '["C6F85698-B8D9-4901-B43A-E3990ACF38B7",1]',
|
| 77 |
+
},
|
| 78 |
+
False,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
)
|
| 80 |
|
| 81 |
+
# Legacy / Aliases (using same confirmed headers for robust fallback)
|
| 82 |
+
G_2_0_FLASH = (
|
| 83 |
+
"gemini-2.0-flash",
|
| 84 |
+
{
|
| 85 |
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]',
|
| 86 |
+
"x-goog-ext-73010989-jspb": '[0]',
|
| 87 |
+
"x-goog-ext-525005358-jspb": '["F6E300D3-4DBF-4D05-AC9C-78D136AE9E57",1]',
|
| 88 |
+
},
|
| 89 |
+
False,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
)
|
| 91 |
+
G_2_0_FLASH_THINKING = (
|
| 92 |
+
"gemini-2.0-flash-thinking",
|
| 93 |
+
{
|
| 94 |
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"5bf011840784117a",null,null,0,[4],null,null,1]',
|
| 95 |
+
"x-goog-ext-73010989-jspb": '[0]',
|
| 96 |
+
"x-goog-ext-525005358-jspb": '["C6F85698-B8D9-4901-B43A-E3990ACF38B7",1]',
|
| 97 |
+
},
|
| 98 |
+
False,
|
| 99 |
+
)
|
| 100 |
+
# Updating 2.5 aliases to also use the working headers to avoid confusion
|
| 101 |
+
G_2_5_FLASH = (
|
| 102 |
+
"gemini-2.5-flash",
|
| 103 |
+
{
|
| 104 |
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]',
|
| 105 |
+
"x-goog-ext-73010989-jspb": '[0]',
|
| 106 |
+
"x-goog-ext-525005358-jspb": '["F6E300D3-4DBF-4D05-AC9C-78D136AE9E57",1]',
|
| 107 |
+
},
|
| 108 |
+
False,
|
| 109 |
+
)
|
| 110 |
+
G_2_5_PRO = (
|
| 111 |
+
"gemini-2.5-pro",
|
| 112 |
+
{"x-goog-ext-525001261-jspb": '[1,null,null,null,"2525e3954d185b3c"]'}, # Keeping original PRO headers as we don't have new ones for Pro specifically, but user uses Flash/Thinking usually.
|
| 113 |
+
False,
|
| 114 |
+
)
|
| 115 |
+
NANO_BANANA = (
|
| 116 |
+
"gemini-nano",
|
| 117 |
+
{
|
| 118 |
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]',
|
| 119 |
+
"x-goog-ext-73010989-jspb": '[0]',
|
| 120 |
+
"x-goog-ext-525005358-jspb": '["F6E300D3-4DBF-4D05-AC9C-78D136AE9E57",1]',
|
| 121 |
+
},
|
| 122 |
+
False
|
| 123 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
def __init__(self, name, header, advanced_only):
|
| 127 |
+
"""
|
| 128 |
+
Initialize a Model enum member.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
name (str): Model name.
|
| 132 |
+
header (dict): Model-specific headers.
|
| 133 |
+
advanced_only (bool): If True, model is for advanced users only.
|
| 134 |
+
"""
|
| 135 |
+
self.model_name = name
|
| 136 |
+
self.model_header = header
|
| 137 |
+
self.advanced_only = advanced_only
|
| 138 |
+
|
| 139 |
+
@classmethod
|
| 140 |
+
def from_name(cls, name: str):
|
| 141 |
+
"""
|
| 142 |
+
Get a Model enum member by its model name.
|
| 143 |
+
|
| 144 |
+
Args:
|
| 145 |
+
name (str): Name of the model.
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
Model: Corresponding Model enum member.
|
| 149 |
+
|
| 150 |
+
Raises:
|
| 151 |
+
ValueError: If the model name is not found.
|
| 152 |
+
"""
|
| 153 |
+
for model in cls:
|
| 154 |
+
if model.model_name == name:
|
| 155 |
+
return model
|
| 156 |
+
raise ValueError(
|
| 157 |
+
f"Unknown model name: {name}. Available models: {', '.join([model.model_name for model in cls])}"
|
| 158 |
+
)
|