Metadata-Version: 2.4
Name: ogacai
Version: 0.1.0
Summary: Pipeline standalone: subtítulos de YouTube (.json3) -> artículo de blog en Markdown, vía DeepSeek, Cloudflare Workers AI o Gemini.
Author-email: Retired64 <retired64.github@gmail.com>
License-Expression: MIT
Keywords: youtube,blog,cli,deepseek,subtitulos,markdown
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Text Processing :: Markup :: Markdown
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25
Requires-Dist: tomli>=2.0; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Provides-Extra: fetch
Requires-Dist: yt-dlp>=2024.1; extra == "fetch"
Provides-Extra: portada
Requires-Dist: pillow>=10.0; extra == "portada"
Requires-Dist: numpy>=1.24; extra == "portada"
Requires-Dist: cairosvg>=2.7; extra == "portada"
Dynamic: license-file

# ogacai

Pipeline de línea de comandos que convierte subtítulos automáticos de YouTube (`.json3`) en un artículo de blog en Markdown, usando DeepSeek (o, opcionalmente, Cloudflare Workers AI / Gemini) para la redacción.

## Descripción

`ogacai` resuelve un problema puntual: pasar de un video de YouTube a un artículo publicable sin perder fidelidad al contenido hablado y sin depender de un proceso manual de transcripción y limpieza.

El flujo se divide en etapas separadas, cada una con una responsabilidad clara:

```
url de YouTube  →  .json3  →  raw.txt + clean.txt  →  IA  →  artículo.md validado + portada.png
   (fetch, opcional)              (procesar)              (procesar)    (generar + validar)      (portada, al final de `run` salvo --sin-portada)
```

0. **Obtención** (`ogacai/fetch.py`, opcional): descarga el `.json3` de subtítulos automáticos de un video de YouTube vía `yt-dlp`, sin descargar el video. Es la única etapa que depende de una herramienta externa — por eso `yt-dlp` es una dependencia opcional (`pip install "ogacai[fetch]"`), no obligatoria para quien ya tiene su propio `.json3`.
1. **Parseo y limpieza** (`ogacai/procesar.py`): lee el `.json3`, reconstruye el texto hablado y produce dos versiones — una extracción fiel (`raw`) y una limpia (`clean`, sin repeticiones de "rolling captions" ni anotaciones tipo `[música]`). No llama a ninguna API; es reproducible y auditable.
2. **Generación** (`ogacai/deepseek.py` / `ogacai/workers_ai.py` / `ogacai/gemini.py` + `ogacai/prompt.py`): envía el texto limpio a un proveedor de IA (DeepSeek por defecto; Cloudflare Workers AI o Gemini como alternativas, seleccionables con `--proveedor`) con un system prompt fijo. El modelo solo recibe texto y solo devuelve texto — sin tool-calling, sin acceso a disco.
3. **Validación** (`ogacai/validar.py`): revisa que el artículo generado tenga la estructura esperada (front matter completo, tags coherentes, bloques de código bien cerrados). No valida fidelidad al contenido original — eso sigue siendo responsabilidad de una revisión humana.

Esta separación permite calibrar cada etapa por separado (por ejemplo, ajustar el prompt) sin gastar llamadas a la API mientras se depura el parseo.

## Características

- Parseo de `.json3` de subtítulos automáticos de YouTube, con deduplicación de texto solapado por "rolling captions".
- Limpieza opcional de muletillas comunes en español (`eh`, `mmm`, `este`, `o sea`, `bueno`, `digo`, entre otras) vía `--sin-muletillas`.
- Eliminación de anotaciones entre corchetes (`[música]`, `[aplausos]`, etc.) que no son habla real.
- Vista previa (`preview`) de qué palabras exactas quita la limpieza, sin necesidad de leer `raw.txt`/`clean.txt` completos a mano.
- Generación de artículos en Markdown vía DeepSeek, Cloudflare Workers AI o Gemini (`--proveedor deepseek|workers-ai|gemini`, default `deepseek`), con un system prompt fijo que distingue entre artículo narrativo (caso de estudio) y artículo explicativo/técnico, y reintentos con backoff ante fallas transitorias de red.
- Validación estructural del front matter (`title`, `description`, `date`, `image`, `imageAlt`, `tags`) y de los tags según el tipo de artículo.
- Resolución de configuración (API key y carpeta de salida) por variable de entorno o archivo `config.toml`.
- Pipeline completo (`run`) con modo `--dry-run` para revisar el resultado antes de escribir el archivo final, y generación de la portada OpenGraph como paso final (salvo `--sin-portada`).
- Sin dependencia de binarios compilados: corre igual en Termux, Linux o macOS usando el resolvedor DNS y el almacén de certificados del sistema (vía `requests`).

