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.
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)
Application.builder().token(settings.telegram_bot_token).build()- Registra CommandHandlers + MessageHandler
_post_initcorre AL ARRANCAR el bot:- Notifica "Bot encendido" a
ALLOWED_CHAT_ID - Lanza 6 workers (
_queue_worker) _drain_pending_on_startup(app)— recupera galleries pendientes/fallidas- Si
settings.api_enabled→ lanza_start_api_server() _periodic_cleanup_loop()— cleanup cada 24h_daily_nhentai_feed_loop(app)— feed diario a las 6AM ART (UTC-3)
- Notifica "Bot encendido" a
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:
- Si ya está
processingopending→ skip - Si ya completada (artifacts dir o status completed) → skip
- Si fallida antes → limpia work_dir, resetea a pending, reintenta
- 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 enpost_init- UrlQueue se persiste en
data/bot_queue.json(JSON) - Items en estado "processing" al reiniciar se resetean a "pending" (crash recovery)
failed_galleries.jsontambién se drena al arranquereload_from_disk()es función module-level enurl_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(StarletteBaseHTTPMiddleware) - Header:
X-API-Key - Bypass paths:
/api/v1/healthsiempre público - Bypass IPs:
127.0.0.1,::1,::ffff:127.0.0.1 - Dev mode: si
api_keyestá 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()conmax_tokens=14000
- El modelo real es el mismo que usa traducción (seteado vía
os.environ["OPENAI_MODEL"]) - El default
mimo-v2.5-freeen 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: Ajustadetection_size,text_threshold,box_threshold,unclip_ratiosegú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):
- Config adaptativa
- Detector
defaultcon thresholds bajos (0.15/0.2), 1536px ctdcon thresholds mínimos (0.1/0.15), 1024pxdefaultcon thresholds mínimos, 2048px
language.py — Detección de Idioma + Config Routing
detect_language(tags) -> str: Detecta idioma de tags (e-hentailanguage:japaneseo nhentaijapanese)select_config_path(language) -> str: Mapea idioma a archivo de configuraciónwrite_metadata(gallery_dir, gid, title, tags, source, language): Escribesource_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/statusy APIGET /statusclear_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}/conimages/,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 desdeSettings(usaminimax_api_keycomo fallback)
runner.py — CLI Wrapper para manga-image-translator
run_translator(input_dir, output_dir, config): Correpython -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 actualget_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/modelscada 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 120s401/ 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())seteaos.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, OCR48px, targetESP, translatorchatgpt - EN/KR/CN requieren
lama_largecon 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 detitle_jpn(tier 1) oartist + clean_title(tier 2) media_id_cacheseparada (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ónicoreload_from_disk(): module-level function para recargar desde discoreload_from_disk()enqueue.py: Force-reload para scripts externos
GalleryQueue (src/translator/queue.py) — Cola del Pipeline
- JSON-backed singleton en
data/modal_queue/queue.jsoncon estado compartido module-level - Thread-safe via
threading.Lock - Gallery dirs en
data/modal_queue/gallery_{gid}/conimages/,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) ycli.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
StealthySessionpara 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,statsformat_dry_run_table(report): Render tabla legible para dry-rundetect_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)
- Priority 1: title_jpn markers (
e-hentai (src/hentai_scraper/ + src/ehentai_search.py)
- API method:
gdata(no legacygmetadata) - 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-hentaifetch_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=Trueennhentai_scraper/scheduler.py - Text regions grouped via Union-Find clustering (25px proximity)
- Fallback font:
fonts/Anime_Ace_3.ttf FansubPipelineclass en__init__.py,run_fansub_pipeline()enpipeline.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 stateNhentaiStateDB— nhentai gallery stateMangaStateDB— 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-dataal data dir de Nextclouddocker 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.copy2aoutput_dir/{subfolder}/- Backend default cuando
delivery_backend=local
Notifications (src/notifier.py)
- Protocol-based:
NotifierProtocol class - Implementaciones:
ConsoleNotifier— log a stdoutTelegramNotifier— envía mensajes a TelegramNullNotifier— 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(MangaTranslatoruse_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 textoconvert(src, fmt): Detecta formato y convierte_fetch_metadata(stem): Fetch metadata viafetch-ebook-metadata_safe_extract(zf, dest): ZIP extraction segura (previene path traversal)
src/file_handler.py
download_file(update, tmp_dir): Descarga archivo de Telegramdownload_from_url(url, tmp_dir, timeout): Stream-download desde URLcleanup_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/ydata/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
- Elimina archivos >72h de
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 directaBook: 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_intervalsegundos _validate(filepath): Verifica extensión y contenido (imágenes dentro de ZIP/CBR)_translate_if_enabled(filepath): Traduce sitranslation_enabled=True_convert(input_path): KCC → MOBI_upload_to_nextcloud(output_dir, title): Upload a Nextcloud- Trackea estado via
MangaStateDB
- Polling loop cada
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-c2een su propio grupo de procesos con timeout (600s default)- Si el proceso cuelga:
kill -- -$PGID(Linux) otaskkill /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
scraper.pydead code:fetch_gallery_links_httpx()raisesNotImplementedError— nunca se llamascraper.pydead parameter:should_download()aceptatranslation_enabledpero nunca lo usascraper.pycode duplication:_fetch_gallery_httpx()y_fetch_gallery_sync()tienen lógica de parsing casi idéntica- Dual state tracking: SQLite (
NhentaiStateDB/HentaiStateDB) Y GalleryQueue/UrlQueue trackean estado de galleries sin reconciliación cruzada process.pycoexiste conartifacts.py— versión anterior del pipelinebot.py(legacy, 26KB) coexiste conbot_artifacts.py(producción, ~30KB)- Hardcoded paths:
docker exec -u 33 nextcloudennextcloud_uploader.py - No centralized rate limiting: cada scraper maneja su propio rate limit
mark_processingenbase_state.pyNO incrementaretry_count(se hace por separado)url_queue.pydead code:_build_work_key_for_itemtiene unreturninalcanzable después de un docstring duplicado dereload_from_disk- API REST no tiene autenticación por defecto en dev (
api_keyvacío = sin auth) _render_*.pyy_smart_render_*.pyy_vision_feedback*.pyson experimentales sin entry point — coexisten como archivos muertos en la raízsrc/converter.pyusasubprocessparaebook-convertyfetch-ebook-metadatade 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