Update app.py
Browse files
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.
|
| 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))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|