habulaj commited on
Commit
ede7084
·
verified ·
1 Parent(s): 767e3da

Update gemini_client/enums.py

Browse files
Files changed (1) hide show
  1. gemini_client/enums.py +151 -906
gemini_client/enums.py CHANGED
@@ -1,913 +1,158 @@
1
- from fastapi import FastAPI, HTTPException
2
- from fastapi.responses import JSONResponse
3
- from pydantic import BaseModel
4
- from typing import Optional
5
- import os
6
- import tempfile
7
- from pathlib import Path
8
- import re
9
- import requests
10
- import time
11
- import base64
12
- import io
13
- from PIL import Image
14
-
15
- from gemini_client import AsyncChatbot, Model, load_cookies
16
-
17
- app = FastAPI(title="Gemini Chat API", description="API para interagir com Google Gemini")
18
-
19
- # Inicializar chatbot globalmente
20
-
21
- # Inicializar chatbots globalmente
22
- chatbots = {}
23
- upscale_chatbot = None
24
-
25
- async def update_cookie_if_needed(cookie_path: str, secure_1psid: str, secure_1psidts: str, additional_cookies: dict):
26
- """
27
- Tenta atualizar o cookie __Secure-1PSIDTS se necessário.
28
- Retorna o novo cookie ou o original se não precisar atualizar.
29
- """
30
- # Não tentar atualizar proativamente - deixar o sistema fazer isso quando necessário
31
- # Isso evita erros 401/404 quando o cookie já expirou
32
- return secure_1psidts
33
-
34
- async def init_chatbot(retry_count=0, max_retries=2):
35
- """
36
- Inicializa o chatbot com os cookies de forma assíncrona.
37
- Tenta atualizar cookies automaticamente se falhar.
38
- """
39
-
40
- global chatbots, upscale_chatbot
41
- cookie_path = os.getenv("COOKIE_PATH", "cookies.json")
42
-
43
- if not os.path.exists(cookie_path):
44
- raise FileNotFoundError(f"Arquivo de cookies não encontrado: {cookie_path}")
45
-
46
- try:
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
- for attempt in range(max_retries):
268
- try:
269
- if attempt > 0:
270
- # Backoff exponencial: 2^attempt segundos
271
- wait_time = 2 ** attempt
272
- print(f"⏳ Aguardando {wait_time}s antes de tentar novamente (tentativa {attempt + 1}/{max_retries})...")
273
- time.sleep(wait_time)
274
-
275
- print(f"📥 Tentativa {attempt + 1}/{max_retries} - Baixando arquivo de: {url}")
276
- response = requests.get(url, headers=headers, timeout=timeout, stream=True)
277
-
278
- # Tratar erro 429 (Too Many Requests)
279
- if response.status_code == 429:
280
- retry_after = response.headers.get('Retry-After')
281
- if retry_after:
282
- wait_time = int(retry_after)
283
- print(f"⚠️ Rate limit atingido. Aguardando {wait_time}s conforme Retry-After header...")
284
- time.sleep(wait_time)
285
- elif attempt < max_retries - 1:
286
- # Se não houver Retry-After, usar backoff exponencial
287
- wait_time = (2 ** attempt) * 5 # 5s, 10s, 20s...
288
- print(f"⚠️ Rate limit atingido. Aguardando {wait_time}s antes de tentar novamente...")
289
- time.sleep(wait_time)
290
- continue
291
- else:
292
- raise HTTPException(
293
- status_code=429,
294
- detail=f"Rate limit atingido após {max_retries} tentativas. Tente novamente mais tarde."
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
- class ChatRequest(BaseModel):
324
- message: str
325
- context: Optional[str] = None
326
- model: Optional[str] = "flash" # 'flash' or 'thinking'
327
-
328
- @app.post("/chat")
329
- async def chat_endpoint(request: ChatRequest):
330
- """
331
- Endpoint para conversas de texto simples.
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
- # A URL final após redirect
746
- final_url = redirect_response.url
747
- print(f"📍 URL após redirect: {final_url[:80]}...")
748
-
749
- # Remover parâmetros existentes e adicionar =s0-d-I para full resolution
750
- if "?" in final_url:
751
- final_url = final_url.split("?")[0]
752
- # Remover qualquer =sXXX existente
753
- if "=s" in final_url:
754
- final_url = final_url.rsplit("=s", 1)[0]
755
-
756
- download_url = final_url + "=s0-d-I"
757
- print(f"📥 Baixando imagem full res de: {download_url[:80]}...")
758
-
759
- # Segunda requisição: baixar a imagem em full resolution COM cookies
760
- img_response = requests.get(download_url, timeout=60,
761
- cookies=cookies_dict,
762
- headers={
763
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
764
- 'Referer': 'https://gemini.google.com/',
765
- 'Accept': 'image/*,*/*'
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
- except HTTPException:
897
- raise
898
- except Exception as e:
899
- import traceback
900
- traceback.print_exc()
901
- raise HTTPException(status_code=500, detail=f"Erro interno no upscale: {str(e)}")
902
- finally:
903
- # Limpar arquivos temporários
904
- if temp_file and os.path.exists(temp_file.name):
905
- try:
906
- os.unlink(temp_file.name)
907
- except:
908
- pass
909
- if temp_flipped_file and os.path.exists(temp_flipped_file):
910
- try:
911
- os.unlink(temp_flipped_file)
912
- except:
913
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ )