# worstscan — Manga Translation Pipeline Pipeline de traducción automática de manga (JP/EN/KR/CN → ES) con bot de Telegram, API REST, y delivery a Kindle vía Nextcloud. Descarga de nhentai/e-hentai, traduce con LLM via proxy OpenAI-compatible, inpinta globos con modelos de visión, renderiza texto, empaqueta en MOBI con KCC y sube a Nextcloud. --- ## Arquitectura General ``` ┌──────────────────────────────────────────────────────┐ │ Entry Points │ │ bot_artifacts.py │ artifacts.py │ cli.py │ └────────┬──────────┴───────┬──────────┴───────┬────────┘ │ │ │ ┌────────▼──────────────────▼──────────────────▼────────┐ │ Queues & State │ │ UrlQueue (JSON) GalleryQueue (JSON) StateDB │ │ data/bot_queue.json data/modal_queue/ SQLite │ └────────────────────────┬──────────────────────────────┘ │ ┌────────────────────────▼──────────────────────────────┐ │ Translation Pipeline │ │ ┌──────┐ ┌──────────┐ ┌──────┐ ┌────────┐ ┐ │ │ │DL │→ │Preprocess│→ │Refine│→ │Inpaint │→ │ │ │ │.cbz │ │detect/ │ │LLM │ │AOT/ │ │ │ │ │ │ │OCR/trans │ │polish│ │lama │ │ │ │ └──────┘ └──────────┘ └──────┘ └────────┘ │ │ │ ┌────────────────────────┐ ┌──────────┐ ┐ │ │ │ │Complete (V14 Render) │→ │KCC→MOBI │→ │ │ │ │ │PIL+textbbox typesetting│ │KPW34 │ │ │ │ │ └────────────────────────┘ └──────────┘ ┘ │ │ └────────────────────────┬──────────────────────────────┘ │ ┌────────────────────────▼──────────────────────────────┐ │ Delivery │ │ Nextcloud (sudo cp + occ files:scan) │ │ Local (shutil.copy2 to output/) │ └────────────────────────┬──────────────────────────────┘ │ ┌────────────────────────▼──────────────────────────────┐ │ API REST (FastAPI) │ │ GET /health | /status | POST /galleries | /search │ │ /summary | /cover | X-API-Key auth. Puerto 8080. │ │ Swagger en /docs. Puerto 8080. │ └───────────────────────────────────────────────────────┘ ``` --- ## Entry Points (Completo) | File | Rol | Cómo ejecutar | Producción | |------|-----|---------------|------------| | **`bot_artifacts.py`** | **Bot Telegram + API REST + Feed diario** | `uv run python3 bot_artifacts.py` (systemd `bot-artifacts.service`) | ✅ | | **`artifacts.py`** | **Pipeline completa** (download → translate → inpaint → render → MOBI) | `python artifacts.py [--skip-mobi] [--no-es-search] ` | ✅ | | **`cli.py`** | **CLI tool** (11 subcomandos) | `python cli.py [args]` | ✅ | | `inject_gallery.py` | Inyección directa al pipeline | `python inject_gallery.py ...` | ❌ aux | | `process.py` | Pipeline anterior (718 lines, coexiste) | — | ❌ legacy | | `bot.py` | Bot Telegram legacy (26KB, 749 lines) | `python -m src.bot` | ❌ legacy | | `visual_feedback.py` | Bucle de feedback visual LLM | — | ❌ standalone | | `_render_all.py` | Render interno variante | — | ❌ exp | | `_render_gallery.py` | Render interno | — | ❌ exp | | `_render_no_stroke.py` | Render sin stroke | — | ❌ exp | | `_render_variants.py` | Variantes de render | — | ❌ exp | | `_smart_render.py` | Smart render v1 | — | ❌ exp | | `_smart_render_v2.py` | Smart render v2 | — | ❌ exp | | `_smart_render_v3.py` | Smart render v3 | — | ❌ exp | | `_vision_feedback.py` | Vision feedback v1 | — | ❌ exp | | `_vision_feedback_v2.py` | Vision feedback v2 | — | ❌ exp | | `scripts/batch_convert_cbz.py` | Batch KCC de CBZs en Nextcloud | Standalone | ❌ script | | `scripts/download_type90.py` | Descarga TYPE.90 español | Standalone | ❌ script | | `scripts/test_pipeline_653011.py` | Test end-to-end nhentai | Standalone | ❌ script | ### systemd service ```ini # /etc/systemd/system/bot-artifacts.service ExecStart=/home/ren/.local/bin/uv run python3 bot_artifacts.py Restart=always ``` Restart: `sudo systemctl restart bot-artifacts.service` Logs: `sudo journalctl -u bot-artifacts.service -f` ### Import Quirk `src/` NO está instalado como paquete. Todos los comandos se ejecutan desde la raíz del proyecto, o `PYTHONPATH=.` cuando se corre desde otro lado. --- ## Config (.env vía pydantic-settings) Todas las settings via `src/config.py:Settings(BaseSettings)`. `.env` en la raíz del proyecto. ### Telegram y Delivery | Variable | Default | Descripción | |----------|---------|-------------| | `telegram_bot_token` | `""` | Token del bot (obligatorio para bot_artifacts.py) | | `telegram_chat_id` | `0` | Chat/User autorizado | | `delivery_backend` | `"local"` | `"local"` o `"nextcloud"` | | `output_dir` | `"output"` | Directorio de salida para delivery local | | `nextcloud_url` | `""` | Nextcloud server URL | | `nextcloud_user` | `""` | Nextcloud username | | `nextcloud_pass` | `""` | Nextcloud password | | `nextcloud_base_path` | `"/kindle"` | Nextcloud base path | | `nextcloud_data_path` | `""` | Nextcloud data directory path | | `state_db_path` | `"data/manga_state.db"` | SQLite DB path | ### Translation Pipeline | Variable | Default | Descripción | |----------|---------|-------------| | `translation_enabled` | `True` | Flag para activar/desactivar traducción | | `translation_api_key` | `""` | API key para proxy de traducción | | `translation_api_base` | `"http://127.0.0.1:55990/v1"` | Endpoint del proxy (OpenAI-compatible) | | `translation_model` | `"xiaomi/mimo-v2.5"` | Modelo LLM por defecto | | `translation_target_lang` | `"ESP"` | Lengua destino | | `translation_config_path` | `"translate_config.json"` | Config file de manga-translator | | `minimax_api_key` | `""` | MiniMax API key (fallback de translation_api_key) | | `minimax_api_host` | `"https://api.minimax.io"` | MiniMax host | | `vlm_endpoint` | `"/v1/coding_plan/vlm"` | VLM endpoint | | `minimax_chat_model` | `"MiniMax-Text-01"` | MiniMax chat model | | `vlm_timeout` | `90` | VLM timeout en segundos | ### Scrapers | Variable | Default | Descripción | |----------|---------|-------------| | `hentai_search_url` | `"https://e-hentai.org/..."` | URL por defecto de búsqueda e-hentai | | `hentai_subfolder` | `"hentai"` | Subcarpeta para hentai en Nextcloud | | `hentai_cleanup_days` | `7` | Días para cleanup | | `hentai_run_hour` | `6` | Hora del scheduler | | `hentai_rate_limit` | `1.5` | Rate limit en segundos | | `priority_rate_limit` | `0.75` | Rate limit para priority | | `ehentai_manga_run_hour` | `6` | Hora del EhentaiMangaScheduler | | `nhentai_daily_count` | `10` | Límite diario nhentai | | `fansub_enabled` | `True` | Activar fansub pipeline | ### REST API | Variable | Default | Descripción | |----------|---------|-------------| | `api_enabled` | `True` | Activar servidor API REST | | `api_host` | `"0.0.0.0"` | Bind address | | `api_port` | `8080` | Puerto | | `api_key` | `""` | API key (vacío = sin auth en dev) | | `api_rate_limit_per_min` | `60` | Rate limit general req/min | | `api_submit_limit_per_min` | `10` | Rate limit para submits/min | ### Misc | Variable | Default | Descripción | |----------|---------|-------------| | `mangas_folder` | `"/mnt/mangas"` | Carpeta vigilada por MangaWatcher | | `kindle_profile` | `"KPW34"` | Perfil Kindle para KCC | | `watch_interval` | `30` | Intervalo MangaWatcher en segundos | --- ## Pipeline de Traducción — Flujo de Cascada `bot_artifacts.py` y `artifacts.py` ejecutan el mismo pipeline de 5 fases: ``` URL (nhentai/e-hentai) │ ▼ ┌───────────────────────────────────────────────────────┐ │ [1/5] Download (artifacts.download_only) │ │ nhentai CDN (i.nhentai.net, ~0.1s/page) │ │ o fallback: e-hentai scraper (~2s/page) │ │ → data/work_{gid}/download_meta.json (enriquecido) │ │ → data/work_{gid}/ CBZ extraído en images/ │ └───────────────────────┬───────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────┐ │ [2/5] Preprocess (preprocess.preprocess_gallery) │ │ Detección de texto (CTD para JP, dbconvnext para EN) │ │ OCR (48px) → LLM translate → máscaras de inpaint │ │ Dynamic detection con 4 fallback detectors │ │ Pages en chunks de 50 con gc.collect() entre chunks │ │ → data/work_{gid}/ regions/ y masks/ │ └───────────────────────┬───────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────┐ │ [3/5] Quality + Refine (refine.refine_gallery) │ │ LLM polish con contexto completo de la galería │ │ 2 pasadas de refinamiento por página │ │ Chunking: si >50 págs, chunks de 50 │ │ Timeout: n_chunks * 120s (mín 300s) │ │ max_tokens=14000 (límite 16K del modelo) │ └───────────────────────┬───────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────┐ │ [4/5] Inpaint (inpaint_local.inpaint_gallery_local) │ │ GPU auto-detection (CUDA > MPS > CPU) │ │ Modelo global único: AOT (JP) o lama_large (EN/KR/CN) │ │ Semaphore: min(max(cpu//2,3),8) páginas concurrentes │ │ Missing/empty masks → copia original │ │ → data/work_{gid}/ inpainted/ │ └───────────────────────┬───────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────┐ │ [5/5] Complete + MOBI (complete.complete_gallery_cbz) │ │ V14 PIL renderer con textbbox real │ │ Binary-search font size (24-64px, fallback 12-23px) │ │ Flood-fill bubble detection + overlap resolution │ │ → CBZ → KCC (--format MOBI --profile KPW34) → .mobi │ │ → output/artifacts/{gid}/ + upload a Nextcloud │ └───────────────────────────────────────────────────────┘ ``` ### Enriched download metadata (`download_meta.json`) A partir del commit de /summary + /cover, `_download()` en `artifacts.py` escribe `download_meta.json` enriquecido con: | Campo | Tipo | Descripción | |-------|------|-------------| | `gid` | str | Gallery ID | | `title` | str | Título en inglés | | `title_jpn` | str | Título en japonés (si existe) | | `config` | str | Ruta del config de traducción | | `num_pages` | int | Número de páginas | | `pages` | int | Alias backward-compat de `num_pages` | | `is_spanish` | bool | Si ya está en español (skip translate) | | `url` | str | URL original | | `source` | str | `"nhentai"` o `"ehentai"` | | `media_id` | str | media_id de nhentai (para cover URL) | | `tags` | list[str] | Tags completos (`artist:xxx`, `parody:yyy`, etc.) | | `artist` | str | Artista extraído de tags | | `parody` | str | Parodia/franquicia extraída de tags | | `cover_url` | str | URL externa de portada (nhentai CDN o e-hentai thumb) | Las galleries **viejas** (descargadas antes del cambio) no tendrán estos campos. El endpoint `/summary` hace fallback a `QueueItem` de `UrlQueue` para cubrir `media_id`/`title_jpn`/`artist`/`parody`. --- ## bot_artifacts.py — Bot Telegram + API Server Archivo de **1586 líneas**. Se ejecuta como servicio systemd. Es el entry point más importante. ### Arquitectura de Procesamiento (Two-Phase) El procesamiento de galleries se divide en dos fases con **semáforos separados**: ``` asyncio.Queue (_processing_queue) ┌─────────────────────┐ │ 6 Workers (_queue_worker) │ │ Consume (url, bot, chat_id)│ └──────────┬──────────┘ │ ┌────────────────────┼────────────────────┐ │ Phase 1 (I/O) │ Phase 2 (CPU) │ ▼ ▼ ▼ ┌──────────────────┐ ┌────────────────────┐ │ _download_sem │ │ _translate_sem │ │ MAX_CONCURRENT=2 │ │ MAX_CONCURRENT=2 │ │ artifacts. │ │ _process_one() → │ │ download_only() │ │ artifacts.process()│ │ (descarga CBZ) │ │ (translate+inpaint │ │ │ │ +render+MOBI+up) │ └──────────────────┘ └────────────────────┘ ``` ### Inicialización (`main()` + `_post_init`) 1. `Application.builder().token(settings.telegram_bot_token).build()` 2. Registra CommandHandlers + MessageHandler 3. `_post_init` corre AL ARRANCAR el bot: 1. Notifica "Bot encendido" a `ALLOWED_CHAT_ID` 2. Lanza 6 workers (`_queue_worker`) 3. `_drain_pending_on_startup(app)` — recupera galleries pendientes/fallidas 4. Si `settings.api_enabled` → lanza `_start_api_server()` 5. `_periodic_cleanup_loop()` — cleanup cada 24h 6. `_daily_nhentai_feed_loop(app)` — feed diario a las 6AM ART (UTC-3) ### Comandos de Telegram | Comando | Handler | Descripción | |---------|---------|-------------| | `/start` | `start_command` | Texto de ayuda listando todos los comandos | | `/status` | `status_command` | Galleries activas, slots, UrlQueue stats, CPU/RAM, fallidas | | `/retry [gid]` | `retry_command` | Reintentar todas o una gallery fallida | | `/clean` | `clean_command` | Limpiar archivos >72h (vía `src.cleaner.clean_old_files`) | | `/b ` | `bypass_command` | Procesar gallery saltando filtros de tags | | `/update` | `nhfeed_command` | Activar manualmente el feed de portada de nhentai.net | | `/nhfeed` | `nhfeed_command` | Alias de /update | | `/wipe` | `wipe_command` | Limpiar cola + work dirs (NO borra output/artifacts ni Nextcloud) | ### Message Handler: `handle_message` Detecta URLs en el mensaje usando `MessageEntity` (offset+length exacto) con fallback a regex. **Tipos de URL** y sus rutas: | Tipo URL | Regex | Routing | |----------|-------|---------| | `gallery` | `e-hentai.org/g/(\d+)/([a-f0-9]+)` o `nhentai.net/g/(\d+)` | `_processing_queue.put()` → `_queue_worker` → `_process_gallery_url()` | | `search` | `e-hentai.org/?` | `asyncio.create_task(_process_search_url())` | | `nh_search` | `nhentai.net/search?` | `asyncio.create_task(_process_nhentai_search_url())` | | `nh_artist` | `nhentai.net/artist/([a-z0-9_-]+)` | `asyncio.create_task(_process_nhentai_artist_url())` | **Dedup en handle_message** para URLs de tipo `gallery`: 1. Si ya está `processing` o `pending` → skip 2. Si ya completada (artifacts dir o status completed) → skip 3. Si fallida antes → limpia work_dir, resetea a pending, reintenta 4. Si nueva → `UrlQueue.add()` + encola ### Funciones Internas Clave | Función | Firma | Rol | |---------|-------|-----| | `_queue_worker` | `(worker_id: int) -> None` | Loop infinito: consume `_processing_queue`, llama `_process_gallery_url` | | `_process_gallery_url` | `(url, bot, chat_id, bypass_filter=False)` | **Two-phase**: download_sem → `artifacts.download_only()`; translate_sem → `_process_one()` | | `_process_one` | `(url, bot, chat_id) -> bool` | Fase 2: `artifacts.process()` → CBZ → `send_to_kindle()` → actualiza stats | | `_process_search_url` | `(url, bot, chat_id)` | Búsqueda e-hentai: scrape → batch metadata → dedup 5 niveles → batch processing | | `_process_nhentai_search_url` | `(url, bot, chat_id)` | Búsqueda nhentai: content filter → FC→B&W redirect → dedup → batch processing | | `_process_nhentai_artist_url` | `(url, bot, chat_id)` | Artista nhentai: mismo flujo que search | | `_find_bw_alternative` | `(detail) -> NhentariGallery \| None` | Busca versión B&W alternativa cuando `should_download` bloquea por full color | | `_check_duplicate` | `(gid, url, metadata) -> str \| None` | 5 niveles: artifacts dir → work dir → work_key → parent key → SQLite fingerprint | | `_gdata_metadata` | `(gid, token) -> dict \| None` | Fetch metadata de e-hentai gdata API | | `_drain_pending_on_startup` | `(application)` | Recupera URLs pendientes + fallidas post-reinicio | | `_periodic_cleanup_loop` | `()` | Cleanup cada 24h via `src.cleaner.clean_old_files()` | | `_daily_nhentai_feed_loop` | `(app)` | Feed diario nhentai a las 6AM ART | | `_start_api_server` | `()` | Inicia FastAPI/uvicorn en el mismo event loop | | `_wipe_all` | `()` | Wipe standalone (sin bot) — llamado por API DELETE /queue | | `_clean_idle_memory` | `()` | Limpia cachés globales de manga-translator + `gc.collect()` cuando idle | | `_load_failed` / `_save_failed` | `() -> list` / `(list)` | Persistencia de fallidas en `data/failed_galleries.json` | | `_add_failed` / `_remove_failed` | `(url, gid, title, error)` / `(gid)` | Manager de lista de fallidas | | `_extract_urls` | `(text, entities) -> list[(url, type)]` | Extrae URLs + tipo del texto del mensaje | | `_extract_nh_query` | `(url) -> str` | Extrae `?q=` de URL de búsqueda nhentai | | `_extract_nh_artist_sort` | `(url) -> str` | Extrae `?sort=` de URL de artista nhentai | | `_build_parent_work_key` | `(title, title_jpn, tags) -> str` | Work key que ignora chapter/volume markers | | `_detect_batch_lang` | `(tags) -> str` | Detecta idioma de tags para dedup batch | | `_send_batch_summary` | `(batch) -> None` | Envía resumen de batch processing | | `_notify_batch_done` | `(batch_id) -> None` | Tracking interno de progreso de batch | ### Variables Globales ```python MAX_CONCURRENT = 2 # Slots de download y translate _NUM_WORKERS = 6 # Workers de la cola _translate_sem = Semaphore(2) _download_sem = Semaphore(2) _stats = {"completed": 0, "failed": 0, "active": 0} _wiping = False _processing_queue: asyncio.Queue = asyncio.Queue() _batch_tracker: dict[str, dict] = {} _LANG_PRIORITY = {"english": 0, "spanish": 1, "japanese": 2, ...} FAILED_FILE = Path("data/failed_galleries.json") ARTIFACTS_DIR = Path("output/artifacts") ``` ### Restart Recovery - `_drain_pending_on_startup()` corre en `post_init` - UrlQueue se persiste en `data/bot_queue.json` (JSON) - Items en estado "processing" al reiniciar se resetean a "pending" (crash recovery) - `failed_galleries.json` también se drena al arranque - `reload_from_disk()` es función **module-level** en `url_queue.py` --- ## REST API (FastAPI) — `src/api/` Servidor FastAPI en el mismo event loop que el bot de Telegram. Corre en `0.0.0.0:8080` por defecto. ### Inicio ```python # bot_artifacts.py:1497-1498 y 1550-1582 if settings.api_enabled: asyncio.create_task(_start_api_server()) async def _start_api_server(): api_app = create_app( download_sem=_download_sem, translate_sem=_translate_sem, process_gallery_url=_process_gallery_url, wipe_fn=_wipe_all, api_key=settings.api_key, ... ) config = uvicorn.Config(api_app, host=..., port=..., log_level="warning") server = uvicorn.Server(config) await server.serve() ``` ### Estructura de Archivos | Archivo | Rol | |---------|-----| | `src/api/__init__.py` | Factory `create_app()` — recibe semáforos y callbacks | | `src/api/main.py` | `create_fastapi_app()` — monta middleware CORS, auth, routers | | `src/api/auth.py` | `ApiKeyMiddleware` — auth via header `X-API-Key` | | `src/api/deps.py` | Inyección de dependencias (semáforos, funciones) | | `src/api/models.py` | Schemas Pydantic + helpers `ok()`, `err()`, `slot_info()` | | `src/api/errors.py` | Rate limiter token-bucket + exception handlers | | `src/api/routes/__init__.py` | Agregación de routers | | `src/api/routes/system.py` | `GET /health`, `GET /status` | | `src/api/routes/galleries.py` | CRUD de galleries | | `src/api/routes/queue.py` | Queue management | | `src/api/routes/search.py` | Search endpoints | ### Endpoints | Método | Ruta | Descripción | Auth | Rate Limit | |--------|------|-------------|------|------------| | `GET` | `/api/v1/health` | Health check simple (uptime, version) | ❌ Público | — | | `GET` | `/api/v1/status` | Estado completo: cola, slots, activas, RAM, CPU, modelo actual | ✅ | — | | `POST` | `/api/v1/galleries` | Enviar URL para procesamiento completo | ✅ | Submit | | `POST` | `/api/v1/galleries/download` | Solo descarga (fase 1) | ✅ | Submit | | `GET` | `/api/v1/galleries` | Lista galleries (filtro por status + paginación) | ✅ | — | | `GET` | `/api/v1/galleries/{gid}` | Detalle de una gallery | ✅ | — | | `GET` | `/api/v1/galleries/{gid}/summary` | Resumen completo: title, title_jpn, tags, artist, parody, cover URL, pipeline status | ✅ | — | | `GET` | `/api/v1/galleries/{gid}/cover` | Foto de portada (página 1) como FileResponse | ✅ | — | | `GET` | `/api/v1/galleries/{gid}/artifacts` | Archivos generados | ✅ | — | | `DELETE` | `/api/v1/galleries/{gid}` | Cancelar y eliminar gallery | ✅ | — | | `POST` | `/api/v1/search` | Buscar en nhentai/e-hentai (sin procesar) | ✅ | General | | `POST` | `/api/v1/search/process` | Buscar + auto-encolar resultados no bloqueados | ✅ | Submit | | `GET` | `/api/v1/queue` | Lista cola (filtro + paginación) | ✅ | — | | `GET` | `/api/v1/queue/stats` | Estadísticas de cola + slots | ✅ | — | | `POST` | `/api/v1/queue/{gid}/retry` | Reintentar gallery fallida | ✅ | — | | `DELETE` | `/api/v1/queue` | Wipe completo (equivalente a `/wipe`) | ✅ | — | Swagger UI en `GET /docs`, ReDoc en `GET /redoc`, schema en `GET /openapi.json`. ### Autenticación - **Middleware**: `ApiKeyMiddleware` (Starlette `BaseHTTPMiddleware`) - **Header**: `X-API-Key` - **Bypass paths**: `/api/v1/health` siempre público - **Bypass IPs**: `127.0.0.1`, `::1`, `::ffff:127.0.0.1` - **Dev mode**: si `api_key` está vacío en `.env`, no requiere auth - **401 response**: `{"error": {"code": "unauthorized", "message": "Missing or invalid API key"}}` ### Rate Limiting Token bucket in-memory por IP: - **General**: `api_rate_limit_per_min` (default 60/min) — aplica a GETs y search - **Submit**: `api_submit_limit_per_min` (default 10/min) — aplica a POST gallerias + search/process - **429 response**: `{"error": {"code": "rate_limited", "message": "..."}}` ### NullBot La API usa `_NullBot` como reemplazo de `telegram.Bot` para los mensajes de progreso. Los absorbe y los envía a logger en vez de a Telegram. ### CORS Permite todos los orígenes (`*`) para scripts locales. Lockear en producción vía nginx. --- ## Módulos del Pipeline (`src/translator/`) ### `preprocess.py` — Detección, OCR, Traducción, Máscaras - `preprocess_gallery(gallery_dir, device)`: Procesa página por página - Detecta texto (dynamic detection con 4 fallbacks) - OCR con tesseract/manga-ocr - LLM translate (proxy OpenAI-compatible) - Genera máscaras de inpaint (por región, con `_rebuild_mask_per_region`) - Chunks de 50 páginas con `gc.collect()` entre chunks - RSS threshold: `min(max(total_mem * 0.7, 6.0), 24.0)` GB - Timeout por página: 600s (env `PAGE_TIMEOUT`) - Concurrencia dinámica: `set_preprocess_concurrent(n)` ### `refine.py` — LLM Polish - `refine_gallery(gallery_dir, chunk_size=0)`: - 2 pasadas de refinamiento LLM por página - Chunking automático (>50 pages → chunks de 50) - Timeout dinámico: `n_chunks * 120s` (mín 300s) - `_call_llm()` con `max_tokens=14000` - El modelo real es el mismo que usa traducción (seteado vía `os.environ["OPENAI_MODEL"]`) - El default `mimo-v2.5-free` en refine.py es un **fallback muerto** — nunca se usa ### `inpaint_local.py` — Inpainting - `inpaint_gallery_local(gallery_dir, device)`: - Modelo global único (AOT para JP, lama_large para EN/KR/CN) - Semaphore: `min(max(cpu_count // 2, 3), 8)` páginas concurrentes - Missing/empty masks → copia original - Failed → fallback a original ### `complete.py` — Render V14 + CBZ Packaging **V14 Renderer** (PIL-based, producción): - `render_page_v14(inpainted_img, regions_data, mask_gray) -> np.ndarray`: - Flood-fill bounded bubble detection sobre imagen inpainted - Binary-search font size con PIL `textbbox` (24-64px, fallback 12-23px) - White stroke para legibilidad - Iterative overlap resolution (3 pasadas, shift 25-35px) - Character-by-character font fallback para símbolos no soportados - CJK stripping para targets no-CJK - Normaliza puntuación CJK (… → ..., ‼ → !!) - Spanish word hyphenation (`_split_word_spanish`) - `_draw_text_with_fallback`: si un carácter no está en el font principal, usa Arial Unicode - `complete_gallery_cbz(gallery_dir, inpainted_dir, output_cbz) -> Path`: - Procesa cada página: render V14 si tiene texto, inpainted direct si no - Empaqueta en CBZ (JPEG quality 92) - Pipeline state tracking por página ### `dynamic_detection.py` — Adaptive Detection - `compute_adaptive_config(config_dict, image) -> dict`: Ajusta `detection_size`, `text_threshold`, `box_threshold`, `unclip_ratio` según: - Tamaño de imagen: <1MP→1024, 1-3MP→1536, >3MP→2048 - Contraste: <30→th 0.1, 30-50→0.15, >50→0.2 - Text scale (Laplacian edge density) - Fallback detector chain (4 intentos): 1. Config adaptativa 2. Detector `default` con thresholds bajos (0.15/0.2), 1536px 3. `ctd` con thresholds mínimos (0.1/0.15), 1024px 4. `default` con thresholds mínimos, 2048px ### `language.py` — Detección de Idioma + Config Routing - `detect_language(tags) -> str`: Detecta idioma de tags (e-hentai `language:japanese` o nhentai `japanese`) - `select_config_path(language) -> str`: Mapea idioma a archivo de configuración - `write_metadata(gallery_dir, gid, title, tags, source, language)`: Escribe `source_metadata.json` - Priority: Spanish > English > Korean > Chinese > Japanese - Config files: japonés→`translate_config.json`, otros→ `translate_config_english/korean/chinese.json` ### `pipeline_state.py` — Tracking de Estado ```python _active_galleries: dict[str, PipelineState] = {} ``` - `set_pipeline_state(gallery_id, phase, done, total)`: Actualiza estado (llamado desde preprocess, inpaint, render, mobi, upload) - `get_active_galleries() -> dict`: Usado por `/status` y API `GET /status` - `clear_pipeline_state(gallery_id)`: Limpia al completar/fallar ### `queue.py` — GalleryQueue - JSON-backed singleton en `data/modal_queue/queue.json` - Thread-safe via `threading.Lock` — múltiples instancias comparten estado - Gallery dirs en `data/modal_queue/gallery_{gid}/` con `images/`, `masks/`, `regions/` - Status: `pending → processing → completed/failed` - Métodos: `add()`, `get()`, `mark_processing()`, `mark_completed()`, `mark_failed()`, `set_cbz_path()`, `set_priority()`, `reset_to_pending()`, `remove()`, `cleanup_completed()`, `get_pending()`, `get_incomplete()`, `get_failed()` ### `config.py` — TranslationConfig ```python @dataclass class TranslationConfig: api_key: str = "" api_base: str = "https://api.z.ai/api/coding/paas/v4" model: str = "GLM-5.1" target_lang: str = "ESP" config_path: str = "translate_config.json" ``` - `from_settings(settings)`: build desde `Settings` (usa `minimax_api_key` como fallback) ### `runner.py` — CLI Wrapper para manga-image-translator - `run_translator(input_dir, output_dir, config)`: Corre `python -m manga_translator local` - Parallel batch processing: divide imágenes en batches, corre concurrentemente - CPU optimization flags: `OMP_NUM_THREADS`, `MKL_NUM_THREADS`, etc. - Fallback: copia originales para páginas sin texto detectado - Workers: `min(max(cpu_count // 2, 3), 8)` ### `__init__.py` — translate_manga() - `translate_manga(cbz_path, config, output_dir=None) -> Path`: Alto nivel para CBZ - Extrae CBZ → corre translator → repack → devuelve path del CBZ traducido - On failure **RAISES** (no fallback al original) ### `_threadloop.py` — Thread-Local Event Loops - Cada thread worker tiene su propio event loop persistente (evita `asyncio.run()` y memory leaks) - `run_coro(coro)`: corre coroutine en el loop del thread actual - `get_translator()`: crea/obtiene MangaTranslator thread-local con HTTP client propio - Dummy 64x64 al init fuerza carga de modelos en el thread correcto - PyTorch model weights SHARED globalmente (solo HTTP client es per-instance) ### `api_fallback.py` — API Fallback y Model Scoring - Primary proxy: `http://127.0.0.1:6446/v1` (OpenAI-compatible local) - NSFW fallback: NVIDIA API con `minimaxai/minimax-m3` - Proxy health-check via `GET /v1/models` cada 30 segundos - **Model scoring**: success +0.1 (max 2.0); retryable errors -0.2 (min 0.1); hard errors -0.5 (min 0.1) con streak penalty - **Circuit breaker**: - `503` / service unavailable → banned 120s - `401` / unauthorized → banned 1800s (30 min — dead model) - `pick_model()` filtra modelos baneados - Default target: `nvidia/nemotron-3-ultra-550b-a55b:free` - Blocklist: `stepfun/step-3.7-flash:free` (returns empty/None) - Scores persisten en `data/model_scores.json` - `setup_api(pick_model())` setea `os.environ["OPENAI_MODEL"]` **antes** de cualquier refine call --- ## Configs de Traducción por Idioma | Config | Detector | Detection Size | Inpainter | Inpaint Size | Font Offset | Mask Dilation/Kernel | |--------|----------|---------------|-----------|-------------|-------------|---------------------| | `translate_config.json` (JP) | ctd | 2048 | default (AOT) | 1024 | +10 | 35/5 | | `translate_config_english.json` (EN) | default | 2048 | **lama_large** | 2048 | +10 | **55/8** | | `translate_config_korean.json` (KR) | default | 2048 | **lama_large** | 2048 | +10 | **55/8** | | `translate_config_chinese.json` (CN) | default | 2048 | **lama_large** | 2048 | +10 | **55/8** | - Todos usan renderer `manga2eng`, OCR `48px`, target `ESP`, translator `chatgpt` - EN/KR/CN requieren `lama_large` con dilation/kernel más alto (texto no-CJK ocupa más espacio) - JP usa detector CTD (entrenado en kanji); otros usan default (dbconvnext) --- ## Sistema de Colas ### UrlQueue (`src/url_queue.py`) — Cola del Bot - JSON-backed (thread-safe) - Persiste en `data/bot_queue.json` - Items clave: `gid`, `url`, `token`, `title`, `work_key`, `is_spanish`, `filecount`, `status` - Status: `pending → processing → completed/failed` - Crash recovery: items en "processing" al cargar disco se resetean a "pending" - Dedup via `work_key`: fingerprint canónico de `title_jpn` (tier 1) o `artist + clean_title` (tier 2) - `media_id_cache` separada (`data/media_id_cache.json`) mapea e-hentai gid → nhentai media_id - Funciones clave: `add()`, `get()`, `find_duplicate()`, `claim_next_pending()`, `mark_completed()`, `mark_failed()`, `reset_to_pending()`, `get_pending()`, `get_all()` - `build_work_key(tags, title, title_jpn) -> str`: Construye key canónico - `reload_from_disk()`: **module-level function** para recargar desde disco - `reload_from_disk()` en `queue.py`: Force-reload para scripts externos ### GalleryQueue (`src/translator/queue.py`) — Cola del Pipeline - JSON-backed singleton en `data/modal_queue/queue.json` con estado compartido module-level - Thread-safe via `threading.Lock` - Gallery dirs en `data/modal_queue/gallery_{gid}/` con `images/`, `masks/`, `regions/` - Diferencia clave con UrlQueue: esta es para el **pipeline de traducción local**, no para el bot - `QueuedGallery`: `gallery_id`, `title`, `source`, `num_pages`, `status`, `cbz_path`, `priority`, `language`, `token` --- ## Pipeline Orchestration (`src/pipeline.py`) - `run_pipeline_once(settings)`: Entry point único - Retry failures → preprocess → inpaint → render → MOBI → deliver - Llamado desde `cli.py worker` (loop) y `cli.py prioridad`/`manga` (inline) - Paralelismo: `min(max(cpu_count // 4, 3), 6)` galleries en paralelo - Timeout por gallery: `max(num_pages * 120, 1800)` segundos - Permanent error patterns (no retry): "opencv", "content filtered", "no images found" --- ## Scrapers ### nhentai (`src/nhentai_scraper/`) - Scrapling `StealthySession` para Cloudflare bypass - CDN download preferido: `i.nhentai.net/galleries/{media_id}/{page}.{ext}` (~0.1s/page) - e-hentai fallback para no-Spanish - Content filter: skip non-manga, non-JP source, <10 pages - Rate limiting + retry con exponential backoff - Funciones: `scrape_search`, `search_nhentai`, `_fetch_gallery_v2`, `should_download`, `fetch_gallery_links_httpx_async`, `safe_nh_api_get` **Dedup** (`src/nhentai_scraper/dedup.py`): - `select_winners(raw_galleries, state_db, notify_fn) -> DedupReport`: - Phase 1: Enrich each raw gallery with v2 metadata - Phase 1b: Bridge galleries without title_jpn via EN-title matching - Phase 2: Group by fingerprint - Phase 3: DB cross-reference + select winner per group - Winner selection: Spanish > English > Japanese > Chinese, then page count, then higher GID - `DedupReport`: `groups`, `no_fingerprint`, `winners`, `stats` - `format_dry_run_table(report)`: Render tabla legible para dry-run - `detect_language(tags, title_jpn)`: Detecta idioma via title markers (más confiable que tags) - Priority 1: title_jpn markers (`[スペイン翻訳]`, `[英訳]`, etc.) - Priority 2: tags (bare or prefixed) ### e-hentai (`src/hentai_scraper/` + `src/ehentai_search.py`) - API method: `gdata` (no legacy `gmetadata`) - Front page scrape para scheduler diario; metadata fetch via API - `search_spanish_version(title_jpn, artist_tag, title)`: Busca versión española en e-hentai - Step 1: Búsqueda específica (artist + title keywords + language:spanish) - Step 2: Fallback a búsqueda amplia (artist + language:spanish) + CJK title matching - Match por fingerprint de título (CJK o EN) - `scrape_front_page(category_mask, max_pages)`: Scrapea front page de e-hentai - `fetch_metadata_batch(galleries)`: Fetch metadata batch via API (25 por lote) ### EhentaiMangaScheduler (`src/ehentai_manga_scheduler.py`) Scheduler diario que reemplaza a `HentaiScheduler` + `NhentaiScheduler` legacy. ``` Front page e-hentai (Manga + Doujinshi) → Metadata batch API → Filter: only Manga/Doujinshi, not processed, content-safe → Dedup + fingerprint grouping (ESP > EN > JP > CN) → Por cada gallery: ├─ Spanish tag → download (nhentai CDN preferido, fallback e-hentai) → MOBI → upload └─ Not Spanish → search_spanish_version() ├─ Found Spanish → download esa versión → MOBI → upload └─ Not found → download de nhentai (o e-hentai) + GalleryQueue para traducción ``` - `_probe_nhentai_cdn(gid)`: Verifica si gallery existe en nhentai CDN - `_resolve_nhentai_media_id(gid, title)`: Resuelve media_id correcto (fast: gid, slow: v2 API search) - `_search_nhentai_v2(title)`: Busca en nhentai v2 API por título - `_download_and_upload(meta)`: Download + KCC + Nextcloud upload - `_download_and_translate(meta)`: Download + GalleryQueue encolado para traducción --- ## Fansub Pipeline (`src/fansub/`) Pipeline alternativo para nhentai galleries (independiente de `src/translator/`). ``` nhentai gallery → RT-DETR ONNX detection (class 0=bodies, 1=text, 2=frames) → MiniMax VLM OCR/translation → TELEA inpainting → Pillow typesetting → CBZ ``` - ONNX model en `/tmp/rtdetr/model.onnx` (descargado en Docker build) - Activado via `fansub_enabled=True` en `nhentai_scraper/scheduler.py` - Text regions grouped via Union-Find clustering (25px proximity) - Fallback font: `fonts/Anime_Ace_3.ttf` - `FansubPipeline` class en `__init__.py`, `run_fansub_pipeline()` en `pipeline.py` - Módulos: `detector.py`, `text_detector.py`, `translator.py`, `redrawer.py`, `typesetter.py`, `cleaner.py`, `packager.py`, `config.py` - Requirements propios: `src/fansub/requirements_fansub.txt` --- ## State Database (`src/base_state.py`) - SQLite via `BaseStateDB` — nunca escribir raw SQL fuera de este módulo - Subclasses: - `HentaiStateDB` — e-hentai gallery state - `NhentaiStateDB` — nhentai gallery state - `MangaStateDB` — manga (MangaWatcher) state - Status: `pending → downloading → downloaded → processing → completed → failed` - `find_by_fingerprint(fp)`: Cross-db dedup por fingerprint de title_jpn **Dual State Tracking (KNOWN ISSUE)**: Tanto SQLite (`NhentaiStateDB`/`HentaiStateDB`) como GalleryQueue/UrlQueue trackean estado de galleries SIN reconciliación cruzada. --- ## Delivery ### Nextcloud (`src/nextcloud_uploader.py`) ```python send_to_kindle(path, settings, subfolder) ``` - `sudo cp + chown www-data:www-data` al data dir de Nextcloud - `docker exec -u 33 nextcloud occ files:scan --path=...` para indexar - No WebDAV — copia directa al filesystem ### Local (`src/local_uploader.py`) ```python send_to_kindle(path, settings, subfolder) ``` - `shutil.copy2` a `output_dir/{subfolder}/` - Backend default cuando `delivery_backend=local` --- ## Notifications (`src/notifier.py`) - Protocol-based: `Notifier` Protocol class - Implementaciones: - `ConsoleNotifier` — log a stdout - `TelegramNotifier` — envía mensajes a Telegram - `NullNotifier` — no-op (default) - Registro global via `utils.set_notifier(notifier)` al startup - Uso: `utils.notify(message=...)` desde cualquier módulo --- ## GPU/Device Auto-Detection (`src/device.py`) ```python def get_device() -> str: # "cuda" | "mps" | "cpu" def prefer_gpu() -> bool: # get_device() != "cpu" ``` - No config, no env vars — detecta en runtime via `torch.cuda.is_available()` / `torch.backends.mps.is_available()` - Usado por: `_threadloop.py` (MangaTranslator `use_gpu`), `inpaint_local.py` (inpainting device), `artifacts.py`/`process.py` --- ## Utilidades Compartidas (`src/utils.py`) | Función | Descripción | |---------|-------------| | `set_notifier(notifier)` | Registra notificador activo | | `notify(message)` | Despacha mensaje via notificador activo | | `gallery_date_folder()` | Fecha UTC como `M_D_YYYY` (portable, sin zero-padding) | | `sanitize_filename(name, max_len)` | Sanitiza nombre de archivo (elimina chars inseguros) | | `format_gallery_name(lang, title, gid)` | Formato `[LANG] (title) (gallery_id)` | | `normalize_title_for_dedup(title)` | Normaliza título EN para dedup cross-source | | `normalize_jp_title_for_dedup(title_jpn)` | Normaliza título JP para dedup (strip brackets, chapter markers, etc.) | | `title_has_english(title)` | Check si título tiene `[English]` | | `title_has_spanish(title)` | Check si título tiene `[Spanish]` | | `format_summary(downloaded, failed)` | Formatea resumen de descargas | --- ## Otros Módulos ### `src/converter.py` Conversión de formatos: EPUB, PDF, CBR, CBZ, ZIP → Kindle format. - `is_image_pdf(src)`: Detecta si PDF es imagen vs texto - `convert(src, fmt)`: Detecta formato y convierte - `_fetch_metadata(stem)`: Fetch metadata via `fetch-ebook-metadata` - `_safe_extract(zf, dest)`: ZIP extraction segura (previene path traversal) ### `src/file_handler.py` - `download_file(update, tmp_dir)`: Descarga archivo de Telegram - `download_from_url(url, tmp_dir, timeout)`: Stream-download desde URL - `cleanup_tmp_dir(tmp_dir)`: Elimina directorio temporal ### `src/cleaner.py` - `clean_old_files(artifacts_dir, data_dir, max_age_hours, dry_run) -> CleanupStats`: - Elimina archivos >72h de `output/artifacts/` y `data/work_*` - Protege `bot_queue.json`, `manga_state.db`, `failed_galleries.json`, `model_scores.json`, `media_id_cache.json` - Soportado como script standalone: `python -m src.cleaner --hours 72 --dry-run` ### `src/libgen.py` Búsqueda en Library Genesis: - `search_books(query, by_author=False) -> list[Book]`: Busca y devuelve top 5 (EPUB first) - `resolve_download_url(book) -> str | None`: Resuelve URL de descarga directa - `Book`: id, md5, title, author, year, pages, language, extension, size, mirrors - Usa BeautifulSoup para parsear HTML de resultados ### `src/manga_watcher.py` - `MangaWatcher(config, bot_app)`: Vigila carpeta de Nextcloud para nuevos mangas - Polling loop cada `watch_interval` segundos - `_validate(filepath)`: Verifica extensión y contenido (imágenes dentro de ZIP/CBR) - `_translate_if_enabled(filepath)`: Traduce si `translation_enabled=True` - `_convert(input_path)`: KCC → MOBI - `_upload_to_nextcloud(output_dir, title)`: Upload a Nextcloud - Trackea estado via `MangaStateDB` ### `src/exceptions.py` ```python KindleError(Exception) # Base ├─ ConversionError # Conversión fallida ├─ UploadError # Upload fallido ├─ ScrapingError # Scraping/download fallido └─ TranslationError # Traducción fallida ``` --- ## Scripts (`scripts/`) | Script | Descripción | |--------|-------------| | `batch_convert_cbz.py` | Batch KCC conversion de CBZs en Nextcloud | | `download_type90.py` | Descargar TYPE.90 español, convertir, subir | | `llama_server.ps1` | PowerShell management de llama.cpp server local | | `test_pipeline_653011.py` | Test end-to-end nhentai pipeline | --- ## KCC Wrapper (`src/kcc.py`) - `kcc-c2e` en su propio grupo de procesos con timeout (**600s default**) - Si el proceso cuelga: `kill -- -$PGID` (Linux) o `taskkill /T /F` (Windows) - File-based completion detection (espera archivo `.mobi`, polling) - Flags: `--format MOBI --profile KPW34 --manga-style --stretch --splitter 2` --- ## Visual Feedback Loop (`visual_feedback.py`) - Vision LLM (xiaomi/mimo-v2.5) evalúa legibilidad del texto renderizado por burbuja - Verdictos: READABLE / TOO_SMALL / OVERFLOWING / EMPTY - Auto-ajusta `font_size_offset`, `font_size_minimum`, `font_size_maximum` - Sanity bounds: offset [-20, 50], minimum [6, 60], maximum [25, 100] --- ## Windows Encoding Set `PYTHONUTF8=1` o `sys.stdin.reconfigure(encoding='utf-8')` para evitar problemas cp1252. Sin esto, `asyncio.create_subprocess_exec` con `PIPE` puede corromper output de manga-translator en Windows. --- ## Known Bugs & Design Issues 1. `scraper.py` dead code: `fetch_gallery_links_httpx()` raises `NotImplementedError` — nunca se llama 2. `scraper.py` dead parameter: `should_download()` acepta `translation_enabled` pero nunca lo usa 3. `scraper.py` code duplication: `_fetch_gallery_httpx()` y `_fetch_gallery_sync()` tienen lógica de parsing casi idéntica 4. Dual state tracking: SQLite (`NhentaiStateDB`/`HentaiStateDB`) Y GalleryQueue/UrlQueue trackean estado de galleries sin reconciliación cruzada 5. `process.py` coexiste con `artifacts.py` — versión anterior del pipeline 6. `bot.py` (legacy, 26KB) coexiste con `bot_artifacts.py` (producción, ~30KB) 7. Hardcoded paths: `docker exec -u 33 nextcloud` en `nextcloud_uploader.py` 8. No centralized rate limiting: cada scraper maneja su propio rate limit 9. `mark_processing` en `base_state.py` NO incrementa `retry_count` (se hace por separado) 10. `url_queue.py` dead code: `_build_work_key_for_item` tiene un `return` inalcanzable después de un docstring duplicado de `reload_from_disk` 11. API REST no tiene autenticación por defecto en dev (`api_key` vacío = sin auth) 12. `_render_*.py` y `_smart_render_*.py` y `_vision_feedback*.py` son experimentales sin entry point — coexisten como archivos muertos en la raíz 13. `src/converter.py` usa `subprocess` para `ebook-convert` y `fetch-ebook-metadata` de Calibre — puede fallar si no está instalado --- ## What NOT to Add - No CI/CD pipelines, no pre-commit hooks, no linter/formatter configs - No WebDAV endpoints (replaced by local filesystem copy) - No new entry points without discussion