feat(media-routing): implement 007 media article routing, runtime architecture diagram and update graphify knowledge graph
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"name": "media_classifier",
|
||||
"title": "MediaClassifierOutput",
|
||||
"description": "Contrato estruturado estrito de dois campos emitido pelo classificador semântico",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content_type",
|
||||
"media_type"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"content_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"text",
|
||||
"media"
|
||||
],
|
||||
"description": "Classificação da publicação: text para texto jornalístico substancial, media para conteúdo predominantemente de mídia"
|
||||
},
|
||||
"media_type": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"video",
|
||||
"image",
|
||||
"images",
|
||||
"embed",
|
||||
"mixed",
|
||||
null
|
||||
],
|
||||
"description": "Subtipo de mídia quando content_type=media, ou null quando content_type=text"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
# CLI Contract & Routing Specification: `scripts/extract_article_contents.py`
|
||||
|
||||
## 1. Interface de Linha de Comando (CLI)
|
||||
|
||||
O script `scripts/extract_article_contents.py` preserva 100% da interface CLI existente sem novos argumentos:
|
||||
|
||||
```bash
|
||||
python scripts/extract_article_contents.py \
|
||||
-i, --input PATH # (Obrigatório) JSON de busca de notícias de entrada
|
||||
[-o, --output PATH] # (Opcional) Caminho do JSON de saída textual
|
||||
[-l, --limit N] # (Opcional) Limite máximo de artigos a processar
|
||||
[--lang, --language CODE] # (Opcional) Código do idioma para NLP (ex: pt, es, en)
|
||||
[-t, --timeout SEC] # (Opcional) Timeout em segundos para navegação (default: 30)
|
||||
[-s, --silent] # (Opcional) Suprime logs no stderr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Regras de Resolução de Arquivos de Saída
|
||||
|
||||
### Caso A: Sem `-o/--output` (Padrão)
|
||||
|
||||
Para uma entrada: `out/river_plate.json`
|
||||
|
||||
- **Saída Textual**: `out/river_plate_extracted.json`
|
||||
- **Saída de Mídia**: `out/river_plate_media.json` (gerado obrigatoriamente; contém `{"articles": []}` se nenhuma mídia for identificada)
|
||||
|
||||
### Caso B: Com `-o/--output` customizado
|
||||
|
||||
Para uma entrada: `out/noticias.json` com `-o out/processados/brasil_completo.json`
|
||||
|
||||
- **Saída Textual**: `out/processados/brasil_completo.json`
|
||||
- **Saída de Mídia**: `out/processados/brasil_completo_media.json` (gerado obrigatoriamente; contém `{"articles": []}` se nenhuma mídia for identificada)
|
||||
|
||||
Regra:
|
||||
- Diretório: mesmo diretório da saída textual informada.
|
||||
- Nome base (stem): mesmo stem da saída textual informada.
|
||||
- Sufixo: `_media`.
|
||||
- Extensão: `.json`.
|
||||
- Geração: O arquivo `*_media.json` MUST ser gerado em toda execução bem-sucedida do lote, sem omissão condicional.
|
||||
|
||||
---
|
||||
|
||||
## 3. Estrutura do Arquivo de Saída Textual (`*_extracted.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"source_file": "out/river_plate.json",
|
||||
"processed_at": "2026-08-24T20:30:00.000000+00:00",
|
||||
"total_articles": 8,
|
||||
"successful_articles": 7,
|
||||
"failed_articles": 1,
|
||||
"articles": [
|
||||
{
|
||||
"input_meta": {
|
||||
"titulo": "Notícia textual completa",
|
||||
"url": "https://example.com/noticia-1",
|
||||
"subtitulo": "Subtítulo",
|
||||
"quando_publicado": "há 2 horas",
|
||||
"pagina": 1
|
||||
},
|
||||
"extraction_status": "success",
|
||||
"error_message": null,
|
||||
"crawled_url": "https://example.com/noticia-1",
|
||||
"page_title": "Título no DOM",
|
||||
"http_status": 200,
|
||||
"trafilatura": { "text": "...", "markdown": "...", "title": "..." },
|
||||
"newspaper4k": { "text": "...", "summary": "...", "keywords": [] },
|
||||
"readability": { "cleaned_text": "...", "cleaned_html": "..." }
|
||||
},
|
||||
{
|
||||
"input_meta": {
|
||||
"titulo": "Notícia onde todos provedores falharam",
|
||||
"url": "https://example.com/falha"
|
||||
},
|
||||
"classification_status": "failed",
|
||||
"error_message": "media classification providers unavailable",
|
||||
"crawled_url": "https://example.com/falha",
|
||||
"page_title": null,
|
||||
"http_status": null,
|
||||
"trafilatura": null,
|
||||
"newspaper4k": null,
|
||||
"readability": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Estrutura do Arquivo de Saída de Mídia (`*_media.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"articles": [
|
||||
{
|
||||
"input_meta": {
|
||||
"titulo": "Vídeo dos melhores momentos do jogo",
|
||||
"url": "https://example.com/video-gols",
|
||||
"subtitulo": "Assista ao lance",
|
||||
"quando_publicado": "há 1 hora",
|
||||
"pagina": 1
|
||||
},
|
||||
"crawled_url": "https://example.com/video-gols",
|
||||
"page_title": "Vídeo dos Melhores Momentos",
|
||||
"http_status": 200,
|
||||
"content_type": "media",
|
||||
"media_type": "video"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Códigos de Saída (Exit Codes)
|
||||
|
||||
| Exit Code | Significado |
|
||||
|:---|:---|
|
||||
| `0` | Lote processado com sucesso (arquivos gravados) |
|
||||
| `1` | Arquivo de entrada inexistente ou erro de argumentos CLI |
|
||||
| `2` | Erro fatal não tratado |
|
||||
| `130` | Interrupção pelo usuário (`SIGINT` / Ctrl+C) |
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MediaOutputFileSchema",
|
||||
"description": "Schema de validação do arquivo de saída de mídias (*_media.json)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"articles"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"articles": {
|
||||
"type": "array",
|
||||
"description": "Lista de publicações classificadas como predominantemente de mídia",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"input_meta",
|
||||
"crawled_url",
|
||||
"page_title",
|
||||
"http_status",
|
||||
"content_type",
|
||||
"media_type"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input_meta": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"titulo",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"titulo": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"subtitulo": { "type": ["string", "null"] },
|
||||
"quando_publicado": { "type": ["string", "null"] },
|
||||
"pagina": { "type": ["integer", "null"] }
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"crawled_url": { "type": "string" },
|
||||
"page_title": { "type": ["string", "null"] },
|
||||
"http_status": { "type": ["integer", "null"] },
|
||||
"content_type": {
|
||||
"type": "string",
|
||||
"enum": ["media"]
|
||||
},
|
||||
"media_type": {
|
||||
"type": "string",
|
||||
"enum": ["video", "image", "images", "embed", "mixed"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user