habulaj commited on
Commit
81945e1
·
verified ·
1 Parent(s): ea86889

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -328
app.py CHANGED
@@ -700,6 +700,129 @@ Deve ter parametro start e end, e cortar o vídeo enviado nesse tempo. Pra anali
700
  except: pass
701
 
702
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
 
704
 
705
  def flip_image_both_axes(image_path: str) -> str:
@@ -1289,7 +1412,7 @@ INSTRUÇÕES/CONTEXTO DO USUÁRIO: {processed_context}
1289
  # But wait, did we shift srt_filtered before sending to Gemini?
1290
  # NO. srt_filtered is 0-based.
1291
  # So send 0-based to Gemini. Gemini returns 0-based.
1292
- # We shift cleaned_srt.v
1293
  # Optionally shift original_srt for reference
1294
  srt_filtered = shift_srt_timestamps(srt_filtered, request.time_start)
1295
 
@@ -1302,330 +1425,4 @@ INSTRUÇÕES/CONTEXTO DO USUÁRIO: {processed_context}
1302
  except Exception as e:
1303
  import traceback
1304
  traceback.print_exc()
1305
- raise HTTPException(status_code=500, detail=str(e))
1306
-
1307
-
1308
- class GenerateElementsRequest(BaseModel):
1309
- video_url: str
1310
- context: Optional[str] = None
1311
- start: Optional[str] = None
1312
- end: Optional[str] = None
1313
- model: Optional[str] = "flash"
1314
-
1315
-
1316
- @app.post("/generate-elements")
1317
- async def generate_elements_endpoint(request: GenerateElementsRequest):
1318
- """
1319
- Gera elementos estruturados (título, nomes, metadados, descrição) para linha do tempo de vídeo.
1320
- """
1321
- if not chatbots:
1322
- raise HTTPException(status_code=500, detail="Chatbot não inicializado")
1323
-
1324
- temp_file = None
1325
- cut_file = None
1326
-
1327
- try:
1328
- # 1. Validar e Baixar Vídeo
1329
- if not request.video_url:
1330
- raise HTTPException(status_code=400, detail="URL do vídeo é obrigatória")
1331
-
1332
- print(f"📥 [GenerateElements] Baixando vídeo: {request.video_url}")
1333
-
1334
- response = download_file_with_retry(request.video_url, timeout=600)
1335
-
1336
- content_type = response.headers.get('content-type', '').lower()
1337
- ext = '.mp4'
1338
- if 'webm' in content_type: ext = '.webm'
1339
- elif 'mkv' in content_type: ext = '.mkv'
1340
-
1341
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
1342
- for chunk in response.iter_content(chunk_size=1024*1024):
1343
- if chunk:
1344
- temp_file.write(chunk)
1345
- temp_file.close()
1346
-
1347
- video_path_to_analyze = temp_file.name
1348
-
1349
- # 2. Cortar Vídeo
1350
- if request.start and request.end:
1351
- print(f"✂️ [GenerateElements] Cortando vídeo de {request.start} até {request.end}...")
1352
- cut_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
1353
- cut_file.close()
1354
-
1355
- success = cut_video(temp_file.name, cut_file.name, request.start, request.end)
1356
- if success:
1357
- video_path_to_analyze = cut_file.name
1358
- print(f"✅ Vídeo cortado: {video_path_to_analyze}")
1359
- else:
1360
- print("⚠️ Falha ao cortar vídeo, usando original.")
1361
-
1362
- # 3. Preparar Prompt
1363
- contexto_add = f"\\n{request.context}" if request.context else ""
1364
-
1365
- prompt = f"""Analise essa notícia e o vídeo:
1366
-
1367
- {contexto_add}
1368
-
1369
- Analise uma notícia e seu contexto ampliado na internet, extraindo dados factuais, contexto editorial e informações complementares que ajudem a compor a narrativa jornalística. A partir dessa análise, gere exclusivamente um JSON estruturado com elementos pensados para inserção em uma linha do tempo de vídeo.
1370
-
1371
- Retorne apenas JSON válido, sem comentários, explicações ou formatação adicional.
1372
- RETORNE APENAS O JSON VÁLIDO. NÃO INCLUA NENHUM TEXTO ANTES OU DEPOIS DO JSON. NÃO USE TAGS MARKDOWN ```json. APENAS O JSON RAW.
1373
- O JSON deve ser uma lista contendo uma lista ordenada de elementos, respeitando rigorosamente a ordem de exibição no vídeo.
1374
-
1375
- Cada elemento deve conter obrigatoriamente:
1376
- - type: tipo do elemento
1377
- - start: tempo de início no vídeo (MM:SS)
1378
- - end: tempo de término no vídeo (MM:SS)
1379
-
1380
- Tipos de elementos permitidos:
1381
-
1382
- 1) title
1383
- - type: "title"
1384
- - text: título contextualizado da notícia, traduzido para português do Brasil
1385
- - Duração máxima: 5 segundos
1386
-
1387
- 2) name
1388
- - type: "name"
1389
- - name: nome da pessoa
1390
- - profession: profissão ou cargo, traduzido para português do Brasil
1391
- - Duração máxima: 5 segundos
1392
-
1393
- 3) metadata
1394
- - type: "metadata"
1395
- - Pode conter um ou mais dos seguintes campos:
1396
- - date: data no formato ISO (YYYY-MM-DD)
1397
- - location: local traduzido para português do Brasil
1398
- - credits: créditos da imagem ou vídeo, se disponíveis
1399
- - Deve começar exatamente no mesmo start do elemento imediatamente abaixo
1400
- - Duração máxima: 5 segundos
1401
-
1402
- 4) description
1403
- - type: "description"
1404
- - text: texto descritivo que explica fatos, contexto ou desdobramentos da notícia
1405
- - A duração deve ser calculada com base no tamanho do texto
1406
- - Ritmo recomendado de leitura: entre 12 e 15 caracteres por segundo
1407
- - Evitar tempos longos para textos curtos
1408
-
1409
- Regras de tempo:
1410
- - Os tempos devem ser sequenciais e coerentes
1411
- - title, name e metadata nunca devem ultrapassar 5 segundos
1412
- - description deve ter duração proporcional ao tamanho do texto
1413
- - Metadata sempre deve começar exatamente junto com o elemento logo abaixo
1414
-
1415
- Contexto editorial:
1416
- - Considere informações relevantes disponíveis em fontes confiáveis
1417
- - Inclua contexto quando isso enriquecer a compreensão da notícia
1418
- - Priorize clareza, concisão e linguagem jornalística, no estilo The New York Times.
1419
- - Todo o conteúdo textual deve estar em português do Brasil
1420
- - Considere que o jornal é imparcial na apresentação dos fatos, mas possui uma inclinação editorial claramente mais conservadora, republicana e de direita na entonação. Adapte os títulos e descrições de acordo com esse posicionamento, mantendo rigor factual e linguagem jornalística.
1421
-
1422
- # EXEMPLO ESPERADO 1:
1423
-
1424
- [
1425
- {{
1426
- "type": "title",
1427
- "start": "00:00",
1428
- "end": "00:05",
1429
- "text": "A mãe de Savannah Guthrie, apresentadora do programa 'Today', foi levada 'contra a própria vontade', segundo as autoridades."
1430
- }},
1431
- {{
1432
- "type": "name",
1433
- "start": "00:05",
1434
- "end": "00:10",
1435
- "name": "Chris Nanos",
1436
- "profession": "Xerife do Condado de Pima"
1437
- }},
1438
- {{
1439
- "type": "metadata",
1440
- "start": "00:10",
1441
- "end": "00:15",
1442
- "date": "2023-03-15",
1443
- "credits": "Nathan Congleton/NBC, via Getty Images"
1444
- }},
1445
- {{
1446
- "type": "description",
1447
- "start": "00:10",
1448
- "end": "00:22",
1449
- "text": "As autoridades informaram que o desaparecimento de Nancy Guthrie, de 84 anos, estava sendo tratado como um sequestro."
1450
- }},
1451
- {{
1452
- "type": "description",
1453
- "start": "00:22",
1454
- "end": "00:28",
1455
- "text": "Segundo o xerife, Guthrie foi vista pela última vez em sua casa, em Tucson, no sábado."
1456
- }},
1457
- {{
1458
- "type": "metadata",
1459
- "start": "00:28",
1460
- "end": "00:33",
1461
- "location": "Sydney, Austrália",
1462
- "date": "2015-05-04"
1463
- }},
1464
- {{
1465
- "type": "description",
1466
- "start": "00:28",
1467
- "end": "00:40",
1468
- "text": "O xerife afirmou que ela tinha limitações físicas, mas que o caso não estava relacionado à demência."
1469
- }},
1470
- {{
1471
- "type": "metadata",
1472
- "start": "00:40",
1473
- "end": "00:45",
1474
- "location": "Tucson, Arizona",
1475
- "date": "2026-02-02"
1476
- }}
1477
- ]
1478
-
1479
- # EXEMPLO ESPERADO 2
1480
-
1481
- [
1482
- {{
1483
- "type": "metadata",
1484
- "start": "00:00",
1485
- "end": "00:05",
1486
- "location": "Paramount, Califórnia",
1487
- "date": "2025-07-05"
1488
- }},
1489
- {{
1490
- "type": "title",
1491
- "start": "00:00",
1492
- "end": "00:05",
1493
- "text": "Este vídeo mostra o agente da Patrulha de Fronteira Gregory Bovino dando instruções a agentes federais."
1494
- }},
1495
- {{
1496
- "type": "description",
1497
- "start": "00:05",
1498
- "end": "00:20",
1499
- "text": "O momento foi registrado no verão passado, em Los Angeles, quando a repressão à imigração começou a se intensificar."
1500
- }},
1501
- {{
1502
- "type": "description",
1503
- "start": "00:20",
1504
- "end": "00:30",
1505
- "text": "O vídeo voltou a circular nesta semana após Bovino ter sido chamado de volta de Minnesota."
1506
- }},
1507
- {{
1508
- "type": "description",
1509
- "start": "00:30",
1510
- "end": "00:48",
1511
- "text": "As imagens foram divulgadas no ano passado como parte de uma ação judicial federal sobre a aplicação das leis de imigração na região de Chicago."
1512
- }},
1513
- {{
1514
- "type": "description",
1515
- "start": "00:48",
1516
- "end": "01:08",
1517
- "text": "Operações de imigração provocaram protestos na região das Cidades Gêmeas depois que dois cidadãos americanos foram mortos a tiros por agentes."
1518
- }},
1519
- {{
1520
- "type": "description",
1521
- "start": "01:08",
1522
- "end": "01:18",
1523
- "text": "O Departamento de Segurança Interna não respondeu imediatamente a um pedido de comentário."
1524
- }}
1525
- ]
1526
- """
1527
-
1528
- # 4. Enviar para o Gemini
1529
- model_name = request.model or "flash"
1530
- chatbot = chatbots.get(model_name, chatbots.get('flash', chatbots['default']))
1531
-
1532
- print(f"🧠 [GenerateElements] Enviando para Gemini ({model_name})...")
1533
-
1534
- # Implementar Retry logic para o chatbot.ask
1535
- max_retries = 3
1536
- response_gemini = None
1537
- last_error = None
1538
-
1539
- for attempt in range(max_retries):
1540
- try:
1541
- if attempt > 0:
1542
- print(f"🔄 [GenerateElements] Tentativa {attempt+1}/{max_retries}...")
1543
- import asyncio
1544
- await asyncio.sleep(2 * attempt) # Backoff
1545
-
1546
- response_gemini = await chatbot.ask(prompt, video=video_path_to_analyze)
1547
-
1548
- if response_gemini.get("error"):
1549
- error_msg = response_gemini.get("content", "")
1550
-
1551
- # Se for "Gemini API Error", NÃO retentar (a menos que seja um código específico transiente)
1552
- if "Gemini API Error" in error_msg:
1553
- # ERROR 4 (DEADLINE_EXCEEDED) e ERROR 14 (UNAVAILABLE) podem ser transientes
1554
- if "Gemini API Error: 4" in error_msg or "Gemini API Error: 14" in error_msg:
1555
- print(f"⚠️ [GenerateElements] Erro transiente da API do Gemini detectado: {error_msg}. Retentando...")
1556
- last_error = error_msg
1557
- continue
1558
-
1559
- print(f"🛑 [GenerateElements] Erro GRAVE da API do Gemini detectado: {error_msg}. Abortando retentativas.")
1560
- last_error = error_msg
1561
- break # Sai do loop de retentativas
1562
-
1563
- if "Failed to parse response body" in error_msg or "500" in error_msg:
1564
- last_error = error_msg
1565
- print(f"⚠️ [GenerateElements] Erro transiente detectado: {error_msg}. Retentando...")
1566
- continue
1567
- else:
1568
- # Erro não transiente (ex: recusa de segurança), não retentar
1569
- raise Exception(error_msg)
1570
-
1571
- # Sucesso
1572
- break
1573
- except Exception as e:
1574
- last_error = str(e)
1575
- print(f"⚠️ [GenerateElements] Exceção na tentativa {attempt+1}: {e}")
1576
- if "Failed to parse response body" in str(e):
1577
- continue
1578
- if "Gemini API Error" in str(e):
1579
- print(f"🛑 [GenerateElements] Abortando por erro de API: {e}")
1580
- break
1581
-
1582
- if response_gemini is None or response_gemini.get("error"):
1583
- detail = response_gemini.get('content') if response_gemini else str(last_error)
1584
-
1585
- # Tentar ler o arquivo de debug se existir
1586
- debug_content = ""
1587
- debug_path = "last_gemini_response_debug.txt"
1588
- if os.path.exists(debug_path):
1589
- try:
1590
- with open(debug_path, "r", encoding="utf-8") as f:
1591
- debug_content = f.read()[:10000] # Aumentar limite para 10000 chars
1592
- detail += f"\n\n--- DEBUG INFO (RAW GEMINI RESPONSE) ---\n{debug_content}"
1593
- except Exception as e:
1594
- print(f"Erro ao ler arquivo de debug: {e}")
1595
- detail += f"\n\n--- DEBUG INFO ---\nErro ao ler arquivo de debug: {e}"
1596
- else:
1597
- detail += f"\n\n--- DEBUG INFO ---\nArquivo de debug não encontrado em {debug_path}"
1598
-
1599
- # Print detail to logs for visibility
1600
- print(f"❌ [GenerateElements] Erro final retornado ao cliente:\n{detail}")
1601
-
1602
- raise HTTPException(status_code=500, detail=f"Erro no Gemini após {max_retries} tentativas: {detail}")
1603
-
1604
- content = response_gemini.get("content", "")
1605
- print(f"✅ Resposta recebida ({len(content)} chars)")
1606
-
1607
- # 5. Processar Resposta
1608
- elements_data = extract_json_from_text(content)
1609
-
1610
- if not elements_data:
1611
- print(f"⚠️ Falha ao extrair JSON. Conteúdo bruto: {content[:200]}...")
1612
- return JSONResponse(content={"raw_content": content, "error": "Failed to parse JSON"}, status_code=200)
1613
-
1614
- return elements_data
1615
-
1616
- except HTTPException:
1617
- raise
1618
- except Exception as e:
1619
- import traceback
1620
- traceback.print_exc()
1621
- raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
1622
- finally:
1623
- # Limpar arquivos temporários
1624
- if temp_file and os.path.exists(temp_file.name):
1625
- try:
1626
- os.unlink(temp_file.name)
1627
- except: pass
1628
- if cut_file and os.path.exists(cut_file.name):
1629
- try:
1630
- os.unlink(cut_file.name)
1631
- except: pass
 
