openjutsu
One-click character swap for videos: an open-source, local Higgsfield Genjutsu alternative built on ComfyUI and MiniMax H3, with FLUX.3 Klein relighting.
Verificación asistida por IA de noticias sospechosas, con citas de fuentes de confianza y su historial de confiabilidad.
Translation pending
MediaPython · TypeScriptGitHub @SantiagoTyrsArvOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Plataforma de verificación asistida por IA de noticias sospechosas, con perfil histórico de confiabilidad de fuentes y autenticación de dos pasos (TOTP) opcional.
Principio de producto: la plataforma nunca declara una noticia "verdadera" o "falsa". Entrega
un reporte de credibilidad trazable: afirmaciones verificadas, fuentes concretas que las
respaldan/contradicen, y el historial de confiabilidad de cada fuente. Toda conclusión tiene citas
(URL + fragmento + fecha); sin citas, el veredicto es INSUFFICIENT.
cp .env.example .env # genera JWT_SECRET_KEY y TOTP_SECRET_ENCRYPTION_KEY (ver el archivo)
docker compose up --build -d # web, api, worker, postgres, redis
make seed # usuario demo con 2FA + casos de ejemplo ya procesados
make seed imprime el email y la contraseña del usuario demo, su secreto TOTP (agrégalo a Google
Authenticator/Authy, o genera el código con pyotp.TOTP(secreto).now()) y 10 códigos de
recuperación. Sin make (p. ej. en Windows sin GNU Make):
docker compose exec api uv run python -m app.cli.seed_demo. Luego:
/history.bbc.com y bbc.co.uk son una sola voz
independiente); el caso fallido muestra cómo se ve un error del pipeline./sources (público) lista la confiabilidad de las 16 fuentes de confianza./submit envía un caso real. Requiere ANTHROPIC_API_KEY en .env; sin ella el caso
recorre la cola y termina en failed en la extracción de afirmaciones, lo cual también se ve
en la interfaz.Detalles del seed (apps/api/app/cli/seed_demo.py): es idempotente (el secreto TOTP se conserva
entre ejecuciones; los códigos de recuperación se regeneran porque su texto plano nunca se guarda),
se niega a correr con ENVIRONMENT=production, marca todo el contenido con [DEMO] y cada
fragmento de evidencia como ilustrativo, y no actualiza la reputación de las fuentes — así los
datos inventados de la demo no contaminan los scores reales.
flowchart LR
U[Navegador] -->|páginas| W[web<br/>Next.js 16]
U -->|fetch con cookies httpOnly + CSRF| A[api<br/>FastAPI]
A -->|SQLAlchemy async| P[(postgres)]
A -->|rate limit + encolar job| R[(redis)]
R -->|run_pipeline| K[worker<br/>ARQ]
K --> P
K -->|trafilatura / yt-dlp + faster-whisper| I[Contenido externo<br/>URL o video]
K -->|messages.parse + web_search<br/>allowed_domains = fuentes| C[Claude API]
| Componente | Responsabilidad |
|---|---|
apps/web | UI en Next.js 16 (App Router, componentes cliente). proxy.ts solo redirige a /login cuando falta la cookie; la API es el único límite de autorización. lib/api.ts renueva la sesión ante un 401. |
apps/api — auth | Registro, login con verificación en dos pasos TOTP opcional (se activa en Ajustes), sesión con access + refresh rotativo, CSRF de doble envío, lockout y rate limiting. |
apps/api — submissions | Crea casos y los encola; estado consultable por polling. |
apps/api — pipeline | Worker: ingest → transcribe → claims → verify → scoring, con validación de cada cita contra los resultados reales de la búsqueda y la lista blanca de dominios. |
apps/api — sources | Fuentes de confianza y su reputación Beta-Bernoulli (docs/reputation.md). |
apps/api — reports | Arma el reporte de solo lectura a partir de lo persistido (sin llamadas al LLM). |
apps/api — cli | seed_demo para la demo. |
Flujo de un caso: POST /submissions → fila queued + job en Redis → el worker avanza el
estado (ingesting → transcribing → extracting → verifying → scoring → done|failed) → la página
/reports/[id] consulta GET /submissions/{id} cada 2 s y, al llegar a done, pide
GET /reports/{id}.
Modelo de confianza respecto al LLM: el modelo propone veredictos y citas, pero ninguna cita
llega al reporte sin pasar dos chequeos deterministas (URL presente en los resultados reales de
web_search de esa misma respuesta, y dominio en la lista blanca). Sin citas válidas el veredicto
se fuerza a INSUFFICIENT. El contenido del usuario viaja delimitado y marcado como no confiable.
| Método y ruta | Auth | Descripción |
|---|---|---|
POST /auth/register | — | Crea la cuenta (argon2id). |
POST /auth/login | — | Sin 2FA emite la sesión (status: "authenticated"); con 2FA devuelve un token limitado mfa_pending (status: "mfa_required"). |
POST /auth/2fa/setup · /auth/2fa/confirm | sesión + CSRF | Activa (o reconfigura) TOTP y genera códigos de recuperación. |
POST /auth/2fa/disable | sesión + CSRF + código | Desactiva 2FA con un código TOTP o de recuperación. |
POST /auth/2fa/verify | token limitado | Emite la sesión completa (cookies). |
POST /auth/refresh · /auth/logout | cookie + CSRF | Rota o revoca la sesión. |
GET /auth/me | sesión | Usuario actual. |
POST /submissions | sesión + CSRF | Crea y encola un caso. |
GET /submissions · /submissions/{id} | sesión | Historial y estado (solo casos propios). |
GET /reports/{id} | sesión | Reporte de un caso done propio (409 si no terminó). |
GET /sources · /sources/{domain} | — | Confiabilidad pública de fuentes. |
GET /health · /health/ready | — | Liveness y readiness (DB + Redis). |
Documentación interactiva completa en http://localhost:8000/docs.
Fase 1 — Monorepo apps/web (Next.js 16 App Router + TypeScript + Tailwind + shadcn/ui) y
apps/api (FastAPI + SQLAlchemy 2 async + Alembic). docker-compose.yml con web, api,
worker (ARQ), postgres y redis. Health checks, configuración por variables de entorno,
logging estructurado con redacción de secretos, CORS restringido. Linters: ruff (API) y
eslint (web). Pruebas: pytest (API).
Fase 2 — Registro (argon2id, validación de fortaleza de contraseña) y login que nunca emite
una sesión completa solo con contraseña: siempre devuelve un token de alcance limitado
(mfa_setup_pending o mfa_pending, 5 min). Enrolamiento TOTP (pyotp, secreto cifrado en
reposo con Fernet, QR en base64) con 10 códigos de recuperación de un solo uso. Verificación TOTP
con ventana ±1 y protección contra reutilización de código. Sesión completa (access 15 min +
refresh rotativo) solo tras /auth/2fa/verify, en cookies httpOnly/SameSite=Lax (Secure se activa
automáticamente con ENVIRONMENT=production), con protección CSRF de doble envío para
/auth/refresh y /auth/logout. Detección de reutilización de refresh token (revoca toda la
familia de tokens). Rate limiting y bloqueo temporal tras 5 intentos fallidos. Páginas
/register, /login, /login/2fa (QR + códigos de recuperación) y /settings/security
(reenrolamiento), protegidas por proxy.ts (Next.js 16 renombró middleware.ts a proxy.ts).
Fase 3 — POST /submissions (texto, URL o video) encola el caso en ARQ; GET /submissions/{id}
y GET /submissions para consultar estado/listado (pensado para polling cada 2s desde el
frontend). El worker corre ingesting → (transcribing) → extracting → verifying → scoring → done:
trafilatura para URLs, yt-dlp + faster-whisper (interfaz Transcriber
intercambiable) para video, con límite de duración validado antes de descargar.client.messages.parse con output_format Pydantic — JSON garantizado por
schema) extrae hasta 8 afirmaciones factuales autocontenidas, descartando opiniones/predicciones;
reintenta hasta 3 veces.La verificación en dos pasos es opcional: una cuenta sin 2FA entra solo con la contraseña, y la
interfaz le recuerda activarla (aviso en /history y /submit). La garantía es: una cuenta con
2FA activada no puede abrir sesión solo con la contraseña (el token mfa_pending no da acceso a
ninguna ruta protegida).
POST /auth/register con email/password.POST /auth/login → {"status": "authenticated", ...} y Set-Cookie con access_token,
refresh_token y csrf_token.POST /auth/2fa/setup con esas cookies y X-CSRF-Token: <csrf_token> → devuelve otpauth_uri
y qr_code_base64. Puedes decodificar el QR o extraer el parámetro secret de la URI y generar
el código con cualquier librería TOTP (p. ej. pyotp.TOTP(secret).now()).POST /auth/2fa/confirm (cookies + CSRF) con {"code": "<código de 6 dígitos>"} → activa 2FA
y devuelve 10 recovery_codes (solo se muestran una vez).POST /auth/login devuelve {"status": "mfa_required", "token": ...}. Envía
POST /auth/2fa/verify con Authorization: Bearer <token> y {"code": "..."} o
{"recovery_code": "..."} para obtener la sesión.POST /auth/2fa/disable (cookies + CSRF) con {"code": "..."} o {"recovery_code": "..."}
desactiva 2FA y borra el secreto y los códigos de recuperación./settings/security activa (QR + códigos de recuperación con descarga),
reconfigura o desactiva la 2FA; con ella activa, /login pasa por /login/2fa.cp .env.example .env
# Edita .env: como mínimo, genera JWT_SECRET_KEY y TOTP_SECRET_ENCRYPTION_KEY (instrucciones
# dentro del archivo). Sin ANTHROPIC_API_KEY el pipeline de verificación (Fase 3) encola y corre,
# pero cada caso termina en status=failed al llegar al paso de extracción de afirmaciones.
docker compose up --build
make seed # opcional: usuario demo + casos de ejemplo (ver "Demo en 5 minutos")
El despliegue necesita cinco servicios: web, api, worker, PostgreSQL y Redis. Railway no
ejecuta este docker-compose.yml directamente; crea cada servicio desde el mismo repositorio y usa
las bases de datos gestionadas de Railway.
Postgres y
Redis para que las referencias de variables del ejemplo se resuelvan.api desde el repositorio con directorio raíz apps/api y el archivo de configuración
/railway.toml. Este ejecuta las migraciones antes de arrancar y comprueba /health/ready.worker desde el mismo repositorio y directorio raíz apps/api; selecciona
/apps/api/railway.worker.toml como archivo de configuración. El worker consume la cola Redis y
procesa los casos.web desde el repositorio con directorio raíz apps/web y su Dockerfile.web (por ejemplo, app.example.com) y otro a api
(api.example.com). Deben compartir el mismo dominio raíz para que el navegador envíe las
cookies de sesión SameSite=Lax en las llamadas de la web a la API. Los dominios
*.up.railway.app no sirven para esto: up.railway.app es un sufijo público y el navegador
no comparte cookies entre sus subdominios.Configura estas variables en api y worker (puedes compartir las variables comunes del proyecto):
ENVIRONMENT=production
DATABASE_URL=${{Postgres.DATABASE_URL}}
REDIS_URL=${{Redis.REDIS_URL}}
JWT_SECRET_KEY=<secreto aleatorio de al menos 48 bytes>
TOTP_SECRET_ENCRYPTION_KEY=<clave Fernet generada para este entorno>
ANTHROPIC_API_KEY=<clave de Anthropic>
ANTHROPIC_MODEL=claude-sonnet-5
CORS_ORIGINS=["https://app.example.com"]
COOKIE_DOMAIN=.example.com
COOKIE_DOMAIN es obligatorio cuando web y API están en subdominios distintos: sin él, las cookies
quedan atadas a api.example.com, la web no puede leer csrf_token (todo POST falla con 403) y
proxy.ts no ve la sesión, así que cada página protegida redirige a /login.
La API arranca con el CMD del Dockerfile, que escucha en el $PORT que inyecta Railway. Para que
el rate limiting y el bloqueo por intentos usen la IP real del cliente, configura
TRUSTED_PROXY_HOPS=1 (solo el proxy de Railway). La API toma la IP contando proxies desde la derecha
de X-Forwarded-For, así un cliente no puede falsificarla añadiendo entradas a la cabecera.
En web, configura NEXT_PUBLIC_API_URL=https://api.example.com. Es una variable de build y debe
estar definida antes de desplegar la web. La API normaliza las URLs PostgreSQL postgres:// y
postgresql:// del proveedor al driver asyncpg requerido por la aplicación.
Si la web está en *.vercel.app y la API en *.up.railway.app, no comparten dominio y las cookies
no llegarían. En ese caso la web hace de proxy de la API en /api, así las cookies quedan en el
dominio de la web:
apps/web): NEXT_PUBLIC_API_URL=/api (se lee al compilar),
API_PROXY_TARGET=https://<api>.up.railway.app y API_PROXY_SECRET=<secreto aleatorio>.api: COOKIE_PATH_PREFIX=/api, COOKIE_DOMAIN vacío,
CORS_ORIGINS=["https://<web>.vercel.app"], TRUSTED_PROXY_HOPS=1 y el mismo
API_PROXY_SECRET.El proxy de /api está en apps/web/proxy.ts: reenvía la petición y añade la IP del visitante
(X-Client-IP, tomada de las cabeceras que fija el edge de Vercel) junto con X-Proxy-Secret. La
API solo acepta esa IP si el secreto coincide; quien llame directamente a *.up.railway.app queda
identificado por la IP que vio Railway, así que no puede elegir una IP falsa para saltarse el rate
limiting. Este esquema confía en el edge de Vercel: si alojas la web en otro sitio, asegúrate de que
su proxy sobrescriba X-Real-IP/X-Forwarded-For antes de usar API_PROXY_SECRET.
Genera los secretos con los comandos documentados en .env.example; no reutilices
claves entre desarrollo y producción. El procesamiento con Claude requiere una clave con acceso a
messages.parse y a la herramienta de búsqueda web. Configura recursos suficientes para el worker:
la transcripción de video usa FFmpeg y descarga el modelo Whisper al procesar videos.
Tras el primer despliegue, comprueba https://api.example.com/health/ready, abre la web y completa
un registro/login con TOTP. Envía un texto de prueba y revisa que el worker lo lleve a done; un
estado failed suele indicar una clave/modelo de Anthropic inválido o un error de acceso al
servicio externo.
Para la demo en producción, siembra el usuario demo a propósito con
railway ssh -s <api> -- uv run python -m app.cli.seed_demo --production: genera una contraseña
aleatoria (distinta en cada ejecución; la fija del repo nunca se usa en producción) y la imprime una
sola vez junto al secreto TOTP. make seed sin --production se niega a correr ahí.
cd apps/api
uv sync
uv run pytest
uv run ruff check .
cd apps/web
pnpm install
pnpm dev
pnpm run lint
Ver .env.example en la raíz — cada variable está documentada ahí mismo. Nunca commitees el
archivo .env real.
El índice está en docs/README.md. Para retomar el trabajo, empieza por docs/ESTADO.md: estado actual, historial de PR y pendientes. La auditoría técnica está en docs/AUDITORIA.md y el análisis de seguridad en docs/SEGURIDAD.md.
Lista consolidada, agrupada por la fase en que se introdujo cada componente.
app/data/trusted_sources.json). En desarrollo todo se sirve
por HTTP; la bandera Secure de las cookies depende de ENVIRONMENT=production y de un proxy
TLS delante, que no forma parte de este docker-compose.yml.423 en vez de 401 cuando el email existe y está
bloqueado, lo cual filtra mínimamente si un email está registrado (trade-off común de los
esquemas de lockout; no hay mitigación adicional en el MVP). /2fa/confirm no rota
totp_last_used_step a propósito (ver comentarios en service.py), así que el mismo código
puede reutilizarse entre /2fa/confirm y el siguiente /2fa/verify inmediato — la reutilización
sí se bloquea a partir de ahí.ANTHROPIC_API_KEY configurada, cualquier caso termina en status=failed en el paso
de extracción de afirmaciones (validado deliberadamente así en el stack real — ver historial de
build). El campo evidence.published_at es texto libre (page_age de la búsqueda, p. ej.
"April 30, 2025"), no una fecha normalizada. La ruta de video (yt-dlp + faster-whisper) está
cubierta por pruebas unitarias con mocks, pero no se probó con una descarga/transcripción real
contra el stack en Docker (sí se probaron text y url de punta a punta). Ingesta de URL/video protegida
contra SSRF (pipeline/url_safety.py): se valida el esquema y se resuelve el host antes de
descargar, rechazando loopback/red privada/link-local (incluye endpoints de metadata cloud como
169.254.169.254); video además restringido a una lista blanca de plataformas conocidas
(ALLOWED_VIDEO_HOSTS). Validado en vivo contra el stack en Docker apuntando a un servicio
interno (postgres:5432), un endpoint de metadata y localhost — los tres se bloquean antes de
hacer ninguna petición. Las páginas se descargan con fetch_public_page: cada redirección se
revalida (hasta 5) y la conexión va a la misma IP que se validó (el Host y el SNI llevan el
dominio, así que el certificado se sigue verificando), lo que también cierra el DNS rebinding;
trafilatura solo extrae el texto. Limitación que queda: yt-dlp sigue sus propias
redirecciones; lo acota la lista blanca de plataformas de video.web_search
(allowed_domains = dominios de sources, sin dynamic filtering) investiga y devuelve
veredicto + evidencias en JSON. Nunca se confía en el JSON del modelo a ciegas: cada URL de
evidencia se valida contra los resultados reales que la herramienta de búsqueda devolvió en esa
misma respuesta (bloques web_search_tool_result) y contra la lista blanca de dominios; toda
evidencia que no pase ambos chequeos se descarta, y si no queda ninguna evidencia válida el
veredicto se fuerza a INSUFFICIENT. Verificaciones en paralelo (asyncio.Semaphore) con
timeout por afirmación; un fallo (timeout, error de API, JSON inválido tras reintentos) marca
esa afirmación como INSUFFICIENT sin tumbar el resto del caso.<document>/<claim>) con instrucción explícita de ignorar cualquier orden
contenida ahí.sources/seed_loader.py carga trusted_sources.json en la tabla sources de forma idempotente
(se ejecuta al arrancar el worker).Fase 4 — El paso scoring del pipeline ahora actualiza reputación real
(sources/reputation.py, ver la fórmula completa y sus limitaciones en docs/reputation.md):
modelo Beta-Bernoulli (score = alpha/(alpha+beta)), consenso ponderado entre fuentes (peso =
score actual con piso de 0.05), deduplicación de fuentes sindicadas (syndication_group) para
que no infle el conteo de independencia ni el voto de consenso, mínimo 2 fuentes independientes
para puntuar, e idempotencia garantizada por una restricción única (source_id, claim_id) en
source_score_events. GET /sources y GET /sources/{domain} (públicos, sin autenticación)
exponen score, cases_count, bandera low_sample (<10 casos), intervalo aproximado (aproximación
normal a la Beta) e historial reciente de eventos.
Fase 5 — GET /reports/{submission_id} (módulo reports, solo el dueño del caso; un caso
ajeno responde 404 igual que uno inexistente, y uno sin terminar responde 409) arma el
reporte de credibilidad a partir de lo que el pipeline ya persistió — no hace llamadas al LLM:
resumen de veredictos, cada afirmación con su razonamiento y su evidencia (URL + fragmento +
fecha + postura), y para cada evidencia el score de la fuente, cases_count y low_sample.
Incluye siempre un descargo que recuerda que el reporte no declara nada verdadero ni falso.
GET /submissions ahora también devuelve raw_input (para el historial). Frontend:
/submit (texto, URL o video), /reports/[id] (polling cada 2s con barra de progreso por
etapa del pipeline y luego el reporte con citas enlazadas), /history (casos del usuario),
/sources (listado público de confiabilidad). Tras el 2FA se redirige a /history.
apiFetch renueva la sesión automáticamente ante un 401 (una sola llamada a /auth/refresh
compartida entre peticiones concurrentes para no reutilizar el refresh token rotativo, y
reenviando el token CSRF nuevo en el reintento).
Fase 6 — make seed real (python -m app.cli.seed_demo, dentro del paquete de la API para
que exista en la imagen de Docker — el placeholder anterior en scripts/ quedaba fuera del
contexto de build y no podía ejecutarse en el contenedor). Arquitectura, diagrama, tabla de
endpoints y guía de demo en este README. Corrección tipográfica: --font-sans se referenciaba a
sí misma en globals.css, así que Geist nunca se aplicaba y el navegador caía en una fuente
serif. Recorrido de punta a punta validado en navegador contra el stack en Docker (login + TOTP
→ historial → reporte → caso fallido → fuentes → envío real).
docs/reputation.mdmissdone; un caso failed muestra el error
pero no un reporte parcial, aunque algunas afirmaciones se hubieran verificado. Las páginas de
casos son componentes cliente que consultan la API desde el navegador (sin render en servidor).
No hay exportación del reporte (PDF/Markdown).--production es aleatoria y rota en cada ejecución.RATE_LIMIT_SUBMISSIONS_PER_MINUTE,
MAX_SUBMISSIONS_PER_DAY, MAX_ACTIVE_SUBMISSIONS_PER_USER). No sustituye un límite de gasto en
la consola de Anthropic, que sigue siendo necesario.VIDEO_INGEST_ENABLED=false (y NEXT_PUBLIC_VIDEO_INGEST_ENABLED=false
en la web) mientras no se haya probado con los recursos reales del worker.