habulaj commited on
Commit
1548d64
·
verified ·
1 Parent(s): 0ce9d36

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +49 -1034
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 tempfile
5
- from typing import Optional, Union
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
- from gemini_client import AsyncChatbot, Model, load_cookies
18
 
19
- from fastapi.staticfiles import StaticFiles
 
 
20
 
21
- app = FastAPI(title="Gemini Chat API", description="API para interagir com Google Gemini")
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
- await init_chatbot()
112
-
113
- @app.get("/")
114
- def root():
115
- """Endpoint raiz"""
116
- return {"status": "ok", "message": "Gemini Chat API está funcionando"}
117
-
118
- def srt_time_to_seconds(timestamp):
119
- """Converte timestamp SRT (HH:MM:SS,mmm) para segundos"""
120
- try:
121
- time_part, ms_part = timestamp.split(",")
122
- h, m, s = map(int, time_part.split(":"))
123
- ms = int(ms_part)
124
- return h * 3600 + m * 60 + s + ms / 1000.0
125
- except:
126
- return 0.0
127
-
128
- def seconds_to_srt_time(seconds):
129
- """Converte segundos para timestamp SRT (HH:MM:SS,mmm)"""
130
- hours = int(seconds // 3600)
131
- minutes = int((seconds % 3600) // 60)
132
- secs = int(seconds % 60)
133
- ms = int((seconds % 1) * 1000)
134
- return f"{hours:02d}:{minutes:02d}:{secs:02d},{ms:03d}"
135
-
136
- def cut_srt_by_time(srt_content, start_time, end_time):
137
- """
138
- Corta legendas SRT baseado em tempo de início e fim.
139
- Ajusta os timestamps para começar do zero.
140
-
141
- Parâmetros:
142
- - srt_content: Conteúdo SRT original
143
- - start_time: Tempo de início em segundos
144
- - end_time: Tempo de fim em segundos
145
-
146
- Retorna: SRT cortado e ajustado
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
- print(f"🔊 [Groq] Pré-processando áudio (has_bg_music={has_bg_music})...")
767
- processed_file_path = process_audio_for_transcription(filepath, has_bg_music=has_bg_music, time_start=time_start, time_end=time_end)
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
- print(f"🧠 [Groq] Enviando ÁUDIO PROCESSADO para API...")
 
790
 
791
- max_retries = 3
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))