TOXI MEDIA — Wiki

interno · solo lectura

Stack tecnológico

HerramientaUso
ViteBundler y dev server. Multi-entrada (una por cada HTML).
Three.jsEscena 3D en la entrada principal (index.html).
Mercado Pago SDKPagos de entradas. Cargado en index.html vía CDN.
Vite PWAService worker e instalación como app. Estrategia: injectManifest.
VercelHosting, routing y funciones serverless (api/).
MuxVideo streaming y gestión de assets de video.
ElevenLabsText-to-Speech del AI voice en plantform.
Google GeminiIA de voz en plantform.
ZohoEmail de notificaciones transaccionales.
💻

Dev local

Instalar dependencias
npm install
Correr el servidor
npm run dev

Corre en http://localhost:5000. También accesible desde la red local.

Para abrir una página específica directamente
npx vite --port 5173 --open wiki.html
🚀

Build & Deploy

El deploy automático ocurre en cada git push a main. Vercel detecta el push y hace el build solo.

Deploy manual

npm run build   # genera /dist
vercel --prod   # deploy inmediato

Regenerar páginas estáticas (SEO)

Después de modificar un proyecto en projects.json:

npm run gen
# equivale a: node generate-static.js
# genera public/[slug]/index.html con OG tags por cada proyecto
🗺

Routing

El routing se define en vercel.json. Hay un catch-all al final que manda cualquier slug a plantform.html.

Regla crítica: cualquier ruta nueva debe ir en vercel.json antes del catch-all /:slug([a-z0-9][a-z0-9-]*). Si va después, la absorbe plantform.

URLArchivo
/index.html — entrada 3D + paywall
/audiontaudiont.html
/trabajostrabajos.html
/tvtv.html
/turingturing.html
/nota-constantenota-constante.html
/otro-dia-en-la-redotro-dia-en-la-red.html
/wikiwiki.html
/:slugplantform.html — proyectos y talentos dinámicos
🔑

Variables de entorno

Truco clave: nunca usar echo ni Write-Output para pasar el valor — agregan un newline que corrompe la clave. Siempre usar node -e "process.stdout.write(...)".

# Agregar variable (sin newline al final)
node -e "process.stdout.write('EL_VALOR')" | npx vercel env add NOMBRE_VAR production

# Eliminar variable
node -e "process.stdout.write('y')" | npx vercel env rm NOMBRE_VAR production

# Listar variables actuales
npx vercel env ls

# Redesplegar después de cualquier cambio
npx vercel --prod

Variables activas

VariableDescripción
GEMINI_API_KEYGoogle Gemini — IA de voz en plantform
ELEVENLABS_API_KEYElevenLabs — TTS en plantform
ELEVENLABS_VOICE_IDID de voz ElevenLabs en plantform
MP_ACCESS_TOKENMercadoPago — token de producción (backend)
VITE_MP_PUBLIC_KEYMercadoPago — public key para el frontend
ENV_MP_PUBLIC_KEYMercadoPago — public key para serverless
ZOHO_EMAILEmail Zoho para notificaciones
ZOHO_PASSWORDContraseña Zoho
MUX_TOKEN_IDMux — token ID
MUX_TOKEN_SECRETMux — token secret

Agregar proyecto

Preparar imágenes

Crear public/[slug]/ y copiar:

  • thumb.jpg — 1280×720
  • poster.jpg — 1920×1080

También se puede usar URLs externas (CDN, Webflow) — poner la URL completa en el JSON.

Agregar entrada en public/data/projects.json
{
  "name":        "Nombre del Proyecto",
  "slug":        "nombre-del-proyecto",
  "description": "Descripción corta (cards + SEO).",
  "contenido":   "<p>Cuerpo HTML. Puede tener iframes.</p>",
  "card":        "/nombre-del-proyecto/thumb.jpg",
  "banner":      "/nombre-del-proyecto/poster.jpg",
  "thumbnail":   "/nombre-del-proyecto/thumb.jpg",
  "talentos":    ["slug-talento-1"],
  "formato":     "1",
  "credits":     "<h4>Dirección</h4><p>Nombre Apellido</p>",
  "web":         "https://url-externa.com"
}
Regenerar meta tags SEO
npm run gen