700
  except: pass
701
 
702
 
703
+ class GenerateElementsRequest(BaseModel):
704
+ video_url: str
705
+ context: Optional[str] = None
706
+ start: Optional[str] = None
707
+ end: Optional[str] = None
708
+ model: Optional[str] = "flash"
709
+
710
+
711
+ @app.post("/generate-elements")
712
+ async def generate_elements_endpoint(request: GenerateElementsRequest):
713
+ """
714
+ Gera elementos a partir de um vídeo (ou trecho dele).
715
+ Duplicação do generate-titles para personalização futura do prompt.
716
+ """
717
+ if not chatbots:
718
+ raise HTTPException(status_code=500, detail="Chatbot não inicializado")
719
+
720
+ temp_file = None
721
+ cut_file = None
722
+
723
+ try:
724
+ # 1. Validar e Baixar Vídeo
725
+ if not request.video_url:
726
+ raise HTTPException(status_code=400, detail="URL do vídeo é obrigatória")
727
+
728
+ print(f"📥 [GenerateElements] Baixando vídeo: {request.video_url}")
729
+
730
+ # Baixar direto para um arquivo temporário
731
+ response = download_file_with_retry(request.video_url, timeout=600)
732
+
733
+ content_type = response.headers.get('content-type', '').lower()
734
+ ext = '.mp4'
735
+ if 'webm' in content_type: ext = '.webm'
736
+ elif 'mkv' in content_type: ext = '.mkv'
737
+
738
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
739
+ for chunk in response.iter_content(chunk_size=1024*1024):
740
+ if chunk:
741
+ temp_file.write(chunk)
742
+ temp_file.close()
743
+
744
+ video_path_to_analyze = temp_file.name
745
+
746
+ # 2. Cortar Vídeo se necessário
747
+ if request.start and request.end:
748
+ print(f"✂️ [GenerateElements] Cortando vídeo de {request.start} até {request.end}...")
749
+ cut_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
750
+ cut_file.close()
751
+
752
+ success = cut_video(temp_file.name, cut_file.name, request.start, request.end)
753
+ if success:
754
+ video_path_to_analyze = cut_file.name
755
+ print(f"✅ Vídeo cortado: {video_path_to_analyze}")
756
+ else:
757
+ print("⚠️ Falha ao cortar vídeo, usando original.")
758
+
759
+ # 3. Preparar Prompt
760
+ contexto_add = f"\n{request.context}" if request.context else ""
761
+
762
+ prompt = f"""Analise essa notícia e o vídeo:
763
+
764
+ {contexto_add}
765
+
766
+ Com base nela, gere titulos de notícias que podem ser extraidos do vídeo/notícia. Títulos de notícias.
767
+
768
+ Exemplos:
769
+
770
+ [
771
+ {{"title":"Trump repreende repórter da CNN por não sorrir"}},
772
+ {{"title":"Duas semanas de fortes nevascas causam caos no Japão"}},
773
+ {{"title":"Nova divulgação de arquivos sobre Jeffrey Epstein revela ligações com homens influentes"}},
774
+ {{"title":"Trump anuncia fechamento do Kennedy Center em julho para reforma de dois anos"}},
775
+ {{"title":"Como tecnologia de guerra foi usada em Minneapolis"}},
776
+ {{"title":"Trump diz ter alcançado base de acordo com a Otan sobre a Groenlândia"}},
777
+ {{"title":"Trump anuncia tarifas contra oito países europeus para forçar venda da Groenlândia"}},
778
+ {{"title":"Trump tem muitas opções para lidar com o Irã, diz porta-voz"}}
779
+ ]
780
+
781
+ Mande apenas os títulos, em forma JSON. Sem texto adicional. Deve analisar todo o contexto do vídeo e verificar qual o melhor título. O título deve ser sólido, evitar emojis, dois pontos, etc. O estilo dos títulos devem ser parecidos com o JSON.
782
+
783
+ Deve ter parametro start e end, e cortar o vídeo enviado nesse tempo. Pra analisar corretamente apenas a parte que o usuário quer. Considere que o jornal é imparcial na apresentação dos fatos, mas possui uma inclinação editorial claramente mais conservadora, republicana e de direita na entonação. Adapte os títulos de acordo com esse posicionamento, mantendo rigor factual e linguagem jornalística."""
784
+
785
+ # 4. Enviar para o Gemini
786
+ model_name = request.model or "flash"
787
+ chatbot = chatbots.get(model_name, chatbots.get('flash', chatbots['default']))
788
+
789
+ print(f"🧠 [GenerateElements] Enviando para Gemini ({model_name})...")
790
+
791
+ response_gemini = await chatbot.ask(prompt, video=video_path_to_analyze)
792
+
793
+ if response_gemini.get("error"):
794
+ raise HTTPException(status_code=500, detail=f"Erro no Gemini: {response_gemini.get('content')}")
795
+
796
+ content = response_gemini.get("content", "")
797
+ print(f"✅ Resposta recebida ({len(content)} chars)")
798
+
799
+ # 5. Processar Resposta
800
+ titles_data = extract_json_from_text(content)
801
+
802
+ if not titles_data:
803
+ print(f"⚠️ Falha ao extrair JSON. Conteúdo bruto: {content[:200]}...")
804
+ return JSONResponse(content={"raw_content": content, "error": "Failed to parse JSON"}, status_code=200)
805
+
806
+ return titles_data
807
+
808
+ except HTTPException:
809
+ raise
810
+ except Exception as e:
811
+ import traceback
812
+ traceback.print_exc()
813
+ raise HTTPException(status_code=500, detail=f"Erro interno: {str(e)}")
814
+ finally:
815
+ # Limpar arquivos temporários
816
+ if temp_file and os.path.exists(temp_file.name):
817
+ try:
818
+ os.unlink(temp_file.name)
819
+ except: pass
820
+ if cut_file and os.path.exists(cut_file.name):
821
+ try:
822
+ os.unlink(cut_file.name)
823
+ except: pass
824
+
825
+
826
 
827
 
828
  def flip_image_both_axes(image_path: str) -> str:
 
1412
  # But wait, did we shift srt_filtered before sending to Gemini?
1413
  # NO. srt_filtered is 0-based.
1414
  # So send 0-based to Gemini. Gemini returns 0-based.
1415
+ # We shift cleaned_srt.
1416
  # Optionally shift original_srt for reference
1417
  srt_filtered = shift_srt_timestamps(srt_filtered, request.time_start)
1418
 
 
1425
  except Exception as e:
1426
  import traceback
1427
  traceback.print_exc()
1428
+ raise HTTPException(status_code=500, detail=str(e))