Files
worst_web/AGENTS.md
T
renato97 25449aa5df feat: worst-scan fansub web — initial release
Posts engine (SQLite + auto-publisher via poller), public feed with
clickable tags, reader, admin panel, submit/search/queue tools.
BFF proxy to pipeline API. Clean dark design. Docker-ready.
2026-07-23 15:58:28 -03:00

47 KiB

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] <url>
cli.py CLI tool (11 subcomandos) python cli.py <subcommand> [args]
inject_gallery.py Inyección directa al pipeline python inject_gallery.py <URL1> <URL2> ... 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

# /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 <url> 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

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

# 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

_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

@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)

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)

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)

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

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