Genera public/[slug]/index.html con OG tags por proyecto.

Commit y push
git add public/data/projects.json public/[slug]/
git commit -m "feat: agregar [Nombre del proyecto]"
git push

IDs de formato

1 — Cine 2 — Podcast 3 — Foto 4 — Texto 5 — VR 6 — Eventos 7 — Software 8 — Música
👤

Agregar talento

Los talentos se cargan desde public/data/talentos.json. El slug debe existir ahí antes de referenciarlo en un proyecto.

{
  "name":  "Nombre Apellido",
  "slug":  "nombre-apellido",
  "rol":   "Director / Editor / ...",
  "bio":   "Bio corta.",
  "foto":  "/nombre-apellido/foto.jpg"
}
📄

Agregar página HTML nueva

Crear el archivo en la raíz
MUNDO-TOXI/nombre-pagina.html
Registrar en vite.config.js

Sin esto, Vite no lo buildea y no existe en producción.

// vite.config.js → build.rollupOptions.input
nombrePagina: resolve(__dirname, 'nombre-pagina.html'),
Agregar rewrite en vercel.json

Tiene que ir antes del catch-all /:slug.

{ "source": "/nombre-pagina", "destination": "/nombre-pagina.html" }
Deploy
vercel --prod
📚

Catálogo de proyectos

Todos los proyectos en public/data/projects.json. Accesibles en toxi.media/[slug].

Pizza & Pagni
/pizza-y-pagni  evento
Hedonismo y Seducción
/hedonismo-y-seduccion  cine
El Jardín de Andarillo
/el-jardin-de-andarillo  VR
Las Catadoras del Führer
/las-catadoras-del-fuhrer  cine
Desarrollos Regenerativos
/desarrollos-regenerativos  cine
Audion't
/audiont  software
Polo IA
/polo-ia  música
¿Para qué sirve todo esto?
/para-que-sirve-todo-esto  cine
Mauda
/mauda  cine
Mundo PEGSA
/mundo-pegsa  VR
Fancy Dogs™
/fancydogs  evento
Patent Quest
/patent-quest  VR
TOXIDESK
/toxidesk  software
Juana la Loca
/juana-la-loca  VR
Las formas del laberinto
/las-formas-del-laberinto  VR
TOXI: Una serie para gente cansada
/toxi-una-serie...  cine
TOXI VR
/toxi-vr  VR
Otro Día en la Red
/otro-dia-en-la-red  cine
Escrol
/escrol  cine
TOXI Fest
/toxi-fest  evento
KABRADEPATA
/kabradepata  cine
Xplora
/xplora  cine
Mamarracho
/mamarracho  evento
American Anthology
/american-anthology  foto
📺

Toxi TV — web

El canal en vivo de toxi.media/tv. Todos los usuarios ven lo mismo en el mismo momento, sin señal en tiempo real.

Cómo funciona la sincronización

  • El video actual se calcula usando EPOCH_SEC = 2026-01-01T00:00:00Z como referencia.
  • Se suma el tiempo desde el epoch y se divide entre la duración total de la playlist para encontrar el slot actual.
  • Cada cliente lo calcula de forma independiente — no hay WebSocket ni señal en tiempo real.

Actualizar la playlist

Hay que editar dos archivos en paralelo:

  1. src/content.js — agregar entrada con onTV: true (para la web).
  2. api/playlist.mjs — agregar el mismo item al array PLAYLIST (para la app Android y el AI voice).

api/ai-voice.mjs importa PLAYLIST directamente desde api/playlist.mjs — no hay que tocar ai-voice.mjs al agregar contenido.

Endpoint

GET https://toxi.media/api/playlist
# devuelve: [{ "id": "MuxPlaybackID", "duration": 339.5, "title": "Título" }, ...]
🤖

App Android TV

App para Android TV / Google TV. Reproduce el canal sincronizado, igual que la versión web.

