Files
TextNLPClassifierApp/tests/integration/test_live_media_classifier_e2e.py
T

226 lines
9.3 KiB
Python

"""
Testes End-to-End (E2E) reais com Ollama e modelo qwen3.5:2b ao vivo.
Executa o funil completo de ponta a ponta:
1. Comunicação HTTP real com Ollama local (qwen3.5:2b);
2. Classificação semântica real para publicações de mídia vs texto;
3. Execução de lote com Foxcape + Ollama + Multimotor (Trafilatura, Newspaper4k, Readability);
4. Validação dos arquivos de saída (*_extracted.json e *_media.json).
"""
from __future__ import annotations
import json
import os
import sys
import urllib.request
from pathlib import Path
import pytest
from bs4 import BeautifulSoup
# Adiciona a raiz do projeto ao path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from scripts.extract_article_contents import (
MediaCandidateInfo,
MediaClassification,
build_compact_payload,
classify_media_content,
detect_candidate_media,
process_batch,
)
def is_ollama_available() -> bool:
"""Verifica se o servidor Ollama está ativo localmente e com qwen3.5:2b disponível."""
try:
url = os.environ.get("OLLAMA_ENDPOINT", "http://localhost:11434").rstrip("/") + "/api/tags"
req = urllib.request.Request(url)
with urllib.request.urlopen(req, timeout=3) as resp:
if resp.status == 200:
data = json.loads(resp.read().decode("utf-8"))
models = [m.get("name", "") for m in data.get("models", [])]
return any("qwen3.5:2b" in m for m in models)
except Exception:
return False
return False
@pytest.mark.skipif(not is_ollama_available(), reason="Ollama local com qwen3.5:2b não está acessível")
def test_live_ollama_video_media_classification() -> None:
"""Valida classificação real via Ollama para publicação de vídeo esportivo com texto descritivo curto."""
html = """
<html>
<head><title>Golaço de falta na Copa Libertadores - Melhores Momentos</title></head>
<body>
<article>
<h1>Golaço de falta na Copa Libertadores</h1>
<video controls src="https://example.com/videos/gol.mp4"></video>
<p>Confira no vídeo acima o golaço de falta marcado no último minuto da partida.</p>
</article>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
candidate_info = detect_candidate_media(soup)
assert candidate_info.has_candidate_media is True
assert candidate_info.has_video is True
payload = build_compact_payload(soup, candidate_info)
metrics = {
"fallback_groq": 0,
"fallback_omniroute": 0,
"classification_failed": 0,
}
classification, err = classify_media_content(payload, metrics, silent=False)
assert err is None
assert classification is not None
assert classification.content_type == "media"
assert classification.media_type in ("video", "mixed")
# Nenhum fallback deve ter sido acionado
assert metrics["fallback_groq"] == 0
assert metrics["fallback_omniroute"] == 0
@pytest.mark.skipif(not is_ollama_available(), reason="Ollama local com qwen3.5:2b não está acessível")
def test_live_ollama_substantive_textual_article_classification() -> None:
"""Valida classificação real via Ollama para matéria jornalística densa acompanhada de foto ilustrativa."""
html = """
<html>
<head><title>Banco Central mantém taxa de juros e sinaliza cautela econômica</title></head>
<body>
<article>
<h1>Banco Central mantém taxa de juros e sinaliza cautela econômica</h1>
<img src="https://example.com/fotos/reuniao_copom.jpg" alt="Reunião do Copom">
<p>O Comitê de Política Monetária (Copom) do Banco Central decidiu por unanimidade manter a taxa básica de juros (Selic) inalterada, conforme amplamente esperado pelos analistas de mercado.</p>
<p>Em seu comunicado oficial, a autoridade monetária destacou que o cenário global permanece volátil e que a conjuntura doméstica exige perseverança na condução da política monetária para consolidar a desinflação.</p>
<p>Economistas ouvidos pelo relatório Focus projetam que os primeiros cortes poderão ocorrer no próximo trimestre, caso as expectativas de inflação continuem ancoradas nas metas estabelecidas pelo Conselho Monetário Nacional.</p>
</article>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
candidate_info = detect_candidate_media(soup)
assert candidate_info.has_candidate_media is True
assert candidate_info.image_count == 1
payload = build_compact_payload(soup, candidate_info)
metrics = {
"fallback_groq": 0,
"fallback_omniroute": 0,
"classification_failed": 0,
}
classification, err = classify_media_content(payload, metrics, silent=False)
assert err is None
assert classification is not None
assert classification.content_type == "text"
assert classification.media_type is None
assert metrics["fallback_groq"] == 0
@pytest.mark.skipif(not is_ollama_available(), reason="Ollama local com qwen3.5:2b não está acessível")
def test_live_e2e_batch_funnel_with_ollama_and_multimotor(tmp_path: Path) -> None:
"""
Executa o funil completo em lote:
- 1 artigo textual puro (sem mídia) -> bypass direto do gate -> extraído pelos 3 motores
- 1 artigo predominantemente de mídia (vídeo) -> classificado pelo Ollama ao vivo -> desviado para *_media.json
- 1 artigo textual longo com foto ilustrativa -> classificado pelo Ollama ao vivo -> extraído pelos 3 motores
"""
input_file = tmp_path / "batch_funnel.json"
batch_data = {
"query": "noticias ao vivo",
"language": "pt",
"items": [
{
"titulo": "Artigo Textual Puro",
"url": "https://example.com/texto-puro",
"custom_id": "item-001",
},
{
"titulo": "Vídeo Highlights do Jogo",
"url": "https://example.com/video-highlights",
"custom_id": "item-002",
},
{
"titulo": "Análise Econômica com Foto",
"url": "https://example.com/analise-economica",
"custom_id": "item-003",
},
],
}
with open(input_file, "w", encoding="utf-8") as f:
json.dump(batch_data, f, ensure_ascii=False, indent=2)
html_puro = """
<html><body><article>
<h1>Artigo Textual Puro</h1>
<p>Primeiro parágrafo longo e substancial sobre política pública e saúde coletiva.</p>
<p>Segundo parágrafo detalhando as medidas sanitárias aprovadas pelo congresso.</p>
</article></body></html>
"""
html_video = """
<html><body><article>
<h1>Vídeo Highlights do Jogo</h1>
<video src="highlights.mp4"></video>
<p>Veja o resumo em vídeo com os melhores momentos da rodada esportiva.</p>
</article></body></html>
"""
html_foto = """
<html><body><article>
<h1>Análise Econômica com Foto</h1>
<img src="foto_grafico.jpg">
<p>O mercado de capitais registrou forte valorização nesta segunda-feira impulsionado por dados positivos do setor industrial.</p>
<p>As ações das principais empresas de commodities lideraram os ganhos no pregão, enquanto o câmbio operou em estabilidade.</p>
<p>Investidores estrangeiros aportaram mais de 500 milhões de reais no mercado à vista ao longo da última semana.</p>
</article></body></html>
"""
def mock_crawl(url: str) -> tuple[str, str, int]:
if "texto-puro" in url:
return html_puro, "Artigo Textual Puro", 200
elif "video-highlights" in url:
return html_video, "Vídeo Highlights do Jogo", 200
else:
return html_foto, "Análise Econômica com Foto", 200
from unittest.mock import patch, MagicMock
with patch("scripts.extract_article_contents.ArticleCrawler") as MockCrawler:
crawler = MagicMock()
crawler.__enter__.return_value = crawler
crawler.__exit__.return_value = None
crawler.crawl.side_effect = mock_crawl
MockCrawler.return_value = crawler
# Executa process_batch usando o Ollama real em localhost:11434
report = process_batch(input_file, silent=False)
# 1. Verifica arquivo textual *_extracted.json
# Devem existir 2 artigos textuais no relatório principal (item-001 e item-003)
assert report.total_articles == 2
assert report.successful_articles == 2
assert report.failed_articles == 0
assert len(report.articles) == 2
# Preservação de input_meta com custom_id nos artigos textuais
assert report.articles[0].input_meta.to_dict()["custom_id"] == "item-001"
assert report.articles[1].input_meta.to_dict()["custom_id"] == "item-003"
# 2. Verifica arquivo de mídia *_media.json
# Deve conter exatamente 1 artigo desviado (item-002)
media_file = tmp_path / "batch_funnel_media.json"
assert media_file.exists()
with open(media_file, "r", encoding="utf-8") as f:
media_data = json.load(f)
assert len(media_data["articles"]) == 1
media_item = media_data["articles"][0]
assert media_item["input_meta"]["custom_id"] == "item-002"
assert media_item["content_type"] == "media"
assert media_item["media_type"] in ("video", "mixed")