Update main.py
Browse files
main.py
CHANGED
|
@@ -39,7 +39,7 @@ class ProcessLogger:
|
|
| 39 |
|
| 40 |
sys.path.append(str(Path(__file__).parent.parent))
|
| 41 |
from detect_crop_image import detect_and_crop_image
|
| 42 |
-
from detect_crop_video import detect_and_crop_video
|
| 43 |
|
| 44 |
from gemini_webapi import GeminiClient
|
| 45 |
from gemini_webapi.constants import Model
|
|
@@ -399,48 +399,84 @@ async def run_process_url(request: ProcessUrlRequest, record_id: int):
|
|
| 399 |
else:
|
| 400 |
logger.log("✂️ Processando vídeo: detectando movimento e bordas...")
|
| 401 |
try:
|
| 402 |
-
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
-
if crop_status == "
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
-
#
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
vid_upload_resp = requests.post(
|
| 433 |
-
"https://habulaj-recurve-save.hf.space/upload",
|
| 434 |
-
files={"files": ("cropped_video.mp4", vf, "video/mp4")},
|
| 435 |
-
data={"long_duration": "yes"},
|
| 436 |
-
timeout=120,
|
| 437 |
-
)
|
| 438 |
-
vid_upload_resp.raise_for_status()
|
| 439 |
-
exported_cropped_video_url = vid_upload_resp.json().get("url", "")
|
| 440 |
-
logger.log(f"✅ URL do vídeo cortado: {exported_cropped_video_url}")
|
| 441 |
else:
|
| 442 |
cropped_video_path = None
|
| 443 |
logger.log("⚠️ Crop de vídeo ignorado ou falhou.")
|
|
|
|
| 444 |
except Exception as e:
|
| 445 |
cropped_video_path = None
|
| 446 |
logger.log(f"⚠️ Erro no processo de crop: {e}")
|
|
@@ -1693,33 +1729,78 @@ async def run_filter_account(account: str, background_tasks: BackgroundTasks = N
|
|
| 1693 |
else:
|
| 1694 |
logger.log("✂️ Processando vídeo: detectando movimento e bordas...")
|
| 1695 |
try:
|
| 1696 |
-
# Criar arquivo temporário para o vídeo cortado
|
| 1697 |
cropped_video_tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
|
| 1698 |
-
|
|
|
|
|
|
|
| 1699 |
|
| 1700 |
-
if crop_status == "
|
| 1701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1702 |
|
| 1703 |
-
# Notificação do Agente no Discord
|
| 1704 |
try:
|
| 1705 |
-
|
| 1706 |
-
|
| 1707 |
-
|
| 1708 |
-
|
| 1709 |
-
|
| 1710 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1711 |
|
| 1712 |
-
requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={
|
| 1713 |
-
"approved_filter": False,
|
| 1714 |
-
"filter_message": "Rejeitado automaticamente: Região útil de texto insuficiente para crop seguro (texto centralizado no vídeo).",
|
| 1715 |
-
"result": {"error": "Rejeitado automaticamente: Região útil de texto insuficiente para crop seguro (texto centralizado). Tente outro vídeo."},
|
| 1716 |
-
"contains_image": False
|
| 1717 |
-
})
|
| 1718 |
-
return # Para o processamento aqui
|
| 1719 |
-
|
| 1720 |
-
if crop_status == "success" and os.path.exists(cropped_video_tmp):
|
| 1721 |
cropped_file_path = cropped_video_tmp
|
| 1722 |
-
logger.log(f"✅ Vídeo
|
| 1723 |
else:
|
| 1724 |
logger.log("⚠️ Crop de vídeo não gerou novo arquivo ou foi ignorado.")
|
| 1725 |
if os.path.exists(cropped_video_tmp): os.unlink(cropped_video_tmp)
|
|
|
|
| 39 |
|
| 40 |
sys.path.append(str(Path(__file__).parent.parent))
|
| 41 |
from detect_crop_image import detect_and_crop_image
|
| 42 |
+
from detect_crop_video import detect_and_crop_video, detect_and_crop_text
|
| 43 |
|
| 44 |
from gemini_webapi import GeminiClient
|
| 45 |
from gemini_webapi.constants import Model
|
|
|
|
| 399 |
else:
|
| 400 |
logger.log("✂️ Processando vídeo: detectando movimento e bordas...")
|
| 401 |
try:
|
| 402 |
+
# Criar arquivo temporário para o vídeo cortado (Passo 1: Motion Crop)
|
| 403 |
+
cropped_video_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 404 |
+
cropped_video_path = cropped_video_tmp # Track for finally cleanup
|
| 405 |
+
# Sempre fazemos o motion crop primeiro sem o text_cut interno
|
| 406 |
+
crop_status = detect_and_crop_video(video_path_to_analyze, cropped_video_tmp, text_cut=False)
|
| 407 |
|
| 408 |
+
if crop_status == "success" and os.path.exists(cropped_video_tmp):
|
| 409 |
+
# Passo 2: Verificação via Gemini (Apenas se o usuário solicitou text_cut)
|
| 410 |
+
if request.text_cut:
|
| 411 |
+
logger.log("🧠 Verificando presença de textos externos via Gemini (process-url)...")
|
| 412 |
+
text_check_model = get_gemini_model("flash")
|
| 413 |
+
text_check_prompt = """Analise o vídeo anexado e determine se há textos de edição externa (overlays de terceiros) adicionados após a produção original ou profissional do conteúdo.
|
| 414 |
+
|
| 415 |
+
Retorne um JSON no formato: {"has_texts": true/false}
|
| 416 |
+
|
| 417 |
+
Regras de Classificação:
|
| 418 |
+
- Considere como FALSE (Não marcar como texto de edição):
|
| 419 |
+
- Gráficos de Transmissão: Nomes de indicados, categorias de premiação, placares esportivos ou "lower thirds" que fazem parte da transmissão oficial da TV/Evento.
|
| 420 |
+
- Créditos Artísticos/Branding Profissional: Logotipos de marcas (ex: "Dior") ou créditos integrados que fazem parte da peça audiovisual original ou de um "lookbook" profissional.
|
| 421 |
+
- Textos de Ambiente: Placas, telões no fundo do palco ou escritos em roupas.
|
| 422 |
+
- Estética Profissional: Qualquer texto que tenha sido claramente renderizado como parte da master original do vídeo.
|
| 423 |
+
- Considere como TRUE (Marcar como texto de edição):
|
| 424 |
+
- Legendas de Redes Sociais: Captions explicativas ou legendas automáticas adicionadas para visualização em redes sociais.
|
| 425 |
+
- Textos de Engajamento: Frases como "Assista até o final", "O que você achou?", emojis flutuantes ou textos genéricos de meme.
|
| 426 |
+
- Marcas d'água de editores externos: Textos que identificam perfis de usuários ou editores que não são os produtores originais do evento.
|
| 427 |
+
|
| 428 |
+
Objetivo: O foco é detectar se o vídeo foi "re-editado" com textos informativos/estáticos típicos de redes sociais. Se o texto parece vir da produção original (Oscar, marcas de luxo, TV), retorne false. Responda apenas com o JSON, sem explicações."""
|
| 429 |
+
|
| 430 |
+
try:
|
| 431 |
+
text_resp = await client.generate_content(text_check_prompt, files=[cropped_video_tmp], model=text_check_model)
|
| 432 |
+
text_data = extract_json_from_text(text_resp.text)
|
| 433 |
+
has_texts = text_data.get("has_texts", False) if isinstance(text_data, dict) else False
|
| 434 |
+
|
| 435 |
+
if has_texts:
|
| 436 |
+
logger.log("🔍 Gemini detectou textos de edição externa. Iniciando OCR...")
|
| 437 |
+
ocr_video_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 438 |
+
text_crop_status = detect_and_crop_text(cropped_video_tmp, ocr_video_tmp)
|
| 439 |
+
|
| 440 |
+
if text_crop_status == "aborted_area_too_small":
|
| 441 |
+
logger.log("🚫 Crop abortado: área de texto no centro (segurança).")
|
| 442 |
+
# Notificação do Agente no Discord (Octavio/Diana)
|
| 443 |
+
try:
|
| 444 |
+
import urllib.parse as _up
|
| 445 |
+
reject_msg = f"Analisando aqui, notei que o texto desse vídeo está bem no centro. Se eu cortasse agora, ia acabar destruindo o conteúdo principal. Como a gente preza pela qualidade da página, preferi reprovar esse aqui automaticamente. ❌"
|
| 446 |
+
_reject_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": reject_msg, "id": discord_id})
|
| 447 |
+
requests.get("https://proxy.onrecurve.com/", params={"quest": _reject_url}, timeout=5)
|
| 448 |
+
except Exception as _e_dc:
|
| 449 |
+
logger.log(f"⚠️ Erro ao enviar Discord de rejeição automática: {_e_dc}")
|
| 450 |
+
|
| 451 |
+
requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={
|
| 452 |
+
"approved_filter": False,
|
| 453 |
+
"filter_message": "Rejeitado automaticamente: Região útil de texto insuficiente para crop seguro (texto centralizado).",
|
| 454 |
+
"result": {"error": "Rejeitado automaticamente: Região útil de texto insuficiente para crop seguro (texto centralizado). Tente outro vídeo."},
|
| 455 |
+
"contains_image": False
|
| 456 |
+
})
|
| 457 |
+
if os.path.exists(cropped_video_tmp): os.unlink(cropped_video_tmp)
|
| 458 |
+
if os.path.exists(ocr_video_tmp): os.unlink(ocr_video_tmp)
|
| 459 |
+
return
|
| 460 |
+
elif text_crop_status == "success" and os.path.exists(ocr_video_tmp):
|
| 461 |
+
if os.path.exists(cropped_video_tmp): os.unlink(cropped_video_tmp)
|
| 462 |
+
cropped_video_tmp = ocr_video_tmp
|
| 463 |
+
cropped_video_path = ocr_video_tmp # Update tracking
|
| 464 |
+
logger.log(f"✅ Vídeo cortado via OCR com sucesso em: {cropped_video_tmp}")
|
| 465 |
+
else:
|
| 466 |
+
if os.path.exists(ocr_video_tmp): os.unlink(ocr_video_tmp)
|
| 467 |
+
logger.log("⚠️ Crop de OCR ignorado ou falhou, seguindo com motion crop.")
|
| 468 |
+
else:
|
| 469 |
+
logger.log("✅ Gemini NÃO detectou textos de social media. Pulando OCR.")
|
| 470 |
+
except Exception as ge:
|
| 471 |
+
logger.log(f"⚠️ Erro na análise Gemini de textos: {ge}. Seguindo apenas com motion crop.")
|
| 472 |
|
| 473 |
+
# Final path assignment
|
| 474 |
+
cropped_video_path = cropped_video_tmp
|
| 475 |
+
logger.log(f"✅ Vídeo (motion/ocr) consolidado em: {cropped_video_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
else:
|
| 477 |
cropped_video_path = None
|
| 478 |
logger.log("⚠️ Crop de vídeo ignorado ou falhou.")
|
| 479 |
+
if os.path.exists(cropped_video_tmp): os.unlink(cropped_video_tmp)
|
| 480 |
except Exception as e:
|
| 481 |
cropped_video_path = None
|
| 482 |
logger.log(f"⚠️ Erro no processo de crop: {e}")
|
|
|
|
| 1729 |
else:
|
| 1730 |
logger.log("✂️ Processando vídeo: detectando movimento e bordas...")
|
| 1731 |
try:
|
| 1732 |
+
# Criar arquivo temporário para o vídeo cortado (Passo 1: Motion Crop)
|
| 1733 |
cropped_video_tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
|
| 1734 |
+
cropped_file_path = cropped_video_tmp # Track for finally cleanup
|
| 1735 |
+
# Desativamos o text_cut inicial para fazer a análise do Gemini antes
|
| 1736 |
+
crop_status = detect_and_crop_video(media_path_to_analyze, cropped_video_tmp, text_cut=False)
|
| 1737 |
|
| 1738 |
+
if crop_status == "success" and os.path.exists(cropped_video_tmp):
|
| 1739 |
+
# Passo 2: Verificação via Gemini para saber se deve rodar OCR
|
| 1740 |
+
logger.log("🧠 Verificando presença de textos externos via Gemini...")
|
| 1741 |
+
text_check_model = get_gemini_model("flash")
|
| 1742 |
+
text_check_prompt = """Analise o vídeo anexado e determine se há textos de edição externa (overlays de terceiros) adicionados após a produção original ou profissional do conteúdo.
|
| 1743 |
+
|
| 1744 |
+
Retorne um JSON no formato: {"has_texts": true/false}
|
| 1745 |
+
|
| 1746 |
+
Regras de Classificação:
|
| 1747 |
+
- Considere como FALSE (Não marcar como texto de edição):
|
| 1748 |
+
- Gráficos de Transmissão: Nomes de indicados, categorias de premiação, placares esportivos ou "lower thirds" que fazem parte da transmissão oficial da TV/Evento.
|
| 1749 |
+
- Créditos Artísticos/Branding Profissional: Logotipos de marcas (ex: "Dior") ou créditos integrados que fazem parte da peça audiovisual original ou de um "lookbook" profissional.
|
| 1750 |
+
- Textos de Ambiente: Placas, telões no fundo do palco ou escritos em roupas.
|
| 1751 |
+
- Estética Profissional: Qualquer texto que tenha sido claramente renderizado como parte da master original do vídeo.
|
| 1752 |
+
- Considere como TRUE (Marcar como texto de edição):
|
| 1753 |
+
- Legendas de Redes Sociais: Captions explicativas ou legendas automáticas adicionadas para visualização em redes sociais.
|
| 1754 |
+
- Textos de Engajamento: Frases como "Assista até o final", "O que você achou?", emojis flutuantes ou textos genéricos de meme.
|
| 1755 |
+
- Marcas d'água de editores externos: Textos que identificam perfis de usuários ou editores que não são os produtores originais do evento.
|
| 1756 |
+
|
| 1757 |
+
Objetivo: O foco é detectar se o vídeo foi "re-editado" com textos informativos/estáticos típicos de redes sociais. Se o texto parece vir da produção original (Oscar, marcas de luxo, TV), retorne false. Responda apenas com o JSON, sem explicações."""
|
| 1758 |
|
|
|
|
| 1759 |
try:
|
| 1760 |
+
text_resp = await client.generate_content(text_check_prompt, files=[cropped_video_tmp], model=text_check_model)
|
| 1761 |
+
text_data = extract_json_from_text(text_resp.text)
|
| 1762 |
+
has_texts = text_data.get("has_texts", False) if isinstance(text_data, dict) else False
|
| 1763 |
+
|
| 1764 |
+
if has_texts:
|
| 1765 |
+
logger.log("🔍 Gemini detectou textos de edição externa. Iniciando OCR...")
|
| 1766 |
+
# Passo 3: OCR Crop (Apenas se o Gemini confirmou)
|
| 1767 |
+
ocr_video_tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
|
| 1768 |
+
text_crop_status = detect_and_crop_text(cropped_video_tmp, ocr_video_tmp)
|
| 1769 |
+
|
| 1770 |
+
if text_crop_status == "aborted_area_too_small":
|
| 1771 |
+
logger.log("🚫 Crop abortado: área de texto no centro (segurança). Reprovando filtro.")
|
| 1772 |
+
# Notificação do Agente no Discord
|
| 1773 |
+
try:
|
| 1774 |
+
import urllib.parse as _up
|
| 1775 |
+
reject_msg = f"Analisei esse vídeo aqui e notei que o texto dele está bem no centro. Se eu fizesse o corte agora, ia acabar cortando partes importantes da mensagem. Por isso, decidi reprovar ele automaticamente para manter a qualidade! ❌"
|
| 1776 |
+
_reject_url = "https://discordmsg.arthurmribeiro51.workers.dev/?" + _up.urlencode({"mensagem": reject_msg, "id": discord_id})
|
| 1777 |
+
requests.get("https://proxy.onrecurve.com/", params={"quest": _reject_url}, timeout=5)
|
| 1778 |
+
except Exception as _e_dc:
|
| 1779 |
+
logger.log(f"⚠️ Erro ao enviar Discord de rejeição automática (crop): {_e_dc}")
|
| 1780 |
+
|
| 1781 |
+
requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={
|
| 1782 |
+
"approved_filter": False,
|
| 1783 |
+
"filter_message": "Rejeitado automaticamente: Região útil de texto insuficiente para crop seguro (texto centralizado no vídeo).",
|
| 1784 |
+
"result": {"error": "Rejeitado automaticamente: Região útil de texto insuficiente para crop seguro (texto centralizado). Tente outro vídeo."},
|
| 1785 |
+
"contains_image": False
|
| 1786 |
+
})
|
| 1787 |
+
return {"status": "ok", "message": "Postagem rejeitada por crop insuficiente.", "next_steps": {}}
|
| 1788 |
+
elif text_crop_status == "success" and os.path.exists(ocr_video_tmp):
|
| 1789 |
+
# Reemplazamos el motion crop original por el OCR crop
|
| 1790 |
+
if os.path.exists(cropped_video_tmp): os.unlink(cropped_video_tmp)
|
| 1791 |
+
cropped_video_tmp = ocr_video_tmp
|
| 1792 |
+
cropped_file_path = ocr_video_tmp # Update tracking
|
| 1793 |
+
logger.log(f"✅ Vídeo cortado via OCR com sucesso em: {cropped_video_tmp}")
|
| 1794 |
+
else:
|
| 1795 |
+
if os.path.exists(ocr_video_tmp): os.unlink(ocr_video_tmp)
|
| 1796 |
+
logger.log("⚠️ Crop de OCR ignorado ou falhou, mantendo apenas motion crop.")
|
| 1797 |
+
else:
|
| 1798 |
+
logger.log("✅ Gemini NÃO detectou textos de social media. Pulando OCR.")
|
| 1799 |
+
except Exception as ge:
|
| 1800 |
+
logger.log(f"⚠️ Erro na análise Gemini de textos: {ge}. Seguindo apenas com motion crop.")
|
| 1801 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1802 |
cropped_file_path = cropped_video_tmp
|
| 1803 |
+
logger.log(f"✅ Vídeo (motion) consolidado em: {cropped_file_path}")
|
| 1804 |
else:
|
| 1805 |
logger.log("⚠️ Crop de vídeo não gerou novo arquivo ou foi ignorado.")
|
| 1806 |
if os.path.exists(cropped_video_tmp): os.unlink(cropped_video_tmp)
|