Estructura

toxi-tv-android/
  app/src/main/java/media/toxi/tv/
    MainActivity.kt   # ExoPlayer + fetch de playlist al arrancar
    Playlist.kt       # Playlist hardcodeada + lógica de slot sincronizado
  app/src/main/res/
    drawable/banner.png   # Banner launcher TV (320×180)
  app/build.gradle.kts

Cómo funciona

  • Al arrancar busca https://toxi.media/api/playlist y reemplaza la hardcodeada.
  • Si falla el fetch, usa HARDCODED_PLAYLIST en Playlist.kt.
  • Cada 30 segundos corrige el drift — si la diferencia es mayor de 5 segundos, hace seek.
  • Los cambios en la playlist se propagan sin actualizar el APK.

Build y distribución

Abrir toxi-tv-android/ en Android Studio.
Build → Generate Signed Bundle / APK → Android App Bundle.
Subir el .aab a Play Console → Toxi Media TV.
📡

Streaming local con Icecast

Tres piezas: PC (fuente de audio) → BUTT (encoder) → Icecast (servidor).

1 — Icecast (Windows)

  • Descargar de icecast.org e instalar.
  • Abrir icecast.xml y agregar CORS (necesario para la onda visual):
<http-headers>
    <header name="Access-Control-Allow-Origin" value="*" />
</http-headers>

2 — BUTT (Broadcast Using This Tool)

Descargar de danielnoethen.de/butt. Settings → Main → Server → Add:

CampoValor
TypeIcecast
Addresslocalhost
Port8000
Usersource (no "admin")
Passwordhackme (default — cambiar en xml)
Mountpoint/stream

3 — Conectar en el código

// src/line.js
const STREAM_URL = 'http://localhost:8000/stream';

Troubleshooting

ProblemaSolución
No entra a localhost:8000La ventana de consola de Icecast debe estar abierta. Probar 127.0.0.1:8000.
No se escucha nadaVerificar que BUTT muestre tiempo corriendo. Abrir la URL del stream directo en el browser.
Onda visual planaProblema de CORS. Revisar el icecast.xml del paso 1.
Error Mixed ContentSi la web está en https://, el stream no puede ser http://. Ver la guía de streaming a internet.
🌐

Streaming a internet (HTTPS)

El problema: el sitio usa HTTPS (Vercel). Icecast local usa HTTP. Los browsers bloquean audio HTTP dentro de páginas HTTPS — "Mixed Content".

Solución recomendada: Zeno.fm (gratis)

Crear estación en zeno.fm/broadcasters.

En el panel obtener: Server Address, Port, Mount Point, Password.

Configurar BUTT con los datos de Zeno.

Reemplazar localhost por stream.zeno.fm y el resto de los datos del panel.

Actualizar STREAM_URL con la URL HTTPS que da Zeno:
const STREAM_URL = 'https://stream.zeno.fm/tu-codigo';

Alternativa temporal: Ngrok

Para testear con alguien sin registrarse. La URL cambia en cada reinicio.

ngrok http 8000
# usar la URL https://xyz.ngrok-free.app/stream
🗂

Estructura de archivos

MUNDO-TOXI/
  index.html          # Entrada principal (keypad + paywall + Three.js)
  plantform.html      # Página dinámica de proyectos y talentos
  tv.html             # Canal en vivo sincronizado
  audiont.html        # Landing de Audion't
  wiki.html           # Este wiki
  [proyecto].html     # Páginas estáticas por proyecto especial

  src/
    main.js           # Escena Three.js
    paywall.js        # Lógica de pagos y acceso
    style.css         # Estilos globales
    content.js        # Contenido de TV (items con onTV: true)

  public/
    data/
      projects.json   # Catálogo completo de proyectos
      talentos.json   # Catálogo de talentos
      formatos.json   # Categorías (Cine, VR, Software, etc.)
    [slug]/           # Imágenes y index.html SEO por proyecto

  api/
    playlist.mjs      # Playlist de TV para app Android y AI voice
    ai-voice.mjs      # AI voice — importa playlist.mjs directo
    transcribe.mjs    # Transcripción de audio (maxDuration: 60s)

  tools/
    upload-to-mux.js          # Subir video a Mux
    upload-downloads-batch.js # Upload en batch

  toxi-tv-android/    # App Android TV (Kotlin + ExoPlayer)
  _archive/           # Archivos viejos preservados

  vite.config.js      # Bundler: entradas HTML + PWA config
  vercel.json         # Routing, rewrites, funciones serverless
  generate-static.js  # Genera meta tags SEO por proyecto