## Requisitos

- Python 3.9 o superior (usa `tomllib` de la librería estándar en 3.11+; en versiones anteriores se instala `tomli` como dependencia).
- Dependencia en tiempo de ejecución: [`requests`](https://pypi.org/project/requests/) (`>=2.25`).
- Una API key de [DeepSeek](https://platform.deepseek.com/) (solo necesaria para los subcomandos que llaman al modelo, y solo si usás el proveedor por defecto). Si preferís otro proveedor, necesitás en cambio credenciales de [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/) (account ID + API token) o de [Gemini](https://ai.google.dev/) (una API key) — ver "Configurar credenciales" más abajo.
- Un archivo `.json3` de subtítulos automáticos de un video de YouTube. `ogacai` puede descargarlo por vos con `ogacai fetch` (ver más abajo, requiere instalar el extra `fetch`), o podés obtenerlo por tu cuenta con `yt-dlp` y pasárselo directamente a `ogacai procesar`/`ogacai run`.
- **Opcional:** [`yt-dlp`](https://pypi.org/project/yt-dlp/) (`>=2024.1`), solo si querés usar `ogacai fetch`. Se instala con `pip install "ogacai[fetch]"`.

## Instalación

**Termux:**

```bash
pkg update
pkg install python
pip install .
ogacai --help
```

**Linux o macOS:**

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install .
ogacai --help
```

**Sin instalar el paquete** (para ejecutar directamente desde el código fuente):

```bash
pip install -r requirements.txt
python -m ogacai --help
```

Para usar `ogacai fetch` (descargar el `.json3` vía `yt-dlp`), instalá el extra correspondiente:

```bash
pip install "ogacai[fetch]"
```

### Configurar credenciales de los proveedores de IA

`ogacai generar`/`ogacai run` usan DeepSeek por defecto (`--proveedor deepseek`), pero podés elegir Cloudflare Workers AI (`--proveedor workers-ai`) o Gemini (`--proveedor gemini`) en su lugar. Solo hace falta configurar las credenciales del proveedor que realmente vayas a usar.

**DeepSeek** (default, una sola credencial), en orden de prioridad (la primera gana si ambas están definidas):

```bash
export DEEPSEEK_API_KEY="tu-key-aqui"
```

o de forma persistente:

```bash
mkdir -p ~/.config/ogacai
cat > ~/.config/ogacai/config.toml << 'EOF'
api_key = "tu-key-aqui"
output_dir = "/ruta/a/tu-repo-blog/src/content/blog"
EOF
```

`output_dir` es opcional: define la carpeta por defecto donde `ogacai run` escribe el artículo final si no se pasa `--output-dir`.

**Cloudflare Workers AI** (`--proveedor workers-ai`, requiere DOS credenciales: account ID + API token):

```bash
export WORKERS_AI_ACCOUNT_ID="tu-account-id"
export WORKERS_AI_API_TOKEN="tu-api-token"
```

o de forma persistente:

```bash
ogacai config set workers_ai.account_id "tu-account-id"
ogacai config set workers_ai.api_token "tu-api-token"
```

**Gemini** (`--proveedor gemini`, una sola credencial, más un modelo opcional):

```bash
export GEMINI_API_KEY="tu-key-aqui"
```

o de forma persistente:

```bash
ogacai config set gemini.api_key "tu-key-aqui"
ogacai config set gemini.model "gemini-3.5-flash-lite"   # opcional; ver nota abajo
```

`gemini.model` es opcional: si no se configura, se usa `gemini-3.5-flash-lite` (el default del cliente). El catálogo de modelos de Gemini rota rápido (versiones previas dejan de estar disponibles para cuentas nuevas), así que conviene poder cambiarlo sin tocar código si Google retira el modelo actual.

`ogacai config show` muestra la configuración resuelta, con las API keys/tokens enmascarados (solo se ven los últimos 4 caracteres).

## Uso

Ejemplo mínimo, de principio a fin:

```bash
# 0. (Opcional) Descargar el .json3 directo desde una URL de YouTube
ogacai fetch "https://youtu.be/tu-video" --salida-dir /tmp/prueba
# -> imprime "Listo: /tmp/prueba/<video_id>.es.json3"

# 1. Parsear el .json3 (gratis, no llama a ninguna API)
ogacai procesar /tmp/prueba/<video_id>.es.json3 --salida-dir /tmp/prueba

# 2. (Opcional) Revisar qué quitó la limpieza antes de gastar la llamada a DeepSeek
ogacai preview /tmp/prueba/<video_id>.es.json3

# 3. Generar el artículo con DeepSeek (default) -- usá --proveedor workers-ai o --proveedor gemini para otro proveedor
export DEEPSEEK_API_KEY="tu-key-aqui"
ogacai generar /tmp/prueba/transcripcion_clean.txt --output /tmp/prueba/articulo.md

# 4. Validar la estructura del artículo generado
ogacai validar /tmp/prueba/articulo.md
```

Pipeline completo en un solo paso, primero en modo de prueba:

```bash
ogacai run mi-video.json3 --dry-run
```

Y, una vez conforme con el resultado, la corrida que escribe el archivo final:

```bash
ogacai run mi-video.json3 --output-dir /ruta/a/tu-repo-blog/src/content/blog
```

## CLI

```bash
ogacai --help
```

```
usage: ogacai [-h] [-V] {fetch,procesar,preview,generar,validar,run} ...
```

| Subcomando | Qué hace |
|---|---|
| `fetch` | Etapa 0, opcional: descarga el `.json3` de subtítulos automáticos de una URL de YouTube vía `yt-dlp`, sin descargar el video. |
| `procesar` | Etapas 1-2: `.json3` → `transcripcion_raw.txt` + `transcripcion_clean.txt`. No llama a ninguna API. |
| `preview` | Corre las etapas 1-2 en memoria (sin escribir archivos) y muestra qué palabras exactas quitó la limpieza. |
| `generar` | Etapa 3: llama a un proveedor de IA (`--proveedor deepseek\|workers-ai\|gemini`, default `deepseek`) con un `.txt` ya limpio y devuelve el artículo por salida estándar (o a un archivo con `--output`). |
| `validar` | Valida estructuralmente un artículo `.md`/`.mdx` ya generado. |
| `run` | Pipeline completo: `procesar` → `generar` → `validar`, con escritura del artículo final y generación de la portada (paso 5, salvo `--sin-portada`). |

### `ogacai fetch`

```
ogacai fetch [-h] [--lang LANG] [--salida-dir SALIDA_DIR] url
```

```bash
ogacai fetch "https://youtu.be/tu-video" --lang es-orig --salida-dir /tmp/prueba
```

- `url`: URL del video de YouTube.
- `--lang`: pista de subtítulos automáticos a descargar (por defecto `es`). `es-orig` (español sin traducción automática) suele ser preferible cuando el video la tiene.
- `--salida-dir`: carpeta donde escribir el `.json3` (por defecto, el directorio actual).

Requiere el extra `fetch` instalado (`pip install "ogacai[fetch]"`). No descarga el video, solo los subtítulos. Si el video no tiene la pista pedida, informa qué pistas automáticas sí están disponibles en vez de fallar en silencio.

### `ogacai procesar`

```
ogacai procesar [-h] [--sin-muletillas] [--salida-dir SALIDA_DIR] archivo_json3
```

```bash
ogacai procesar mi-video.json3 --sin-muletillas --salida-dir /tmp/prueba
```

- `archivo_json3`: ruta al archivo `.json3` de subtítulos.
- `--sin-muletillas`: elimina muletillas comunes en español además de la limpieza básica.
- `--salida-dir`: carpeta de salida (por defecto, el directorio actual).

### `ogacai preview`

```
ogacai preview [-h] [--sin-muletillas] archivo_json3
```

```bash
ogacai preview mi-video.json3 --sin-muletillas
```

Corre las etapas 1-2 en memoria (no escribe `raw.txt`/`clean.txt`) y muestra, con un diff por palabras (`difflib`, sin dependencias externas), exactamente qué se quitó entre `raw` y `clean`. Pensado para confiar en la limpieza antes de gastar una llamada a DeepSeek.

### `ogacai generar`

```
ogacai generar [-h] [--proveedor {deepseek,workers-ai,gemini}] [--output OUTPUT] archivo_clean_txt
```

```bash
ogacai generar /tmp/prueba/transcripcion_clean.txt --output /tmp/prueba/articulo.md
ogacai generar /tmp/prueba/transcripcion_clean.txt --proveedor gemini --output /tmp/prueba/articulo.md
```

- `archivo_clean_txt`: ruta al `.txt` limpio.
- `--proveedor`: proveedor de IA a usar (default `deepseek`). Requiere las credenciales correspondientes ya configuradas (ver "Configurar credenciales de los proveedores de IA" más arriba): `DEEPSEEK_API_KEY` para `deepseek`, `workers_ai.account_id`/`workers_ai.api_token` para `workers-ai`, `GEMINI_API_KEY`/`gemini.api_key` para `gemini`.
- `--output OUTPUT`: archivo donde escribir el artículo generado. Sin este flag, el resultado se imprime por salida estándar (hay que redirigirlo con `>` para guardarlo). Reintenta automáticamente (con backoff) ante timeouts o errores de red — no ante errores ya devueltos por el servidor (autenticación, límite de uso, etc.).

### `ogacai validar`

```
ogacai validar [-h] archivo_articulo
```

```bash
ogacai validar /tmp/prueba/articulo.md
```

Revisa front matter completo, coherencia de tags y bloques de código balanceados. No confirma fidelidad al contenido original.

### `ogacai run`

```
ogacai run [-h] [--sin-muletillas] [--proveedor {deepseek,workers-ai,gemini}] [--output-dir OUTPUT_DIR] [--dry-run] [--sin-portada] archivo_json3
```

```bash
ogacai run mi-video.json3 --output-dir /ruta/a/tu-repo-blog/src/content/blog
ogacai run mi-video.json3 --proveedor gemini --output-dir /ruta/a/tu-repo-blog/src/content/blog
```

- `--sin-muletillas`: igual que en `procesar`.
- `--proveedor`: igual que en `generar` (default `deepseek`).
- `--output-dir`: carpeta de salida final (si no se pasa, usa `config.toml` o el directorio actual).
- `--dry-run`: corre todo el pipeline pero solo imprime el resultado, sin escribir el archivo.
- `--sin-portada`: no genera la portada OpenGraph al final. Por defecto, `run` la genera si la sección `[portada]` de la config está seteada (el extra `ogacai[portada]` sigue siendo opcional: si falta, el error se anota al final del flujo y `run` devuelve código 1).

El nombre del archivo final se deriva del campo `title` del front matter generado (slug en minúsculas, sin tildes, separado por guiones). Si no se puede extraer un título, se usa `articulo-sin-titulo.md`. Con la portada habilitada, `run` además escribe `{assets_dir}/{slug}.png` y corrige el campo `image` del front matter para que apunte a ese archivo (misma lógica que el subcomando `portada`, vía `portada.generar_desde_articulo`).

## Salida / JSON

`ogacai` no genera JSON de salida: parte de un JSON de entrada, el `.json3` de subtítulos que `ogacai procesar`/`ogacai run` esperan recibir (obtenido con `ogacai fetch` o por tu cuenta con `yt-dlp`). El parseo (`ogacai/procesar.py`) valida que el archivo sea JSON válido y que tenga la clave `events`; de lo contrario, corta con un error explícito.

Estructura real de un `.json3` (ejemplo simplificado):

```json
{
  "wireMagic": "pb3",
  "pens": [],
  "wsWinStyles": [],
  "wpWinPositions": [],
  "events": [
    {
      "tStartMs": 0,
      "dDurationMs": 801784,
      "id": 1,
      "wpWinPosId": 1,
      "wsWinStyleId": 1
    },
    {
      "tStartMs": 3840,
      "dDurationMs": 4079,
      "wWinId": 1,
      "segs": [
        { "utf8": "desarrolladores.", "acAsrConf": 0 },
        { "utf8": " Y", "tOffsetMs": 800, "acAsrConf": 0 }
      ]
    }
  ]
}
```

Campos relevantes para `ogacai`:

- `events`: lista de eventos de subtítulo. Es la única clave que el parser exige.
- `segs`: dentro de cada evento, la lista de segmentos de texto. Solo los eventos que tienen `segs` aportan contenido — se concatena el campo `utf8` de cada segmento para reconstruir el texto hablado. Eventos sin `segs` (como marcadores de posición o estilo) se ignoran.
- `tStartMs` / `dDurationMs` / `tOffsetMs`: timestamps que el parser lee pero que, por ahora, no se usan en ninguna salida.

A partir de ese `.json3`, `ogacai procesar` produce dos archivos de texto plano (no JSON): `transcripcion_raw.txt` y `transcripcion_clean.txt`.

## Estructura del proyecto

```
ogacai/
├── cli.py       # Punto de entrada de la CLI (argparse): subcomandos fetch/procesar/preview/generar/validar/run
├── fetch.py     # Etapa 0 (opcional): descarga el .json3 vía yt-dlp
├── procesar.py  # Etapas 1-2: parseo del .json3 y limpieza determinística, sin LLM
├── prompt.py    # System prompt fijo usado en la etapa de generación
├── deepseek.py  # Cliente HTTP a la API de DeepSeek (usa requests), con reintentos ante fallas de red
├── workers_ai.py # Cliente HTTP a Cloudflare Workers AI, proveedor alternativo (mismo contrato que deepseek.py)
├── gemini.py    # Cliente HTTP a Gemini (endpoint interactions), proveedor alternativo (mismo contrato que deepseek.py)
├── normalizar.py # Normalización de salida compartida entre proveedores (ej. quitar fence externo ```yaml)
├── config.py    # Resolución de credenciales de cada proveedor y de la carpeta de salida (env var o config.toml)
└── validar.py   # Validación estructural del artículo generado
tests/           # Suite de tests (pytest) y fixtures de .json3 reales/mínimos
.github/workflows/ci.yml  # CI: pytest, ruff y mypy en push/PR
pyproject.toml   # Metadata del paquete, entry point y el extra opcional [fetch] (yt-dlp)
requirements.txt # Dependencias para ejecutar sin instalar el paquete
```

## Documentación

- [`GETTING_STARTED.md`](./GETTING_STARTED.md): guía paso a paso orientada a quien nunca usó la CLI, con ejemplos de instalación, uso de cada subcomando, casos de uso completos y solución de errores comunes.

## Desarrollo

El proyecto tiene una suite de tests (`pytest`), lint (`ruff`) y type checking (`mypy`), corridos automáticamente en CI (`.github/workflows/ci.yml`) en cada push/PR.

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

pytest -v          # tests
ruff check .        # lint
mypy ogacai          # type checking
```

Con esta instalación en modo desarrollo, los cambios en `ogacai/` se reflejan de inmediato al ejecutar `python -m ogacai` o `pytest`. La carpeta `tests/fixtures/` incluye un `.json3` real y uno mínimo hecho a mano, usados por los tests de `procesar.py`. Los tests de `fetch.py` mockean `yt-dlp` por completo — no hacen llamadas de red reales. La carpeta `prueba/` contiene una transcripción y un artículo de ejemplo ya generados, útiles como referencia rápida sin tener que correr el pipeline completo de nuevo.

## Contribuciones

El repositorio no define un proceso formal de contribución. Para proponer un cambio, el camino directo es abrir un issue o un pull request describiendo el problema o la mejora concreta.

## Licencia

MIT, según lo declarado en `pyproject.toml`. El repositorio no incluye actualmente un archivo `LICENSE` independiente.