📁

Assets públicos

Todo lo que está en public/ se sirve desde la raíz del sitio en dev y en producción. No hace falta importarlos con JS.

public/hedonismo-y-seduccion/thumb.jpg
# accesible en: /hedonismo-y-seduccion/thumb.jpg

Vite no procesa los archivos de public/ — se copian tal cual al dist/. Ideal para imágenes grandes, audio y videos que no necesitan hashing de nombre.

🔍

SEO y meta tags

Las páginas de proyectos en plantform.html son SPA (JS renderiza el contenido). Para que Google y WhatsApp lean los meta tags correctos, se genera un index.html estático por proyecto.

Generar

npm run gen
# genera public/[slug]/index.html con:
#   <title> del proyecto
#   og:title, og:description, og:image
#   twitter:card

Vercel sirve ese HTML estático cuando Google o un scraper accede a /[slug]. El browser lo recibe, carga el JS de plantform y renderiza la página completa.

Siempre correr npm run gen después de modificar name, description, card o banner en projects.json.

👥

Equipo humano

Core — decisiones estratégicas

NombreRol
Juan Manuel CafferataCo-fundador · dueño de la marca · decisiones estratégicas
Fabrizio SanguinettiCo-fundador · dueño de la marca · decisiones estratégicas
Sofía Nicola PatrónCapitalista · socia

Líderes de área

NombreÁrea
Fermín DelíaVR — producción
Julián ToninelloVR — exhibición
Juan Segundo QuirogaSeries — líder
Nicolás MartorellProgramación — líder

Pasantes

NombreÁreaTipo
Luis Fernando Guerra VelardeVRPasante
Martín PolloVRPasante
Ximena ColombresPeriodismoPráctica profesional
🎥

Cámaras & video

EquipoDetalle
Sony A7 IIIFull frame mirrorless · cuerpo principal
Sony A7 IVFull frame mirrorless · cuerpo secundario / B-cam
DJI Ronin SCGimbal de mano para ambas cámaras
Reflector LED 200WIluminación principal
Reflector LED 100WFill / lateral
Reflector LED 50WKicker / backlight
Pantalla verde / chromaTela backdrop en la oficina
Citroën Picasso XsaraTransporte de equipos a locaciones
🥽

VR — headsets

EquipoCantidadUso
Meta Quest 2×5Exhibición de experiencias VR
Meta Quest 3S×2Desarrollo y exhibición premium

Responsables: Fermín Delía (producción) · Julián Toninello (exhibición).

🎙

Audio

Interfaz & grabadores

EquipoDetalle
Focusrite Scarlett i2Interfaz de audio 2 entradas · uso en estudio y set
Zoom H4nGrabador de campo portátil · 4 pistas

Micrófonos

EquipoTipoUso
Samson C01Condensador de diafragma grandeVoz, instrumento en estudio
Rode NTG1Shotgun / cardioideAudio en cámara, entrevistas, set

Instrumentos

InstrumentoModelo / detalle
Piano eléctricoYamaha P90
Guitarras×2
Violín×1
Clarinete×1
Trompeta×1
🏢

Infraestructura

Oficina

EspacioDirección
Sede principalRodríguez Peña 158 · Buenos Aires

Computadoras

EquipoCantidadUso
Dell XPS 15×2Edición, desarrollo, renders livianos
PC Gamer — NVIDIA RTX 1060×2Renders pesados, desarrollo VR, 3D en tiempo real
Sin resultados.